| /* |
| * 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) { |
| 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(); |
| } |
| } |
| } |
| }; |
| |
| NsdServiceFinderAndResolver serviceFinderResolver = |
| new NsdServiceFinderAndResolver( |
| this.nsdManager, |
| serviceInfo, |
| callbackHandle, |
| contextHandle, |
| chipMdnsCallback, |
| timeoutRunnable, |
| multicastLock, |
| mainThreadHandler, |
| nsdManagerResolverAvailState); |
| serviceFinderResolver.start(); |
| |
| 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(); |
| } |
| } |
| } |
| } |