SelectPrinterActivity.java revision 2e3012624b703a653fed44d4dc1b8904406c0d99
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.printspooler.ui;
18
19import android.annotation.NonNull;
20import android.app.Activity;
21import android.app.LoaderManager;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentSender.SendIntentException;
26import android.content.Loader;
27import android.database.DataSetObserver;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.print.PrintManager;
31import android.print.PrintServicesLoader;
32import android.print.PrinterId;
33import android.print.PrinterInfo;
34import android.printservice.PrintServiceInfo;
35import android.provider.Settings;
36import android.text.TextUtils;
37import android.util.ArrayMap;
38import android.util.Log;
39import android.util.TypedValue;
40import android.view.ContextMenu;
41import android.view.ContextMenu.ContextMenuInfo;
42import android.view.Menu;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.View.OnClickListener;
46import android.view.ViewGroup;
47import android.view.accessibility.AccessibilityManager;
48import android.widget.AdapterView;
49import android.widget.AdapterView.AdapterContextMenuInfo;
50import android.widget.BaseAdapter;
51import android.widget.Filter;
52import android.widget.Filterable;
53import android.widget.ImageView;
54import android.widget.LinearLayout;
55import android.widget.ListView;
56import android.widget.SearchView;
57import android.widget.TextView;
58import android.widget.Toast;
59
60import com.android.internal.logging.MetricsLogger;
61import com.android.internal.logging.MetricsProto.MetricsEvent;
62import com.android.printspooler.R;
63
64import java.util.ArrayList;
65import java.util.List;
66
67/**
68 * This is an activity for selecting a printer.
69 */
70public final class SelectPrinterActivity extends Activity implements
71        LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
72
73    private static final String LOG_TAG = "SelectPrinterFragment";
74
75    private static final int LOADER_ID_PRINT_REGISTRY = 1;
76    private static final int LOADER_ID_PRINT_REGISTRY_INT = 2;
77    private static final int LOADER_ID_ENABLED_PRINT_SERVICES = 3;
78
79    public static final String INTENT_EXTRA_PRINTER = "INTENT_EXTRA_PRINTER";
80
81    private static final String EXTRA_PRINTER = "EXTRA_PRINTER";
82    private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID";
83
84    private static final String KEY_NOT_FIRST_CREATE = "KEY_NOT_FIRST_CREATE";
85    private static final String KEY_DID_SEARCH = "DID_SEARCH";
86
87    // Constants for MetricsLogger.count and MetricsLogger.histo
88    private static final String PRINTERS_LISTED_COUNT = "printers_listed";
89    private static final String PRINTERS_ICON_COUNT = "printers_icon";
90    private static final String PRINTERS_INFO_COUNT = "printers_info";
91
92    /** The currently enabled print services by their ComponentName */
93    private ArrayMap<ComponentName, PrintServiceInfo> mEnabledPrintServices;
94
95    private PrinterRegistry mPrinterRegistry;
96
97    private ListView mListView;
98
99    private AnnounceFilterResult mAnnounceFilterResult;
100
101    private boolean mDidSearch;
102
103    private void startAddPrinterActivity() {
104        MetricsLogger.action(this, MetricsEvent.ACTION_PRINT_SERVICE_ADD);
105        startActivity(new Intent(this, AddPrinterActivity.class));
106    }
107
108    @Override
109    public void onCreate(Bundle savedInstanceState) {
110        super.onCreate(savedInstanceState);
111        getActionBar().setIcon(R.drawable.ic_print);
112
113        setContentView(R.layout.select_printer_activity);
114
115        mEnabledPrintServices = new ArrayMap<>();
116
117        mPrinterRegistry = new PrinterRegistry(this, null, LOADER_ID_PRINT_REGISTRY,
118                LOADER_ID_PRINT_REGISTRY_INT);
119
120        // Hook up the list view.
121        mListView = (ListView) findViewById(android.R.id.list);
122        final DestinationAdapter adapter = new DestinationAdapter();
123        adapter.registerDataSetObserver(new DataSetObserver() {
124            @Override
125            public void onChanged() {
126                if (!isFinishing() && adapter.getCount() <= 0) {
127                    updateEmptyView(adapter);
128                }
129            }
130
131            @Override
132            public void onInvalidated() {
133                if (!isFinishing()) {
134                    updateEmptyView(adapter);
135                }
136            }
137        });
138        mListView.setAdapter(adapter);
139
140        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
141            @Override
142            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
143                if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) {
144                    return;
145                }
146
147                PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
148
149                if (printer == null) {
150                    startAddPrinterActivity();
151                } else {
152                    onPrinterSelected(printer);
153                }
154            }
155        });
156
157        findViewById(R.id.button).setOnClickListener(new OnClickListener() {
158            @Override public void onClick(View v) {
159                startAddPrinterActivity();
160            }
161        });
162
163        registerForContextMenu(mListView);
164
165        getLoaderManager().initLoader(LOADER_ID_ENABLED_PRINT_SERVICES, null, this);
166
167        // On first creation:
168        //
169        // If no services are installed, instantly open add printer dialog.
170        // If some are disabled and some are enabled show a toast to notify the user
171        if (savedInstanceState == null || !savedInstanceState.getBoolean(KEY_NOT_FIRST_CREATE)) {
172            List<PrintServiceInfo> allServices =
173                    ((PrintManager) getSystemService(Context.PRINT_SERVICE))
174                            .getPrintServices(PrintManager.ALL_SERVICES);
175            boolean hasEnabledServices = false;
176            boolean hasDisabledServices = false;
177
178            if (allServices != null) {
179                final int numServices = allServices.size();
180                for (int i = 0; i < numServices; i++) {
181                    if (allServices.get(i).isEnabled()) {
182                        hasEnabledServices = true;
183                    } else {
184                        hasDisabledServices = true;
185                    }
186                }
187            }
188
189            if (!hasEnabledServices) {
190                startAddPrinterActivity();
191            } else if (hasDisabledServices) {
192                String disabledServicesSetting = Settings.Secure.getString(getContentResolver(),
193                        Settings.Secure.DISABLED_PRINT_SERVICES);
194                if (!TextUtils.isEmpty(disabledServicesSetting)) {
195                    Toast.makeText(this, getString(R.string.print_services_disabled_toast),
196                            Toast.LENGTH_LONG).show();
197                }
198            }
199        }
200
201        if (savedInstanceState != null) {
202            mDidSearch = savedInstanceState.getBoolean(KEY_DID_SEARCH);
203        }
204    }
205
206    @Override
207    protected void onSaveInstanceState(Bundle outState) {
208        super.onSaveInstanceState(outState);
209        outState.putBoolean(KEY_NOT_FIRST_CREATE, true);
210        outState.putBoolean(KEY_DID_SEARCH, mDidSearch);
211    }
212
213    @Override
214    public boolean onCreateOptionsMenu(Menu menu) {
215        super.onCreateOptionsMenu(menu);
216
217        getMenuInflater().inflate(R.menu.select_printer_activity, menu);
218
219        MenuItem searchItem = menu.findItem(R.id.action_search);
220        SearchView searchView = (SearchView) searchItem.getActionView();
221        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
222            @Override
223            public boolean onQueryTextSubmit(String query) {
224                return true;
225            }
226
227            @Override
228            public boolean onQueryTextChange(String searchString) {
229                ((DestinationAdapter) mListView.getAdapter()).getFilter().filter(searchString);
230                return true;
231            }
232        });
233        searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
234            @Override
235            public void onViewAttachedToWindow(View view) {
236                if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) {
237                    view.announceForAccessibility(getString(
238                            R.string.print_search_box_shown_utterance));
239                }
240            }
241            @Override
242            public void onViewDetachedFromWindow(View view) {
243                if (!isFinishing() && AccessibilityManager.getInstance(
244                        SelectPrinterActivity.this).isEnabled()) {
245                    view.announceForAccessibility(getString(
246                            R.string.print_search_box_hidden_utterance));
247                }
248            }
249        });
250
251        return true;
252    }
253
254    @Override
255    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
256        if (view == mListView) {
257            final int position = ((AdapterContextMenuInfo) menuInfo).position;
258            PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
259
260            menu.setHeaderTitle(printer.getName());
261
262            // Add the select menu item if applicable.
263            if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) {
264                MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer,
265                        Menu.NONE, R.string.print_select_printer);
266                Intent intent = new Intent();
267                intent.putExtra(EXTRA_PRINTER, printer);
268                selectItem.setIntent(intent);
269            }
270
271            // Add the forget menu item if applicable.
272            if (mPrinterRegistry.isFavoritePrinter(printer.getId())) {
273                MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer,
274                        Menu.NONE, R.string.print_forget_printer);
275                Intent intent = new Intent();
276                intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
277                forgetItem.setIntent(intent);
278            }
279        }
280    }
281
282    @Override
283    public boolean onContextItemSelected(MenuItem item) {
284        switch (item.getItemId()) {
285            case R.string.print_select_printer: {
286                PrinterInfo printer = item.getIntent().getParcelableExtra(EXTRA_PRINTER);
287                onPrinterSelected(printer);
288            } return true;
289
290            case R.string.print_forget_printer: {
291                PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID);
292                mPrinterRegistry.forgetFavoritePrinter(printerId);
293            } return true;
294        }
295        return false;
296    }
297
298    /**
299     * Adjust the UI if the enabled print services changed.
300     */
301    private synchronized void onPrintServicesUpdate() {
302        updateEmptyView((DestinationAdapter)mListView.getAdapter());
303        invalidateOptionsMenu();
304    }
305
306    @Override
307    public void onStart() {
308        super.onStart();
309        onPrintServicesUpdate();
310    }
311
312    @Override
313    public void onPause() {
314        if (mAnnounceFilterResult != null) {
315            mAnnounceFilterResult.remove();
316        }
317        super.onPause();
318    }
319
320    @Override
321    public void onStop() {
322        super.onStop();
323    }
324
325    @Override
326    protected void onDestroy() {
327        if (isFinishing()) {
328            DestinationAdapter adapter = (DestinationAdapter) mListView.getAdapter();
329            List<PrinterInfo> printers = adapter.getPrinters();
330            int numPrinters = adapter.getPrinters().size();
331
332            MetricsLogger.action(this, MetricsEvent.PRINT_ALL_PRINTERS, numPrinters);
333            MetricsLogger.count(this, PRINTERS_LISTED_COUNT, numPrinters);
334
335            int numInfoPrinters = 0;
336            int numIconPrinters = 0;
337            for (int i = 0; i < numPrinters; i++) {
338                PrinterInfo printer = printers.get(i);
339
340                if (printer.getInfoIntent() != null) {
341                    numInfoPrinters++;
342                }
343
344                if (printer.getHasCustomPrinterIcon()) {
345                    numIconPrinters++;
346                }
347            }
348
349            MetricsLogger.count(this, PRINTERS_INFO_COUNT, numInfoPrinters);
350            MetricsLogger.count(this, PRINTERS_ICON_COUNT, numIconPrinters);
351        }
352
353        super.onDestroy();
354    }
355
356    private void onPrinterSelected(PrinterInfo printer) {
357        Intent intent = new Intent();
358        intent.putExtra(INTENT_EXTRA_PRINTER, printer);
359        setResult(RESULT_OK, intent);
360        finish();
361    }
362
363    public void updateEmptyView(DestinationAdapter adapter) {
364        if (mListView.getEmptyView() == null) {
365            View emptyView = findViewById(R.id.empty_print_state);
366            mListView.setEmptyView(emptyView);
367        }
368        TextView titleView = (TextView) findViewById(R.id.title);
369        View progressBar = findViewById(R.id.progress_bar);
370        if (mEnabledPrintServices.size() == 0) {
371            titleView.setText(R.string.print_no_print_services);
372            progressBar.setVisibility(View.GONE);
373        } else if (adapter.getUnfilteredCount() <= 0) {
374            titleView.setText(R.string.print_searching_for_printers);
375            progressBar.setVisibility(View.VISIBLE);
376        } else {
377            titleView.setText(R.string.print_no_printers);
378            progressBar.setVisibility(View.GONE);
379        }
380    }
381
382    private void announceSearchResultIfNeeded() {
383        if (AccessibilityManager.getInstance(this).isEnabled()) {
384            if (mAnnounceFilterResult == null) {
385                mAnnounceFilterResult = new AnnounceFilterResult();
386            }
387            mAnnounceFilterResult.post();
388        }
389    }
390
391    @Override
392    public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
393        return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
394                PrintManager.ENABLED_SERVICES);
395    }
396
397    @Override
398    public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
399            List<PrintServiceInfo> services) {
400        mEnabledPrintServices.clear();
401
402        if (services != null && !services.isEmpty()) {
403            final int numServices = services.size();
404            for (int i = 0; i < numServices; i++) {
405                PrintServiceInfo service = services.get(i);
406
407                mEnabledPrintServices.put(service.getComponentName(), service);
408            }
409        }
410
411        onPrintServicesUpdate();
412    }
413
414    @Override
415    public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
416        if (!isFinishing()) {
417            onLoadFinished(loader, null);
418        }
419    }
420
421    private final class DestinationAdapter extends BaseAdapter implements Filterable {
422
423        private final Object mLock = new Object();
424
425        private final List<PrinterInfo> mPrinters = new ArrayList<>();
426
427        private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>();
428
429        private CharSequence mLastSearchString;
430
431        /**
432         * Get the currently known printers.
433         *
434         * @return The currently known printers
435         */
436        @NonNull List<PrinterInfo> getPrinters() {
437            return mPrinters;
438        }
439
440        public DestinationAdapter() {
441            mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() {
442                @Override
443                public void onPrintersChanged(List<PrinterInfo> printers) {
444                    synchronized (mLock) {
445                        mPrinters.clear();
446                        mPrinters.addAll(printers);
447                        mFilteredPrinters.clear();
448                        mFilteredPrinters.addAll(printers);
449                        if (!TextUtils.isEmpty(mLastSearchString)) {
450                            getFilter().filter(mLastSearchString);
451                        }
452                    }
453                    notifyDataSetChanged();
454                }
455
456                @Override
457                public void onPrintersInvalid() {
458                    synchronized (mLock) {
459                        mPrinters.clear();
460                        mFilteredPrinters.clear();
461                    }
462                    notifyDataSetInvalidated();
463                }
464            });
465        }
466
467        @Override
468        public Filter getFilter() {
469            return new Filter() {
470                @Override
471                protected FilterResults performFiltering(CharSequence constraint) {
472                    synchronized (mLock) {
473                        if (TextUtils.isEmpty(constraint)) {
474                            return null;
475                        }
476                        FilterResults results = new FilterResults();
477                        List<PrinterInfo> filteredPrinters = new ArrayList<>();
478                        String constraintLowerCase = constraint.toString().toLowerCase();
479                        final int printerCount = mPrinters.size();
480                        for (int i = 0; i < printerCount; i++) {
481                            PrinterInfo printer = mPrinters.get(i);
482                            String description = printer.getDescription();
483                            if (printer.getName().toLowerCase().contains(constraintLowerCase)
484                                    || description != null && description.toLowerCase()
485                                            .contains(constraintLowerCase)) {
486                                filteredPrinters.add(printer);
487                            }
488                        }
489                        results.values = filteredPrinters;
490                        results.count = filteredPrinters.size();
491                        return results;
492                    }
493                }
494
495                @Override
496                @SuppressWarnings("unchecked")
497                protected void publishResults(CharSequence constraint, FilterResults results) {
498                    final boolean resultCountChanged;
499                    synchronized (mLock) {
500                        final int oldPrinterCount = mFilteredPrinters.size();
501                        mLastSearchString = constraint;
502                        mFilteredPrinters.clear();
503                        if (results == null) {
504                            mFilteredPrinters.addAll(mPrinters);
505                        } else {
506                            List<PrinterInfo> printers = (List<PrinterInfo>) results.values;
507                            mFilteredPrinters.addAll(printers);
508                        }
509                        resultCountChanged = (oldPrinterCount != mFilteredPrinters.size());
510                    }
511                    if (resultCountChanged) {
512                        announceSearchResultIfNeeded();
513                    }
514
515                    if (!mDidSearch) {
516                        MetricsLogger.action(SelectPrinterActivity.this,
517                                MetricsEvent.ACTION_PRINTER_SEARCH);
518                        mDidSearch = true;
519                    }
520                    notifyDataSetChanged();
521                }
522            };
523        }
524
525        public int getUnfilteredCount() {
526            synchronized (mLock) {
527                return mPrinters.size();
528            }
529        }
530
531        @Override
532        public int getCount() {
533            synchronized (mLock) {
534                if (mFilteredPrinters.isEmpty()) {
535                    return 0;
536                } else {
537                    // Add "add printer" item to the end of the list. If the list is empty there is
538                    // a link on the empty view
539                    return mFilteredPrinters.size() + 1;
540                }
541            }
542        }
543
544        @Override
545        public int getViewTypeCount() {
546            return 2;
547        }
548
549        @Override
550        public int getItemViewType(int position) {
551            // Use separate view types for the "add printer" item an the items referring to printers
552            if (getItem(position) == null) {
553                return 0;
554            } else {
555                return 1;
556            }
557        }
558
559        @Override
560        public Object getItem(int position) {
561            synchronized (mLock) {
562                if (position < mFilteredPrinters.size()) {
563                    return mFilteredPrinters.get(position);
564                } else {
565                    // Return null to mark this as the "add printer item"
566                    return null;
567                }
568            }
569        }
570
571        @Override
572        public long getItemId(int position) {
573            return position;
574        }
575
576        @Override
577        public View getDropDownView(int position, View convertView, ViewGroup parent) {
578            return getView(position, convertView, parent);
579        }
580
581        @Override
582        public View getView(int position, View convertView, ViewGroup parent) {
583            final PrinterInfo printer = (PrinterInfo) getItem(position);
584
585            // Handle "add printer item"
586            if (printer == null) {
587                if (convertView == null) {
588                    convertView = getLayoutInflater().inflate(R.layout.add_printer_list_item,
589                            parent, false);
590                }
591
592                return convertView;
593            }
594
595            if (convertView == null) {
596                convertView = getLayoutInflater().inflate(
597                        R.layout.printer_list_item, parent, false);
598            }
599
600            convertView.setEnabled(isActionable(position));
601
602
603            CharSequence title = printer.getName();
604            Drawable icon = printer.loadIcon(SelectPrinterActivity.this);
605
606            PrintServiceInfo service = mEnabledPrintServices.get(printer.getId().getServiceName());
607
608            CharSequence printServiceLabel = null;
609            if (service != null) {
610                printServiceLabel = service.getResolveInfo().loadLabel(getPackageManager())
611                        .toString();
612            }
613
614            CharSequence description = printer.getDescription();
615
616            CharSequence subtitle;
617            if (TextUtils.isEmpty(printServiceLabel)) {
618                subtitle = description;
619            } else if (TextUtils.isEmpty(description)) {
620                subtitle = printServiceLabel;
621            } else {
622                subtitle = getString(R.string.printer_extended_description_template,
623                        printServiceLabel, description);
624            }
625
626            TextView titleView = (TextView) convertView.findViewById(R.id.title);
627            titleView.setText(title);
628
629            TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
630            if (!TextUtils.isEmpty(subtitle)) {
631                subtitleView.setText(subtitle);
632                subtitleView.setVisibility(View.VISIBLE);
633            } else {
634                subtitleView.setText(null);
635                subtitleView.setVisibility(View.GONE);
636            }
637
638            LinearLayout moreInfoView = (LinearLayout) convertView.findViewById(R.id.more_info);
639            if (printer.getInfoIntent() != null) {
640                moreInfoView.setVisibility(View.VISIBLE);
641                moreInfoView.setOnClickListener(new OnClickListener() {
642                    @Override
643                    public void onClick(View v) {
644                        try {
645                            startIntentSender(printer.getInfoIntent().getIntentSender(), null, 0, 0,
646                                    0);
647                        } catch (SendIntentException e) {
648                            Log.e(LOG_TAG, "Could not execute pending info intent: %s", e);
649                        }
650                    }
651                });
652            } else {
653                moreInfoView.setVisibility(View.GONE);
654            }
655
656            ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
657            if (icon != null) {
658                iconView.setVisibility(View.VISIBLE);
659                if (!isActionable(position)) {
660                    icon.mutate();
661
662                    TypedValue value = new TypedValue();
663                    getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
664                    icon.setAlpha((int)(value.getFloat() * 255));
665                }
666                iconView.setImageDrawable(icon);
667            } else {
668                iconView.setVisibility(View.GONE);
669            }
670
671            return convertView;
672        }
673
674        public boolean isActionable(int position) {
675            PrinterInfo printer =  (PrinterInfo) getItem(position);
676
677            if (printer == null) {
678                return true;
679            } else {
680                return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
681            }
682        }
683    }
684
685    private final class AnnounceFilterResult implements Runnable {
686        private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
687
688        public void post() {
689            remove();
690            mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
691        }
692
693        public void remove() {
694            mListView.removeCallbacks(this);
695        }
696
697        @Override
698        public void run() {
699            final int count = mListView.getAdapter().getCount();
700            final String text;
701            if (count <= 0) {
702                text = getString(R.string.print_no_printers);
703            } else {
704                text = getResources().getQuantityString(
705                    R.plurals.print_search_result_count_utterance, count, count);
706            }
707            mListView.announceForAccessibility(text);
708        }
709    }
710}
711