@pietro-fe01/nativescript-connectivity-manager-plugin 0.1.0 → 0.1.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/connectivity-manager-impl.android.d.ts +36 -0
- package/connectivity-manager-impl.android.js +339 -0
- package/connectivity-manager-impl.common.d.ts +4 -0
- package/connectivity-manager-impl.common.js +7 -0
- package/connectivity-manager-impl.ios.d.ts +40 -0
- package/connectivity-manager-impl.ios.js +313 -0
- package/connectivity-manager-interface.d.ts +15 -0
- package/connectivity-manager-interface.js +1 -0
- package/package.json +26 -17
- package/platforms/android/AndroidManifest.xml +15 -0
- package/platforms/android/README.md +9 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Common } from "./connectivity-manager-impl.common";
|
|
2
|
+
import { ConnectivityManagerInterface } from "./connectivity-manager-interface";
|
|
3
|
+
export declare class ConnectivityManagerImpl extends Common implements ConnectivityManagerInterface {
|
|
4
|
+
private readonly WIFI_SSID_BLACKLIST;
|
|
5
|
+
private readonly wifiManager;
|
|
6
|
+
private readonly cellularManager;
|
|
7
|
+
private readonly locationManager;
|
|
8
|
+
private readonly connectivityManager;
|
|
9
|
+
private forcedNetworkCallback;
|
|
10
|
+
private previousConnectionMetered;
|
|
11
|
+
private previousConnectionWiFi;
|
|
12
|
+
private previousSsid;
|
|
13
|
+
private connectResolve;
|
|
14
|
+
private disconnectResolve;
|
|
15
|
+
private safeUnregisterNetworkCallback;
|
|
16
|
+
private static getCapabilities;
|
|
17
|
+
getSSID(): string;
|
|
18
|
+
getSSIDAsync(): Promise<string | null>;
|
|
19
|
+
getWifiNetworkId(): number;
|
|
20
|
+
isWifiEnabled(): boolean;
|
|
21
|
+
isWifiConnected(): boolean;
|
|
22
|
+
isCellularEnabled(): boolean;
|
|
23
|
+
isCellularConnected(): boolean;
|
|
24
|
+
isGpsEnabled(): boolean;
|
|
25
|
+
isGpsConnected(): boolean;
|
|
26
|
+
scanWifiNetworks(): Promise<string[]>;
|
|
27
|
+
connectToWifiNetwork(ssid: string, password: string, milliseconds: number): Promise<boolean>;
|
|
28
|
+
hasInternet(): boolean;
|
|
29
|
+
private static hasInternet;
|
|
30
|
+
disconnectWifiNetwork(timeoutMs: number): Promise<boolean>;
|
|
31
|
+
private static isPreviousOrStableNetwork;
|
|
32
|
+
private static logConnectivityInfo;
|
|
33
|
+
private static getInterfaceName;
|
|
34
|
+
private waitUntilConnectedToWifi;
|
|
35
|
+
private disconnectWifiAndRemoveNetwork;
|
|
36
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { Common } from "./connectivity-manager-impl.common";
|
|
2
|
+
import * as application from "@nativescript/core/application";
|
|
3
|
+
var Context = android.content.Context;
|
|
4
|
+
var WifiConfiguration = android.net.wifi.WifiConfiguration;
|
|
5
|
+
var ConnectivityManagerService = android.net.ConnectivityManager;
|
|
6
|
+
var WifiManagerService = android.net.wifi.WifiManager;
|
|
7
|
+
var LocationManagerService = android.location.LocationManager;
|
|
8
|
+
var NetworkRequest = android.net.NetworkRequest;
|
|
9
|
+
var NetworkCapabilities = android.net.NetworkCapabilities;
|
|
10
|
+
var WifiNetworkSpecifier = android.net.wifi.WifiNetworkSpecifier;
|
|
11
|
+
let IS_Q_VERSION = false;
|
|
12
|
+
try {
|
|
13
|
+
IS_Q_VERSION =
|
|
14
|
+
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q;
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
IS_Q_VERSION = false;
|
|
18
|
+
}
|
|
19
|
+
export class ConnectivityManagerImpl extends Common {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.WIFI_SSID_BLACKLIST = ["", " "];
|
|
23
|
+
this.wifiManager = application.android.context.getSystemService(Context.WIFI_SERVICE);
|
|
24
|
+
this.cellularManager = application.android.context.getSystemService(Context.TELEPHONY_SERVICE);
|
|
25
|
+
this.locationManager = application.android.context.getSystemService(Context.LOCATION_SERVICE);
|
|
26
|
+
this.connectivityManager = application.android.context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
27
|
+
this.previousConnectionMetered = false;
|
|
28
|
+
this.previousConnectionWiFi = false;
|
|
29
|
+
this.previousSsid = undefined;
|
|
30
|
+
this.connectResolve = null;
|
|
31
|
+
this.disconnectResolve = null;
|
|
32
|
+
}
|
|
33
|
+
safeUnregisterNetworkCallback(callback) {
|
|
34
|
+
if (!callback) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
this.connectivityManager.unregisterNetworkCallback(callback);
|
|
39
|
+
}
|
|
40
|
+
catch (_) {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
static getCapabilities(connectivityManager, network) {
|
|
44
|
+
if (!network) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return connectivityManager.getNetworkCapabilities(network);
|
|
48
|
+
}
|
|
49
|
+
getSSID() {
|
|
50
|
+
return this.wifiManager.getConnectionInfo().getSSID();
|
|
51
|
+
}
|
|
52
|
+
getSSIDAsync() {
|
|
53
|
+
return Promise.resolve(this.getSSID());
|
|
54
|
+
}
|
|
55
|
+
getWifiNetworkId() {
|
|
56
|
+
return this.wifiManager.getConnectionInfo().getNetworkId();
|
|
57
|
+
}
|
|
58
|
+
isWifiEnabled() {
|
|
59
|
+
return this.wifiManager.isWifiEnabled();
|
|
60
|
+
}
|
|
61
|
+
isWifiConnected() {
|
|
62
|
+
if (!this.isWifiEnabled()) {
|
|
63
|
+
throw new Error("Wifi is not enabled.");
|
|
64
|
+
}
|
|
65
|
+
if (IS_Q_VERSION) {
|
|
66
|
+
return (this.connectivityManager.getNetworkCapabilities(this.connectivityManager.getActiveNetwork()) &&
|
|
67
|
+
this.connectivityManager
|
|
68
|
+
.getNetworkCapabilities(this.connectivityManager.getActiveNetwork())
|
|
69
|
+
.hasTransport(NetworkCapabilities.TRANSPORT_WIFI));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return this.connectivityManager
|
|
73
|
+
.getNetworkInfo(ConnectivityManagerService.TYPE_WIFI)
|
|
74
|
+
.isConnected();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
isCellularEnabled() {
|
|
78
|
+
return this.cellularManager.isDataEnabled();
|
|
79
|
+
}
|
|
80
|
+
isCellularConnected() {
|
|
81
|
+
if (!this.isCellularEnabled()) {
|
|
82
|
+
throw new Error("Cellular is not enabled.");
|
|
83
|
+
}
|
|
84
|
+
const capabilities = ConnectivityManagerImpl.getCapabilities(this.connectivityManager, this.connectivityManager.getActiveNetwork());
|
|
85
|
+
return capabilities
|
|
86
|
+
? capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
87
|
+
: false;
|
|
88
|
+
}
|
|
89
|
+
isGpsEnabled() {
|
|
90
|
+
return this.locationManager.isProviderEnabled(LocationManagerService.GPS_PROVIDER);
|
|
91
|
+
}
|
|
92
|
+
isGpsConnected() {
|
|
93
|
+
return this.locationManager.isProviderEnabled(LocationManagerService.NETWORK_PROVIDER);
|
|
94
|
+
}
|
|
95
|
+
async scanWifiNetworks() {
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
application.android.registerBroadcastReceiver(WifiManagerService.SCAN_RESULTS_AVAILABLE_ACTION, () => {
|
|
98
|
+
application.android.unregisterBroadcastReceiver(WifiManagerService.SCAN_RESULTS_AVAILABLE_ACTION);
|
|
99
|
+
let wifiSsidList = [];
|
|
100
|
+
let wifiScanResult = this.wifiManager.getScanResults();
|
|
101
|
+
for (let i = 0; i < wifiScanResult.size(); i++) {
|
|
102
|
+
let wifi = wifiScanResult.get(i);
|
|
103
|
+
if (wifiSsidList.indexOf(wifi.SSID) == -1 &&
|
|
104
|
+
this.WIFI_SSID_BLACKLIST.indexOf(wifi.SSID) == -1) {
|
|
105
|
+
wifiSsidList.push(wifi.SSID);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
resolve(wifiSsidList);
|
|
109
|
+
});
|
|
110
|
+
this.wifiManager.startScan();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async connectToWifiNetwork(ssid, password, milliseconds) {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const safeTimeoutMs = Math.max(0, Number(milliseconds) || 0);
|
|
116
|
+
let completed = false;
|
|
117
|
+
let connectTimeout = null;
|
|
118
|
+
const complete = (result) => {
|
|
119
|
+
if (completed) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
completed = true;
|
|
123
|
+
if (connectTimeout) {
|
|
124
|
+
clearTimeout(connectTimeout);
|
|
125
|
+
connectTimeout = null;
|
|
126
|
+
}
|
|
127
|
+
resolve(result);
|
|
128
|
+
};
|
|
129
|
+
if (!this.isWifiEnabled()) {
|
|
130
|
+
complete(false);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const that = this;
|
|
135
|
+
this.previousConnectionMetered = this.connectivityManager.isActiveNetworkMetered();
|
|
136
|
+
if (this.isWifiConnected()) {
|
|
137
|
+
this.previousConnectionWiFi = true;
|
|
138
|
+
this.previousSsid = this.getSSID();
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this.previousConnectionWiFi = false;
|
|
142
|
+
this.previousSsid = undefined;
|
|
143
|
+
}
|
|
144
|
+
this.connectResolve = complete;
|
|
145
|
+
connectTimeout = setTimeout(() => {
|
|
146
|
+
this.safeUnregisterNetworkCallback(this.forcedNetworkCallback);
|
|
147
|
+
complete(false);
|
|
148
|
+
}, safeTimeoutMs);
|
|
149
|
+
if (IS_Q_VERSION) {
|
|
150
|
+
let wifiNetworkSpecifier = new WifiNetworkSpecifier.Builder()
|
|
151
|
+
.setSsid(ssid)
|
|
152
|
+
.setWpa2Passphrase(password)
|
|
153
|
+
.build();
|
|
154
|
+
let networkRequest = new NetworkRequest.Builder()
|
|
155
|
+
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
|
156
|
+
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
157
|
+
.setNetworkSpecifier(wifiNetworkSpecifier)
|
|
158
|
+
.build();
|
|
159
|
+
this.forcedNetworkCallback = new (ConnectivityManagerService.NetworkCallback.extend({
|
|
160
|
+
onAvailable: function (network) {
|
|
161
|
+
console.log("Connected to the network");
|
|
162
|
+
that.connectivityManager.bindProcessToNetwork(network);
|
|
163
|
+
that.connectResolve(true);
|
|
164
|
+
},
|
|
165
|
+
onUnavailable: function () {
|
|
166
|
+
this.super.onUnavailable();
|
|
167
|
+
that.connectResolve(false);
|
|
168
|
+
that.safeUnregisterNetworkCallback(that.forcedNetworkCallback);
|
|
169
|
+
},
|
|
170
|
+
}))();
|
|
171
|
+
console.log("Connecting to the network...");
|
|
172
|
+
this.connectivityManager.requestNetwork(networkRequest, this.forcedNetworkCallback, milliseconds);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const ssidFormatted = `"${ssid}"`;
|
|
176
|
+
let conf = new WifiConfiguration();
|
|
177
|
+
conf.SSID = ssidFormatted;
|
|
178
|
+
if (password) {
|
|
179
|
+
conf.preSharedKey = '"' + password + '"';
|
|
180
|
+
conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
|
|
184
|
+
}
|
|
185
|
+
let networkRequest = new NetworkRequest.Builder()
|
|
186
|
+
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
|
187
|
+
.build();
|
|
188
|
+
this.forcedNetworkCallback = new (ConnectivityManagerService.NetworkCallback.extend({
|
|
189
|
+
onAvailable: function (network) {
|
|
190
|
+
if (that.getSSID() == ssidFormatted) {
|
|
191
|
+
console.log("Connected to the network");
|
|
192
|
+
that.connectivityManager.bindProcessToNetwork(network);
|
|
193
|
+
that.connectivityManager.unregisterNetworkCallback(this);
|
|
194
|
+
that.connectResolve(true);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
onUnavailable: function () {
|
|
198
|
+
this.super.onUnavailable();
|
|
199
|
+
},
|
|
200
|
+
}))();
|
|
201
|
+
this.connectivityManager.registerNetworkCallback(networkRequest, this.forcedNetworkCallback);
|
|
202
|
+
const list = this.wifiManager.getConfiguredNetworks();
|
|
203
|
+
let netId = -1;
|
|
204
|
+
for (let i = 0; i < list.size(); i++) {
|
|
205
|
+
const network = list.get(i);
|
|
206
|
+
if (network.SSID === ssidFormatted) {
|
|
207
|
+
netId = network.networkId;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (netId == -1) {
|
|
212
|
+
netId = this.wifiManager.addNetwork(conf);
|
|
213
|
+
}
|
|
214
|
+
this.wifiManager.enableNetwork(netId, true);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.log("Something went wrong while connecting to the Wi-Fi. " + error);
|
|
219
|
+
complete(false);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
hasInternet() {
|
|
224
|
+
return ConnectivityManagerImpl.hasInternet(this.connectivityManager, this.connectivityManager.getActiveNetwork());
|
|
225
|
+
}
|
|
226
|
+
static hasInternet(connectivityManager, network) {
|
|
227
|
+
const capabilities = ConnectivityManagerImpl.getCapabilities(connectivityManager, network);
|
|
228
|
+
return capabilities
|
|
229
|
+
? capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
230
|
+
: false;
|
|
231
|
+
}
|
|
232
|
+
async disconnectWifiNetwork(timeoutMs) {
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
let that = this;
|
|
235
|
+
let networkConnectivity = null;
|
|
236
|
+
let completed = false;
|
|
237
|
+
const complete = (result) => {
|
|
238
|
+
if (completed) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
completed = true;
|
|
242
|
+
clearTimeout(promiseTimeout);
|
|
243
|
+
that.safeUnregisterNetworkCallback(networkConnectivity);
|
|
244
|
+
resolve(result);
|
|
245
|
+
};
|
|
246
|
+
this.disconnectResolve = complete;
|
|
247
|
+
let promiseTimeout = setTimeout(() => {
|
|
248
|
+
console.log("Ran into timeout when disconnecting and fetching new connection.");
|
|
249
|
+
complete(false);
|
|
250
|
+
}, timeoutMs);
|
|
251
|
+
if (IS_Q_VERSION) {
|
|
252
|
+
networkConnectivity = new (ConnectivityManagerService.NetworkCallback.extend({
|
|
253
|
+
onAvailable: function (network) {
|
|
254
|
+
ConnectivityManagerImpl.logConnectivityInfo(that.wifiManager, that.connectivityManager, network);
|
|
255
|
+
if (ConnectivityManagerImpl.isPreviousOrStableNetwork(that.wifiManager, that.connectivityManager, network, that.previousConnectionMetered, that.previousConnectionWiFi, that.previousSsid)) {
|
|
256
|
+
that.connectivityManager.bindProcessToNetwork(network);
|
|
257
|
+
that.disconnectResolve(true);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
onLost: function (network) {
|
|
261
|
+
console.log("Disconnected.");
|
|
262
|
+
},
|
|
263
|
+
}))();
|
|
264
|
+
this.connectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), networkConnectivity);
|
|
265
|
+
this.safeUnregisterNetworkCallback(this.forcedNetworkCallback);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.wifiManager.disableNetwork(this.wifiManager.getConnectionInfo().getNetworkId());
|
|
269
|
+
complete(true);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
static isPreviousOrStableNetwork(wifiManager, connectivityManager, network, previousNetworkMetered, previousNetworkWiFi, previousNetworkSsid) {
|
|
274
|
+
const networkCapabilities = ConnectivityManagerImpl.getCapabilities(connectivityManager, network);
|
|
275
|
+
let isWifi = networkCapabilities
|
|
276
|
+
? networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|
277
|
+
: false;
|
|
278
|
+
if (previousNetworkWiFi && isWifi) {
|
|
279
|
+
let ssid = wifiManager.getConnectionInfo().getSSID();
|
|
280
|
+
return ssid == previousNetworkSsid;
|
|
281
|
+
}
|
|
282
|
+
else if (previousNetworkWiFi) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (!networkCapabilities) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
let meteredNow = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
|
|
289
|
+
let hasInternet = ConnectivityManagerImpl.hasInternet(connectivityManager, network);
|
|
290
|
+
return meteredNow == previousNetworkMetered && hasInternet;
|
|
291
|
+
}
|
|
292
|
+
static logConnectivityInfo(wifiManager, connectivityManager, network) {
|
|
293
|
+
let activeNetwork = connectivityManager.getActiveNetwork();
|
|
294
|
+
let defaultActive = connectivityManager.isDefaultNetworkActive();
|
|
295
|
+
let boundNetwork = connectivityManager.getBoundNetworkForProcess();
|
|
296
|
+
let allNetworks = connectivityManager.getAllNetworks();
|
|
297
|
+
let ssid = wifiManager.getConnectionInfo().getSSID();
|
|
298
|
+
console.log("Connected via " +
|
|
299
|
+
ConnectivityManagerImpl.getInterfaceName(connectivityManager, network));
|
|
300
|
+
console.log("New network: " + network);
|
|
301
|
+
console.log("Active network: " + activeNetwork);
|
|
302
|
+
console.log("Active network adapter: " +
|
|
303
|
+
ConnectivityManagerImpl.getInterfaceName(connectivityManager, activeNetwork));
|
|
304
|
+
console.log("Default is active?: " + defaultActive);
|
|
305
|
+
console.log("Current bound network: " + boundNetwork);
|
|
306
|
+
console.log("Current bound network adapter: " +
|
|
307
|
+
ConnectivityManagerImpl.getInterfaceName(connectivityManager, boundNetwork));
|
|
308
|
+
console.log("All networks: " + allNetworks);
|
|
309
|
+
console.log("Current SSID:" + ssid);
|
|
310
|
+
}
|
|
311
|
+
static getInterfaceName(connectivityManager, activeNetwork) {
|
|
312
|
+
let linkProperties = connectivityManager.getLinkProperties(activeNetwork);
|
|
313
|
+
if (linkProperties == null) {
|
|
314
|
+
return "";
|
|
315
|
+
}
|
|
316
|
+
return linkProperties.getInterfaceName();
|
|
317
|
+
}
|
|
318
|
+
async waitUntilConnectedToWifi(milliseconds) {
|
|
319
|
+
return new Promise((resolve) => {
|
|
320
|
+
let intervalTimer = setInterval(() => {
|
|
321
|
+
if (this.isWifiConnected()) {
|
|
322
|
+
clearInterval(intervalTimer);
|
|
323
|
+
clearTimeout(timeout);
|
|
324
|
+
resolve(true);
|
|
325
|
+
}
|
|
326
|
+
}, 500);
|
|
327
|
+
let timeout = setTimeout(() => {
|
|
328
|
+
clearInterval(intervalTimer);
|
|
329
|
+
this.disconnectWifiAndRemoveNetwork();
|
|
330
|
+
throw new Error("Could not connect in the allowed time.");
|
|
331
|
+
}, milliseconds);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
disconnectWifiAndRemoveNetwork() {
|
|
335
|
+
this.wifiManager.removeNetwork(this.getWifiNetworkId());
|
|
336
|
+
this.wifiManager.disconnect();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=connectivity-manager-impl.android.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Common } from "./connectivity-manager-impl.common";
|
|
2
|
+
import { ConnectivityManagerInterface } from "./connectivity-manager-interface";
|
|
3
|
+
export declare class ConnectivityManagerImpl extends Common implements ConnectivityManagerInterface {
|
|
4
|
+
private static readonly POLL_INTERVAL_MS;
|
|
5
|
+
private static readonly SSID_FETCH_TIMEOUT_MS;
|
|
6
|
+
private static readonly HOTSPOT_ERROR_PENDING;
|
|
7
|
+
private static readonly HOTSPOT_ERROR_JOIN_ONCE_NOT_SUPPORTED;
|
|
8
|
+
private static readonly HOTSPOT_ERROR_ALREADY_ASSOCIATED;
|
|
9
|
+
private previousSsid;
|
|
10
|
+
private cachedSsid;
|
|
11
|
+
private ssidRefreshInFlight;
|
|
12
|
+
private getNetworkInfo;
|
|
13
|
+
private normalizeSsid;
|
|
14
|
+
private getSsidFromCaptiveNetwork;
|
|
15
|
+
private getSsidFromCaptiveNetworkSafe;
|
|
16
|
+
private getSsidFromHotspotNetwork;
|
|
17
|
+
private getHotspotErrorCode;
|
|
18
|
+
private isRecoverableHotspotConfigurationError;
|
|
19
|
+
private canRetryWithoutJoinOnce;
|
|
20
|
+
private logHotspotConfigurationError;
|
|
21
|
+
private createHotspotConfiguration;
|
|
22
|
+
private applyHotspotConfiguration;
|
|
23
|
+
private applyHotspotConfigurationWithFallback;
|
|
24
|
+
private fetchCurrentSsid;
|
|
25
|
+
private waitUntil;
|
|
26
|
+
private refreshCachedSsidInBackground;
|
|
27
|
+
getSSID(): string;
|
|
28
|
+
getSSIDAsync(): Promise<string | null>;
|
|
29
|
+
getWifiNetworkId(): number;
|
|
30
|
+
isWifiEnabled(): boolean;
|
|
31
|
+
isWifiConnected(): boolean;
|
|
32
|
+
isCellularEnabled(): boolean;
|
|
33
|
+
isCellularConnected(): boolean;
|
|
34
|
+
isGpsEnabled(): boolean;
|
|
35
|
+
isGpsConnected(): boolean;
|
|
36
|
+
hasInternet(): boolean;
|
|
37
|
+
scanWifiNetworks(): Promise<string[]>;
|
|
38
|
+
connectToWifiNetwork(ssid: string, password: string, milliseconds: number): Promise<boolean>;
|
|
39
|
+
disconnectWifiNetwork(timeoutMs: number): Promise<boolean>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { Common } from "./connectivity-manager-impl.common";
|
|
2
|
+
export class ConnectivityManagerImpl extends Common {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.previousSsid = undefined;
|
|
6
|
+
this.cachedSsid = null;
|
|
7
|
+
this.ssidRefreshInFlight = null;
|
|
8
|
+
}
|
|
9
|
+
getNetworkInfo() {
|
|
10
|
+
try {
|
|
11
|
+
let interfaceNames = CNCopySupportedInterfaces();
|
|
12
|
+
if (!interfaceNames) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
for (let i = 0; i < interfaceNames.count; i++) {
|
|
16
|
+
let info = (CNCopyCurrentNetworkInfo(interfaceNames[i]));
|
|
17
|
+
if (!info) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
let ssid = info.valueForKey(kCNNetworkInfoKeySSID);
|
|
21
|
+
if (!ssid) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
return info;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
catch (_) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
normalizeSsid(rawSsid) {
|
|
33
|
+
if (rawSsid === null || rawSsid === undefined) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const normalized = `${rawSsid}`.trim();
|
|
37
|
+
if (!normalized || normalized === "<unknown ssid>") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (normalized.length >= 2 &&
|
|
41
|
+
normalized.startsWith('"') &&
|
|
42
|
+
normalized.endsWith('"')) {
|
|
43
|
+
return normalized.substring(1, normalized.length - 1);
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
getSsidFromCaptiveNetwork() {
|
|
48
|
+
const info = this.getNetworkInfo();
|
|
49
|
+
const ssid = info ? info.valueForKey(kCNNetworkInfoKeySSID) : null;
|
|
50
|
+
return this.normalizeSsid(ssid);
|
|
51
|
+
}
|
|
52
|
+
getSsidFromCaptiveNetworkSafe() {
|
|
53
|
+
try {
|
|
54
|
+
return this.getSsidFromCaptiveNetwork();
|
|
55
|
+
}
|
|
56
|
+
catch (_) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
getSsidFromHotspotNetwork(network) {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
if (!network) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return this.normalizeSsid((_b = (_a = network.SSID) !== null && _a !== void 0 ? _a : network.ssid) !== null && _b !== void 0 ? _b : null);
|
|
66
|
+
}
|
|
67
|
+
getHotspotErrorCode(err) {
|
|
68
|
+
return err ? Number(err.code) : -1;
|
|
69
|
+
}
|
|
70
|
+
isRecoverableHotspotConfigurationError(err) {
|
|
71
|
+
const code = this.getHotspotErrorCode(err);
|
|
72
|
+
return (code === ConnectivityManagerImpl.HOTSPOT_ERROR_ALREADY_ASSOCIATED ||
|
|
73
|
+
code === ConnectivityManagerImpl.HOTSPOT_ERROR_PENDING);
|
|
74
|
+
}
|
|
75
|
+
canRetryWithoutJoinOnce(err) {
|
|
76
|
+
const code = this.getHotspotErrorCode(err);
|
|
77
|
+
return (code === ConnectivityManagerImpl.HOTSPOT_ERROR_JOIN_ONCE_NOT_SUPPORTED);
|
|
78
|
+
}
|
|
79
|
+
logHotspotConfigurationError(err) {
|
|
80
|
+
if (!err) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log("NEHotspotConfiguration error. domain=" +
|
|
84
|
+
err.domain +
|
|
85
|
+
", code=" +
|
|
86
|
+
err.code +
|
|
87
|
+
", message=" +
|
|
88
|
+
err.localizedDescription);
|
|
89
|
+
}
|
|
90
|
+
createHotspotConfiguration(ssid, password, joinOnce) {
|
|
91
|
+
const hotspot = NEHotspotConfiguration.new();
|
|
92
|
+
const configuration = password
|
|
93
|
+
? hotspot.initWithSSIDPassphraseIsWEP(ssid, password, false)
|
|
94
|
+
: hotspot.initWithSSID(ssid);
|
|
95
|
+
configuration.joinOnce = joinOnce;
|
|
96
|
+
return configuration;
|
|
97
|
+
}
|
|
98
|
+
applyHotspotConfiguration(configuration) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
NEHotspotConfigurationManager.sharedManager.applyConfigurationCompletionHandler(configuration, (err) => {
|
|
101
|
+
resolve(err && err instanceof NSError ? err : null);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async applyHotspotConfigurationWithFallback(ssid, password) {
|
|
106
|
+
const initialConfiguration = this.createHotspotConfiguration(ssid, password, true);
|
|
107
|
+
const initialError = await this.applyHotspotConfiguration(initialConfiguration);
|
|
108
|
+
if (!initialError || this.isRecoverableHotspotConfigurationError(initialError)) {
|
|
109
|
+
return initialError;
|
|
110
|
+
}
|
|
111
|
+
if (!this.canRetryWithoutJoinOnce(initialError)) {
|
|
112
|
+
return initialError;
|
|
113
|
+
}
|
|
114
|
+
const fallbackConfiguration = this.createHotspotConfiguration(ssid, password, false);
|
|
115
|
+
return this.applyHotspotConfiguration(fallbackConfiguration);
|
|
116
|
+
}
|
|
117
|
+
fetchCurrentSsid() {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
let settled = false;
|
|
120
|
+
let timeoutHandle = setTimeout(() => {
|
|
121
|
+
finish(this.getSsidFromCaptiveNetworkSafe());
|
|
122
|
+
}, ConnectivityManagerImpl.SSID_FETCH_TIMEOUT_MS);
|
|
123
|
+
const finish = (ssid) => {
|
|
124
|
+
if (settled) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
settled = true;
|
|
128
|
+
clearTimeout(timeoutHandle);
|
|
129
|
+
resolve(ssid);
|
|
130
|
+
};
|
|
131
|
+
const finishWithFallback = () => {
|
|
132
|
+
finish(this.getSsidFromCaptiveNetworkSafe());
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const hotspotNetwork = NEHotspotNetwork;
|
|
136
|
+
if (hotspotNetwork &&
|
|
137
|
+
typeof hotspotNetwork.fetchCurrentWithCompletionHandler === "function") {
|
|
138
|
+
hotspotNetwork.fetchCurrentWithCompletionHandler((network) => {
|
|
139
|
+
const ssid = this.getSsidFromHotspotNetwork(network);
|
|
140
|
+
if (ssid) {
|
|
141
|
+
finish(ssid);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
finishWithFallback();
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (_) {
|
|
150
|
+
}
|
|
151
|
+
finishWithFallback();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
waitUntil(checkFn, timeoutMs, intervalMs = 200) {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const safeTimeoutMs = Math.max(0, Number(timeoutMs) || 0);
|
|
157
|
+
const safeIntervalMs = Math.max(50, Number(intervalMs) || 200);
|
|
158
|
+
const startedAt = Date.now();
|
|
159
|
+
let done = false;
|
|
160
|
+
const finish = (result) => {
|
|
161
|
+
if (done) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
done = true;
|
|
165
|
+
resolve(result);
|
|
166
|
+
};
|
|
167
|
+
const loop = () => {
|
|
168
|
+
if (done) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
Promise.resolve()
|
|
172
|
+
.then(() => checkFn())
|
|
173
|
+
.then((result) => {
|
|
174
|
+
if (result) {
|
|
175
|
+
finish(true);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (Date.now() - startedAt >= safeTimeoutMs) {
|
|
179
|
+
finish(false);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
setTimeout(loop, safeIntervalMs);
|
|
183
|
+
})
|
|
184
|
+
.catch(() => {
|
|
185
|
+
if (Date.now() - startedAt >= safeTimeoutMs) {
|
|
186
|
+
finish(false);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
setTimeout(loop, safeIntervalMs);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
loop();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
refreshCachedSsidInBackground() {
|
|
196
|
+
if (this.ssidRefreshInFlight) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.ssidRefreshInFlight = this.fetchCurrentSsid().then((ssid) => {
|
|
200
|
+
this.cachedSsid = ssid;
|
|
201
|
+
this.ssidRefreshInFlight = null;
|
|
202
|
+
return ssid;
|
|
203
|
+
}, () => {
|
|
204
|
+
this.ssidRefreshInFlight = null;
|
|
205
|
+
return this.cachedSsid;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
getSSID() {
|
|
209
|
+
this.refreshCachedSsidInBackground();
|
|
210
|
+
if (this.cachedSsid) {
|
|
211
|
+
return this.cachedSsid;
|
|
212
|
+
}
|
|
213
|
+
const fallbackSsid = this.getSsidFromCaptiveNetwork();
|
|
214
|
+
this.cachedSsid = fallbackSsid;
|
|
215
|
+
return fallbackSsid;
|
|
216
|
+
}
|
|
217
|
+
getSSIDAsync() {
|
|
218
|
+
return this.fetchCurrentSsid().then((ssid) => {
|
|
219
|
+
this.cachedSsid = ssid;
|
|
220
|
+
return ssid;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
getWifiNetworkId() {
|
|
224
|
+
const info = this.getNetworkInfo();
|
|
225
|
+
return info ? info.valueForKey(kCNNetworkInfoKeyBSSID) : null;
|
|
226
|
+
}
|
|
227
|
+
isWifiEnabled() {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
isWifiConnected() {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
isCellularEnabled() {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
isCellularConnected() {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
isGpsEnabled() {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
isGpsConnected() {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
hasInternet() {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
scanWifiNetworks() {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
connectToWifiNetwork(ssid, password, milliseconds) {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
const expectedSsid = this.normalizeSsid(ssid);
|
|
254
|
+
if (!expectedSsid) {
|
|
255
|
+
resolve(false);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.applyHotspotConfigurationWithFallback(expectedSsid, password)
|
|
259
|
+
.then(async (err) => {
|
|
260
|
+
if (err && !this.isRecoverableHotspotConfigurationError(err)) {
|
|
261
|
+
this.logHotspotConfigurationError(err);
|
|
262
|
+
resolve(false);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const connected = await this.waitUntil(async () => {
|
|
266
|
+
const currentSsid = await this.getSSIDAsync();
|
|
267
|
+
return currentSsid === expectedSsid;
|
|
268
|
+
}, milliseconds, ConnectivityManagerImpl.POLL_INTERVAL_MS);
|
|
269
|
+
if (connected) {
|
|
270
|
+
this.previousSsid = expectedSsid;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log("Timed out while waiting to connect to SSID '" + expectedSsid + "'.");
|
|
274
|
+
}
|
|
275
|
+
resolve(connected);
|
|
276
|
+
})
|
|
277
|
+
.catch(() => {
|
|
278
|
+
resolve(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
disconnectWifiNetwork(timeoutMs) {
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
const ssidToDisconnect = this.normalizeSsid(this.previousSsid);
|
|
285
|
+
if (!ssidToDisconnect) {
|
|
286
|
+
resolve(true);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
NEHotspotConfigurationManager.sharedManager.removeConfigurationForSSID(ssidToDisconnect);
|
|
290
|
+
this.waitUntil(async () => {
|
|
291
|
+
const currentSsid = await this.getSSIDAsync();
|
|
292
|
+
return currentSsid !== ssidToDisconnect;
|
|
293
|
+
}, timeoutMs, ConnectivityManagerImpl.POLL_INTERVAL_MS).then((disconnected) => {
|
|
294
|
+
if (disconnected && this.previousSsid === ssidToDisconnect) {
|
|
295
|
+
this.previousSsid = undefined;
|
|
296
|
+
this.cachedSsid = null;
|
|
297
|
+
}
|
|
298
|
+
else if (!disconnected) {
|
|
299
|
+
console.log("Timed out while waiting to disconnect from SSID '" +
|
|
300
|
+
ssidToDisconnect +
|
|
301
|
+
"'.");
|
|
302
|
+
}
|
|
303
|
+
resolve(disconnected);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
ConnectivityManagerImpl.POLL_INTERVAL_MS = 200;
|
|
309
|
+
ConnectivityManagerImpl.SSID_FETCH_TIMEOUT_MS = 1500;
|
|
310
|
+
ConnectivityManagerImpl.HOTSPOT_ERROR_PENDING = 9;
|
|
311
|
+
ConnectivityManagerImpl.HOTSPOT_ERROR_JOIN_ONCE_NOT_SUPPORTED = 12;
|
|
312
|
+
ConnectivityManagerImpl.HOTSPOT_ERROR_ALREADY_ASSOCIATED = 13;
|
|
313
|
+
//# sourceMappingURL=connectivity-manager-impl.ios.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ConnectivityManagerInterface {
|
|
2
|
+
getSSID(): string;
|
|
3
|
+
getSSIDAsync(): Promise<string | null>;
|
|
4
|
+
getWifiNetworkId(): number;
|
|
5
|
+
isWifiEnabled(): boolean;
|
|
6
|
+
isWifiConnected(): boolean;
|
|
7
|
+
isCellularEnabled(): boolean;
|
|
8
|
+
isCellularConnected(): boolean;
|
|
9
|
+
isGpsEnabled(): boolean;
|
|
10
|
+
isGpsConnected(): boolean;
|
|
11
|
+
hasInternet(): boolean;
|
|
12
|
+
scanWifiNetworks(): Promise<string[]>;
|
|
13
|
+
connectToWifiNetwork(ssid: string, password: string, milliseconds: number): Promise<boolean>;
|
|
14
|
+
disconnectWifiNetwork(timeoutMs: number): Promise<boolean>;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=connectivity-manager-interface.js.map
|
package/package.json
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@pietro-fe01/nativescript-connectivity-manager-plugin",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "This plugin provides a connectivity manager of Android and iOS.",
|
|
5
|
-
"main": "connectivity-manager-impl",
|
|
6
|
-
"typings": "index.d.ts",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@pietro-fe01/nativescript-connectivity-manager-plugin",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "This plugin provides a connectivity manager of Android and iOS.",
|
|
5
|
+
"main": "connectivity-manager-impl",
|
|
6
|
+
"typings": "index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"*.js",
|
|
9
|
+
"*.d.ts",
|
|
10
|
+
"platforms/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"nativescript": {
|
|
15
|
+
"platforms": {
|
|
16
|
+
"android": "7.0.0",
|
|
17
|
+
"ios": "7.0.0"
|
|
18
|
+
}
|
|
12
19
|
},
|
|
13
20
|
"repository": {
|
|
14
21
|
"type": "git",
|
|
15
22
|
"url": "git+https://github.com/Pietro-fe01/nativescript-connectivity-manager-plugin.git"
|
|
16
23
|
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "node scripts/build-native.js",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"demo:
|
|
22
|
-
"
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "node scripts/build-native.js",
|
|
26
|
+
"compile": "tsc -p tsconfig.json",
|
|
27
|
+
"prepack": "npm run compile",
|
|
28
|
+
"demo:reset": "cd ../demo-angular && npx rimraf -- hooks node_modules platforms package-lock.json",
|
|
29
|
+
"demo:ios": "npm i && cd ../demo-angular && tns run ios",
|
|
30
|
+
"demo:android": "npm i && cd ../demo-angular && tns run android --watch",
|
|
31
|
+
"plugin:prepare": "npm run build && cd ../demo-angular && tns plugin remove nativescript-connectivity-manager-plugin && tns plugin add ../src && cd ../demo-angular && tns plugin remove nativescript-connectivity-manager-plugin && tns plugin add ../src"
|
|
23
32
|
},
|
|
24
33
|
"keywords": [
|
|
25
34
|
"IoT",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
|
3
|
+
|
|
4
|
+
<!--Describe the permissions, features or other configurations required by your plugin for Android.
|
|
5
|
+
To read more about android permissions go to https://developer.android.com/guide/topics/permissions/index.html -->
|
|
6
|
+
<!--EXAMPLE: uses-permission android:name="android.permission.INTERNET"/> -->
|
|
7
|
+
|
|
8
|
+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
|
9
|
+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
|
10
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
|
11
|
+
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
|
12
|
+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
|
13
|
+
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
|
14
|
+
<uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions"/>
|
|
15
|
+
</manifest>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Android permissions and dependencies
|
|
2
|
+
|
|
3
|
+
* (Optional) Use AndroidManifest.xml to describe any permissions, features or other configuration specifics required or used by your plugin for Android.
|
|
4
|
+
NOTE: The NativeScript CLI will not resolve any contradicting or duplicate entries during the merge. After the plugin is installed, you need to manually resolve such issues.
|
|
5
|
+
|
|
6
|
+
* (Optional) Use include.gradle configuration to describe any native dependencies. If there are no such, this file can be removed. For more information, see the [include.gradle Specification](http://docs.nativescript.org/plugins/plugins#includegradle-specification)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
[Read more about nativescript plugins](http://docs.nativescript.org/plugins/plugins)
|