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