Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: improve wifi state management #220

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the relevant React Native docs.

}

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()
}
}
Comment on lines +80 to +94
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the relevant React Native docs for sending events from a native module.


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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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 = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a request for the current state when the hook is first initialized - if there is no change event then the state will remain null.

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