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
--- 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