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