Home | History | Annotate | Download | only in ui
      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 
    117     private void startAddPrinterActivity() {
    118         MetricsLogger.action(this, MetricsEvent.ACTION_PRINT_SERVICE_ADD);
    119         startActivity(new Intent(this, AddPrinterActivity.class));
    120     }
    121 
    122     @Override
    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
    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
    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
    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
    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
    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      */
    334     private synchronized void onPrintServicesUpdate() {
    335         updateEmptyView((DestinationAdapter)mListView.getAdapter());
    336         invalidateOptionsMenu();
    337     }
    338 
    339     @Override
    340     public void onStart() {
    341         super.onStart();
    342         onPrintServicesUpdate();
    343     }
    344 
    345     @Override
    346     public void onPause() {
    347         if (mAnnounceFilterResult != null) {
    348             mAnnounceFilterResult.remove();
    349         }
    350         super.onPause();
    351     }
    352 
    353     @Override
    354     public void onStop() {
    355         super.onStop();
    356     }
    357 
    358     @Override
    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
    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 
    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 
    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 
    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
    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
    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
    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      */
    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          */
    507         @NonNull List<PrinterInfo> getPrinters() {
    508             return mPrinters;
    509         }
    510 
    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
    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