Home | History | Annotate | Download | only in discovery
      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 
     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 */
     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 */
    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
    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
    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
    181         public void onStopDiscoveryFailed(String s, int errorCode) {
    182             Log.w(TAG, "onStopDiscoveryFailed: " + errorCode);
    183         }
    184 
    185         @Override
    186         public void onDiscoveryStarted(String s) {
    187         }
    188 
    189         @Override
    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
    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
    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 
    223         Resolver(NsdServiceInfo info) {
    224             mResolveAttempt = mNsdResolveQueue.resolve(info, this);
    225         }
    226 
    227         @Override
    228         public void onResolveFailed(final NsdServiceInfo info, final int errorCode) {
    229             mResolvers.remove(this);
    230         }
    231 
    232         @Override
    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 
    247         void cancel() {
    248             mResolveAttempt.cancel();
    249         }
    250     }
    251 }
    252