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