Home | History | Annotate | Download | only in app
      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.example.android.apis.app;
     18 
     19 import android.app.ListActivity;
     20 import android.content.Context;
     21 import android.content.res.Configuration;
     22 import android.graphics.pdf.PdfDocument.Page;
     23 import android.os.AsyncTask;
     24 import android.os.Bundle;
     25 import android.os.CancellationSignal;
     26 import android.os.CancellationSignal.OnCancelListener;
     27 import android.os.ParcelFileDescriptor;
     28 import android.print.PageRange;
     29 import android.print.PrintAttributes;
     30 import android.print.PrintDocumentAdapter;
     31 import android.print.PrintDocumentInfo;
     32 import android.print.PrintManager;
     33 import android.print.pdf.PrintedPdfDocument;
     34 import android.util.SparseIntArray;
     35 import android.view.LayoutInflater;
     36 import android.view.Menu;
     37 import android.view.MenuItem;
     38 import android.view.View;
     39 import android.view.View.MeasureSpec;
     40 import android.view.ViewGroup;
     41 import android.widget.BaseAdapter;
     42 import android.widget.LinearLayout;
     43 import android.widget.TextView;
     44 
     45 import com.example.android.apis.R;
     46 
     47 import java.io.FileOutputStream;
     48 import java.io.IOException;
     49 import java.util.ArrayList;
     50 import java.util.List;
     51 
     52 /**
     53  * This class demonstrates how to implement custom printing support.
     54  * <p>
     55  * This activity shows the list of the MotoGP champions by year and
     56  * brand. The print option in the overflow menu allows the user to
     57  * print the content. The list list of items is laid out to such that
     58  * it fits the options selected by the user from the UI such as page
     59  * size. Hence, for different page sizes the printed content will have
     60  * different page count.
     61  * </p>
     62  * <p>
     63  * This sample demonstrates how to completely implement a {@link
     64  * PrintDocumentAdapter} in which:
     65  * <ul>
     66  * <li>Layout based on the selected print options is performed.</li>
     67  * <li>Layout work is performed only if print options change would change the content.</li>
     68  * <li>Layout result is properly reported.</li>
     69  * <li>Only requested pages are written.</li>
     70  * <li>Write result is properly reported.</li>
     71  * <li>Both Layout and write respond to cancellation.</li>
     72  * <li>Layout and render of views is demonstrated.</li>
     73  * </ul>
     74  * </p>
     75  *
     76  * @see PrintManager
     77  * @see PrintDocumentAdapter
     78  */
     79 public class PrintCustomContent extends ListActivity {
     80 
     81     private static final int MILS_IN_INCH = 1000;
     82 
     83     @Override
     84     protected void onCreate(Bundle savedInstanceState) {
     85         super.onCreate(savedInstanceState);
     86         setListAdapter(new MotoGpStatAdapter(loadMotoGpStats(),
     87                 getLayoutInflater()));
     88     }
     89 
     90     @Override
     91     public boolean onCreateOptionsMenu(Menu menu) {
     92         super.onCreateOptionsMenu(menu);
     93         getMenuInflater().inflate(R.menu.print_custom_content, menu);
     94         return true;
     95     }
     96 
     97     @Override
     98     public boolean onOptionsItemSelected(MenuItem item) {
     99         if (item.getItemId() == R.id.menu_print) {
    100             print();
    101             return true;
    102         }
    103         return super.onOptionsItemSelected(item);
    104     }
    105 
    106     private void print() {
    107         PrintManager printManager = (PrintManager) getSystemService(
    108                 Context.PRINT_SERVICE);
    109 
    110         printManager.print("MotoGP stats",
    111             new PrintDocumentAdapter() {
    112                 private int mRenderPageWidth;
    113                 private int mRenderPageHeight;
    114 
    115                 private PrintAttributes mPrintAttributes;
    116                 private PrintDocumentInfo mDocumentInfo;
    117                 private Context mPrintContext;
    118 
    119                 @Override
    120                 public void onLayout(final PrintAttributes oldAttributes,
    121                         final PrintAttributes newAttributes,
    122                         final CancellationSignal cancellationSignal,
    123                         final LayoutResultCallback callback,
    124                         final Bundle metadata) {
    125 
    126                     // If we are already cancelled, don't do any work.
    127                     if (cancellationSignal.isCanceled()) {
    128                         callback.onLayoutCancelled();
    129                         return;
    130                     }
    131 
    132                     // Now we determined if the print attributes changed in a way that
    133                     // would change the layout and if so we will do a layout pass.
    134                     boolean layoutNeeded = false;
    135 
    136                     final int density = Math.max(newAttributes.getResolution().getHorizontalDpi(),
    137                             newAttributes.getResolution().getVerticalDpi());
    138 
    139                     // Note that we are using the PrintedPdfDocument class which creates
    140                     // a PDF generating canvas whose size is in points (1/72") not screen
    141                     // pixels. Hence, this canvas is pretty small compared to the screen.
    142                     // The recommended way is to layout the content in the desired size,
    143                     // in this case as large as the printer can do, and set a translation
    144                     // to the PDF canvas to shrink in. Note that PDF is a vector format
    145                     // and you will not lose data during the transformation.
    146 
    147                     // The content width is equal to the page width minus the margins times
    148                     // the horizontal printer density. This way we get the maximal number
    149                     // of pixels the printer can put horizontally.
    150                     final int marginLeft = (int) (density * (float) newAttributes.getMinMargins()
    151                             .getLeftMils() / MILS_IN_INCH);
    152                     final int marginRight = (int) (density * (float) newAttributes.getMinMargins()
    153                             .getRightMils() / MILS_IN_INCH);
    154                     final int contentWidth = (int) (density * (float) newAttributes.getMediaSize()
    155                             .getWidthMils() / MILS_IN_INCH) - marginLeft - marginRight;
    156                     if (mRenderPageWidth != contentWidth) {
    157                         mRenderPageWidth = contentWidth;
    158                         layoutNeeded = true;
    159                     }
    160 
    161                     // The content height is equal to the page height minus the margins times
    162                     // the vertical printer resolution. This way we get the maximal number
    163                     // of pixels the printer can put vertically.
    164                     final int marginTop = (int) (density * (float) newAttributes.getMinMargins()
    165                             .getTopMils() / MILS_IN_INCH);
    166                     final int marginBottom = (int) (density * (float) newAttributes.getMinMargins()
    167                             .getBottomMils() / MILS_IN_INCH);
    168                     final int contentHeight = (int) (density * (float) newAttributes.getMediaSize()
    169                             .getHeightMils() / MILS_IN_INCH) - marginTop - marginBottom;
    170                     if (mRenderPageHeight != contentHeight) {
    171                         mRenderPageHeight = contentHeight;
    172                         layoutNeeded = true;
    173                     }
    174 
    175                     // Create a context for resources at printer density. We will
    176                     // be inflating views to render them and would like them to use
    177                     // resources for a density the printer supports.
    178                     if (mPrintContext == null || mPrintContext.getResources()
    179                             .getConfiguration().densityDpi != density) {
    180                         Configuration configuration = new Configuration();
    181                         configuration.densityDpi = density;
    182                         mPrintContext = createConfigurationContext(
    183                                 configuration);
    184                         mPrintContext.setTheme(android.R.style.Theme_Holo_Light);
    185                     }
    186 
    187                     // If no layout is needed that we did a layout at least once and
    188                     // the document info is not null, also the second argument is false
    189                     // to notify the system that the content did not change. This is
    190                     // important as if the system has some pages and the content didn't
    191                     // change the system will ask, the application to write them again.
    192                     if (!layoutNeeded) {
    193                         callback.onLayoutFinished(mDocumentInfo, false);
    194                         return;
    195                     }
    196 
    197                     // For demonstration purposes we will do the layout off the main
    198                     // thread but for small content sizes like this one it is OK to do
    199                     // that on the main thread.
    200 
    201                     // Store the data as we will layout off the main thread.
    202                     final List<MotoGpStatItem> items = ((MotoGpStatAdapter)
    203                                     getListAdapter()).cloneItems();
    204 
    205                     new AsyncTask<Void, Void, PrintDocumentInfo>() {
    206                         @Override
    207                         protected void onPreExecute() {
    208                             // First register for cancellation requests.
    209                             cancellationSignal.setOnCancelListener(new OnCancelListener() {
    210                                 @Override
    211                                 public void onCancel() {
    212                                     cancel(true);
    213                                 }
    214                             });
    215                             // Stash the attributes as we will need them for rendering.
    216                             mPrintAttributes = newAttributes;
    217                         }
    218 
    219                         @Override
    220                         protected PrintDocumentInfo doInBackground(Void... params) {
    221                             try {
    222                                 // Create an adapter with the stats and an inflater
    223                                 // to load resources for the printer density.
    224                                 MotoGpStatAdapter adapter = new MotoGpStatAdapter(items,
    225                                         (LayoutInflater) mPrintContext.getSystemService(
    226                                                 Context.LAYOUT_INFLATER_SERVICE));
    227 
    228                                 int currentPage = 0;
    229                                 int pageContentHeight = 0;
    230                                 int viewType = -1;
    231                                 View view = null;
    232                                 LinearLayout dummyParent = new LinearLayout(mPrintContext);
    233                                 dummyParent.setOrientation(LinearLayout.VERTICAL);
    234 
    235                                 final int itemCount = adapter.getCount();
    236                                 for (int i = 0; i < itemCount; i++) {
    237                                     // Be nice and respond to cancellation.
    238                                     if (isCancelled()) {
    239                                         return null;
    240                                     }
    241 
    242                                     // Get the next view.
    243                                     final int nextViewType = adapter.getItemViewType(i);
    244                                     if (viewType == nextViewType) {
    245                                         view = adapter.getView(i, view, dummyParent);
    246                                     } else {
    247                                         view = adapter.getView(i, null, dummyParent);
    248                                     }
    249                                     viewType = nextViewType;
    250 
    251                                     // Measure the next view
    252                                     measureView(view);
    253 
    254                                     // Add the height but if the view crosses the page
    255                                     // boundary we will put it to the next page.
    256                                     pageContentHeight += view.getMeasuredHeight();
    257                                     if (pageContentHeight > mRenderPageHeight) {
    258                                         pageContentHeight = view.getMeasuredHeight();
    259                                         currentPage++;
    260                                     }
    261                                 }
    262 
    263                                 // Create a document info describing the result.
    264                                 PrintDocumentInfo info = new PrintDocumentInfo
    265                                         .Builder("MotoGP_stats.pdf")
    266                                     .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
    267                                     .setPageCount(currentPage + 1)
    268                                     .build();
    269 
    270                                 // We completed the layout as a result of print attributes
    271                                 // change. Hence, if we are here the content changed for
    272                                 // sure which is why we pass true as the second argument.
    273                                 callback.onLayoutFinished(info, true);
    274                                 return info;
    275                             } catch (Exception e) {
    276                                 // An unexpected error, report that we failed and
    277                                 // one may pass in a human readable localized text
    278                                 // for what the error is if known.
    279                                 callback.onLayoutFailed(null);
    280                                 throw new RuntimeException(e);
    281                             }
    282                         }
    283 
    284                         @Override
    285                         protected void onPostExecute(PrintDocumentInfo result) {
    286                             // Update the cached info to send it over if the next
    287                             // layout pass does not result in a content change.
    288                             mDocumentInfo = result;
    289                         }
    290 
    291                         @Override
    292                         protected void onCancelled(PrintDocumentInfo result) {
    293                             // Task was cancelled, report that.
    294                             callback.onLayoutCancelled();
    295                         }
    296                     }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
    297                 }
    298 
    299                 @Override
    300                 public void onWrite(final PageRange[] pages,
    301                         final ParcelFileDescriptor destination,
    302                         final CancellationSignal cancellationSignal,
    303                         final WriteResultCallback callback) {
    304 
    305                     // If we are already cancelled, don't do any work.
    306                     if (cancellationSignal.isCanceled()) {
    307                         callback.onWriteCancelled();
    308                         return;
    309                     }
    310 
    311                     // Store the data as we will layout off the main thread.
    312                     final List<MotoGpStatItem> items = ((MotoGpStatAdapter)
    313                                     getListAdapter()).cloneItems();
    314 
    315                     new AsyncTask<Void, Void, Void>() {
    316                         private final SparseIntArray mWrittenPages = new SparseIntArray();
    317                         private final PrintedPdfDocument mPdfDocument = new PrintedPdfDocument(
    318                                 PrintCustomContent.this, mPrintAttributes);
    319 
    320                         @Override
    321                         protected void onPreExecute() {
    322                             // First register for cancellation requests.
    323                             cancellationSignal.setOnCancelListener(new OnCancelListener() {
    324                                 @Override
    325                                 public void onCancel() {
    326                                     cancel(true);
    327                                 }
    328                             });
    329                         }
    330 
    331                         @Override
    332                         protected Void doInBackground(Void... params) {
    333                             // Go over all the pages and write only the requested ones.
    334                             // Create an adapter with the stats and an inflater
    335                             // to load resources for the printer density.
    336                             MotoGpStatAdapter adapter = new MotoGpStatAdapter(items,
    337                                     (LayoutInflater) mPrintContext.getSystemService(
    338                                             Context.LAYOUT_INFLATER_SERVICE));
    339 
    340                             int currentPage = -1;
    341                             int pageContentHeight = 0;
    342                             int viewType = -1;
    343                             View view = null;
    344                             Page page = null;
    345                             LinearLayout dummyParent = new LinearLayout(mPrintContext);
    346                             dummyParent.setOrientation(LinearLayout.VERTICAL);
    347 
    348                             // The content is laid out and rendered in screen pixels with
    349                             // the width and height of the paper size times the print
    350                             // density but the PDF canvas size is in points which are 1/72",
    351                             // so we will scale down the content.
    352                             final float scale =  Math.min(
    353                                     (float) mPdfDocument.getPageContentRect().width()
    354                                             / mRenderPageWidth,
    355                                     (float) mPdfDocument.getPageContentRect().height()
    356                                             / mRenderPageHeight);
    357 
    358                             final int itemCount = adapter.getCount();
    359                             for (int i = 0; i < itemCount; i++) {
    360                                 // Be nice and respond to cancellation.
    361                                 if (isCancelled()) {
    362                                     return null;
    363                                 }
    364 
    365                                 // Get the next view.
    366                                 final int nextViewType = adapter.getItemViewType(i);
    367                                 if (viewType == nextViewType) {
    368                                     view = adapter.getView(i, view, dummyParent);
    369                                 } else {
    370                                     view = adapter.getView(i, null, dummyParent);
    371                                 }
    372                                 viewType = nextViewType;
    373 
    374                                 // Measure the next view
    375                                 measureView(view);
    376 
    377                                 // Add the height but if the view crosses the page
    378                                 // boundary we will put it to the next one.
    379                                 pageContentHeight += view.getMeasuredHeight();
    380                                 if (currentPage < 0 || pageContentHeight > mRenderPageHeight) {
    381                                     pageContentHeight = view.getMeasuredHeight();
    382                                     currentPage++;
    383                                     // Done with the current page - finish it.
    384                                     if (page != null) {
    385                                         mPdfDocument.finishPage(page);
    386                                     }
    387                                     // If the page is requested, render it.
    388                                     if (containsPage(pages, currentPage)) {
    389                                         page = mPdfDocument.startPage(currentPage);
    390                                         page.getCanvas().scale(scale, scale);
    391                                         // Keep track which pages are written.
    392                                         mWrittenPages.append(mWrittenPages.size(), currentPage);
    393                                     } else {
    394                                         page = null;
    395                                     }
    396                                 }
    397 
    398                                 // If the current view is on a requested page, render it.
    399                                 if (page != null) {
    400                                     // Layout an render the content.
    401                                     view.layout(0, 0, view.getMeasuredWidth(),
    402                                             view.getMeasuredHeight());
    403                                     view.draw(page.getCanvas());
    404                                     // Move the canvas for the next view.
    405                                     page.getCanvas().translate(0, view.getHeight());
    406                                 }
    407                             }
    408 
    409                             // Done with the last page.
    410                             if (page != null) {
    411                                 mPdfDocument.finishPage(page);
    412                             }
    413 
    414                             // Write the data and return success or failure.
    415                             try {
    416                                 mPdfDocument.writeTo(new FileOutputStream(
    417                                         destination.getFileDescriptor()));
    418                                 // Compute which page ranges were written based on
    419                                 // the bookkeeping we maintained.
    420                                 PageRange[] pageRanges = computeWrittenPageRanges(mWrittenPages);
    421                                 callback.onWriteFinished(pageRanges);
    422                             } catch (IOException ioe) {
    423                                 callback.onWriteFailed(null);
    424                             } finally {
    425                                 mPdfDocument.close();
    426                             }
    427 
    428                             return null;
    429                         }
    430 
    431                         @Override
    432                         protected void onCancelled(Void result) {
    433                             // Task was cancelled, report that.
    434                             callback.onWriteCancelled();
    435                             mPdfDocument.close();
    436                         }
    437                     }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
    438                 }
    439 
    440                 private void measureView(View view) {
    441                     final int widthMeasureSpec = ViewGroup.getChildMeasureSpec(
    442                             MeasureSpec.makeMeasureSpec(mRenderPageWidth,
    443                             MeasureSpec.EXACTLY), 0, view.getLayoutParams().width);
    444                     final int heightMeasureSpec = ViewGroup.getChildMeasureSpec(
    445                             MeasureSpec.makeMeasureSpec(mRenderPageHeight,
    446                             MeasureSpec.EXACTLY), 0, view.getLayoutParams().height);
    447                     view.measure(widthMeasureSpec, heightMeasureSpec);
    448                 }
    449 
    450                 private PageRange[] computeWrittenPageRanges(SparseIntArray writtenPages) {
    451                     List<PageRange> pageRanges = new ArrayList<PageRange>();
    452 
    453                     int start = -1;
    454                     int end = -1;
    455                     final int writtenPageCount = writtenPages.size();
    456                     for (int i = 0; i < writtenPageCount; i++) {
    457                         if (start < 0) {
    458                             start = writtenPages.valueAt(i);
    459                         }
    460                         int oldEnd = end = start;
    461                         while (i < writtenPageCount && (end - oldEnd) <= 1) {
    462                             oldEnd = end;
    463                             end = writtenPages.valueAt(i);
    464                             i++;
    465                         }
    466                         PageRange pageRange = new PageRange(start, end);
    467                         pageRanges.add(pageRange);
    468                         start = end = -1;
    469                     }
    470 
    471                     PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
    472                     pageRanges.toArray(pageRangesArray);
    473                     return pageRangesArray;
    474                 }
    475 
    476                 private boolean containsPage(PageRange[] pageRanges, int page) {
    477                     final int pageRangeCount = pageRanges.length;
    478                     for (int i = 0; i < pageRangeCount; i++) {
    479                         if (pageRanges[i].getStart() <= page
    480                                 && pageRanges[i].getEnd() >= page) {
    481                             return true;
    482                         }
    483                     }
    484                     return false;
    485                 }
    486         }, null);
    487     }
    488 
    489     private List<MotoGpStatItem> loadMotoGpStats() {
    490         String[] years = getResources().getStringArray(R.array.motogp_years);
    491         String[] champions = getResources().getStringArray(R.array.motogp_champions);
    492         String[] constructors = getResources().getStringArray(R.array.motogp_constructors);
    493 
    494         List<MotoGpStatItem> items = new ArrayList<MotoGpStatItem>();
    495 
    496         final int itemCount = years.length;
    497         for (int i = 0; i < itemCount; i++) {
    498             MotoGpStatItem item = new MotoGpStatItem();
    499             item.year = years[i];
    500             item.champion = champions[i];
    501             item.constructor = constructors[i];
    502             items.add(item);
    503         }
    504 
    505         return items;
    506     }
    507 
    508     private static final class MotoGpStatItem {
    509         String year;
    510         String champion;
    511         String constructor;
    512     }
    513 
    514     private class MotoGpStatAdapter extends BaseAdapter {
    515         private final List<MotoGpStatItem> mItems;
    516         private final LayoutInflater mInflater;
    517 
    518         public MotoGpStatAdapter(List<MotoGpStatItem> items, LayoutInflater inflater) {
    519             mItems = items;
    520             mInflater = inflater;
    521         }
    522 
    523         public List<MotoGpStatItem> cloneItems() {
    524             return new ArrayList<MotoGpStatItem>(mItems);
    525         }
    526 
    527         @Override
    528         public int getCount() {
    529             return mItems.size();
    530         }
    531 
    532         @Override
    533         public Object getItem(int position) {
    534             return mItems.get(position);
    535         }
    536 
    537         @Override
    538         public long getItemId(int position) {
    539             return position;
    540         }
    541 
    542         @Override
    543         public View getView(int position, View convertView, ViewGroup parent) {
    544             if (convertView == null) {
    545                 convertView = mInflater.inflate(R.layout.motogp_stat_item, parent, false);
    546             }
    547 
    548             MotoGpStatItem item = (MotoGpStatItem) getItem(position);
    549 
    550             TextView yearView = (TextView) convertView.findViewById(R.id.year);
    551             yearView.setText(item.year);
    552 
    553             TextView championView = (TextView) convertView.findViewById(R.id.champion);
    554             championView.setText(item.champion);
    555 
    556             TextView constructorView = (TextView) convertView.findViewById(R.id.constructor);
    557             constructorView.setText(item.constructor);
    558 
    559             return convertView;
    560         }
    561     }
    562 }
    563