diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b4f50a7ce..40b7338fe 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + = 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" diff --git a/android/app/src/main/java/com/comapeo/WifiModule.kt b/android/app/src/main/java/com/comapeo/WifiModule.kt new file mode 100644 index 000000000..5b3167338 --- /dev/null +++ b/android/app/src/main/java/com/comapeo/WifiModule.kt @@ -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 + // . + 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." + // + 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() + } +} diff --git a/android/app/src/main/java/com/comapeo/WifiPackage.kt b/android/app/src/main/java/com/comapeo/WifiPackage.kt new file mode 100644 index 000000000..1c3c13929 --- /dev/null +++ b/android/app/src/main/java/com/comapeo/WifiPackage.kt @@ -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>> = + mutableListOf() + + override fun createNativeModules(reactContext: ReactApplicationContext): MutableList = + listOf(WifiModule(reactContext)).toMutableList() +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6e5600a0d..fca1491f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,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", @@ -20193,13 +20192,6 @@ "react-native": ">=0.71.0" } }, - "node_modules/react-native-network-info": { - "version": "5.2.1", - "license": "MIT", - "peerDependencies": { - "react-native": ">=0.47" - } - }, "node_modules/react-native-progress": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz", diff --git a/package.json b/package.json index d4715570b..e8f976fb5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/frontend/hooks/useWifiState.ts b/src/frontend/hooks/useWifiState.ts new file mode 100644 index 000000000..d70a5ebd4 --- /dev/null +++ b/src/frontend/hooks/useWifiState.ts @@ -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({ + 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; +}; diff --git a/src/frontend/hooks/useWifiStatus.ts b/src/frontend/hooks/useWifiStatus.ts deleted file mode 100644 index 007b82c16..000000000 --- a/src/frontend/hooks/useWifiStatus.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react'; -import NetInfo from '@react-native-community/netinfo'; -import {NetworkInfo} from 'react-native-network-info'; - -// import bugsnag from "../lib/logger"; - -const useWifiStatus = () => { - const [ssid, setSsid] = React.useState(null); - - React.useEffect(() => { - const handleConnectionChange = async () => { - // NetInfoData does not actually tell us whether wifi is turned on, it just - // tells us what connection the phone is using for data. E.g. it could be - // connected to a wifi network but instead using 4g for data, in which case - // `data.type` will not be wifi. So instead we just use the event listener - // from NetInfo, and when the connection changes we look up the SSID to see - // whether the user is connected to a wifi network. - // TODO: We currently do not know whether wifi is turned off, we only know - // whether the user is connected to a wifi network or not. - let ssid = null; - try { - ssid = await NetworkInfo.getSSID(); - } catch (e) { - // bugsnag.notify(e); - } finally { - // Even if we don't get the SSID, we still want to show that a wifi - // network is connected. - setSsid(ssid); - } - }; - - // Subscribe to NetInfo to know when the user connects/disconnects to wifi - const unsubscribe = NetInfo.addEventListener(handleConnectionChange); - - return () => unsubscribe(); - }, []); - - return {ssid}; -}; - -export default useWifiStatus; diff --git a/src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectDevice.tsx b/src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectDevice.tsx index cfe81b538..42702669a 100644 --- a/src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectDevice.tsx +++ b/src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectDevice.tsx @@ -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({ @@ -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();