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
17package android.support.v4.print;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.Matrix;
23import android.graphics.RectF;
24import android.graphics.pdf.PdfDocument.Page;
25import android.net.Uri;
26import android.os.AsyncTask;
27import android.os.Bundle;
28import android.os.CancellationSignal;
29import android.os.ParcelFileDescriptor;
30import android.print.PageRange;
31import android.print.PrintAttributes;
32import android.print.PrintDocumentAdapter;
33import android.print.PrintDocumentInfo;
34import android.print.PrintManager;
35import android.print.pdf.PrintedPdfDocument;
36import android.util.Log;
37
38import java.io.FileNotFoundException;
39import java.io.FileOutputStream;
40import java.io.IOException;
41import java.io.InputStream;
42
43/**
44 * Kitkat specific PrintManager API implementation.
45 */
46class PrintHelperKitkat {
47    private static final String LOG_TAG = "PrintHelperKitkat";
48    // will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi)
49    private final static int MAX_PRINT_SIZE = 3500;
50    final Context mContext;
51    BitmapFactory.Options mDecodeOptions = null;
52    private final Object mLock = new Object();
53    /**
54     * image will be scaled but leave white space
55     */
56    public static final int SCALE_MODE_FIT = 1;
57    /**
58     * image will fill the paper and be cropped (default)
59     */
60    public static final int SCALE_MODE_FILL = 2;
61
62    /**
63     * select landscape (default)
64     */
65    public static final int ORIENTATION_LANDSCAPE = 1;
66
67    /**
68     * select portrait
69     */
70    public static final int ORIENTATION_PORTRAIT = 2;
71
72    /**
73     * this is a black and white image
74     */
75    public static final int COLOR_MODE_MONOCHROME = 1;
76    /**
77     * this is a color image (default)
78     */
79    public static final int COLOR_MODE_COLOR = 2;
80
81    public interface OnPrintFinishCallback {
82        public void onFinish();
83    }
84
85    int mScaleMode = SCALE_MODE_FILL;
86
87    int mColorMode = COLOR_MODE_COLOR;
88
89    int mOrientation = ORIENTATION_LANDSCAPE;
90
91    PrintHelperKitkat(Context context) {
92        mContext = context;
93    }
94
95    /**
96     * Selects whether the image will fill the paper and be cropped
97     * <p/>
98     * {@link #SCALE_MODE_FIT}
99     * or whether the image will be scaled but leave white space
100     * {@link #SCALE_MODE_FILL}.
101     *
102     * @param scaleMode {@link #SCALE_MODE_FIT} or
103     *                  {@link #SCALE_MODE_FILL}
104     */
105    public void setScaleMode(int scaleMode) {
106        mScaleMode = scaleMode;
107    }
108
109    /**
110     * Returns the scale mode with which the image will fill the paper.
111     *
112     * @return The scale Mode: {@link #SCALE_MODE_FIT} or
113     * {@link #SCALE_MODE_FILL}
114     */
115    public int getScaleMode() {
116        return mScaleMode;
117    }
118
119    /**
120     * Sets whether the image will be printed in color (default)
121     * {@link #COLOR_MODE_COLOR} or in back and white
122     * {@link #COLOR_MODE_MONOCHROME}.
123     *
124     * @param colorMode The color mode which is one of
125     *                  {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}.
126     */
127    public void setColorMode(int colorMode) {
128        mColorMode = colorMode;
129    }
130
131    /**
132     * Sets whether to select landscape (default), {@link #ORIENTATION_LANDSCAPE}
133     * or portrait {@link #ORIENTATION_PORTRAIT}
134     * @param orientation The page orientation which is one of
135     *                    {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
136     */
137    public void setOrientation(int orientation) {
138        mOrientation = orientation;
139    }
140
141    /**
142     * Gets the page orientation with which the image will be printed.
143     *
144     * @return The preferred orientation which is one of
145     * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}
146     */
147    public int getOrientation() {
148        return mOrientation;
149    }
150
151    /**
152     * Gets the color mode with which the image will be printed.
153     *
154     * @return The color mode which is one of {@link #COLOR_MODE_COLOR}
155     * and {@link #COLOR_MODE_MONOCHROME}.
156     */
157    public int getColorMode() {
158        return mColorMode;
159    }
160
161    /**
162     * Prints a bitmap.
163     *
164     * @param jobName The print job name.
165     * @param bitmap  The bitmap to print.
166     * @param callback Optional callback to observe when printing is finished.
167     */
168    public void printBitmap(final String jobName, final Bitmap bitmap,
169            final OnPrintFinishCallback callback) {
170        if (bitmap == null) {
171            return;
172        }
173        final int fittingMode = mScaleMode; // grab the fitting mode at time of call
174        PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
175        PrintAttributes.MediaSize mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT;
176        if (bitmap.getWidth() > bitmap.getHeight()) {
177            mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE;
178        }
179        PrintAttributes attr = new PrintAttributes.Builder()
180                .setMediaSize(mediaSize)
181                .setColorMode(mColorMode)
182                .build();
183
184        printManager.print(jobName,
185                new PrintDocumentAdapter() {
186                    private PrintAttributes mAttributes;
187
188                    @Override
189                    public void onLayout(PrintAttributes oldPrintAttributes,
190                                         PrintAttributes newPrintAttributes,
191                                         CancellationSignal cancellationSignal,
192                                         LayoutResultCallback layoutResultCallback,
193                                         Bundle bundle) {
194
195                        mAttributes = newPrintAttributes;
196
197                        PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName)
198                                .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
199                                .setPageCount(1)
200                                .build();
201                        boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
202                        layoutResultCallback.onLayoutFinished(info, changed);
203                    }
204
205                    @Override
206                    public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
207                                        CancellationSignal cancellationSignal,
208                                        WriteResultCallback writeResultCallback) {
209                        PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
210                                mAttributes);
211                        try {
212                            Page page = pdfDocument.startPage(1);
213
214                            RectF content = new RectF(page.getInfo().getContentRect());
215
216                            Matrix matrix = getMatrix(bitmap.getWidth(), bitmap.getHeight(),
217                                    content, fittingMode);
218
219                            // Draw the bitmap.
220                            page.getCanvas().drawBitmap(bitmap, matrix, null);
221
222                            // Finish the page.
223                            pdfDocument.finishPage(page);
224
225                            try {
226                                // Write the document.
227                                pdfDocument.writeTo(new FileOutputStream(
228                                        fileDescriptor.getFileDescriptor()));
229                                // Done.
230                                writeResultCallback.onWriteFinished(
231                                        new PageRange[]{PageRange.ALL_PAGES});
232                            } catch (IOException ioe) {
233                                // Failed.
234                                Log.e(LOG_TAG, "Error writing printed content", ioe);
235                                writeResultCallback.onWriteFailed(null);
236                            }
237                        } finally {
238                            if (pdfDocument != null) {
239                                pdfDocument.close();
240                            }
241                            if (fileDescriptor != null) {
242                                try {
243                                    fileDescriptor.close();
244                                } catch (IOException ioe) {
245                                    /* ignore */
246                                }
247                            }
248                        }
249                    }
250
251                    @Override
252                    public void onFinish() {
253                        if (callback != null) {
254                            callback.onFinish();
255                        }
256                    }
257                }, attr);
258    }
259
260    /**
261     * Calculates the transform the print an Image to fill the page
262     *
263     * @param imageWidth  with of bitmap
264     * @param imageHeight height of bitmap
265     * @param content     The output page dimensions
266     * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs {@link #SCALE_MODE_FIT}
267     * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call
268     */
269    private Matrix getMatrix(int imageWidth, int imageHeight, RectF content, int fittingMode) {
270        Matrix matrix = new Matrix();
271
272        // Compute and apply scale to fill the page.
273        float scale = content.width() / imageWidth;
274        if (fittingMode == SCALE_MODE_FILL) {
275            scale = Math.max(scale, content.height() / imageHeight);
276        } else {
277            scale = Math.min(scale, content.height() / imageHeight);
278        }
279        matrix.postScale(scale, scale);
280
281        // Center the content.
282        final float translateX = (content.width()
283                - imageWidth * scale) / 2;
284        final float translateY = (content.height()
285                - imageHeight * scale) / 2;
286        matrix.postTranslate(translateX, translateY);
287        return matrix;
288    }
289
290    /**
291     * Prints an image located at the Uri. Image types supported are those of
292     * <code>BitmapFactory.decodeStream</code> (JPEG, GIF, PNG, BMP, WEBP)
293     *
294     * @param jobName   The print job name.
295     * @param imageFile The <code>Uri</code> pointing to an image to print.
296     * @param callback Optional callback to observe when printing is finished.
297     * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
298     */
299    public void printBitmap(final String jobName, final Uri imageFile,
300            final OnPrintFinishCallback callback) throws FileNotFoundException {
301        final int fittingMode = mScaleMode;
302
303        PrintDocumentAdapter printDocumentAdapter = new PrintDocumentAdapter() {
304            private PrintAttributes mAttributes;
305            AsyncTask<Uri, Boolean, Bitmap> mLoadBitmap;
306            Bitmap mBitmap = null;
307
308            @Override
309            public void onLayout(final PrintAttributes oldPrintAttributes,
310                                 final PrintAttributes newPrintAttributes,
311                                 final CancellationSignal cancellationSignal,
312                                 final LayoutResultCallback layoutResultCallback,
313                                 Bundle bundle) {
314
315                mAttributes = newPrintAttributes;
316
317                if (cancellationSignal.isCanceled()) {
318                    layoutResultCallback.onLayoutCancelled();
319                    return;
320                }
321                // we finished the load
322                if (mBitmap != null) {
323                    PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName)
324                            .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
325                            .setPageCount(1)
326                            .build();
327                    boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
328                    layoutResultCallback.onLayoutFinished(info, changed);
329                    return;
330                }
331
332                mLoadBitmap = new AsyncTask<Uri, Boolean, Bitmap>() {
333
334                    @Override
335                    protected void onPreExecute() {
336                        // First register for cancellation requests.
337                        cancellationSignal.setOnCancelListener(
338                                new CancellationSignal.OnCancelListener() {
339                                    @Override
340                                    public void onCancel() { // on different thread
341                                        cancelLoad();
342                                        cancel(false);
343                                    }
344                                });
345                    }
346
347                    @Override
348                    protected Bitmap doInBackground(Uri... uris) {
349                        try {
350                            return loadConstrainedBitmap(imageFile, MAX_PRINT_SIZE);
351                        } catch (FileNotFoundException e) {
352                          /* ignore */
353                        }
354                        return null;
355                    }
356
357                    @Override
358                    protected void onPostExecute(Bitmap bitmap) {
359                        super.onPostExecute(bitmap);
360                        mBitmap = bitmap;
361                        if (bitmap != null) {
362                            PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName)
363                                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
364                                    .setPageCount(1)
365                                    .build();
366                            boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
367
368                            layoutResultCallback.onLayoutFinished(info, changed);
369
370                        } else {
371                            layoutResultCallback.onLayoutFailed(null);
372                        }
373                        mLoadBitmap = null;
374                    }
375
376                    @Override
377                    protected void onCancelled(Bitmap result) {
378                        // Task was cancelled, report that.
379                        layoutResultCallback.onLayoutCancelled();
380                        mLoadBitmap = null;
381                    }
382                }.execute();
383            }
384
385            private void cancelLoad() {
386                synchronized (mLock) { // prevent race with set null below
387                    if (mDecodeOptions != null) {
388                        mDecodeOptions.requestCancelDecode();
389                        mDecodeOptions = null;
390                    }
391                }
392            }
393
394            @Override
395            public void onFinish() {
396                super.onFinish();
397                cancelLoad();
398                if (mLoadBitmap != null) {
399                    mLoadBitmap.cancel(true);
400                }
401                if (callback != null) {
402                    callback.onFinish();
403                }
404            }
405
406            @Override
407            public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
408                                CancellationSignal cancellationSignal,
409                                WriteResultCallback writeResultCallback) {
410                PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
411                        mAttributes);
412                try {
413
414                    Page page = pdfDocument.startPage(1);
415                    RectF content = new RectF(page.getInfo().getContentRect());
416
417                    // Compute and apply scale to fill the page.
418                    Matrix matrix = getMatrix(mBitmap.getWidth(), mBitmap.getHeight(),
419                            content, fittingMode);
420
421                    // Draw the bitmap.
422                    page.getCanvas().drawBitmap(mBitmap, matrix, null);
423
424                    // Finish the page.
425                    pdfDocument.finishPage(page);
426
427                    try {
428                        // Write the document.
429                        pdfDocument.writeTo(new FileOutputStream(
430                                fileDescriptor.getFileDescriptor()));
431                        // Done.
432                        writeResultCallback.onWriteFinished(
433                                new PageRange[]{PageRange.ALL_PAGES});
434                    } catch (IOException ioe) {
435                        // Failed.
436                        Log.e(LOG_TAG, "Error writing printed content", ioe);
437                        writeResultCallback.onWriteFailed(null);
438                    }
439                } finally {
440                    if (pdfDocument != null) {
441                        pdfDocument.close();
442                    }
443                    if (fileDescriptor != null) {
444                        try {
445                            fileDescriptor.close();
446                        } catch (IOException ioe) {
447                            /* ignore */
448                        }
449                    }
450                }
451            }
452        };
453
454        PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
455        PrintAttributes.Builder builder = new PrintAttributes.Builder();
456        builder.setColorMode(mColorMode);
457
458        if (mOrientation == ORIENTATION_LANDSCAPE) {
459            builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE);
460        } else if (mOrientation == ORIENTATION_PORTRAIT) {
461            builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT);
462        }
463        PrintAttributes attr = builder.build();
464
465        printManager.print(jobName, printDocumentAdapter, attr);
466    }
467
468    /**
469     * Loads a bitmap while limiting its size
470     *
471     * @param uri           location of a valid image
472     * @param maxSideLength the maximum length of a size
473     * @return the Bitmap
474     * @throws FileNotFoundException if the Uri does not point to an image
475     */
476    private Bitmap loadConstrainedBitmap(Uri uri, int maxSideLength) throws FileNotFoundException {
477        if (maxSideLength <= 0 || uri == null || mContext == null) {
478            throw new IllegalArgumentException("bad argument to getScaledBitmap");
479        }
480        // Get width and height of stored bitmap
481        BitmapFactory.Options opt = new BitmapFactory.Options();
482        opt.inJustDecodeBounds = true;
483        loadBitmap(uri, opt);
484
485        int w = opt.outWidth;
486        int h = opt.outHeight;
487
488        // If bitmap cannot be decoded, return null
489        if (w <= 0 || h <= 0) {
490            return null;
491        }
492
493        // Find best downsampling size
494        int imageSide = Math.max(w, h);
495
496        int sampleSize = 1;
497        while (imageSide > maxSideLength) {
498            imageSide >>>= 1;
499            sampleSize <<= 1;
500        }
501
502        // Make sure sample size is reasonable
503        if (sampleSize <= 0 || 0 >= (int) (Math.min(w, h) / sampleSize)) {
504            return null;
505        }
506        BitmapFactory.Options decodeOptions = null;
507        synchronized (mLock) { // prevent race with set null below
508            mDecodeOptions = new BitmapFactory.Options();
509            mDecodeOptions.inMutable = true;
510            mDecodeOptions.inSampleSize = sampleSize;
511            decodeOptions = mDecodeOptions;
512        }
513        try {
514            return loadBitmap(uri, decodeOptions);
515        } finally {
516            synchronized (mLock) {
517                mDecodeOptions = null;
518            }
519        }
520    }
521
522    /**
523     * Returns the bitmap from the given uri loaded using the given options.
524     * Returns null on failure.
525     */
526    private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException {
527        if (uri == null || mContext == null) {
528            throw new IllegalArgumentException("bad argument to loadBitmap");
529        }
530        InputStream is = null;
531        try {
532            is = mContext.getContentResolver().openInputStream(uri);
533            return BitmapFactory.decodeStream(is, null, o);
534        } finally {
535            if (is != null) {
536                try {
537                    is.close();
538                } catch (IOException t) {
539                    Log.w(LOG_TAG, "close fail ", t);
540                }
541            }
542        }
543    }
544}