Bug 1236130 - Part 1: Use an explicit state machine to control GeckoNetworkManager r=mcomella draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Thu, 05 May 2016 01:27:35 -0700
changeset 363658 f7f3ee9f335c09de6280e4d222c5c275bec04210
parent 363468 217746cce0e6db6caf838fb91fbccdce44746cca
child 363659 3dee47d4a04be5c1ca152bd0ef9e688eca1c74e5
push id17276
push usergkruglov@mozilla.com
push dateThu, 05 May 2016 08:28:21 +0000
reviewersmcomella
bugs1236130
milestone49.0a1
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