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.java413
1 files changed, 413 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 0000000000..02ed848f6b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,413 @@
+/* -*- 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 android.annotation.SuppressLint;
+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.WifiManager;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.WrapForJNI;
+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;
+
+/**
+ * Provides connection type, subtype and general network status (up/down).
+ *
+ * <p>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.
+ *
+ * <p>Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
+ *
+ * <p>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 {
+ private static final String LOGTAG = "GeckoNetworkManager";
+
+ // If network configuration and/or status changed, we send details of what changed.
+ // If we received a "check out new network state!" intent from the OS but nothing in it looks
+ // different, we ignore it. See Bug 1330836 for some relevant details.
+ 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 mContext;
+
+ 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 mCurrentState = ManagerState.OffNoListeners;
+ private ConnectionType mCurrentConnectionType = ConnectionType.NONE;
+ private ConnectionType mPreviousConnectionType = ConnectionType.NONE;
+ private ConnectionSubType mCurrentConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private ConnectionSubType mPreviousConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private NetworkStatus mCurrentNetworkStatus = NetworkStatus.UNKNOWN;
+ private NetworkStatus mPreviousNetworkStatus = NetworkStatus.UNKNOWN;
+
+ private GeckoNetworkManager() {}
+
+ private void onDestroy() {
+ handleManagerEvent(ManagerEvent.stop);
+ }
+
+ public static GeckoNetworkManager getInstance() {
+ if (instance == null) {
+ instance = new GeckoNetworkManager();
+ }
+
+ return instance;
+ }
+
+ public double[] getCurrentInformation() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ final ConnectionType connectionType = mCurrentConnectionType;
+ return new double[] {
+ connectionType.value,
+ connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
+ connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
+ };
+ }
+
+ @Override
+ public void onReceive(final Context aContext, final Intent aIntent) {
+ handleManagerEvent(ManagerEvent.receivedUpdate);
+ }
+
+ public void start(final Context context) {
+ mContext = 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(final ManagerEvent event) {
+ final ManagerState nextState = getNextState(mCurrentState, event);
+
+ Log.d(LOGTAG, "Incoming event " + event + " for state " + mCurrentState + " -> " + nextState);
+ if (nextState == null) {
+ Log.w(LOGTAG, "Invalid event " + event + " for state " + mCurrentState);
+ 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 (mContext != null) {
+ contextForAction = mContext;
+ } else {
+ contextForAction = GeckoAppShell.getApplicationContext();
+ }
+
+ if (contextForAction == null) {
+ Log.w(
+ LOGTAG,
+ "Context is not available while processing event "
+ + event
+ + " for state "
+ + mCurrentState);
+ return false;
+ }
+
+ performActionsForStateEvent(contextForAction, mCurrentState, event);
+ mCurrentState = 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(
+ final @NonNull ManagerState currentState, final @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.");
+ }
+ mCurrentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
+ mCurrentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
+ mCurrentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
+ Log.d(
+ LOGTAG,
+ "New network state: "
+ + mCurrentNetworkStatus
+ + ", "
+ + mCurrentConnectionType
+ + ", "
+ + mCurrentConnectionSubtype);
+ }
+
+ @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) {
+ final boolean connectionTypeOrSubtypeChanged =
+ mCurrentConnectionType != mPreviousConnectionType
+ || mCurrentConnectionSubtype != mPreviousConnectionSubtype;
+ if (connectionTypeOrSubtypeChanged) {
+ mPreviousConnectionType = mCurrentConnectionType;
+ mPreviousConnectionSubtype = mCurrentConnectionSubtype;
+
+ final boolean isWifi = mCurrentConnectionType == ConnectionType.WIFI;
+ final int gateway = !isWifi ? 0 : wifiDhcpGatewayAddress(context);
+
+ if (GeckoThread.isRunning()) {
+ onConnectionChanged(
+ mCurrentConnectionType.value, mCurrentConnectionSubtype.value, isWifi, gateway);
+ } else {
+ GeckoThread.queueNativeCall(
+ GeckoNetworkManager.class,
+ "onConnectionChanged",
+ mCurrentConnectionType.value,
+ String.class,
+ mCurrentConnectionSubtype.value,
+ isWifi,
+ gateway);
+ }
+ }
+
+ // If neither network status nor network configuration changed, do nothing.
+ if (mCurrentNetworkStatus == mPreviousNetworkStatus && !connectionTypeOrSubtypeChanged) {
+ return;
+ }
+
+ // If network status remains the same, send "changed". Otherwise, send new network status.
+ // See Bug 1330836 for relevant details.
+ final String status;
+ if (mCurrentNetworkStatus == mPreviousNetworkStatus) {
+ status = LINK_DATA_CHANGED;
+ } else {
+ mPreviousNetworkStatus = mCurrentNetworkStatus;
+ status = mCurrentNetworkStatus.value;
+ }
+
+ 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 {
+ final WifiManager mgr =
+ (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ if (mgr == null) {
+ return 0;
+ }
+
+ @SuppressLint("MissingPermission")
+ final DhcpInfo d = mgr.getDhcpInfo();
+ if (d == null) {
+ return 0;
+ }
+
+ return d.gateway;
+
+ } catch (final 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;
+ }
+ }
+}