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 }