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