• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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