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