Skip to content

Commit

Permalink
chore: improve wifi state management
Browse files Browse the repository at this point in the history
We need the device's wifi state at a few points in the app. The way we
previously did this had a few disadvantages:

1. If you were connected to wifi and another network (like cellular),
   we might get the wrong network's IP address. You might also be unable
   to fetch the SSID.

2. It used two different dependencies, because neither does exactly what
   we want.

3. These dependencies rely on deprecated APIs.

This change fixes those by implementing a native module, `WifiModule`,
to address these problems. It also adds an React hook for using it.

I think this is a useful change on its own, but will also help us as we
try to [improve local peer discovery][0] in upcoming work.

[0]: digidem/comapeo-core#474
  • Loading branch information
EvanHahn committed Apr 4, 2024
1 parent 626e116 commit 8a2b459
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 56 deletions.
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<application
android:name=".MainApplication"
Expand Down
5 changes: 1 addition & 4 deletions android/app/src/main/java/com/comapeo/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ class MainApplication : Application(), ReactApplication {
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually
// here,
// for example:
// add(MyReactNativePackage())
add(WifiPackage())
}

override fun getJSMainModuleName(): String = "index"
Expand Down
183 changes: 183 additions & 0 deletions android/app/src/main/java/com/comapeo/WifiModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.comapeo

import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.net.wifi.WifiInfo
import android.os.Build
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule

class WifiModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val context = reactContext.applicationContext
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as? WifiManager

private val networkCallback =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onAvailable(network: Network) {
emit(network)
}

override fun onLost(network: Network) {
emit(null)
}

override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
emit(network, networkCapabilities = networkCapabilities)
}

override fun onLinkPropertiesChanged(
network: Network,
linkProperties: LinkProperties
) {
emit(network, linkProperties = linkProperties)
}
}
} else {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
emit(network)
}

override fun onLost(network: Network) {
emit(null)
}

override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
emit(network, networkCapabilities = networkCapabilities)
}

override fun onLinkPropertiesChanged(
network: Network,
linkProperties: LinkProperties
) {
emit(network, linkProperties = linkProperties)
}
}
}

private var numberOfListeners = 0

override fun getName() = "WifiModule"

@ReactMethod
fun addListener(eventName: String) {
if (numberOfListeners == 0) {
startListening()
}
numberOfListeners++
}

@ReactMethod
fun removeListeners(count: Int) {
numberOfListeners -= count
if (numberOfListeners == 0) {
stopListening()
}
}

private fun startListening() {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager.registerNetworkCallback(request, networkCallback)
}

private fun stopListening() {
connectivityManager.unregisterNetworkCallback(networkCallback)
}

private fun getState(
network: Network?,
networkCapabilities: NetworkCapabilities?,
linkProperties: LinkProperties?
): WritableMap {
val result = Arguments.createMap()
result.putString("ssid", getSsid(network, networkCapabilities))
result.putString("ipAddress", getIpAddress(network, linkProperties))
return result
}

private fun emit(
network: Network?,
networkCapabilities: NetworkCapabilities? = null,
linkProperties: LinkProperties? = null
) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("change", getState(network, networkCapabilities, linkProperties))
}

private fun getSsid(network: Network?, networkCapabilities: NetworkCapabilities?): String? {
val capabilities: NetworkCapabilities? = networkCapabilities
?: try {
connectivityManager.getNetworkCapabilities(network)
} catch (_: SecurityException) {
// Old Android versions can throw errors here. See
// <https://android.googlesource.com/platform/frameworks/base/+/249be21013e389837f5b2beb7d36890b25ecfaaf%5E%21/>.
null
}
val wifiInfoFromCapabilities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
capabilities?.transportInfo as? WifiInfo
} else {
null
}
// We use the deprecated `WifiManager#connectionInfo` here. Not ideal, but it is required
// on older Android versions.
return getSsid(wifiInfoFromCapabilities) ?: getSsid(wifiManager?.connectionInfo)
}

private fun getSsid(wifiInfo: WifiInfo?): String? {
var result = wifiInfo?.ssid ?: return null

// "If the SSID can be decoded as UTF-8, it will be returned surrounded by
// double quotation marks. Otherwise, it is returned as a string of hex
// digits. [...] Prior to `Build.VERSION_CODES.JELLY_BEAN_MR1`, this method
// always returned the SSID with no quotes around it."
// <https://developer.android.com/reference/android/net/wifi/WifiInfo#getSSID()>
if (result.startsWith('"') && result.endsWith('"')) {
result = result.substring(1, result.length - 1)
}

// "The SSID may be `WifiManager#UNKNOWN_SSID`, if there is no network
// currently connected or if the caller has insufficient permissions to
// access the SSID."
if (result == WifiManager.UNKNOWN_SSID) {
return null
}

return result
}

private fun getIpAddress(network: Network?, linkProperties: LinkProperties?): String? {
val properties: LinkProperties? = linkProperties
?: try {
connectivityManager.getLinkProperties(network)
} catch (_: SecurityException) {
// See SecurityException catch above for an explanation.
null
}

return properties
?.linkAddresses
?.firstOrNull { !it.address.isLoopbackAddress }
?.toString()
}
}
16 changes: 16 additions & 0 deletions android/app/src/main/java/com/comapeo/WifiPackage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.comapeo

import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager

class WifiPackage : ReactPackage {
override fun createViewManagers(reactApplicationContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> =
mutableListOf()

override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> =
listOf(WifiModule(reactContext)).toMutableList()
}
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"react-native-indicators": "^0.17.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-mmkv": "^2.12.1",
"react-native-network-info": "^5.2.1",
"react-native-progress": "^5.0.1",
"react-native-reanimated": "~3.6.2",
"react-native-restart": "^0.0.27",
Expand Down
45 changes: 45 additions & 0 deletions src/frontend/hooks/useWifiState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {useEffect, useState} from 'react';
import {NativeModules, NativeEventEmitter} from 'react-native';

const {WifiModule} = NativeModules;

type WifiState = Readonly<{
ssid: null | string;
ipAddress: null | string;
}>;

const isNullOrString = (value: unknown): value is null | string =>
value === null || typeof value === 'string';

const parseState = (value: unknown): WifiState => {
if (
value &&
typeof value === 'object' &&
'ssid' in value &&
'ipAddress' in value &&
isNullOrString(value.ssid) &&
isNullOrString(value.ipAddress)
) {
return {ssid: value.ssid, ipAddress: value.ipAddress};
}
throw new Error('Invalid wifi state from native module');
};

export const useWifiState = () => {
const [result, setResult] = useState<WifiState>({
ssid: null,
ipAddress: null,
});

useEffect(() => {
const emitter = new NativeEventEmitter(WifiModule);
const listener = emitter.addListener('change', (rawState: unknown) => {
setResult(parseState(rawState));
});
return () => {
listener.remove();
};
}, []);

return result;
};
41 changes: 0 additions & 41 deletions src/frontend/hooks/useWifiStatus.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {ScrollView, StyleSheet, View} from 'react-native';
import WifiIcon from '../../../../images/WifiIcon.svg';
import {Text} from '../../../../sharedComponents/Text';
import {DeviceCard} from '../../../../sharedComponents/DeviceCard';
import {useLocalDiscoveryState} from '../../../../hooks/useLocalDiscoveryState';
import {useWifiState} from '../../../../hooks/useWifiState';
import {useLocalPeers} from '../../../../hooks/useLocalPeers';

const m = defineMessages({
Expand All @@ -29,7 +29,7 @@ const m = defineMessages({
export const SelectDevice: NativeNavigationComponent<'SelectDevice'> = ({
navigation,
}) => {
const ssid = useLocalDiscoveryState(state => state.ssid);
const {ssid} = useWifiState();
const {formatMessage: t} = useIntl();

const devices = useLocalPeers();
Expand Down

0 comments on commit 8a2b459

Please sign in to comment.