Home | History | Annotate | Download | only in ui
      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
     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