@capgo/capacitor-wifi 8.4.0 → 8.5.0-beta.pr20.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/android/src/main/java/ee/forgr/plugin/capacitor_wifi/CapacitorWifiPlugin.java +534 -43
- package/dist/docs.json +19 -3
- package/dist/esm/definitions.d.ts +146 -6
- package/dist/esm/definitions.js +73 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.cjs.js +73 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +73 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorWifiPlugin/CapacitorWifiPlugin.swift +220 -8
- package/package.json +3 -2
|
@@ -12,6 +12,7 @@ import android.net.Network;
|
|
|
12
12
|
import android.net.NetworkCapabilities;
|
|
13
13
|
import android.net.NetworkRequest;
|
|
14
14
|
import android.net.NetworkSpecifier;
|
|
15
|
+
import android.net.TransportInfo;
|
|
15
16
|
import android.net.Uri;
|
|
16
17
|
import android.net.wifi.ScanResult;
|
|
17
18
|
import android.net.wifi.WifiConfiguration;
|
|
@@ -20,6 +21,8 @@ import android.net.wifi.WifiManager;
|
|
|
20
21
|
import android.net.wifi.WifiNetworkSpecifier;
|
|
21
22
|
import android.net.wifi.WifiNetworkSuggestion;
|
|
22
23
|
import android.os.Build;
|
|
24
|
+
import android.os.Handler;
|
|
25
|
+
import android.os.Looper;
|
|
23
26
|
import android.provider.Settings;
|
|
24
27
|
import androidx.activity.result.ActivityResult;
|
|
25
28
|
import androidx.annotation.NonNull;
|
|
@@ -41,6 +44,9 @@ import java.net.NetworkInterface;
|
|
|
41
44
|
import java.util.ArrayList;
|
|
42
45
|
import java.util.Collections;
|
|
43
46
|
import java.util.List;
|
|
47
|
+
import java.util.concurrent.Executor;
|
|
48
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
49
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
44
50
|
|
|
45
51
|
@CapacitorPlugin(
|
|
46
52
|
name = "CapacitorWifi",
|
|
@@ -50,7 +56,7 @@ import java.util.List;
|
|
|
50
56
|
)
|
|
51
57
|
public class CapacitorWifiPlugin extends Plugin {
|
|
52
58
|
|
|
53
|
-
private final String pluginVersion = "8.
|
|
59
|
+
private final String pluginVersion = "8.5.0";
|
|
54
60
|
|
|
55
61
|
private WifiManager wifiManager;
|
|
56
62
|
private ConnectivityManager connectivityManager;
|
|
@@ -63,6 +69,19 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
63
69
|
private static final String EXTRA_WIFI_SSID = "wifi_ssid";
|
|
64
70
|
private static final String EXTRA_WIFI_PRE_SHARED_KEY = "wifi_psk";
|
|
65
71
|
private static final int WIFI_SECURITY_WPA_PSK = 2;
|
|
72
|
+
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 30000;
|
|
73
|
+
private static final long SSID_VERIFY_POLL_MS = 500;
|
|
74
|
+
|
|
75
|
+
private final Object connectLock = new Object();
|
|
76
|
+
private final AtomicInteger connectGeneration = new AtomicInteger(0);
|
|
77
|
+
private final Handler connectHandler = new Handler(Looper.getMainLooper());
|
|
78
|
+
private PluginCall activeConnectCall;
|
|
79
|
+
private AtomicBoolean connectCompleted;
|
|
80
|
+
private Runnable connectTimeoutRunnable;
|
|
81
|
+
private Runnable ssidVerifyRunnable;
|
|
82
|
+
private WifiManager.LocalOnlyConnectionFailureListener localOnlyFailureListener;
|
|
83
|
+
private String pendingConnectSsid;
|
|
84
|
+
private Boolean pendingAutoRouteTraffic;
|
|
66
85
|
|
|
67
86
|
// Lock for thread-safe access to boundNetwork
|
|
68
87
|
|
|
@@ -236,6 +255,23 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
236
255
|
|
|
237
256
|
@PluginMethod
|
|
238
257
|
public void connect(PluginCall call) {
|
|
258
|
+
synchronized (connectLock) {
|
|
259
|
+
if (activeConnectCall != null && connectCompleted != null && !connectCompleted.get()) {
|
|
260
|
+
rejectConnect(
|
|
261
|
+
call,
|
|
262
|
+
"CONNECTION_IN_PROGRESS",
|
|
263
|
+
"Another Wi-Fi connection attempt is already in progress.",
|
|
264
|
+
"request",
|
|
265
|
+
null,
|
|
266
|
+
null
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Reserve the slot early so permission prompts cannot overlap with another connect().
|
|
271
|
+
activeConnectCall = call;
|
|
272
|
+
connectCompleted = new AtomicBoolean(false);
|
|
273
|
+
}
|
|
274
|
+
|
|
239
275
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
240
276
|
connectModern(call);
|
|
241
277
|
} else {
|
|
@@ -247,7 +283,18 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
247
283
|
private void connectModern(PluginCall call) {
|
|
248
284
|
String ssid = call.getString("ssid");
|
|
249
285
|
if (ssid == null || ssid.isEmpty()) {
|
|
250
|
-
call
|
|
286
|
+
rejectConnectAndClear(call, "INVALID_CONFIGURATION", "SSID is required.", "validation");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
Integer timeoutMs = resolveTimeoutMs(call);
|
|
291
|
+
if (timeoutMs == null) {
|
|
292
|
+
rejectConnectAndClear(call, "INVALID_CONFIGURATION", "timeoutMs must be a positive number.", "validation");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!wifiManager.isWifiEnabled()) {
|
|
297
|
+
rejectConnectAndClear(call, "WIFI_DISABLED", "Wi-Fi is disabled.", "request");
|
|
251
298
|
return;
|
|
252
299
|
}
|
|
253
300
|
|
|
@@ -255,7 +302,24 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
255
302
|
Boolean isHiddenSsid = call.getBoolean("isHiddenSsid", false);
|
|
256
303
|
Boolean autoRouteTraffic = call.getBoolean("autoRouteTraffic", false);
|
|
257
304
|
|
|
305
|
+
if (password != null && !password.isEmpty() && !isValidWpaPassphrase(password)) {
|
|
306
|
+
rejectConnectAndClear(call, "INVALID_CONFIGURATION", "Invalid WPA passphrase format.", "validation");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
final int generation = beginConnectAttempt(call, ssid, autoRouteTraffic, timeoutMs);
|
|
311
|
+
|
|
258
312
|
try {
|
|
313
|
+
// Drop any previous local-only callback so a new request can take over.
|
|
314
|
+
if (networkCallback != null) {
|
|
315
|
+
try {
|
|
316
|
+
connectivityManager.unregisterNetworkCallback(networkCallback);
|
|
317
|
+
} catch (Exception ignored) {
|
|
318
|
+
// already unregistered
|
|
319
|
+
}
|
|
320
|
+
networkCallback = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
259
323
|
WifiNetworkSpecifier.Builder specifierBuilder = new WifiNetworkSpecifier.Builder().setSsid(ssid);
|
|
260
324
|
|
|
261
325
|
if (isHiddenSsid != null && isHiddenSsid) {
|
|
@@ -274,17 +338,26 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
274
338
|
|
|
275
339
|
// Only remove internet capability if autoRouteTraffic is false
|
|
276
340
|
// If autoRouteTraffic is true, we want Android to consider this network valid for routing
|
|
277
|
-
|
|
341
|
+
boolean localOnly = autoRouteTraffic == null || !autoRouteTraffic;
|
|
342
|
+
if (localOnly) {
|
|
278
343
|
requestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
|
|
279
344
|
}
|
|
280
345
|
|
|
281
346
|
NetworkRequest request = requestBuilder.build();
|
|
282
347
|
|
|
348
|
+
if (localOnly && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
349
|
+
registerLocalOnlyFailureListener(generation, specifier);
|
|
350
|
+
}
|
|
351
|
+
|
|
283
352
|
networkCallback = new ConnectivityManager.NetworkCallback() {
|
|
284
353
|
@Override
|
|
285
354
|
public void onAvailable(@NonNull Network network) {
|
|
286
355
|
super.onAvailable(network);
|
|
287
356
|
|
|
357
|
+
if (!isCurrentConnectAttempt(generation)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
288
361
|
// Bind process to network if autoRouteTraffic is enabled
|
|
289
362
|
if (autoRouteTraffic != null && autoRouteTraffic) {
|
|
290
363
|
try {
|
|
@@ -306,19 +379,60 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
306
379
|
}
|
|
307
380
|
}
|
|
308
381
|
|
|
309
|
-
|
|
382
|
+
startSsidVerification(generation, network, ssid, true);
|
|
310
383
|
}
|
|
311
384
|
|
|
312
385
|
@Override
|
|
313
386
|
public void onUnavailable() {
|
|
314
387
|
super.onUnavailable();
|
|
315
|
-
|
|
388
|
+
// onUnavailable has no reliable detailed reason.
|
|
389
|
+
// On API 34+ local-only connects, prefer LocalOnlyConnectionFailureListener.
|
|
390
|
+
if (localOnly && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
completeConnectFailure(
|
|
394
|
+
generation,
|
|
395
|
+
"CONNECTION_FAILED",
|
|
396
|
+
"Failed to connect to network.",
|
|
397
|
+
"association",
|
|
398
|
+
null,
|
|
399
|
+
null,
|
|
400
|
+
true
|
|
401
|
+
);
|
|
316
402
|
}
|
|
317
403
|
};
|
|
318
404
|
|
|
319
405
|
connectivityManager.requestNetwork(request, networkCallback);
|
|
406
|
+
} catch (SecurityException e) {
|
|
407
|
+
completeConnectFailure(
|
|
408
|
+
generation,
|
|
409
|
+
"PERMISSION_DENIED",
|
|
410
|
+
"Missing permission to connect to Wi-Fi.",
|
|
411
|
+
"permission",
|
|
412
|
+
null,
|
|
413
|
+
e.getMessage(),
|
|
414
|
+
true
|
|
415
|
+
);
|
|
416
|
+
} catch (IllegalArgumentException e) {
|
|
417
|
+
completeConnectFailure(
|
|
418
|
+
generation,
|
|
419
|
+
"INVALID_CONFIGURATION",
|
|
420
|
+
"Invalid Wi-Fi connection configuration.",
|
|
421
|
+
"validation",
|
|
422
|
+
null,
|
|
423
|
+
e.getMessage(),
|
|
424
|
+
true
|
|
425
|
+
);
|
|
320
426
|
} catch (Exception e) {
|
|
321
|
-
|
|
427
|
+
completeConnectFailure(
|
|
428
|
+
generation,
|
|
429
|
+
"UNKNOWN",
|
|
430
|
+
"Unexpected error while connecting to Wi-Fi.",
|
|
431
|
+
"request",
|
|
432
|
+
null,
|
|
433
|
+
e.getMessage(),
|
|
434
|
+
true
|
|
435
|
+
);
|
|
322
436
|
}
|
|
323
437
|
}
|
|
324
438
|
|
|
@@ -330,7 +444,18 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
330
444
|
|
|
331
445
|
String ssid = call.getString("ssid");
|
|
332
446
|
if (ssid == null || ssid.isEmpty()) {
|
|
333
|
-
call
|
|
447
|
+
rejectConnectAndClear(call, "INVALID_CONFIGURATION", "SSID is required.", "validation");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
Integer timeoutMs = resolveTimeoutMs(call);
|
|
452
|
+
if (timeoutMs == null) {
|
|
453
|
+
rejectConnectAndClear(call, "INVALID_CONFIGURATION", "timeoutMs must be a positive number.", "validation");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!wifiManager.isWifiEnabled()) {
|
|
458
|
+
rejectConnectAndClear(call, "WIFI_DISABLED", "Wi-Fi is disabled.", "request");
|
|
334
459
|
return;
|
|
335
460
|
}
|
|
336
461
|
|
|
@@ -338,6 +463,13 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
338
463
|
Boolean isHiddenSsid = call.getBoolean("isHiddenSsid", false);
|
|
339
464
|
Boolean autoRouteTraffic = call.getBoolean("autoRouteTraffic", false);
|
|
340
465
|
|
|
466
|
+
if (password != null && !password.isEmpty() && !isValidWpaPassphrase(password)) {
|
|
467
|
+
rejectConnectAndClear(call, "INVALID_CONFIGURATION", "Invalid WPA passphrase format.", "validation");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
final int generation = beginConnectAttempt(call, ssid, autoRouteTraffic, timeoutMs);
|
|
472
|
+
|
|
341
473
|
try {
|
|
342
474
|
WifiConfiguration wifiConfig = new WifiConfiguration();
|
|
343
475
|
wifiConfig.SSID = "\"" + ssid + "\"";
|
|
@@ -354,66 +486,93 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
354
486
|
|
|
355
487
|
int netId = wifiManager.addNetwork(wifiConfig);
|
|
356
488
|
if (netId == -1) {
|
|
357
|
-
|
|
489
|
+
completeConnectFailure(
|
|
490
|
+
generation,
|
|
491
|
+
"CONNECTION_FAILED",
|
|
492
|
+
"Failed to add network configuration.",
|
|
493
|
+
"request",
|
|
494
|
+
null,
|
|
495
|
+
null,
|
|
496
|
+
true
|
|
497
|
+
);
|
|
358
498
|
return;
|
|
359
499
|
}
|
|
360
500
|
|
|
361
501
|
boolean disconnectResult = wifiManager.disconnect();
|
|
362
502
|
if (!disconnectResult) {
|
|
363
|
-
|
|
503
|
+
completeConnectFailure(
|
|
504
|
+
generation,
|
|
505
|
+
"CONNECTION_FAILED",
|
|
506
|
+
"Failed to disconnect from current network.",
|
|
507
|
+
"request",
|
|
508
|
+
null,
|
|
509
|
+
null,
|
|
510
|
+
true
|
|
511
|
+
);
|
|
364
512
|
return;
|
|
365
513
|
}
|
|
366
514
|
|
|
367
515
|
boolean enableResult = wifiManager.enableNetwork(netId, true);
|
|
368
516
|
if (!enableResult) {
|
|
369
|
-
|
|
517
|
+
completeConnectFailure(generation, "CONNECTION_FAILED", "Failed to enable network.", "request", null, null, true);
|
|
370
518
|
return;
|
|
371
519
|
}
|
|
372
520
|
|
|
373
521
|
boolean reconnectResult = wifiManager.reconnect();
|
|
374
522
|
if (!reconnectResult) {
|
|
375
|
-
|
|
523
|
+
completeConnectFailure(generation, "CONNECTION_FAILED", "Failed to reconnect.", "request", null, null, true);
|
|
376
524
|
return;
|
|
377
525
|
}
|
|
378
526
|
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
android.util.Log.e("CapacitorWifi", "Failed to bind process to network: " + e.getMessage());
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
500
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
call.resolve();
|
|
527
|
+
// Do not resolve after reconnect(); wait until the connected SSID matches.
|
|
528
|
+
startSsidVerification(generation, null, ssid, false);
|
|
529
|
+
} catch (SecurityException e) {
|
|
530
|
+
completeConnectFailure(
|
|
531
|
+
generation,
|
|
532
|
+
"PERMISSION_DENIED",
|
|
533
|
+
"Missing permission to connect to Wi-Fi.",
|
|
534
|
+
"permission",
|
|
535
|
+
null,
|
|
536
|
+
e.getMessage(),
|
|
537
|
+
true
|
|
538
|
+
);
|
|
539
|
+
} catch (IllegalArgumentException e) {
|
|
540
|
+
completeConnectFailure(
|
|
541
|
+
generation,
|
|
542
|
+
"INVALID_CONFIGURATION",
|
|
543
|
+
"Invalid Wi-Fi connection configuration.",
|
|
544
|
+
"validation",
|
|
545
|
+
null,
|
|
546
|
+
e.getMessage(),
|
|
547
|
+
true
|
|
548
|
+
);
|
|
409
549
|
} catch (Exception e) {
|
|
410
|
-
|
|
550
|
+
completeConnectFailure(
|
|
551
|
+
generation,
|
|
552
|
+
"UNKNOWN",
|
|
553
|
+
"Unexpected error while connecting to Wi-Fi.",
|
|
554
|
+
"request",
|
|
555
|
+
null,
|
|
556
|
+
e.getMessage(),
|
|
557
|
+
true
|
|
558
|
+
);
|
|
411
559
|
}
|
|
412
560
|
}
|
|
413
561
|
|
|
414
562
|
@PluginMethod
|
|
415
563
|
public void disconnect(PluginCall call) {
|
|
416
564
|
try {
|
|
565
|
+
int generation = connectGeneration.get();
|
|
566
|
+
completeConnectFailure(
|
|
567
|
+
generation,
|
|
568
|
+
"CONNECTION_FAILED",
|
|
569
|
+
"Connection canceled because disconnect() was called.",
|
|
570
|
+
"request",
|
|
571
|
+
null,
|
|
572
|
+
null,
|
|
573
|
+
false
|
|
574
|
+
);
|
|
575
|
+
|
|
417
576
|
// Unbind from network if we were bound
|
|
418
577
|
synchronized (boundNetworkLock) {
|
|
419
578
|
if (boundNetwork != null) {
|
|
@@ -431,6 +590,7 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
431
590
|
} else {
|
|
432
591
|
wifiManager.disconnect();
|
|
433
592
|
}
|
|
593
|
+
removeLocalOnlyFailureListener();
|
|
434
594
|
call.resolve();
|
|
435
595
|
} catch (Exception e) {
|
|
436
596
|
call.reject("Failed to disconnect: " + e.getMessage(), e);
|
|
@@ -856,7 +1016,7 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
856
1016
|
if (getPermissionState("location") == PermissionState.GRANTED) {
|
|
857
1017
|
connectLegacy(call);
|
|
858
1018
|
} else {
|
|
859
|
-
call
|
|
1019
|
+
rejectConnectAndClear(call, "PERMISSION_DENIED", "Location permission is required.", "permission");
|
|
860
1020
|
}
|
|
861
1021
|
}
|
|
862
1022
|
|
|
@@ -925,4 +1085,335 @@ public class CapacitorWifiPlugin extends Plugin {
|
|
|
925
1085
|
networkCallback = null;
|
|
926
1086
|
}
|
|
927
1087
|
}
|
|
1088
|
+
|
|
1089
|
+
private void clearConnectReservation() {
|
|
1090
|
+
synchronized (connectLock) {
|
|
1091
|
+
activeConnectCall = null;
|
|
1092
|
+
connectCompleted = null;
|
|
1093
|
+
pendingConnectSsid = null;
|
|
1094
|
+
pendingAutoRouteTraffic = null;
|
|
1095
|
+
}
|
|
1096
|
+
clearConnectTimeout();
|
|
1097
|
+
clearSsidVerification();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private void rejectConnectAndClear(PluginCall call, String code, String message, String stage) {
|
|
1101
|
+
clearConnectReservation();
|
|
1102
|
+
rejectConnect(call, code, message, stage, null, null);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private Integer resolveTimeoutMs(PluginCall call) {
|
|
1106
|
+
if (!call.hasOption("timeoutMs")) {
|
|
1107
|
+
return DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1108
|
+
}
|
|
1109
|
+
Integer timeoutMs = call.getInt("timeoutMs");
|
|
1110
|
+
if (timeoutMs == null || timeoutMs <= 0) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
return timeoutMs;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private boolean isValidWpaPassphrase(String password) {
|
|
1117
|
+
int length = password.length();
|
|
1118
|
+
if (length >= 8 && length <= 63) {
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
if (length == 64) {
|
|
1122
|
+
for (int i = 0; i < length; i++) {
|
|
1123
|
+
char c = password.charAt(i);
|
|
1124
|
+
boolean hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
|
1125
|
+
if (!hex) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private int beginConnectAttempt(PluginCall call, String ssid, Boolean autoRouteTraffic, int timeoutMs) {
|
|
1135
|
+
int generation = connectGeneration.incrementAndGet();
|
|
1136
|
+
synchronized (connectLock) {
|
|
1137
|
+
activeConnectCall = call;
|
|
1138
|
+
if (connectCompleted == null) {
|
|
1139
|
+
connectCompleted = new AtomicBoolean(false);
|
|
1140
|
+
} else {
|
|
1141
|
+
connectCompleted.set(false);
|
|
1142
|
+
}
|
|
1143
|
+
pendingConnectSsid = ssid;
|
|
1144
|
+
pendingAutoRouteTraffic = autoRouteTraffic;
|
|
1145
|
+
}
|
|
1146
|
+
clearConnectTimeout();
|
|
1147
|
+
clearSsidVerification();
|
|
1148
|
+
removeLocalOnlyFailureListener();
|
|
1149
|
+
|
|
1150
|
+
connectTimeoutRunnable = () ->
|
|
1151
|
+
completeConnectFailure(
|
|
1152
|
+
generation,
|
|
1153
|
+
"CONNECTION_TIMEOUT",
|
|
1154
|
+
"Timed out waiting for Wi-Fi connection confirmation.",
|
|
1155
|
+
"verification",
|
|
1156
|
+
null,
|
|
1157
|
+
null,
|
|
1158
|
+
true
|
|
1159
|
+
);
|
|
1160
|
+
connectHandler.postDelayed(connectTimeoutRunnable, timeoutMs);
|
|
1161
|
+
return generation;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private boolean isCurrentConnectAttempt(int generation) {
|
|
1165
|
+
return generation == connectGeneration.get();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private void startSsidVerification(int generation, @Nullable Network network, String expectedSsid, boolean keepCallbackOnSuccess) {
|
|
1169
|
+
clearSsidVerification();
|
|
1170
|
+
ssidVerifyRunnable = new Runnable() {
|
|
1171
|
+
@Override
|
|
1172
|
+
public void run() {
|
|
1173
|
+
if (!isCurrentConnectAttempt(generation)) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
synchronized (connectLock) {
|
|
1177
|
+
if (connectCompleted == null || connectCompleted.get()) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (ssidMatches(expectedSsid, network)) {
|
|
1183
|
+
completeConnectSuccess(generation, network, keepCallbackOnSuccess);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
connectHandler.postDelayed(this, SSID_VERIFY_POLL_MS);
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
connectHandler.post(ssidVerifyRunnable);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
private boolean ssidMatches(String expectedSsid, @Nullable Network network) {
|
|
1194
|
+
String current = getConnectedSsid(network);
|
|
1195
|
+
if (current == null) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
return expectedSsid.equals(current);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
@Nullable
|
|
1202
|
+
private String getConnectedSsid(@Nullable Network network) {
|
|
1203
|
+
try {
|
|
1204
|
+
if (network != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1205
|
+
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
|
|
1206
|
+
if (capabilities != null) {
|
|
1207
|
+
TransportInfo transportInfo = capabilities.getTransportInfo();
|
|
1208
|
+
if (transportInfo instanceof WifiInfo) {
|
|
1209
|
+
return normalizeSsid(((WifiInfo) transportInfo).getSSID());
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
|
|
1215
|
+
if (wifiInfo != null) {
|
|
1216
|
+
return normalizeSsid(wifiInfo.getSSID());
|
|
1217
|
+
}
|
|
1218
|
+
} catch (Exception e) {
|
|
1219
|
+
android.util.Log.e("CapacitorWifi", "Failed to read connected SSID: " + e.getMessage());
|
|
1220
|
+
}
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
@Nullable
|
|
1225
|
+
private String normalizeSsid(@Nullable String ssid) {
|
|
1226
|
+
if (ssid == null || ssid.isEmpty() || "<unknown ssid>".equalsIgnoreCase(ssid)) {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
if (ssid.startsWith("\"") && ssid.endsWith("\"") && ssid.length() >= 2) {
|
|
1230
|
+
return ssid.substring(1, ssid.length() - 1);
|
|
1231
|
+
}
|
|
1232
|
+
return ssid;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
@RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
1236
|
+
private void registerLocalOnlyFailureListener(int generation, NetworkSpecifier specifier) {
|
|
1237
|
+
removeLocalOnlyFailureListener();
|
|
1238
|
+
if (!(specifier instanceof WifiNetworkSpecifier)) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
localOnlyFailureListener = (failedSpecifier, failureReason) -> {
|
|
1242
|
+
if (!isCurrentConnectAttempt(generation)) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
// Only one plugin connect attempt is active at a time; accept the failure for this generation.
|
|
1246
|
+
String code = mapLocalOnlyFailureReason(failureReason);
|
|
1247
|
+
String stage = mapLocalOnlyFailureStage(failureReason);
|
|
1248
|
+
completeConnectFailure(generation, code, "Failed to connect to network.", stage, failureReason, null, true);
|
|
1249
|
+
};
|
|
1250
|
+
Executor executor = (command) -> connectHandler.post(command);
|
|
1251
|
+
wifiManager.addLocalOnlyConnectionFailureListener(executor, localOnlyFailureListener);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
private String mapLocalOnlyFailureReason(int failureReason) {
|
|
1255
|
+
if (failureReason == WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION) {
|
|
1256
|
+
return "AUTHENTICATION_FAILED";
|
|
1257
|
+
}
|
|
1258
|
+
if (failureReason == WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_NOT_FOUND) {
|
|
1259
|
+
return "NETWORK_NOT_FOUND";
|
|
1260
|
+
}
|
|
1261
|
+
if (failureReason == WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_USER_REJECT) {
|
|
1262
|
+
return "USER_DENIED";
|
|
1263
|
+
}
|
|
1264
|
+
// ASSOCIATION, IP_PROVISIONING, NO_RESPONSE, UNKNOWN -> CONNECTION_FAILED
|
|
1265
|
+
return "CONNECTION_FAILED";
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private String mapLocalOnlyFailureStage(int failureReason) {
|
|
1269
|
+
if (failureReason == WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_AUTHENTICATION) {
|
|
1270
|
+
return "authentication";
|
|
1271
|
+
}
|
|
1272
|
+
if (failureReason == WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_IP_PROVISIONING) {
|
|
1273
|
+
return "ipProvisioning";
|
|
1274
|
+
}
|
|
1275
|
+
if (failureReason == WifiManager.STATUS_LOCAL_ONLY_CONNECTION_FAILURE_ASSOCIATION) {
|
|
1276
|
+
return "association";
|
|
1277
|
+
}
|
|
1278
|
+
return "request";
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
private void completeConnectSuccess(int generation, @Nullable Network network, boolean keepCallbackOnSuccess) {
|
|
1282
|
+
if (!isCurrentConnectAttempt(generation)) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
PluginCall call;
|
|
1287
|
+
Boolean autoRouteTraffic;
|
|
1288
|
+
synchronized (connectLock) {
|
|
1289
|
+
if (connectCompleted == null || !connectCompleted.compareAndSet(false, true)) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
call = activeConnectCall;
|
|
1293
|
+
autoRouteTraffic = pendingAutoRouteTraffic;
|
|
1294
|
+
activeConnectCall = null;
|
|
1295
|
+
pendingConnectSsid = null;
|
|
1296
|
+
pendingAutoRouteTraffic = null;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
clearConnectTimeout();
|
|
1300
|
+
clearSsidVerification();
|
|
1301
|
+
removeLocalOnlyFailureListener();
|
|
1302
|
+
|
|
1303
|
+
// Legacy path: bind after SSID confirmation when requested.
|
|
1304
|
+
if (!keepCallbackOnSuccess && autoRouteTraffic != null && autoRouteTraffic) {
|
|
1305
|
+
try {
|
|
1306
|
+
synchronized (boundNetworkLock) {
|
|
1307
|
+
if (boundNetwork != null) {
|
|
1308
|
+
connectivityManager.bindProcessToNetwork(null);
|
|
1309
|
+
}
|
|
1310
|
+
Network activeNetwork = network != null ? network : connectivityManager.getActiveNetwork();
|
|
1311
|
+
if (activeNetwork != null) {
|
|
1312
|
+
boolean bound = connectivityManager.bindProcessToNetwork(activeNetwork);
|
|
1313
|
+
if (bound) {
|
|
1314
|
+
boundNetwork = activeNetwork;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
} catch (Exception e) {
|
|
1319
|
+
android.util.Log.e("CapacitorWifi", "Failed to bind process to network: " + e.getMessage());
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (!keepCallbackOnSuccess && networkCallback != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1324
|
+
// Legacy connect does not keep a NetworkCallback.
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (call != null) {
|
|
1328
|
+
call.resolve();
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
private void completeConnectFailure(
|
|
1333
|
+
int generation,
|
|
1334
|
+
String code,
|
|
1335
|
+
String message,
|
|
1336
|
+
String stage,
|
|
1337
|
+
@Nullable Integer nativeCode,
|
|
1338
|
+
@Nullable String nativeMessage,
|
|
1339
|
+
boolean cleanupNetworkCallback
|
|
1340
|
+
) {
|
|
1341
|
+
if (!isCurrentConnectAttempt(generation)) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
PluginCall call;
|
|
1346
|
+
synchronized (connectLock) {
|
|
1347
|
+
if (connectCompleted == null || !connectCompleted.compareAndSet(false, true)) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
call = activeConnectCall;
|
|
1351
|
+
activeConnectCall = null;
|
|
1352
|
+
pendingConnectSsid = null;
|
|
1353
|
+
pendingAutoRouteTraffic = null;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
clearConnectTimeout();
|
|
1357
|
+
clearSsidVerification();
|
|
1358
|
+
removeLocalOnlyFailureListener();
|
|
1359
|
+
|
|
1360
|
+
if (cleanupNetworkCallback && networkCallback != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1361
|
+
try {
|
|
1362
|
+
connectivityManager.unregisterNetworkCallback(networkCallback);
|
|
1363
|
+
} catch (Exception ignored) {
|
|
1364
|
+
// already unregistered
|
|
1365
|
+
}
|
|
1366
|
+
networkCallback = null;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (call != null) {
|
|
1370
|
+
rejectConnect(call, code, message, stage, nativeCode, nativeMessage);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private void rejectConnect(
|
|
1375
|
+
PluginCall call,
|
|
1376
|
+
String code,
|
|
1377
|
+
String message,
|
|
1378
|
+
String stage,
|
|
1379
|
+
@Nullable Integer nativeCode,
|
|
1380
|
+
@Nullable String nativeMessage
|
|
1381
|
+
) {
|
|
1382
|
+
JSObject data = new JSObject();
|
|
1383
|
+
data.put("platform", "android");
|
|
1384
|
+
data.put("androidApiLevel", Build.VERSION.SDK_INT);
|
|
1385
|
+
data.put("connectionStage", stage);
|
|
1386
|
+
if (nativeCode != null) {
|
|
1387
|
+
data.put("nativeCode", nativeCode);
|
|
1388
|
+
}
|
|
1389
|
+
if (nativeMessage != null) {
|
|
1390
|
+
data.put("nativeMessage", nativeMessage);
|
|
1391
|
+
}
|
|
1392
|
+
call.reject(message, code, data);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private void clearConnectTimeout() {
|
|
1396
|
+
if (connectTimeoutRunnable != null) {
|
|
1397
|
+
connectHandler.removeCallbacks(connectTimeoutRunnable);
|
|
1398
|
+
connectTimeoutRunnable = null;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
private void clearSsidVerification() {
|
|
1403
|
+
if (ssidVerifyRunnable != null) {
|
|
1404
|
+
connectHandler.removeCallbacks(ssidVerifyRunnable);
|
|
1405
|
+
ssidVerifyRunnable = null;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
private void removeLocalOnlyFailureListener() {
|
|
1410
|
+
if (localOnlyFailureListener != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
1411
|
+
try {
|
|
1412
|
+
wifiManager.removeLocalOnlyConnectionFailureListener(localOnlyFailureListener);
|
|
1413
|
+
} catch (Exception ignored) {
|
|
1414
|
+
// already removed
|
|
1415
|
+
}
|
|
1416
|
+
localOnlyFailureListener = null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
928
1419
|
}
|