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
17package com.android.printspooler.ui;
18
19import android.annotation.IntRange;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.app.ListActivity;
23import android.app.LoaderManager;
24import android.content.ActivityNotFoundException;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.Loader;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.database.DataSetObserver;
32import android.net.Uri;
33import android.os.Bundle;
34import android.print.PrintManager;
35import android.printservice.recommendation.RecommendationInfo;
36import android.print.PrintServiceRecommendationsLoader;
37import android.print.PrintServicesLoader;
38import android.printservice.PrintServiceInfo;
39import android.provider.Settings;
40import android.text.TextUtils;
41import android.util.ArraySet;
42import android.util.Log;
43import android.util.Pair;
44import android.view.View;
45import android.view.ViewGroup;
46import android.widget.Adapter;
47import android.widget.AdapterView;
48import android.widget.BaseAdapter;
49import android.widget.ImageView;
50import android.widget.TextView;
51import com.android.internal.logging.MetricsLogger;
52import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
53import com.android.printspooler.R;
54
55import java.text.Collator;
56import java.util.ArrayList;
57import java.util.Collections;
58import java.util.Comparator;
59import 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 */
73public 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