1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.printservice.recommendation.plugin.mdnsFilter;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.StringRes;
22import android.content.Context;
23import android.net.nsd.NsdManager;
24import android.net.nsd.NsdServiceInfo;
25import android.util.Log;
26import com.android.internal.annotations.GuardedBy;
27import com.android.internal.util.Preconditions;
28import com.android.printservice.recommendation.PrintServicePlugin;
29import com.android.printservice.recommendation.util.DiscoveryListenerMultiplexer;
30import com.android.printservice.recommendation.util.NsdResolveQueue;
31
32import java.util.HashSet;
33import java.util.List;
34
35/**
36 * A plugin listening for mDNS results and only adding the ones that {@link
37 * MDNSUtils#isVendorPrinter match} configured list
38 */
39public class MDNSFilterPlugin implements PrintServicePlugin, NsdManager.DiscoveryListener {
40    private static final String LOG_TAG = "MDNSFilterPlugin";
41
42    private static final String PRINTER_SERVICE_TYPE = "_ipp._tcp";
43
44    /** Name of the print service this plugin is for */
45    private final @StringRes int mName;
46
47    /** Package name of the print service this plugin is for */
48    private final @NonNull CharSequence mPackageName;
49
50    /** mDNS names handled by the print service this plugin is for */
51    private final @NonNull HashSet<String> mMDNSNames;
52
53    /** Printer identifiers of the mPrinters found. */
54    @GuardedBy("mLock")
55    private final @NonNull HashSet<String> mPrinters;
56
57    /** Context of the user of this plugin */
58    private final @NonNull Context mContext;
59
60    /**
61     * Call back to report the number of mPrinters found.
62     *
63     * We assume that {@link #start} and {@link #stop} are never called in parallel, hence it is
64     * safe to not synchronize access to this field.
65     */
66    private @Nullable PrinterDiscoveryCallback mCallback;
67
68    /** Queue used to resolve nsd infos */
69    private final @NonNull NsdResolveQueue mResolveQueue;
70
71    /**
72     * Create new stub that assumes that a print service can be used to print on all mPrinters
73     * matching some mDNS names.
74     *
75     * @param context     The context the plugin runs in
76     * @param name        The user friendly name of the print service
77     * @param packageName The package name of the print service
78     * @param mDNSNames   The mDNS names of the printer.
79     */
80    public MDNSFilterPlugin(@NonNull Context context, @NonNull String name,
81            @NonNull CharSequence packageName, @NonNull List<String> mDNSNames) {
82        mContext = Preconditions.checkNotNull(context, "context");
83        mName = mContext.getResources().getIdentifier(Preconditions.checkStringNotEmpty(name,
84                "name"), null, "com.android.printservice.recommendation");
85        mPackageName = Preconditions.checkStringNotEmpty(packageName);
86        mMDNSNames = new HashSet<>(Preconditions
87                .checkCollectionNotEmpty(Preconditions.checkCollectionElementsNotNull(mDNSNames,
88                        "mDNSNames"), "mDNSNames"));
89
90        mResolveQueue = NsdResolveQueue.getInstance();
91        mPrinters = new HashSet<>();
92    }
93
94    @Override
95    public @NonNull CharSequence getPackageName() {
96        return mPackageName;
97    }
98
99    /**
100     * @return The NDS manager
101     */
102    private NsdManager getNDSManager() {
103        return (NsdManager) mContext.getSystemService(Context.NSD_SERVICE);
104    }
105
106    @Override
107    public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
108        mCallback = callback;
109
110        DiscoveryListenerMultiplexer.addListener(getNDSManager(), PRINTER_SERVICE_TYPE, this);
111    }
112
113    @Override
114    public @StringRes int getName() {
115        return mName;
116    }
117
118    @Override
119    public void stop() throws Exception {
120        mCallback.onChanged(0);
121        mCallback = null;
122
123        DiscoveryListenerMultiplexer.removeListener(getNDSManager(), this);
124    }
125
126    @Override
127    public void onStartDiscoveryFailed(String serviceType, int errorCode) {
128        Log.w(LOG_TAG, "Failed to start network discovery for type " + serviceType + ": "
129                + errorCode);
130    }
131
132    @Override
133    public void onStopDiscoveryFailed(String serviceType, int errorCode) {
134        Log.w(LOG_TAG, "Failed to stop network discovery for type " + serviceType + ": "
135                + errorCode);
136    }
137
138    @Override
139    public void onDiscoveryStarted(String serviceType) {
140        // empty
141    }
142
143    @Override
144    public void onDiscoveryStopped(String serviceType) {
145        mPrinters.clear();
146    }
147
148    @Override
149    public void onServiceFound(NsdServiceInfo serviceInfo) {
150        mResolveQueue.resolve(getNDSManager(), serviceInfo,
151                new NsdManager.ResolveListener() {
152            @Override
153            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
154                Log.w(LOG_TAG, "Service found: could not resolve " + serviceInfo + ": " +
155                        errorCode);
156            }
157
158            @Override
159            public void onServiceResolved(NsdServiceInfo serviceInfo) {
160                if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
161                    if (mCallback != null) {
162                        boolean added = mPrinters.add(serviceInfo.getHost().getHostAddress());
163
164                        if (added) {
165                            mCallback.onChanged(mPrinters.size());
166                        }
167                    }
168                }
169            }
170        });
171    }
172
173    @Override
174    public void onServiceLost(NsdServiceInfo serviceInfo) {
175        mResolveQueue.resolve(getNDSManager(), serviceInfo,
176                new NsdManager.ResolveListener() {
177            @Override
178            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
179                Log.w(LOG_TAG, "Service lost: Could not resolve " + serviceInfo + ": "
180                        + errorCode);
181            }
182
183            @Override
184            public void onServiceResolved(NsdServiceInfo serviceInfo) {
185                if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
186                    if (mCallback != null) {
187                        boolean removed = mPrinters
188                                .remove(serviceInfo.getHost().getHostAddress());
189
190                        if (removed) {
191                            mCallback.onChanged(mPrinters.size());
192                        }
193                    }
194                }
195            }
196        });
197    }
198}
199