SelectPrinterActivity.java revision bb9f686b40743df2642b7d3b7778dbf7284ae665
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.AlertDialog;
21import android.app.Dialog;
22import android.app.DialogFragment;
23import android.app.Fragment;
24import android.app.FragmentTransaction;
25import android.content.ActivityNotFoundException;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.content.IntentSender.SendIntentException;
31import android.content.pm.ActivityInfo;
32import android.content.pm.PackageInfo;
33import android.content.pm.PackageManager;
34import android.content.pm.PackageManager.NameNotFoundException;
35import android.content.pm.ResolveInfo;
36import android.content.pm.ServiceInfo;
37import android.database.ContentObserver;
38import android.database.DataSetObserver;
39import android.graphics.drawable.Drawable;
40import android.net.Uri;
41import android.os.Bundle;
42import android.os.Handler;
43import android.print.PrintManager;
44import android.print.PrinterId;
45import android.print.PrinterInfo;
46import android.printservice.PrintServiceInfo;
47import android.provider.Settings;
48import android.text.TextUtils;
49import android.util.Log;
50import android.view.ContextMenu;
51import android.view.ContextMenu.ContextMenuInfo;
52import android.view.Menu;
53import android.view.MenuItem;
54import android.view.View;
55import android.view.View.OnClickListener;
56import android.view.ViewGroup;
57import android.view.accessibility.AccessibilityManager;
58import android.widget.AdapterView;
59import android.widget.AdapterView.AdapterContextMenuInfo;
60import android.widget.ArrayAdapter;
61import android.widget.BaseAdapter;
62import android.widget.Filter;
63import android.widget.Filterable;
64import android.widget.ImageView;
65import android.widget.ListView;
66import android.widget.SearchView;
67import android.widget.TextView;
68
69import com.android.printspooler.R;
70
71import java.util.ArrayList;
72import java.util.List;
73
74/**
75 * This is an activity for selecting a printer.
76 */
77public final class SelectPrinterActivity extends Activity {
78
79    private static final String LOG_TAG = "SelectPrinterFragment";
80
81    public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
82
83    private static final String FRAGMENT_TAG_ADD_PRINTER_DIALOG =
84            "FRAGMENT_TAG_ADD_PRINTER_DIALOG";
85
86    private static final String FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS =
87            "FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS";
88
89    private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID";
90
91    /** If there are any enabled print services */
92    private boolean mHasEnabledPrintServices;
93
94    private final ArrayList<PrintServiceInfo> mAddPrinterServices =
95            new ArrayList<>();
96
97    private PrinterRegistry mPrinterRegistry;
98
99    private ListView mListView;
100
101    private AnnounceFilterResult mAnnounceFilterResult;
102
103    /** Monitor if new print services get enabled or disabled */
104    private ContentObserver mPrintServicesObserver;
105
106    @Override
107    public void onCreate(Bundle savedInstanceState) {
108        super.onCreate(savedInstanceState);
109        getActionBar().setIcon(R.drawable.ic_print);
110
111        setContentView(R.layout.select_printer_activity);
112
113        mPrinterRegistry = new PrinterRegistry(this, null);
114
115        // Hook up the list view.
116        mListView = (ListView) findViewById(android.R.id.list);
117        final DestinationAdapter adapter = new DestinationAdapter();
118        adapter.registerDataSetObserver(new DataSetObserver() {
119            @Override
120            public void onChanged() {
121                if (!isFinishing() && adapter.getCount() <= 0) {
122                    updateEmptyView(adapter);
123                }
124            }
125
126            @Override
127            public void onInvalidated() {
128                if (!isFinishing()) {
129                    updateEmptyView(adapter);
130                }
131            }
132        });
133        mListView.setAdapter(adapter);
134
135        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
136            @Override
137            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
138                if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) {
139                    return;
140                }
141
142                PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
143                onPrinterSelected(printer.getId());
144            }
145        });
146
147        registerForContextMenu(mListView);
148    }
149
150    @Override
151    public boolean onCreateOptionsMenu(Menu menu) {
152        super.onCreateOptionsMenu(menu);
153
154        getMenuInflater().inflate(R.menu.select_printer_activity, menu);
155
156        MenuItem searchItem = menu.findItem(R.id.action_search);
157        SearchView searchView = (SearchView) searchItem.getActionView();
158        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
159            @Override
160            public boolean onQueryTextSubmit(String query) {
161                return true;
162            }
163
164            @Override
165            public boolean onQueryTextChange(String searchString) {
166                ((DestinationAdapter) mListView.getAdapter()).getFilter().filter(searchString);
167                return true;
168            }
169        });
170        searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
171            @Override
172            public void onViewAttachedToWindow(View view) {
173                if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) {
174                    view.announceForAccessibility(getString(
175                            R.string.print_search_box_shown_utterance));
176                }
177            }
178            @Override
179            public void onViewDetachedFromWindow(View view) {
180                if (!isFinishing() && AccessibilityManager.getInstance(
181                        SelectPrinterActivity.this).isEnabled()) {
182                    view.announceForAccessibility(getString(
183                            R.string.print_search_box_hidden_utterance));
184                }
185            }
186        });
187
188        return true;
189    }
190
191    @Override
192    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
193        if (view == mListView) {
194            final int position = ((AdapterContextMenuInfo) menuInfo).position;
195            PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
196
197            menu.setHeaderTitle(printer.getName());
198
199            // Add the select menu item if applicable.
200            if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) {
201                MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer,
202                        Menu.NONE, R.string.print_select_printer);
203                Intent intent = new Intent();
204                intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
205                selectItem.setIntent(intent);
206            }
207
208            // Add the forget menu item if applicable.
209            if (mPrinterRegistry.isFavoritePrinter(printer.getId())) {
210                MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer,
211                        Menu.NONE, R.string.print_forget_printer);
212                Intent intent = new Intent();
213                intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
214                forgetItem.setIntent(intent);
215            }
216        }
217    }
218
219    @Override
220    public boolean onContextItemSelected(MenuItem item) {
221        switch (item.getItemId()) {
222            case R.string.print_select_printer: {
223                PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID);
224                onPrinterSelected(printerId);
225            } return true;
226
227            case R.string.print_forget_printer: {
228                PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID);
229                mPrinterRegistry.forgetFavoritePrinter(printerId);
230            } return true;
231        }
232        return false;
233    }
234
235    /**
236     * Adjust the UI if the enabled print services changed.
237     */
238    private synchronized void onPrintServicesUpdate() {
239        updateServicesWithAddPrinterActivity();
240        updateEmptyView((DestinationAdapter)mListView.getAdapter());
241        invalidateOptionsMenu();
242    }
243
244    /**
245     * Register listener for changes to the enabled print services.
246     */
247    private void registerServiceMonitor() {
248        mPrintServicesObserver = new ContentObserver(new Handler()) {
249            @Override
250            public void onChange(boolean selfChange) {
251                onPrintServicesUpdate();
252            }
253        };
254
255        getContentResolver().registerContentObserver(
256                Settings.Secure.getUriFor(Settings.Secure.ENABLED_PRINT_SERVICES), false,
257                mPrintServicesObserver);
258    }
259
260    /**
261     * Unregister {@link #mPrintServicesObserver listener for changes to the enabled print services}
262     * or nothing if the listener is not registered.
263     */
264    private void unregisterServiceMonitorIfNeeded() {
265        if (mPrintServicesObserver != null) {
266            getContentResolver().unregisterContentObserver(mPrintServicesObserver);
267
268            mPrintServicesObserver = null;
269        }
270    }
271
272    @Override
273    public void onStart() {
274        super.onStart();
275        registerServiceMonitor();
276        onPrintServicesUpdate();
277    }
278
279    @Override
280    public void onPause() {
281        if (mAnnounceFilterResult != null) {
282            mAnnounceFilterResult.remove();
283        }
284        super.onPause();
285    }
286
287    @Override
288    public void onStop() {
289        unregisterServiceMonitorIfNeeded();
290        super.onStop();
291    }
292
293    @Override
294    public boolean onOptionsItemSelected(MenuItem item) {
295        if (item.getItemId() == R.id.action_add_printer) {
296            showAddPrinterSelectionDialog();
297            return true;
298        }
299        return super.onOptionsItemSelected(item);
300    }
301
302    private void onPrinterSelected(PrinterId printerId) {
303        Intent intent = new Intent();
304        intent.putExtra(INTENT_EXTRA_PRINTER_ID, printerId);
305        setResult(RESULT_OK, intent);
306        finish();
307    }
308
309    private void updateServicesWithAddPrinterActivity() {
310        mHasEnabledPrintServices = true;
311        mAddPrinterServices.clear();
312
313        // Get all enabled print services.
314        PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
315        List<PrintServiceInfo> enabledServices = printManager.getEnabledPrintServices();
316
317        // No enabled print services - done.
318        if (enabledServices.isEmpty()) {
319            mHasEnabledPrintServices = false;
320            return;
321        }
322
323        // Find the services with valid add printers activities.
324        final int enabledServiceCount = enabledServices.size();
325        for (int i = 0; i < enabledServiceCount; i++) {
326            PrintServiceInfo enabledService = enabledServices.get(i);
327
328            // No add printers activity declared - next.
329            if (TextUtils.isEmpty(enabledService.getAddPrintersActivityName())) {
330                continue;
331            }
332
333            ServiceInfo serviceInfo = enabledService.getResolveInfo().serviceInfo;
334            ComponentName addPrintersComponentName = new ComponentName(
335                    serviceInfo.packageName, enabledService.getAddPrintersActivityName());
336            Intent addPritnersIntent = new Intent()
337                .setComponent(addPrintersComponentName);
338
339            // The add printers activity is valid - add it.
340            PackageManager pm = getPackageManager();
341            List<ResolveInfo> resolvedActivities = pm.queryIntentActivities(addPritnersIntent, 0);
342            if (!resolvedActivities.isEmpty()) {
343                // The activity is a component name, therefore it is one or none.
344                ActivityInfo activityInfo = resolvedActivities.get(0).activityInfo;
345                if (activityInfo.exported
346                        && (activityInfo.permission == null
347                                || pm.checkPermission(activityInfo.permission, getPackageName())
348                                        == PackageManager.PERMISSION_GRANTED)) {
349                    mAddPrinterServices.add(enabledService);
350                }
351            }
352        }
353    }
354
355    private void showAddPrinterSelectionDialog() {
356        FragmentTransaction transaction = getFragmentManager().beginTransaction();
357        Fragment oldFragment = getFragmentManager().findFragmentByTag(
358                FRAGMENT_TAG_ADD_PRINTER_DIALOG);
359        if (oldFragment != null) {
360            transaction.remove(oldFragment);
361        }
362        AddPrinterAlertDialogFragment newFragment = new AddPrinterAlertDialogFragment();
363        Bundle arguments = new Bundle();
364        arguments.putParcelableArrayList(FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS,
365                mAddPrinterServices);
366        newFragment.setArguments(arguments);
367        transaction.add(newFragment, FRAGMENT_TAG_ADD_PRINTER_DIALOG);
368        transaction.commit();
369    }
370
371    public void updateEmptyView(DestinationAdapter adapter) {
372        if (mListView.getEmptyView() == null) {
373            View emptyView = findViewById(R.id.empty_print_state);
374            mListView.setEmptyView(emptyView);
375        }
376        TextView titleView = (TextView) findViewById(R.id.title);
377        View progressBar = findViewById(R.id.progress_bar);
378        if (!mHasEnabledPrintServices) {
379            titleView.setText(R.string.print_no_print_services);
380            progressBar.setVisibility(View.GONE);
381        } else if (adapter.getUnfilteredCount() <= 0) {
382            titleView.setText(R.string.print_searching_for_printers);
383            progressBar.setVisibility(View.VISIBLE);
384        } else {
385            titleView.setText(R.string.print_no_printers);
386            progressBar.setVisibility(View.GONE);
387        }
388    }
389
390    private void announceSearchResultIfNeeded() {
391        if (AccessibilityManager.getInstance(this).isEnabled()) {
392            if (mAnnounceFilterResult == null) {
393                mAnnounceFilterResult = new AnnounceFilterResult();
394            }
395            mAnnounceFilterResult.post();
396        }
397    }
398
399    public static class AddPrinterAlertDialogFragment extends DialogFragment {
400
401        private String mAddPrintServiceItem;
402
403        @Override
404        @SuppressWarnings("unchecked")
405        public Dialog onCreateDialog(Bundle savedInstanceState) {
406            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
407                    .setTitle(R.string.choose_print_service);
408
409            final List<PrintServiceInfo> printServices = (List<PrintServiceInfo>) (List<?>)
410                    getArguments().getParcelableArrayList(FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS);
411
412            final ArrayAdapter<String> adapter = new ArrayAdapter<>(
413                    getActivity(), android.R.layout.simple_list_item_1);
414            final int printServiceCount = printServices.size();
415            for (int i = 0; i < printServiceCount; i++) {
416                PrintServiceInfo printService = printServices.get(i);
417                adapter.add(printService.getResolveInfo().loadLabel(
418                        getActivity().getPackageManager()).toString());
419            }
420
421            final String searchUri = Settings.Secure.getString(getActivity().getContentResolver(),
422                    Settings.Secure.PRINT_SERVICE_SEARCH_URI);
423            final Intent viewIntent;
424            if (!TextUtils.isEmpty(searchUri)) {
425                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri));
426                if (getActivity().getPackageManager().resolveActivity(intent, 0) != null) {
427                    viewIntent = intent;
428                    mAddPrintServiceItem = getString(R.string.add_print_service_label);
429                    adapter.add(mAddPrintServiceItem);
430                } else {
431                    viewIntent = null;
432                }
433            } else {
434                viewIntent = null;
435            }
436
437            builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
438                @Override
439                public void onClick(DialogInterface dialog, int which) {
440                    String item = adapter.getItem(which);
441                    if (item.equals(mAddPrintServiceItem)) {
442                        try {
443                            startActivity(viewIntent);
444                        } catch (ActivityNotFoundException anfe) {
445                            Log.w(LOG_TAG, "Couldn't start add printer activity", anfe);
446                        }
447                    } else {
448                        PrintServiceInfo printService = printServices.get(which);
449                        ComponentName componentName = new ComponentName(
450                                printService.getResolveInfo().serviceInfo.packageName,
451                                printService.getAddPrintersActivityName());
452                        Intent intent = new Intent(Intent.ACTION_MAIN);
453                        intent.setComponent(componentName);
454                        try {
455                            startActivity(intent);
456                        } catch (ActivityNotFoundException anfe) {
457                            Log.w(LOG_TAG, "Couldn't start add printer activity", anfe);
458                        }
459                    }
460                }
461            });
462
463            return builder.create();
464        }
465    }
466
467    private final class DestinationAdapter extends BaseAdapter implements Filterable {
468
469        private final Object mLock = new Object();
470
471        private final List<PrinterInfo> mPrinters = new ArrayList<>();
472
473        private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>();
474
475        private CharSequence mLastSearchString;
476
477        public DestinationAdapter() {
478            mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() {
479                @Override
480                public void onPrintersChanged(List<PrinterInfo> printers) {
481                    synchronized (mLock) {
482                        mPrinters.clear();
483                        mPrinters.addAll(printers);
484                        mFilteredPrinters.clear();
485                        mFilteredPrinters.addAll(printers);
486                        if (!TextUtils.isEmpty(mLastSearchString)) {
487                            getFilter().filter(mLastSearchString);
488                        }
489                    }
490                    notifyDataSetChanged();
491                }
492
493                @Override
494                public void onPrintersInvalid() {
495                    synchronized (mLock) {
496                        mPrinters.clear();
497                        mFilteredPrinters.clear();
498                    }
499                    notifyDataSetInvalidated();
500                }
501            });
502        }
503
504        @Override
505        public Filter getFilter() {
506            return new Filter() {
507                @Override
508                protected FilterResults performFiltering(CharSequence constraint) {
509                    synchronized (mLock) {
510                        if (TextUtils.isEmpty(constraint)) {
511                            return null;
512                        }
513                        FilterResults results = new FilterResults();
514                        List<PrinterInfo> filteredPrinters = new ArrayList<>();
515                        String constraintLowerCase = constraint.toString().toLowerCase();
516                        final int printerCount = mPrinters.size();
517                        for (int i = 0; i < printerCount; i++) {
518                            PrinterInfo printer = mPrinters.get(i);
519                            if (printer.getName().toLowerCase().contains(constraintLowerCase)) {
520                                filteredPrinters.add(printer);
521                            }
522                        }
523                        results.values = filteredPrinters;
524                        results.count = filteredPrinters.size();
525                        return results;
526                    }
527                }
528
529                @Override
530                @SuppressWarnings("unchecked")
531                protected void publishResults(CharSequence constraint, FilterResults results) {
532                    final boolean resultCountChanged;
533                    synchronized (mLock) {
534                        final int oldPrinterCount = mFilteredPrinters.size();
535                        mLastSearchString = constraint;
536                        mFilteredPrinters.clear();
537                        if (results == null) {
538                            mFilteredPrinters.addAll(mPrinters);
539                        } else {
540                            List<PrinterInfo> printers = (List<PrinterInfo>) results.values;
541                            mFilteredPrinters.addAll(printers);
542                        }
543                        resultCountChanged = (oldPrinterCount != mFilteredPrinters.size());
544                    }
545                    if (resultCountChanged) {
546                        announceSearchResultIfNeeded();
547                    }
548                    notifyDataSetChanged();
549                }
550            };
551        }
552
553        public int getUnfilteredCount() {
554            synchronized (mLock) {
555                return mPrinters.size();
556            }
557        }
558
559        @Override
560        public int getCount() {
561            synchronized (mLock) {
562                return mFilteredPrinters.size();
563            }
564        }
565
566        @Override
567        public Object getItem(int position) {
568            synchronized (mLock) {
569                return mFilteredPrinters.get(position);
570            }
571        }
572
573        @Override
574        public long getItemId(int position) {
575            return position;
576        }
577
578        @Override
579        public View getDropDownView(int position, View convertView, ViewGroup parent) {
580            return getView(position, convertView, parent);
581        }
582
583        @Override
584        public View getView(int position, View convertView, ViewGroup parent) {
585            if (convertView == null) {
586                convertView = getLayoutInflater().inflate(
587                        R.layout.printer_list_item, parent, false);
588            }
589
590            convertView.setEnabled(isActionable(position));
591
592            final PrinterInfo printer = (PrinterInfo) getItem(position);
593
594            CharSequence title = printer.getName();
595            Drawable icon = printer.loadIcon(SelectPrinterActivity.this);
596
597            CharSequence printServiceLabel;
598            try {
599                PackageInfo packageInfo = getPackageManager().getPackageInfo(
600                        printer.getId().getServiceName().getPackageName(), 0);
601
602                printServiceLabel = packageInfo.applicationInfo.loadLabel(getPackageManager());
603            } catch (NameNotFoundException e) {
604                printServiceLabel = null;
605            }
606
607            CharSequence description = printer.getDescription();
608
609            CharSequence subtitle;
610            if (printServiceLabel == null) {
611                subtitle = description;
612            } else if (description == null) {
613                subtitle = printServiceLabel;
614            } else {
615                subtitle = getString(R.string.printer_extended_description_template,
616                        printServiceLabel, description);
617            }
618
619            TextView titleView = (TextView) convertView.findViewById(R.id.title);
620            titleView.setText(title);
621
622            TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
623            if (!TextUtils.isEmpty(subtitle)) {
624                subtitleView.setText(subtitle);
625                subtitleView.setVisibility(View.VISIBLE);
626            } else {
627                subtitleView.setText(null);
628                subtitleView.setVisibility(View.GONE);
629            }
630
631            ImageView moreInfoView = (ImageView) convertView.findViewById(R.id.more_info);
632            if (printer.getInfoIntent() != null) {
633                moreInfoView.setVisibility(View.VISIBLE);
634                moreInfoView.setOnClickListener(new OnClickListener() {
635                    @Override
636                    public void onClick(View v) {
637                        try {
638                            startIntentSender(printer.getInfoIntent().getIntentSender(), null, 0, 0, 0);
639                        } catch (SendIntentException e) {
640                            Log.e(LOG_TAG, "Could not execute pending info intent: %s", e);
641                        }
642                    }
643                });
644            }
645
646            ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
647            if (icon != null) {
648                iconView.setImageDrawable(icon);
649                iconView.setVisibility(View.VISIBLE);
650            } else {
651                iconView.setVisibility(View.GONE);
652            }
653
654            return convertView;
655        }
656
657        public boolean isActionable(int position) {
658            PrinterInfo printer =  (PrinterInfo) getItem(position);
659            return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
660        }
661    }
662
663    private final class AnnounceFilterResult implements Runnable {
664        private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
665
666        public void post() {
667            remove();
668            mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
669        }
670
671        public void remove() {
672            mListView.removeCallbacks(this);
673        }
674
675        @Override
676        public void run() {
677            final int count = mListView.getAdapter().getCount();
678            final String text;
679            if (count <= 0) {
680                text = getString(R.string.print_no_printers);
681            } else {
682                text = getResources().getQuantityString(
683                    R.plurals.print_search_result_count_utterance, count, count);
684            }
685            mListView.announceForAccessibility(text);
686        }
687    }
688}
689