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.JsonReader;
29 import android.util.JsonWriter;
30 import android.util.Log;
31 
32 import com.android.bips.discovery.DiscoveredPrinter;
33 import com.android.bips.discovery.Discovery;
34 import com.android.bips.ipp.CapabilitiesCache;
35 
36 import java.io.File;
37 import java.io.FileReader;
38 import java.io.FileWriter;
39 import java.io.IOException;
40 import java.net.InetAddress;
41 import java.net.UnknownHostException;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 
50 class LocalDiscoverySession extends PrinterDiscoverySession implements Discovery.Listener,
51         PrintManager.PrintServiceRecommendationsChangeListener,
52         PrintManager.PrintServicesChangeListener {
53     private static final String TAG = LocalDiscoverySession.class.getSimpleName();
54     private static final boolean DEBUG = false;
55 
56     // Printers are removed after not being seen for this long
57     static final long PRINTER_EXPIRATION_MILLIS = 3000;
58 
59     private static final String KNOWN_GOOD_FILE = "knowngood.json";
60     private static final int KNOWN_GOOD_MAX = 50;
61 
62     private final BuiltInPrintService mPrintService;
63     private final CapabilitiesCache mCapabilitiesCache;
64     private final Map<PrinterId, LocalPrinter> mPrinters = new HashMap<>();
65     private final Set<PrinterId> mPriorityIds = new HashSet<>();
66     private final Set<PrinterId> mTrackingIds = new HashSet<>();
67     private final List<PrinterId> mKnownGood = new ArrayList<>();
68     private Runnable mExpirePrinters;
69 
70     PrintManager mPrintManager;
71 
72     /** Package names of all currently enabled print services beside this one */
73     private ArraySet<String> mEnabledServices = new ArraySet<>();
74 
75     /**
76      * Address of printers that can be handled by print services, ordered by package name of the
77      * print service. The print service might not be enabled. For that, look at
78      * {@link #mEnabledServices}.
79      *
80      * <p>This print service only shows a printer if another print service does not show it.
81      */
82     private final ArrayMap<InetAddress, ArrayList<String>> mPrintersOfOtherService =
83             new ArrayMap<>();
84 
LocalDiscoverySession(BuiltInPrintService service)85     LocalDiscoverySession(BuiltInPrintService service) {
86         mPrintService = service;
87         mCapabilitiesCache = service.getCapabilitiesCache();
88         mPrintManager = mPrintService.getSystemService(PrintManager.class);
89         loadKnownGood();
90     }
91 
92     @Override
onStartPrinterDiscovery(List<PrinterId> priorityList)93     public void onStartPrinterDiscovery(List<PrinterId> priorityList) {
94         if (DEBUG) Log.d(TAG, "onStartPrinterDiscovery() " + priorityList);
95 
96         // Replace priority IDs with the current list.
97         mPriorityIds.clear();
98         mPriorityIds.addAll(priorityList);
99 
100         // Mark all known printers as "not found". They may return shortly or may expire
101         mPrinters.values().forEach(LocalPrinter::notFound);
102         monitorExpiredPrinters();
103 
104         mPrintService.getDiscovery().start(this);
105 
106         mPrintManager.addPrintServicesChangeListener(this, null);
107         onPrintServicesChanged();
108 
109         mPrintManager.addPrintServiceRecommendationsChangeListener(this, null);
110         onPrintServiceRecommendationsChanged();
111     }
112 
113     @Override
onStopPrinterDiscovery()114     public void onStopPrinterDiscovery() {
115         if (DEBUG) Log.d(TAG, "onStopPrinterDiscovery()");
116         mPrintService.getDiscovery().stop(this);
117 
118         PrintManager printManager = mPrintService.getSystemService(PrintManager.class);
119         printManager.removePrintServicesChangeListener(this);
120         printManager.removePrintServiceRecommendationsChangeListener(this);
121 
122         if (mExpirePrinters != null) {
123             mPrintService.getMainHandler().removeCallbacks(mExpirePrinters);
124             mExpirePrinters = null;
125         }
126     }
127 
128     @Override
onValidatePrinters(List<PrinterId> printerIds)129     public void onValidatePrinters(List<PrinterId> printerIds) {
130         if (DEBUG) Log.d(TAG, "onValidatePrinters() " + printerIds);
131     }
132 
133     @Override
onStartPrinterStateTracking(final PrinterId printerId)134     public void onStartPrinterStateTracking(final PrinterId printerId) {
135         if (DEBUG) Log.d(TAG, "onStartPrinterStateTracking() " + printerId);
136         LocalPrinter localPrinter = mPrinters.get(printerId);
137         mTrackingIds.add(printerId);
138 
139         // We cannot track the printer yet; wait until it is discovered
140         if (localPrinter == null || !localPrinter.isFound()) return;
141 
142         // Immediately request a refresh of capabilities
143         localPrinter.requestCapabilities();
144     }
145 
146     @Override
onStopPrinterStateTracking(PrinterId printerId)147     public void onStopPrinterStateTracking(PrinterId printerId) {
148         if (DEBUG) Log.d(TAG, "onStopPrinterStateTracking() " + printerId.getLocalId());
149         mTrackingIds.remove(printerId);
150     }
151 
152     @Override
onDestroy()153     public void onDestroy() {
154         if (DEBUG) Log.d(TAG, "onDestroy");
155         saveKnownGood();
156     }
157 
158     /**
159      * A printer was found during discovery
160      */
161     @Override
onPrinterFound(DiscoveredPrinter discoveredPrinter)162     public void onPrinterFound(DiscoveredPrinter discoveredPrinter) {
163         if (DEBUG) Log.d(TAG, "onPrinterFound() " + discoveredPrinter);
164         if (isDestroyed()) {
165             Log.w(TAG, "Destroyed; ignoring");
166             return;
167         }
168 
169         final PrinterId printerId = discoveredPrinter.getId(mPrintService);
170         LocalPrinter localPrinter = mPrinters.get(printerId);
171         if (localPrinter == null) {
172             localPrinter = new LocalPrinter(mPrintService, this, discoveredPrinter);
173             mPrinters.put(printerId, localPrinter);
174         }
175         localPrinter.found();
176     }
177 
178     /**
179      * A printer was lost during discovery
180      */
181     @Override
onPrinterLost(DiscoveredPrinter lostPrinter)182     public void onPrinterLost(DiscoveredPrinter lostPrinter) {
183         if (DEBUG) Log.d(TAG, "onPrinterLost() " + lostPrinter);
184 
185         PrinterId printerId = lostPrinter.getId(mPrintService);
186         if (printerId.getLocalId().startsWith("ipp")) {
187             // Forget capabilities for network addresses (which are not globally unique)
188             mCapabilitiesCache.remove(lostPrinter.getUri());
189         }
190 
191         LocalPrinter localPrinter = mPrinters.get(printerId);
192         if (localPrinter == null) return;
193 
194         localPrinter.notFound();
195         handlePrinter(localPrinter);
196         monitorExpiredPrinters();
197     }
198 
monitorExpiredPrinters()199     private void monitorExpiredPrinters() {
200         if (mExpirePrinters == null && !mPrinters.isEmpty()) {
201             mExpirePrinters = new ExpirePrinters();
202             mPrintService.getMainHandler().postDelayed(mExpirePrinters, PRINTER_EXPIRATION_MILLIS);
203         }
204     }
205 
206     /** A complete printer record is available */
handlePrinter(LocalPrinter localPrinter)207     void handlePrinter(LocalPrinter localPrinter) {
208         if (localPrinter.getCapabilities() == null &&
209                 !mKnownGood.contains(localPrinter.getPrinterId())) {
210             // Ignore printers that have no capabilities and are not known-good
211             return;
212         }
213 
214         PrinterInfo info = localPrinter.createPrinterInfo();
215 
216         mKnownGood.remove(localPrinter.getPrinterId());
217 
218         if (info == null) return;
219 
220         // Update known-good database with current results.
221         if (info.getStatus() == PrinterInfo.STATUS_IDLE && localPrinter.getUuid() != null) {
222             // Mark UUID-based printers with IDLE status as known-good
223             mKnownGood.add(0, localPrinter.getPrinterId());
224         }
225 
226         if (DEBUG) {
227             Log.d(TAG, "handlePrinter: reporting " + localPrinter +
228                     " caps=" + (info.getCapabilities() != null) + " status=" + info.getStatus());
229         }
230 
231         if (!isHandledByOtherService(localPrinter)) {
232             addPrinters(Collections.singletonList(info));
233         }
234     }
235 
236     /**
237      * Return true if the {@link PrinterId} corresponds to a high-priority printer
238      */
isPriority(PrinterId printerId)239     boolean isPriority(PrinterId printerId) {
240         return mPriorityIds.contains(printerId) || mTrackingIds.contains(printerId);
241     }
242 
243     /**
244      * Return true if the {@link PrinterId} corresponds to a known printer
245      */
isKnown(PrinterId printerId)246     boolean isKnown(PrinterId printerId) {
247         return mPrinters.containsKey(printerId);
248     }
249 
250     /**
251      * Load "known good" printer IDs from storage, if possible
252      */
loadKnownGood()253     private void loadKnownGood() {
254         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
255         if (!file.exists()) return;
256         try (JsonReader reader = new JsonReader(new FileReader(file))) {
257             reader.beginArray();
258             while (reader.hasNext()) {
259                 String localId = reader.nextString();
260                 mKnownGood.add(mPrintService.generatePrinterId(localId));
261             }
262             reader.endArray();
263         } catch (IOException e) {
264             Log.w(TAG, "Failed to read known good list", e);
265         }
266     }
267 
268     /**
269      * Save "known good" printer IDs to storage, if possible
270      */
saveKnownGood()271     private void saveKnownGood() {
272         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
273         try (JsonWriter writer = new JsonWriter(new FileWriter(file))) {
274             writer.beginArray();
275             for (int i = 0; i < Math.min(KNOWN_GOOD_MAX, mKnownGood.size()); i++) {
276                 writer.value(mKnownGood.get(i).getLocalId());
277             }
278             writer.endArray();
279         } catch (IOException e) {
280             Log.w(TAG, "Failed to write known good list", e);
281         }
282     }
283 
284     /**
285      * Is this printer handled by another print service and should be suppressed?
286      *
287      * @param printer The printer that might need to be suppressed
288      *
289      * @return {@code true} iff the printer should be suppressed
290      */
isHandledByOtherService(LocalPrinter printer)291     private boolean isHandledByOtherService(LocalPrinter printer) {
292         ArrayList<String> printerServices;
293         try {
294             printerServices = mPrintersOfOtherService.get(printer.getAddress());
295         } catch (UnknownHostException e) {
296             Log.e(TAG, "Cannot resolve address for " + printer, e);
297             return false;
298         }
299 
300         if (printerServices != null) {
301             int numServices = printerServices.size();
302             for (int i = 0; i < numServices; i++) {
303                 if (mEnabledServices.contains(printerServices.get(i))) {
304                     return true;
305                 }
306             }
307         }
308 
309         return false;
310     }
311 
312     /**
313      * If the system's print service state changed some printer might be newly suppressed or not
314      * suppressed anymore.
315      */
onPrintServicesStateUpdated()316     private void onPrintServicesStateUpdated() {
317         ArrayList<PrinterInfo> printersToAdd = new ArrayList<>();
318         ArrayList<PrinterId> printersToRemove = new ArrayList<>();
319         for (LocalPrinter printer : mPrinters.values()) {
320             PrinterInfo info = printer.createPrinterInfo();
321 
322             if (printer.getCapabilities() != null && printer.isFound()
323                     && !isHandledByOtherService(printer) && info != null) {
324                 printersToAdd.add(info);
325             } else {
326                 printersToRemove.add(printer.getPrinterId());
327             }
328         }
329 
330         removePrinters(printersToRemove);
331         addPrinters(printersToAdd);
332     }
333 
334     @Override
onPrintServiceRecommendationsChanged()335     public void onPrintServiceRecommendationsChanged() {
336         mPrintersOfOtherService.clear();
337 
338         List<RecommendationInfo> infos = mPrintManager.getPrintServiceRecommendations();
339 
340         int numInfos = infos.size();
341         for (int i = 0; i < numInfos; i++) {
342             RecommendationInfo info = infos.get(i);
343             String packageName = info.getPackageName().toString();
344 
345             if (!packageName.equals(mPrintService.getPackageName())) {
346                 for (InetAddress address : info.getDiscoveredPrinters()) {
347                     ArrayList<String> services = mPrintersOfOtherService.get(address);
348 
349                     if (services == null) {
350                         services = new ArrayList<>(1);
351                         mPrintersOfOtherService.put(address, services);
352                     }
353 
354                     services.add(packageName);
355                 }
356             }
357         }
358 
359         onPrintServicesStateUpdated();
360     }
361 
362     @Override
onPrintServicesChanged()363     public void onPrintServicesChanged() {
364         mEnabledServices.clear();
365 
366         List<PrintServiceInfo> infos = mPrintManager.getPrintServices(
367                 PrintManager.ENABLED_SERVICES);
368 
369         int numInfos = infos.size();
370         for (int i = 0; i < numInfos; i++) {
371             PrintServiceInfo info = infos.get(i);
372             String packageName = info.getComponentName().getPackageName();
373 
374             if (!packageName.equals(mPrintService.getPackageName())) {
375                 mEnabledServices.add(packageName);
376             }
377         }
378 
379         onPrintServicesStateUpdated();
380     }
381 
382     /** A runnable that periodically removes expired printers, when any exist */
383     private class ExpirePrinters implements Runnable {
384         @Override
run()385         public void run() {
386             boolean allFound = true;
387             List<PrinterId> idsToRemove = new ArrayList<>();
388 
389             for (LocalPrinter localPrinter : mPrinters.values()) {
390                 if (localPrinter.isExpired()) {
391                     if (DEBUG) Log.d(TAG, "Expiring " + localPrinter);
392                     idsToRemove.add(localPrinter.getPrinterId());
393                 }
394                 if (!localPrinter.isFound()) allFound = false;
395             }
396             idsToRemove.forEach(mPrinters::remove);
397             removePrinters(idsToRemove);
398             if (!allFound) {
399                 mPrintService.getMainHandler().postDelayed(this, PRINTER_EXPIRATION_MILLIS);
400             } else {
401                 mExpirePrinters = null;
402             }
403         }
404     }
405 }