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