• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         for (PrinterInfo knownInfo : getPrinters()) {
227             if (knownInfo.getId().equals(info.getId()) && (info.getCapabilities() == null)) {
228                 if (DEBUG) Log.d(TAG, "Ignore update with no caps " + localPrinter);
229                 return;
230             }
231         }
232 
233         if (DEBUG) {
234             Log.d(TAG, "handlePrinter: reporting " + localPrinter +
235                     " caps=" + (info.getCapabilities() != null) + " status=" + info.getStatus());
236         }
237 
238         if (!isHandledByOtherService(localPrinter)) {
239             addPrinters(Collections.singletonList(info));
240         }
241     }
242 
243     /**
244      * Return true if the {@link PrinterId} corresponds to a high-priority printer
245      */
isPriority(PrinterId printerId)246     boolean isPriority(PrinterId printerId) {
247         return mTrackingIds.contains(printerId);
248     }
249 
250     /**
251      * Return true if the {@link PrinterId} corresponds to a known printer
252      */
isKnown(PrinterId printerId)253     boolean isKnown(PrinterId printerId) {
254         return mPrinters.containsKey(printerId);
255     }
256 
257     /**
258      * Load "known good" printer IDs from storage, if possible
259      */
loadKnownGood()260     private void loadKnownGood() {
261         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
262         if (!file.exists()) return;
263         try (JsonReader reader = new JsonReader(new FileReader(file))) {
264             reader.beginArray();
265             while (reader.hasNext()) {
266                 String localId = reader.nextString();
267                 mKnownGood.add(mPrintService.generatePrinterId(localId));
268             }
269             reader.endArray();
270         } catch (IOException e) {
271             Log.w(TAG, "Failed to read known good list", e);
272         }
273     }
274 
275     /**
276      * Save "known good" printer IDs to storage, if possible
277      */
saveKnownGood()278     private void saveKnownGood() {
279         File file = new File(mPrintService.getCacheDir(), KNOWN_GOOD_FILE);
280         try (JsonWriter writer = new JsonWriter(new FileWriter(file))) {
281             writer.beginArray();
282             for (int i = 0; i < Math.min(KNOWN_GOOD_MAX, mKnownGood.size()); i++) {
283                 writer.value(mKnownGood.get(i).getLocalId());
284             }
285             writer.endArray();
286         } catch (IOException e) {
287             Log.w(TAG, "Failed to write known good list", e);
288         }
289     }
290 
291     /**
292      * Is this printer handled by another print service and should be suppressed?
293      *
294      * @param printer The printer that might need to be suppressed
295      *
296      * @return {@code true} iff the printer should be suppressed
297      */
isHandledByOtherService(LocalPrinter printer)298     private boolean isHandledByOtherService(LocalPrinter printer) {
299         InetAddress address = printer.getAddress();
300         if (address == null) return false;
301 
302         ArrayList<String> printerServices = mPrintersOfOtherService.get(printer.getAddress());
303 
304         if (printerServices != null) {
305             int numServices = printerServices.size();
306             for (int i = 0; i < numServices; i++) {
307                 if (mEnabledServices.contains(printerServices.get(i))) {
308                     return true;
309                 }
310             }
311         }
312 
313         return false;
314     }
315 
316     /**
317      * If the system's print service state changed some printer might be newly suppressed or not
318      * suppressed anymore.
319      */
onPrintServicesStateUpdated()320     private void onPrintServicesStateUpdated() {
321         ArrayList<PrinterInfo> printersToAdd = new ArrayList<>();
322         ArrayList<PrinterId> printersToRemove = new ArrayList<>();
323         for (LocalPrinter printer : mPrinters.values()) {
324             PrinterInfo info = printer.createPrinterInfo();
325 
326             if (printer.getCapabilities() != null && printer.isFound()
327                     && !isHandledByOtherService(printer) && info != null) {
328                 printersToAdd.add(info);
329             } else {
330                 printersToRemove.add(printer.getPrinterId());
331             }
332         }
333 
334         removePrinters(printersToRemove);
335         addPrinters(printersToAdd);
336     }
337 
338     @Override
onPrintServiceRecommendationsChanged()339     public void onPrintServiceRecommendationsChanged() {
340         mPrintersOfOtherService.clear();
341 
342         List<RecommendationInfo> infos = mPrintManager.getPrintServiceRecommendations();
343 
344         int numInfos = infos.size();
345         for (int i = 0; i < numInfos; i++) {
346             RecommendationInfo info = infos.get(i);
347             String packageName = info.getPackageName().toString();
348 
349             if (!packageName.equals(mPrintService.getPackageName())) {
350                 for (InetAddress address : info.getDiscoveredPrinters()) {
351                     ArrayList<String> services = mPrintersOfOtherService.get(address);
352 
353                     if (services == null) {
354                         services = new ArrayList<>(1);
355                         mPrintersOfOtherService.put(address, services);
356                     }
357 
358                     services.add(packageName);
359                 }
360             }
361         }
362 
363         onPrintServicesStateUpdated();
364     }
365 
366     @Override
onPrintServicesChanged()367     public void onPrintServicesChanged() {
368         mEnabledServices.clear();
369 
370         List<PrintServiceInfo> infos = mPrintManager.getPrintServices(
371                 PrintManager.ENABLED_SERVICES);
372 
373         int numInfos = infos.size();
374         for (int i = 0; i < numInfos; i++) {
375             PrintServiceInfo info = infos.get(i);
376             String packageName = info.getComponentName().getPackageName();
377 
378             if (!packageName.equals(mPrintService.getPackageName())) {
379                 mEnabledServices.add(packageName);
380             }
381         }
382 
383         onPrintServicesStateUpdated();
384     }
385 
386     /** A runnable that periodically removes expired printers, when any exist */
387     private class ExpirePrinters implements Runnable {
388         @Override
run()389         public void run() {
390             boolean allFound = true;
391             List<PrinterId> idsToRemove = new ArrayList<>();
392 
393             for (LocalPrinter localPrinter : mPrinters.values()) {
394                 if (localPrinter.isExpired()) {
395                     if (DEBUG) Log.d(TAG, "Expiring " + localPrinter);
396                     idsToRemove.add(localPrinter.getPrinterId());
397                 }
398                 if (!localPrinter.isFound()) allFound = false;
399             }
400             idsToRemove.forEach(mPrinters::remove);
401             removePrinters(idsToRemove);
402             if (!allFound) {
403                 mPrintService.getMainHandler().postDelayed(this, PRINTER_EXPIRATION_MILLIS);
404             } else {
405                 mExpirePrinters = null;
406             }
407         }
408     }
409 }