summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java491
1 files changed, 491 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
new file mode 100644
index 000000000..0cb56a7d2
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,491 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * 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.annotation.WrapForJNI;
+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.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;
+
+/**
+ * Provides connection type, subtype and general network status (up/down).
+ *
+ * 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.
+ * This class depends on access to the context, so only use after GeckoAppShell has been initialized.
+ */
+public class GeckoNetworkManager extends BroadcastReceiver implements NativeEventListener {
+ private static final String LOGTAG = "GeckoNetworkManager";
+
+ private static final String LINK_DATA_CHANGED = "changed";
+
+ private static GeckoNetworkManager instance;
+
+ // We hackishly (yet harmlessly, in this case) keep a Context reference passed in via the start method.
+ // See context handling notes in handleManagerEvent, and Bug 1277333.
+ private Context context;
+
+ public static void destroy() {
+ if (instance != null) {
+ instance.onDestroy();
+ instance = null;
+ }
+ }
+
+ public enum ManagerState {
+ OffNoListeners,
+ OffWithListeners,
+ OnNoListeners,
+ OnWithListeners
+ }
+
+ public enum ManagerEvent {
+ start,
+ stop,
+ enableNotifications,
+ disableNotifications,
+ receivedUpdate
+ }
+
+ private ManagerState currentState = ManagerState.OffNoListeners;
+ private ConnectionType currentConnectionType = ConnectionType.NONE;
+ private ConnectionType previousConnectionType = ConnectionType.NONE;
+ private ConnectionSubType currentConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private ConnectionSubType previousConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private NetworkStatus currentNetworkStatus = NetworkStatus.UNKNOWN;
+ private NetworkStatus previousNetworkStatus = NetworkStatus.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");
+ }
+
+ public static GeckoNetworkManager getInstance() {
+ if (instance == null) {
+ instance = new GeckoNetworkManager();
+ }
+
+ return instance;
+ }
+
+ public double[] getCurrentInformation() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ 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) {
+ handleManagerEvent(ManagerEvent.receivedUpdate);
+ }
+
+ public void start(final Context context) {
+ this.context = context;
+ 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;
+ }
+
+ // We're being deliberately careful about handling context here; it's possible that in some
+ // rare cases and possibly related to timing of when this is called (seems to be early in the startup phase),
+ // GeckoAppShell.getApplicationContext() will be null, and .start() wasn't called yet,
+ // so we don't have a local Context reference either. If both of these are true, we have to drop the event.
+ // NB: this is hacky (and these checks attempt to isolate the hackiness), and root cause
+ // seems to be how this class fits into the larger ecosystem and general flow of events.
+ // See Bug 1277333.
+ final Context contextForAction;
+ if (context != null) {
+ contextForAction = context;
+ } else {
+ contextForAction = GeckoAppShell.getApplicationContext();
+ }
+
+ if (contextForAction == null) {
+ Log.w(LOGTAG, "Context is not available while processing event " + event + " for state " + currentState);
+ return false;
+ }
+
+ performActionsForStateEvent(contextForAction, currentState, event);
+ currentState = nextState;
+
+ return true;
+ }
+
+ /**
+ * 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;
+ case receivedUpdate:
+ return ManagerState.OnNoListeners;
+ 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());
+ }
+ }
+
+ /**
+ * 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(final Context context, final ManagerState currentState, final 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(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ }
+ break;
+ case OnNoListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ break;
+ case OnWithListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ case OffWithListeners:
+ if (event == ManagerEvent.start) {
+ registerBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * Update current network state and connection types.
+ */
+ private void updateNetworkStateAndConnectionType(final Context context) {
+ final ConnectivityManager connectivityManager = (ConnectivityManager) context.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);
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onConnectionChanged(int type, String subType,
+ boolean isWifi, int DHCPGateway);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onStatusChanged(String status);
+
+ /**
+ * Send current network state and connection type to whomever is listening.
+ */
+ private void sendNetworkStateToListeners(final Context context) {
+ if (currentConnectionType != previousConnectionType ||
+ currentConnectionSubtype != previousConnectionSubtype) {
+ previousConnectionType = currentConnectionType;
+ previousConnectionSubtype = currentConnectionSubtype;
+
+ final boolean isWifi = currentConnectionType == ConnectionType.WIFI;
+ final int gateway = !isWifi ? 0 :
+ wifiDhcpGatewayAddress(context);
+
+ if (GeckoThread.isRunning()) {
+ onConnectionChanged(currentConnectionType.value,
+ currentConnectionSubtype.value, isWifi, gateway);
+ } else {
+ GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onConnectionChanged",
+ currentConnectionType.value,
+ String.class, currentConnectionSubtype.value,
+ isWifi, gateway);
+ }
+ }
+
+ final String status;
+
+ if (currentNetworkStatus != previousNetworkStatus) {
+ previousNetworkStatus = currentNetworkStatus;
+ status = currentNetworkStatus.value;
+ } else {
+ status = LINK_DATA_CHANGED;
+ }
+
+ if (GeckoThread.isRunning()) {
+ onStatusChanged(status);
+ } else {
+ GeckoThread.queueNativeCall(GeckoNetworkManager.class, "onStatusChanged",
+ String.class, status);
+ }
+ }
+
+ /**
+ * Stop listening for network state updates.
+ */
+ private static void unregisterBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
+ context.unregisterReceiver(receiver);
+ }
+
+ /**
+ * Start listening for network state updates.
+ */
+ private static void registerBroadcastReceiver(final Context context, final BroadcastReceiver receiver) {
+ final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(receiver, filter);
+ }
+
+ private static int wifiDhcpGatewayAddress(final 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) {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ switch (event) {
+ 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);
+ applicationContext.startActivity(intent);
+ }
+ break;
+ case "Wifi:GetIPAddress":
+ getWifiIPAddress(callback);
+ break;
+ }
+ }
+
+ // This function only works for IPv4; not part of the state machine flow.
+ private void getWifiIPAddress(final EventCallback callback) {
+ final WifiManager mgr = (WifiManager) GeckoAppShell.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+
+ if (mgr == null) {
+ callback.sendError("Cannot get WifiManager");
+ return;
+ }
+
+ final WifiInfo info = mgr.getConnectionInfo();
+ if (info == null) {
+ callback.sendError("Cannot get connection info");
+ return;
+ }
+
+ int ip = info.getIpAddress();
+ if (ip == 0) {
+ callback.sendError("Cannot get IPv4 address");
+ return;
+ }
+ callback.sendSuccess(Formatter.formatIpAddress(ip));
+ }
+
+ 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");
+ return -1;
+ }
+
+ String networkOperator = tel.getNetworkOperator();
+ if (networkOperator == null || networkOperator.length() <= 3) {
+ return -1;
+ }
+
+ if (type == InfoType.MNC) {
+ return Integer.parseInt(networkOperator.substring(3));
+ }
+
+ if (type == InfoType.MCC) {
+ return Integer.parseInt(networkOperator.substring(0, 3));
+ }
+
+ return -1;
+ }
+
+ /**
+ * These are called from JavaScript ctypes. Avoid letting ProGuard delete them.
+ *
+ * Note that these methods must only be called after GeckoAppShell has been
+ * initialized: they depend on access to the context.
+ *
+ * Not part of the state machine flow.
+ */
+ @JNITarget
+ public static int getMCC() {
+ return getNetworkOperator(InfoType.MCC, GeckoAppShell.getApplicationContext());
+ }
+
+ @JNITarget
+ public static int getMNC() {
+ return getNetworkOperator(InfoType.MNC, GeckoAppShell.getApplicationContext());
+ }
+}