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 
17 package com.android.printspooler.ui;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.app.DialogFragment;
23 import android.app.Fragment;
24 import android.app.FragmentTransaction;
25 import android.content.ActivityNotFoundException;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.pm.ActivityInfo;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.PackageManager.NameNotFoundException;
34 import android.content.pm.ResolveInfo;
35 import android.content.pm.ServiceInfo;
36 import android.database.DataSetObserver;
37 import android.graphics.drawable.Drawable;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.print.PrintManager;
41 import android.print.PrinterId;
42 import android.print.PrinterInfo;
43 import android.printservice.PrintServiceInfo;
44 import android.provider.Settings;
45 import android.text.TextUtils;
46 import android.util.Log;
47 import android.view.ContextMenu;
48 import android.view.ContextMenu.ContextMenuInfo;
49 import android.view.Menu;
50 import android.view.MenuItem;
51 import android.view.View;
52 import android.view.ViewGroup;
53 import android.view.accessibility.AccessibilityManager;
54 import android.widget.AdapterView;
55 import android.widget.AdapterView.AdapterContextMenuInfo;
56 import android.widget.ArrayAdapter;
57 import android.widget.BaseAdapter;
58 import android.widget.Filter;
59 import android.widget.Filterable;
60 import android.widget.ImageView;
61 import android.widget.ListView;
62 import android.widget.SearchView;
63 import android.widget.TextView;
64 
65 import com.android.printspooler.R;
66 
67 import java.util.ArrayList;
68 import java.util.List;
69 
70 /**
71  * This is an activity for selecting a printer.
72  */
73 public 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
onCreate(Bundle savedInstanceState)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
onCreateOptionsMenu(Menu menu)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
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)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
onContextItemSelected(MenuItem item)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
onResume()230     public void onResume() {
231         super.onResume();
232         updateServicesWithAddPrinterActivity();
233         invalidateOptionsMenu();
234     }
235 
236     @Override
onPause()237     public void onPause() {
238         if (mAnnounceFilterResult != null) {
239             mAnnounceFilterResult.remove();
240         }
241         super.onPause();
242     }
243 
244     @Override
onOptionsItemSelected(MenuItem item)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 
onPrinterSelected(PrinterId printerId)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 
updateServicesWithAddPrinterActivity()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 
showAddPrinterSelectionDialog()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 
updateEmptyView(DestinationAdapter adapter)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 
announceSearchResultIfNeeded()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")
onCreateDialog(Bundle savedInstanceState)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 
DestinationAdapter()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
getFilter()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