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