diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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.java | 413 |
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; + } + } +} |