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.Locale;
     32 import java.util.Map;
     33 
     34 /**
     35  * Search the local network for devices advertising IPP print services
     36  */
     37 public class MdnsDiscovery extends Discovery {
     38     private static final String TAG = MdnsDiscovery.class.getSimpleName();
     39     private static final boolean DEBUG = false;
     40 
     41     // Prepend this to a UUID to create a proper URN
     42     private static final String PREFIX_URN_UUID = "urn:uuid:";
     43 
     44     // Keys for expected txtRecord attributes
     45     private static final String ATTRIBUTE_RP = "rp";
     46     private static final String ATTRIBUTE_UUID = "UUID";
     47     private static final String ATTRIBUTE_NOTE = "note";
     48     private static final String ATTRIBUTE_PRINT_WFDS = "print_wfds";
     49     private static final String VALUE_PRINT_WFDS_OPT_OUT = "F";
     50 
     51     // Service name of interest
     52     private static final String SERVICE_IPP = "_ipp._tcp";
     53 
     54     /** Network Service Discovery Manager */
     55     private final NsdManager mNsdManager;
     56 
     57     /** Handler used for posting to main thread */
     58     private final Handler mMainHandler;
     59 
     60     /** Handle to listener when registered */
     61     private NsdServiceListener mServiceListener;
     62 
     63     public MdnsDiscovery(BuiltInPrintService printService) {
     64         this(printService, (NsdManager) printService.getSystemService(Context.NSD_SERVICE));
     65     }
     66 
     67     /** Constructor for use by test */
     68     MdnsDiscovery(BuiltInPrintService printService, NsdManager nsdManager) {
     69         super(printService);
     70         mNsdManager = nsdManager;
     71         mMainHandler = new Handler(printService.getMainLooper());
     72     }
     73 
     74     /** Return a valid {@link DiscoveredPrinter} from {@link NsdServiceInfo}, or null if invalid */
     75     private static DiscoveredPrinter toNetworkPrinter(NsdServiceInfo info) {
     76         // Honor printers that deliberately opt-out
     77         if (VALUE_PRINT_WFDS_OPT_OUT.equals(getStringAttribute(info, ATTRIBUTE_PRINT_WFDS))) {
     78             if (DEBUG) Log.d(TAG, "Opted out: " + info);
     79             return null;
     80         }
     81 
     82         // Collect resource path
     83         String resourcePath = getStringAttribute(info, ATTRIBUTE_RP);
     84         if (TextUtils.isEmpty(resourcePath)) {
     85             if (DEBUG) Log.d(TAG, "Missing RP" + info);
     86             return null;
     87         }
     88         if (resourcePath.startsWith("/")) {
     89             resourcePath = resourcePath.substring(1);
     90         }
     91 
     92         // Hopefully has a UUID
     93         Uri uuidUri = null;
     94         String uuid = getStringAttribute(info, ATTRIBUTE_UUID);
     95         if (!TextUtils.isEmpty(uuid)) {
     96             uuidUri = Uri.parse(PREFIX_URN_UUID + uuid);
     97         }
     98 
     99         // Must be IPv4
    100         if (!(info.getHost() instanceof Inet4Address)) {
    101             if (DEBUG) Log.d(TAG, "Not IPv4" + info);
    102             return null;
    103         }
    104 
    105         Uri path = Uri.parse("ipp://" + info.getHost().getHostAddress() +
    106                 ":" + info.getPort() + "/" + resourcePath);
    107         String location = getStringAttribute(info, ATTRIBUTE_NOTE);
    108 
    109         return new DiscoveredPrinter(uuidUri, info.getServiceName(), path, location);
    110     }
    111 
    112     /** Return the value of an attribute or null if not present */
    113     private static String getStringAttribute(NsdServiceInfo info, String key) {
    114         key = key.toLowerCase(Locale.US);
    115         for (Map.Entry<String, byte[]> entry : info.getAttributes().entrySet()) {
    116             if (entry.getKey().toLowerCase(Locale.US).equals(key) && entry.getValue() != null) {
    117                 return new String(entry.getValue());
    118             }
    119         }
    120         return null;
    121     }
    122 
    123     @Override
    124     void onStart() {
    125         if (DEBUG) Log.d(TAG, "onStart()");
    126         mServiceListener = new NsdServiceListener();
    127         mNsdManager.discoverServices(SERVICE_IPP, NsdManager.PROTOCOL_DNS_SD, mServiceListener);
    128     }
    129 
    130     @Override
    131     void onStop() {
    132         if (DEBUG) Log.d(TAG, "onStop()");
    133 
    134         if (mServiceListener != null) {
    135             mNsdManager.stopServiceDiscovery(mServiceListener);
    136             mServiceListener = null;
    137         }
    138         mMainHandler.removeCallbacksAndMessages(null);
    139         NsdResolveQueue.getInstance(getPrintService()).clear();
    140     }
    141 
    142     /**
    143      * Manage notifications from NsdManager
    144      */
    145     private class NsdServiceListener implements NsdManager.DiscoveryListener,
    146             NsdManager.ResolveListener {
    147         @Override
    148         public void onStartDiscoveryFailed(String serviceType, int errorCode) {
    149             Log.w(TAG, "onStartDiscoveryFailed: " + errorCode);
    150             mServiceListener = null;
    151         }
    152 
    153         @Override
    154         public void onStopDiscoveryFailed(String s, int errorCode) {
    155             Log.w(TAG, "onStopDiscoveryFailed: " + errorCode);
    156         }
    157 
    158         @Override
    159         public void onDiscoveryStarted(String s) {
    160             if (DEBUG) Log.d(TAG, "onDiscoveryStarted");
    161         }
    162 
    163         @Override
    164         public void onDiscoveryStopped(String s) {
    165             if (DEBUG) Log.d(TAG, "onDiscoveryStopped");
    166 
    167             // On the main thread, notify loss of all known printers
    168             mMainHandler.post(() -> allPrintersLost());
    169         }
    170 
    171         @Override
    172         public void onServiceFound(final NsdServiceInfo info) {
    173             if (DEBUG) Log.d(TAG, "onServiceFound - " + info.getServiceName());
    174             NsdResolveQueue.getInstance(getPrintService()).resolve(mNsdManager, info, this);
    175         }
    176 
    177         @Override
    178         public void onServiceLost(final NsdServiceInfo info) {
    179             if (DEBUG) Log.d(TAG, "onServiceLost - " + info.getServiceName());
    180 
    181             // On the main thread, seek the missing printer by name and notify its loss
    182             mMainHandler.post(() -> {
    183                 for (DiscoveredPrinter printer : getPrinters()) {
    184                     if (TextUtils.equals(printer.name, info.getServiceName())) {
    185                         printerLost(printer.getUri());
    186                         return;
    187                     }
    188                 }
    189             });
    190         }
    191 
    192         @Override
    193         public void onResolveFailed(final NsdServiceInfo info, final int errorCode) {
    194         }
    195 
    196         @Override
    197         public void onServiceResolved(final NsdServiceInfo info) {
    198             final DiscoveredPrinter printer = toNetworkPrinter(info);
    199             if (DEBUG) Log.d(TAG, "Service " + info.getServiceName() + " resolved to " + printer);
    200             if (printer == null) {
    201                 return;
    202             }
    203 
    204             mMainHandler.post(() -> printerFound(printer));
    205         }
    206     }
    207 }