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.getHost());
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