Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2016 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.IntRange;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.app.ListActivity;
     23 import android.app.LoaderManager;
     24 import android.content.ActivityNotFoundException;
     25 import android.content.ComponentName;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.Loader;
     29 import android.content.pm.PackageManager;
     30 import android.content.pm.ResolveInfo;
     31 import android.database.DataSetObserver;
     32 import android.net.Uri;
     33 import android.os.Bundle;
     34 import android.print.PrintManager;
     35 import android.printservice.recommendation.RecommendationInfo;
     36 import android.print.PrintServiceRecommendationsLoader;
     37 import android.print.PrintServicesLoader;
     38 import android.printservice.PrintServiceInfo;
     39 import android.provider.Settings;
     40 import android.text.TextUtils;
     41 import android.util.ArraySet;
     42 import android.util.Log;
     43 import android.util.Pair;
     44 import android.view.View;
     45 import android.view.ViewGroup;
     46 import android.widget.Adapter;
     47 import android.widget.AdapterView;
     48 import android.widget.BaseAdapter;
     49 import android.widget.ImageView;
     50 import android.widget.TextView;
     51 import com.android.internal.logging.MetricsLogger;
     52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     53 import com.android.printspooler.R;
     54 
     55 import java.text.Collator;
     56 import java.util.ArrayList;
     57 import java.util.Collections;
     58 import java.util.Comparator;
     59 import java.util.List;
     60 
     61 /**
     62  * This is an activity for adding a printer or. It consists of a list fed from three adapters:
     63  * <ul>
     64  *     <li>{@link #mEnabledServicesAdapter} for all enabled services. If a service has an {@link
     65  *         PrintServiceInfo#getAddPrintersActivityName() add printer activity} this is started
     66  *         when the item is clicked.</li>
     67  *     <li>{@link #mDisabledServicesAdapter} for all disabled services. Once clicked the settings page
     68  *         for this service is opened.</li>
     69  *     <li>{@link #mRecommendedServicesAdapter} for a link to all services. If this item is clicked
     70  *         the market app is opened to show all print services.</li>
     71  * </ul>
     72  */
     73 public class AddPrinterActivity extends ListActivity implements AdapterView.OnItemClickListener {
     74     private static final String LOG_TAG = "AddPrinterActivity";
     75 
     76     /** Ids for the loaders */
     77     private static final int LOADER_ID_ENABLED_SERVICES = 1;
     78     private static final int LOADER_ID_DISABLED_SERVICES = 2;
     79     private static final int LOADER_ID_RECOMMENDED_SERVICES = 3;
     80     private static final int LOADER_ID_ALL_SERVICES = 4;
     81 
     82     /**
     83      * The enabled services list. This is filled from the {@link #LOADER_ID_ENABLED_SERVICES}
     84      * loader in {@link PrintServiceInfoLoaderCallbacks#onLoadFinished}.
     85      */
     86     private EnabledServicesAdapter mEnabledServicesAdapter;
     87 
     88     /**
     89      * The disabled services list. This is filled from the {@link #LOADER_ID_DISABLED_SERVICES}
     90      * loader in {@link PrintServiceInfoLoaderCallbacks#onLoadFinished}.
     91      */
     92     private DisabledServicesAdapter mDisabledServicesAdapter;
     93 
     94     /**
     95      * The recommended services list. This is filled from the
     96      * {@link #LOADER_ID_RECOMMENDED_SERVICES} loader in
     97      * {@link PrintServicePrintServiceRecommendationLoaderCallbacks#onLoadFinished}.
     98      */
     99     private RecommendedServicesAdapter mRecommendedServicesAdapter;
    100 
    101     private static final String PKG_NAME_VENDING = "com.android.vending";
    102     private boolean mHasVending;
    103     private NoPrintServiceMessageAdapter mNoPrintServiceMessageAdapter;
    104 
    105     @Override
    106     protected void onCreate(@Nullable Bundle savedInstanceState) {
    107         super.onCreate(savedInstanceState);
    108 
    109         setContentView(R.layout.add_printer_activity);
    110 
    111         try {
    112             getPackageManager().getPackageInfo(PKG_NAME_VENDING, 0);
    113             mHasVending = true;
    114         } catch (PackageManager.NameNotFoundException e) {
    115             mHasVending = false;
    116         }
    117         mEnabledServicesAdapter = new EnabledServicesAdapter();
    118         mDisabledServicesAdapter = new DisabledServicesAdapter();
    119         if (mHasVending) {
    120             mRecommendedServicesAdapter = new RecommendedServicesAdapter();
    121         } else {
    122             mNoPrintServiceMessageAdapter = new NoPrintServiceMessageAdapter();
    123         }
    124 
    125         ArrayList<ActionAdapter> adapterList = new ArrayList<>(3);
    126         adapterList.add(mEnabledServicesAdapter);
    127         if (mHasVending) {
    128             adapterList.add(mRecommendedServicesAdapter);
    129         }
    130         adapterList.add(mDisabledServicesAdapter);
    131         if (!mHasVending) {
    132             adapterList.add(mNoPrintServiceMessageAdapter);
    133         }
    134 
    135         setListAdapter(new CombinedAdapter(adapterList));
    136 
    137         getListView().setOnItemClickListener(this);
    138 
    139         PrintServiceInfoLoaderCallbacks printServiceLoaderCallbacks =
    140                 new PrintServiceInfoLoaderCallbacks();
    141 
    142         getLoaderManager().initLoader(LOADER_ID_ENABLED_SERVICES, null, printServiceLoaderCallbacks);
    143         getLoaderManager().initLoader(LOADER_ID_DISABLED_SERVICES, null, printServiceLoaderCallbacks);
    144         if (mHasVending) {
    145             getLoaderManager().initLoader(LOADER_ID_RECOMMENDED_SERVICES, null,
    146                     new PrintServicePrintServiceRecommendationLoaderCallbacks());
    147         }
    148         getLoaderManager().initLoader(LOADER_ID_ALL_SERVICES, null, printServiceLoaderCallbacks);
    149     }
    150 
    151     @Override
    152     protected void onDestroy() {
    153         if (isFinishing()) {
    154             MetricsLogger.action(this, MetricsEvent.PRINT_ADD_PRINTERS,
    155                     mEnabledServicesAdapter.getCount());
    156         }
    157 
    158         super.onDestroy();
    159     }
    160 
    161     /**
    162      * Callbacks for the loaders operating on list of {@link PrintServiceInfo print service infos}.
    163      */
    164     private class PrintServiceInfoLoaderCallbacks implements
    165             LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
    166         @Override
    167         public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
    168             switch (id) {
    169                 case LOADER_ID_ENABLED_SERVICES:
    170                     return new PrintServicesLoader(
    171                             (PrintManager) getSystemService(Context.PRINT_SERVICE),
    172                             AddPrinterActivity.this, PrintManager.ENABLED_SERVICES);
    173                 case LOADER_ID_DISABLED_SERVICES:
    174                     return new PrintServicesLoader(
    175                             (PrintManager) getSystemService(Context.PRINT_SERVICE),
    176                             AddPrinterActivity.this, PrintManager.DISABLED_SERVICES);
    177                 case LOADER_ID_ALL_SERVICES:
    178                     return new PrintServicesLoader(
    179                             (PrintManager) getSystemService(Context.PRINT_SERVICE),
    180                             AddPrinterActivity.this, PrintManager.ALL_SERVICES);
    181                 default:
    182                     // not reached
    183                     return null;
    184             }
    185         }
    186 
    187 
    188         @Override
    189         public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
    190                 List<PrintServiceInfo> data) {
    191             switch (loader.getId()) {
    192                 case LOADER_ID_ENABLED_SERVICES:
    193                     mEnabledServicesAdapter.updateData(data);
    194                     break;
    195                 case LOADER_ID_DISABLED_SERVICES:
    196                     mDisabledServicesAdapter.updateData(data);
    197                     break;
    198                 case LOADER_ID_ALL_SERVICES:
    199                     if (mHasVending) {
    200                         mRecommendedServicesAdapter.updateInstalledServices(data);
    201                     } else {
    202                         mNoPrintServiceMessageAdapter.updateInstalledServices(data);
    203                     }
    204                 default:
    205                     // not reached
    206             }
    207         }
    208 
    209         @Override
    210         public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
    211             if (!isFinishing()) {
    212                 switch (loader.getId()) {
    213                     case LOADER_ID_ENABLED_SERVICES:
    214                         mEnabledServicesAdapter.updateData(null);
    215                         break;
    216                     case LOADER_ID_DISABLED_SERVICES:
    217                         mDisabledServicesAdapter.updateData(null);
    218                         break;
    219                     case LOADER_ID_ALL_SERVICES:
    220                         if (mHasVending) {
    221                             mRecommendedServicesAdapter.updateInstalledServices(null);
    222                         } else {
    223                             mNoPrintServiceMessageAdapter.updateInstalledServices(null);
    224                         }
    225                         break;
    226                     default:
    227                         // not reached
    228                 }
    229             }
    230         }
    231     }
    232 
    233     /**
    234      * Callbacks for the loaders operating on list of {@link RecommendationInfo print service
    235      * recommendations}.
    236      */
    237     private class PrintServicePrintServiceRecommendationLoaderCallbacks implements
    238             LoaderManager.LoaderCallbacks<List<RecommendationInfo>> {
    239         @Override
    240         public Loader<List<RecommendationInfo>> onCreateLoader(int id, Bundle args) {
    241             return new PrintServiceRecommendationsLoader(
    242                     (PrintManager) getSystemService(Context.PRINT_SERVICE),
    243                     AddPrinterActivity.this);
    244         }
    245 
    246 
    247         @Override
    248         public void onLoadFinished(Loader<List<RecommendationInfo>> loader,
    249                 List<RecommendationInfo> data) {
    250             mRecommendedServicesAdapter.updateRecommendations(data);
    251         }
    252 
    253         @Override
    254         public void onLoaderReset(Loader<List<RecommendationInfo>> loader) {
    255             if (!isFinishing()) {
    256                 mRecommendedServicesAdapter.updateRecommendations(null);
    257             }
    258         }
    259     }
    260 
    261     @Override
    262     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    263         ((ActionAdapter) getListAdapter()).performAction(position);
    264     }
    265 
    266     /**
    267      * Marks an adapter that can can perform an action for a position in it's list.
    268      */
    269     private abstract class ActionAdapter extends BaseAdapter {
    270         /**
    271          * Perform the action for a position in the list.
    272          *
    273          * @param position The position of the item
    274          */
    275         abstract void performAction(@IntRange(from = 0) int position);
    276 
    277         @Override
    278         public boolean areAllItemsEnabled() {
    279             return false;
    280         }
    281     }
    282 
    283     /**
    284      * An adapter presenting multiple sub adapters as a single combined adapter.
    285      */
    286     private class CombinedAdapter extends ActionAdapter {
    287         /** The adapters to combine */
    288         private final @NonNull ArrayList<ActionAdapter> mAdapters;
    289 
    290         /**
    291          * Create a combined adapter.
    292          *
    293          * @param adapters the list of adapters to combine
    294          */
    295         CombinedAdapter(@NonNull ArrayList<ActionAdapter> adapters) {
    296             mAdapters = adapters;
    297 
    298             final int numAdapters = mAdapters.size();
    299             for (int i = 0; i < numAdapters; i++) {
    300                 mAdapters.get(i).registerDataSetObserver(new DataSetObserver() {
    301                     @Override
    302                     public void onChanged() {
    303                         notifyDataSetChanged();
    304                     }
    305 
    306                     @Override
    307                     public void onInvalidated() {
    308                         notifyDataSetChanged();
    309                     }
    310                 });
    311             }
    312         }
    313 
    314         @Override
    315         public int getCount() {
    316             int totalCount = 0;
    317 
    318             final int numAdapters = mAdapters.size();
    319             for (int i = 0; i < numAdapters; i++) {
    320                 totalCount += mAdapters.get(i).getCount();
    321             }
    322 
    323             return totalCount;
    324         }
    325 
    326         /**
    327          * Find the sub adapter and the position in the sub-adapter the position in the combined
    328          * adapter refers to.
    329          *
    330          * @param position The position in the combined adapter
    331          *
    332          * @return The pair of adapter and position in sub adapter
    333          */
    334         private @NonNull Pair<ActionAdapter, Integer> getSubAdapter(int position) {
    335             final int numAdapters = mAdapters.size();
    336             for (int i = 0; i < numAdapters; i++) {
    337                 ActionAdapter adapter = mAdapters.get(i);
    338 
    339                 if (position < adapter.getCount()) {
    340                     return new Pair<>(adapter, position);
    341                 } else {
    342                     position -= adapter.getCount();
    343                 }
    344             }
    345 
    346             throw new IllegalArgumentException("Invalid position");
    347         }
    348 
    349         @Override
    350         public int getItemViewType(int position) {
    351             int numLowerViewTypes = 0;
    352 
    353             final int numAdapters = mAdapters.size();
    354             for (int i = 0; i < numAdapters; i++) {
    355                 Adapter adapter = mAdapters.get(i);
    356 
    357                 if (position < adapter.getCount()) {
    358                     return numLowerViewTypes + adapter.getItemViewType(position);
    359                 } else {
    360                     numLowerViewTypes += adapter.getViewTypeCount();
    361                     position -= adapter.getCount();
    362                 }
    363             }
    364 
    365             throw new IllegalArgumentException("Invalid position");
    366         }
    367 
    368         @Override
    369         public int getViewTypeCount() {
    370             int totalViewCount = 0;
    371 
    372             final int numAdapters = mAdapters.size();
    373             for (int i = 0; i < numAdapters; i++) {
    374                 totalViewCount += mAdapters.get(i).getViewTypeCount();
    375             }
    376 
    377             return totalViewCount;
    378         }
    379 
    380         @Override
    381         public View getView(int position, View convertView, ViewGroup parent) {
    382             Pair<ActionAdapter, Integer> realPosition = getSubAdapter(position);
    383 
    384             return realPosition.first.getView(realPosition.second, convertView, parent);
    385         }
    386 
    387         @Override
    388         public Object getItem(int position) {
    389             Pair<ActionAdapter, Integer> realPosition = getSubAdapter(position);
    390 
    391             return realPosition.first.getItem(realPosition.second);
    392         }
    393 
    394         @Override
    395         public long getItemId(int position) {
    396             return position;
    397         }
    398 
    399         @Override
    400         public boolean isEnabled(int position) {
    401             Pair<ActionAdapter, Integer> realPosition = getSubAdapter(position);
    402 
    403             return realPosition.first.isEnabled(realPosition.second);
    404         }
    405 
    406         @Override
    407         public void performAction(@IntRange(from = 0) int position) {
    408             Pair<ActionAdapter, Integer> realPosition = getSubAdapter(position);
    409 
    410             realPosition.first.performAction(realPosition.second);
    411         }
    412     }
    413 
    414     /**
    415      * Superclass for all adapters that just display a list of {@link PrintServiceInfo}.
    416      */
    417     private abstract class PrintServiceInfoAdapter extends ActionAdapter {
    418         /**
    419          * Raw data of the list.
    420          *
    421          * @see #updateData(List)
    422          */
    423         private @NonNull List<PrintServiceInfo> mServices;
    424 
    425         /**
    426          * Create a new adapter.
    427          */
    428         PrintServiceInfoAdapter() {
    429             mServices = Collections.emptyList();
    430         }
    431 
    432         /**
    433          * Update the data.
    434          *
    435          * @param services The new raw data.
    436          */
    437         void updateData(@Nullable List<PrintServiceInfo> services) {
    438             if (services == null || services.isEmpty()) {
    439                 mServices = Collections.emptyList();
    440             } else {
    441                 mServices = services;
    442             }
    443 
    444             notifyDataSetChanged();
    445         }
    446 
    447         @Override
    448         public int getViewTypeCount() {
    449             return 2;
    450         }
    451 
    452         @Override
    453         public int getItemViewType(int position) {
    454             if (position == 0) {
    455                 return 0;
    456             } else {
    457                 return 1;
    458             }
    459         }
    460 
    461         @Override
    462         public int getCount() {
    463             if (mServices.isEmpty()) {
    464                 return 0;
    465             } else {
    466                 return mServices.size() + 1;
    467             }
    468         }
    469 
    470         @Override
    471         public Object getItem(int position) {
    472             if (position == 0) {
    473                 return null;
    474             } else {
    475                 return mServices.get(position - 1);
    476             }
    477         }
    478 
    479         @Override
    480         public boolean isEnabled(int position) {
    481             return position != 0;
    482         }
    483 
    484         @Override
    485         public long getItemId(int position) {
    486             return position;
    487         }
    488     }
    489 
    490     /**
    491      * Adapter for the enabled services.
    492      */
    493     private class EnabledServicesAdapter extends PrintServiceInfoAdapter {
    494         @Override
    495         public void performAction(@IntRange(from = 0) int position) {
    496             Intent intent = getAddPrinterIntent((PrintServiceInfo) getItem(position));
    497             if (intent != null) {
    498                 try {
    499                     startActivity(intent);
    500                 } catch (ActivityNotFoundException|SecurityException e) {
    501                     Log.e(LOG_TAG, "Cannot start add printers activity", e);
    502                 }
    503             }
    504         }
    505 
    506         /**
    507          * Get the intent used to launch the add printers activity.
    508          *
    509          * @param service The service the printer should be added for
    510          *
    511          * @return The intent to launch the activity or null if the activity could not be launched.
    512          */
    513         private Intent getAddPrinterIntent(@NonNull PrintServiceInfo service) {
    514             String addPrinterActivityName = service.getAddPrintersActivityName();
    515 
    516             if (!TextUtils.isEmpty(addPrinterActivityName)) {
    517                 Intent intent = new Intent(Intent.ACTION_MAIN);
    518                 intent.setComponent(new ComponentName(service.getComponentName().getPackageName(),
    519                                 addPrinterActivityName));
    520 
    521                 List<ResolveInfo> resolvedActivities = getPackageManager().queryIntentActivities(
    522                         intent, 0);
    523                 if (!resolvedActivities.isEmpty()) {
    524                     // The activity is a component name, therefore it is one or none.
    525                     if (resolvedActivities.get(0).activityInfo.exported) {
    526                         return intent;
    527                     }
    528                 }
    529             }
    530 
    531             return null;
    532         }
    533 
    534         @Override
    535         public View getView(int position, View convertView, ViewGroup parent) {
    536             if (position == 0) {
    537                 if (convertView == null) {
    538                     convertView = getLayoutInflater().inflate(R.layout.add_printer_list_header,
    539                             parent, false);
    540                 }
    541 
    542                 ((TextView) convertView.findViewById(R.id.text))
    543                         .setText(R.string.enabled_services_title);
    544 
    545                 return convertView;
    546             }
    547 
    548             if (convertView == null) {
    549                 convertView = getLayoutInflater().inflate(R.layout.enabled_print_services_list_item,
    550                         parent, false);
    551             }
    552 
    553             PrintServiceInfo service = (PrintServiceInfo) getItem(position);
    554 
    555             TextView title = (TextView) convertView.findViewById(R.id.title);
    556             ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
    557             TextView subtitle = (TextView) convertView.findViewById(R.id.subtitle);
    558 
    559             title.setText(service.getResolveInfo().loadLabel(getPackageManager()));
    560             icon.setImageDrawable(service.getResolveInfo().loadIcon(getPackageManager()));
    561 
    562             if (getAddPrinterIntent(service) == null) {
    563                 subtitle.setText(getString(R.string.cannot_add_printer));
    564             } else {
    565                 subtitle.setText(getString(R.string.select_to_add_printers));
    566             }
    567 
    568             return convertView;
    569         }
    570     }
    571 
    572     /**
    573      * Adapter for the disabled services.
    574      */
    575     private class DisabledServicesAdapter extends PrintServiceInfoAdapter {
    576         @Override
    577         public void performAction(@IntRange(from = 0) int position) {
    578             ((PrintManager) getSystemService(Context.PRINT_SERVICE)).setPrintServiceEnabled(
    579                     ((PrintServiceInfo) getItem(position)).getComponentName(), true);
    580         }
    581 
    582         @Override
    583         public View getView(int position, View convertView, ViewGroup parent) {
    584             if (position == 0) {
    585                 if (convertView == null) {
    586                     convertView = getLayoutInflater().inflate(R.layout.add_printer_list_header,
    587                             parent, false);
    588                 }
    589 
    590                 ((TextView) convertView.findViewById(R.id.text))
    591                         .setText(R.string.disabled_services_title);
    592 
    593                 return convertView;
    594             }
    595 
    596             if (convertView == null) {
    597                 convertView = getLayoutInflater().inflate(
    598                         R.layout.disabled_print_services_list_item, parent, false);
    599             }
    600 
    601             PrintServiceInfo service = (PrintServiceInfo) getItem(position);
    602 
    603             TextView title = (TextView) convertView.findViewById(R.id.title);
    604             ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
    605 
    606             title.setText(service.getResolveInfo().loadLabel(getPackageManager()));
    607             icon.setImageDrawable(service.getResolveInfo().loadIcon(getPackageManager()));
    608 
    609             return convertView;
    610         }
    611     }
    612 
    613     /**
    614      * Adapter for the recommended services.
    615      */
    616     private class RecommendedServicesAdapter extends ActionAdapter {
    617         /** Package names of all installed print services */
    618         private @NonNull final ArraySet<String> mInstalledServices;
    619 
    620         /** All print service recommendations */
    621         private @Nullable List<RecommendationInfo> mRecommendations;
    622 
    623         /**
    624          * Sorted print service recommendations for services that are not installed
    625          *
    626          * @see #filterRecommendations
    627          */
    628         private @Nullable List<RecommendationInfo> mFilteredRecommendations;
    629 
    630         /**
    631          * Create a new adapter.
    632          */
    633         private RecommendedServicesAdapter() {
    634             mInstalledServices = new ArraySet<>();
    635         }
    636 
    637         @Override
    638         public int getCount() {
    639             if (mFilteredRecommendations == null) {
    640                 return 2;
    641             } else {
    642                 return mFilteredRecommendations.size() + 2;
    643             }
    644         }
    645 
    646         @Override
    647         public int getViewTypeCount() {
    648             return 3;
    649         }
    650 
    651         /**
    652          * @return The position the all services link is at.
    653          */
    654         private int getAllServicesPos() {
    655             return getCount() - 1;
    656         }
    657 
    658         @Override
    659         public int getItemViewType(int position) {
    660             if (position == 0) {
    661                 return 0;
    662             } else if (getAllServicesPos() == position) {
    663                 return 1;
    664             } else {
    665                 return 2;
    666             }
    667         }
    668 
    669         @Override
    670         public Object getItem(int position) {
    671             if (position == 0 || position == getAllServicesPos()) {
    672                 return null;
    673             } else {
    674                 return mFilteredRecommendations.get(position - 1);
    675             }
    676         }
    677 
    678         @Override
    679         public long getItemId(int position) {
    680             return position;
    681         }
    682 
    683         @Override
    684         public View getView(int position, View convertView, ViewGroup parent) {
    685             if (position == 0) {
    686                 if (convertView == null) {
    687                     convertView = getLayoutInflater().inflate(R.layout.add_printer_list_header,
    688                             parent, false);
    689                 }
    690 
    691                 ((TextView) convertView.findViewById(R.id.text))
    692                         .setText(R.string.recommended_services_title);
    693 
    694                 return convertView;
    695             } else if (position == getAllServicesPos()) {
    696                 if (convertView == null) {
    697                     convertView = getLayoutInflater().inflate(R.layout.all_print_services_list_item,
    698                             parent, false);
    699                 }
    700             } else {
    701                 RecommendationInfo recommendation = (RecommendationInfo) getItem(position);
    702 
    703                 if (convertView == null) {
    704                     convertView = getLayoutInflater().inflate(
    705                             R.layout.print_service_recommendations_list_item, parent, false);
    706                 }
    707 
    708                 ((TextView) convertView.findViewById(R.id.title)).setText(recommendation.getName());
    709 
    710                 ((TextView) convertView.findViewById(R.id.subtitle)).setText(getResources()
    711                         .getQuantityString(R.plurals.print_services_recommendation_subtitle,
    712                                 recommendation.getNumDiscoveredPrinters(),
    713                                 recommendation.getNumDiscoveredPrinters()));
    714 
    715                 return convertView;
    716             }
    717 
    718             return convertView;
    719         }
    720 
    721         @Override
    722         public boolean isEnabled(int position) {
    723             return position != 0;
    724         }
    725 
    726         @Override
    727         public void performAction(@IntRange(from = 0) int position) {
    728             if (position == getAllServicesPos()) {
    729                 String searchUri = Settings.Secure
    730                         .getString(getContentResolver(), Settings.Secure.PRINT_SERVICE_SEARCH_URI);
    731 
    732                 if (searchUri != null) {
    733                     try {
    734                         startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)));
    735                     } catch (ActivityNotFoundException e) {
    736                         Log.e(LOG_TAG, "Cannot start market", e);
    737                     }
    738                 }
    739             } else {
    740                 RecommendationInfo recommendation = (RecommendationInfo) getItem(position);
    741 
    742                 MetricsLogger.action(AddPrinterActivity.this,
    743                         MetricsEvent.ACTION_PRINT_RECOMMENDED_SERVICE_INSTALL,
    744                         recommendation.getPackageName().toString());
    745 
    746                 try {
    747                     startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(
    748                             R.string.uri_package_details, recommendation.getPackageName()))));
    749                 } catch (ActivityNotFoundException e) {
    750                     Log.e(LOG_TAG, "Cannot start market", e);
    751                 }
    752             }
    753         }
    754 
    755         /**
    756          * Filter recommended services.
    757          */
    758         private void filterRecommendations() {
    759             if (mRecommendations == null) {
    760                 mFilteredRecommendations = null;
    761             } else {
    762                 mFilteredRecommendations = new ArrayList<>();
    763 
    764                 // Filter out recommendations for already installed services
    765                 final int numRecommendations = mRecommendations.size();
    766                 for (int i = 0; i < numRecommendations; i++) {
    767                     RecommendationInfo recommendation = mRecommendations.get(i);
    768 
    769                     if (!mInstalledServices.contains(recommendation.getPackageName())) {
    770                         mFilteredRecommendations.add(recommendation);
    771                     }
    772                 }
    773             }
    774 
    775             notifyDataSetChanged();
    776         }
    777 
    778         /**
    779          * Update the installed print services.
    780          *
    781          * @param services The new set of services
    782          */
    783         public void updateInstalledServices(List<PrintServiceInfo> services) {
    784             mInstalledServices.clear();
    785 
    786             if (services != null) {
    787                 final int numServices = services.size();
    788                 for (int i = 0; i < numServices; i++) {
    789                     mInstalledServices.add(services.get(i).getComponentName().getPackageName());
    790                 }
    791             }
    792 
    793             filterRecommendations();
    794         }
    795 
    796         /**
    797          * Update the recommended print services.
    798          *
    799          * @param recommendations The new set of recommendations
    800          */
    801         public void updateRecommendations(List<RecommendationInfo> recommendations) {
    802             if (recommendations != null) {
    803                 final Collator collator = Collator.getInstance();
    804 
    805                 // Sort recommendations (early conditions are more important)
    806                 // - higher number of discovered printers first
    807                 // - single vendor services first
    808                 // - alphabetically
    809                 Collections.sort(recommendations,
    810                         new Comparator<RecommendationInfo>() {
    811                             @Override public int compare(RecommendationInfo o1,
    812                                     RecommendationInfo o2) {
    813                                 if (o1.getNumDiscoveredPrinters() !=
    814                                         o2.getNumDiscoveredPrinters()) {
    815                                     return o2.getNumDiscoveredPrinters() -
    816                                             o1.getNumDiscoveredPrinters();
    817                                 } else if (o1.recommendsMultiVendorService()
    818                                         != o2.recommendsMultiVendorService()) {
    819                                     if (o1.recommendsMultiVendorService()) {
    820                                         return 1;
    821                                     } else {
    822                                         return -1;
    823                                     }
    824                                 } else {
    825                                     return collator.compare(o1.getName().toString(),
    826                                             o2.getName().toString());
    827                                 }
    828                             }
    829                         });
    830             }
    831 
    832             mRecommendations = recommendations;
    833 
    834             filterRecommendations();
    835         }
    836     }
    837 
    838     private class NoPrintServiceMessageAdapter extends ActionAdapter {
    839         private boolean mHasPrintService;
    840 
    841         void updateInstalledServices(@Nullable List<PrintServiceInfo> services) {
    842             if (services == null || services.isEmpty()) {
    843                 mHasPrintService = false;
    844             } else {
    845                 mHasPrintService = true;
    846             }
    847             notifyDataSetChanged();
    848         }
    849 
    850         @Override
    851         public int getCount() {
    852             return mHasPrintService ? 0 : 1;
    853         }
    854 
    855         @Override
    856         public int getViewTypeCount() {
    857             return 1;
    858         }
    859 
    860         @Override
    861         public int getItemViewType(int position) {
    862             return 0;
    863         }
    864 
    865         @Override
    866         public Object getItem(int position) {
    867             return null;
    868         }
    869 
    870         @Override
    871         public long getItemId(int position) {
    872             return position;
    873         }
    874 
    875         @Override
    876         public View getView(int position, View convertView, ViewGroup parent) {
    877             if (convertView == null) {
    878                 convertView = getLayoutInflater().inflate(R.layout.no_print_services_message,
    879                     parent, false);
    880             }
    881             return convertView;
    882         }
    883 
    884         @Override
    885         public boolean isEnabled(int position) {
    886             return position != 0;
    887         }
    888 
    889         @Override
    890         public void performAction(@IntRange(from = 0) int position) {
    891             return;
    892         }
    893     }
    894 }
    895