1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * Copyright (C) 2016 Mopria Alliance, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.bips.discovery; 19 20 import android.net.Uri; 21 import android.net.nsd.NsdManager; 22 import android.net.nsd.NsdServiceInfo; 23 import android.net.wifi.WifiManager; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import com.android.bips.BuiltInPrintService; 28 29 import java.net.Inet4Address; 30 import java.util.ArrayList; 31 import java.util.List; 32 import java.util.Locale; 33 import java.util.Map; 34 35 /** 36 * Search the local network for devices advertising IPP print services 37 */ 38 public class MdnsDiscovery extends Discovery { 39 public static final String SCHEME_IPP = "ipp"; 40 public static final String SCHEME_IPPS = "ipps"; 41 42 private static final String TAG = MdnsDiscovery.class.getSimpleName(); 43 private static final boolean DEBUG = false; 44 45 // Prepend this to a UUID to create a proper URN 46 private static final String PREFIX_URN_UUID = "urn:uuid:"; 47 48 // Keys for expected txtRecord attributes 49 private static final String ATTRIBUTE_RP = "rp"; 50 private static final String ATTRIBUTE_UUID = "UUID"; 51 private static final String ATTRIBUTE_NOTE = "note"; 52 private static final String ATTRIBUTE_PRINT_WFDS = "print_wfds"; 53 private static final String VALUE_PRINT_WFDS_OPT_OUT = "F"; 54 55 // Service names of interest 56 private static final String SERVICE_IPP = "_ipp._tcp"; 57 private static final String SERVICE_IPPS = "_ipps._tcp"; 58 59 private final String mServiceName; 60 private final List<NsdServiceListener> mServiceListeners = new ArrayList<>(); 61 private final List<Resolver> mResolvers = new ArrayList<>(); 62 private final NsdResolveQueue mNsdResolveQueue; 63 64 /** Lock to keep multi-cast enabled */ 65 private WifiManager.MulticastLock mMulticastLock; 66 MdnsDiscovery(BuiltInPrintService printService, String scheme)67 public MdnsDiscovery(BuiltInPrintService printService, String scheme) { 68 super(printService); 69 70 switch (scheme) { 71 case SCHEME_IPP: 72 mServiceName = SERVICE_IPP; 73 break; 74 case SCHEME_IPPS: 75 mServiceName = SERVICE_IPPS; 76 break; 77 default: 78 throw new IllegalArgumentException("unrecognized scheme " + scheme); 79 } 80 mNsdResolveQueue = printService.getNsdResolveQueue(); 81 } 82 83 /** Return a valid {@link DiscoveredPrinter} from {@link NsdServiceInfo}, or null if invalid */ toNetworkPrinter(NsdServiceInfo info)84 private static DiscoveredPrinter toNetworkPrinter(NsdServiceInfo info) { 85 // Honor printers that deliberately opt-out 86 if (VALUE_PRINT_WFDS_OPT_OUT.equals(getStringAttribute(info, ATTRIBUTE_PRINT_WFDS))) { 87 if (DEBUG) Log.d(TAG, "Opted out: " + info); 88 return null; 89 } 90 91 // Collect resource path 92 String resourcePath = getStringAttribute(info, ATTRIBUTE_RP); 93 if (TextUtils.isEmpty(resourcePath)) { 94 if (DEBUG) Log.d(TAG, "Missing RP " + info); 95 return null; 96 } 97 if (resourcePath.startsWith("/")) { 98 resourcePath = resourcePath.substring(1); 99 } 100 101 // Hopefully has a UUID 102 Uri uuidUri = null; 103 String uuid = getStringAttribute(info, ATTRIBUTE_UUID); 104 if (!TextUtils.isEmpty(uuid)) { 105 uuidUri = Uri.parse(PREFIX_URN_UUID + uuid); 106 } 107 108 // Must be IPv4 109 if (!(info.getHost() instanceof Inet4Address)) { 110 if (DEBUG) Log.d(TAG, "Not IPv4" + info); 111 return null; 112 } 113 114 String scheme = info.getServiceType().contains(SERVICE_IPPS) ? SCHEME_IPPS : SCHEME_IPP; 115 Uri path = Uri.parse(scheme + "://" + info.getHost().getHostAddress() + ":" + info.getPort() 116 + "/" + resourcePath); 117 String location = getStringAttribute(info, ATTRIBUTE_NOTE); 118 119 return new DiscoveredPrinter(uuidUri, info.getServiceName(), path, location); 120 } 121 122 /** Return the value of an attribute or null if not present */ getStringAttribute(NsdServiceInfo info, String key)123 private static String getStringAttribute(NsdServiceInfo info, String key) { 124 key = key.toLowerCase(Locale.US); 125 for (Map.Entry<String, byte[]> entry : info.getAttributes().entrySet()) { 126 if (entry.getKey().toLowerCase(Locale.US).equals(key) && entry.getValue() != null) { 127 return new String(entry.getValue()); 128 } 129 } 130 return null; 131 } 132 133 @Override onStart()134 void onStart() { 135 if (DEBUG) Log.d(TAG, "onStart() " + mServiceName); 136 NsdServiceListener serviceListener = new NsdServiceListener() { 137 @Override 138 public void onStartDiscoveryFailed(String s, int i) { 139 // Do nothing 140 } 141 }; 142 143 WifiManager wifiManager = getPrintService().getSystemService(WifiManager.class); 144 if (wifiManager != null) { 145 if (mMulticastLock == null) { 146 mMulticastLock = wifiManager.createMulticastLock(this.getClass().getName()); 147 } 148 149 mMulticastLock.acquire(); 150 } 151 152 NsdManager nsdManager = mNsdResolveQueue.getNsdManager(); 153 nsdManager.discoverServices(mServiceName, NsdManager.PROTOCOL_DNS_SD, serviceListener); 154 mServiceListeners.add(serviceListener); 155 } 156 157 @Override onStop()158 void onStop() { 159 if (DEBUG) Log.d(TAG, "onStop() " + mServiceName); 160 NsdManager nsdManager = mNsdResolveQueue.getNsdManager(); 161 for (NsdServiceListener listener : mServiceListeners) { 162 nsdManager.stopServiceDiscovery(listener); 163 } 164 mServiceListeners.clear(); 165 166 for (Resolver resolver : mResolvers) { 167 resolver.cancel(); 168 } 169 mResolvers.clear(); 170 171 if (mMulticastLock != null) { 172 mMulticastLock.release(); 173 } 174 } 175 176 /** 177 * Manage notifications from NsdManager 178 */ 179 private abstract class NsdServiceListener implements NsdManager.DiscoveryListener { 180 @Override onStopDiscoveryFailed(String s, int errorCode)181 public void onStopDiscoveryFailed(String s, int errorCode) { 182 Log.w(TAG, "onStopDiscoveryFailed: " + errorCode); 183 } 184 185 @Override onDiscoveryStarted(String s)186 public void onDiscoveryStarted(String s) { 187 } 188 189 @Override onDiscoveryStopped(String service)190 public void onDiscoveryStopped(String service) { 191 // On the main thread, notify loss of all known printers 192 getHandler().post(MdnsDiscovery.this::allPrintersLost); 193 } 194 195 @Override onServiceFound(final NsdServiceInfo info)196 public void onServiceFound(final NsdServiceInfo info) { 197 if (DEBUG) Log.d(TAG, "found " + mServiceName + " name=" + info.getServiceName()); 198 getHandler().post(() -> mResolvers.add(new Resolver(info))); 199 } 200 201 @Override onServiceLost(final NsdServiceInfo info)202 public void onServiceLost(final NsdServiceInfo info) { 203 if (DEBUG) Log.d(TAG, "lost " + mServiceName + " name=" + info.getServiceName()); 204 205 // On the main thread, seek the missing printer by name and notify its loss 206 getHandler().post(() -> { 207 for (DiscoveredPrinter printer : getPrinters()) { 208 if (TextUtils.equals(printer.name, info.getServiceName())) { 209 printerLost(printer.getUri()); 210 return; 211 } 212 } 213 }); 214 } 215 } 216 217 /** 218 * Handle individual attempts to resolve 219 */ 220 private class Resolver implements NsdManager.ResolveListener { 221 private final NsdResolveQueue.NsdResolveRequest mResolveAttempt; 222 Resolver(NsdServiceInfo info)223 Resolver(NsdServiceInfo info) { 224 mResolveAttempt = mNsdResolveQueue.resolve(info, this); 225 } 226 227 @Override onResolveFailed(final NsdServiceInfo info, final int errorCode)228 public void onResolveFailed(final NsdServiceInfo info, final int errorCode) { 229 mResolvers.remove(this); 230 } 231 232 @Override onServiceResolved(final NsdServiceInfo info)233 public void onServiceResolved(final NsdServiceInfo info) { 234 mResolvers.remove(this); 235 if (!isStarted()) { 236 return; 237 } 238 239 DiscoveredPrinter printer = toNetworkPrinter(info); 240 if (DEBUG) Log.d(TAG, "Service " + info.getServiceName() + " resolved to " + printer); 241 if (printer == null) { 242 return; 243 } 244 printerFound(printer); 245 } 246 cancel()247 void cancel() { 248 mResolveAttempt.cancel(); 249 } 250 } 251 } 252