summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
blob: 0cb56a7d28eb7b1be2b96af1fa8e139bc0ddedff (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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
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());
    }
}