diff --git a/example/build.gradle b/example/build.gradle index 33f163f5..174af8cb 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -4,11 +4,11 @@ archivesBaseName = 'android-crop-example' android { compileSdkVersion 23 - buildToolsVersion '23.0.1' + buildToolsVersion '23.0.2' defaultConfig { - minSdkVersion 10 - targetSdkVersion 22 + minSdkVersion 19 + targetSdkVersion 23 versionCode Integer.parseInt(project.VERSION_CODE) versionName project.VERSION } diff --git a/example/src/main/java/com/soundcloud/android/crop/example/MainActivity.java b/example/src/main/java/com/soundcloud/android/crop/example/MainActivity.java index 079775aa..f75d22b7 100644 --- a/example/src/main/java/com/soundcloud/android/crop/example/MainActivity.java +++ b/example/src/main/java/com/soundcloud/android/crop/example/MainActivity.java @@ -1,27 +1,34 @@ package com.soundcloud.android.crop.example; -import com.soundcloud.android.crop.Crop; - import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; import android.view.Menu; import android.view.MenuItem; import android.widget.ImageView; import android.widget.Toast; +import com.soundcloud.android.crop.Crop; + import java.io.File; public class MainActivity extends Activity { private ImageView resultView; + private int REQUEST_PHOTO = 1; + private File PHOTO_CAPTURED, PHOTO_CROPPED; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); resultView = (ImageView) findViewById(R.id.result_image); + + PHOTO_CAPTURED = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "PhotoCaptured.jpg"); + PHOTO_CROPPED = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "PhotoCropped.jpg"); } @Override @@ -36,22 +43,36 @@ public boolean onOptionsItemSelected(MenuItem item) { resultView.setImageDrawable(null); Crop.pickImage(this); return true; + } else if (item.getItemId() == R.id.action_camera) { + resultView.setImageDrawable(null); + takePhoto(); + return true; } return super.onOptionsItemSelected(item); } + private void takePhoto() { + final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(PHOTO_CAPTURED)); + startActivityForResult(intent, REQUEST_PHOTO); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent result) { if (requestCode == Crop.REQUEST_PICK && resultCode == RESULT_OK) { beginCrop(result.getData()); + } else if (requestCode == REQUEST_PHOTO && resultCode == RESULT_OK) { + beginCrop(Uri.fromFile(PHOTO_CAPTURED)); } else if (requestCode == Crop.REQUEST_CROP) { handleCrop(resultCode, result); } } private void beginCrop(Uri source) { - Uri destination = Uri.fromFile(new File(getCacheDir(), "cropped")); - Crop.of(source, destination).asSquare().start(this); + Crop.of(source, Uri.fromFile(PHOTO_CROPPED)) + .withMaxSize(1600,1600) + .withJpgQuality(90) + .start(this); } private void handleCrop(int resultCode, Intent result) { @@ -61,5 +82,4 @@ private void handleCrop(int resultCode, Intent result) { Toast.makeText(this, Crop.getError(result).getMessage(), Toast.LENGTH_SHORT).show(); } } - } diff --git a/example/src/main/res/menu/activity_main.xml b/example/src/main/res/menu/activity_main.xml index ede18fd7..77854524 100644 --- a/example/src/main/res/menu/activity_main.xml +++ b/example/src/main/res/menu/activity_main.xml @@ -6,4 +6,7 @@ android:title="@string/action_select" android:showAsAction="always" /> + \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index 20e1a767..f05475db 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -7,11 +7,11 @@ archivesBaseName = 'android-crop' android { compileSdkVersion 23 - buildToolsVersion '23.0.1' + buildToolsVersion '23.0.2' defaultConfig { - minSdkVersion 10 - targetSdkVersion 22 + minSdkVersion 19 + targetSdkVersion 23 testApplicationId 'com.soundcloud.android.crop.test' testInstrumentationRunner 'android.test.InstrumentationTestRunner' diff --git a/lib/src/main/java/com/soundcloud/android/crop/Crop.java b/lib/src/main/java/com/soundcloud/android/crop/Crop.java index 564f5b52..3bf46d74 100644 --- a/lib/src/main/java/com/soundcloud/android/crop/Crop.java +++ b/lib/src/main/java/com/soundcloud/android/crop/Crop.java @@ -25,6 +25,7 @@ interface Extra { String ASPECT_Y = "aspect_y"; String MAX_X = "max_x"; String MAX_Y = "max_y"; + String JPG_QUALITY = "jpg_quality"; String ERROR = "error"; } @@ -79,6 +80,18 @@ public Crop withMaxSize(int width, int height) { return this; } + /** + * Set JPG quality + * + * @param quality JPG Quality (10< q <100) + */ + public Crop withJpgQuality(int quality) { + if ( (quality>=10) || (quality<=100) ) { + cropIntent.putExtra(Extra.JPG_QUALITY, quality); + } + return this; + } + /** * Send the crop Intent from an Activity * diff --git a/lib/src/main/java/com/soundcloud/android/crop/CropImageActivity.java b/lib/src/main/java/com/soundcloud/android/crop/CropImageActivity.java index 97a0466a..30d57a48 100644 --- a/lib/src/main/java/com/soundcloud/android/crop/CropImageActivity.java +++ b/lib/src/main/java/com/soundcloud/android/crop/CropImageActivity.java @@ -295,23 +295,22 @@ private void onSaveClicked() { } if (croppedImage != null) { - imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true); - imageView.center(); + //imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true); + //imageView.center(); imageView.highlightViews.clear(); } saveImage(croppedImage); } - private void saveImage(Bitmap croppedImage) { - if (croppedImage != null) { - final Bitmap b = croppedImage; + private void saveImage(final Bitmap b) { + if (b != null) { CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving), new Runnable() { public void run() { saveOutput(b); + b.recycle(); } - }, handler - ); + }, handler); } else { finish(); } @@ -337,6 +336,14 @@ private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) { RectF adjusted = new RectF(); matrix.mapRect(adjusted, new RectF(rect)); + //if the cutting box are rectangle( outWidth != outHeight ),and the exifRotation is 90 or 270, + //the outWidth and outHeight should be interchanged + if (exifRotation==90 || exifRotation==270) { + int temp=outWidth; + outWidth=outHeight; + outHeight=temp; + } + // Adjust to account for origin at 0,0 adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0); rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom); @@ -344,11 +351,14 @@ private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) { try { croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options()); - if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) { - Matrix matrix = new Matrix(); + Matrix matrix = new Matrix(); + if (rect.width() > outWidth || rect.height() > outHeight) { matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height()); - croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true); } + //If the picture's exifRotation !=0 ,they should be rotated to 0 degrees + //If the picture need not to be scale, they also need to be rotate to 0 degrees + matrix.postRotate(exifRotation); + croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true); } catch (IllegalArgumentException e) { // Rethrow with some extra information throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image (" @@ -376,33 +386,35 @@ private void clearImageView() { } private void saveOutput(Bitmap croppedImage) { - if (saveUri != null) { - OutputStream outputStream = null; - try { - outputStream = getContentResolver().openOutputStream(saveUri); + int jpgQuality = 90; + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + if (extras != null) { + if (extras.containsKey(Crop.Extra.JPG_QUALITY)) + jpgQuality = extras.getInt(Crop.Extra.JPG_QUALITY); + } + + OutputStream outputStream = null; + try { + if (saveUri != null) { + outputStream = this.getContentResolver().openOutputStream(saveUri); if (outputStream != null) { - croppedImage.compress(Bitmap.CompressFormat.JPEG, 90, outputStream); + croppedImage.compress(Bitmap.CompressFormat.JPEG, jpgQuality, outputStream); } - } catch (IOException e) { - setResultException(e); - Log.e("Cannot open file: " + saveUri, e); - } finally { - CropUtil.closeSilently(outputStream); + croppedImage.recycle(); } - CropUtil.copyExifRotation( - CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri), - CropUtil.getFromMediaUri(this, getContentResolver(), saveUri) - ); - setResultUri(saveUri); + } catch(IOException e) { + setResultException(e); + Log.e("Error saving file: " + saveUri, e); + } finally { + CropUtil.closeSilently(outputStream); } - final Bitmap b = croppedImage; handler.post(new Runnable() { public void run() { imageView.clear(); - b.recycle(); } }); @@ -433,5 +445,4 @@ private void setResultUri(Uri uri) { private void setResultException(Throwable throwable) { setResult(Crop.RESULT_ERROR, new Intent().putExtra(Crop.Extra.ERROR, throwable)); } - } diff --git a/lib/src/main/java/com/soundcloud/android/crop/CropUtil.java b/lib/src/main/java/com/soundcloud/android/crop/CropUtil.java index 04e83224..8e4dbf28 100644 --- a/lib/src/main/java/com/soundcloud/android/crop/CropUtil.java +++ b/lib/src/main/java/com/soundcloud/android/crop/CropUtil.java @@ -18,21 +18,20 @@ import android.app.ProgressDialog; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.media.ExifInterface; import android.net.Uri; +import android.os.Build; +import android.os.Environment; import android.os.Handler; -import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; import android.provider.MediaStore; import android.support.annotation.Nullable; -import android.text.TextUtils; import java.io.Closeable; import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; /* @@ -73,90 +72,174 @@ public static int getExifRotation(File imageFile) { } } - public static boolean copyExifRotation(File sourceFile, File destFile) { - if (sourceFile == null || destFile == null) return false; - try { - ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath()); - ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath()); - exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION)); - exifDest.saveAttributes(); - return true; - } catch (IOException e) { - Log.e("Error copying Exif data", e); - return false; + @Nullable + public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) { + if (uri != null) { + String path = getPath(context, uri); + if (path != null && isLocal(path)) { + return new File(path); + } } + return null; } - @Nullable - public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) { - if (uri == null) return null; + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders.
+ *
+ * Callers should check whether the path is local before assuming it + * represents a local file. + * + * @param context The context. + * @param uri The Uri to query. + * @author paulburke + */ + public static String getPath(final Context context, final Uri uri) { - if (SCHEME_FILE.equals(uri.getScheme())) { - return new File(uri.getPath()); - } else if (SCHEME_CONTENT.equals(uri.getScheme())) { - final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME }; - Cursor cursor = null; - try { - cursor = resolver.query(uri, filePathColumn, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ? - cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) : - cursor.getColumnIndex(MediaStore.MediaColumns.DATA); - // Picasa images on API 13+ - if (columnIndex != -1) { - String filePath = cursor.getString(columnIndex); - if (!TextUtils.isEmpty(filePath)) { - return new File(filePath); - } - } + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; } - } catch (IllegalArgumentException e) { - // Google Drive images - return getFromMediaUriPfd(context, resolver, uri); - } catch (SecurityException ignored) { - // Nothing we can do - } finally { - if (cursor != null) cursor.close(); + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); } } - return null; - } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { - private static String getTempFilename(Context context) throws IOException { - File outputDir = context.getCacheDir(); - File outputFile = File.createTempFile("image", "tmp", outputDir); - return outputFile.getAbsolutePath(); - } + // Return the remote address + if (isGooglePhotosUri(uri) || isGoogleDriveUri(uri)) + return uri.getLastPathSegment(); - @Nullable - private static File getFromMediaUriPfd(Context context, ContentResolver resolver, Uri uri) { - if (uri == null) return null; + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } - FileInputStream input = null; - FileOutputStream output = null; - try { - ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); - FileDescriptor fd = pfd.getFileDescriptor(); - input = new FileInputStream(fd); + return null; + } - String tempFilename = getTempFilename(context); - output = new FileOutputStream(tempFilename); + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { column }; - int read; - byte[] bytes = new byte[4096]; - while ((read = input.read(bytes)) != -1) { - output.write(bytes, 0, read); + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); } - return new File(tempFilename); - } catch (IOException ignored) { - // Nothing we can do } finally { - closeSilently(input); - closeSilently(output); + if (cursor != null) + cursor.close(); } return null; } + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Drive. + */ + private static boolean isGoogleDriveUri(Uri uri) { + return "com.google.android.apps.docs.storage".equals(uri.getAuthority()); + } + + /** + * @return Whether the URI is a local one. + */ + private static boolean isLocal(String url) { + return (url != null && !url.startsWith("http://") && !url.startsWith("https://")); + } + public static void startBackgroundJob(MonitoredActivity activity, String title, String message, Runnable job, Handler handler) { // Make the progress dialog uncancelable, so that we can guarantee @@ -214,5 +297,4 @@ public void onActivityStarted(MonitoredActivity activity) { dialog.show(); } } - }