com.ultramegasoft.flavordex2.util.PhotoUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.ultramegasoft.flavordex2.util.PhotoUtils.java

Source

/*
 * The MIT License (MIT)
 * Copyright  2016 Steve Guidetti
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the Software?), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED AS IS?, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.ultramegasoft.flavordex2.util;

import android.content.ClipData;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.media.ExifInterface;
import android.support.v4.content.FileProvider;
import android.util.Log;

import com.ultramegasoft.flavordex2.BuildConfig;
import com.ultramegasoft.flavordex2.provider.Tables;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * Utilities for capturing and manipulating images.
 *
 * @author Steve Guidetti
 */
public class PhotoUtils {
    private static final String TAG = "PhotoUtils";

    /**
     * The name of the album to store photos taken with the camera
     */
    private static final String ALBUM_DIR = "Flavordex";

    /**
     * The prefix for photo file names
     */
    private static final String JPEG_FILE_PREFIX = "IMG_";

    /**
     * The extension to use for photo file names
     */
    private static final String JPEG_FILE_SUFFIX = ".jpg";

    /**
     * The prefix for cached thumbnails
     */
    private static final String THUMB_FILE_PREFIX = "thumb_";

    /**
     * The width and height of thumbnail Bitmaps
     */
    private static final int THUMB_SIZE = 40;

    /**
     * The shared memory cache for thumbnails
     */
    private static final BitmapCache sThumbCache = new BitmapCache();

    /**
     * Get an Intent to capture a photo.
     *
     * @return Image capture Intent
     */
    @Nullable
    public static Intent getTakePhotoIntent(@NonNull Context context) {
        try {
            final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider",
                    getOutputMediaFile());
            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                intent.setClipData(ClipData.newUri(context.getContentResolver(), null, uri));
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            } else {
                final List<ResolveInfo> activities = context.getPackageManager().queryIntentActivities(intent,
                        PackageManager.MATCH_DEFAULT_ONLY);
                for (ResolveInfo activity : activities) {
                    final String name = activity.activityInfo.packageName;
                    context.grantUriPermission(name, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                }
            }

            return intent;
        } catch (IOException e) {
            Log.e(TAG, "Failed to create new file", e);
        }
        return null;
    }

    /**
     * Get an Intent to select a photo from the gallery.
     *
     * @return Get content Intent
     */
    @NonNull
    public static Intent getSelectPhotoIntent() {
        final Intent intent;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
        } else {
            intent = new Intent(Intent.ACTION_GET_CONTENT);
        }
        intent.setType("image/*");
        return intent;
    }

    /**
     * Get the memory cache for storing thumbnails.
     *
     * @return The thumbnail cache
     */
    @NonNull
    public static BitmapCache getThumbCache() {
        return sThumbCache;
    }

    /**
     * Calculate the sample size for an image being loaded.
     *
     * @param options   Options object containing the original dimensions
     * @param reqWidth  The requested width of the decoded Bitmap
     * @param reqHeight The requested height of the decoded Bitmap
     * @return The sample size
     */
    private static int calculateInSampleSize(@NonNull Options options, int reqWidth, int reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight || (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

    /**
     * Rotate an image according to its EXIF data.
     *
     * @param context The Context
     * @param uri     The Uri to the image file
     * @param bitmap  The Bitmap to rotate
     * @return The rotated Bitmap
     */
    @NonNull
    private static Bitmap rotatePhoto(@NonNull Context context, @NonNull Uri uri, @NonNull Bitmap bitmap) {
        uri = getImageUri(context, uri);
        int rotation = 0;

        if ("file".equals(uri.getScheme())) {
            try {
                final ExifInterface exif = new ExifInterface(uri.getPath());
                switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    rotation = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    rotation = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    rotation = 270;
                }
            } catch (IOException e) {
                Log.e(TAG, "Failed to read EXIF data for " + uri.toString(), e);
            }
        } else {
            final ContentResolver cr = context.getContentResolver();
            final String[] projection = new String[] { MediaStore.Images.ImageColumns.ORIENTATION };
            final Cursor cursor = cr.query(uri, projection, null, null, null);
            if (cursor != null) {
                try {
                    if (cursor.moveToFirst() && cursor.getColumnCount() > 0) {
                        rotation = cursor.getInt(0);
                    }
                } finally {
                    cursor.close();
                }
            }
        }

        if (rotation != 0) {
            final Matrix matrix = new Matrix();
            matrix.postRotate(rotation);
            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        }

        return bitmap;
    }

    /**
     * Get an image media or file Uri based on its document Uri.
     *
     * @param context The Context
     * @param uri     The Uri to convert
     * @return The image or file Uri or the original Uri if it could not be converted
     */
    @NonNull
    private static Uri getImageUri(@NonNull Context context, @NonNull Uri uri) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] parts = docId.split(":");
            if ("image".equals(parts[0])) {
                return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, parts[1]);
            } else if ("com.android.externalstorage.documents".equals(uri.getAuthority())) {
                return Uri.fromFile(new File(Environment.getExternalStorageDirectory(), parts[1]));
            }
        }
        return uri;
    }

    /**
     * Load a Bitmap from an image file.
     *
     * @param context   The Context
     * @param uri       The Uri to the image file
     * @param reqWidth  The requested width of the decoded Bitmap
     * @param reqHeight The requested height of the decoded Bitmap
     * @return A Bitmap
     */
    @Nullable
    public static Bitmap loadBitmap(@NonNull Context context, @NonNull Uri uri, int reqWidth, int reqHeight) {
        final ContentResolver cr = context.getContentResolver();
        try {
            final ParcelFileDescriptor parcelFileDescriptor = cr.openFileDescriptor(uri, "r");
            if (parcelFileDescriptor == null) {
                Log.w(TAG, "Unable to open " + uri.toString());
                return null;
            }
            try {
                final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
                final Options opts = new BitmapFactory.Options();
                opts.inJustDecodeBounds = true;
                BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts);

                opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight);
                opts.inJustDecodeBounds = false;

                final Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, opts);
                if ("image/jpeg".equals(opts.outMimeType)) {
                    return rotatePhoto(context, uri, bitmap);
                }
                return bitmap;
            } catch (OutOfMemoryError e) {
                Log.e(TAG, "Out of memory", e);
            } finally {
                parcelFileDescriptor.close();
            }
        } catch (FileNotFoundException e) {
            Log.w(TAG, "File not found: " + uri.toString());
        } catch (SecurityException e) {
            Log.w(TAG, "Permission denied for Uri: " + uri.toString());
        } catch (IOException e) {
            Log.e(TAG, "Failed to load bitmap", e);
        }
        return null;
    }

    /**
     * Generate a thumbnail image file and save it to the persistent cache. If no photos exist for
     * the entry, te current file is deleted.
     *
     * @param context The Context
     * @param id      Te ID for the entry
     */
    private static void generateThumb(@NonNull Context context, long id) {
        if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            return;
        }

        final ContentResolver cr = context.getContentResolver();
        final Uri uri = Uri.withAppendedPath(Tables.Entries.CONTENT_ID_URI_BASE, id + "/photos");
        final String where = Tables.Photos.PATH + " NOT NULL";
        final Cursor cursor = cr.query(uri, new String[] { Tables.Photos.PATH }, where, null,
                Tables.Photos.POS + " ASC");
        if (cursor != null) {
            try {
                if (cursor.moveToFirst()) {
                    generateThumb(context, parsePath(cursor.getString(0)), id);
                } else {
                    generateThumb(context, null, id);
                }
            } finally {
                cursor.close();
            }
        }
    }

    /**
     * Load a Bitmap as a thumbnail and save it to the persistent cache.
     *
     * @param context The Context
     * @param uri     The Uri to the original image
     * @param id      The ID of the entry the image belongs to
     */
    private static void generateThumb(@NonNull Context context, @Nullable Uri uri, long id) {
        try {
            if (uri != null) {
                final Bitmap inputBitmap = loadBitmap(context, uri, THUMB_SIZE, THUMB_SIZE);

                if (inputBitmap != null) {
                    final FileOutputStream os = new FileOutputStream(getThumbFile(context, id));
                    inputBitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
                    os.close();

                    sThumbCache.remove(id);
                    inputBitmap.recycle();
                }
            }

            //noinspection ResultOfMethodCallIgnored
            getThumbFile(context, id).createNewFile();
        } catch (IOException e) {
            Log.e(TAG, "Error writing thumbnail bitmap", e);
        }
    }

    /**
     * Get the thumbnail for an entry, generating one as needed.
     *
     * @param context The Context
     * @param id      The entry ID
     * @return A Bitmap
     */
    @Nullable
    public static Bitmap getThumb(@NonNull Context context, long id) {
        final File file = getThumbFile(context, id);

        if (!file.exists()) {
            if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                    && PermissionUtils.hasExternalStoragePerm(context)) {
                generateThumb(context, id);
            } else {
                return null;
            }
        }

        if (file.length() == 0) {
            return null;
        }

        try {
            return BitmapFactory.decodeFile(file.getPath(), null);
        } catch (OutOfMemoryError e) {
            Log.e(TAG, "Out of memory", e);
        }
        return null;
    }

    /**
     * Delete a thumbnail.
     *
     * @param context The Context
     * @param id      The entry ID
     */
    public static void deleteThumb(@NonNull Context context, long id) {
        final File file = getThumbFile(context, id);
        if (file.exists()) {
            sThumbCache.remove(id);
            //noinspection ResultOfMethodCallIgnored
            file.delete();
            final ContentResolver cr = context.getContentResolver();
            final Uri uri = ContentUris.withAppendedId(Tables.Entries.CONTENT_ID_URI_BASE, id);
            cr.notifyChange(uri, null);
        }
    }

    /**
     * Get a Bitmap file for an entry.
     *
     * @param context The Context
     * @param id      The entry ID
     * @return A reference to the image file
     */
    @NonNull
    private static File getThumbFile(@NonNull Context context, long id) {
        final String fileName = THUMB_FILE_PREFIX + id + JPEG_FILE_SUFFIX;
        return new File(context.getCacheDir(), fileName);
    }

    /**
     * Get the output file for a new captured image.
     *
     * @return A File object pointing to the file
     */
    @NonNull
    private static File getOutputMediaFile() throws IOException {
        if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            throw new IOException("Media storage not mounted");
        }

        final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
        return new File(getMediaStorageDir(), JPEG_FILE_PREFIX + timeStamp + JPEG_FILE_SUFFIX);
    }

    /**
     * Get the directory where captured images are stored.
     *
     * @return The media storage directory
     */
    @NonNull
    private static File getMediaStorageDir() throws IOException {
        final File mediaStorageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
        final File albumDir = new File(mediaStorageDir, ALBUM_DIR);

        if (!albumDir.exists()) {
            if (!albumDir.mkdirs()) {
                throw new IOException("Failure creating directories");
            }
        }

        return albumDir;
    }

    /**
     * Get the file Uri for the given Uri.
     *
     * @param cr  The ContentResolver
     * @param uri The original Uri
     * @return The file Uri
     */
    @Nullable
    public static Uri getFileUri(@NonNull ContentResolver cr, @NonNull Uri uri) {
        final String name = getName(cr, uri);
        if (name != null) {
            final File file;
            try {
                file = new File(getMediaStorageDir(), name);
                if (file.exists()) {
                    return Uri.fromFile(file);
                }
                return savePhotoFromUri(cr, uri, file);
            } catch (IOException e) {
                Log.w(TAG, "Unable to check for existing file", e);
            }
        }
        return savePhotoFromUri(cr, uri, null);
    }

    /**
     * Save a photo from a content Uri to the external storage.
     *
     * @param cr   The ContentResolver
     * @param uri  The content Uri
     * @param file The File to write to or null to create one
     * @return The file Uri for the new file
     */
    @Nullable
    private static Uri savePhotoFromUri(@NonNull ContentResolver cr, @NonNull Uri uri, @Nullable File file) {
        try {
            if (file == null) {
                file = getOutputMediaFile();
            }
            final InputStream inputStream = cr.openInputStream(uri);
            if (inputStream != null) {
                final OutputStream outputStream = new FileOutputStream(file);
                try {
                    final byte[] buffer = new byte[8192];
                    int read;
                    while ((read = inputStream.read(buffer)) > 0) {
                        outputStream.write(buffer, 0, read);
                    }
                    return Uri.fromFile(file);
                } finally {
                    inputStream.close();
                    outputStream.close();
                }
            }
        } catch (FileNotFoundException e) {
            Log.w(TAG, "File not found for Uri: " + uri.getPath());
        } catch (IOException e) {
            Log.e(TAG, "Failed to save the photo", e);
        }
        return null;
    }

    /**
     * Save a photo from a stream, checking for duplicates.
     *
     * @param inputStream The source stream
     * @param fileName    The output file name
     * @return The saved file
     */
    @NonNull
    public static File savePhotoFromStream(@NonNull InputStream inputStream, @NonNull String fileName)
            throws IOException {
        final File tempFile = File.createTempFile("import_", fileName);
        FileUtils.dumpStream(inputStream, tempFile);

        final File directory = getMediaStorageDir();
        final String name = fileName.substring(0, fileName.lastIndexOf('.'));
        final String extension = fileName.substring(fileName.lastIndexOf('.'));
        File outputFile = new File(directory, fileName);
        if (outputFile.exists()) {
            int i = 2;
            final String sourceHash = getMD5Hash(tempFile);
            if (sourceHash != null) {
                while (outputFile.exists()) {
                    final String targetHash = getMD5Hash(outputFile);
                    if (targetHash != null && sourceHash.equals(targetHash)) {
                        break;
                    }

                    outputFile = new File(directory, name + " (" + i++ + ")" + extension);
                }
            }
        }

        FileUtils.dumpStream(new FileInputStream(tempFile), outputFile);
        //noinspection ResultOfMethodCallIgnored
        tempFile.delete();

        return outputFile;
    }

    /**
     * Get the file name from a content Uri.
     *
     * @param cr  The ContentResolver
     * @param uri The Uri
     * @return The file name
     */
    @Nullable
    private static String getName(@NonNull ContentResolver cr, @NonNull Uri uri) {
        if ("file".equals(uri.getScheme())) {
            return uri.getLastPathSegment();
        } else {
            final String[] projection = new String[] { MediaStore.MediaColumns.DISPLAY_NAME };
            final Cursor cursor = cr.query(uri, projection, null, null, null);
            if (cursor != null) {
                try {
                    if (cursor.moveToFirst()) {
                        return cursor.getString(0);
                    }
                } finally {
                    cursor.close();
                }
            }
        }
        return null;
    }

    /**
     * Get the MD5 hash of a file as a 32 character hex string.
     *
     * @param file The input file
     * @return The MD5 hash of the file or null on failure
     */
    @Nullable
    private static String getMD5Hash(@NonNull File file) {
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(file);
            return getMD5Hash(inputStream);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Failed to generate MD5 hash", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
        }

        return null;
    }

    /**
     * Get the MD5 hash of a file as a 32 character hex string.
     *
     * @param inputStream The input stream
     * @return The MD5 hash of the file or null on failure
     */
    @Nullable
    private static String getMD5Hash(@NonNull InputStream inputStream) {
        try {
            final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            final byte[] buffer = new byte[8192];
            int read;
            while ((read = inputStream.read(buffer)) > 0) {
                messageDigest.update(buffer, 0, read);
            }
            final BigInteger digest = new BigInteger(1, messageDigest.digest());
            return String.format("%32s", digest.toString(16)).replace(" ", "0");
        } catch (NoSuchAlgorithmException | IOException e) {
            Log.e(TAG, "Failed to generate MD5 hash", e);
        }

        return null;
    }

    /**
     * Get the MD5 hash of a file as a 32 character hex string.
     *
     * @param cr  The ContentResolver
     * @param uri The Uri representing the file
     * @return The MD5 hash of the file or null on failure
     */
    @Nullable
    public static String getMD5Hash(@NonNull ContentResolver cr, @NonNull Uri uri) {
        try {
            final InputStream inputStream = cr.openInputStream(uri);
            if (inputStream == null) {
                Log.w(TAG, "Unable to open stream from " + uri.toString());
                return null;
            }
            try {
                return getMD5Hash(inputStream);
            } finally {
                inputStream.close();
            }
        } catch (FileNotFoundException e) {
            Log.i(TAG, e.getMessage());
        } catch (IOException e) {
            Log.e(TAG, "Failed to generate MD5 hash", e);
        }

        return null;
    }

    /**
     * Parse a string into a Uri.
     *
     * @param path The string to parse
     * @return The Uri
     */
    @Nullable
    public static Uri parsePath(@NonNull String path) {
        if (path.charAt(0) == '/') {
            return Uri.fromFile(new File(path));
        } else if (path.startsWith("file://") || path.startsWith("content://")) {
            return Uri.parse(path);
        }
        try {
            return Uri.fromFile(new File(getMediaStorageDir(), path));
        } catch (IOException e) {
            Log.e(TAG, "Failed to parse path: " + path, e);
        }
        return null;
    }

    /**
     * Get the simplest representation of a photo path from a Uri.
     *
     * @param uri The photo Uri
     * @return The file name or full Uri string
     */
    @NonNull
    static String getPathString(@NonNull Uri uri) {
        if ("file".equals(uri.getScheme())) {
            final File file = new File(uri.getPath());
            try {
                if (file.getParentFile().equals(getMediaStorageDir())) {
                    return file.getName();
                }
            } catch (IOException e) {
                Log.w(TAG, "Unable to check file location", e);
            }
        }
        return uri.toString();
    }
}