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 }