Bug 1236130 - Part 1: Use an explicit state machine to control GeckoNetworkManager r=mcomella
authorGrigory Kruglov <gkruglov@mozilla.com>
Thu, 05 May 2016 01:27:35 -0700
changeset 296390 3cac818d4803432669bbe3ed916af12172b56d2e
parent 296389 d7f05bf878f48b70a52c8ee5b355491766e34a52
child 296391 bbda46c289f1c84303db1f8efc89094b4b78ccff
push id76311
push usercbook@mozilla.com
push dateFri, 06 May 2016 12:26:12 +0000
treeherdermozilla-inbound@84a3e5716801 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs1236130
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1236130 - Part 1: Use an explicit state machine to control GeckoNetworkManager r=mcomella - specifying states, events and transition side-effects explicitely makes this code easier to read/maintain - move bunch of network state helper methods into NetworkUtils - ensure to update both network state (up/down/unknown), as well as connection type/subtype every time we need to update network state -- this should fix the buggy behaviour when we'd miss certain network state transitions - tests for the FSM transition matrix, and everything in the NetworkUtils MozReview-Commit-ID: LvrfHyFdkpB
mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
mobile/android/base/java/org/mozilla/gecko/util/NetworkUtils.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -4,203 +4,368 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionSubType;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionType;
+import org.mozilla.gecko.util.NetworkUtils.NetworkStatus;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
 import android.net.DhcpInfo;
-import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.telephony.TelephonyManager;
 import android.text.format.Formatter;
 import android.util.Log;
 
-/*
- * A part of the work of GeckoNetworkManager is to give an general connection
- * type based on the current connection. According to spec of NetworkInformation
- * API version 3, connection types include: bluetooth, cellular, ethernet, none,
- * wifi and other. The objective of providing such general connection is due to
- * some security concerns. In short, we don't want to expose the information of
- * exact network type, especially the cellular network type.
+/**
+ * Provides connection type, subtype and general network status (up/down).
  *
- * Current connection is firstly obtained from Android's ConnectivityManager,
- * which is represented by the constant, and then will be mapped into the
- * connection type defined in Network Information API version 3.
+ * According to spec of Network Information API version 3, connection types include:
+ * bluetooth, cellular, ethernet, none, wifi and other. The objective of providing such general
+ * connection is due to some security concerns. In short, we don't want to expose exact network type,
+ * especially the cellular network type.
+ *
+ * Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
+ *
+ * Logic is implemented as a state machine, so see the transition matrix to figure out what happens when.
  */
-
 public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
-    /*
-     * Keep the below constants in sync with
-     * http://mxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
-     */
-    private static final String LINK_DATA_UP = "up";
-    private static final String LINK_DATA_DOWN = "down";
-    private static final String LINK_DATA_CHANGED = "changed";
-    private static final String LINK_DATA_UNKNOWN = "unknown";
-
-    private static final String LINK_TYPE_UNKONWN = "unknown";
-    private static final String LINK_TYPE_ETHERNET = "ethernet";
-    private static final String LINK_TYPE_WIFI = "wifi";
-    private static final String LINK_TYPE_WIMAX = "wimax";
-    private static final String LINK_TYPE_2G = "2g";
-    private static final String LINK_TYPE_3G = "3g";
-    private static final String LINK_TYPE_4G = "4g";
     private static final String LOGTAG = "GeckoNetworkManager";
 
-    private static GeckoNetworkManager sInstance;
+    private static final String LINK_DATA_CHANGED = "changed";
+
+    private static GeckoNetworkManager instance;
 
     public static void destroy() {
-        if (sInstance != null) {
-            sInstance.onDestroy();
-            sInstance = null;
+        if (instance != null) {
+            instance.onDestroy();
+            instance = null;
         }
     }
 
-    // Connection Type defined in Network Information API v3.
-    private enum ConnectionType {
-        CELLULAR(0),
-        BLUETOOTH(1),
-        ETHERNET(2),
-        WIFI(3),
-        OTHER(4),
-        NONE(5);
+    public enum ManagerState {
+        OffNoListeners,
+        OffWithListeners,
+        OnNoListeners,
+        OnWithListeners
+    }
 
-        public final int value;
+    public enum ManagerEvent {
+        start,
+        stop,
+        enableNotifications,
+        disableNotifications,
+        receivedUpdate
+    }
 
-        private ConnectionType(int value) {
-            this.value = value;
-        }
-    }
+    private ManagerState currentState = ManagerState.OffNoListeners;
+    private ConnectionType currentConnectionType = ConnectionType.NONE;
+    private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
+    private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
 
     private enum InfoType {
         MCC,
         MNC
     }
 
     private GeckoNetworkManager() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
                 "Wifi:Enable",
                 "Wifi:GetIPAddress");
     }
 
     private void onDestroy() {
+        handleManagerEvent(ManagerEvent.stop);
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
                 "Wifi:Enable",
                 "Wifi:GetIPAddress");
     }
 
-    private volatile ConnectionType mConnectionType = ConnectionType.NONE;
-    private final IntentFilter mNetworkFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
-
-    // Whether the manager should be listening to Network Information changes.
-    private boolean mShouldBeListening;
-
-    // Whether the manager should notify Gecko that a change in Network
-    // Information happened.
-    private boolean mShouldNotify;
-
-    // The application context used for registering receivers, so
-    // we can unregister them again later.
-    private volatile Context mApplicationContext;
-    private boolean mIsListening;
+    // The application context used for registering/unregistering receivers and obtaining system services
+    private volatile Context applicationContext;
 
     public static GeckoNetworkManager getInstance() {
-        if (sInstance == null) {
-            sInstance = new GeckoNetworkManager();
+        if (instance == null) {
+            instance = new GeckoNetworkManager();
         }
 
-        return sInstance;
+        return instance;
+    }
+
+    public double[] getCurrentInformation() {
+        final ConnectionType connectionType = currentConnectionType;
+        return new double[] {
+                connectionType.value,
+                connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
+                connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
+        };
     }
 
     @Override
     public void onReceive(Context aContext, Intent aIntent) {
-        updateConnectionType();
-        updateLinkStatus(aContext);
+        handleManagerEvent(ManagerEvent.receivedUpdate);
     }
 
     public void start(final Context context) {
-        // Note that this initialization clause only runs once.
-        mApplicationContext = context.getApplicationContext();
-        if (mConnectionType == ConnectionType.NONE) {
-            mConnectionType = getConnectionType();
+        applicationContext = context.getApplicationContext();
+        handleManagerEvent(ManagerEvent.start);
+    }
+
+    public void stop() {
+        handleManagerEvent(ManagerEvent.stop);
+    }
+
+    public void enableNotifications() {
+        handleManagerEvent(ManagerEvent.enableNotifications);
+    }
+
+    public void disableNotifications() {
+        handleManagerEvent(ManagerEvent.disableNotifications);
+    }
+
+    /**
+     * For a given event, figure out the next state, run any transition by-product actions, and switch
+     * current state to the next state. If event is invalid for the current state, this is a no-op.
+     *
+     * @param event Incoming event
+     * @return Boolean indicating if transition was performed.
+     */
+    private synchronized boolean handleManagerEvent(ManagerEvent event) {
+        final ManagerState nextState = getNextState(currentState, event);
+
+        Log.d(LOGTAG, "Incoming event " + event + " for state " + currentState + " -> " + nextState);
+        if (nextState == null) {
+            Log.w(LOGTAG, "Invalid event " + event + " for state " + currentState);
+            return false;
         }
 
-        mShouldBeListening = true;
-        updateConnectionType();
+        performActionsForStateEvent(currentState, event);
+        currentState = nextState;
+
+        return true;
+    }
 
-        if (mShouldNotify) {
-            startListening();
+    /**
+     * Defines a transition matrix for our state machine. For a given state/event pair, returns nextState.
+     *
+     * @param currentState Current state against which we have an incoming event
+     * @param event Incoming event for which we'd like to figure out the next state
+     * @return State into which we should transition as result of given event
+     */
+    @Nullable
+    public static ManagerState getNextState(@NonNull ManagerState currentState, @NonNull ManagerEvent event) {
+        switch (currentState) {
+            case OffNoListeners:
+                switch (event) {
+                    case start:
+                        return ManagerState.OnNoListeners;
+                    case enableNotifications:
+                        return ManagerState.OffWithListeners;
+                    default:
+                        return null;
+                }
+            case OnNoListeners:
+                switch (event) {
+                    case stop:
+                        return ManagerState.OffNoListeners;
+                    case enableNotifications:
+                        return ManagerState.OnWithListeners;
+                    default:
+                        return null;
+                }
+            case OnWithListeners:
+                switch (event) {
+                    case stop:
+                        return ManagerState.OffWithListeners;
+                    case disableNotifications:
+                        return ManagerState.OnNoListeners;
+                    case receivedUpdate:
+                        return ManagerState.OnWithListeners;
+                    default:
+                        return null;
+                }
+            case OffWithListeners:
+                switch (event) {
+                    case start:
+                        return ManagerState.OnWithListeners;
+                    case disableNotifications:
+                        return ManagerState.OffNoListeners;
+                    default:
+                        return null;
+                }
+            default:
+                throw new IllegalStateException("Unknown current state: " + currentState.name());
         }
     }
 
-    private void startListening() {
-        if (mIsListening) {
-            Log.w(LOGTAG, "Already started!");
-            return;
-        }
-
-        final Context appContext = mApplicationContext;
-        if (appContext == null) {
-            Log.w(LOGTAG, "Not registering receiver: no context!");
-            return;
-        }
-
-        // registerReceiver will return null if registering fails.
-        if (appContext.registerReceiver(this, mNetworkFilter) == null) {
-            Log.e(LOGTAG, "Registering receiver failed");
-        } else {
-            mIsListening = true;
+    /**
+     * For a given state/event combination, run any actions which are by-products of leaving the state
+     * because of a given event. Since this is a deterministic state machine, we can easily do that
+     * without any additional information.
+     *
+     * @param currentState State which we are leaving
+     * @param event Event which is causing us to leave the state
+     */
+    private void performActionsForStateEvent(ManagerState currentState, ManagerEvent event) {
+        // NB: network state might be queried via getCurrentInformation at any time; pre-rewrite behaviour was
+        // that network state was updated whenever enableNotifications was called. To avoid deviating
+        // from previous behaviour and causing weird side-effects, we call updateNetworkStateAndConnectionType
+        // whenever notifications are enabled.
+        switch (currentState) {
+            case OffNoListeners:
+                if (event == ManagerEvent.start) {
+                    updateNetworkStateAndConnectionType();
+                }
+                if (event == ManagerEvent.enableNotifications) {
+                    updateNetworkStateAndConnectionType();
+                }
+                break;
+            case OnNoListeners:
+                if (event == ManagerEvent.enableNotifications) {
+                    updateNetworkStateAndConnectionType();
+                    registerBroadcastReceiver();
+                }
+                break;
+            case OnWithListeners:
+                if (event == ManagerEvent.receivedUpdate) {
+                    updateNetworkStateAndConnectionType();
+                    sendNetworkStateToListeners();
+                }
+                if (event == ManagerEvent.stop) {
+                    unregisterBroadcastReceiver();
+                }
+                if (event == ManagerEvent.disableNotifications) {
+                    unregisterBroadcastReceiver();
+                }
+                break;
+            case OffWithListeners:
+                if (event == ManagerEvent.start) {
+                    registerBroadcastReceiver();
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unknown current state: " + currentState.name());
         }
     }
 
-    public void stop() {
-        mShouldBeListening = false;
+    /**
+     * Update current network state and connection types.
+     */
+    private void updateNetworkStateAndConnectionType() {
+        final ConnectivityManager connectivityManager = (ConnectivityManager) applicationContext.getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        // Type/status getters below all have a defined behaviour for when connectivityManager == null
+        if (connectivityManager == null) {
+            Log.e(LOGTAG, "ConnectivityManager does not exist.");
+        }
+        currentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
+        currentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
+        currentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
+        Log.d(LOGTAG, "New network state: " + currentNetworkStatus + ", " + currentConnectionType + ", " + currentConnectionSubtype);
+    }
+
+    /**
+     * Send current network state and connection type as a GeckoEvent, to whomever is listening.
+     */
+    private void sendNetworkStateToListeners() {
+        if (GeckoThread.isRunning()) {
+            GeckoAppShell.sendEventToGecko(
+                    GeckoEvent.createNetworkEvent(
+                            currentConnectionType.value,
+                            currentConnectionType == ConnectionType.WIFI,
+                            wifiDhcpGatewayAddress(applicationContext),
+                            currentConnectionSubtype.value
+                    )
+            );
+
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(currentNetworkStatus.value));
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
+        }
+    }
 
-        if (mShouldNotify) {
-            stopListening();
+    /**
+     * Stop listening for network state updates.
+     */
+    private void unregisterBroadcastReceiver() {
+        applicationContext.unregisterReceiver(this);
+    }
+
+    /**
+     * Start listening for network state updates.
+     */
+    private void registerBroadcastReceiver() {
+        final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+        if (applicationContext.registerReceiver(this, filter) == null) {
+            Log.e(LOGTAG, "Registering receiver failed");
+        }
+    }
+
+    private static int wifiDhcpGatewayAddress(Context context) {
+        if (context == null) {
+            return 0;
+        }
+
+        try {
+            WifiManager mgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+            DhcpInfo d = mgr.getDhcpInfo();
+            if (d == null) {
+                return 0;
+            }
+
+            return d.gateway;
+
+        } catch (Exception ex) {
+            // getDhcpInfo() is not documented to require any permissions, but on some devices
+            // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
+            // here and returning 0. Not logging because this could be noisy.
+            return 0;
         }
     }
 
     @Override
+    /**
+     * Handles native messages, not part of the state machine flow.
+     */
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
         switch (event) {
-            case "Wifi:Enable": {
-                final WifiManager mgr = (WifiManager) mApplicationContext.getSystemService(Context.WIFI_SERVICE);
+            case "Wifi:Enable":
+                final WifiManager mgr = (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
 
                 if (!mgr.isWifiEnabled()) {
                     mgr.setWifiEnabled(true);
                 } else {
                     // If Wifi is enabled, maybe you need to select a network
                     Intent intent = new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS);
                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                    mApplicationContext.startActivity(intent);
+                    applicationContext.startActivity(intent);
                 }
                 break;
-            }
-            case "Wifi:GetIPAddress": {
+            case "Wifi:GetIPAddress":
                 getWifiIPAddress(callback);
                 break;
-            }
         }
     }
 
     // This function only works for IPv4
     private void getWifiIPAddress(final EventCallback callback) {
-        final WifiManager mgr = (WifiManager) mApplicationContext.getSystemService(Context.WIFI_SERVICE);
+        final WifiManager mgr = (WifiManager) applicationContext.getSystemService(Context.WIFI_SERVICE);
 
         if (mgr == null) {
             callback.sendError("Cannot get WifiManager");
             return;
         }
 
         final WifiInfo info = mgr.getConnectionInfo();
         if (info == null) {
@@ -211,211 +376,16 @@ public class GeckoNetworkManager extends
         int ip = info.getIpAddress();
         if (ip == 0) {
             callback.sendError("Cannot get IPv4 address");
             return;
         }
         callback.sendSuccess(Formatter.formatIpAddress(ip));
     }
 
-    private void stopListening() {
-        if (null == mApplicationContext) {
-            return;
-        }
-
-        if (!mIsListening) {
-            Log.w(LOGTAG, "Already stopped!");
-            return;
-        }
-
-        mApplicationContext.unregisterReceiver(this);
-        mIsListening = false;
-    }
-
-    private int wifiDhcpGatewayAddress() {
-        if (mConnectionType != ConnectionType.WIFI) {
-            return 0;
-        }
-
-        if (null == mApplicationContext) {
-            return 0;
-        }
-
-        try {
-            WifiManager mgr = (WifiManager) mApplicationContext.getSystemService(Context.WIFI_SERVICE);
-            DhcpInfo d = mgr.getDhcpInfo();
-            if (d == null) {
-                return 0;
-            }
-
-            return d.gateway;
-
-        } catch (Exception ex) {
-            // getDhcpInfo() is not documented to require any permissions, but on some devices
-            // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
-            // here and returning 0. Not logging because this could be noisy.
-            return 0;
-        }
-    }
-
-    private void updateConnectionType() {
-        final ConnectionType previousConnectionType = mConnectionType;
-        final ConnectionType newConnectionType = getConnectionType();
-        if (newConnectionType == previousConnectionType) {
-            return;
-        }
-
-        mConnectionType = newConnectionType;
-
-        if (!mShouldNotify) {
-            return;
-        }
-
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkEvent(
-                                       newConnectionType.value,
-                                       newConnectionType == ConnectionType.WIFI,
-                                       wifiDhcpGatewayAddress(),
-                                       getConnectionSubType()));
-    }
-
-    public void updateLinkStatus(Context context) {
-        ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        NetworkInfo info = cm.getActiveNetworkInfo();
-
-        final String status;
-        if (info == null) {
-            status = LINK_DATA_UNKNOWN;
-        } else if (!info.isConnected()) {
-            status = LINK_DATA_DOWN;
-        } else {
-            status = LINK_DATA_UP;
-        }
-
-        if (GeckoThread.isRunning()) {
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(status));
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createNetworkLinkChangeEvent(LINK_DATA_CHANGED));
-        }
-    }
-
-    public double[] getCurrentInformation() {
-        final ConnectionType connectionType = mConnectionType;
-        return new double[] { connectionType.value,
-                              connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
-                              wifiDhcpGatewayAddress() };
-    }
-
-    public void enableNotifications() {
-        // We set mShouldNotify *after* calling updateConnectionType() to make sure we
-        // don't notify an eventual change in mConnectionType.
-        mConnectionType = ConnectionType.NONE; // force a notification
-        updateConnectionType();
-        mShouldNotify = true;
-
-        if (mShouldBeListening) {
-            startListening();
-        }
-    }
-
-    public void disableNotifications() {
-        mShouldNotify = false;
-
-        if (mShouldBeListening) {
-            stopListening();
-        }
-    }
-
-    private NetworkInfo getConnectivityManager() {
-        final Context appContext = mApplicationContext;
-
-        if (null == appContext) {
-          return null;
-        }
-
-        ConnectivityManager cm = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-        if (cm == null) {
-          Log.e(LOGTAG, "Connectivity service does not exist");
-          return null;
-        }
-
-        try {
-            return cm.getActiveNetworkInfo();
-        } catch (SecurityException se) {
-            return null;
-        }
-    }
-
-    private ConnectionType getConnectionType() {
-        NetworkInfo ni = getConnectivityManager();
-
-        if (ni == null) {
-            return ConnectionType.NONE;
-        }
-
-        switch (ni.getType()) {
-            case ConnectivityManager.TYPE_BLUETOOTH:
-                return ConnectionType.BLUETOOTH;
-            case ConnectivityManager.TYPE_ETHERNET:
-                return ConnectionType.ETHERNET;
-            case ConnectivityManager.TYPE_MOBILE:
-            case ConnectivityManager.TYPE_WIMAX:
-                return ConnectionType.CELLULAR;
-            case ConnectivityManager.TYPE_WIFI:
-                return ConnectionType.WIFI;
-            default:
-                Log.w(LOGTAG, "Ignoring the current network type.");
-                return ConnectionType.OTHER;
-        }
-    }
-
-    private String getConnectionSubType() {
-        NetworkInfo ni = getConnectivityManager();
-
-        if (ni == null) {
-            return LINK_TYPE_UNKONWN;
-        }
-
-        switch (ni.getType()) {
-            case ConnectivityManager.TYPE_ETHERNET:
-                return LINK_TYPE_ETHERNET;
-            case ConnectivityManager.TYPE_MOBILE:
-                switch (ni.getSubtype()) {
-                    case TelephonyManager.NETWORK_TYPE_GPRS:
-                    case TelephonyManager.NETWORK_TYPE_EDGE:
-                    case TelephonyManager.NETWORK_TYPE_CDMA:
-                    case TelephonyManager.NETWORK_TYPE_1xRTT:
-                    case TelephonyManager.NETWORK_TYPE_IDEN:
-                        return LINK_TYPE_2G;
-                    case TelephonyManager.NETWORK_TYPE_UMTS:
-                    case TelephonyManager.NETWORK_TYPE_EVDO_0:
-                    case TelephonyManager.NETWORK_TYPE_EVDO_A:
-                    case TelephonyManager.NETWORK_TYPE_HSDPA:
-                    case TelephonyManager.NETWORK_TYPE_HSUPA:
-                    case TelephonyManager.NETWORK_TYPE_HSPA:
-                    case TelephonyManager.NETWORK_TYPE_EVDO_B:
-                    case TelephonyManager.NETWORK_TYPE_EHRPD:
-                    case TelephonyManager.NETWORK_TYPE_HSPAP:
-                        return LINK_TYPE_3G;
-                    case TelephonyManager.NETWORK_TYPE_LTE:
-                        return LINK_TYPE_4G;
-                    default:
-                        return LINK_TYPE_2G;
-                    /* We are not returning LINK_TYPE_UNKONOWN because we treat unknown
-                     * as "no connection" in code elsewhere, which is not the case.
-                     * TODO: Network change notification issue causes a caching problem (Bug 1236130).
-                     */
-                }
-            case ConnectivityManager.TYPE_WIMAX:
-                return LINK_TYPE_WIMAX;
-            case ConnectivityManager.TYPE_WIFI:
-                return LINK_TYPE_WIFI;
-            default:
-                return LINK_TYPE_UNKONWN;
-        }
-    }
-
     private static int getNetworkOperator(InfoType type, Context context) {
         if (null == context) {
             return -1;
         }
 
         TelephonyManager tel = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
         if (tel == null) {
             Log.e(LOGTAG, "Telephony service does not exist");
--- a/mobile/android/base/java/org/mozilla/gecko/util/NetworkUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/NetworkUtils.java
@@ -4,26 +4,71 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.util;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.support.annotation.Nullable;
+import android.support.annotation.NonNull;
+import android.telephony.TelephonyManager;
 
 public class NetworkUtils {
+    /*
+     * Keep the below constants in sync with
+     * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+     */
+    public enum ConnectionSubType {
+        CELL_2G("2g"),
+        CELL_3G("3g"),
+        CELL_4G("4g"),
+        ETHERNET("ethernet"),
+        WIFI("wifi"),
+        WIMAX("wimax"),
+        UNKNOWN("unknown");
 
-    /**
-     * Indicates whether network connectivity exists and it is possible to establish connections and pass data.
+        public final String value;
+        ConnectionSubType(String value) {
+            this.value = value;
+        }
+    }
+
+    /*
+     * Keep the below constants in sync with
+     * http://dxr.mozilla.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
      */
-    public static boolean isConnected(Context context) {
-        final NetworkInfo networkInfo = getActiveNetworkInfo(context);
-        return networkInfo != null &&
-                networkInfo.isConnected();
+    public enum NetworkStatus {
+        UP("up"),
+        DOWN("down"),
+        UNKNOWN("unknown");
+
+        public final String value;
+
+        NetworkStatus(String value) {
+            this.value = value;
+        }
+    }
+
+    // Connection Type defined in Network Information API v3.
+    // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax, mixed, unknown.
+    // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
+    public enum ConnectionType {
+        CELLULAR(0),
+        BLUETOOTH(1),
+        ETHERNET(2),
+        WIFI(3),
+        OTHER(4),
+        NONE(5);
+
+        public final int value;
+
+        ConnectionType(int value) {
+            this.value = value;
+        }
     }
 
     public static boolean isBackgroundDataEnabled(final Context context) {
         final NetworkInfo networkInfo = getActiveNetworkInfo(context);
         return networkInfo != null &&
                 networkInfo.isAvailable() &&
                 networkInfo.isConnectedOrConnecting();
     }
@@ -31,9 +76,118 @@ public class NetworkUtils {
     @Nullable
     private static NetworkInfo getActiveNetworkInfo(final Context context) {
         final ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
         if (connectivity == null) {
             return null;
         }
         return connectivity.getActiveNetworkInfo(); // can return null.
     }
+
+    /**
+     * Indicates whether network connectivity exists and it is possible to establish connections and pass data.
+     */
+    public static boolean isConnected(@NonNull Context context) {
+        return isConnected((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
+    }
+
+    public static boolean isConnected(ConnectivityManager connectivityManager) {
+        if (connectivityManager == null) {
+            return false;
+        }
+
+        final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+        return networkInfo != null && networkInfo.isConnected();
+    }
+
+    /**
+     * For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket.
+     */
+    public static ConnectionSubType getConnectionSubType(ConnectivityManager connectivityManager) {
+        if (connectivityManager == null) {
+            return ConnectionSubType.UNKNOWN;
+        }
+
+        final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+
+        if (networkInfo == null) {
+            return ConnectionSubType.UNKNOWN;
+        }
+
+        switch (networkInfo.getType()) {
+            case ConnectivityManager.TYPE_ETHERNET:
+                return ConnectionSubType.ETHERNET;
+            case ConnectivityManager.TYPE_MOBILE:
+                return getGenericMobileSubtype(networkInfo.getSubtype());
+            case ConnectivityManager.TYPE_WIMAX:
+                return ConnectionSubType.WIMAX;
+            case ConnectivityManager.TYPE_WIFI:
+                return ConnectionSubType.WIFI;
+            default:
+                return ConnectionSubType.UNKNOWN;
+        }
+    }
+
+    public static ConnectionType getConnectionType(ConnectivityManager connectivityManager) {
+        if (connectivityManager == null) {
+            return ConnectionType.NONE;
+        }
+
+        final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+        if (networkInfo == null) {
+            return ConnectionType.NONE;
+        }
+
+        switch (networkInfo.getType()) {
+            case ConnectivityManager.TYPE_BLUETOOTH:
+                return ConnectionType.BLUETOOTH;
+            case ConnectivityManager.TYPE_ETHERNET:
+                return ConnectionType.ETHERNET;
+            // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
+            case ConnectivityManager.TYPE_MOBILE:
+            case ConnectivityManager.TYPE_WIMAX:
+                return ConnectionType.CELLULAR;
+            case ConnectivityManager.TYPE_WIFI:
+                return ConnectionType.WIFI;
+            default:
+                return ConnectionType.OTHER;
+        }
+    }
+
+    public static NetworkStatus getNetworkStatus(ConnectivityManager connectivityManager) {
+        if (connectivityManager == null) {
+            return NetworkStatus.UNKNOWN;
+        }
+
+        if (isConnected(connectivityManager)) {
+            return NetworkStatus.UP;
+        }
+        return NetworkStatus.DOWN;
+    }
+
+    private static ConnectionSubType getGenericMobileSubtype(int subtype) {
+        switch (subtype) {
+            // 2G types: fallthrough 5x
+            case TelephonyManager.NETWORK_TYPE_GPRS:
+            case TelephonyManager.NETWORK_TYPE_EDGE:
+            case TelephonyManager.NETWORK_TYPE_CDMA:
+            case TelephonyManager.NETWORK_TYPE_1xRTT:
+            case TelephonyManager.NETWORK_TYPE_IDEN:
+                return ConnectionSubType.CELL_2G;
+            // 3G types: fallthrough 9x
+            case TelephonyManager.NETWORK_TYPE_UMTS:
+            case TelephonyManager.NETWORK_TYPE_EVDO_0:
+            case TelephonyManager.NETWORK_TYPE_EVDO_A:
+            case TelephonyManager.NETWORK_TYPE_HSDPA:
+            case TelephonyManager.NETWORK_TYPE_HSUPA:
+            case TelephonyManager.NETWORK_TYPE_HSPA:
+            case TelephonyManager.NETWORK_TYPE_EVDO_B:
+            case TelephonyManager.NETWORK_TYPE_EHRPD:
+            case TelephonyManager.NETWORK_TYPE_HSPAP:
+                return ConnectionSubType.CELL_3G;
+            // 4G - just one type!
+            case TelephonyManager.NETWORK_TYPE_LTE:
+                return ConnectionSubType.CELL_4G;
+            default:
+                return ConnectionSubType.UNKNOWN;
+        }
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/GeckoNetworkManagerTest.java
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.GeckoNetworkManager.ManagerState;
+import org.mozilla.gecko.GeckoNetworkManager.ManagerEvent;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class GeckoNetworkManagerTest {
+    /**
+     * Tests the transition matrix.
+     */
+    @Test
+    public void testGetNextState() {
+        ManagerState testingState;
+
+        testingState = ManagerState.OffNoListeners;
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+        assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+        assertEquals(ManagerState.OffWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+
+        testingState = ManagerState.OnNoListeners;
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+        assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+        assertEquals(ManagerState.OffNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+
+        testingState = ManagerState.OnWithListeners;
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+        assertEquals(ManagerState.OffWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+        assertEquals(ManagerState.OnNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+        assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+
+        testingState = ManagerState.OffWithListeners;
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.stop));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.enableNotifications));
+        assertNull(GeckoNetworkManager.getNextState(testingState, ManagerEvent.receivedUpdate));
+        assertEquals(ManagerState.OnWithListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.start));
+        assertEquals(ManagerState.OffNoListeners, GeckoNetworkManager.getNextState(testingState, ManagerEvent.disableNotifications));
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/util/NetworkUtilsTest.java
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.util;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.util.NetworkUtils.*;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.internal.ShadowExtractor;
+import org.robolectric.shadows.ShadowConnectivityManager;
+import org.robolectric.shadows.ShadowNetworkInfo;
+
+import static org.junit.Assert.*;
+
+@RunWith(TestRunner.class)
+public class NetworkUtilsTest {
+    private ConnectivityManager connectivityManager;
+    private ShadowConnectivityManager shadowConnectivityManager;
+
+    @Before
+    public void setUp() {
+        connectivityManager = (ConnectivityManager) RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+        // Not using Shadows.shadowOf(connectivityManager) because of Robolectric bug when using API23+
+        // See: https://github.com/robolectric/robolectric/issues/1862
+        shadowConnectivityManager = (ShadowConnectivityManager) ShadowExtractor.extract(connectivityManager);
+    }
+
+    @Test
+    public void testIsConnected() throws Exception {
+        assertFalse(NetworkUtils.isConnected((ConnectivityManager) null));
+
+        shadowConnectivityManager.setActiveNetworkInfo(null);
+        assertFalse(NetworkUtils.isConnected(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true)
+        );
+        assertTrue(NetworkUtils.isConnected(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.DISCONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, false)
+        );
+        assertFalse(NetworkUtils.isConnected(connectivityManager));
+    }
+
+    @Test
+    public void testGetConnectionSubType() throws Exception {
+        assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(null));
+
+        shadowConnectivityManager.setActiveNetworkInfo(null);
+        assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager));
+
+        // We don't seem to care about figuring out all connection types. So...
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_VPN, 0, true, true)
+        );
+        assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager));
+
+        // But anything below we should recognize.
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_ETHERNET, 0, true, true)
+        );
+        assertEquals(ConnectionSubType.ETHERNET, NetworkUtils.getConnectionSubType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true)
+        );
+        assertEquals(ConnectionSubType.WIFI, NetworkUtils.getConnectionSubType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIMAX, 0, true, true)
+        );
+        assertEquals(ConnectionSubType.WIMAX, NetworkUtils.getConnectionSubType(connectivityManager));
+
+        // Unknown mobile
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, true, true)
+        );
+        assertEquals(ConnectionSubType.UNKNOWN, NetworkUtils.getConnectionSubType(connectivityManager));
+
+        // 2G mobile types
+        int[] cell2gTypes = new int[] {
+                TelephonyManager.NETWORK_TYPE_GPRS,
+                TelephonyManager.NETWORK_TYPE_EDGE,
+                TelephonyManager.NETWORK_TYPE_CDMA,
+                TelephonyManager.NETWORK_TYPE_1xRTT,
+                TelephonyManager.NETWORK_TYPE_IDEN
+        };
+        for (int i = 0; i < cell2gTypes.length; i++) {
+            shadowConnectivityManager.setActiveNetworkInfo(
+                    ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, cell2gTypes[i], true, true)
+            );
+            assertEquals(ConnectionSubType.CELL_2G, NetworkUtils.getConnectionSubType(connectivityManager));
+        }
+
+        // 3G mobile types
+        int[] cell3gTypes = new int[] {
+                TelephonyManager.NETWORK_TYPE_UMTS,
+                TelephonyManager.NETWORK_TYPE_EVDO_0,
+                TelephonyManager.NETWORK_TYPE_EVDO_A,
+                TelephonyManager.NETWORK_TYPE_HSDPA,
+                TelephonyManager.NETWORK_TYPE_HSUPA,
+                TelephonyManager.NETWORK_TYPE_HSPA,
+                TelephonyManager.NETWORK_TYPE_EVDO_B,
+                TelephonyManager.NETWORK_TYPE_EHRPD,
+                TelephonyManager.NETWORK_TYPE_HSPAP
+        };
+        for (int i = 0; i < cell3gTypes.length; i++) {
+            shadowConnectivityManager.setActiveNetworkInfo(
+                    ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, cell3gTypes[i], true, true)
+            );
+            assertEquals(ConnectionSubType.CELL_3G, NetworkUtils.getConnectionSubType(connectivityManager));
+        }
+
+        // 4G mobile type
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, true, true)
+        );
+        assertEquals(ConnectionSubType.CELL_4G, NetworkUtils.getConnectionSubType(connectivityManager));
+    }
+
+    @Test
+    public void testGetConnectionType() {
+        shadowConnectivityManager.setActiveNetworkInfo(null);
+        assertEquals(ConnectionType.NONE, NetworkUtils.getConnectionType(connectivityManager));
+        assertEquals(ConnectionType.NONE, NetworkUtils.getConnectionType(null));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_VPN, 0, true, true)
+        );
+        assertEquals(ConnectionType.OTHER, NetworkUtils.getConnectionType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIFI, 0, true, true)
+        );
+        assertEquals(ConnectionType.WIFI, NetworkUtils.getConnectionType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, 0, true, true)
+        );
+        assertEquals(ConnectionType.CELLULAR, NetworkUtils.getConnectionType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_ETHERNET, 0, true, true)
+        );
+        assertEquals(ConnectionType.ETHERNET, NetworkUtils.getConnectionType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_BLUETOOTH, 0, true, true)
+        );
+        assertEquals(ConnectionType.BLUETOOTH, NetworkUtils.getConnectionType(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_WIMAX, 0, true, true)
+        );
+        assertEquals(ConnectionType.CELLULAR, NetworkUtils.getConnectionType(connectivityManager));
+    }
+
+    @Test
+    public void testGetNetworkStatus() {
+        assertEquals(NetworkStatus.UNKNOWN, NetworkUtils.getNetworkStatus(null));
+
+        shadowConnectivityManager.setActiveNetworkInfo(null);
+        assertEquals(NetworkStatus.DOWN, NetworkUtils.getNetworkStatus(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTING, ConnectivityManager.TYPE_MOBILE, 0, true, false)
+        );
+        assertEquals(NetworkStatus.DOWN, NetworkUtils.getNetworkStatus(connectivityManager));
+
+        shadowConnectivityManager.setActiveNetworkInfo(
+                ShadowNetworkInfo.newInstance(NetworkInfo.DetailedState.CONNECTED, ConnectivityManager.TYPE_MOBILE, 0, true, true)
+        );
+        assertEquals(NetworkStatus.UP, NetworkUtils.getNetworkStatus(connectivityManager));
+    }
+}
\ No newline at end of file