Skip to content

Commit 157ade4

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

3 files changed

Lines changed: 284 additions & 0 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@
186186
<service android:name=".apis.MicRecorderAPI$MicRecorderService"
187187
android:exported="false" />
188188

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

0 commit comments

Comments
 (0)