summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
blob: 02ed848f6bcadb5439c7624e4f4089db38636c6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
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;
    }
  }
}