SelectPrinterActivity.java revision bb9f686b40743df2642b7d3b7778dbf7284ae665
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.app.Activity; 20import android.app.AlertDialog; 21import android.app.Dialog; 22import android.app.DialogFragment; 23import android.app.Fragment; 24import android.app.FragmentTransaction; 25import android.content.ActivityNotFoundException; 26import android.content.ComponentName; 27import android.content.Context; 28import android.content.DialogInterface; 29import android.content.Intent; 30import android.content.IntentSender.SendIntentException; 31import android.content.pm.ActivityInfo; 32import android.content.pm.PackageInfo; 33import android.content.pm.PackageManager; 34import android.content.pm.PackageManager.NameNotFoundException; 35import android.content.pm.ResolveInfo; 36import android.content.pm.ServiceInfo; 37import android.database.ContentObserver; 38import android.database.DataSetObserver; 39import android.graphics.drawable.Drawable; 40import android.net.Uri; 41import android.os.Bundle; 42import android.os.Handler; 43import android.print.PrintManager; 44import android.print.PrinterId; 45import android.print.PrinterInfo; 46import android.printservice.PrintServiceInfo; 47import android.provider.Settings; 48import android.text.TextUtils; 49import android.util.Log; 50import android.view.ContextMenu; 51import android.view.ContextMenu.ContextMenuInfo; 52import android.view.Menu; 53import android.view.MenuItem; 54import android.view.View; 55import android.view.View.OnClickListener; 56import android.view.ViewGroup; 57import android.view.accessibility.AccessibilityManager; 58import android.widget.AdapterView; 59import android.widget.AdapterView.AdapterContextMenuInfo; 60import android.widget.ArrayAdapter; 61import android.widget.BaseAdapter; 62import android.widget.Filter; 63import android.widget.Filterable; 64import android.widget.ImageView; 65import android.widget.ListView; 66import android.widget.SearchView; 67import android.widget.TextView; 68 69import com.android.printspooler.R; 70 71import java.util.ArrayList; 72import java.util.List; 73 74/** 75 * This is an activity for selecting a printer. 76 */ 77public final class SelectPrinterActivity extends Activity { 78 79 private static final String LOG_TAG = "SelectPrinterFragment"; 80 81 public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID"; 82 83 private static final String FRAGMENT_TAG_ADD_PRINTER_DIALOG = 84 "FRAGMENT_TAG_ADD_PRINTER_DIALOG"; 85 86 private static final String FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS = 87 "FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS"; 88 89 private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID"; 90 91 /** If there are any enabled print services */ 92 private boolean mHasEnabledPrintServices; 93 94 private final ArrayList<PrintServiceInfo> mAddPrinterServices = 95 new ArrayList<>(); 96 97 private PrinterRegistry mPrinterRegistry; 98 99 private ListView mListView; 100 101 private AnnounceFilterResult mAnnounceFilterResult; 102 103 /** Monitor if new print services get enabled or disabled */ 104 private ContentObserver mPrintServicesObserver; 105 106 @Override 107 public void onCreate(Bundle savedInstanceState) { 108 super.onCreate(savedInstanceState); 109 getActionBar().setIcon(R.drawable.ic_print); 110 111 setContentView(R.layout.select_printer_activity); 112 113 mPrinterRegistry = new PrinterRegistry(this, null); 114 115 // Hook up the list view. 116 mListView = (ListView) findViewById(android.R.id.list); 117 final DestinationAdapter adapter = new DestinationAdapter(); 118 adapter.registerDataSetObserver(new DataSetObserver() { 119 @Override 120 public void onChanged() { 121 if (!isFinishing() && adapter.getCount() <= 0) { 122 updateEmptyView(adapter); 123 } 124 } 125 126 @Override 127 public void onInvalidated() { 128 if (!isFinishing()) { 129 updateEmptyView(adapter); 130 } 131 } 132 }); 133 mListView.setAdapter(adapter); 134 135 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 136 @Override 137 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 138 if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) { 139 return; 140 } 141 142 PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position); 143 onPrinterSelected(printer.getId()); 144 } 145 }); 146 147 registerForContextMenu(mListView); 148 } 149 150 @Override 151 public boolean onCreateOptionsMenu(Menu menu) { 152 super.onCreateOptionsMenu(menu); 153 154 getMenuInflater().inflate(R.menu.select_printer_activity, menu); 155 156 MenuItem searchItem = menu.findItem(R.id.action_search); 157 SearchView searchView = (SearchView) searchItem.getActionView(); 158 searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 159 @Override 160 public boolean onQueryTextSubmit(String query) { 161 return true; 162 } 163 164 @Override 165 public boolean onQueryTextChange(String searchString) { 166 ((DestinationAdapter) mListView.getAdapter()).getFilter().filter(searchString); 167 return true; 168 } 169 }); 170 searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 171 @Override 172 public void onViewAttachedToWindow(View view) { 173 if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) { 174 view.announceForAccessibility(getString( 175 R.string.print_search_box_shown_utterance)); 176 } 177 } 178 @Override 179 public void onViewDetachedFromWindow(View view) { 180 if (!isFinishing() && AccessibilityManager.getInstance( 181 SelectPrinterActivity.this).isEnabled()) { 182 view.announceForAccessibility(getString( 183 R.string.print_search_box_hidden_utterance)); 184 } 185 } 186 }); 187 188 return true; 189 } 190 191 @Override 192 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 193 if (view == mListView) { 194 final int position = ((AdapterContextMenuInfo) menuInfo).position; 195 PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position); 196 197 menu.setHeaderTitle(printer.getName()); 198 199 // Add the select menu item if applicable. 200 if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) { 201 MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer, 202 Menu.NONE, R.string.print_select_printer); 203 Intent intent = new Intent(); 204 intent.putExtra(EXTRA_PRINTER_ID, printer.getId()); 205 selectItem.setIntent(intent); 206 } 207 208 // Add the forget menu item if applicable. 209 if (mPrinterRegistry.isFavoritePrinter(printer.getId())) { 210 MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer, 211 Menu.NONE, R.string.print_forget_printer); 212 Intent intent = new Intent(); 213 intent.putExtra(EXTRA_PRINTER_ID, printer.getId()); 214 forgetItem.setIntent(intent); 215 } 216 } 217 } 218 219 @Override 220 public boolean onContextItemSelected(MenuItem item) { 221 switch (item.getItemId()) { 222 case R.string.print_select_printer: { 223 PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID); 224 onPrinterSelected(printerId); 225 } return true; 226 227 case R.string.print_forget_printer: { 228 PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID); 229 mPrinterRegistry.forgetFavoritePrinter(printerId); 230 } return true; 231 } 232 return false; 233 } 234 235 /** 236 * Adjust the UI if the enabled print services changed. 237 */ 238 private synchronized void onPrintServicesUpdate() { 239 updateServicesWithAddPrinterActivity(); 240 updateEmptyView((DestinationAdapter)mListView.getAdapter()); 241 invalidateOptionsMenu(); 242 } 243 244 /** 245 * Register listener for changes to the enabled print services. 246 */ 247 private void registerServiceMonitor() { 248 mPrintServicesObserver = new ContentObserver(new Handler()) { 249 @Override 250 public void onChange(boolean selfChange) { 251 onPrintServicesUpdate(); 252 } 253 }; 254 255 getContentResolver().registerContentObserver( 256 Settings.Secure.getUriFor(Settings.Secure.ENABLED_PRINT_SERVICES), false, 257 mPrintServicesObserver); 258 } 259 260 /** 261 * Unregister {@link #mPrintServicesObserver listener for changes to the enabled print services} 262 * or nothing if the listener is not registered. 263 */ 264 private void unregisterServiceMonitorIfNeeded() { 265 if (mPrintServicesObserver != null) { 266 getContentResolver().unregisterContentObserver(mPrintServicesObserver); 267 268 mPrintServicesObserver = null; 269 } 270 } 271 272 @Override 273 public void onStart() { 274 super.onStart(); 275 registerServiceMonitor(); 276 onPrintServicesUpdate(); 277 } 278 279 @Override 280 public void onPause() { 281 if (mAnnounceFilterResult != null) { 282 mAnnounceFilterResult.remove(); 283 } 284 super.onPause(); 285 } 286 287 @Override 288 public void onStop() { 289 unregisterServiceMonitorIfNeeded(); 290 super.onStop(); 291 } 292 293 @Override 294 public boolean onOptionsItemSelected(MenuItem item) { 295 if (item.getItemId() == R.id.action_add_printer) { 296 showAddPrinterSelectionDialog(); 297 return true; 298 } 299 return super.onOptionsItemSelected(item); 300 } 301 302 private void onPrinterSelected(PrinterId printerId) { 303 Intent intent = new Intent(); 304 intent.putExtra(INTENT_EXTRA_PRINTER_ID, printerId); 305 setResult(RESULT_OK, intent); 306 finish(); 307 } 308 309 private void updateServicesWithAddPrinterActivity() { 310 mHasEnabledPrintServices = true; 311 mAddPrinterServices.clear(); 312 313 // Get all enabled print services. 314 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); 315 List<PrintServiceInfo> enabledServices = printManager.getEnabledPrintServices(); 316 317 // No enabled print services - done. 318 if (enabledServices.isEmpty()) { 319 mHasEnabledPrintServices = false; 320 return; 321 } 322 323 // Find the services with valid add printers activities. 324 final int enabledServiceCount = enabledServices.size(); 325 for (int i = 0; i < enabledServiceCount; i++) { 326 PrintServiceInfo enabledService = enabledServices.get(i); 327 328 // No add printers activity declared - next. 329 if (TextUtils.isEmpty(enabledService.getAddPrintersActivityName())) { 330 continue; 331 } 332 333 ServiceInfo serviceInfo = enabledService.getResolveInfo().serviceInfo; 334 ComponentName addPrintersComponentName = new ComponentName( 335 serviceInfo.packageName, enabledService.getAddPrintersActivityName()); 336 Intent addPritnersIntent = new Intent() 337 .setComponent(addPrintersComponentName); 338 339 // The add printers activity is valid - add it. 340 PackageManager pm = getPackageManager(); 341 List<ResolveInfo> resolvedActivities = pm.queryIntentActivities(addPritnersIntent, 0); 342 if (!resolvedActivities.isEmpty()) { 343 // The activity is a component name, therefore it is one or none. 344 ActivityInfo activityInfo = resolvedActivities.get(0).activityInfo; 345 if (activityInfo.exported 346 && (activityInfo.permission == null 347 || pm.checkPermission(activityInfo.permission, getPackageName()) 348 == PackageManager.PERMISSION_GRANTED)) { 349 mAddPrinterServices.add(enabledService); 350 } 351 } 352 } 353 } 354 355 private void showAddPrinterSelectionDialog() { 356 FragmentTransaction transaction = getFragmentManager().beginTransaction(); 357 Fragment oldFragment = getFragmentManager().findFragmentByTag( 358 FRAGMENT_TAG_ADD_PRINTER_DIALOG); 359 if (oldFragment != null) { 360 transaction.remove(oldFragment); 361 } 362 AddPrinterAlertDialogFragment newFragment = new AddPrinterAlertDialogFragment(); 363 Bundle arguments = new Bundle(); 364 arguments.putParcelableArrayList(FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS, 365 mAddPrinterServices); 366 newFragment.setArguments(arguments); 367 transaction.add(newFragment, FRAGMENT_TAG_ADD_PRINTER_DIALOG); 368 transaction.commit(); 369 } 370 371 public void updateEmptyView(DestinationAdapter adapter) { 372 if (mListView.getEmptyView() == null) { 373 View emptyView = findViewById(R.id.empty_print_state); 374 mListView.setEmptyView(emptyView); 375 } 376 TextView titleView = (TextView) findViewById(R.id.title); 377 View progressBar = findViewById(R.id.progress_bar); 378 if (!mHasEnabledPrintServices) { 379 titleView.setText(R.string.print_no_print_services); 380 progressBar.setVisibility(View.GONE); 381 } else if (adapter.getUnfilteredCount() <= 0) { 382 titleView.setText(R.string.print_searching_for_printers); 383 progressBar.setVisibility(View.VISIBLE); 384 } else { 385 titleView.setText(R.string.print_no_printers); 386 progressBar.setVisibility(View.GONE); 387 } 388 } 389 390 private void announceSearchResultIfNeeded() { 391 if (AccessibilityManager.getInstance(this).isEnabled()) { 392 if (mAnnounceFilterResult == null) { 393 mAnnounceFilterResult = new AnnounceFilterResult(); 394 } 395 mAnnounceFilterResult.post(); 396 } 397 } 398 399 public static class AddPrinterAlertDialogFragment extends DialogFragment { 400 401 private String mAddPrintServiceItem; 402 403 @Override 404 @SuppressWarnings("unchecked") 405 public Dialog onCreateDialog(Bundle savedInstanceState) { 406 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) 407 .setTitle(R.string.choose_print_service); 408 409 final List<PrintServiceInfo> printServices = (List<PrintServiceInfo>) (List<?>) 410 getArguments().getParcelableArrayList(FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS); 411 412 final ArrayAdapter<String> adapter = new ArrayAdapter<>( 413 getActivity(), android.R.layout.simple_list_item_1); 414 final int printServiceCount = printServices.size(); 415 for (int i = 0; i < printServiceCount; i++) { 416 PrintServiceInfo printService = printServices.get(i); 417 adapter.add(printService.getResolveInfo().loadLabel( 418 getActivity().getPackageManager()).toString()); 419 } 420 421 final String searchUri = Settings.Secure.getString(getActivity().getContentResolver(), 422 Settings.Secure.PRINT_SERVICE_SEARCH_URI); 423 final Intent viewIntent; 424 if (!TextUtils.isEmpty(searchUri)) { 425 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); 426 if (getActivity().getPackageManager().resolveActivity(intent, 0) != null) { 427 viewIntent = intent; 428 mAddPrintServiceItem = getString(R.string.add_print_service_label); 429 adapter.add(mAddPrintServiceItem); 430 } else { 431 viewIntent = null; 432 } 433 } else { 434 viewIntent = null; 435 } 436 437 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { 438 @Override 439 public void onClick(DialogInterface dialog, int which) { 440 String item = adapter.getItem(which); 441 if (item.equals(mAddPrintServiceItem)) { 442 try { 443 startActivity(viewIntent); 444 } catch (ActivityNotFoundException anfe) { 445 Log.w(LOG_TAG, "Couldn't start add printer activity", anfe); 446 } 447 } else { 448 PrintServiceInfo printService = printServices.get(which); 449 ComponentName componentName = new ComponentName( 450 printService.getResolveInfo().serviceInfo.packageName, 451 printService.getAddPrintersActivityName()); 452 Intent intent = new Intent(Intent.ACTION_MAIN); 453 intent.setComponent(componentName); 454 try { 455 startActivity(intent); 456 } catch (ActivityNotFoundException anfe) { 457 Log.w(LOG_TAG, "Couldn't start add printer activity", anfe); 458 } 459 } 460 } 461 }); 462 463 return builder.create(); 464 } 465 } 466 467 private final class DestinationAdapter extends BaseAdapter implements Filterable { 468 469 private final Object mLock = new Object(); 470 471 private final List<PrinterInfo> mPrinters = new ArrayList<>(); 472 473 private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>(); 474 475 private CharSequence mLastSearchString; 476 477 public DestinationAdapter() { 478 mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() { 479 @Override 480 public void onPrintersChanged(List<PrinterInfo> printers) { 481 synchronized (mLock) { 482 mPrinters.clear(); 483 mPrinters.addAll(printers); 484 mFilteredPrinters.clear(); 485 mFilteredPrinters.addAll(printers); 486 if (!TextUtils.isEmpty(mLastSearchString)) { 487 getFilter().filter(mLastSearchString); 488 } 489 } 490 notifyDataSetChanged(); 491 } 492 493 @Override 494 public void onPrintersInvalid() { 495 synchronized (mLock) { 496 mPrinters.clear(); 497 mFilteredPrinters.clear(); 498 } 499 notifyDataSetInvalidated(); 500 } 501 }); 502 } 503 504 @Override 505 public Filter getFilter() { 506 return new Filter() { 507 @Override 508 protected FilterResults performFiltering(CharSequence constraint) { 509 synchronized (mLock) { 510 if (TextUtils.isEmpty(constraint)) { 511 return null; 512 } 513 FilterResults results = new FilterResults(); 514 List<PrinterInfo> filteredPrinters = new ArrayList<>(); 515 String constraintLowerCase = constraint.toString().toLowerCase(); 516 final int printerCount = mPrinters.size(); 517 for (int i = 0; i < printerCount; i++) { 518 PrinterInfo printer = mPrinters.get(i); 519 if (printer.getName().toLowerCase().contains(constraintLowerCase)) { 520 filteredPrinters.add(printer); 521 } 522 } 523 results.values = filteredPrinters; 524 results.count = filteredPrinters.size(); 525 return results; 526 } 527 } 528 529 @Override 530 @SuppressWarnings("unchecked") 531 protected void publishResults(CharSequence constraint, FilterResults results) { 532 final boolean resultCountChanged; 533 synchronized (mLock) { 534 final int oldPrinterCount = mFilteredPrinters.size(); 535 mLastSearchString = constraint; 536 mFilteredPrinters.clear(); 537 if (results == null) { 538 mFilteredPrinters.addAll(mPrinters); 539 } else { 540 List<PrinterInfo> printers = (List<PrinterInfo>) results.values; 541 mFilteredPrinters.addAll(printers); 542 } 543 resultCountChanged = (oldPrinterCount != mFilteredPrinters.size()); 544 } 545 if (resultCountChanged) { 546 announceSearchResultIfNeeded(); 547 } 548 notifyDataSetChanged(); 549 } 550 }; 551 } 552 553 public int getUnfilteredCount() { 554 synchronized (mLock) { 555 return mPrinters.size(); 556 } 557 } 558 559 @Override 560 public int getCount() { 561 synchronized (mLock) { 562 return mFilteredPrinters.size(); 563 } 564 } 565 566 @Override 567 public Object getItem(int position) { 568 synchronized (mLock) { 569 return mFilteredPrinters.get(position); 570 } 571 } 572 573 @Override 574 public long getItemId(int position) { 575 return position; 576 } 577 578 @Override 579 public View getDropDownView(int position, View convertView, ViewGroup parent) { 580 return getView(position, convertView, parent); 581 } 582 583 @Override 584 public View getView(int position, View convertView, ViewGroup parent) { 585 if (convertView == null) { 586 convertView = getLayoutInflater().inflate( 587 R.layout.printer_list_item, parent, false); 588 } 589 590 convertView.setEnabled(isActionable(position)); 591 592 final PrinterInfo printer = (PrinterInfo) getItem(position); 593 594 CharSequence title = printer.getName(); 595 Drawable icon = printer.loadIcon(SelectPrinterActivity.this); 596 597 CharSequence printServiceLabel; 598 try { 599 PackageInfo packageInfo = getPackageManager().getPackageInfo( 600 printer.getId().getServiceName().getPackageName(), 0); 601 602 printServiceLabel = packageInfo.applicationInfo.loadLabel(getPackageManager()); 603 } catch (NameNotFoundException e) { 604 printServiceLabel = null; 605 } 606 607 CharSequence description = printer.getDescription(); 608 609 CharSequence subtitle; 610 if (printServiceLabel == null) { 611 subtitle = description; 612 } else if (description == null) { 613 subtitle = printServiceLabel; 614 } else { 615 subtitle = getString(R.string.printer_extended_description_template, 616 printServiceLabel, description); 617 } 618 619 TextView titleView = (TextView) convertView.findViewById(R.id.title); 620 titleView.setText(title); 621 622 TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle); 623 if (!TextUtils.isEmpty(subtitle)) { 624 subtitleView.setText(subtitle); 625 subtitleView.setVisibility(View.VISIBLE); 626 } else { 627 subtitleView.setText(null); 628 subtitleView.setVisibility(View.GONE); 629 } 630 631 ImageView moreInfoView = (ImageView) convertView.findViewById(R.id.more_info); 632 if (printer.getInfoIntent() != null) { 633 moreInfoView.setVisibility(View.VISIBLE); 634 moreInfoView.setOnClickListener(new OnClickListener() { 635 @Override 636 public void onClick(View v) { 637 try { 638 startIntentSender(printer.getInfoIntent().getIntentSender(), null, 0, 0, 0); 639 } catch (SendIntentException e) { 640 Log.e(LOG_TAG, "Could not execute pending info intent: %s", e); 641 } 642 } 643 }); 644 } 645 646 ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); 647 if (icon != null) { 648 iconView.setImageDrawable(icon); 649 iconView.setVisibility(View.VISIBLE); 650 } else { 651 iconView.setVisibility(View.GONE); 652 } 653 654 return convertView; 655 } 656 657 public boolean isActionable(int position) { 658 PrinterInfo printer = (PrinterInfo) getItem(position); 659 return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; 660 } 661 } 662 663 private final class AnnounceFilterResult implements Runnable { 664 private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec 665 666 public void post() { 667 remove(); 668 mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY); 669 } 670 671 public void remove() { 672 mListView.removeCallbacks(this); 673 } 674 675 @Override 676 public void run() { 677 final int count = mListView.getAdapter().getCount(); 678 final String text; 679 if (count <= 0) { 680 text = getString(R.string.print_no_printers); 681 } else { 682 text = getResources().getQuantityString( 683 R.plurals.print_search_result_count_utterance, count, count); 684 } 685 mListView.announceForAccessibility(text); 686 } 687 } 688} 689