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 
70 import java.util.ArrayList;
71 import java.util.List;
72 
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         // Hook up the list view.
138         mListView = findViewById(android.R.id.list);
139         final DestinationAdapter adapter = new DestinationAdapter();
140         adapter.registerDataSetObserver(new DataSetObserver() {
141             @Override
142             public void onChanged() {
143                 if (!isFinishing() && adapter.getCount() <= 0) {
144                     updateEmptyView(adapter);
145                 }
146             }
147 
148             @Override
149             public void onInvalidated() {
150                 if (!isFinishing()) {
151                     updateEmptyView(adapter);
152                 }
153             }
154         });
155         mListView.setAdapter(adapter);
156 
157         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
158             @Override
159             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
160                 if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) {
161                     return;
162                 }
163 
164                 PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
165 
166                 if (printer == null) {
167                     startAddPrinterActivity();
168                 } else {
169                     onPrinterSelected(printer);
170                 }
171             }
172         });
173 
174         findViewById(R.id.button).setOnClickListener(new OnClickListener() {
175             @Override public void onClick(View v) {
176                 startAddPrinterActivity();
177             }
178         });
179 
180         registerForContextMenu(mListView);
181 
182         getLoaderManager().initLoader(LOADER_ID_ENABLED_PRINT_SERVICES, null, this);
183 
184         // On first creation:
185         //
186         // If no services are installed, instantly open add printer dialog.
187         // If some are disabled and some are enabled show a toast to notify the user
188         if (savedInstanceState == null || !savedInstanceState.getBoolean(KEY_NOT_FIRST_CREATE)) {
189             List<PrintServiceInfo> allServices =
190                     ((PrintManager) getSystemService(Context.PRINT_SERVICE))
191                             .getPrintServices(PrintManager.ALL_SERVICES);
192             boolean hasEnabledServices = false;
193             boolean hasDisabledServices = false;
194 
195             if (allServices != null) {
196                 final int numServices = allServices.size();
197                 for (int i = 0; i < numServices; i++) {
198                     if (allServices.get(i).isEnabled()) {
199                         hasEnabledServices = true;
200                     } else {
201                         hasDisabledServices = true;
202                     }
203                 }
204             }
205 
206             if (!hasEnabledServices) {
207                 startAddPrinterActivity();
208             } else if (hasDisabledServices) {
209                 String disabledServicesSetting = Settings.Secure.getString(getContentResolver(),
210                         Settings.Secure.DISABLED_PRINT_SERVICES);
211                 if (!TextUtils.isEmpty(disabledServicesSetting)) {
212                     Toast.makeText(this, getString(R.string.print_services_disabled_toast),
213                             Toast.LENGTH_LONG).show();
214                 }
215             }
216         }
217 
218         if (savedInstanceState != null) {
219             mDidSearch = savedInstanceState.getBoolean(KEY_DID_SEARCH);
220             mPrinterForInfoIntent = savedInstanceState.getParcelable(KEY_PRINTER_FOR_INFO_INTENT);
221         }
222     }
223 
224     @Override
onSaveInstanceState(Bundle outState)225     protected void onSaveInstanceState(Bundle outState) {
226         super.onSaveInstanceState(outState);
227         outState.putBoolean(KEY_NOT_FIRST_CREATE, true);
228         outState.putBoolean(KEY_DID_SEARCH, mDidSearch);
229         outState.putParcelable(KEY_PRINTER_FOR_INFO_INTENT, mPrinterForInfoIntent);
230     }
231 
232     @Override
onCreateOptionsMenu(Menu menu)233     public boolean onCreateOptionsMenu(Menu menu) {
234         super.onCreateOptionsMenu(menu);
235 
236         getMenuInflater().inflate(R.menu.select_printer_activity, menu);
237 
238         MenuItem searchItem = menu.findItem(R.id.action_search);
239         SearchView searchView = (SearchView) searchItem.getActionView();
240         searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
241             @Override
242             public boolean onQueryTextSubmit(String query) {
243                 return true;
244             }
245 
246             @Override
247             public boolean onQueryTextChange(String searchString) {
248                 ((DestinationAdapter) mListView.getAdapter()).getFilter().filter(searchString);
249                 return true;
250             }
251         });
252         searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
253             @Override
254             public void onViewAttachedToWindow(View view) {
255                 if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) {
256                     view.announceForAccessibility(getString(
257                             R.string.print_search_box_shown_utterance));
258                 }
259             }
260             @Override
261             public void onViewDetachedFromWindow(View view) {
262                 if (!isFinishing() && AccessibilityManager.getInstance(
263                         SelectPrinterActivity.this).isEnabled()) {
264                     view.announceForAccessibility(getString(
265                             R.string.print_search_box_hidden_utterance));
266                 }
267             }
268         });
269 
270         return true;
271     }
272 
273     @Override
onOptionsItemSelected(MenuItem item)274     public boolean onOptionsItemSelected(MenuItem item) {
275         if (item.getItemId() == android.R.id.home) {
276             finish();
277             return true;
278         } else {
279             return super.onOptionsItemSelected(item);
280         }
281     }
282 
283     @Override
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)284     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
285         if (view == mListView) {
286             final int position = ((AdapterContextMenuInfo) menuInfo).position;
287             PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
288 
289             // Printer is null if this is a context menu for the "add printer" entry
290             if (printer == null) {
291                 return;
292             }
293 
294             menu.setHeaderTitle(printer.getName());
295 
296             // Add the select menu item if applicable.
297             if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) {
298                 MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer,
299                         Menu.NONE, R.string.print_select_printer);
300                 Intent intent = new Intent();
301                 intent.putExtra(EXTRA_PRINTER, printer);
302                 selectItem.setIntent(intent);
303             }
304 
305             // Add the forget menu item if applicable.
306             if (mPrinterRegistry.isFavoritePrinter(printer.getId())) {
307                 MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer,
308                         Menu.NONE, R.string.print_forget_printer);
309                 Intent intent = new Intent();
310                 intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
311                 forgetItem.setIntent(intent);
312             }
313         }
314     }
315 
316     @Override
onContextItemSelected(MenuItem item)317     public boolean onContextItemSelected(MenuItem item) {
318         final int itemId = item.getItemId();
319         if (itemId == R.string.print_select_printer) {
320             PrinterInfo printer = item.getIntent().getParcelableExtra(EXTRA_PRINTER);
321             onPrinterSelected(printer);
322             return true;
323         } else if (itemId == 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                         Bundle options = ActivityOptions.makeBasic()
719                                 .setPendingIntentBackgroundActivityStartMode(
720                                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
721                                 .toBundle();
722                         startIntentSenderForResult(printer.getInfoIntent().getIntentSender(),
723                                 INFO_INTENT_REQUEST_CODE, fillInIntent, 0, 0, 0,
724                                 options);
725                     } catch (SendIntentException e) {
726                         mPrinterForInfoIntent = null;
727                         Log.e(LOG_TAG, "Could not execute pending info intent: %s", e);
728                     }
729                 });
730             } else {
731                 moreInfoView.setVisibility(View.GONE);
732             }
733 
734             ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
735             if (icon != null) {
736                 iconView.setVisibility(View.VISIBLE);
737                 if (!isActionable(position)) {
738                     icon.mutate();
739 
740                     TypedValue value = new TypedValue();
741                     getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
742                     icon.setAlpha((int)(value.getFloat() * 255));
743                 }
744                 iconView.setImageDrawable(icon);
745             } else {
746                 iconView.setVisibility(View.GONE);
747             }
748 
749             return convertView;
750         }
751 
752         public boolean isActionable(int position) {
753             PrinterInfo printer =  (PrinterInfo) getItem(position);
754 
755             if (printer == null) {
756                 return true;
757             } else {
758                 return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
759             }
760         }
761     }
762 
763     private final class AnnounceFilterResult implements Runnable {
764         private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
765 
766         public void post() {
767             remove();
768             mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
769         }
770 
771         public void remove() {
772             mListView.removeCallbacks(this);
773         }
774 
775         @Override
776         public void run() {
777             final int count = mListView.getAdapter().getCount();
778             final String text;
779             if (count <= 0) {
780                 text = getString(R.string.print_no_printers);
781             } else {
782                 text = getResources().getQuantityString(
783                     R.plurals.print_search_result_count_utterance, count, count);
784             }
785             mListView.announceForAccessibility(text);
786         }
787     }
788 }
789