react-native-zeroconf 0.16.1 → 0.17.0
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/NOTICE +0 -3
- package/README.md +1 -1
- package/android/build.gradle +0 -3
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +1 -1
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +12 -2
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +129 -4
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +72 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +633 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +331 -34
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceTypesQuery.java +296 -0
- package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +1 -1
- package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +59 -22
- package/dist/index.js +234 -48
- package/index.d.ts +101 -6
- package/ios/RNZeroconf/RNZeroconf.m +285 -22
- package/package.json +28 -4
- package/src/index.js +214 -44
- package/windows/ExperimentalFeatures.props +33 -0
- package/windows/NuGet.config +11 -0
- package/windows/RNZeroconf/RNZeroconf.cpp +227 -0
- package/windows/RNZeroconf/RNZeroconf.def +3 -0
- package/windows/RNZeroconf/RNZeroconf.h +70 -0
- package/windows/RNZeroconf/RNZeroconf.rc +0 -0
- package/windows/RNZeroconf/RNZeroconf.vcxproj +150 -0
- package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +50 -0
- package/windows/RNZeroconf/ReactPackageProvider.cpp +20 -0
- package/windows/RNZeroconf/ReactPackageProvider.h +24 -0
- package/windows/RNZeroconf/ReactPackageProvider.idl +9 -0
- package/windows/RNZeroconf/ZeroconfCore.cpp +1042 -0
- package/windows/RNZeroconf/ZeroconfCore.h +165 -0
- package/windows/RNZeroconf/pch.cpp +1 -0
- package/windows/RNZeroconf/pch.h +30 -0
- package/windows/RNZeroconf/resource.h +5 -0
- package/windows/RNZeroconf/targetver.h +8 -0
- package/windows/RNZeroconf.sln +43 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +0 -305
- package/android/src/main/java/com/github/druk/rx2dnssd/BonjourService.java +0 -400
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2BrowseListener.java +0 -55
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2Dnssd.java +0 -127
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdCommon.java +0 -280
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdEmbedded.java +0 -31
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2QueryListener.java +0 -70
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2RegisterListener.java +0 -47
- package/android/src/main/java/com/github/druk/rx2dnssd/Rx2ResolveListener.java +0 -51
package/NOTICE
CHANGED
|
@@ -35,9 +35,6 @@ android/src/main/jni/*
|
|
|
35
35
|
android/src/main/java/com/github/druk/dnssd/*
|
|
36
36
|
- Java DNS-SD API classes
|
|
37
37
|
|
|
38
|
-
android/src/main/java/com/github/druk/rx2dnssd/*
|
|
39
|
-
- RxJava2 reactive wrappers for DNS-SD
|
|
40
|
-
|
|
41
38
|
================================================================================
|
|
42
39
|
|
|
43
40
|
mDNSResponder
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src=".github/banner.png" alt="react-native-zeroconf: Bonjour / mDNS discovery and publishing for React Native, on iOS, Android, macOS, tvOS and Expo" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
Find the services other devices advertise on the local network (printers, speakers, cameras, your own servers) and advertise your own, on iOS, Android, macOS and
|
|
5
|
+
Find the services other devices advertise on the local network (printers, speakers, cameras, your own servers) and advertise your own, on iOS, Android, macOS, tvOS and Windows.
|
|
6
6
|
|
|
7
7
|
**[Read the documentation](https://zeroconf.balthazar.dev)**
|
|
8
8
|
|
package/android/build.gradle
CHANGED
|
@@ -42,7 +42,4 @@ repositories {
|
|
|
42
42
|
|
|
43
43
|
dependencies {
|
|
44
44
|
implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
|
45
|
-
// RxJava2 for Rx2Dnssd (bundled from Discord's RxDNSSD fork)
|
|
46
|
-
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
|
|
47
|
-
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
|
48
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package com.balthazargronon.RCTZeroconf;
|
|
2
2
|
|
|
3
3
|
import com.balthazargronon.RCTZeroconf.nsd.NsdServiceImpl;
|
|
4
|
-
import com.balthazargronon.RCTZeroconf.
|
|
4
|
+
import com.balthazargronon.RCTZeroconf.dnssd.DnssdImpl;
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
6
|
import com.facebook.react.bridge.ReactContext;
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ public interface Zeroconf {
|
|
|
10
10
|
/**
|
|
11
11
|
* @param scanId id of the JS instance starting the scan, several scans can run at once
|
|
12
12
|
*/
|
|
13
|
-
void scan(String scanId, String type, String protocol, String domain);
|
|
13
|
+
void scan(String scanId, String type, String protocol, String domain, ZeroconfOptions options);
|
|
14
14
|
|
|
15
15
|
void stop(String scanId);
|
|
16
16
|
|
|
@@ -25,7 +25,17 @@ public interface Zeroconf {
|
|
|
25
25
|
* @param txt TXT records as ordered [key, value] pairs
|
|
26
26
|
* @param promise resolved with the service once registered
|
|
27
27
|
*/
|
|
28
|
-
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, Promise promise);
|
|
28
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, ZeroconfOptions options, Promise promise);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Replaces the TXT record of a published service
|
|
32
|
+
*/
|
|
33
|
+
void updateService(String serviceName, ReadableArray txt, Promise promise);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves one service by name, without scanning
|
|
37
|
+
*/
|
|
38
|
+
void resolveService(String name, String type, String protocol, String domain, ZeroconfOptions options, Promise promise);
|
|
29
39
|
|
|
30
40
|
/**
|
|
31
41
|
* Unregister every service published through this implementation
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
package com.balthazargronon.RCTZeroconf;
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.pm.PackageManager;
|
|
6
|
+
import android.net.ConnectivityManager;
|
|
7
|
+
import android.net.LinkProperties;
|
|
8
|
+
import android.net.Network;
|
|
9
|
+
import android.os.Build;
|
|
4
10
|
import android.util.Log;
|
|
5
11
|
|
|
6
12
|
import com.facebook.react.bridge.Promise;
|
|
@@ -9,6 +15,7 @@ import com.facebook.react.bridge.ReactContext;
|
|
|
9
15
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
10
16
|
import com.facebook.react.bridge.ReactMethod;
|
|
11
17
|
import com.facebook.react.bridge.ReadableArray;
|
|
18
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
12
19
|
import com.facebook.react.bridge.WritableMap;
|
|
13
20
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
14
21
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
@@ -46,6 +53,11 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
46
53
|
public static final String ERROR_DOMAIN_DNSSD = "DNSSD";
|
|
47
54
|
public static final String ERROR_DOMAIN_LIBRARY = "RNZeroconf";
|
|
48
55
|
public static final String ERROR_CODE_EXCEPTION = "EXCEPTION";
|
|
56
|
+
public static final String ERROR_CODE_TIMEOUT = "TIMEOUT";
|
|
57
|
+
public static final String ERROR_CODE_UNKNOWN_INTERFACE = "UNKNOWN_INTERFACE";
|
|
58
|
+
public static final String ERROR_CODE_UNSUPPORTED = "UNSUPPORTED";
|
|
59
|
+
// Manifest.permission.ACCESS_LOCAL_NETWORK, API 37
|
|
60
|
+
public static final String PERMISSION_ACCESS_LOCAL_NETWORK = "android.permission.ACCESS_LOCAL_NETWORK";
|
|
49
61
|
public static final String ERROR_CODE_NOT_PUBLISHED = "NOT_PUBLISHED";
|
|
50
62
|
|
|
51
63
|
private ZeroConfImplFactory zeroConfFactory;
|
|
@@ -61,9 +73,9 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
@ReactMethod
|
|
64
|
-
public void scan(String scanId, String type, String protocol, String domain, String implType) {
|
|
76
|
+
public void scan(String scanId, String type, String protocol, String domain, String implType, @Nullable ReadableMap options) {
|
|
65
77
|
try {
|
|
66
|
-
getZeroconfImpl(implType).scan(scanId, type, protocol, domain);
|
|
78
|
+
getZeroconfImpl(implType).scan(scanId, type, protocol, domain, ZeroconfOptions.from(options));
|
|
67
79
|
} catch (Throwable e) {
|
|
68
80
|
Log.e(getClass().getName(), e.getMessage(), e);
|
|
69
81
|
sendError(ERROR_DOMAIN_LIBRARY, ERROR_CODE_EXCEPTION, "Exception during scan: " + e.getMessage(), null, scanId);
|
|
@@ -93,9 +105,9 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
@ReactMethod
|
|
96
|
-
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, String implType, Promise promise) {
|
|
108
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, String implType, @Nullable ReadableMap options, Promise promise) {
|
|
97
109
|
try {
|
|
98
|
-
getZeroconfImpl(implType).registerService(type, protocol, domain, name, port, txt, promise);
|
|
110
|
+
getZeroconfImpl(implType).registerService(type, protocol, domain, name, port, txt, ZeroconfOptions.from(options), promise);
|
|
99
111
|
} catch (Throwable e) {
|
|
100
112
|
Log.e(getClass().getName(), e.getMessage(), e);
|
|
101
113
|
String message = "Exception while registering service: " + e.getMessage();
|
|
@@ -104,6 +116,43 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
104
116
|
}
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
@ReactMethod
|
|
120
|
+
public void updateService(String serviceName, ReadableArray txt, String implType, Promise promise) {
|
|
121
|
+
try {
|
|
122
|
+
getZeroconfImpl(implType).updateService(serviceName, txt, promise);
|
|
123
|
+
} catch (Throwable e) {
|
|
124
|
+
Log.e(getClass().getName(), e.getMessage(), e);
|
|
125
|
+
reject(promise, ERROR_DOMAIN_LIBRARY, ERROR_CODE_EXCEPTION, "Exception while updating service: " + e.getMessage(), serviceName);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@ReactMethod
|
|
130
|
+
public void resolveService(String name, String type, String protocol, String domain, String implType, @Nullable ReadableMap options, Promise promise) {
|
|
131
|
+
try {
|
|
132
|
+
getZeroconfImpl(implType).resolveService(name, type, protocol, domain, ZeroconfOptions.from(options), promise);
|
|
133
|
+
} catch (Throwable e) {
|
|
134
|
+
Log.e(getClass().getName(), e.getMessage(), e);
|
|
135
|
+
reject(promise, ERROR_DOMAIN_LIBRARY, ERROR_CODE_EXCEPTION, "Exception while resolving service: " + e.getMessage(), name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolves "granted" or "denied". On Android 17 (API 37) devices, the ACCESS_LOCAL_NETWORK runtime permission
|
|
141
|
+
* is enforced for apps targeting API 37 and for apps declaring it in their manifest. Otherwise there is nothing to grant.
|
|
142
|
+
*/
|
|
143
|
+
@ReactMethod
|
|
144
|
+
public void checkLocalNetworkAccess(Promise promise) {
|
|
145
|
+
Context context = getReactApplicationContext();
|
|
146
|
+
boolean required = Build.VERSION.SDK_INT >= 37
|
|
147
|
+
&& (context.getApplicationInfo().targetSdkVersion >= 37 || declaresPermission(context, PERMISSION_ACCESS_LOCAL_NETWORK));
|
|
148
|
+
if (!required) {
|
|
149
|
+
promise.resolve("granted");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
boolean granted = context.checkSelfPermission(PERMISSION_ACCESS_LOCAL_NETWORK) == PackageManager.PERMISSION_GRANTED;
|
|
153
|
+
promise.resolve(granted ? "granted" : "denied");
|
|
154
|
+
}
|
|
155
|
+
|
|
107
156
|
@ReactMethod
|
|
108
157
|
public void unregisterService(String serviceName, String implType, Promise promise) {
|
|
109
158
|
try {
|
|
@@ -173,6 +222,65 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
173
222
|
return error;
|
|
174
223
|
}
|
|
175
224
|
|
|
225
|
+
public static final String SERVICE_TYPES_TYPE = "services._dns-sd";
|
|
226
|
+
public static final String SERVICE_TYPES_PROTOCOL = "udp";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Whether a scan browses _services._dns-sd._udp, whose results are service types
|
|
230
|
+
*/
|
|
231
|
+
public static boolean isServiceTypesScan(String type, String protocol) {
|
|
232
|
+
return SERVICE_TYPES_TYPE.equals(type) && SERVICE_TYPES_PROTOCOL.equals(protocol);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Service type from a _services._dns-sd._udp result: "_http" and "_tcp.local." give "_http._tcp"
|
|
237
|
+
*/
|
|
238
|
+
public static String serviceTypeFromResult(String name, @Nullable String regType) {
|
|
239
|
+
if (regType != null) {
|
|
240
|
+
for (String label : regType.split("\\.")) {
|
|
241
|
+
if (label.startsWith("_")) {
|
|
242
|
+
return name + "." + label;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return name;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static boolean declaresPermission(Context context, String permission) {
|
|
250
|
+
try {
|
|
251
|
+
String[] requested = context.getPackageManager()
|
|
252
|
+
.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
|
|
253
|
+
if (requested != null) {
|
|
254
|
+
for (String declared : requested) {
|
|
255
|
+
if (permission.equals(declared)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
261
|
+
// Our own package is always found
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The Android network of a network interface ("wlan0"), null when there is none
|
|
268
|
+
*/
|
|
269
|
+
@Nullable
|
|
270
|
+
public static Network findNetwork(Context context, String interfaceName) {
|
|
271
|
+
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
272
|
+
if (connectivityManager == null) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
for (Network network : connectivityManager.getAllNetworks()) {
|
|
276
|
+
LinkProperties properties = connectivityManager.getLinkProperties(network);
|
|
277
|
+
if (properties != null && interfaceName.equals(properties.getInterfaceName())) {
|
|
278
|
+
return network;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
176
284
|
/**
|
|
177
285
|
* Readable description of an NsdManager failure code
|
|
178
286
|
*/
|
|
@@ -183,6 +291,23 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
183
291
|
case 4: return "maximum number of requests reached"; // FAILURE_MAX_LIMIT
|
|
184
292
|
case 5: return "operation not running"; // FAILURE_OPERATION_NOT_RUNNING (API 34)
|
|
185
293
|
case 6: return "bad parameters"; // FAILURE_BAD_PARAMETERS (API 34)
|
|
294
|
+
case 7: return "permission denied, the app needs the ACCESS_LOCAL_NETWORK permission"; // FAILURE_PERMISSION_DENIED (API 37)
|
|
295
|
+
default: return "error " + errorCode;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Readable description of a DNSServiceErrorType code, as on iOS
|
|
301
|
+
*/
|
|
302
|
+
public static String describeDnssdError(int errorCode) {
|
|
303
|
+
switch (errorCode) {
|
|
304
|
+
case -65548: return "name already in use"; // kDNSServiceErr_NameConflict
|
|
305
|
+
case -65540: return "bad parameter"; // kDNSServiceErr_BadParam
|
|
306
|
+
case -65538: return "no such name"; // kDNSServiceErr_NoSuchName
|
|
307
|
+
case -65554: return "no such record"; // kDNSServiceErr_NoSuchRecord
|
|
308
|
+
case -65563: return "mDNSResponder is not running"; // kDNSServiceErr_ServiceNotRunning
|
|
309
|
+
case -65539: return "out of memory"; // kDNSServiceErr_NoMemory
|
|
310
|
+
case -65544: return "unsupported"; // kDNSServiceErr_Unsupported
|
|
186
311
|
default: return "error " + errorCode;
|
|
187
312
|
}
|
|
188
313
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.balthazargronon.RCTZeroconf;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
4
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
5
|
+
import com.facebook.react.bridge.ReadableType;
|
|
6
|
+
|
|
7
|
+
import java.util.ArrayList;
|
|
8
|
+
import java.util.List;
|
|
9
|
+
|
|
10
|
+
import javax.annotation.Nullable;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options of scan, registerService and resolveService
|
|
14
|
+
*/
|
|
15
|
+
public class ZeroconfOptions {
|
|
16
|
+
/** Scan: subtype to browse, "_printer" */
|
|
17
|
+
@Nullable public String subtype;
|
|
18
|
+
/** Publish: subtypes to register, "_printer" */
|
|
19
|
+
public final List<String> subtypes = new ArrayList<>();
|
|
20
|
+
/** Network interface name, "wlan0" */
|
|
21
|
+
@Nullable public String networkInterface;
|
|
22
|
+
/** resolveService: seconds before giving up */
|
|
23
|
+
public double timeoutSeconds = 5;
|
|
24
|
+
|
|
25
|
+
public static ZeroconfOptions from(@Nullable ReadableMap map) {
|
|
26
|
+
ZeroconfOptions options = new ZeroconfOptions();
|
|
27
|
+
if (map == null) {
|
|
28
|
+
return options;
|
|
29
|
+
}
|
|
30
|
+
options.subtype = subtypeLabel(getString(map, "subtype"));
|
|
31
|
+
if (map.hasKey("subtypes") && map.getType("subtypes") == ReadableType.Array) {
|
|
32
|
+
ReadableArray subtypes = map.getArray("subtypes");
|
|
33
|
+
for (int i = 0; i < subtypes.size(); i++) {
|
|
34
|
+
String label = subtypes.getType(i) == ReadableType.String ? subtypeLabel(subtypes.getString(i)) : null;
|
|
35
|
+
if (label != null) {
|
|
36
|
+
options.subtypes.add(label);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
String networkInterface = getString(map, "networkInterface");
|
|
41
|
+
options.networkInterface = networkInterface == null || networkInterface.isEmpty() ? null : networkInterface;
|
|
42
|
+
if (map.hasKey("timeout") && map.getType("timeout") == ReadableType.Number && map.getDouble("timeout") > 0) {
|
|
43
|
+
options.timeoutSeconds = map.getDouble("timeout");
|
|
44
|
+
}
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* "_ipp._tcp" with the subtypes: "_ipp._tcp,_printer", the syntax of mDNSResponder and NsdManager
|
|
50
|
+
*/
|
|
51
|
+
public static String withSubtypes(String serviceType, List<String> subtypes) {
|
|
52
|
+
StringBuilder type = new StringBuilder(serviceType);
|
|
53
|
+
for (String subtype : subtypes) {
|
|
54
|
+
type.append(',').append(subtype);
|
|
55
|
+
}
|
|
56
|
+
return type.toString();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Nullable
|
|
60
|
+
private static String getString(ReadableMap map, String key) {
|
|
61
|
+
return map.hasKey(key) && map.getType(key) == ReadableType.String ? map.getString(key) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// "printer" or "_printer" -> "_printer"
|
|
65
|
+
@Nullable
|
|
66
|
+
private static String subtypeLabel(@Nullable String subtype) {
|
|
67
|
+
if (subtype == null || subtype.isEmpty()) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return subtype.startsWith("_") ? subtype : "_" + subtype;
|
|
71
|
+
}
|
|
72
|
+
}
|