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