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