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