Skip to content

Commit 5e2f342

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

File tree

3 files changed

+280
-0
lines changed

3 files changed

+280
-0
lines changed

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

0 commit comments

Comments
 (0)