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