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;
19 
20 import android.print.PrintManager;
21 import android.print.PrinterId;
22 import android.print.PrinterInfo;
23 import android.printservice.PrintServiceInfo;
24 import android.printservice.PrinterDiscoverySession;
25 import android.printservice.recommendation.RecommendationInfo;
26 import android.util.ArrayMap;
27 import android.util.ArraySet;
28 import android.util.Log;
29 
30 import com.android.bips.discovery.DiscoveredPrinter;
31 import com.android.bips.discovery.Discovery;
32 
33 import java.net.InetAddress;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Set;
41 
42 class LocalDiscoverySession extends PrinterDiscoverySession implements Discovery.Listener,
43         PrintManager.PrintServiceRecommendationsChangeListener,
44         PrintManager.PrintServicesChangeListener {
45     private static final String TAG = LocalDiscoverySession.class.getSimpleName();
46     private static final boolean DEBUG = false;
47 
48     // Printers are removed after not being seen for this long
49     static final int PRINTER_EXPIRATION_MILLIS = 3000;
50 
51     private final BuiltInPrintService mPrintService;
52     private final Map<PrinterId, LocalPrinter> mPrinters = new HashMap<>();
53     private final Set<PrinterId> mTrackingIds = new HashSet<>();
54     private final LocalDiscoverySessionInfo mInfo;
55     private DelayedAction mExpirePrinters;
56     private PrintManager mPrintManager;
57 
58     /** Package names of all currently enabled print services beside this one */
59     private ArraySet<String> mEnabledServices = new ArraySet<>();
60 
61     /**
62      * Address of printers that can be handled by print services, ordered by package name of the
63      * print service. The print service might not be enabled. For that, look at
64      * {@link #mEnabledServices}.
65      *
66      * <p>This print service only shows a printer if another print service does not show it.
67      */
68     private final ArrayMap<InetAddress, ArrayList<String>> mPrintersOfOtherService =
69             new ArrayMap<>();
70 
LocalDiscoverySession(BuiltInPrintService service)71     LocalDiscoverySession(BuiltInPrintService service) {
72         mPrintService = service;
73         mPrintManager = mPrintService.getSystemService(PrintManager.class);
74         mInfo = new LocalDiscoverySessionInfo(service);
75     }
76 
77     @Override
onStartPrinterDiscovery(List<PrinterId> priorityList)78     public void onStartPrinterDiscovery(List<PrinterId> priorityList) {
79         if (DEBUG) Log.d(TAG, "onStartPrinterDiscovery() " + priorityList);
80 
81         // Mark all known printers as "not found". They may return shortly or may expire
82         for (LocalPrinter printer : mPrinters.values()) {
83             printer.notFound();
84         }
85         monitorExpiredPrinters();
86 
87         mPrintService.getDiscovery().start(this);
88 
89         mPrintManager.addPrintServicesChangeListener(this, null);
90         onPrintServicesChanged();
91 
92         mPrintManager.addPrintServiceRecommendationsChangeListener(this, null);
93         onPrintServiceRecommendationsChanged();
94     }
95 
96     @Override
onStopPrinterDiscovery()97     public void onStopPrinterDiscovery() {
98         if (DEBUG) Log.d(TAG, "onStopPrinterDiscovery()");
99         mPrintService.getDiscovery().stop(this);
100 
101         PrintManager printManager = mPrintService.getSystemService(PrintManager.class);
102         printManager.removePrintServicesChangeListener(this);
103         printManager.removePrintServiceRecommendationsChangeListener(this);
104 
105         if (mExpirePrinters != null) {
106             mExpirePrinters.cancel();
107             mExpirePrinters = null;
108         }
109     }
110 
111     @Override
onValidatePrinters(List<PrinterId> printerIds)112     public void onValidatePrinters(List<PrinterId> printerIds) {
113         if (DEBUG) Log.d(TAG, "onValidatePrinters() " + printerIds);
114     }
115 
116     @Override
onStartPrinterStateTracking(final PrinterId printerId)117     public void onStartPrinterStateTracking(final PrinterId printerId) {
118         if (DEBUG) Log.d(TAG, "onStartPrinterStateTracking() " + printerId);
119         LocalPrinter localPrinter = mPrinters.get(printerId);
120         mTrackingIds.add(printerId);
121 
122         // We cannot track the printer yet; wait until it is discovered
123         if (localPrinter == null || !localPrinter.isFound()) {
124             return;
125         }
126         localPrinter.track();
127     }
128 
129     @Override
onStopPrinterStateTracking(PrinterId printerId)130     public void onStopPrinterStateTracking(PrinterId printerId) {
131         if (DEBUG) Log.d(TAG, "onStopPrinterStateTracking() " + printerId.getLocalId());
132         LocalPrinter localPrinter = mPrinters.get(printerId);
133         if (localPrinter != null) {
134             localPrinter.stopTracking();
135         }
136         mTrackingIds.remove(printerId);
137     }
138 
139     @Override
onDestroy()140     public void onDestroy() {
141         if (DEBUG) Log.d(TAG, "onDestroy");
142         mInfo.save();
143     }
144 
145     /**
146      * A printer was found during discovery
147      */
148     @Override
onPrinterFound(DiscoveredPrinter discoveredPrinter)149     public void onPrinterFound(DiscoveredPrinter discoveredPrinter) {
150         if (DEBUG) Log.d(TAG, "onPrinterFound() " + discoveredPrinter);
151         if (isDestroyed()) {
152             Log.w(TAG, "Destroyed; ignoring");
153             return;
154         }
155 
156         PrinterId printerId = discoveredPrinter.getId(mPrintService);
157         LocalPrinter localPrinter = mPrinters.computeIfAbsent(printerId,
158                 id -> new LocalPrinter(mPrintService, this, discoveredPrinter));
159 
160         localPrinter.found(discoveredPrinter);
161         if (mTrackingIds.contains(printerId)) {
162             localPrinter.track();
163         }
164     }
165 
166     /**
167      * A printer was lost during discovery
168      */
169     @Override
onPrinterLost(DiscoveredPrinter lostPrinter)170     public void onPrinterLost(DiscoveredPrinter lostPrinter) {
171         if (DEBUG) Log.d(TAG, "onPrinterLost() " + lostPrinter);
172 
173         mPrintService.getCapabilitiesCache().remove(lostPrinter.path);
174 
175         PrinterId printerId = lostPrinter.getId(mPrintService);
176         LocalPrinter localPrinter = mPrinters.get(printerId);
177         if (localPrinter == null) {
178             return;
179         }
180 
181         localPrinter.notFound();
182         handlePrinter(localPrinter);
183         monitorExpiredPrinters();
184     }
185 
monitorExpiredPrinters()186     private void monitorExpiredPrinters() {
187         if (mExpirePrinters == null && !mPrinters.isEmpty()) {
188             mExpirePrinters = mPrintService.delay(PRINTER_EXPIRATION_MILLIS, () -> {
189                 mExpirePrinters = null;
190                 boolean allFound = true;
191                 List<PrinterId> idsToRemove = new ArrayList<>();
192 
193                 for (LocalPrinter localPrinter : mPrinters.values()) {
194                     if (localPrinter.isExpired()) {
195                         if (DEBUG) Log.d(TAG, "Expiring " + localPrinter);
196                         idsToRemove.add(localPrinter.getPrinterId());
197                     }
198                     if (!localPrinter.isFound()) {
199                         allFound = false;
200                     }
201                 }
202                 for (PrinterId id : idsToRemove) {
203                     mPrinters.remove(id);
204                 }
205                 removePrinters(idsToRemove);
206                 if (!allFound) {
207                     monitorExpiredPrinters();
208                 }
209             });
210         }
211     }
212 
213     /** A complete printer record is available */
handlePrinter(LocalPrinter localPrinter)214     void handlePrinter(LocalPrinter localPrinter) {
215         if (DEBUG) Log.d(TAG, "handlePrinter record " + localPrinter);
216 
217         boolean knownGood = mInfo.isKnownGood(localPrinter.getPrinterId());
218         PrinterInfo info = localPrinter.createPrinterInfo(knownGood);
219         if (info == null) {
220             return;
221         }
222 
223         if (info.getStatus() == PrinterInfo.STATUS_IDLE && localPrinter.getUuid() != null) {
224             // Mark UUID-based printers with IDLE status as known-good
225             mInfo.setKnownGood(localPrinter.getPrinterId());
226         }
227 
228         for (PrinterInfo knownInfo : getPrinters()) {
229             if (knownInfo.getId().equals(info.getId()) && (info.getCapabilities() == null)) {
230                 if (DEBUG) Log.d(TAG, "Ignore update with no caps " + localPrinter);
231                 return;
232             }
233         }
234 
235         if (DEBUG) {
236             Log.d(TAG, "handlePrinter: reporting " + localPrinter
237                     + " caps=" + (info.getCapabilities() != null) + " status=" + info.getStatus()
238                     + " summary=" + info.getDescription());
239         }
240 
241         if (!isHandledByOtherService(localPrinter)) {
242             addPrinters(Collections.singletonList(info));
243         }
244     }
245 
246     /**
247      * Return true if the {@link PrinterId} corresponds to a high-priority printer
248      */
isPriority(PrinterId printerId)249     boolean isPriority(PrinterId printerId) {
250         return mTrackingIds.contains(printerId);
251     }
252 
253     /**
254      * Return true if the {@link PrinterId} corresponds to a known printer
255      */
isKnown(PrinterId printerId)256     boolean isKnown(PrinterId printerId) {
257         return mPrinters.containsKey(printerId);
258     }
259 
260     /**
261      * Is this printer handled by another print service and should be suppressed?
262      *
263      * @param printer The printer that might need to be suppressed
264      *
265      * @return {@code true} iff the printer should be suppressed
266      */
isHandledByOtherService(LocalPrinter printer)267     private boolean isHandledByOtherService(LocalPrinter printer) {
268         InetAddress address = printer.getAddress();
269         if (address == null) {
270             return false;
271         }
272 
273         ArrayList<String> printerServices = mPrintersOfOtherService.get(printer.getAddress());
274 
275         if (printerServices != null) {
276             int numServices = printerServices.size();
277             for (int i = 0; i < numServices; i++) {
278                 if (mEnabledServices.contains(printerServices.get(i))) {
279                     return true;
280                 }
281             }
282         }
283 
284         return false;
285     }
286 
287     /**
288      * If the system's print service state changed some printer might be newly suppressed or not
289      * suppressed anymore.
290      */
onPrintServicesStateUpdated()291     private void onPrintServicesStateUpdated() {
292         ArrayList<PrinterInfo> printersToAdd = new ArrayList<>();
293         ArrayList<PrinterId> printersToRemove = new ArrayList<>();
294         for (LocalPrinter printer : mPrinters.values()) {
295             boolean knownGood = mInfo.isKnownGood(printer.getPrinterId());
296             PrinterInfo info = printer.createPrinterInfo(knownGood);
297 
298             if (printer.getCapabilities() != null && printer.isFound()
299                     && !isHandledByOtherService(printer) && info != null) {
300                 printersToAdd.add(info);
301             } else {
302                 printersToRemove.add(printer.getPrinterId());
303             }
304         }
305 
306         removePrinters(printersToRemove);
307         addPrinters(printersToAdd);
308     }
309 
310     @Override
onPrintServiceRecommendationsChanged()311     public void onPrintServiceRecommendationsChanged() {
312         mPrintersOfOtherService.clear();
313 
314         List<RecommendationInfo> infos = mPrintManager.getPrintServiceRecommendations();
315 
316         int numInfos = infos.size();
317         for (int i = 0; i < numInfos; i++) {
318             RecommendationInfo info = infos.get(i);
319             String packageName = info.getPackageName().toString();
320 
321             if (!packageName.equals(mPrintService.getPackageName())) {
322                 for (InetAddress address : info.getDiscoveredPrinters()) {
323                     ArrayList<String> services = mPrintersOfOtherService.get(address);
324 
325                     if (services == null) {
326                         services = new ArrayList<>(1);
327                         mPrintersOfOtherService.put(address, services);
328                     }
329 
330                     services.add(packageName);
331                 }
332             }
333         }
334 
335         onPrintServicesStateUpdated();
336     }
337 
338     @Override
onPrintServicesChanged()339     public void onPrintServicesChanged() {
340         mEnabledServices.clear();
341 
342         List<PrintServiceInfo> infos = mPrintManager.getPrintServices(
343                 PrintManager.ENABLED_SERVICES);
344 
345         int numInfos = infos.size();
346         for (int i = 0; i < numInfos; i++) {
347             PrintServiceInfo info = infos.get(i);
348             String packageName = info.getComponentName().getPackageName();
349 
350             if (!packageName.equals(mPrintService.getPackageName())) {
351                 mEnabledServices.add(packageName);
352             }
353         }
354 
355         onPrintServicesStateUpdated();
356     }
357 }
358