SelectPrinterActivity.java revision 2e3012624b703a653fed44d4dc1b8904406c0d99
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.ui; 18 19import android.annotation.NonNull; 20import android.app.Activity; 21import android.app.LoaderManager; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentSender.SendIntentException; 26import android.content.Loader; 27import android.database.DataSetObserver; 28import android.graphics.drawable.Drawable; 29import android.os.Bundle; 30import android.print.PrintManager; 31import android.print.PrintServicesLoader; 32import android.print.PrinterId; 33import android.print.PrinterInfo; 34import android.printservice.PrintServiceInfo; 35import android.provider.Settings; 36import android.text.TextUtils; 37import android.util.ArrayMap; 38import android.util.Log; 39import android.util.TypedValue; 40import android.view.ContextMenu; 41import android.view.ContextMenu.ContextMenuInfo; 42import android.view.Menu; 43import android.view.MenuItem; 44import android.view.View; 45import android.view.View.OnClickListener; 46import android.view.ViewGroup; 47import android.view.accessibility.AccessibilityManager; 48import android.widget.AdapterView; 49import android.widget.AdapterView.AdapterContextMenuInfo; 50import android.widget.BaseAdapter; 51import android.widget.Filter; 52import android.widget.Filterable; 53import android.widget.ImageView; 54import android.widget.LinearLayout; 55import android.widget.ListView; 56import android.widget.SearchView; 57import android.widget.TextView; 58import android.widget.Toast; 59 60import com.android.internal.logging.MetricsLogger; 61import com.android.internal.logging.MetricsProto.MetricsEvent; 62import com.android.printspooler.R; 63 64import java.util.ArrayList; 65import java.util.List; 66 67/** 68 * This is an activity for selecting a printer. 69 */ 70public final class SelectPrinterActivity extends Activity implements 71 LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> { 72 73 private static final String LOG_TAG = "SelectPrinterFragment"; 74 75 private static final int LOADER_ID_PRINT_REGISTRY = 1; 76 private static final int LOADER_ID_PRINT_REGISTRY_INT = 2; 77 private static final int LOADER_ID_ENABLED_PRINT_SERVICES = 3; 78 79 public static final String INTENT_EXTRA_PRINTER = "INTENT_EXTRA_PRINTER"; 80 81 private static final String EXTRA_PRINTER = "EXTRA_PRINTER"; 82 private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID"; 83 84 private static final String KEY_NOT_FIRST_CREATE = "KEY_NOT_FIRST_CREATE"; 85 private static final String KEY_DID_SEARCH = "DID_SEARCH"; 86 87 // Constants for MetricsLogger.count and MetricsLogger.histo 88 private static final String PRINTERS_LISTED_COUNT = "printers_listed"; 89 private static final String PRINTERS_ICON_COUNT = "printers_icon"; 90 private static final String PRINTERS_INFO_COUNT = "printers_info"; 91 92 /** The currently enabled print services by their ComponentName */ 93 private ArrayMap<ComponentName, PrintServiceInfo> mEnabledPrintServices; 94 95 private PrinterRegistry mPrinterRegistry; 96 97 private ListView mListView; 98 99 private AnnounceFilterResult mAnnounceFilterResult; 100 101 private boolean mDidSearch; 102 103 private void startAddPrinterActivity() { 104 MetricsLogger.action(this, MetricsEvent.ACTION_PRINT_SERVICE_ADD); 105 startActivity(new Intent(this, AddPrinterActivity.class)); 106 } 107 108 @Override 109 public void onCreate(Bundle savedInstanceState) { 110 super.onCreate(savedInstanceState); 111 getActionBar().setIcon(R.drawable.ic_print); 112 113 setContentView(R.layout.select_printer_activity); 114 115 mEnabledPrintServices = new ArrayMap<>(); 116 117 mPrinterRegistry = new PrinterRegistry(this, null, LOADER_ID_PRINT_REGISTRY, 118 LOADER_ID_PRINT_REGISTRY_INT); 119 120 // Hook up the list view. 121 mListView = (ListView) findViewById(android.R.id.list); 122 final DestinationAdapter adapter = new DestinationAdapter(); 123 adapter.registerDataSetObserver(new DataSetObserver() { 124 @Override 125 public void onChanged() { 126 if (!isFinishing() && adapter.getCount() <= 0) { 127 updateEmptyView(adapter); 128 } 129 } 130 131 @Override 132 public void onInvalidated() { 133 if (!isFinishing()) { 134 updateEmptyView(adapter); 135 } 136 } 137 }); 138 mListView.setAdapter(adapter); 139 140 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 141 @Override 142 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 143 if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) { 144 return; 145 } 146 147 PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position); 148 149 if (printer == null) { 150 startAddPrinterActivity(); 151 } else { 152 onPrinterSelected(printer); 153 } 154 } 155 }); 156 157 findViewById(R.id.button).setOnClickListener(new OnClickListener() { 158 @Override public void onClick(View v) { 159 startAddPrinterActivity(); 160 } 161 }); 162 163 registerForContextMenu(mListView); 164 165 getLoaderManager().initLoader(LOADER_ID_ENABLED_PRINT_SERVICES, null, this); 166 167 // On first creation: 168 // 169 // If no services are installed, instantly open add printer dialog. 170 // If some are disabled and some are enabled show a toast to notify the user 171 if (savedInstanceState == null || !savedInstanceState.getBoolean(KEY_NOT_FIRST_CREATE)) { 172 List<PrintServiceInfo> allServices = 173 ((PrintManager) getSystemService(Context.PRINT_SERVICE)) 174 .getPrintServices(PrintManager.ALL_SERVICES); 175 boolean hasEnabledServices = false; 176 boolean hasDisabledServices = false; 177 178 if (allServices != null) { 179 final int numServices = allServices.size(); 180 for (int i = 0; i < numServices; i++) { 181 if (allServices.get(i).isEnabled()) { 182 hasEnabledServices = true; 183 } else { 184 hasDisabledServices = true; 185 } 186 } 187 } 188 189 if (!hasEnabledServices) { 190 startAddPrinterActivity(); 191 } else if (hasDisabledServices) { 192 String disabledServicesSetting = Settings.Secure.getString(getContentResolver(), 193 Settings.Secure.DISABLED_PRINT_SERVICES); 194 if (!TextUtils.isEmpty(disabledServicesSetting)) { 195 Toast.makeText(this, getString(R.string.print_services_disabled_toast), 196 Toast.LENGTH_LONG).show(); 197 } 198 } 199 } 200 201 if (savedInstanceState != null) { 202 mDidSearch = savedInstanceState.getBoolean(KEY_DID_SEARCH); 203 } 204 } 205 206 @Override 207 protected void onSaveInstanceState(Bundle outState) { 208 super.onSaveInstanceState(outState); 209 outState.putBoolean(KEY_NOT_FIRST_CREATE, true); 210 outState.putBoolean(KEY_DID_SEARCH, mDidSearch); 211 } 212 213 @Override 214 public boolean onCreateOptionsMenu(Menu menu) { 215 super.onCreateOptionsMenu(menu); 216 217 getMenuInflater().inflate(R.menu.select_printer_activity, menu); 218 219 MenuItem searchItem = menu.findItem(R.id.action_search); 220 SearchView searchView = (SearchView) searchItem.getActionView(); 221 searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 222 @Override 223 public boolean onQueryTextSubmit(String query) { 224 return true; 225 } 226 227 @Override 228 public boolean onQueryTextChange(String searchString) { 229 ((DestinationAdapter) mListView.getAdapter()).getFilter().filter(searchString); 230 return true; 231 } 232 }); 233 searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 234 @Override 235 public void onViewAttachedToWindow(View view) { 236 if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) { 237 view.announceForAccessibility(getString( 238 R.string.print_search_box_shown_utterance)); 239 } 240 } 241 @Override 242 public void onViewDetachedFromWindow(View view) { 243 if (!isFinishing() && AccessibilityManager.getInstance( 244 SelectPrinterActivity.this).isEnabled()) { 245 view.announceForAccessibility(getString( 246 R.string.print_search_box_hidden_utterance)); 247 } 248 } 249 }); 250 251 return true; 252 } 253 254 @Override 255 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 256 if (view == mListView) { 257 final int position = ((AdapterContextMenuInfo) menuInfo).position; 258 PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position); 259 260 menu.setHeaderTitle(printer.getName()); 261 262 // Add the select menu item if applicable. 263 if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) { 264 MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer, 265 Menu.NONE, R.string.print_select_printer); 266 Intent intent = new Intent(); 267 intent.putExtra(EXTRA_PRINTER, printer); 268 selectItem.setIntent(intent); 269 } 270 271 // Add the forget menu item if applicable. 272 if (mPrinterRegistry.isFavoritePrinter(printer.getId())) { 273 MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer, 274 Menu.NONE, R.string.print_forget_printer); 275 Intent intent = new Intent(); 276 intent.putExtra(EXTRA_PRINTER_ID, printer.getId()); 277 forgetItem.setIntent(intent); 278 } 279 } 280 } 281 282 @Override 283 public boolean onContextItemSelected(MenuItem item) { 284 switch (item.getItemId()) { 285 case R.string.print_select_printer: { 286 PrinterInfo printer = item.getIntent().getParcelableExtra(EXTRA_PRINTER); 287 onPrinterSelected(printer); 288 } return true; 289 290 case R.string.print_forget_printer: { 291 PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID); 292 mPrinterRegistry.forgetFavoritePrinter(printerId); 293 } return true; 294 } 295 return false; 296 } 297 298 /** 299 * Adjust the UI if the enabled print services changed. 300 */ 301 private synchronized void onPrintServicesUpdate() { 302 updateEmptyView((DestinationAdapter)mListView.getAdapter()); 303 invalidateOptionsMenu(); 304 } 305 306 @Override 307 public void onStart() { 308 super.onStart(); 309 onPrintServicesUpdate(); 310 } 311 312 @Override 313 public void onPause() { 314 if (mAnnounceFilterResult != null) { 315 mAnnounceFilterResult.remove(); 316 } 317 super.onPause(); 318 } 319 320 @Override 321 public void onStop() { 322 super.onStop(); 323 } 324 325 @Override 326 protected void onDestroy() { 327 if (isFinishing()) { 328 DestinationAdapter adapter = (DestinationAdapter) mListView.getAdapter(); 329 List<PrinterInfo> printers = adapter.getPrinters(); 330 int numPrinters = adapter.getPrinters().size(); 331 332 MetricsLogger.action(this, MetricsEvent.PRINT_ALL_PRINTERS, numPrinters); 333 MetricsLogger.count(this, PRINTERS_LISTED_COUNT, numPrinters); 334 335 int numInfoPrinters = 0; 336 int numIconPrinters = 0; 337 for (int i = 0; i < numPrinters; i++) { 338 PrinterInfo printer = printers.get(i); 339 340 if (printer.getInfoIntent() != null) { 341 numInfoPrinters++; 342 } 343 344 if (printer.getHasCustomPrinterIcon()) { 345 numIconPrinters++; 346 } 347 } 348 349 MetricsLogger.count(this, PRINTERS_INFO_COUNT, numInfoPrinters); 350 MetricsLogger.count(this, PRINTERS_ICON_COUNT, numIconPrinters); 351 } 352 353 super.onDestroy(); 354 } 355 356 private void onPrinterSelected(PrinterInfo printer) { 357 Intent intent = new Intent(); 358 intent.putExtra(INTENT_EXTRA_PRINTER, printer); 359 setResult(RESULT_OK, intent); 360 finish(); 361 } 362 363 public void updateEmptyView(DestinationAdapter adapter) { 364 if (mListView.getEmptyView() == null) { 365 View emptyView = findViewById(R.id.empty_print_state); 366 mListView.setEmptyView(emptyView); 367 } 368 TextView titleView = (TextView) findViewById(R.id.title); 369 View progressBar = findViewById(R.id.progress_bar); 370 if (mEnabledPrintServices.size() == 0) { 371 titleView.setText(R.string.print_no_print_services); 372 progressBar.setVisibility(View.GONE); 373 } else if (adapter.getUnfilteredCount() <= 0) { 374 titleView.setText(R.string.print_searching_for_printers); 375 progressBar.setVisibility(View.VISIBLE); 376 } else { 377 titleView.setText(R.string.print_no_printers); 378 progressBar.setVisibility(View.GONE); 379 } 380 } 381 382 private void announceSearchResultIfNeeded() { 383 if (AccessibilityManager.getInstance(this).isEnabled()) { 384 if (mAnnounceFilterResult == null) { 385 mAnnounceFilterResult = new AnnounceFilterResult(); 386 } 387 mAnnounceFilterResult.post(); 388 } 389 } 390 391 @Override 392 public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { 393 return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this, 394 PrintManager.ENABLED_SERVICES); 395 } 396 397 @Override 398 public void onLoadFinished(Loader<List<PrintServiceInfo>> loader, 399 List<PrintServiceInfo> services) { 400 mEnabledPrintServices.clear(); 401 402 if (services != null && !services.isEmpty()) { 403 final int numServices = services.size(); 404 for (int i = 0; i < numServices; i++) { 405 PrintServiceInfo service = services.get(i); 406 407 mEnabledPrintServices.put(service.getComponentName(), service); 408 } 409 } 410 411 onPrintServicesUpdate(); 412 } 413 414 @Override 415 public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) { 416 if (!isFinishing()) { 417 onLoadFinished(loader, null); 418 } 419 } 420 421 private final class DestinationAdapter extends BaseAdapter implements Filterable { 422 423 private final Object mLock = new Object(); 424 425 private final List<PrinterInfo> mPrinters = new ArrayList<>(); 426 427 private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>(); 428 429 private CharSequence mLastSearchString; 430 431 /** 432 * Get the currently known printers. 433 * 434 * @return The currently known printers 435 */ 436 @NonNull List<PrinterInfo> getPrinters() { 437 return mPrinters; 438 } 439 440 public DestinationAdapter() { 441 mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() { 442 @Override 443 public void onPrintersChanged(List<PrinterInfo> printers) { 444 synchronized (mLock) { 445 mPrinters.clear(); 446 mPrinters.addAll(printers); 447 mFilteredPrinters.clear(); 448 mFilteredPrinters.addAll(printers); 449 if (!TextUtils.isEmpty(mLastSearchString)) { 450 getFilter().filter(mLastSearchString); 451 } 452 } 453 notifyDataSetChanged(); 454 } 455 456 @Override 457 public void onPrintersInvalid() { 458 synchronized (mLock) { 459 mPrinters.clear(); 460 mFilteredPrinters.clear(); 461 } 462 notifyDataSetInvalidated(); 463 } 464 }); 465 } 466 467 @Override 468 public Filter getFilter() { 469 return new Filter() { 470 @Override 471 protected FilterResults performFiltering(CharSequence constraint) { 472 synchronized (mLock) { 473 if (TextUtils.isEmpty(constraint)) { 474 return null; 475 } 476 FilterResults results = new FilterResults(); 477 List<PrinterInfo> filteredPrinters = new ArrayList<>(); 478 String constraintLowerCase = constraint.toString().toLowerCase(); 479 final int printerCount = mPrinters.size(); 480 for (int i = 0; i < printerCount; i++) { 481 PrinterInfo printer = mPrinters.get(i); 482 String description = printer.getDescription(); 483 if (printer.getName().toLowerCase().contains(constraintLowerCase) 484 || description != null && description.toLowerCase() 485 .contains(constraintLowerCase)) { 486 filteredPrinters.add(printer); 487 } 488 } 489 results.values = filteredPrinters; 490 results.count = filteredPrinters.size(); 491 return results; 492 } 493 } 494 495 @Override 496 @SuppressWarnings("unchecked") 497 protected void publishResults(CharSequence constraint, FilterResults results) { 498 final boolean resultCountChanged; 499 synchronized (mLock) { 500 final int oldPrinterCount = mFilteredPrinters.size(); 501 mLastSearchString = constraint; 502 mFilteredPrinters.clear(); 503 if (results == null) { 504 mFilteredPrinters.addAll(mPrinters); 505 } else { 506 List<PrinterInfo> printers = (List<PrinterInfo>) results.values; 507 mFilteredPrinters.addAll(printers); 508 } 509 resultCountChanged = (oldPrinterCount != mFilteredPrinters.size()); 510 } 511 if (resultCountChanged) { 512 announceSearchResultIfNeeded(); 513 } 514 515 if (!mDidSearch) { 516 MetricsLogger.action(SelectPrinterActivity.this, 517 MetricsEvent.ACTION_PRINTER_SEARCH); 518 mDidSearch = true; 519 } 520 notifyDataSetChanged(); 521 } 522 }; 523 } 524 525 public int getUnfilteredCount() { 526 synchronized (mLock) { 527 return mPrinters.size(); 528 } 529 } 530 531 @Override 532 public int getCount() { 533 synchronized (mLock) { 534 if (mFilteredPrinters.isEmpty()) { 535 return 0; 536 } else { 537 // Add "add printer" item to the end of the list. If the list is empty there is 538 // a link on the empty view 539 return mFilteredPrinters.size() + 1; 540 } 541 } 542 } 543 544 @Override 545 public int getViewTypeCount() { 546 return 2; 547 } 548 549 @Override 550 public int getItemViewType(int position) { 551 // Use separate view types for the "add printer" item an the items referring to printers 552 if (getItem(position) == null) { 553 return 0; 554 } else { 555 return 1; 556 } 557 } 558 559 @Override 560 public Object getItem(int position) { 561 synchronized (mLock) { 562 if (position < mFilteredPrinters.size()) { 563 return mFilteredPrinters.get(position); 564 } else { 565 // Return null to mark this as the "add printer item" 566 return null; 567 } 568 } 569 } 570 571 @Override 572 public long getItemId(int position) { 573 return position; 574 } 575 576 @Override 577 public View getDropDownView(int position, View convertView, ViewGroup parent) { 578 return getView(position, convertView, parent); 579 } 580 581 @Override 582 public View getView(int position, View convertView, ViewGroup parent) { 583 final PrinterInfo printer = (PrinterInfo) getItem(position); 584 585 // Handle "add printer item" 586 if (printer == null) { 587 if (convertView == null) { 588 convertView = getLayoutInflater().inflate(R.layout.add_printer_list_item, 589 parent, false); 590 } 591 592 return convertView; 593 } 594 595 if (convertView == null) { 596 convertView = getLayoutInflater().inflate( 597 R.layout.printer_list_item, parent, false); 598 } 599 600 convertView.setEnabled(isActionable(position)); 601 602 603 CharSequence title = printer.getName(); 604 Drawable icon = printer.loadIcon(SelectPrinterActivity.this); 605 606 PrintServiceInfo service = mEnabledPrintServices.get(printer.getId().getServiceName()); 607 608 CharSequence printServiceLabel = null; 609 if (service != null) { 610 printServiceLabel = service.getResolveInfo().loadLabel(getPackageManager()) 611 .toString(); 612 } 613 614 CharSequence description = printer.getDescription(); 615 616 CharSequence subtitle; 617 if (TextUtils.isEmpty(printServiceLabel)) { 618 subtitle = description; 619 } else if (TextUtils.isEmpty(description)) { 620 subtitle = printServiceLabel; 621 } else { 622 subtitle = getString(R.string.printer_extended_description_template, 623 printServiceLabel, description); 624 } 625 626 TextView titleView = (TextView) convertView.findViewById(R.id.title); 627 titleView.setText(title); 628 629 TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle); 630 if (!TextUtils.isEmpty(subtitle)) { 631 subtitleView.setText(subtitle); 632 subtitleView.setVisibility(View.VISIBLE); 633 } else { 634 subtitleView.setText(null); 635 subtitleView.setVisibility(View.GONE); 636 } 637 638 LinearLayout moreInfoView = (LinearLayout) convertView.findViewById(R.id.more_info); 639 if (printer.getInfoIntent() != null) { 640 moreInfoView.setVisibility(View.VISIBLE); 641 moreInfoView.setOnClickListener(new OnClickListener() { 642 @Override 643 public void onClick(View v) { 644 try { 645 startIntentSender(printer.getInfoIntent().getIntentSender(), null, 0, 0, 646 0); 647 } catch (SendIntentException e) { 648 Log.e(LOG_TAG, "Could not execute pending info intent: %s", e); 649 } 650 } 651 }); 652 } else { 653 moreInfoView.setVisibility(View.GONE); 654 } 655 656 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); 657 if (icon != null) { 658 iconView.setVisibility(View.VISIBLE); 659 if (!isActionable(position)) { 660 icon.mutate(); 661 662 TypedValue value = new TypedValue(); 663 getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true); 664 icon.setAlpha((int)(value.getFloat() * 255)); 665 } 666 iconView.setImageDrawable(icon); 667 } else { 668 iconView.setVisibility(View.GONE); 669 } 670 671 return convertView; 672 } 673 674 public boolean isActionable(int position) { 675 PrinterInfo printer = (PrinterInfo) getItem(position); 676 677 if (printer == null) { 678 return true; 679 } else { 680 return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; 681 } 682 } 683 } 684 685 private final class AnnounceFilterResult implements Runnable { 686 private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec 687 688 public void post() { 689 remove(); 690 mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY); 691 } 692 693 public void remove() { 694 mListView.removeCallbacks(this); 695 } 696 697 @Override 698 public void run() { 699 final int count = mListView.getAdapter().getCount(); 700 final String text; 701 if (count <= 0) { 702 text = getString(R.string.print_no_printers); 703 } else { 704 text = getResources().getQuantityString( 705 R.plurals.print_search_result_count_utterance, count, count); 706 } 707 mListView.announceForAccessibility(text); 708 } 709 } 710} 711