diff --git a/README.md b/README.md
index 76faef4..34706f9 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,18 @@ EasyImage allows you to easily capture images and videos from the gallery, camer
# Setup
## Runtime permissions
-This library requires specific runtime permissions. Declare it in your `AndroidManifest.xml`:
+No additional permisions are required if you DO NOT `use setCopyImagesToPublicGalleryFolder()` setting. But if you do:
+
+### For devices running Android 10 and newer:
+Nothing is required
+
+### For devices running Android 9 or lower:
+Permission need to be specified in Manifest:
```xml
-
```
-**Please note**: for devices running API 23 (marshmallow) you have to request this permissions in the runtime, before calling `EasyImage.openCamera()`. It's demonstrated in the sample app.
+Also you'll need to ask for this permission in the runtime in the moment of your choice. Sample app does that.
**There is also one issue about runtime permissions**. According to the docs:
diff --git a/library/src/main/java/pl/aprilapps/easyphotopicker/EasyImage.kt b/library/src/main/java/pl/aprilapps/easyphotopicker/EasyImage.kt
index 4c02980..0173919 100644
--- a/library/src/main/java/pl/aprilapps/easyphotopicker/EasyImage.kt
+++ b/library/src/main/java/pl/aprilapps/easyphotopicker/EasyImage.kt
@@ -234,7 +234,7 @@ class EasyImage private constructor(
try {
if (cameraFile.uri.toString().isEmpty()) Intents.revokeWritePermission(activity, cameraFile.uri)
val files = mutableListOf(cameraFile)
- if (copyImagesToPublicGalleryFolder) Files.copyFilesInSeparateThread(activity, folderName, files.map { it.file })
+ if (copyImagesToPublicGalleryFolder) Files.copyImagesToPublicGallery(activity, folderName, files.map { it.file })
callbacks.onMediaFilesPicked(files.toTypedArray(), MediaSource.CAMERA_IMAGE)
} catch (error: Throwable) {
error.printStackTrace()
@@ -250,7 +250,8 @@ class EasyImage private constructor(
try {
if (cameraFile.uri.toString().isEmpty()) Intents.revokeWritePermission(activity, cameraFile.uri)
val files = mutableListOf(cameraFile)
- if (copyImagesToPublicGalleryFolder) Files.copyFilesInSeparateThread(activity, folderName, files.map { it.file })
+// if (copyImagesToPublicGalleryFolder) Files.copyFilesInSeparateThread(activity, folderName, files.map { it.file })
+ //FIXME
callbacks.onMediaFilesPicked(files.toTypedArray(), MediaSource.CAMERA_VIDEO)
} catch (error: Throwable) {
error.printStackTrace()
@@ -263,7 +264,7 @@ class EasyImage private constructor(
private fun onFileReturnedFromChooser(resultIntent: Intent?, activity: Activity, callbacks: Callbacks) {
Log.d(EASYIMAGE_LOG_TAG, "File returned from chooser")
if (resultIntent != null && !Intents.isTherePhotoTakenWithCameraInsideIntent(resultIntent)
- && (resultIntent.data != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && resultIntent.clipData != null )) {
+ && (resultIntent.data != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && resultIntent.clipData != null)) {
onPickedExistingPictures(resultIntent, activity, callbacks)
removeCameraFileAndCleanup()
} else if (lastCameraFile != null) {
diff --git a/library/src/main/java/pl/aprilapps/easyphotopicker/Files.kt b/library/src/main/java/pl/aprilapps/easyphotopicker/Files.kt
index 42a0454..3a11f6d 100644
--- a/library/src/main/java/pl/aprilapps/easyphotopicker/Files.kt
+++ b/library/src/main/java/pl/aprilapps/easyphotopicker/Files.kt
@@ -1,16 +1,22 @@
package pl.aprilapps.easyphotopicker
import android.content.ContentResolver
+import android.content.ContentValues
import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Matrix
+import android.media.ExifInterface
import android.media.MediaScannerConnection
import android.net.Uri
+import android.os.Build
import android.os.Environment
+import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
+import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import java.io.*
-import java.text.SimpleDateFormat
-import java.util.*
object Files {
@@ -41,53 +47,87 @@ object Files {
}
}
- @Throws(IOException::class)
- private fun copyFile(src: File, dst: File) {
- val inputStream = FileInputStream(src)
- writeToFile(inputStream, dst)
+ private fun rotateImage(bitmap: Bitmap, degrees: Float): Bitmap {
+ val matrix = Matrix()
+ matrix.postRotate(degrees)
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
- internal fun copyFilesInSeparateThread(context: Context, folderName: String, filesToCopy: List) {
- Thread(Runnable {
- val copiedFiles = ArrayList()
- var i = 1
- for (fileToCopy in filesToCopy) {
- val dstDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), folderName)
- if (!dstDir.exists()) dstDir.mkdirs()
-
- val filenameSplit = fileToCopy.name.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
- val extension = "." + filenameSplit[filenameSplit.size - 1]
- val datePart = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Calendar.getInstance().time)
- val filename = "IMG_${datePart}_$i.$extension%d.%s"
- val dstFile = File(dstDir, filename)
- try {
- dstFile.createNewFile()
- copyFile(fileToCopy, dstFile)
- copiedFiles.add(dstFile)
- } catch (e: IOException) {
- e.printStackTrace()
- }
+ private fun flipImage(bitmap: Bitmap, horizontal: Boolean, vertical: Boolean): Bitmap {
+ val matrix = Matrix()
+ matrix.preScale((if (horizontal) -1 else 1).toFloat(), (if (vertical) -1 else 1).toFloat())
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ }
- i++
+ private fun getFixedRotationBitmap(bitmapFile: File): Bitmap {
+ val exifInterface = ExifInterface(bitmapFile.path)
+ val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+
+ return with(BitmapFactory.decodeFile(bitmapFile.path)) {
+ when (orientation) {
+ ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(this, 90f)
+ ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(this, 180f)
+ ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(this, 270f)
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipImage(this, horizontal = true, vertical = false)
+ ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipImage(this, horizontal = false, vertical = true)
+ else -> this
}
- scanCopiedImages(context, copiedFiles)
- }).run()
+ }
+ }
+
+ @RequiresApi(29)
+ private fun copyImageToPublicGallery(context: Context, fileToCopy: File, folderName: String): String {
+ val bitmapToCopy = getFixedRotationBitmap(fileToCopy)
+ val contentResolver = context.contentResolver
+ val contentValues = ContentValues()
+ contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileToCopy.name)
+ contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
+ contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/$folderName")
+ val copyUri: Uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!!
+ val outputStream: OutputStream = contentResolver.openOutputStream(copyUri)!!
+ bitmapToCopy.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
+ outputStream.close()
+ Log.d(EASYIMAGE_LOG_TAG, "Copied image to public gallery: ${copyUri.path}")
+ return copyUri.path!!
+ }
+
+ private fun legacyCopyImageToPublicGallery(fileToCopy: File, folderName: String): String {
+ val bitmapToCopy = getFixedRotationBitmap(fileToCopy)
+ val legacyExternalStorageDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), folderName)
+ if (!legacyExternalStorageDir.exists()) legacyExternalStorageDir.mkdirs()
+ val copyFile = File(legacyExternalStorageDir, fileToCopy.name)
+ copyFile.createNewFile()
+ val outputStream = FileOutputStream(copyFile)
+ bitmapToCopy.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
+ outputStream.close()
+ Log.d(EASYIMAGE_LOG_TAG, "Copied image to public gallery: ${copyFile.path}")
+ return copyFile.path
}
- private fun scanCopiedImages(context: Context, copiedImages: List) {
- val paths = arrayOfNulls(copiedImages.size)
- for (i in copiedImages.indices) {
- paths[i] = copiedImages[i].toString()
- }
- MediaScannerConnection.scanFile(context,
- paths, null,
- object : MediaScannerConnection.OnScanCompletedListener {
- override fun onScanCompleted(path: String, uri: Uri) {
- Log.d(javaClass.simpleName, "Scanned $path:")
- Log.d(javaClass.simpleName, "-> uri=$uri")
+ internal fun copyImagesToPublicGallery(context: Context, folderName: String, filesToCopy: List) {
+ Thread {
+ val copiedFilesPaths: List = filesToCopy.map { fileToCopy ->
+ try {
+ if (Build.VERSION.SDK_INT >= 29) {
+ copyImageToPublicGallery(context, fileToCopy, folderName)
+ } else {
+ legacyCopyImageToPublicGallery(fileToCopy, folderName)
}
- })
+ } catch (error: Throwable) {
+ error.printStackTrace()
+ Log.e(EASYIMAGE_LOG_TAG, "File couldn't be copied to public gallery: ${fileToCopy.name}")
+ null
+ }
+ }
+ runMediaScanner(context, copiedFilesPaths.filterNotNull())
+ }.run()
+ }
+
+ private fun runMediaScanner(context: Context, paths: List) {
+ MediaScannerConnection.scanFile(context, paths.toTypedArray(), null) { path, uri ->
+ Log.d(EASYIMAGE_LOG_TAG, "Scanned media with path: $path | uri: $uri")
+ }
}
@Throws(IOException::class)
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index d43d438..ebd6a2d 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -9,10 +9,15 @@
+
+
diff --git a/sample/src/main/java/pl/aprilapps/easyphotopicker/sample/MainActivity.java b/sample/src/main/java/pl/aprilapps/easyphotopicker/sample/MainActivity.java
index 21b2a9f..4c42ebf 100644
--- a/sample/src/main/java/pl/aprilapps/easyphotopicker/sample/MainActivity.java
+++ b/sample/src/main/java/pl/aprilapps/easyphotopicker/sample/MainActivity.java
@@ -1,7 +1,9 @@
package pl.aprilapps.easyphotopicker.sample;
+import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
@@ -29,6 +31,7 @@ public class MainActivity extends AppCompatActivity implements EasyImage.EasyIma
private static final int CHOOSER_PERMISSIONS_REQUEST_CODE = 7459;
private static final int GALLERY_REQUEST_CODE = 7502;
private static final int DOCUMENTS_REQUEST_CODE = 7503;
+ private static final int LEGACY_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 456;
protected RecyclerView recyclerView;
@@ -40,6 +43,8 @@ public class MainActivity extends AppCompatActivity implements EasyImage.EasyIma
private EasyImage easyImage;
+ private static final String[] LEGACY_WRITE_PERMISSIONS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -60,7 +65,7 @@ protected void onCreate(Bundle savedInstanceState) {
easyImage = new EasyImage.Builder(this)
.setChooserTitle("Pick media")
- .setCopyImagesToPublicGalleryFolder(false)
+ .setCopyImagesToPublicGalleryFolder(true) // THIS requires granting WRITE_EXTERNAL_STORAGE permission for devices running Android 9 or lower
// .setChooserType(ChooserType.CAMERA_AND_DOCUMENTS)
.setChooserType(ChooserType.CAMERA_AND_GALLERY)
.setFolderName("EasyImage sample")
@@ -74,7 +79,11 @@ protected void onCreate(Bundle savedInstanceState) {
findViewById(R.id.gallery_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- easyImage.openGallery(MainActivity.this);
+ if (isLegacyExternalStoragePermissionRequired()) {
+ requestLegacyWriteExternalStoragePermission();
+ } else {
+ easyImage.openGallery(MainActivity.this);
+ }
}
});
@@ -82,28 +91,44 @@ public void onClick(View view) {
findViewById(R.id.camera_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- easyImage.openCameraForImage(MainActivity.this);
+ if (isLegacyExternalStoragePermissionRequired()) {
+ requestLegacyWriteExternalStoragePermission();
+ } else {
+ easyImage.openCameraForImage(MainActivity.this);
+ }
}
});
findViewById(R.id.camera_video_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- easyImage.openCameraForVideo(MainActivity.this);
+ if (isLegacyExternalStoragePermissionRequired()) {
+ requestLegacyWriteExternalStoragePermission();
+ } else {
+ easyImage.openCameraForVideo(MainActivity.this);
+ }
}
});
findViewById(R.id.documents_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- easyImage.openDocuments(MainActivity.this);
+ if (isLegacyExternalStoragePermissionRequired()) {
+ requestLegacyWriteExternalStoragePermission();
+ } else {
+ easyImage.openDocuments(MainActivity.this);
+ }
}
});
findViewById(R.id.chooser_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- easyImage.openChooser(MainActivity.this);
+ if (isLegacyExternalStoragePermissionRequired()) {
+ requestLegacyWriteExternalStoragePermission();
+ } else {
+ easyImage.openChooser(MainActivity.this);
+ }
}
});
@@ -181,16 +206,12 @@ private void onPhotosReturned(@NonNull MediaFile[] returnedPhotos) {
recyclerView.scrollToPosition(photos.size() - 1);
}
- private boolean arePermissionsGranted(String[] permissions) {
- for (String permission : permissions) {
- if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED)
- return false;
-
- }
- return true;
+ private boolean isLegacyExternalStoragePermissionRequired() {
+ boolean permissionGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ return Build.VERSION.SDK_INT < 29 && !permissionGranted;
}
- private void requestPermissionsCompat(String[] permissions, int requestCode) {
- ActivityCompat.requestPermissions(MainActivity.this, permissions, requestCode);
+ private void requestLegacyWriteExternalStoragePermission() {
+ ActivityCompat.requestPermissions(MainActivity.this, LEGACY_WRITE_PERMISSIONS, LEGACY_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
}