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.content.Context; 21 import android.net.Uri; 22 import android.net.nsd.NsdManager; 23 import android.net.nsd.NsdServiceInfo; 24 import android.os.Handler; 25 import android.text.TextUtils; 26 import android.util.Log; 27 28 import com.android.bips.BuiltInPrintService; 29 30 import java.net.Inet4Address; 31 import java.util.HashMap; 32 import java.util.Locale; 33 import java.util.Map; 34 import java.util.Timer; 35 import java.util.TimerTask; 36 37 /** 38 * Search the local network for devices advertising IPP print services 39 */ 40 public class MdnsDiscovery extends Discovery { 41 private static final String TAG = MdnsDiscovery.class.getSimpleName(); 42 private static final boolean DEBUG = false; 43 private static final long IPPS_DELAY = 150; 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 name of interest 56 private static final String SERVICE_IPP = "_ipp._tcp"; 57 private static final String SERVICE_IPPS = "_ipps._tcp"; 58 59 private static final String SCHEME_IPP = "ipp"; 60 private static final String SCHEME_IPPS = "ipps"; 61 62 /** Network Service Discovery Manager */ 63 private final NsdManager mNsdManager; 64 65 /** Handler used for posting to main thread */ 66 private final Handler mMainHandler; 67 68 /** Handle to listener when registered */ 69 private NsdServiceListener mIppServiceListener; 70 private NsdServiceListener mIppsServiceListener; 71 72 private Map<Uri, IppsDelay> mIppsDelays = new HashMap<>(); 73 MdnsDiscovery(BuiltInPrintService printService)74 public MdnsDiscovery(BuiltInPrintService printService) { 75 this(printService, (NsdManager) printService.getSystemService(Context.NSD_SERVICE)); 76 } 77 78 /** Constructor for use by test */ MdnsDiscovery(BuiltInPrintService printService, NsdManager nsdManager)79 MdnsDiscovery(BuiltInPrintService printService, NsdManager nsdManager) { 80 super(printService); 81 mNsdManager = nsdManager; 82 mMainHandler = new Handler(printService.getMainLooper()); 83 } 84 85 /** Return a valid {@link DiscoveredPrinter} from {@link NsdServiceInfo}, or null if invalid */ toNetworkPrinter(NsdServiceInfo info)86 private static DiscoveredPrinter toNetworkPrinter(NsdServiceInfo info) { 87 // Honor printers that deliberately opt-out 88 if (VALUE_PRINT_WFDS_OPT_OUT.equals(getStringAttribute(info, ATTRIBUTE_PRINT_WFDS))) { 89 if (DEBUG) Log.d(TAG, "Opted out: " + info); 90 return null; 91 } 92 93 // Collect resource path 94 String resourcePath = getStringAttribute(info, ATTRIBUTE_RP); 95 if (TextUtils.isEmpty(resourcePath)) { 96 if (DEBUG) Log.d(TAG, "Missing RP" + info); 97 return null; 98 } 99 if (resourcePath.startsWith("/")) { 100 resourcePath = resourcePath.substring(1); 101 } 102 103 // Hopefully has a UUID 104 Uri uuidUri = null; 105 String uuid = getStringAttribute(info, ATTRIBUTE_UUID); 106 if (!TextUtils.isEmpty(uuid)) { 107 uuidUri = Uri.parse(PREFIX_URN_UUID + uuid); 108 } 109 110 // Must be IPv4 111 if (!(info.getHost() instanceof Inet4Address)) { 112 if (DEBUG) Log.d(TAG, "Not IPv4" + info); 113 return null; 114 } 115 116 String scheme = info.getServiceType().contains(SERVICE_IPPS) ? SCHEME_IPPS : SCHEME_IPP; 117 Uri path = Uri.parse(scheme + "://" + info.getHost().getHostAddress() + ":" + info.getPort() + "/" + 118 resourcePath); 119 String location = getStringAttribute(info, ATTRIBUTE_NOTE); 120 121 return new DiscoveredPrinter(uuidUri, info.getServiceName(), path, location); 122 } 123 124 /** Return the value of an attribute or null if not present */ getStringAttribute(NsdServiceInfo info, String key)125 private static String getStringAttribute(NsdServiceInfo info, String key) { 126 key = key.toLowerCase(Locale.US); 127 for (Map.Entry<String, byte[]> entry : info.getAttributes().entrySet()) { 128 if (entry.getKey().toLowerCase(Locale.US).equals(key) && entry.getValue() != null) { 129 return new String(entry.getValue()); 130 } 131 } 132 return null; 133 } 134 135 @Override onStart()136 void onStart() { 137 if (DEBUG) Log.d(TAG, "onStart()"); 138 mIppServiceListener = new NsdServiceListener() { 139 @Override 140 public void onStartDiscoveryFailed(String s, int i) { 141 mIppServiceListener = null; 142 } 143 }; 144 145 mNsdManager.discoverServices(SERVICE_IPP, NsdManager.PROTOCOL_DNS_SD, mIppServiceListener); 146 147 mIppsServiceListener = new NsdServiceListener() { 148 @Override 149 public void onStartDiscoveryFailed(String s, int i) { 150 mIppServiceListener = null; 151 } 152 }; 153 mNsdManager.discoverServices(SERVICE_IPPS, NsdManager.PROTOCOL_DNS_SD, mIppsServiceListener); 154 } 155 156 @Override onStop()157 void onStop() { 158 if (DEBUG) Log.d(TAG, "onStop()"); 159 160 NsdResolveQueue.getInstance(getPrintService()).clear(); 161 for (IppsDelay ippsDelay : mIppsDelays.values()) { 162 mMainHandler.removeCallbacks(ippsDelay); 163 } 164 mIppsDelays.clear(); 165 166 if (mIppServiceListener != null) { 167 mNsdManager.stopServiceDiscovery(mIppServiceListener); 168 mIppServiceListener = null; 169 } 170 171 if (mIppsServiceListener != null) { 172 mNsdManager.stopServiceDiscovery(mIppsServiceListener); 173 mIppsServiceListener = null; 174 } 175 176 mMainHandler.removeCallbacksAndMessages(null); 177 NsdResolveQueue.getInstance(getPrintService()).clear(); 178 } 179 180 /** 181 * Manage notifications from NsdManager 182 */ 183 private abstract class NsdServiceListener implements NsdManager.DiscoveryListener, 184 NsdManager.ResolveListener { 185 186 @Override onStopDiscoveryFailed(String s, int errorCode)187 public void onStopDiscoveryFailed(String s, int errorCode) { 188 Log.w(TAG, "onStopDiscoveryFailed: " + errorCode); 189 } 190 191 @Override onDiscoveryStarted(String s)192 public void onDiscoveryStarted(String s) { 193 if (DEBUG) Log.d(TAG, "onDiscoveryStarted"); 194 } 195 196 @Override onDiscoveryStopped(String s)197 public void onDiscoveryStopped(String s) { 198 if (DEBUG) Log.d(TAG, "onDiscoveryStopped"); 199 200 // On the main thread, notify loss of all known printers 201 mMainHandler.post(() -> allPrintersLost()); 202 } 203 204 @Override onServiceFound(final NsdServiceInfo info)205 public void onServiceFound(final NsdServiceInfo info) { 206 if (DEBUG) Log.d(TAG, "onServiceFound - " + info.getServiceName()); 207 NsdResolveQueue.getInstance(getPrintService()).resolve(mNsdManager, info, this); 208 } 209 210 @Override onServiceLost(final NsdServiceInfo info)211 public void onServiceLost(final NsdServiceInfo info) { 212 if (DEBUG) Log.d(TAG, "onServiceLost - " + info.getServiceName()); 213 214 // On the main thread, seek the missing printer by name and notify its loss 215 mMainHandler.post(() -> { 216 for (DiscoveredPrinter printer : getPrinters()) { 217 if (TextUtils.equals(printer.name, info.getServiceName())) { 218 cancelIppsDelay(printer.getUri()); 219 printerLost(printer.getUri()); 220 return; 221 } 222 } 223 }); 224 } 225 226 @Override onResolveFailed(final NsdServiceInfo info, final int errorCode)227 public void onResolveFailed(final NsdServiceInfo info, final int errorCode) { 228 } 229 230 @Override onServiceResolved(final NsdServiceInfo info)231 public void onServiceResolved(final NsdServiceInfo info) { 232 final DiscoveredPrinter printer = toNetworkPrinter(info); 233 if (DEBUG) Log.d(TAG, "Service " + info.getServiceName() + " resolved to " + printer); 234 if (printer == null) { 235 return; 236 } 237 238 Uri printerUri = printer.getUri(); 239 if (printer.path.getScheme().equals(SCHEME_IPPS)) { 240 DiscoveredPrinter oldPrinter = getPrinter(printerUri); 241 IppsDelay ippsDelay = mIppsDelays.get(printerUri); 242 if (oldPrinter == null && ippsDelay == null) { 243 // This IPPS printer is not known yet so delay a short time to see if IPP arrives 244 mIppsDelays.put(printerUri, new IppsDelay(printer)); 245 } 246 return; 247 } else { 248 // IPP discovered, so cancel any outstanding IPPS delay 249 cancelIppsDelay(printerUri); 250 } 251 252 mMainHandler.post(() -> printerFound(printer)); 253 } 254 } 255 cancelIppsDelay(Uri printerUri)256 private void cancelIppsDelay(Uri printerUri) { 257 IppsDelay ippsDelay = mIppsDelays.get(printerUri); 258 mMainHandler.removeCallbacks(ippsDelay); 259 mIppsDelays.remove(printerUri); 260 } 261 262 private class IppsDelay implements Runnable { 263 final DiscoveredPrinter printer; 264 IppsDelay(DiscoveredPrinter printer)265 IppsDelay(DiscoveredPrinter printer) { 266 this.printer = printer; 267 mMainHandler.postDelayed(this, IPPS_DELAY); 268 } 269 270 @Override run()271 public void run() { 272 printerFound(printer); 273 } 274 } 275 } 276