PrintHelper.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.print;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.pdf.PdfDocument;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.print.pdf.PrintedPdfDocument;
import android.util.Log;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Helper for printing bitmaps.
 */
public final class PrintHelper {
    private static final String LOG_TAG = "PrintHelper";
    // will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi)
    private static final int MAX_PRINT_SIZE = 3500;

    /**
     * Whether the PrintActivity respects the suggested orientation.
     *
     * There is a bug in the PrintActivity that causes it to ignore the orientation
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static final boolean PRINT_ACTIVITY_RESPECTS_ORIENTATION =
            Build.VERSION.SDK_INT < 20 || Build.VERSION.SDK_INT > 23;

    /**
     * Whether the print subsystem handles min margins correctly. If not the print helper needs
     * to fake this.
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static final boolean IS_MIN_MARGINS_HANDLING_CORRECT = Build.VERSION.SDK_INT != 23;

    /**
     * image will be scaled but leave white space
     */
    public static final int SCALE_MODE_FIT = 1;

    /**
     * image will fill the paper and be cropped (default)
     */
    public static final int SCALE_MODE_FILL = 2;

    /**
     * this is a black and white image
     */
    @SuppressLint("InlinedApi")
    public static final int COLOR_MODE_MONOCHROME = PrintAttributes.COLOR_MODE_MONOCHROME;

    /**
     * this is a color image (default)
     */
    @SuppressLint("InlinedApi")
    public static final int COLOR_MODE_COLOR = PrintAttributes.COLOR_MODE_COLOR;

    /**
     * Print the image in landscape orientation (default).
     */
    public static final int ORIENTATION_LANDSCAPE = 1;

    /**
     * Print the image in  portrait orientation.
     */
    public static final int ORIENTATION_PORTRAIT = 2;

    /**
     * Callback for observing when a print operation is completed.
     * When print is finished either the system acquired the
     * document to print or printing was cancelled.
     */
    public interface OnPrintFinishCallback {
        /**
         * Called when a print operation is finished.
         */
        void onFinish();
    }

    @IntDef({SCALE_MODE_FIT, SCALE_MODE_FILL})
    @Retention(RetentionPolicy.SOURCE)
    private @interface ScaleMode {}

    @IntDef({COLOR_MODE_MONOCHROME, COLOR_MODE_COLOR})
    @Retention(RetentionPolicy.SOURCE)
    private @interface ColorMode {}

    @IntDef({ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT})
    @Retention(RetentionPolicy.SOURCE)
    private @interface Orientation {}

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Context mContext;

    BitmapFactory.Options mDecodeOptions = null;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mLock = new Object();

    @ScaleMode int mScaleMode = SCALE_MODE_FILL;
    @ColorMode int mColorMode = COLOR_MODE_COLOR;
    @Orientation int mOrientation = ORIENTATION_LANDSCAPE;

    /**
     * Gets whether the system supports printing.
     *
     * @return True if printing is supported.
     */
    public static boolean systemSupportsPrint() {
        // Supported on Android 4.4 or later.
        return Build.VERSION.SDK_INT >= 19;
    }

    /**
     * Constructs the PrintHelper that can be used to print images.
     *
     * @param context A context for accessing system resources.
     */
    public PrintHelper(@NonNull Context context) {
        mContext = context;
    }

    /**
     * Selects whether the image will fill the paper and be cropped
     * {@link #SCALE_MODE_FIT}
     * or whether the image will be scaled but leave white space
     * {@link #SCALE_MODE_FILL}.
     *
     * @param scaleMode {@link #SCALE_MODE_FIT} or
     *                  {@link #SCALE_MODE_FILL}
     */
    public void setScaleMode(@ScaleMode int scaleMode) {
        mScaleMode = scaleMode;
    }

    /**
     * Returns the scale mode with which the image will fill the paper.
     *
     * @return The scale Mode: {@link #SCALE_MODE_FIT} or
     * {@link #SCALE_MODE_FILL}
     */
    @ScaleMode
    public int getScaleMode() {
        return mScaleMode;
    }

    /**
     * Sets whether the image will be printed in color (default)
     * {@link #COLOR_MODE_COLOR} or in back and white
     * {@link #COLOR_MODE_MONOCHROME}.
     *
     * @param colorMode The color mode which is one of
     * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}.
     */
    public void setColorMode(@ColorMode int colorMode) {
        mColorMode = colorMode;
    }

    /**
     * Gets the color mode with which the image will be printed.
     *
     * @return The color mode which is one of {@link #COLOR_MODE_COLOR}
     * and {@link #COLOR_MODE_MONOCHROME}.
     */
    @ColorMode
    public int getColorMode() {
        return mColorMode;
    }

    /**
     * Sets whether the image will be printed in landscape {@link #ORIENTATION_LANDSCAPE} (default)
     * or portrait {@link #ORIENTATION_PORTRAIT}.
     *
     * @param orientation The page orientation which is one of
     *                    {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
     */
    public void setOrientation(int orientation) {
        mOrientation = orientation;
    }

    /**
     * Gets whether the image will be printed in landscape or portrait.
     *
     * @return The page orientation which is one of
     * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
     */
    public int getOrientation() {
        // Unset defaults to landscape but might turn image
        if (Build.VERSION.SDK_INT >= 19 && mOrientation == 0) {
            return ORIENTATION_LANDSCAPE;
        }
        return mOrientation;
    }


    /**
     * Prints a bitmap.
     *
     * @param jobName The print job name.
     * @param bitmap  The bitmap to print.
     */
    public void printBitmap(@NonNull String jobName, @NonNull Bitmap bitmap) {
        printBitmap(jobName, bitmap, null);
    }

    /**
     * Prints a bitmap.
     *
     * @param jobName The print job name.
     * @param bitmap  The bitmap to print.
     * @param callback Optional callback to observe when printing is finished.
     */
    public void printBitmap(@NonNull final String jobName, @NonNull final Bitmap bitmap,
            @Nullable final OnPrintFinishCallback callback) {
        if (Build.VERSION.SDK_INT < 19 || bitmap == null) {
            return;
        }

        PrintManager printManager =
                (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
        PrintAttributes.MediaSize mediaSize;
        if (isPortrait(bitmap)) {
            mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT;
        } else {
            mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE;
        }
        PrintAttributes attr = new PrintAttributes.Builder()
                .setMediaSize(mediaSize)
                .setColorMode(mColorMode)
                .build();

        printManager.print(jobName,
                new PrintBitmapAdapter(jobName, mScaleMode, bitmap, callback), attr);
    }

    @RequiresApi(19)
    private class PrintBitmapAdapter extends PrintDocumentAdapter {
        private final String mJobName;
        private final int mFittingMode;
        private final Bitmap mBitmap;
        private final OnPrintFinishCallback mCallback;
        private PrintAttributes mAttributes;

        PrintBitmapAdapter(String jobName, int fittingMode, Bitmap bitmap,
                OnPrintFinishCallback callback) {
            mJobName = jobName;
            mFittingMode = fittingMode;
            mBitmap = bitmap;
            mCallback = callback;
        }

        @Override
        public void onLayout(PrintAttributes oldPrintAttributes,
                PrintAttributes newPrintAttributes,
                CancellationSignal cancellationSignal,
                LayoutResultCallback layoutResultCallback,
                Bundle bundle) {

            mAttributes = newPrintAttributes;

            PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
                    .setPageCount(1)
                    .build();
            boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
            layoutResultCallback.onLayoutFinished(info, changed);
        }

        @Override
        public void onWrite(PageRange[] pageRanges,
                ParcelFileDescriptor fileDescriptor,
                CancellationSignal cancellationSignal,
                WriteResultCallback writeResultCallback) {
            writeBitmap(mAttributes, mFittingMode, mBitmap, fileDescriptor,
                    cancellationSignal, writeResultCallback);
        }

        @Override
        public void onFinish() {
            if (mCallback != null) {
                mCallback.onFinish();
            }
        }
    }

    /**
     * Prints an image located at the Uri. Image types supported are those of
     * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
     * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)}
     *
     * @param jobName   The print job name.
     * @param imageFile The <code>Uri</code> pointing to an image to print.
     * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
     */
    public void printBitmap(@NonNull String jobName, @NonNull Uri imageFile)
            throws FileNotFoundException {
        printBitmap(jobName, imageFile, null);
    }

    /**
     * Prints an image located at the Uri. Image types supported are those of
     * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
     * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)}
     *
     * @param jobName   The print job name.
     * @param imageFile The <code>Uri</code> pointing to an image to print.
     * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
     * @param callback Optional callback to observe when printing is finished.
     */
    public void printBitmap(@NonNull final String jobName, @NonNull final Uri imageFile,
            @Nullable final OnPrintFinishCallback callback)
            throws FileNotFoundException {
        if (Build.VERSION.SDK_INT < 19) {
            return;
        }

        PrintDocumentAdapter printDocumentAdapter = new PrintUriAdapter(jobName, imageFile,
                callback, mScaleMode);

        PrintManager printManager =
                (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
        PrintAttributes.Builder builder = new PrintAttributes.Builder();
        builder.setColorMode(mColorMode);

        if (mOrientation == ORIENTATION_LANDSCAPE || mOrientation == 0) {
            builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE);
        } else if (mOrientation == ORIENTATION_PORTRAIT) {
            builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT);
        }
        PrintAttributes attr = builder.build();

        printManager.print(jobName, printDocumentAdapter, attr);
    }

    @RequiresApi(19)
    private class PrintUriAdapter extends PrintDocumentAdapter {
        final String mJobName;
        final Uri mImageFile;
        final OnPrintFinishCallback mCallback;
        final int mFittingMode;
        PrintAttributes mAttributes;
        AsyncTask<Uri, Boolean, Bitmap> mLoadBitmap;
        Bitmap mBitmap;

        PrintUriAdapter(String jobName, Uri imageFile, OnPrintFinishCallback callback,
                int fittingMode) {
            mJobName = jobName;
            mImageFile = imageFile;
            mCallback = callback;
            mFittingMode = fittingMode;
            mBitmap = null;
        }

        @Override
        public void onLayout(final PrintAttributes oldPrintAttributes,
                final PrintAttributes newPrintAttributes,
                final CancellationSignal cancellationSignal,
                final LayoutResultCallback layoutResultCallback,
                Bundle bundle) {

            synchronized (this) {
                mAttributes = newPrintAttributes;
            }


            if (cancellationSignal.isCanceled()) {
                layoutResultCallback.onLayoutCancelled();
                return;
            }
            // we finished the load
            if (mBitmap != null) {
                PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
                        .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
                        .setPageCount(1)
                        .build();
                boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
                layoutResultCallback.onLayoutFinished(info, changed);
                return;
            }

            mLoadBitmap = new AsyncTask<Uri, Boolean, Bitmap>() {
                @Override
                protected void onPreExecute() {
                    // First register for cancellation requests.
                    cancellationSignal.setOnCancelListener(
                            new CancellationSignal.OnCancelListener() {
                                @Override
                                public void onCancel() { // on different thread
                                    cancelLoad();
                                    cancel(false);
                                }
                            });
                }

                @Override
                protected Bitmap doInBackground(Uri... uris) {
                    try {
                        return loadConstrainedBitmap(mImageFile);
                    } catch (FileNotFoundException e) {
                        /* ignore */
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Bitmap bitmap) {
                    super.onPostExecute(bitmap);

                    // If orientation was not set by the caller, try to fit the bitmap on
                    // the current paper by potentially rotating the bitmap by 90 degrees.
                    if (bitmap != null
                            && (!PRINT_ACTIVITY_RESPECTS_ORIENTATION || mOrientation == 0)) {
                        PrintAttributes.MediaSize mediaSize;

                        synchronized (this) {
                            mediaSize = mAttributes.getMediaSize();
                        }

                        if (mediaSize != null) {
                            if (mediaSize.isPortrait() != isPortrait(bitmap)) {
                                Matrix rotation = new Matrix();

                                rotation.postRotate(90);
                                bitmap = Bitmap.createBitmap(bitmap, 0, 0,
                                        bitmap.getWidth(), bitmap.getHeight(), rotation,
                                        true);
                            }
                        }
                    }

                    mBitmap = bitmap;
                    if (bitmap != null) {
                        PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
                                .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
                                .setPageCount(1)
                                .build();

                        boolean changed = !newPrintAttributes.equals(oldPrintAttributes);

                        layoutResultCallback.onLayoutFinished(info, changed);

                    } else {
                        layoutResultCallback.onLayoutFailed(null);
                    }
                    mLoadBitmap = null;
                }

                @Override
                protected void onCancelled(Bitmap result) {
                    // Task was cancelled, report that.
                    layoutResultCallback.onLayoutCancelled();
                    mLoadBitmap = null;
                }
            }.execute();
        }

        @SuppressWarnings("deprecation") // Call to requestCancelDecode()
        void cancelLoad() {
            synchronized (mLock) { // prevent race with set null below
                if (mDecodeOptions != null) {
                    if (Build.VERSION.SDK_INT < 24) {
                        // This only affects API 23 and older.
                        mDecodeOptions.requestCancelDecode();
                    }
                    mDecodeOptions = null;
                }
            }
        }

        @Override
        public void onFinish() {
            super.onFinish();
            cancelLoad();
            if (mLoadBitmap != null) {
                mLoadBitmap.cancel(true);
            }
            if (mCallback != null) {
                mCallback.onFinish();
            }
            if (mBitmap != null) {
                mBitmap.recycle();
                mBitmap = null;
            }
        }

        @Override
        public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
                CancellationSignal cancellationSignal,
                WriteResultCallback writeResultCallback) {
            writeBitmap(mAttributes, mFittingMode, mBitmap, fileDescriptor,
                    cancellationSignal, writeResultCallback);
        }
    }

    /**
     * Check if the supplied bitmap should best be printed on a portrait orientation paper.
     *
     * @param bitmap The bitmap to be printed.
     * @return true iff the picture should best be printed on a portrait orientation paper.
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static boolean isPortrait(Bitmap bitmap) {
        return bitmap.getWidth() <= bitmap.getHeight();
    }

    /**
     * Create a build with a copy from the other print attributes.
     *
     * @param other The other print attributes
     *
     * @return A builder that will build print attributes that match the other attributes
     */
    @RequiresApi(19)
    private static PrintAttributes.Builder copyAttributes(PrintAttributes other) {
        PrintAttributes.Builder b = (new PrintAttributes.Builder())
                .setMediaSize(other.getMediaSize())
                .setResolution(other.getResolution())
                .setMinMargins(other.getMinMargins());

        if (other.getColorMode() != 0) {
            b.setColorMode(other.getColorMode());
        }

        if (Build.VERSION.SDK_INT >= 23) {
            if (other.getDuplexMode() != 0) {
                b.setDuplexMode(other.getDuplexMode());
            }
        }

        return b;
    }

    /**
     * Calculates the transform the print an Image to fill the page
     *
     * @param imageWidth  with of bitmap
     * @param imageHeight height of bitmap
     * @param content     The output page dimensions
     * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs
     *                    {@link #SCALE_MODE_FIT}
     * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static Matrix getMatrix(int imageWidth, int imageHeight, RectF content,
            @ScaleMode int fittingMode) {
        Matrix matrix = new Matrix();

        // Compute and apply scale to fill the page.
        float scale = content.width() / imageWidth;
        if (fittingMode == SCALE_MODE_FILL) {
            scale = Math.max(scale, content.height() / imageHeight);
        } else {
            scale = Math.min(scale, content.height() / imageHeight);
        }
        matrix.postScale(scale, scale);

        // Center the content.
        final float translateX = (content.width()
                - imageWidth * scale) / 2;
        final float translateY = (content.height()
                - imageHeight * scale) / 2;
        matrix.postTranslate(translateX, translateY);
        return matrix;
    }

    /**
     * Write a bitmap for a PDF document.
     *
     * @param attributes          The print attributes
     * @param fittingMode         How to fit the bitmap
     * @param bitmap              The bitmap to write
     * @param fileDescriptor      The file to write to
     * @param cancellationSignal  Signal cancelling operation
     * @param writeResultCallback Callback to call once written
     */
    @RequiresApi(19)
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void writeBitmap(final PrintAttributes attributes, final int fittingMode,
            final Bitmap bitmap, final ParcelFileDescriptor fileDescriptor,
            final CancellationSignal cancellationSignal,
            final PrintDocumentAdapter.WriteResultCallback writeResultCallback) {
        final PrintAttributes pdfAttributes;
        if (IS_MIN_MARGINS_HANDLING_CORRECT) {
            pdfAttributes = attributes;
        } else {
            // If the handling of any margin != 0 is broken, strip the margins and add them to
            // the bitmap later
            pdfAttributes = copyAttributes(attributes)
                    .setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)).build();
        }

        (new AsyncTask<Void, Void, Throwable>() {
            @Override
            protected Throwable doInBackground(Void... params) {
                try {
                    if (cancellationSignal.isCanceled()) {
                        return null;
                    }

                    PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
                            pdfAttributes);

                    Bitmap maybeGrayscale = convertBitmapForColorMode(bitmap,
                            pdfAttributes.getColorMode());

                    if (cancellationSignal.isCanceled()) {
                        return null;
                    }

                    try {
                        PdfDocument.Page page = pdfDocument.startPage(1);

                        RectF contentRect;
                        if (IS_MIN_MARGINS_HANDLING_CORRECT) {
                            contentRect = new RectF(page.getInfo().getContentRect());
                        } else {
                            // Create dummy doc that has the margins to compute correctly sized
                            // content rectangle
                            PrintedPdfDocument dummyDocument = new PrintedPdfDocument(mContext,
                                    attributes);
                            PdfDocument.Page dummyPage = dummyDocument.startPage(1);
                            contentRect = new RectF(dummyPage.getInfo().getContentRect());
                            dummyDocument.finishPage(dummyPage);
                            dummyDocument.close();
                        }

                        // Resize bitmap
                        Matrix matrix = getMatrix(
                                maybeGrayscale.getWidth(), maybeGrayscale.getHeight(),
                                contentRect, fittingMode);

                        if (IS_MIN_MARGINS_HANDLING_CORRECT) {
                            // The pdfDocument takes care of the positioning and margins
                        } else {
                            // Move it to the correct position.
                            matrix.postTranslate(contentRect.left, contentRect.top);

                            // Cut off margins
                            page.getCanvas().clipRect(contentRect);
                        }

                        // Draw the bitmap.
                        page.getCanvas().drawBitmap(maybeGrayscale, matrix, null);

                        // Finish the page.
                        pdfDocument.finishPage(page);

                        if (cancellationSignal.isCanceled()) {
                            return null;
                        }

                        // Write the document.
                        pdfDocument.writeTo(
                                new FileOutputStream(fileDescriptor.getFileDescriptor()));
                        return null;
                    } finally {
                        pdfDocument.close();

                        if (fileDescriptor != null) {
                            try {
                                fileDescriptor.close();
                            } catch (IOException ioe) {
                                // ignore
                            }
                        }
                        // If we created a new instance for grayscaling, then recycle it here.
                        if (maybeGrayscale != bitmap) {
                            maybeGrayscale.recycle();
                        }
                    }
                } catch (Throwable t) {
                    return t;
                }
            }

            @Override
            protected void onPostExecute(Throwable throwable) {
                if (cancellationSignal.isCanceled()) {
                    // Cancelled.
                    writeResultCallback.onWriteCancelled();
                } else if (throwable == null) {
                    // Done.
                    writeResultCallback.onWriteFinished(
                            new PageRange[] { PageRange.ALL_PAGES });
                } else {
                    // Failed.
                    Log.e(LOG_TAG, "Error writing printed content", throwable);
                    writeResultCallback.onWriteFailed(null);
                }
            }
        }).execute();
    }

    /**
     * Loads a bitmap while limiting its size
     *
     * @param uri           location of a valid image
     * @return the Bitmap
     * @throws FileNotFoundException if the Uri does not point to an image
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    Bitmap loadConstrainedBitmap(Uri uri) throws FileNotFoundException {
        if (uri == null || mContext == null) {
            throw new IllegalArgumentException("bad argument to getScaledBitmap");
        }
        // Get width and height of stored bitmap
        BitmapFactory.Options opt = new BitmapFactory.Options();
        opt.inJustDecodeBounds = true;
        loadBitmap(uri, opt);

        int w = opt.outWidth;
        int h = opt.outHeight;

        // If bitmap cannot be decoded, return null
        if (w <= 0 || h <= 0) {
            return null;
        }

        // Find best downsampling size
        int imageSide = Math.max(w, h);

        int sampleSize = 1;
        while (imageSide > MAX_PRINT_SIZE) {
            imageSide >>>= 1;
            sampleSize <<= 1;
        }

        // Make sure sample size is reasonable
        if (sampleSize <= 0 || 0 >= (Math.min(w, h) / sampleSize)) {
            return null;
        }
        BitmapFactory.Options decodeOptions;
        synchronized (mLock) { // prevent race with set null below
            mDecodeOptions = new BitmapFactory.Options();
            mDecodeOptions.inMutable = true;
            mDecodeOptions.inSampleSize = sampleSize;
            decodeOptions = mDecodeOptions;
        }
        try {
            return loadBitmap(uri, decodeOptions);
        } finally {
            synchronized (mLock) {
                mDecodeOptions = null;
            }
        }
    }

    /**
     * Returns the bitmap from the given uri loaded using the given options.
     * Returns null on failure.
     */
    private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException {
        if (uri == null || mContext == null) {
            throw new IllegalArgumentException("bad argument to loadBitmap");
        }
        InputStream is = null;
        try {
            is = mContext.getContentResolver().openInputStream(uri);
            return BitmapFactory.decodeStream(is, null, o);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException t) {
                    Log.w(LOG_TAG, "close fail ", t);
                }
            }
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static Bitmap convertBitmapForColorMode(Bitmap original, @ColorMode int colorMode) {
        if (colorMode != COLOR_MODE_MONOCHROME) {
            return original;
        }
        // Create a grayscale bitmap
        Bitmap grayscale = Bitmap.createBitmap(original.getWidth(), original.getHeight(),
                Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(grayscale);
        Paint p = new Paint();
        ColorMatrix cm = new ColorMatrix();
        cm.setSaturation(0);
        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
        p.setColorFilter(f);
        c.drawBitmap(original, 0, 0, p);
        c.setBitmap(null);

        return grayscale;
    }
}