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(com.android.internal.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             // Printer is null if this is a context menu for the "add printer" entry
289             if (printer == null) {
290                 return;
291             }
292 
293             menu.setHeaderTitle(printer.getName());
294 
295             // Add the select menu item if applicable.
296             if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) {
297                 MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer,
298                         Menu.NONE, R.string.print_select_printer);
299                 Intent intent = new Intent();
300                 intent.putExtra(EXTRA_PRINTER, printer);
301                 selectItem.setIntent(intent);
302             }
303 
304             // Add the forget menu item if applicable.
305             if (mPrinterRegistry.isFavoritePrinter(printer.getId())) {
306                 MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer,
307                         Menu.NONE, R.string.print_forget_printer);
308                 Intent intent = new Intent();
309                 intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
310                 forgetItem.setIntent(intent);
311             }
312         }
313     }
314 
315     @Override
onContextItemSelected(MenuItem item)316     public boolean onContextItemSelected(MenuItem item) {
317         switch (item.getItemId()) {
318             case R.string.print_select_printer: {
319                 PrinterInfo printer = item.getIntent().getParcelableExtra(EXTRA_PRINTER);
320                 onPrinterSelected(printer);
321             } return true;
322 
323             case R.string.print_forget_printer: {
324                 PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID);
325                 mPrinterRegistry.forgetFavoritePrinter(printerId);
326             } return true;
327         }
328         return false;
329     }
330 
331     /**
332      * Adjust the UI if the enabled print services changed.
333      */
onPrintServicesUpdate()334     private synchronized void onPrintServicesUpdate() {
335         updateEmptyView((DestinationAdapter)mListView.getAdapter());
336         invalidateOptionsMenu();
337     }
338 
339     @Override
onStart()340     public void onStart() {
341         super.onStart();
342         onPrintServicesUpdate();
343     }
344 
345     @Override
onPause()346     public void onPause() {
347         if (mAnnounceFilterResult != null) {
348             mAnnounceFilterResult.remove();
349         }
350         super.onPause();
351     }
352 
353     @Override
onStop()354     public void onStop() {
355         super.onStop();
356     }
357 
358     @Override
onDestroy()359     protected void onDestroy() {
360         if (isFinishing()) {
361             DestinationAdapter adapter = (DestinationAdapter) mListView.getAdapter();
362             List<PrinterInfo> printers = adapter.getPrinters();
363             int numPrinters = adapter.getPrinters().size();
364 
365             MetricsLogger.action(this, MetricsEvent.PRINT_ALL_PRINTERS, numPrinters);
366             MetricsLogger.count(this, PRINTERS_LISTED_COUNT, numPrinters);
367 
368             int numInfoPrinters = 0;
369             int numIconPrinters = 0;
370             for (int i = 0; i < numPrinters; i++) {
371                 PrinterInfo printer = printers.get(i);
372 
373                 if (printer.getInfoIntent() != null) {
374                     numInfoPrinters++;
375                 }
376 
377                 if (printer.getHasCustomPrinterIcon()) {
378                     numIconPrinters++;
379                 }
380             }
381 
382             MetricsLogger.count(this, PRINTERS_INFO_COUNT, numInfoPrinters);
383             MetricsLogger.count(this, PRINTERS_ICON_COUNT, numIconPrinters);
384         }
385 
386         super.onDestroy();
387     }
388 
389     @Override
onActivityResult(int requestCode, int resultCode, Intent data)390     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
391         switch (requestCode) {
392             case INFO_INTENT_REQUEST_CODE:
393                 if (resultCode == RESULT_OK &&
394                         data != null &&
395                         data.getBooleanExtra(PrintService.EXTRA_SELECT_PRINTER, false) &&
396                         mPrinterForInfoIntent != null &&
397                         mPrinterForInfoIntent.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) {
398                     onPrinterSelected(mPrinterForInfoIntent);
399                 }
400                 mPrinterForInfoIntent = null;
401                 break;
402             default:
403                 // not reached
404         }
405     }
406 
onPrinterSelected(PrinterInfo printer)407     private void onPrinterSelected(PrinterInfo printer) {
408         Intent intent = new Intent();
409         intent.putExtra(INTENT_EXTRA_PRINTER, printer);
410         setResult(RESULT_OK, intent);
411         finish();
412     }
413 
updateEmptyView(DestinationAdapter adapter)414     public void updateEmptyView(DestinationAdapter adapter) {
415         if (mListView.getEmptyView() == null) {
416             View emptyView = findViewById(R.id.empty_print_state);
417             mListView.setEmptyView(emptyView);
418         }
419         TextView titleView = findViewById(R.id.title);
420         View progressBar = findViewById(R.id.progress_bar);
421         if (mEnabledPrintServices.size() == 0) {
422             titleView.setText(R.string.print_no_print_services);
423             progressBar.setVisibility(View.GONE);
424         } else if (adapter.getUnfilteredCount() <= 0) {
425             titleView.setText(R.string.print_searching_for_printers);
426             progressBar.setVisibility(View.VISIBLE);
427         } else {
428             titleView.setText(R.string.print_no_printers);
429             progressBar.setVisibility(View.GONE);
430         }
431     }
432 
announceSearchResultIfNeeded()433     private void announceSearchResultIfNeeded() {
434         if (AccessibilityManager.getInstance(this).isEnabled()) {
435             if (mAnnounceFilterResult == null) {
436                 mAnnounceFilterResult = new AnnounceFilterResult();
437             }
438             mAnnounceFilterResult.post();
439         }
440     }
441 
442     @Override
onCreateLoader(int id, Bundle args)443     public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
444         return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
445                 PrintManager.ENABLED_SERVICES);
446     }
447 
448     @Override
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)449     public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
450             List<PrintServiceInfo> services) {
451         mEnabledPrintServices.clear();
452 
453         if (services != null && !services.isEmpty()) {
454             final int numServices = services.size();
455             for (int i = 0; i < numServices; i++) {
456                 PrintServiceInfo service = services.get(i);
457 
458                 mEnabledPrintServices.put(service.getComponentName(), service);
459             }
460         }
461 
462         onPrintServicesUpdate();
463     }
464 
465     @Override
onLoaderReset(Loader<List<PrintServiceInfo>> loader)466     public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
467         if (!isFinishing()) {
468             onLoadFinished(loader, null);
469         }
470     }
471 
472     /**
473      * Return the target SDK of the package that defined the printer.
474      *
475      * @param printer The printer
476      *
477      * @return The target SDK that defined a printer.
478      */
getTargetSDKOfPrintersService(@onNull PrinterInfo printer)479     private int getTargetSDKOfPrintersService(@NonNull PrinterInfo printer) {
480         ApplicationInfo serviceAppInfo;
481         try {
482             serviceAppInfo = getPackageManager().getApplicationInfo(
483                     printer.getId().getServiceName().getPackageName(), 0);
484         } catch (PackageManager.NameNotFoundException e) {
485             Log.e(LOG_TAG, "Could not find package that defined the printer", e);
486             return Build.VERSION_CODES.KITKAT;
487         }
488 
489         return serviceAppInfo.targetSdkVersion;
490     }
491 
492     private final class DestinationAdapter extends BaseAdapter implements Filterable {
493 
494         private final Object mLock = new Object();
495 
496         private final List<PrinterInfo> mPrinters = new ArrayList<>();
497 
498         private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>();
499 
500         private CharSequence mLastSearchString;
501 
502         /**
503          * Get the currently known printers.
504          *
505          * @return The currently known printers
506          */
getPrinters()507         @NonNull List<PrinterInfo> getPrinters() {
508             return mPrinters;
509         }
510 
DestinationAdapter()511         public DestinationAdapter() {
512             mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() {
513                 @Override
514                 public void onPrintersChanged(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 onPrintersInvalid() {
529                     synchronized (mLock) {
530                         mPrinters.clear();
531                         mFilteredPrinters.clear();
532                     }
533                     notifyDataSetInvalidated();
534                 }
535             });
536         }
537 
538         @Override
getFilter()539         public Filter getFilter() {
540             return new Filter() {
541                 @Override
542                 protected FilterResults performFiltering(CharSequence constraint) {
543                     synchronized (mLock) {
544                         if (TextUtils.isEmpty(constraint)) {
545                             return null;
546                         }
547                         FilterResults results = new FilterResults();
548                         List<PrinterInfo> filteredPrinters = new ArrayList<>();
549                         String constraintLowerCase = constraint.toString().toLowerCase();
550                         final int printerCount = mPrinters.size();
551                         for (int i = 0; i < printerCount; i++) {
552                             PrinterInfo printer = mPrinters.get(i);
553                             String description = printer.getDescription();
554                             if (printer.getName().toLowerCase().contains(constraintLowerCase)
555                                     || description != null && description.toLowerCase()
556                                             .contains(constraintLowerCase)) {
557                                 filteredPrinters.add(printer);
558                             }
559                         }
560                         results.values = filteredPrinters;
561                         results.count = filteredPrinters.size();
562                         return results;
563                     }
564                 }
565 
566                 @Override
567                 @SuppressWarnings("unchecked")
568                 protected void publishResults(CharSequence constraint, FilterResults results) {
569                     final boolean resultCountChanged;
570                     synchronized (mLock) {
571                         final int oldPrinterCount = mFilteredPrinters.size();
572                         mLastSearchString = constraint;
573                         mFilteredPrinters.clear();
574                         if (results == null) {
575                             mFilteredPrinters.addAll(mPrinters);
576                         } else {
577                             List<PrinterInfo> printers = (List<PrinterInfo>) results.values;
578                             mFilteredPrinters.addAll(printers);
579                         }
580                         resultCountChanged = (oldPrinterCount != mFilteredPrinters.size());
581                     }
582                     if (resultCountChanged) {
583                         announceSearchResultIfNeeded();
584                     }
585 
586                     if (!mDidSearch) {
587                         MetricsLogger.action(SelectPrinterActivity.this,
588                                 MetricsEvent.ACTION_PRINTER_SEARCH);
589                         mDidSearch = true;
590                     }
591                     notifyDataSetChanged();
592                 }
593             };
594         }
595 
596         public int getUnfilteredCount() {
597             synchronized (mLock) {
598                 return mPrinters.size();
599             }
600         }
601 
602         @Override
603         public int getCount() {
604             synchronized (mLock) {
605                 if (mFilteredPrinters.isEmpty()) {
606                     return 0;
607                 } else {
608                     // Add "add printer" item to the end of the list. If the list is empty there is
609                     // a link on the empty view
610                     return mFilteredPrinters.size() + 1;
611                 }
612             }
613         }
614 
615         @Override
616         public int getViewTypeCount() {
617             return 2;
618         }
619 
620         @Override
621         public int getItemViewType(int position) {
622             // Use separate view types for the "add printer" item an the items referring to printers
623             if (getItem(position) == null) {
624                 return 0;
625             } else {
626                 return 1;
627             }
628         }
629 
630         @Override
631         public Object getItem(int position) {
632             synchronized (mLock) {
633                 if (position < mFilteredPrinters.size()) {
634                     return mFilteredPrinters.get(position);
635                 } else {
636                     // Return null to mark this as the "add printer item"
637                     return null;
638                 }
639             }
640         }
641 
642         @Override
643         public long getItemId(int position) {
644             return position;
645         }
646 
647         @Override
648         public View getDropDownView(int position, View convertView, ViewGroup parent) {
649             return getView(position, convertView, parent);
650         }
651 
652         @Override
653         public View getView(int position, View convertView, ViewGroup parent) {
654             final PrinterInfo printer = (PrinterInfo) getItem(position);
655 
656             // Handle "add printer item"
657             if (printer == null) {
658                 if (convertView == null) {
659                     convertView = getLayoutInflater().inflate(R.layout.add_printer_list_item,
660                             parent, false);
661                 }
662 
663                 return convertView;
664             }
665 
666             if (convertView == null) {
667                 convertView = getLayoutInflater().inflate(
668                         R.layout.printer_list_item, parent, false);
669             }
670 
671             convertView.setEnabled(isActionable(position));
672 
673 
674             CharSequence title = printer.getName();
675             Drawable icon = printer.loadIcon(SelectPrinterActivity.this);
676 
677             PrintServiceInfo service = mEnabledPrintServices.get(printer.getId().getServiceName());
678 
679             CharSequence printServiceLabel = null;
680             if (service != null) {
681                 printServiceLabel = service.getResolveInfo().loadLabel(getPackageManager())
682                         .toString();
683             }
684 
685             CharSequence description = printer.getDescription();
686 
687             CharSequence subtitle;
688             if (TextUtils.isEmpty(printServiceLabel)) {
689                 subtitle = description;
690             } else if (TextUtils.isEmpty(description)) {
691                 subtitle = printServiceLabel;
692             } else {
693                 subtitle = getString(R.string.printer_extended_description_template,
694                         printServiceLabel, description);
695             }
696 
697             TextView titleView = (TextView) convertView.findViewById(R.id.title);
698             titleView.setText(title);
699 
700             TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
701             if (!TextUtils.isEmpty(subtitle)) {
702                 subtitleView.setText(subtitle);
703                 subtitleView.setVisibility(View.VISIBLE);
704             } else {
705                 subtitleView.setText(null);
706                 subtitleView.setVisibility(View.GONE);
707             }
708 
709             LinearLayout moreInfoView = (LinearLayout) convertView.findViewById(R.id.more_info);
710             if (printer.getInfoIntent() != null) {
711                 moreInfoView.setVisibility(View.VISIBLE);
712                 moreInfoView.setOnClickListener(v -> {
713                     Intent fillInIntent = new Intent();
714                     fillInIntent.putExtra(PrintService.EXTRA_CAN_SELECT_PRINTER, true);
715 
716                     try {
717                         mPrinterForInfoIntent = printer;
718                         startIntentSenderForResult(printer.getInfoIntent().getIntentSender(),
719                                 INFO_INTENT_REQUEST_CODE, fillInIntent, 0, 0, 0);
720                     } catch (SendIntentException e) {
721                         mPrinterForInfoIntent = null;
722                         Log.e(LOG_TAG, "Could not execute pending info intent: %s", e);
723                     }
724                 });
725             } else {
726                 moreInfoView.setVisibility(View.GONE);
727             }
728 
729             ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
730             if (icon != null) {
731                 iconView.setVisibility(View.VISIBLE);
732                 if (!isActionable(position)) {
733                     icon.mutate();
734 
735                     TypedValue value = new TypedValue();
736                     getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
737                     icon.setAlpha((int)(value.getFloat() * 255));
738                 }
739                 iconView.setImageDrawable(icon);
740             } else {
741                 iconView.setVisibility(View.GONE);
742             }
743 
744             return convertView;
745         }
746 
747         public boolean isActionable(int position) {
748             PrinterInfo printer =  (PrinterInfo) getItem(position);
749 
750             if (printer == null) {
751                 return true;
752             } else {
753                 return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
754             }
755         }
756     }
757 
758     private final class AnnounceFilterResult implements Runnable {
759         private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
760 
761         public void post() {
762             remove();
763             mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
764         }
765 
766         public void remove() {
767             mListView.removeCallbacks(this);
768         }
769 
770         @Override
771         public void run() {
772             final int count = mListView.getAdapter().getCount();
773             final String text;
774             if (count <= 0) {
775                 text = getString(R.string.print_no_printers);
776             } else {
777                 text = getResources().getQuantityString(
778                     R.plurals.print_search_result_count_utterance, count, count);
779             }
780             mListView.announceForAccessibility(text);
781         }
782     }
783 }
784