Skip to content

Commit f57fdaf

Browse files
committed
Added(NsdApi): Network service discovery API.
Fixes #688.
1 parent 97b4100 commit f57fdaf

3 files changed

Lines changed: 305 additions & 0 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<uses-permission android:name="android.permission.CAMERA" />
1616
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
1717
<uses-permission android:name="android.permission.INTERNET" />
18+
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
1819
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
1920
<uses-permission android:name="android.permission.NFC"/>
2021
<uses-permission android:name="android.permission.READ_CALL_LOG" />
@@ -186,6 +187,10 @@
186187
<service android:name=".apis.MicRecorderAPI$MicRecorderService"
187188
android:exported="false" />
188189

190+
<service android:name=".apis.NsdApi$NsdService"
191+
android:exported="false" />
192+
193+
189194
<service
190195
android:name=".apis.NotificationListAPI$NotificationService"
191196
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"

app/src/main/java/com/termux/api/TermuxApiReceiver.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.termux.api.apis.MediaPlayerAPI;
2727
import com.termux.api.apis.MediaScannerAPI;
2828
import com.termux.api.apis.MicRecorderAPI;
29+
import com.termux.api.apis.NsdApi;
2930
import com.termux.api.apis.NfcAPI;
3031
import com.termux.api.apis.NotificationAPI;
3132
import com.termux.api.apis.NotificationListAPI;
@@ -164,6 +165,9 @@ private void doWork(Context context, Intent intent) {
164165
MicRecorderAPI.onReceive(context, intent);
165166
}
166167
break;
168+
case "Nsd":
169+
NsdApi.onReceive(context, intent);
170+
break;
167171
case "Nfc":
168172
NfcAPI.onReceive(context, intent);
169173
break;
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
package com.termux.api.apis;
2+
3+
import static com.termux.api.apis.NsdApi.ResultJson.resultJson;
4+
import static java.util.Objects.requireNonNull;
5+
6+
import android.app.Service;
7+
import android.content.Context;
8+
import android.content.Intent;
9+
import android.net.nsd.NsdManager;
10+
import android.net.nsd.NsdServiceInfo;
11+
import android.net.wifi.WifiManager;
12+
import android.os.Build;
13+
import android.os.IBinder;
14+
import android.os.ext.SdkExtensions;
15+
import android.util.JsonWriter;
16+
17+
import androidx.annotation.Nullable;
18+
19+
import com.termux.api.util.ResultReturner;
20+
import com.termux.api.util.ResultReturner.ResultJsonWriter;
21+
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.UUID;
27+
import java.util.function.Consumer;
28+
import java.util.function.Predicate;
29+
30+
public class NsdApi {
31+
32+
@FunctionalInterface
33+
public interface JsonConsumer extends Consumer<JsonWriter> {
34+
void write(JsonWriter writer) throws Exception;
35+
36+
default void accept(JsonWriter jsonWriter) {
37+
try {
38+
write(jsonWriter);
39+
} catch (Exception e) {
40+
throw new RuntimeException(e);
41+
}
42+
}
43+
}
44+
45+
private static class ResultCallback {
46+
private final Context context;
47+
private final Intent intent;
48+
private Runnable onSuccess;
49+
50+
public ResultCallback(Context applicationContext, Intent intent) {
51+
this.context = applicationContext;
52+
this.intent = intent;
53+
}
54+
55+
public void send(Consumer<JsonWriter> visitor) {
56+
ResultReturner.returnData(context, intent, new ResultJsonWriter() {
57+
@Override
58+
public void writeJson(JsonWriter out) throws Exception {
59+
out.beginObject();
60+
visitor.accept(out);
61+
out.endObject();
62+
}
63+
});
64+
}
65+
66+
public void success(Consumer<JsonWriter> data) {
67+
if (onSuccess != null) onSuccess.run();
68+
send(resultJson().code(0).andThen(data));
69+
}
70+
71+
public void error(int errorCode, String msg, Object... args) {
72+
send(resultJson().code(errorCode).message(msg, args));
73+
}
74+
75+
public ResultCallback onSuccess(Runnable r) {
76+
this.onSuccess = r;
77+
return this;
78+
}
79+
}
80+
81+
private static class RegistrationListener implements NsdManager.RegistrationListener {
82+
83+
private ResultCallback result;
84+
private final UUID id;
85+
private final NsdServiceInfo serviceInfo;
86+
87+
public RegistrationListener(NsdServiceInfo info) {
88+
this.serviceInfo = info;
89+
this.id = UUID.randomUUID();
90+
}
91+
92+
@Override
93+
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
94+
if (result != null) {
95+
result.error(errorCode, "%s registration failed", serviceInfo);
96+
result = null;
97+
}
98+
}
99+
100+
@Override
101+
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
102+
if (result != null) {
103+
result.error(errorCode, "%s unregistration failed", serviceInfo);
104+
result = null;
105+
}
106+
}
107+
108+
@Override
109+
public void onServiceRegistered(NsdServiceInfo regInfo) {
110+
final var registeredName = regInfo.getServiceName();
111+
serviceInfo.setServiceName(registeredName);
112+
if (result != null) {
113+
result.success(resultJson().id(id)
114+
.message("registered %s", serviceInfo)
115+
.stringField("name", registeredName));
116+
117+
result = null;
118+
}
119+
}
120+
121+
@Override
122+
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
123+
if (result != null) {
124+
result.success(resultJson().message("unregistered %s", serviceInfo));
125+
result = null;
126+
}
127+
}
128+
129+
public RegistrationListener setResultCallback(ResultCallback result) {
130+
this.result = result;
131+
return this;
132+
}
133+
}
134+
135+
public static class NsdService extends Service {
136+
private final ArrayList<RegistrationListener> registrations = new ArrayList<>();
137+
private WifiManager.MulticastLock multicastLock;
138+
139+
private WifiManager.MulticastLock multicastLock() {
140+
if (multicastLock == null) {
141+
final var wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
142+
multicastLock = wifiManager.createMulticastLock(this.getClass().getSimpleName());
143+
multicastLock.setReferenceCounted(true);
144+
}
145+
return multicastLock;
146+
}
147+
148+
@Override
149+
public int onStartCommand(Intent intent, int flags, int startId) {
150+
final var nsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE);
151+
152+
final var command = intent.getStringExtra("command");
153+
final var callback = new ResultCallback(getApplicationContext(), intent);
154+
try {
155+
if ("register".equals(command)) {
156+
var info = nsdServiceInfo(intent);
157+
var registration = new RegistrationListener(info);
158+
registration.setResultCallback(callback.onSuccess(() -> {
159+
registrations.add(registration);
160+
multicastLock().acquire();
161+
}));
162+
nsdManager.registerService(info, NsdManager.PROTOCOL_DNS_SD, registration);
163+
} else if ("unregister".equals(command)) {
164+
findListener(intent).ifPresentOrElse(r -> {
165+
r.setResultCallback(callback.onSuccess(() -> {
166+
registrations.remove(r);
167+
multicastLock().release();
168+
}));
169+
nsdManager.unregisterService(r);
170+
}, () -> callback.error(-1, "registration not found"));
171+
} else if ("list".equals(command)) {
172+
callback.success((JsonConsumer) out -> {
173+
out.name("registrations");
174+
out.beginArray();
175+
for (var r : registrations) {
176+
out.beginObject()
177+
.name("id").value(r.id.toString())
178+
.name("name").value(r.serviceInfo.getServiceName())
179+
.name("type").value(r.serviceInfo.getServiceType())
180+
.name("port").value(r.serviceInfo.getPort())
181+
.endObject();
182+
}
183+
out.endArray();
184+
});
185+
} else {
186+
callback.error(-1, "Unsupported command: %s", command);
187+
}
188+
} catch (Exception e) {
189+
callback.error(-2, "Exception: %s", e.getMessage());
190+
}
191+
192+
return START_NOT_STICKY;
193+
}
194+
195+
@Override
196+
public void onDestroy() {
197+
final var nsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE);
198+
registrations.forEach(nsdManager::unregisterService);
199+
}
200+
201+
private static Predicate<RegistrationListener> search(Intent intent) {
202+
var id = intent.getStringExtra("id");
203+
if (id != null) {
204+
return r -> r.id.toString().equals(id);
205+
}
206+
207+
var name = requireNonNull(intent.getStringExtra("name"));
208+
var type = requireNonNull(intent.getStringExtra("type"));
209+
return r -> name.equals(r.serviceInfo.getServiceName())
210+
&& type.equals(r.serviceInfo.getServiceType());
211+
}
212+
213+
private Optional<RegistrationListener> findListener(Intent intent) {
214+
return registrations.stream()
215+
.filter(r -> r.serviceInfo != null)
216+
.filter(search(intent))
217+
.findFirst();
218+
}
219+
220+
private static NsdServiceInfo nsdServiceInfo(Intent intent) {
221+
final var nsdServiceInfo = new NsdServiceInfo();
222+
nsdServiceInfo.setServiceName(intent.getStringExtra("name"));
223+
nsdServiceInfo.setServiceType(intent.getStringExtra("type"));
224+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.TIRAMISU) >= 12) {
225+
Optional.ofNullable(intent.getStringArrayExtra("subTypes"))
226+
.map(Set::of)
227+
.ifPresent(nsdServiceInfo::setSubtypes);
228+
}
229+
230+
Optional.ofNullable(intent.getStringArrayExtra("attributes"))
231+
.stream().flatMap(Arrays::stream)
232+
.map(s -> s.split("=", 2))
233+
.forEach(a -> nsdServiceInfo.setAttribute(a[0], a[1]));
234+
235+
int port = intent.getIntExtra("port", 0);
236+
if (port <= 0) {
237+
throw new IllegalArgumentException("invalid port value");
238+
}
239+
240+
nsdServiceInfo.setPort(port);
241+
return nsdServiceInfo;
242+
}
243+
244+
@Nullable
245+
@Override
246+
public IBinder onBind(Intent intent) {
247+
return null;
248+
}
249+
}
250+
251+
public static void onReceive(final Context context, Intent intent) {
252+
final var serviceIntent = new Intent(context, NsdService.class);
253+
Optional.ofNullable(intent.getExtras()).ifPresent(serviceIntent::putExtras);
254+
context.startService(serviceIntent);
255+
}
256+
257+
static class ResultJson implements Consumer<JsonWriter> {
258+
private Consumer<JsonWriter> delegate;
259+
260+
public ResultJson() {
261+
this.delegate = out -> {
262+
};
263+
}
264+
265+
public static ResultJson resultJson() {
266+
return new ResultJson();
267+
}
268+
269+
public ResultJson longField(String name, long value) {
270+
delegate = delegate.andThen((JsonConsumer) (out) -> out.name(name).value(value));
271+
return this;
272+
}
273+
274+
public ResultJson stringField(String name, Object value) {
275+
delegate = delegate.andThen((JsonConsumer) (out) -> out.name(name).value(value.toString()));
276+
return this;
277+
}
278+
279+
public ResultJson code(int errorCode) {
280+
return longField("code", errorCode);
281+
}
282+
283+
public ResultJson message(String message, Object... args) {
284+
return stringField("message", String.format(message, args));
285+
}
286+
287+
public ResultJson id(Object id) {
288+
return stringField("id", id);
289+
}
290+
291+
@Override
292+
public void accept(JsonWriter jsonWriter) {
293+
delegate.accept(jsonWriter);
294+
}
295+
}
296+
}

0 commit comments

Comments
 (0)