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
onCreate(@ullable Bundle savedInstanceState)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
onDestroy()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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> data)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
onLoaderReset(Loader<List<PrintServiceInfo>> loader)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<List<RecommendationInfo>> loader, List<RecommendationInfo> data)248         public void onLoadFinished(Loader<List<RecommendationInfo>> loader,
249                 List<RecommendationInfo> data) {
250             mRecommendedServicesAdapter.updateRecommendations(data);
251         }
252 
253         @Override
onLoaderReset(Loader<List<RecommendationInfo>> loader)254         public void onLoaderReset(Loader<List<RecommendationInfo>> loader) {
255             if (!isFinishing()) {
256                 mRecommendedServicesAdapter.updateRecommendations(null);
257             }
258         }
259     }
260 
261     @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)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          */
performAction(@ntRangefrom = 0) int position)275         abstract void performAction(@IntRange(from = 0) int position);
276 
277         @Override
areAllItemsEnabled()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          */
CombinedAdapter(@onNull ArrayList<ActionAdapter> adapters)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
getCount()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          */
getSubAdapter(int position)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
getItemViewType(int position)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
getViewTypeCount()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
getView(int position, View convertView, ViewGroup parent)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
getItem(int position)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
getItemId(int position)395         public long getItemId(int position) {
396             return position;
397         }
398 
399         @Override
isEnabled(int position)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
performAction(@ntRangefrom = 0) int position)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          */
PrintServiceInfoAdapter()428         PrintServiceInfoAdapter() {
429             mServices = Collections.emptyList();
430         }
431 
432         /**
433          * Update the data.
434          *
435          * @param services The new raw data.
436          */
updateData(@ullable List<PrintServiceInfo> services)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
getViewTypeCount()448         public int getViewTypeCount() {
449             return 2;
450         }
451 
452         @Override
getItemViewType(int position)453         public int getItemViewType(int position) {
454             if (position == 0) {
455                 return 0;
456             } else {
457                 return 1;
458             }
459         }
460 
461         @Override
getCount()462         public int getCount() {
463             if (mServices.isEmpty()) {
464                 return 0;
465             } else {
466                 return mServices.size() + 1;
467             }
468         }
469 
470         @Override
getItem(int position)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
isEnabled(int position)480         public boolean isEnabled(int position) {
481             return position != 0;
482         }
483 
484         @Override
getItemId(int position)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
performAction(@ntRangefrom = 0) int position)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          */
getAddPrinterIntent(@onNull PrintServiceInfo service)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
getView(int position, View convertView, ViewGroup parent)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
performAction(@ntRangefrom = 0) int position)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
getView(int position, View convertView, ViewGroup parent)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          */
RecommendedServicesAdapter()633         private RecommendedServicesAdapter() {
634             mInstalledServices = new ArraySet<>();
635         }
636 
637         @Override
getCount()638         public int getCount() {
639             if (mFilteredRecommendations == null) {
640                 return 2;
641             } else {
642                 return mFilteredRecommendations.size() + 2;
643             }
644         }
645 
646         @Override
getViewTypeCount()647         public int getViewTypeCount() {
648             return 3;
649         }
650 
651         /**
652          * @return The position the all services link is at.
653          */
getAllServicesPos()654         private int getAllServicesPos() {
655             return getCount() - 1;
656         }
657 
658         @Override
getItemViewType(int position)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
getItem(int position)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
getItemId(int position)679         public long getItemId(int position) {
680             return position;
681         }
682 
683         @Override
getView(int position, View convertView, ViewGroup parent)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
isEnabled(int position)722         public boolean isEnabled(int position) {
723             return position != 0;
724         }
725 
726         @Override
performAction(@ntRangefrom = 0) int position)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          */
filterRecommendations()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          */
updateInstalledServices(List<PrintServiceInfo> services)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          */
updateRecommendations(List<RecommendationInfo> recommendations)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 
updateInstalledServices(@ullable List<PrintServiceInfo> services)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
getCount()851         public int getCount() {
852             return mHasPrintService ? 0 : 1;
853         }
854 
855         @Override
getViewTypeCount()856         public int getViewTypeCount() {
857             return 1;
858         }
859 
860         @Override
getItemViewType(int position)861         public int getItemViewType(int position) {
862             return 0;
863         }
864 
865         @Override
getItem(int position)866         public Object getItem(int position) {
867             return null;
868         }
869 
870         @Override
getItemId(int position)871         public long getItemId(int position) {
872             return position;
873         }
874 
875         @Override
getView(int position, View convertView, ViewGroup parent)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
isEnabled(int position)885         public boolean isEnabled(int position) {
886             return position != 0;
887         }
888 
889         @Override
performAction(@ntRangefrom = 0) int position)890         public void performAction(@IntRange(from = 0) int position) {
891             return;
892         }
893     }
894 }
895