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