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.LoaderManager;
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentSender.SendIntentException;
     25 import android.content.Loader;
     26 import android.database.DataSetObserver;
     27 import android.graphics.drawable.Drawable;
     28 import android.os.Bundle;
     29 import android.print.PrintManager;
     30 import android.print.PrintServicesLoader;
     31 import android.print.PrinterId;
     32 import android.print.PrinterInfo;
     33 import android.printservice.PrintServiceInfo;
     34 import android.provider.Settings;
     35 import android.text.TextUtils;
     36 import android.util.ArrayMap;
     37 import android.util.Log;
     38 import android.util.TypedValue;
     39 import android.view.ContextMenu;
     40 import android.view.ContextMenu.ContextMenuInfo;
     41 import android.view.Menu;
     42 import android.view.MenuItem;
     43 import android.view.View;
     44 import android.view.View.OnClickListener;
     45 import android.view.ViewGroup;
     46 import android.view.accessibility.AccessibilityManager;
     47 import android.widget.AdapterView;
     48 import android.widget.AdapterView.AdapterContextMenuInfo;
     49 import android.widget.BaseAdapter;
     50 import android.widget.Filter;
     51 import android.widget.Filterable;
     52 import android.widget.ImageView;
     53 import android.widget.LinearLayout;
     54 import android.widget.ListView;
     55 import android.widget.SearchView;
     56 import android.widget.TextView;
     57 import android.widget.Toast;
     58 
     59 import com.android.printspooler.R;
     60 
     61 import java.util.ArrayList;
     62 import java.util.List;
     63 
     64 /**
     65  * This is an activity for selecting a printer.
     66  */
     67 public final class SelectPrinterActivity extends Activity implements
     68         LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
     69 
     70     private static final String LOG_TAG = "SelectPrinterFragment";
     71 
     72     private static final int LOADER_ID_PRINT_REGISTRY = 1;
     73     private static final int LOADER_ID_PRINT_REGISTRY_INT = 2;
     74     private static final int LOADER_ID_ENABLED_PRINT_SERVICES = 3;
     75 
     76     public static final String INTENT_EXTRA_PRINTER = "INTENT_EXTRA_PRINTER";
     77 
     78     private static final String EXTRA_PRINTER = "EXTRA_PRINTER";
     79     private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID";
     80 
     81     private static final String KEY_NOT_FIRST_CREATE = "KEY_NOT_FIRST_CREATE";
     82 
     83     /** The currently enabled print services by their ComponentName */
     84     private ArrayMap<ComponentName, PrintServiceInfo> mEnabledPrintServices;
     85 
     86     private PrinterRegistry mPrinterRegistry;
     87 
     88     private ListView mListView;
     89 
     90     private AnnounceFilterResult mAnnounceFilterResult;
     91 
     92     private void startAddPrinterActivity() {
     93         startActivity(new Intent(this, AddPrinterActivity.class));
     94     }
     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         mEnabledPrintServices = new ArrayMap<>();
    104 
    105         mPrinterRegistry = new PrinterRegistry(this, null, LOADER_ID_PRINT_REGISTRY,
    106                 LOADER_ID_PRINT_REGISTRY_INT);
    107 
    108         // Hook up the list view.
    109         mListView = (ListView) findViewById(android.R.id.list);
    110         final DestinationAdapter adapter = new DestinationAdapter();
    111         adapter.registerDataSetObserver(new DataSetObserver() {
    112             @Override
    113             public void onChanged() {
    114                 if (!isFinishing() && adapter.getCount() <= 0) {
    115                     updateEmptyView(adapter);
    116                 }
    117             }
    118 
    119             @Override
    120             public void onInvalidated() {
    121                 if (!isFinishing()) {
    122                     updateEmptyView(adapter);
    123                 }
    124             }
    125         });
    126         mListView.setAdapter(adapter);
    127 
    128         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    129             @Override
    130             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    131                 if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) {
    132                     return;
    133                 }
    134 
    135                 PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
    136 
    137                 if (printer == null) {
    138                     startAddPrinterActivity();
    139                 } else {
    140                     onPrinterSelected(printer);
    141                 }
    142             }
    143         });
    144 
    145         findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    146             @Override public void onClick(View v) {
    147                 startAddPrinterActivity();
    148             }
    149         });
    150 
    151         registerForContextMenu(mListView);
    152 
    153         getLoaderManager().initLoader(LOADER_ID_ENABLED_PRINT_SERVICES, null, this);
    154 
    155         // On first creation:
    156         //
    157         // If no services are installed, instantly open add printer dialog.
    158         // If some are disabled and some are enabled show a toast to notify the user
    159         if (savedInstanceState == null || !savedInstanceState.getBoolean(KEY_NOT_FIRST_CREATE)) {
    160             List<PrintServiceInfo> allServices =
    161                     ((PrintManager) getSystemService(Context.PRINT_SERVICE))
    162                             .getPrintServices(PrintManager.ALL_SERVICES);
    163             boolean hasEnabledServices = false;
    164             boolean hasDisabledServices = false;
    165 
    166             if (allServices != null) {
    167                 final int numServices = allServices.size();
    168                 for (int i = 0; i < numServices; i++) {
    169                     if (allServices.get(i).isEnabled()) {
    170                         hasEnabledServices = true;
    171                     } else {
    172                         hasDisabledServices = true;
    173                     }
    174                 }
    175             }
    176 
    177             if (!hasEnabledServices) {
    178                 startAddPrinterActivity();
    179             } else if (hasDisabledServices) {
    180                 String disabledServicesSetting = Settings.Secure.getString(getContentResolver(),
    181                         Settings.Secure.DISABLED_PRINT_SERVICES);
    182                 if (!TextUtils.isEmpty(disabledServicesSetting)) {
    183                     Toast.makeText(this, getString(R.string.print_services_disabled_toast),
    184                             Toast.LENGTH_LONG).show();
    185                 }
    186             }
    187         }
    188     }
    189 
    190     @Override
    191     protected void onSaveInstanceState(Bundle outState) {
    192         super.onSaveInstanceState(outState);
    193         outState.putBoolean(KEY_NOT_FIRST_CREATE, true);
    194     }
    195 
    196     @Override
    197     public boolean onCreateOptionsMenu(Menu menu) {
    198         super.onCreateOptionsMenu(menu);
    199 
    200         getMenuInflater().inflate(R.menu.select_printer_activity, menu);
    201 
    202         MenuItem searchItem = menu.findItem(R.id.action_search);
    203         SearchView searchView = (SearchView) searchItem.getActionView();
    204         searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    205             @Override
    206             public boolean onQueryTextSubmit(String query) {
    207                 return true;
    208             }
    209 
    210             @Override
    211             public boolean onQueryTextChange(String searchString) {
    212                 ((DestinationAdapter) mListView.getAdapter()).getFilter().filter(searchString);
    213                 return true;
    214             }
    215         });
    216         searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
    217             @Override
    218             public void onViewAttachedToWindow(View view) {
    219                 if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) {
    220                     view.announceForAccessibility(getString(
    221                             R.string.print_search_box_shown_utterance));
    222                 }
    223             }
    224             @Override
    225             public void onViewDetachedFromWindow(View view) {
    226                 if (!isFinishing() && AccessibilityManager.getInstance(
    227                         SelectPrinterActivity.this).isEnabled()) {
    228                     view.announceForAccessibility(getString(
    229                             R.string.print_search_box_hidden_utterance));
    230                 }
    231             }
    232         });
    233 
    234         return true;
    235     }
    236 
    237     @Override
    238     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
    239         if (view == mListView) {
    240             final int position = ((AdapterContextMenuInfo) menuInfo).position;
    241             PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position);
    242 
    243             menu.setHeaderTitle(printer.getName());
    244 
    245             // Add the select menu item if applicable.
    246             if (printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) {
    247                 MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer,
    248                         Menu.NONE, R.string.print_select_printer);
    249                 Intent intent = new Intent();
    250                 intent.putExtra(EXTRA_PRINTER, printer);
    251                 selectItem.setIntent(intent);
    252             }
    253 
    254             // Add the forget menu item if applicable.
    255             if (mPrinterRegistry.isFavoritePrinter(printer.getId())) {
    256                 MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer,
    257                         Menu.NONE, R.string.print_forget_printer);
    258                 Intent intent = new Intent();
    259                 intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
    260                 forgetItem.setIntent(intent);
    261             }
    262         }
    263     }
    264 
    265     @Override
    266     public boolean onContextItemSelected(MenuItem item) {
    267         switch (item.getItemId()) {
    268             case R.string.print_select_printer: {
    269                 PrinterInfo printer = item.getIntent().getParcelableExtra(EXTRA_PRINTER);
    270                 onPrinterSelected(printer);
    271             } return true;
    272 
    273             case R.string.print_forget_printer: {
    274                 PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID);
    275                 mPrinterRegistry.forgetFavoritePrinter(printerId);
    276             } return true;
    277         }
    278         return false;
    279     }
    280 
    281     /**
    282      * Adjust the UI if the enabled print services changed.
    283      */
    284     private synchronized void onPrintServicesUpdate() {
    285         updateEmptyView((DestinationAdapter)mListView.getAdapter());
    286         invalidateOptionsMenu();
    287     }
    288 
    289     @Override
    290     public void onStart() {
    291         super.onStart();
    292         onPrintServicesUpdate();
    293     }
    294 
    295     @Override
    296     public void onPause() {
    297         if (mAnnounceFilterResult != null) {
    298             mAnnounceFilterResult.remove();
    299         }
    300         super.onPause();
    301     }
    302 
    303     @Override
    304     public void onStop() {
    305         super.onStop();
    306     }
    307 
    308     private void onPrinterSelected(PrinterInfo printer) {
    309         Intent intent = new Intent();
    310         intent.putExtra(INTENT_EXTRA_PRINTER, printer);
    311         setResult(RESULT_OK, intent);
    312         finish();
    313     }
    314 
    315     public void updateEmptyView(DestinationAdapter adapter) {
    316         if (mListView.getEmptyView() == null) {
    317             View emptyView = findViewById(R.id.empty_print_state);
    318             mListView.setEmptyView(emptyView);
    319         }
    320         TextView titleView = (TextView) findViewById(R.id.title);
    321         View progressBar = findViewById(R.id.progress_bar);
    322         if (mEnabledPrintServices.size() == 0) {
    323             titleView.setText(R.string.print_no_print_services);
    324             progressBar.setVisibility(View.GONE);
    325         } else if (adapter.getUnfilteredCount() <= 0) {
    326             titleView.setText(R.string.print_searching_for_printers);
    327             progressBar.setVisibility(View.VISIBLE);
    328         } else {
    329             titleView.setText(R.string.print_no_printers);
    330             progressBar.setVisibility(View.GONE);
    331         }
    332     }
    333 
    334     private void announceSearchResultIfNeeded() {
    335         if (AccessibilityManager.getInstance(this).isEnabled()) {
    336             if (mAnnounceFilterResult == null) {
    337                 mAnnounceFilterResult = new AnnounceFilterResult();
    338             }
    339             mAnnounceFilterResult.post();
    340         }
    341     }
    342 
    343     @Override
    344     public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
    345         return new PrintServicesLoader((PrintManager) getSystemService(Context.PRINT_SERVICE), this,
    346                 PrintManager.ENABLED_SERVICES);
    347     }
    348 
    349     @Override
    350     public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
    351             List<PrintServiceInfo> services) {
    352         mEnabledPrintServices.clear();
    353 
    354         if (services != null && !services.isEmpty()) {
    355             final int numServices = services.size();
    356             for (int i = 0; i < numServices; i++) {
    357                 PrintServiceInfo service = services.get(i);
    358 
    359                 mEnabledPrintServices.put(service.getComponentName(), service);
    360             }
    361         }
    362 
    363         onPrintServicesUpdate();
    364     }
    365 
    366     @Override
    367     public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
    368         if (!isFinishing()) {
    369             onLoadFinished(loader, null);
    370         }
    371     }
    372 
    373     private final class DestinationAdapter extends BaseAdapter implements Filterable {
    374 
    375         private final Object mLock = new Object();
    376 
    377         private final List<PrinterInfo> mPrinters = new ArrayList<>();
    378 
    379         private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>();
    380 
    381         private CharSequence mLastSearchString;
    382 
    383         public DestinationAdapter() {
    384             mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() {
    385                 @Override
    386                 public void onPrintersChanged(List<PrinterInfo> printers) {
    387                     synchronized (mLock) {
    388                         mPrinters.clear();
    389                         mPrinters.addAll(printers);
    390                         mFilteredPrinters.clear();
    391                         mFilteredPrinters.addAll(printers);
    392                         if (!TextUtils.isEmpty(mLastSearchString)) {
    393                             getFilter().filter(mLastSearchString);
    394                         }
    395                     }
    396                     notifyDataSetChanged();
    397                 }
    398 
    399                 @Override
    400                 public void onPrintersInvalid() {
    401                     synchronized (mLock) {
    402                         mPrinters.clear();
    403                         mFilteredPrinters.clear();
    404                     }
    405                     notifyDataSetInvalidated();
    406                 }
    407             });
    408         }
    409 
    410         @Override
    411         public Filter getFilter() {
    412             return new Filter() {
    413                 @Override
    414                 protected FilterResults performFiltering(CharSequence constraint) {
    415                     synchronized (mLock) {
    416                         if (TextUtils.isEmpty(constraint)) {
    417                             return null;
    418                         }
    419                         FilterResults results = new FilterResults();
    420                         List<PrinterInfo> filteredPrinters = new ArrayList<>();
    421                         String constraintLowerCase = constraint.toString().toLowerCase();
    422                         final int printerCount = mPrinters.size();
    423                         for (int i = 0; i < printerCount; i++) {
    424                             PrinterInfo printer = mPrinters.get(i);
    425                             String description = printer.getDescription();
    426                             if (printer.getName().toLowerCase().contains(constraintLowerCase)
    427                                     || description != null && description.toLowerCase()
    428                                             .contains(constraintLowerCase)) {
    429                                 filteredPrinters.add(printer);
    430                             }
    431                         }
    432                         results.values = filteredPrinters;
    433                         results.count = filteredPrinters.size();
    434                         return results;
    435                     }
    436                 }
    437 
    438                 @Override
    439                 @SuppressWarnings("unchecked")
    440                 protected void publishResults(CharSequence constraint, FilterResults results) {
    441                     final boolean resultCountChanged;
    442                     synchronized (mLock) {
    443                         final int oldPrinterCount = mFilteredPrinters.size();
    444                         mLastSearchString = constraint;
    445                         mFilteredPrinters.clear();
    446                         if (results == null) {
    447                             mFilteredPrinters.addAll(mPrinters);
    448                         } else {
    449                             List<PrinterInfo> printers = (List<PrinterInfo>) results.values;
    450                             mFilteredPrinters.addAll(printers);
    451                         }
    452                         resultCountChanged = (oldPrinterCount != mFilteredPrinters.size());
    453                     }
    454                     if (resultCountChanged) {
    455                         announceSearchResultIfNeeded();
    456                     }
    457                     notifyDataSetChanged();
    458                 }
    459             };
    460         }
    461 
    462         public int getUnfilteredCount() {
    463             synchronized (mLock) {
    464                 return mPrinters.size();
    465             }
    466         }
    467 
    468         @Override
    469         public int getCount() {
    470             synchronized (mLock) {
    471                 if (mFilteredPrinters.isEmpty()) {
    472                     return 0;
    473                 } else {
    474                     // Add "add printer" item to the end of the list. If the list is empty there is
    475                     // a link on the empty view
    476                     return mFilteredPrinters.size() + 1;
    477                 }
    478             }
    479         }
    480 
    481         @Override
    482         public int getViewTypeCount() {
    483             return 2;
    484         }
    485 
    486         @Override
    487         public int getItemViewType(int position) {
    488             // Use separate view types for the "add printer" item an the items referring to printers
    489             if (getItem(position) == null) {
    490                 return 0;
    491             } else {
    492                 return 1;
    493             }
    494         }
    495 
    496         @Override
    497         public Object getItem(int position) {
    498             synchronized (mLock) {
    499                 if (position < mFilteredPrinters.size()) {
    500                     return mFilteredPrinters.get(position);
    501                 } else {
    502                     // Return null to mark this as the "add printer item"
    503                     return null;
    504                 }
    505             }
    506         }
    507 
    508         @Override
    509         public long getItemId(int position) {
    510             return position;
    511         }
    512 
    513         @Override
    514         public View getDropDownView(int position, View convertView, ViewGroup parent) {
    515             return getView(position, convertView, parent);
    516         }
    517 
    518         @Override
    519         public View getView(int position, View convertView, ViewGroup parent) {
    520             final PrinterInfo printer = (PrinterInfo) getItem(position);
    521 
    522             // Handle "add printer item"
    523             if (printer == null) {
    524                 if (convertView == null) {
    525                     convertView = getLayoutInflater().inflate(R.layout.add_printer_list_item,
    526                             parent, false);
    527                 }
    528 
    529                 return convertView;
    530             }
    531 
    532             if (convertView == null) {
    533                 convertView = getLayoutInflater().inflate(
    534                         R.layout.printer_list_item, parent, false);
    535             }
    536 
    537             convertView.setEnabled(isActionable(position));
    538 
    539 
    540             CharSequence title = printer.getName();
    541             Drawable icon = printer.loadIcon(SelectPrinterActivity.this);
    542 
    543             PrintServiceInfo service = mEnabledPrintServices.get(printer.getId().getServiceName());
    544 
    545             CharSequence printServiceLabel = null;
    546             if (service != null) {
    547                 printServiceLabel = service.getResolveInfo().loadLabel(getPackageManager())
    548                         .toString();
    549             }
    550 
    551             CharSequence description = printer.getDescription();
    552 
    553             CharSequence subtitle;
    554             if (TextUtils.isEmpty(printServiceLabel)) {
    555                 subtitle = description;
    556             } else if (TextUtils.isEmpty(description)) {
    557                 subtitle = printServiceLabel;
    558             } else {
    559                 subtitle = getString(R.string.printer_extended_description_template,
    560                         printServiceLabel, description);
    561             }
    562 
    563             TextView titleView = (TextView) convertView.findViewById(R.id.title);
    564             titleView.setText(title);
    565 
    566             TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle);
    567             if (!TextUtils.isEmpty(subtitle)) {
    568                 subtitleView.setText(subtitle);
    569                 subtitleView.setVisibility(View.VISIBLE);
    570             } else {
    571                 subtitleView.setText(null);
    572                 subtitleView.setVisibility(View.GONE);
    573             }
    574 
    575             LinearLayout moreInfoView = (LinearLayout) convertView.findViewById(R.id.more_info);
    576             if (printer.getInfoIntent() != null) {
    577                 moreInfoView.setVisibility(View.VISIBLE);
    578                 moreInfoView.setOnClickListener(new OnClickListener() {
    579                     @Override
    580                     public void onClick(View v) {
    581                         try {
    582                             startIntentSender(printer.getInfoIntent().getIntentSender(), null, 0, 0,
    583                                     0);
    584                         } catch (SendIntentException e) {
    585                             Log.e(LOG_TAG, "Could not execute pending info intent: %s", e);
    586                         }
    587                     }
    588                 });
    589             } else {
    590                 moreInfoView.setVisibility(View.GONE);
    591             }
    592 
    593             ImageView iconView = (ImageView) convertView.findViewById(R.id.icon);
    594             if (icon != null) {
    595                 iconView.setVisibility(View.VISIBLE);
    596                 if (!isActionable(position)) {
    597                     icon.mutate();
    598 
    599                     TypedValue value = new TypedValue();
    600                     getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true);
    601                     icon.setAlpha((int)(value.getFloat() * 255));
    602                 }
    603                 iconView.setImageDrawable(icon);
    604             } else {
    605                 iconView.setVisibility(View.GONE);
    606             }
    607 
    608             return convertView;
    609         }
    610 
    611         public boolean isActionable(int position) {
    612             PrinterInfo printer =  (PrinterInfo) getItem(position);
    613 
    614             if (printer == null) {
    615                 return true;
    616             } else {
    617                 return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE;
    618             }
    619         }
    620     }
    621 
    622     private final class AnnounceFilterResult implements Runnable {
    623         private static final int SEARCH_RESULT_ANNOUNCEMENT_DELAY = 1000; // 1 sec
    624 
    625         public void post() {
    626             remove();
    627             mListView.postDelayed(this, SEARCH_RESULT_ANNOUNCEMENT_DELAY);
    628         }
    629 
    630         public void remove() {
    631             mListView.removeCallbacks(this);
    632         }
    633 
    634         @Override
    635         public void run() {
    636             final int count = mListView.getAdapter().getCount();
    637             final String text;
    638             if (count <= 0) {
    639                 text = getString(R.string.print_no_printers);
    640             } else {
    641                 text = getResources().getQuantityString(
    642                     R.plurals.print_search_result_count_utterance, count, count);
    643             }
    644             mListView.announceForAccessibility(text);
    645         }
    646     }
    647 }
    648