blob: 11b6241d2deb851d4011d27fb575352a645d6988 [file] [log] [blame]
/*
* Copyright (c) 2021 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package chip.platform;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class NsdManagerServiceResolver implements ServiceResolver {
private static final String TAG = NsdManagerServiceResolver.class.getSimpleName();
private static final long RESOLVE_SERVICE_TIMEOUT = 30000;
private final NsdManager nsdManager;
private MulticastLock multicastLock;
private Handler mainThreadHandler;
private List<NsdManager.RegistrationListener> registrationListeners = new ArrayList<>();
private final CopyOnWriteArrayList<String> mMFServiceName = new CopyOnWriteArrayList<>();
@Nullable private final NsdManagerResolverAvailState nsdManagerResolverAvailState;
/**
* @param context application context
* @param nsdManagerResolverAvailState Passing NsdManagerResolverAvailState allows
* NsdManagerServiceResolver to synchronize on the usage of NsdManager's resolveService() API
*/
public NsdManagerServiceResolver(
Context context, @Nullable NsdManagerResolverAvailState nsdManagerResolverAvailState) {
this.nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
this.mainThreadHandler = new Handler(Looper.getMainLooper());
this.multicastLock =
((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
.createMulticastLock("chipMulticastLock");
this.multicastLock.setReferenceCounted(true);
this.nsdManagerResolverAvailState = nsdManagerResolverAvailState;
}
public NsdManagerServiceResolver(Context context) {
this(context, null);
}
@Override
public void resolve(
final String instanceName,
final String serviceType,
final long callbackHandle,
final long contextHandle,
final ChipMdnsCallback chipMdnsCallback) {
multicastLock.acquire();
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName(instanceName);
serviceInfo.setServiceType(serviceType);
Log.d(
TAG,
"resolve: Starting service resolution for '"
+ instanceName
+ "' type '"
+ serviceType
+ "'");
Runnable timeoutRunnable =
new Runnable() {
@Override
public void run() {
// Ensure we always release the multicast lock. It's possible that we release the
// multicast lock here before ResolveListener returns, but since NsdManager has no API
// to cancel service resolution, there's not much we can do here.
Log.d(TAG, "resolve: Timing out");
if (multicastLock.isHeld()) {
multicastLock.release();
if (nsdManagerResolverAvailState != null) {
nsdManagerResolverAvailState.signalFree();
}
}
}
};
if (nsdManagerResolverAvailState != null) {
nsdManagerResolverAvailState.acquireResolver();
}
this.nsdManager.resolveService(
serviceInfo,
new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.w(
TAG,
"Failed to resolve service '" + serviceInfo.getServiceName() + "': " + errorCode);
chipMdnsCallback.handleServiceResolve(
instanceName, serviceType, null, null, 0, null, callbackHandle, contextHandle);
if (multicastLock.isHeld()) {
multicastLock.release();
if (nsdManagerResolverAvailState != null) {
nsdManagerResolverAvailState.signalFree();
}
}
mainThreadHandler.removeCallbacks(timeoutRunnable);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
Log.i(
TAG,
"Resolved service '"
+ serviceInfo.getServiceName()
+ "' to "
+ serviceInfo.getHost());
// TODO: Find out if DNS-SD results for Android should contain interface ID
chipMdnsCallback.handleServiceResolve(
instanceName,
serviceType,
serviceInfo.getHost().getHostName(),
serviceInfo.getHost().getHostAddress(),
serviceInfo.getPort(),
serviceInfo.getAttributes(),
callbackHandle,
contextHandle);
if (multicastLock.isHeld()) {
multicastLock.release();
if (nsdManagerResolverAvailState != null) {
nsdManagerResolverAvailState.signalFree();
}
}
mainThreadHandler.removeCallbacks(timeoutRunnable);
}
});
mainThreadHandler.postDelayed(timeoutRunnable, RESOLVE_SERVICE_TIMEOUT);
}
@Override
public void publish(
String serviceName,
String hostName,
String type,
int port,
String[] textEntriesKeys,
byte[][] textEntriesDatas,
String[] subTypes) {
/**
* Note, MF's NSDService will be repeatedly registered until it exceeds the OS's
* maximum(http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/NsdService.java#MAX_LIMIT)
* limit at which time the registration will fail.
*/
if (serviceName.contains("-") && mMFServiceName.contains(serviceName)) {
Log.w(TAG, "publish: duplicate MF nsdService");
return;
}
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName(serviceName);
/**
* Note, subtypes registration is using an undocumented feature of android dns-sd
* service/mDNSResponder which MAY STOP WORKING in future Android versions. Here, set type =
* "${type},${subtypes1},${subtypes2},...", then subtypes1, subtypes2 etc are all registered to
* this dns-sd server, we can usd `dns-sd -B ${type},${subtypes}` or avahi-browse
* ${subtypes}._sub.${type} -r to browser it
*/
StringBuilder sb = new StringBuilder(type);
for (String subType : subTypes) {
sb.append(",").append(subType);
}
serviceInfo.setServiceType(sb.toString());
serviceInfo.setPort(port);
Log.i(TAG, "publish serviceName=" + serviceName + " type=" + sb.toString() + " port=" + port);
int cnt = Math.min(textEntriesDatas.length, textEntriesKeys.length);
for (int i = 0; i < cnt; i++) {
String value = new String(textEntriesDatas[i]);
serviceInfo.setAttribute(textEntriesKeys[i], value);
Log.i(TAG, " " + textEntriesKeys[i] + "=" + value);
}
NsdManager.RegistrationListener registrationListener =
new NsdManager.RegistrationListener() {
@Override
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.w(
TAG,
"service " + serviceInfo.getServiceName() + " onRegistrationFailed:" + errorCode);
}
@Override
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
Log.w(
TAG,
"service " + serviceInfo.getServiceName() + " onUnregistrationFailed:" + errorCode);
}
@Override
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
Log.i(
TAG,
"service " + serviceInfo.getServiceName() + "(" + this + ") onServiceRegistered");
}
@Override
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
Log.i(
TAG,
"service " + serviceInfo.getServiceName() + "(" + this + ") onServiceUnregistered");
}
};
if (registrationListeners.size() == 0) {
multicastLock.acquire();
}
registrationListeners.add(registrationListener);
mMFServiceName.add(serviceName);
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
Log.d(TAG, "publish " + registrationListener + " count = " + registrationListeners.size());
}
@Override
public void removeServices() {
Log.d(TAG, "removeServices: ");
if (registrationListeners.size() > 0) {
multicastLock.release();
}
for (NsdManager.RegistrationListener l : registrationListeners) {
Log.i(TAG, "Remove " + l);
nsdManager.unregisterService(l);
}
registrationListeners.clear();
mMFServiceName.clear();
}
/**
* The Android NsdManager calls back on the NsdManager.ResolveListener with a
* FAILURE_ALREADY_ACTIVE(3) if any application code calls resolveService() on it while the
* resolve operation is already active (from another call made previously). An object of
* NsdManagerResolverAvailState allows NsdManagerServiceResolver to synchronize on the usage of
* NsdManager's resolveService() API
*/
public static class NsdManagerResolverAvailState {
private static final String TAG = NsdManagerResolverAvailState.class.getSimpleName();
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean busy = false;
/**
* Waits if the NsdManager is already busy with resolving a service. Otherwise, it marks it as
* busy and returns
*/
public void acquireResolver() {
lock.lock();
try {
while (busy) {
Log.d(TAG, "Found NsdManager Resolver busy, waiting");
condition.await();
}
Log.d(TAG, "Found NsdManager Resolver free, using it and marking it as busy");
busy = true;
} catch (InterruptedException e) {
Log.e(TAG, "Failure while waiting for condition: " + e);
} finally {
lock.unlock();
}
}
/** Signals the NsdManager resolver as free */
public void signalFree() {
lock.lock();
try {
Log.d(TAG, "Signaling NsdManager Resolver as free");
busy = false;
condition.signal();
} finally {
lock.unlock();
}
}
}
}