1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.printspooler; 18 19import android.content.ComponentName; 20import android.content.Context; 21import android.content.Loader; 22import android.content.pm.ServiceInfo; 23import android.os.AsyncTask; 24import android.print.PrintManager; 25import android.print.PrinterDiscoverySession; 26import android.print.PrinterDiscoverySession.OnPrintersChangeListener; 27import android.print.PrinterId; 28import android.print.PrinterInfo; 29import android.printservice.PrintServiceInfo; 30import android.util.ArrayMap; 31import android.util.ArraySet; 32import android.util.AtomicFile; 33import android.util.Log; 34import android.util.Slog; 35import android.util.Xml; 36 37import com.android.internal.util.FastXmlSerializer; 38 39import org.xmlpull.v1.XmlPullParser; 40import org.xmlpull.v1.XmlPullParserException; 41import org.xmlpull.v1.XmlSerializer; 42 43import java.io.File; 44import java.io.FileInputStream; 45import java.io.FileNotFoundException; 46import java.io.FileOutputStream; 47import java.io.IOException; 48import java.util.ArrayList; 49import java.util.Collections; 50import java.util.List; 51import java.util.Map; 52import java.util.Set; 53 54import libcore.io.IoUtils; 55 56/** 57 * This class is responsible for loading printers by doing discovery 58 * and merging the discovered printers with the previously used ones. 59 */ 60public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { 61 private static final String LOG_TAG = "FusedPrintersProvider"; 62 63 private static final boolean DEBUG = false; 64 65 private static final double WEIGHT_DECAY_COEFFICIENT = 0.95f; 66 private static final int MAX_HISTORY_LENGTH = 50; 67 68 private static final int MAX_FAVORITE_PRINTER_COUNT = 4; 69 70 private final List<PrinterInfo> mPrinters = 71 new ArrayList<PrinterInfo>(); 72 73 private final List<PrinterInfo> mFavoritePrinters = 74 new ArrayList<PrinterInfo>(); 75 76 private final PersistenceManager mPersistenceManager; 77 78 private PrinterDiscoverySession mDiscoverySession; 79 80 private PrinterId mTrackedPrinter; 81 82 public FusedPrintersProvider(Context context) { 83 super(context); 84 mPersistenceManager = new PersistenceManager(context); 85 } 86 87 public void addHistoricalPrinter(PrinterInfo printer) { 88 mPersistenceManager.addPrinterAndWritePrinterHistory(printer); 89 } 90 91 private void computeAndDeliverResult(Map<PrinterId, PrinterInfo> discoveredPrinters) { 92 List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); 93 94 // Add the updated favorite printers. 95 final int favoritePrinterCount = mFavoritePrinters.size(); 96 for (int i = 0; i < favoritePrinterCount; i++) { 97 PrinterInfo favoritePrinter = mFavoritePrinters.get(i); 98 PrinterInfo updatedPrinter = discoveredPrinters.remove( 99 favoritePrinter.getId()); 100 if (updatedPrinter != null) { 101 printers.add(updatedPrinter); 102 } else { 103 printers.add(favoritePrinter); 104 } 105 } 106 107 // Add other updated printers. 108 final int printerCount = mPrinters.size(); 109 for (int i = 0; i < printerCount; i++) { 110 PrinterInfo printer = mPrinters.get(i); 111 PrinterInfo updatedPrinter = discoveredPrinters.remove( 112 printer.getId()); 113 if (updatedPrinter != null) { 114 printers.add(updatedPrinter); 115 } 116 } 117 118 // Add the new printers, i.e. what is left. 119 printers.addAll(discoveredPrinters.values()); 120 121 // Update the list of printers. 122 mPrinters.clear(); 123 mPrinters.addAll(printers); 124 125 if (isStarted()) { 126 // Deliver the printers. 127 deliverResult(printers); 128 } 129 } 130 131 @Override 132 protected void onStartLoading() { 133 if (DEBUG) { 134 Log.i(LOG_TAG, "onStartLoading() " + FusedPrintersProvider.this.hashCode()); 135 } 136 // The contract is that if we already have a valid, 137 // result the we have to deliver it immediately. 138 if (!mPrinters.isEmpty()) { 139 deliverResult(new ArrayList<PrinterInfo>(mPrinters)); 140 } 141 // Always load the data to ensure discovery period is 142 // started and to make sure obsolete printers are updated. 143 onForceLoad(); 144 } 145 146 @Override 147 protected void onStopLoading() { 148 if (DEBUG) { 149 Log.i(LOG_TAG, "onStopLoading() " + FusedPrintersProvider.this.hashCode()); 150 } 151 onCancelLoad(); 152 } 153 154 @Override 155 protected void onForceLoad() { 156 if (DEBUG) { 157 Log.i(LOG_TAG, "onForceLoad() " + FusedPrintersProvider.this.hashCode()); 158 } 159 loadInternal(); 160 } 161 162 private void loadInternal() { 163 if (mDiscoverySession == null) { 164 PrintManager printManager = (PrintManager) getContext() 165 .getSystemService(Context.PRINT_SERVICE); 166 mDiscoverySession = printManager.createPrinterDiscoverySession(); 167 mPersistenceManager.readPrinterHistory(); 168 } 169 if (mPersistenceManager.isReadHistoryCompleted() 170 && !mDiscoverySession.isPrinterDiscoveryStarted()) { 171 mDiscoverySession.setOnPrintersChangeListener(new OnPrintersChangeListener() { 172 @Override 173 public void onPrintersChanged() { 174 if (DEBUG) { 175 Log.i(LOG_TAG, "onPrintersChanged() count:" 176 + mDiscoverySession.getPrinters().size() 177 + " " + FusedPrintersProvider.this.hashCode()); 178 } 179 updatePrinters(mDiscoverySession.getPrinters()); 180 } 181 }); 182 final int favoriteCount = mFavoritePrinters.size(); 183 List<PrinterId> printerIds = new ArrayList<PrinterId>(favoriteCount); 184 for (int i = 0; i < favoriteCount; i++) { 185 printerIds.add(mFavoritePrinters.get(i).getId()); 186 } 187 mDiscoverySession.startPrinterDisovery(printerIds); 188 List<PrinterInfo> printers = mDiscoverySession.getPrinters(); 189 if (!printers.isEmpty()) { 190 updatePrinters(printers); 191 } 192 } 193 } 194 195 private void updatePrinters(List<PrinterInfo> printers) { 196 if (mPrinters.equals(printers)) { 197 return; 198 } 199 ArrayMap<PrinterId, PrinterInfo> printersMap = 200 new ArrayMap<PrinterId, PrinterInfo>(); 201 final int printerCount = printers.size(); 202 for (int i = 0; i < printerCount; i++) { 203 PrinterInfo printer = printers.get(i); 204 printersMap.put(printer.getId(), printer); 205 } 206 computeAndDeliverResult(printersMap); 207 } 208 209 @Override 210 protected boolean onCancelLoad() { 211 if (DEBUG) { 212 Log.i(LOG_TAG, "onCancelLoad() " + FusedPrintersProvider.this.hashCode()); 213 } 214 return cancelInternal(); 215 } 216 217 private boolean cancelInternal() { 218 if (mDiscoverySession != null 219 && mDiscoverySession.isPrinterDiscoveryStarted()) { 220 if (mTrackedPrinter != null) { 221 mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter); 222 mTrackedPrinter = null; 223 } 224 mDiscoverySession.stopPrinterDiscovery(); 225 return true; 226 } else if (mPersistenceManager.isReadHistoryInProgress()) { 227 return mPersistenceManager.stopReadPrinterHistory(); 228 } 229 return false; 230 } 231 232 @Override 233 protected void onReset() { 234 if (DEBUG) { 235 Log.i(LOG_TAG, "onReset() " + FusedPrintersProvider.this.hashCode()); 236 } 237 onStopLoading(); 238 mPrinters.clear(); 239 if (mDiscoverySession != null) { 240 mDiscoverySession.destroy(); 241 mDiscoverySession = null; 242 } 243 } 244 245 @Override 246 protected void onAbandon() { 247 if (DEBUG) { 248 Log.i(LOG_TAG, "onAbandon() " + FusedPrintersProvider.this.hashCode()); 249 } 250 onStopLoading(); 251 } 252 253 public void setTrackedPrinter(PrinterId printerId) { 254 if (isStarted() && mDiscoverySession != null 255 && mDiscoverySession.isPrinterDiscoveryStarted()) { 256 if (mTrackedPrinter != null) { 257 if (mTrackedPrinter.equals(printerId)) { 258 return; 259 } 260 mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter); 261 } 262 mTrackedPrinter = printerId; 263 mDiscoverySession.startPrinterStateTracking(printerId); 264 } 265 } 266 267 private final class PersistenceManager { 268 private static final String PERSIST_FILE_NAME = "printer_history.xml"; 269 270 private static final String TAG_PRINTERS = "printers"; 271 272 private static final String TAG_PRINTER = "printer"; 273 private static final String TAG_PRINTER_ID = "printerId"; 274 275 private static final String ATTR_LOCAL_ID = "localId"; 276 private static final String ATTR_SERVICE_NAME = "serviceName"; 277 278 private static final String ATTR_NAME = "name"; 279 private static final String ATTR_DESCRIPTION = "description"; 280 private static final String ATTR_STATUS = "status"; 281 282 private final AtomicFile mStatePersistFile; 283 284 private List<PrinterInfo> mHistoricalPrinters; 285 286 private boolean mReadHistoryCompleted; 287 private boolean mReadHistoryInProgress; 288 289 private ReadTask mReadTask; 290 291 private PersistenceManager(Context context) { 292 mStatePersistFile = new AtomicFile(new File(context.getFilesDir(), 293 PERSIST_FILE_NAME)); 294 } 295 296 public boolean isReadHistoryInProgress() { 297 return mReadHistoryInProgress; 298 } 299 300 public boolean isReadHistoryCompleted() { 301 return mReadHistoryCompleted; 302 } 303 304 public boolean stopReadPrinterHistory() { 305 final boolean cancelled = mReadTask.cancel(true); 306 mReadTask = null; 307 return cancelled; 308 } 309 310 public void readPrinterHistory() { 311 if (DEBUG) { 312 Log.i(LOG_TAG, "read history started " 313 + FusedPrintersProvider.this.hashCode()); 314 } 315 mReadHistoryInProgress = true; 316 mReadTask = new ReadTask(); 317 mReadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); 318 } 319 320 @SuppressWarnings("unchecked") 321 public void addPrinterAndWritePrinterHistory(PrinterInfo printer) { 322 if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) { 323 mHistoricalPrinters.remove(0); 324 } 325 mHistoricalPrinters.add(printer); 326 new WriteTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, 327 new ArrayList<PrinterInfo>(mHistoricalPrinters)); 328 } 329 330 private List<PrinterInfo> computeFavoritePrinters(List<PrinterInfo> printers) { 331 Map<PrinterId, PrinterRecord> recordMap = 332 new ArrayMap<PrinterId, PrinterRecord>(); 333 334 // Recompute the weights. 335 float currentWeight = 1.0f; 336 final int printerCount = printers.size(); 337 for (int i = printerCount - 1; i >= 0; i--) { 338 PrinterInfo printer = printers.get(i); 339 // Aggregate weight for the same printer 340 PrinterRecord record = recordMap.get(printer.getId()); 341 if (record == null) { 342 record = new PrinterRecord(printer); 343 recordMap.put(printer.getId(), record); 344 } 345 record.weight += currentWeight; 346 currentWeight *= WEIGHT_DECAY_COEFFICIENT; 347 } 348 349 // Soft the favorite printers. 350 List<PrinterRecord> favoriteRecords = new ArrayList<PrinterRecord>( 351 recordMap.values()); 352 Collections.sort(favoriteRecords); 353 354 // Write the favorites to the output. 355 final int favoriteCount = Math.min(favoriteRecords.size(), 356 MAX_FAVORITE_PRINTER_COUNT); 357 List<PrinterInfo> favoritePrinters = new ArrayList<PrinterInfo>(favoriteCount); 358 for (int i = 0; i < favoriteCount; i++) { 359 PrinterInfo printer = favoriteRecords.get(i).printer; 360 favoritePrinters.add(printer); 361 } 362 363 return favoritePrinters; 364 } 365 366 private final class PrinterRecord implements Comparable<PrinterRecord> { 367 public final PrinterInfo printer; 368 public float weight; 369 370 public PrinterRecord(PrinterInfo printer) { 371 this.printer = printer; 372 } 373 374 @Override 375 public int compareTo(PrinterRecord another) { 376 return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight); 377 } 378 } 379 380 private final class ReadTask extends AsyncTask<Void, Void, List<PrinterInfo>> { 381 @Override 382 protected List<PrinterInfo> doInBackground(Void... args) { 383 return doReadPrinterHistory(); 384 } 385 386 @Override 387 protected void onPostExecute(List<PrinterInfo> printers) { 388 if (DEBUG) { 389 Log.i(LOG_TAG, "read history completed " 390 + FusedPrintersProvider.this.hashCode()); 391 } 392 393 // Ignore printer records whose target services are not enabled. 394 PrintManager printManager = (PrintManager) getContext() 395 .getSystemService(Context.PRINT_SERVICE); 396 List<PrintServiceInfo> services = printManager 397 .getEnabledPrintServices(); 398 399 Set<ComponentName> enabledComponents = new ArraySet<ComponentName>(); 400 final int installedServiceCount = services.size(); 401 for (int i = 0; i < installedServiceCount; i++) { 402 ServiceInfo serviceInfo = services.get(i).getResolveInfo().serviceInfo; 403 ComponentName componentName = new ComponentName( 404 serviceInfo.packageName, serviceInfo.name); 405 enabledComponents.add(componentName); 406 } 407 408 final int printerCount = printers.size(); 409 for (int i = printerCount - 1; i >= 0; i--) { 410 ComponentName printerServiceName = printers.get(i).getId().getServiceName(); 411 if (!enabledComponents.contains(printerServiceName)) { 412 printers.remove(i); 413 } 414 } 415 416 // Store the filtered list. 417 mHistoricalPrinters = printers; 418 419 // Compute the favorite printers. 420 mFavoritePrinters.clear(); 421 mFavoritePrinters.addAll(computeFavoritePrinters(mHistoricalPrinters)); 422 423 mReadHistoryInProgress = false; 424 mReadHistoryCompleted = true; 425 426 // Deliver the favorites. 427 Map<PrinterId, PrinterInfo> discoveredPrinters = Collections.emptyMap(); 428 computeAndDeliverResult(discoveredPrinters); 429 430 // Start loading the available printers. 431 loadInternal(); 432 433 // We are done. 434 mReadTask = null; 435 } 436 437 private List<PrinterInfo> doReadPrinterHistory() { 438 FileInputStream in = null; 439 try { 440 in = mStatePersistFile.openRead(); 441 } catch (FileNotFoundException fnfe) { 442 if (DEBUG) { 443 Log.i(LOG_TAG, "No existing printer history " 444 + FusedPrintersProvider.this.hashCode()); 445 } 446 return new ArrayList<PrinterInfo>(); 447 } 448 try { 449 List<PrinterInfo> printers = new ArrayList<PrinterInfo>(); 450 XmlPullParser parser = Xml.newPullParser(); 451 parser.setInput(in, null); 452 parseState(parser, printers); 453 return printers; 454 } catch (IllegalStateException ise) { 455 Slog.w(LOG_TAG, "Failed parsing ", ise); 456 } catch (NullPointerException npe) { 457 Slog.w(LOG_TAG, "Failed parsing ", npe); 458 } catch (NumberFormatException nfe) { 459 Slog.w(LOG_TAG, "Failed parsing ", nfe); 460 } catch (XmlPullParserException xppe) { 461 Slog.w(LOG_TAG, "Failed parsing ", xppe); 462 } catch (IOException ioe) { 463 Slog.w(LOG_TAG, "Failed parsing ", ioe); 464 } catch (IndexOutOfBoundsException iobe) { 465 Slog.w(LOG_TAG, "Failed parsing ", iobe); 466 } finally { 467 IoUtils.closeQuietly(in); 468 } 469 470 return Collections.emptyList(); 471 } 472 473 private void parseState(XmlPullParser parser, List<PrinterInfo> outPrinters) 474 throws IOException, XmlPullParserException { 475 parser.next(); 476 skipEmptyTextTags(parser); 477 expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS); 478 parser.next(); 479 480 while (parsePrinter(parser, outPrinters)) { 481 // Be nice and respond to cancellation 482 if (isCancelled()) { 483 return; 484 } 485 parser.next(); 486 } 487 488 skipEmptyTextTags(parser); 489 expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS); 490 } 491 492 private boolean parsePrinter(XmlPullParser parser, List<PrinterInfo> outPrinters) 493 throws IOException, XmlPullParserException { 494 skipEmptyTextTags(parser); 495 if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) { 496 return false; 497 } 498 499 String name = parser.getAttributeValue(null, ATTR_NAME); 500 String description = parser.getAttributeValue(null, ATTR_DESCRIPTION); 501 final int status = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATUS)); 502 503 parser.next(); 504 505 skipEmptyTextTags(parser); 506 expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID); 507 String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID); 508 ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue( 509 null, ATTR_SERVICE_NAME)); 510 PrinterId printerId = new PrinterId(service, localId); 511 parser.next(); 512 skipEmptyTextTags(parser); 513 expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID); 514 parser.next(); 515 516 PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name, status); 517 builder.setDescription(description); 518 PrinterInfo printer = builder.build(); 519 520 outPrinters.add(printer); 521 522 if (DEBUG) { 523 Log.i(LOG_TAG, "[RESTORED] " + printer); 524 } 525 526 skipEmptyTextTags(parser); 527 expect(parser, XmlPullParser.END_TAG, TAG_PRINTER); 528 529 return true; 530 } 531 532 private void expect(XmlPullParser parser, int type, String tag) 533 throws IOException, XmlPullParserException { 534 if (!accept(parser, type, tag)) { 535 throw new XmlPullParserException("Exepected event: " + type 536 + " and tag: " + tag + " but got event: " + parser.getEventType() 537 + " and tag:" + parser.getName()); 538 } 539 } 540 541 private void skipEmptyTextTags(XmlPullParser parser) 542 throws IOException, XmlPullParserException { 543 while (accept(parser, XmlPullParser.TEXT, null) 544 && "\n".equals(parser.getText())) { 545 parser.next(); 546 } 547 } 548 549 private boolean accept(XmlPullParser parser, int type, String tag) 550 throws IOException, XmlPullParserException { 551 if (parser.getEventType() != type) { 552 return false; 553 } 554 if (tag != null) { 555 if (!tag.equals(parser.getName())) { 556 return false; 557 } 558 } else if (parser.getName() != null) { 559 return false; 560 } 561 return true; 562 } 563 }; 564 565 private final class WriteTask extends AsyncTask<List<PrinterInfo>, Void, Void> { 566 @Override 567 protected Void doInBackground(List<PrinterInfo>... printers) { 568 doWritePrinterHistory(printers[0]); 569 return null; 570 } 571 572 private void doWritePrinterHistory(List<PrinterInfo> printers) { 573 FileOutputStream out = null; 574 try { 575 out = mStatePersistFile.startWrite(); 576 577 XmlSerializer serializer = new FastXmlSerializer(); 578 serializer.setOutput(out, "utf-8"); 579 serializer.startDocument(null, true); 580 serializer.startTag(null, TAG_PRINTERS); 581 582 final int printerCount = printers.size(); 583 for (int i = 0; i < printerCount; i++) { 584 PrinterInfo printer = printers.get(i); 585 586 serializer.startTag(null, TAG_PRINTER); 587 588 serializer.attribute(null, ATTR_NAME, printer.getName()); 589 // Historical printers are always stored as unavailable. 590 serializer.attribute(null, ATTR_STATUS, String.valueOf( 591 PrinterInfo.STATUS_UNAVAILABLE)); 592 String description = printer.getDescription(); 593 if (description != null) { 594 serializer.attribute(null, ATTR_DESCRIPTION, description); 595 } 596 597 PrinterId printerId = printer.getId(); 598 serializer.startTag(null, TAG_PRINTER_ID); 599 serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId()); 600 serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName() 601 .flattenToString()); 602 serializer.endTag(null, TAG_PRINTER_ID); 603 604 serializer.endTag(null, TAG_PRINTER); 605 606 if (DEBUG) { 607 Log.i(LOG_TAG, "[PERSISTED] " + printer); 608 } 609 } 610 611 serializer.endTag(null, TAG_PRINTERS); 612 serializer.endDocument(); 613 mStatePersistFile.finishWrite(out); 614 615 if (DEBUG) { 616 Log.i(LOG_TAG, "[PERSIST END]"); 617 } 618 } catch (IOException ioe) { 619 Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe); 620 mStatePersistFile.failWrite(out); 621 } finally { 622 IoUtils.closeQuietly(out); 623 } 624 } 625 }; 626 } 627} 628