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