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