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

iOS: Add new method to connect to a SSIDPrefix protected only once, Android: Try to address issue #303 #377

Merged
merged 11 commits into from
Aug 8, 2024
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ The following methods work only on iOS

### `connectToProtectedSSIDPrefix(SSIDPrefix: string, password: string, isWep: boolean): Promise`

### `connectToProtectedSSIDPrefixOnce(SSIDPrefix: string, password: string, isWep: boolean, joinOnce: boolean): Promise`

Use this function when you want to match a known SSID prefix, but don’t have a full SSID. If the system finds multiple Wi-Fi networks whose SSID string matches the given prefix, it selects the network with the greatest signal strength.

#### SSIDPrefix
Expand Down
81 changes: 67 additions & 14 deletions android/src/main/java/com/reactlibrary/rnwifi/RNWifiModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@
import java.util.List;

public class RNWifiModule extends ReactContextBaseJavaModule {
private Network joinedNetwork;
private final WifiManager wifi;
private final ReactApplicationContext context;
private static String TAG = "RNWifiModule";

private static final int TIMEOUT_MILLIS = 15000;
private static final int TIMEOUT_REMOVE_MILLIS = 10000;

RNWifiModule(ReactApplicationContext context) {
super(context);
Expand Down Expand Up @@ -138,6 +140,14 @@ public void forceWifiUsageWithOptions(final boolean useWifi, @Nullable final Rea
}

if (useWifi) {
// thanks to https://github.com/flutternetwork/WiFiFlutter/pull/309
// SDK-31 If not previously in a disconnected state, select the joinedNetwork to ensure
// the correct network is used for communications, else fallback to network manager network.
// https://developer.android.com/about/versions/12/behavior-changes-12#concurrent-connections
if (joinedNetwork != null) {
selectNetwork(joinedNetwork, connectivityManager);
promise.resolve(null);
} else {
final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);

Expand All @@ -149,17 +159,14 @@ public void forceWifiUsageWithOptions(final boolean useWifi, @Nullable final Rea
@Override
public void onAvailable(@NonNull final Network network) {
super.onAvailable(network);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectivityManager.bindProcessToNetwork(network);
} else {
ConnectivityManager.setProcessDefaultNetwork(network);
}
selectNetwork(network, connectivityManager);

connectivityManager.unregisterNetworkCallback(this);

promise.resolve(null);
}
});
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectivityManager.bindProcessToNetwork(null);
Expand Down Expand Up @@ -237,7 +244,7 @@ public void connectToProtectedSSID(@NonNull final String SSID, @NonNull final St

this.removeWifiNetwork(SSID, promise, () -> {
connectToWifiDirectly(SSID, password, isHidden, TIMEOUT_MILLIS, promise);
});
}, TIMEOUT_REMOVE_MILLIS);
}


Expand Down Expand Up @@ -265,11 +272,10 @@ public void connectToProtectedWifiSSID(@NonNull ReadableMap options, final Promi
boolean isHidden = options.hasKey("isHidden") && options.getBoolean("isHidden");
int secondsTimeout = options.hasKey("timeout") ? options.getInt("timeout") * 1000 : TIMEOUT_MILLIS;


this.removeWifiNetwork(ssid, promise, () -> {
assert ssid != null;
connectToWifiDirectly(ssid, password, isHidden, secondsTimeout, promise);
});
}, TIMEOUT_REMOVE_MILLIS);
}


Expand Down Expand Up @@ -300,14 +306,28 @@ public void connectionStatus(final Promise promise) {
*/
@ReactMethod
public void disconnect(final Promise promise) {
final int timeout = TIMEOUT_REMOVE_MILLIS;
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
final Runnable timeoutRunnable = () -> {
promise.reject(ConnectErrorCodes.timeoutOccurred.toString(), "Connection timeout");
if (isAndroidTenOrLater()) {
DisconnectCallbackHolder.getInstance().unbindProcessFromNetwork();
DisconnectCallbackHolder.getInstance().disconnect();
}
};

timeoutHandler.postDelayed(timeoutRunnable, timeout);

WifiUtils.withContext(this.context).disconnect(new DisconnectionSuccessListener() {
@Override
public void success() {
timeoutHandler.removeCallbacks(timeoutRunnable);
promise.resolve(true);
}

@Override
public void failed(@NonNull DisconnectionErrorCode errorCode) {
timeoutHandler.removeCallbacks(timeoutRunnable);
switch (errorCode) {
case COULD_NOT_GET_WIFI_MANAGER: {
promise.reject(DisconnectErrorCodes.couldNotGetWifiManager.toString(), "Could not get WifiManager.");
Expand Down Expand Up @@ -391,20 +411,33 @@ public void getIP(final Promise promise) {
*/
@ReactMethod
public void isRemoveWifiNetwork(final String SSID, final Promise promise) {
removeWifiNetwork(SSID, promise, null);
removeWifiNetwork(SSID, promise, null, TIMEOUT_REMOVE_MILLIS);
}

private void removeWifiNetwork(final String SSID, final Promise promise, final Runnable onSuccess) {
private void removeWifiNetwork(final String SSID, final Promise promise, final Runnable onSuccess, final int timeout) {
final boolean locationPermissionGranted = PermissionUtils.isLocationPermissionGranted(context);
if (!locationPermissionGranted) {
promise.reject(IsRemoveWifiNetworkErrorCodes.locationPermissionMissing.toString(), "Location permission (ACCESS_FINE_LOCATION) is not granted");
return;
}

final Handler timeoutHandler = new Handler(Looper.getMainLooper());
final Runnable timeoutRunnable = () -> {
promise.reject(ConnectErrorCodes.timeoutOccurred.toString(), "Connection timeout");
if (isAndroidTenOrLater()) {
DisconnectCallbackHolder.getInstance().unbindProcessFromNetwork();
DisconnectCallbackHolder.getInstance().disconnect();
}
};

timeoutHandler.postDelayed(timeoutRunnable, timeout);

WifiUtils.withContext(this.context)
.remove(SSID, new RemoveSuccessListener() {
@Override
public void success() {
timeoutHandler.removeCallbacks(timeoutRunnable);
joinedNetwork = null;
if (onSuccess != null) {
onSuccess.run();
return;
Expand All @@ -414,6 +447,7 @@ public void success() {

@Override
public void failed(@NonNull RemoveErrorCode errorCode) {
timeoutHandler.removeCallbacks(timeoutRunnable);
switch (errorCode) {
case COULD_NOT_GET_WIFI_MANAGER: {
promise.reject(IsRemoveWifiNetworkErrorCodes.couldNotGetWifiManager.toString(), "Could not get WifiManager.");
Expand Down Expand Up @@ -456,7 +490,7 @@ public void reScanAndLoadWifiList(final Promise promise) {

private void connectToWifiDirectly(@NonNull final String SSID, @NonNull final String password, final boolean isHidden, final int timeout, final Promise promise) {
if (isAndroidTenOrLater()) {
connectAndroidQ(SSID, password, isHidden,timeout, promise);
connectAndroidQ(SSID, password, isHidden, timeout, promise);
} else {
connectPreAndroidQ(SSID, password, promise);
}
Expand Down Expand Up @@ -492,6 +526,14 @@ private void connectPreAndroidQ(@NonNull final String SSID, @NonNull final Strin
promise.resolve("connected");
}

private boolean selectNetwork(final Network network, final ConnectivityManager manager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return manager.bindProcessToNetwork(network);
} else {
return ConnectivityManager.setProcessDefaultNetwork(network);
}
}

@RequiresApi(api = Build.VERSION_CODES.Q)
private void connectAndroidQ(@NonNull final String SSID, @NonNull final String password, final boolean isHidden, final int timeout, final Promise promise) {
WifiNetworkSpecifier.Builder wifiNetworkSpecifier = new WifiNetworkSpecifier.Builder()
Expand All @@ -504,12 +546,19 @@ private void connectAndroidQ(@NonNull final String SSID, @NonNull final String p

NetworkRequest nr = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setNetworkSpecifier(wifiNetworkSpecifier.build())
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
//.addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
.setNetworkSpecifier(wifiNetworkSpecifier.build())
.build();

// cleanup previous connections just in case
DisconnectCallbackHolder.getInstance().disconnect();

joinedNetwork = null;

ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

final Handler timeoutHandler = new Handler(Looper.getMainLooper());
final Runnable timeoutRunnable = () -> {
promise.reject(ConnectErrorCodes.timeoutOccurred.toString(), "Connection timeout");
Expand All @@ -524,8 +573,9 @@ private void connectAndroidQ(@NonNull final String SSID, @NonNull final String p
public void onAvailable(@NonNull Network network) {
super.onAvailable(network);
timeoutHandler.removeCallbacks(timeoutRunnable);
joinedNetwork = network;
DisconnectCallbackHolder.getInstance().bindProcessToNetwork(network);
connectivityManager.setNetworkPreference(ConnectivityManager.DEFAULT_NETWORK_PREFERENCE);
//connectivityManager.setNetworkPreference(ConnectivityManager.DEFAULT_NETWORK_PREFERENCE);
if (!pollForValidSSID(3, SSID)) {
promise.reject(ConnectErrorCodes.android10ImmediatelyDroppedConnection.toString(), "Firmware bugs on OnePlus prevent it from connecting on some firmware versions.");
return;
Expand All @@ -537,12 +587,15 @@ public void onAvailable(@NonNull Network network) {
public void onUnavailable() {
super.onUnavailable();
timeoutHandler.removeCallbacks(timeoutRunnable);
joinedNetwork = null;
promise.reject(ConnectErrorCodes.didNotFindNetwork.toString(), "Network not found or network request cannot be fulfilled.");
}

@Override
public void onLost(@NonNull Network network) {
super.onLost(network);
timeoutHandler.removeCallbacks(timeoutRunnable);
joinedNetwork = null;
DisconnectCallbackHolder.getInstance().unbindProcessFromNetwork();
DisconnectCallbackHolder.getInstance().disconnect();
}
Expand Down
20 changes: 15 additions & 5 deletions ios/RNWifi.m
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,36 @@ + (BOOL)requiresMainQueueSetup
}
}

RCT_EXPORT_METHOD(connectToProtectedSSIDPrefix:(NSString*)ssid
RCT_EXPORT_METHOD(connectToProtectedSSIDPrefix:(NSString*)ssidPrefix
withPassphrase:(NSString*)passphrase
isWEP:(BOOL)isWEP
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {

[self connectToProtectedSSIDPrefixOnce:ssidPrefix withPassphrase:passphrase isWEP:isWEP joinOnce:false resolver:resolve rejecter:reject];
}

RCT_EXPORT_METHOD(connectToProtectedSSIDPrefixOnce:(NSString*)ssidPrefix
withPassphrase:(NSString*)passphrase
isWEP:(BOOL)isWEP
joinOnce:(BOOL)joinOnce
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {

if (@available(iOS 13.0, *)) {
NEHotspotConfiguration* configuration = [[NEHotspotConfiguration alloc] initWithSSIDPrefix:ssid passphrase:passphrase isWEP:isWEP];
configuration.joinOnce = false;
NEHotspotConfiguration* configuration = [[NEHotspotConfiguration alloc] initWithSSIDPrefix:ssidPrefix passphrase:passphrase isWEP:isWEP];
configuration.joinOnce = joinOnce;

[[NEHotspotConfigurationManager sharedManager] applyConfiguration:configuration completionHandler:^(NSError * _Nullable error) {
if (error != nil) {
reject([self parseError:error], @"Error while configuring WiFi", error);
} else {
// Verify SSID connection
[self getWifiSSID:^(NSString* result) {
if ([result hasPrefix:ssid]){
if ([result hasPrefix:ssidPrefix]){
resolve(nil);
} else {
reject([ConnectError code:UnableToConnect], [NSString stringWithFormat:@"%@/%@", @"Unable to connect to Wi-Fi with prefix ", ssid], nil);
reject([ConnectError code:UnableToConnect], [NSString stringWithFormat:@"%@/%@", @"Unable to connect to Wi-Fi with prefix ", ssidPrefix], nil);
}
}];
}
Expand Down
14 changes: 14 additions & 0 deletions lib/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ declare module 'react-native-wifi-reborn' {
password: string,
isWEP: boolean
): Promise<void>;
/**
* Connects to a WiFi network that start with SSIDPrefix. Rejects with an error if it couldn't connect.
*
* @param SSIDPrefix Wifi name prefix.
* @param password `null` for open networks.
* @param isWep Used on iOS. If `true`, the network is WEP Wi-Fi; otherwise it is a WPA or WPA2 personal Wi-Fi network.
* @param joinOnce Used on iOS. If `true`, restricts the lifetime of a configuration to the operating status of the app that created it.
*/
export function connectToProtectedSSIDPrefixOnce(
SSIDPrefix: string,
password: string | null,
isWEP: boolean,
joinOnce: boolean
): Promise<void>;

//#endregion

Expand Down
Loading