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.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 
     74     public MdnsDiscovery(BuiltInPrintService printService) {
     75         this(printService, (NsdManager) printService.getSystemService(Context.NSD_SERVICE));
     76     }
     77 
     78     /** Constructor for use by test */
     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 */
     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 */
    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
    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
    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
    187         public void onStopDiscoveryFailed(String s, int errorCode) {
    188             Log.w(TAG, "onStopDiscoveryFailed: " + errorCode);
    189         }
    190 
    191         @Override
    192         public void onDiscoveryStarted(String s) {
    193             if (DEBUG) Log.d(TAG, "onDiscoveryStarted");
    194         }
    195 
    196         @Override
    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
    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
    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
    227         public void onResolveFailed(final NsdServiceInfo info, final int errorCode) {
    228         }
    229 
    230         @Override
    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 
    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 
    265         IppsDelay(DiscoveredPrinter printer) {
    266             this.printer = printer;
    267             mMainHandler.postDelayed(this, IPPS_DELAY);
    268         }
    269 
    270         @Override
    271         public void run() {
    272             printerFound(printer);
    273         }
    274     }
    275 }
    276