package co.smartreceipts.android.imports; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.media.ExifInterface; import android.support.v4.content.ContextCompat; import com.google.common.base.Preconditions; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import co.smartreceipts.android.model.Trip; import co.smartreceipts.android.settings.UserPreferenceManager; import co.smartreceipts.android.settings.catalog.UserPreference; import co.smartreceipts.android.utils.UriUtils; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Single; import wb.android.image.ImageUtils; import wb.android.storage.StorageManager; public class ImageImportProcessor implements FileImportProcessor { private static final int MAX_DIMENSION = 1024; private static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"; private final Trip mTrip; private final StorageManager mStorageManner; private final UserPreferenceManager mPreferences; private final Context mContext; private final ContentResolver mContentResolver; public ImageImportProcessor(@NonNull Trip trip, @NonNull StorageManager storageManager, @NonNull UserPreferenceManager preferences, @NonNull Context context) { this(trip, storageManager, preferences, context, context.getContentResolver()); } public ImageImportProcessor(@NonNull Trip trip, @NonNull StorageManager storageManager, @NonNull UserPreferenceManager preferences, @NonNull Context context, @NonNull ContentResolver contentResolver) { mTrip = Preconditions.checkNotNull(trip); mStorageManner = Preconditions.checkNotNull(storageManager); mPreferences = Preconditions.checkNotNull(preferences); mContext = Preconditions.checkNotNull(context.getApplicationContext()); mContentResolver = Preconditions.checkNotNull(contentResolver); } @NonNull @Override public Single<File> process(@NonNull final Uri uri) { return Single.create(emitter -> { InputStream inputStream = null; try { inputStream = mContentResolver.openInputStream(uri); if (inputStream != null) { final int scale = getImageScaleFactor(uri); // Get scaled bitmap final BitmapFactory.Options smallerOpts = new BitmapFactory.Options(); smallerOpts.inSampleSize = scale; Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, smallerOpts); // Perform image processing if (mPreferences.get(UserPreference.Camera.SaveImagesInGrayScale)) { bitmap = ImageUtils.convertToGrayScale(bitmap); } if (mPreferences.get(UserPreference.Camera.AutomaticallyRotateImages)) { Logger.debug(ImageImportProcessor.this, "Configured for auto-rotation. Attempting to determine the orientation"); int orientation = getOrientationFromMediaStore(uri); if (orientation == ExifInterface.ORIENTATION_UNDEFINED) { Logger.warn(ImageImportProcessor.this, "Failed to fetch orientation information from the content store. Trying from Exif."); InputStream exifInputStream = null; // Note: Re-open to avoid issues with #reset() try { exifInputStream = mContentResolver.openInputStream(uri); if (exifInputStream != null) { final ExifInterface exif = new ExifInterface(exifInputStream); orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); Logger.info(ImageImportProcessor.this, "Read exif orientation as {}", orientation); } } catch (IOException e) { Logger.error(ImageImportProcessor.this, "An Exif parsing exception occurred", e); } finally { StorageManager.closeQuietly(exifInputStream); } } if (orientation != ExifInterface.ORIENTATION_UNDEFINED) { Logger.info(ImageImportProcessor.this, "Image orientation determined as {}. Rotating...", orientation); bitmap = ImageUtils.rotateBitmap(bitmap, orientation); } else { Logger.warn(ImageImportProcessor.this, "Indeterminate orientation. Skipping rotation"); } } else { Logger.info(ImageImportProcessor.this, "Image import rotation is disabled. Ignoring..."); } final File destination = mStorageManner.getFile(mTrip.getDirectory(), System.currentTimeMillis() + "." + UriUtils.getExtension(uri, mContentResolver)); if (!mStorageManner.writeBitmap(Uri.fromFile(destination), bitmap, Bitmap.CompressFormat.JPEG, 85)) { Logger.error(ImageImportProcessor.this, "Failed to write the image data. Aborting"); emitter.onError(new IOException("Failed to write the image data. Aborting")); } else { Logger.info(ImageImportProcessor.this, "Successfully saved the image to {}.", destination); emitter.onSuccess(destination); } } else { emitter.onError(new FileNotFoundException()); } } catch (IOException e) { emitter.onError(e); } finally { StorageManager.closeQuietly(inputStream); } }); } private int getImageScaleFactor(@NonNull Uri uri) { InputStream inputStream = null; try { inputStream = mContentResolver.openInputStream(uri); if (inputStream != null) { final BitmapFactory.Options inJustDecodeBoundsOptions = new BitmapFactory.Options(); inJustDecodeBoundsOptions.inJustDecodeBounds = true; // Decode data to our option bounds but don't read the full image BitmapFactory.decodeStream(inputStream, null, inJustDecodeBoundsOptions); int fullWidth = inJustDecodeBoundsOptions.outWidth; int fullHeight = inJustDecodeBoundsOptions.outHeight; int scale = 1; while (fullWidth > MAX_DIMENSION && fullHeight > MAX_DIMENSION) { fullWidth >>>= 1; fullHeight >>>= 1; scale <<= 1; } return scale; } } catch (IOException e) { Logger.warn(this, "Failed to process image scale", e); } finally { StorageManager.closeQuietly(inputStream); } return 1; } private int getOrientationFromMediaStore(@NonNull Uri externalUri) { final boolean hasStoragePermission = ContextCompat.checkSelfPermission(mContext, READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; if (!hasStoragePermission) { Logger.warn(ImageImportProcessor.this, "The user has not provided storage permissions. Unabled to determine the rotation from the content provider."); return ExifInterface.ORIENTATION_UNDEFINED; } Cursor cursor = null; try { final String[] imageColumns = { MediaStore.Images.Media.DATA, MediaStore.Images.Media.ORIENTATION }; cursor = mContentResolver.query(externalUri, imageColumns, null, null, null); if(cursor != null && cursor.moveToFirst() && cursor.getColumnCount() > 0){ return cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION)); } else { Logger.warn(this, "Failed to find the URI to determine the orientation"); return ExifInterface.ORIENTATION_UNDEFINED; } } catch (Exception e) { Logger.error(this, "An exception occurred when fetching the content orientation", e); return ExifInterface.ORIENTATION_UNDEFINED; } finally { if (cursor != null) { cursor.close(); } } } }