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
onCreate(Bundle savedInstanceState)84     protected void onCreate(Bundle savedInstanceState) {
85         super.onCreate(savedInstanceState);
86         setListAdapter(new MotoGpStatAdapter(loadMotoGpStats(),
87                 getLayoutInflater()));
88     }
89 
90     @Override
onCreateOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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 
print()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 
loadMotoGpStats()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 
MotoGpStatAdapter(List<MotoGpStatItem> items, LayoutInflater inflater)518         public MotoGpStatAdapter(List<MotoGpStatItem> items, LayoutInflater inflater) {
519             mItems = items;
520             mInflater = inflater;
521         }
522 
cloneItems()523         public List<MotoGpStatItem> cloneItems() {
524             return new ArrayList<MotoGpStatItem>(mItems);
525         }
526 
527         @Override
getCount()528         public int getCount() {
529             return mItems.size();
530         }
531 
532         @Override
getItem(int position)533         public Object getItem(int position) {
534             return mItems.get(position);
535         }
536 
537         @Override
getItemId(int position)538         public long getItemId(int position) {
539             return position;
540         }
541 
542         @Override
getView(int position, View convertView, ViewGroup parent)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