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
18package com.android.bips.discovery;
19
20import android.content.Context;
21import android.net.Uri;
22import android.net.nsd.NsdManager;
23import android.net.nsd.NsdServiceInfo;
24import android.os.Handler;
25import android.text.TextUtils;
26import android.util.Log;
27
28import com.android.bips.BuiltInPrintService;
29
30import java.net.Inet4Address;
31import java.util.Locale;
32import java.util.Map;
33
34/**
35 * Search the local network for devices advertising IPP print services
36 */
37public 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}