From e6cb1fa89600fd1a393e4650555ae6dc8f7afe86 Mon Sep 17 00:00:00 2001 From: Dhaval Patel Date: Tue, 28 Apr 2020 23:52:53 +0530 Subject: [PATCH] Added scoped storage support Added scoped storage support and removed redundant storage permission. #29 #75 --- .gitignore | 2 + README.md | 3 +- imagepicker/build.gradle | 7 +- imagepicker/src/main/AndroidManifest.xml | 3 - .../dhaval2404/imagepicker/ImagePicker.kt | 18 ++- .../imagepicker/ImagePickerActivity.kt | 82 ++++-------- .../imagepicker/ImagePickerFileProvider.kt | 2 +- .../imagepicker/provider/BaseProvider.kt | 19 ++- .../imagepicker/provider/CameraProvider.kt | 35 ++++-- .../provider/CompressionProvider.kt | 69 ++++++----- .../imagepicker/provider/CropProvider.kt | 32 +++-- .../imagepicker/provider/GalleryProvider.kt | 13 +- .../imagepicker/util/FileUriUtils.kt | 23 +++- .../dhaval2404/imagepicker/util/FileUtil.kt | 117 +++++++++++++++--- .../dhaval2404/imagepicker/util/ImageUtil.kt | 5 +- .../imagepicker/util/IntentUtils.kt | 34 ++++- .../imagepicker/util/PermissionUtil.kt | 5 +- sample/build.gradle | 7 +- .../imagepicker/MainActivityEspressoTest.kt | 1 - sample/src/main/AndroidManifest.xml | 2 - .../sample/ImageViewerDialog.kt | 9 +- .../sample/MainActivity.kt | 59 +++++---- .../sample/util/FileUtil.kt | 75 ++++++++--- 23 files changed, 397 insertions(+), 225 deletions(-) diff --git a/.gitignore b/.gitignore index ba7e0945..9e83bef0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ /.idea/gradle.xml /.idea/runConfigurations.xml /.idea/codeStyles +/.idea/dictionaries +/.idea/icon.png .DS_Store /build /captures diff --git a/README.md b/README.md index 6d7de23a..a9bf6aab 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ ![Language](https://img.shields.io/badge/language-Kotlin-orange.svg) [![Android Arsenal]( https://img.shields.io/badge/Android%20Arsenal-ImagePicker-green.svg?style=flat )]( https://android-arsenal.com/details/1/7510 ) [![PRWelcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Dhaval2404/ImagePicker) -[![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/Dhaval2404) +[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=102)](https://opensource.org/licenses/Apache-2.0) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/Dhaval2404/ImagePicker/blob/master/LICENSE) [![Twitter](https://img.shields.io/twitter/url/https/github.com/Dhaval2404/ImagePicker.svg?style=social)](https://twitter.com/intent/tweet?text=Check+out+an+ImagePicker+library+to+Pick+an+image+from+the+Gallery+or+Capture+an+image+with+Camera.+https%3A%2F%2Fgithub.com%2FDhaval2404%2FImagePicker+%40dhaval2404+%23Android+%23Kotlin+%23AndroidDev)
diff --git a/imagepicker/build.gradle b/imagepicker/build.gradle index 719f6987..25ed3cda 100644 --- a/imagepicker/build.gradle +++ b/imagepicker/build.gradle @@ -7,12 +7,12 @@ apply plugin: 'kotlin-android-extensions' apply from: "../ktlint.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 19 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 9 versionName "1.7.1" @@ -46,7 +46,8 @@ dependencies { implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' - implementation "androidx.exifinterface:exifinterface:1.1.0" + implementation "androidx.exifinterface:exifinterface:1.2.0" + implementation 'androidx.documentfile:documentfile:1.0.1' //More Info: https://github.com/Yalantis/uCrop implementation 'com.github.yalantis:ucrop:2.2.4' diff --git a/imagepicker/src/main/AndroidManifest.xml b/imagepicker/src/main/AndroidManifest.xml index d7baeee7..b940fe9c 100644 --- a/imagepicker/src/main/AndroidManifest.xml +++ b/imagepicker/src/main/AndroidManifest.xml @@ -1,9 +1,6 @@ - - - mCropProvider.startIntent(file) - mCompressionProvider.isCompressionRequired(file) -> mCompressionProvider.compress(file) - else -> setResult(file) + mCropProvider.isCropEnabled() -> mCropProvider.startIntent(uri) + mCompressionProvider.isCompressionRequired(uri) -> mCompressionProvider.compress(uri) + else -> setResult(uri) } } @@ -160,54 +137,47 @@ class ImagePickerActivity : AppCompatActivity() { * * Check if compression is enable/required. If yes then start compression else return result. * - * @param file Crop image file + * @param uri Crop image uri */ - fun setCropImage(file: File) { - mCropFile = file - - mCameraProvider?.let { - // Delete Camera file after crop. Else there will be two image for the same action. - // In case of Gallery Provider, we will get original image path, so we will not delete that. - mImageFile?.delete() - mImageFile = null - } + fun setCropImage(uri: Uri) { + // Delete Camera file after crop. Else there will be two image for the same action. + // In case of Gallery Provider, we will get original image path, so we will not delete that. + mCameraProvider?.delete() - if (mCompressionProvider.isCompressionRequired(file)) { - mCompressionProvider.compress(file) + if (mCompressionProvider.isCompressionRequired(uri)) { + mCompressionProvider.compress(uri) } else { - setResult(file) + setResult(uri) } } /** * {@link CompressionProvider} Result will be available here. * - * @param file Compressed image file + * @param uri Compressed image Uri */ - fun setCompressedImage(file: File) { + fun setCompressedImage(uri: Uri) { // This is the case when Crop is not enabled - mCameraProvider?.let { - // Delete Camera file after Compress. Else there will be two image for the same action. - // In case of Gallery Provider, we will get original image path, so we will not delete that. - mImageFile?.delete() - } + + // Delete Camera file after crop. Else there will be two image for the same action. + // In case of Gallery Provider, we will get original image path, so we will not delete that. + mCameraProvider?.delete() // If crop file is not null, Delete it after crop - mCropFile?.delete() - mCropFile = null + mCropProvider.delete() - setResult(file) + setResult(uri) } /** * Set Result, Image is successfully capture/picked/cropped/compressed. * - * @param file final image file + * @param uri final image Uri */ - private fun setResult(file: File) { + private fun setResult(uri: Uri) { val intent = Intent() - intent.data = Uri.fromFile(file) - intent.putExtra(ImagePicker.EXTRA_FILE_PATH, file.absolutePath) + intent.data = uri + intent.putExtra(ImagePicker.EXTRA_FILE_PATH, FileUriUtils.getRealPath(this, uri)) setResult(Activity.RESULT_OK, intent) finish() } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerFileProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerFileProvider.kt index bfcc59ae..206ecd80 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerFileProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerFileProvider.kt @@ -1,4 +1,4 @@ -package com.github.dhaval2404.imagepicker; +package com.github.dhaval2404.imagepicker import androidx.core.content.FileProvider diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/BaseProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/BaseProvider.kt index b4ba8bff..6ff66c78 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/BaseProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/BaseProvider.kt @@ -2,8 +2,8 @@ package com.github.dhaval2404.imagepicker.provider import android.content.ContextWrapper import android.os.Bundle -import android.widget.Toast import com.github.dhaval2404.imagepicker.ImagePickerActivity +import java.io.File /** * Abstract Provider class @@ -12,7 +12,13 @@ import com.github.dhaval2404.imagepicker.ImagePickerActivity * @version 1.0 * @since 04 January 2019 */ -abstract class BaseProvider(protected val activity: ImagePickerActivity) : ContextWrapper(activity) { +abstract class BaseProvider(protected val activity: ImagePickerActivity) : + ContextWrapper(activity) { + + fun getFileDir(path: String?): File { + return if (path != null) File(path) + else File(getExternalFilesDir(null), "Images") + } /** * Cancel operation and Set Error Message @@ -33,15 +39,6 @@ abstract class BaseProvider(protected val activity: ImagePickerActivity) : Conte setError(getString(errorRes)) } - /** - * Show Short Toast Message - * - * @param messageRes String message resource - */ - protected fun showToast(messageRes: Int) { - Toast.makeText(this, messageRes, Toast.LENGTH_SHORT).show() - } - /** * Call this method when task is cancel in between the operation. * E.g. user hit back-press diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CameraProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CameraProvider.kt index bddb9664..2333b156 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CameraProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CameraProvider.kt @@ -4,6 +4,7 @@ import android.Manifest import android.app.Activity import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.core.app.ActivityCompat.requestPermissions import com.github.dhaval2404.imagepicker.ImagePicker @@ -63,16 +64,14 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { /** * Camera image will be stored in below file directory */ - private var mFileDir: File? = null + private val mFileDir: File init { val bundle = activity.intent.extras!! // Get File Directory val fileDir = bundle.getString(ImagePicker.EXTRA_SAVE_DIRECTORY) - fileDir?.let { - mFileDir = File(it) - } + mFileDir = getFileDir(fileDir) } /** @@ -99,7 +98,9 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { } /** - * Start Camera Capture Intent + * Start Camera Intent + * + * Create Temporary File object and Pass it to Camera Intent */ fun startIntent() { if (!IntentUtils.isCameraAppAvailable(this)) { @@ -107,7 +108,7 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { return } - checkPermission() + startCameraIntent() } /** @@ -132,7 +133,7 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { */ private fun startCameraIntent() { // Create and get empty file to store capture image content - val file = FileUtil.getImageFile(dir = mFileDir) + val file = FileUtil.getImageFile(fileDir = mFileDir) mCameraFile = file // Check if file exists @@ -152,7 +153,7 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { // Check again if permission is granted if (isPermissionGranted(this)) { // Permission is granted, Start Camera Intent - startCameraIntent() + startIntent() } else { // Exit with error message val errorRes = if (mAskCameraPermission) { @@ -175,7 +176,7 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == CAMERA_INTENT_REQ_CODE) { if (resultCode == Activity.RESULT_OK) { - handleResult(data) + handleResult() } else { setResultCancel() } @@ -185,15 +186,15 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { /** * This method will be called when final result fot this provider is enabled. */ - private fun handleResult(data: Intent?) { - activity.setImage(mCameraFile!!) + private fun handleResult() { + activity.setImage(Uri.fromFile(mCameraFile)) } /** * Delete Camera file is exists */ override fun onFailure() { - mCameraFile?.delete() + delete() } /** @@ -229,4 +230,14 @@ class CameraProvider(activity: ImagePickerActivity) : BaseProvider(activity) { } return false } + + /** + * Delete Camera File, If not required + * + * After Camera Image Crop/Compress Original File will not required + */ + fun delete() { + mCameraFile?.delete() + mCameraFile = null + } } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt index afa6cbf2..3e142b78 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt @@ -2,7 +2,7 @@ package com.github.dhaval2404.imagepicker.provider import android.annotation.SuppressLint import android.graphics.Bitmap -import android.graphics.BitmapFactory +import android.net.Uri import android.os.AsyncTask import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePickerActivity @@ -27,8 +27,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity private val mMaxHeight: Int private val mMaxFileSize: Long - private var mOriginalFile: File? = null - private var mFileDir: File? = null + private val mFileDir: File init { val bundle = activity.intent.extras!! @@ -42,9 +41,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity // Get File Directory val fileDir = bundle.getString(ImagePicker.EXTRA_SAVE_DIRECTORY) - fileDir?.let { - mFileDir = File(it) - } + mFileDir = getFileDir(fileDir) } /** @@ -52,7 +49,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity * * @return Boolean. True if Compression should be enabled else false. */ - fun isCompressEnabled(): Boolean { + private fun isCompressEnabled(): Boolean { return mMaxFileSize > 0L } @@ -60,12 +57,26 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity * Check if compression is required * @param file File object to apply Compression */ - fun isCompressionRequired(file: File): Boolean { + private fun isCompressionRequired(file: File): Boolean { val status = isCompressEnabled() && getSizeDiff(file) > 0L if (!status && mMaxWidth > 0 && mMaxHeight > 0) { // Check image resolution - val sizes = getImageSize(file) - return sizes[0] > mMaxWidth || sizes[1] > mMaxHeight + val resolution = FileUtil.getImageResolution(file) + return resolution.first > mMaxWidth || resolution.second > mMaxHeight + } + return status + } + + /** + * Check if compression is required + * @param uri Uri object to apply Compression + */ + fun isCompressionRequired(uri: Uri): Boolean { + val status = isCompressEnabled() && getSizeDiff(uri) > 0L + if (!status && mMaxWidth > 0 && mMaxHeight > 0) { + // Check image resolution + val resolution = FileUtil.getImageResolution(this, uri) + return resolution.first > mMaxWidth || resolution.second > mMaxHeight } return status } @@ -74,25 +85,30 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity return file.length() - mMaxFileSize } + private fun getSizeDiff(uri: Uri): Long { + val length = FileUtil.getImageSize(this, uri) + return length - mMaxFileSize + } + /** * Compress given file if enabled. * - * @param file File to compress + * @param uri Uri to compress */ - fun compress(file: File) { - startCompressionWorker(file) + fun compress(uri: Uri) { + startCompressionWorker(uri) } /** * Start Compression in Background */ @SuppressLint("StaticFieldLeak") - private fun startCompressionWorker(file: File) { - mOriginalFile = file - object : AsyncTask() { - override fun doInBackground(vararg params: File): File? { + private fun startCompressionWorker(uri: Uri) { + object : AsyncTask() { + override fun doInBackground(vararg params: Uri): File? { // Perform operation in background - return startCompression(params[0]) + val file = FileUtil.getTempFile(this@CompressionProvider, params[0]) ?: return null + return startCompression(file) } override fun onPostExecute(file: File?) { @@ -105,7 +121,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity setError(com.github.dhaval2404.imagepicker.R.string.error_failed_to_compress_image) } } - }.execute(file) + }.execute(uri) } /** @@ -175,7 +191,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity quality = 100 } - val compressFile: File? = FileUtil.getImageFile(dir = mFileDir) + val compressFile: File? = FileUtil.getImageFile(fileDir = mFileDir) return if (compressFile != null) { ImageUtil.compressImage( file, maxWidth.toFloat(), maxHeight.toFloat(), @@ -214,18 +230,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity * This method will be called when final result fot this provider is enabled. */ private fun handleResult(file: File) { - activity.setCompressedImage(file) + activity.setCompressedImage(Uri.fromFile(file)) } - /** - * - * @param file File to get Image Size - * @return Int Array, Index 0 has width and Index 1 has height - */ - private fun getImageSize(file: File): IntArray { - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(file.absolutePath, options) - return intArrayOf(options.outWidth, options.outHeight) - } } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt index c4cf7131..63e6d5f2 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt @@ -39,7 +39,7 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { private val mCropAspectX: Float private val mCropAspectY: Float private var mCropImageFile: File? = null - private var mFileDir: File? = null + private val mFileDir: File init { val bundle = activity.intent.extras!! @@ -55,9 +55,7 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { // Get File Directory val fileDir = bundle.getString(ImagePicker.EXTRA_SAVE_DIRECTORY) - fileDir?.let { - mFileDir = File(it) - } + mFileDir = getFileDir(fileDir) } /** @@ -93,17 +91,17 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { /** * Start Crop Activity */ - fun startIntent(file: File) { - cropImage(file) + fun startIntent(uri: Uri) { + cropImage(uri) } /** - * @param file Image File to be cropped + * @param uri Uri to be cropped * @throws IOException if failed to crop image */ @Throws(IOException::class) - private fun cropImage(file: File) { - mCropImageFile = FileUtil.getImageFile(dir = mFileDir) + private fun cropImage(uri: Uri) { + mCropImageFile = FileUtil.getImageFile(fileDir = mFileDir) if (mCropImageFile == null || !mCropImageFile!!.exists()) { Log.e(TAG, "Failed to create crop image file") @@ -112,7 +110,7 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { } val options = UCrop.Options() - val uCrop = UCrop.of(Uri.fromFile(file), Uri.fromFile(mCropImageFile)) + val uCrop = UCrop.of(uri, Uri.fromFile(mCropImageFile)) .withOptions(options) if (mCropAspectX > 0 && mCropAspectY > 0) { @@ -162,16 +160,26 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { */ private fun handleResult(file: File?) { if (file != null) { - activity.setCropImage(file) + activity.setCropImage(Uri.fromFile(file)) } else { setError(R.string.error_failed_to_crop_image) } } /** - * Delete Crop file is exists + * Handle Crop Failed */ override fun onFailure() { + delete() + } + + /** + * Delete Crop File, If not required + * + * After Image Compression, Crop File will not required + */ + fun delete() { mCropImageFile?.delete() + mCropImageFile = null } } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt index 2ca327bb..a58f685a 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt @@ -7,10 +7,8 @@ import androidx.core.app.ActivityCompat.requestPermissions import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePickerActivity import com.github.dhaval2404.imagepicker.R -import com.github.dhaval2404.imagepicker.util.FileUriUtils import com.github.dhaval2404.imagepicker.util.IntentUtils import com.github.dhaval2404.imagepicker.util.PermissionUtil -import java.io.File /** * Select image from Storage @@ -28,7 +26,7 @@ class GalleryProvider(activity: ImagePickerActivity) : * to crop or compress image write permission is also required. as both permission is in * same group, we have used write permission here. */ - private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) + private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) private const val GALLERY_INTENT_REQ_CODE = 4261 private const val PERMISSION_INTENT_REQ_CODE = 4262 @@ -47,7 +45,7 @@ class GalleryProvider(activity: ImagePickerActivity) : * Start Gallery Capture Intent */ fun startIntent() { - checkPermission() + startGalleryIntent() } /** @@ -110,12 +108,7 @@ class GalleryProvider(activity: ImagePickerActivity) : private fun handleResult(data: Intent?) { val uri = data?.data if (uri != null) { - val filePath: String? = FileUriUtils.getRealPath(activity, uri) - if (!filePath.isNullOrEmpty()) { - activity.setImage(File(filePath)) - } else { - setError(R.string.error_failed_pick_gallery_image) - } + activity.setImage(uri) } else { setError(R.string.error_failed_pick_gallery_image) } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUriUtils.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUriUtils.kt index 66283f67..f7e3cd89 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUriUtils.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUriUtils.kt @@ -8,6 +8,7 @@ import android.os.Build import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore +import androidx.documentfile.provider.DocumentFile import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -25,6 +26,15 @@ import java.io.OutputStream object FileUriUtils { + fun isFileUri(uri: Uri): Boolean { + return "file".equals(uri.scheme, ignoreCase = true) + } + + fun isUriExposed(context: Context, uri: Uri): Boolean { + val file = DocumentFile.fromSingleUri(context, uri) + return file?.canRead() == true + } + fun getRealPath(context: Context, uri: Uri): String? { var path = getPathFromLocalUri(context, uri) if (path == null) { @@ -64,7 +74,8 @@ object FileUriUtils { } else if (isDownloadsDocument(uri)) { val fileName = getFilePath(context, uri) if (fileName != null) { - return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName + return Environment.getExternalStorageDirectory() + .toString() + "/Download/" + fileName } val id = DocumentsContract.getDocumentId(uri) @@ -95,7 +106,12 @@ object FileUriUtils { } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { // Return the remote address - return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { return uri.path } // File @@ -116,7 +132,8 @@ object FileUriUtils { val projection = arrayOf(column) try { - cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + cursor = + context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) if (cursor != null && cursor.moveToFirst()) { val index = cursor.getColumnIndexOrThrow(column) return cursor.getString(index) diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUtil.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUtil.kt index a2c88590..101bca5c 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUtil.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/FileUtil.kt @@ -1,8 +1,13 @@ package com.github.dhaval2404.imagepicker.util -import android.os.Environment +import android.content.Context +import android.graphics.BitmapFactory +import android.net.Uri import android.os.StatFs +import androidx.documentfile.provider.DocumentFile import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream import java.io.IOException import java.text.SimpleDateFormat import java.util.Date @@ -22,25 +27,23 @@ object FileUtil { * * Default it will take Camera folder as it's directory * - * @param dir File Folder in which file needs tobe created. + * @param fileDir File Folder in which file needs tobe created. * @param extension String Image file extension. * @return Return Empty file to store camera image. * @throws IOException if permission denied of failed to create new file. */ - fun getImageFile(dir: File? = null, extension: String? = null): File? { + fun getImageFile(fileDir: File, extension: String? = null): File? { try { // Create an image file name val ext = extension ?: ".jpg" - val imageFileName = "IMG_${getTimestamp()}$ext" - - // Create File Directory Object - val storageDir = dir ?: getCameraDirectory() + val fileName = getFileName() + val imageFileName = "$fileName$ext" // Create Directory If not exist - if (!storageDir.exists()) storageDir.mkdirs() + if (!fileDir.exists()) fileDir.mkdirs() // Create File Object - val file = File(storageDir, imageFileName) + val file = File(fileDir, imageFileName) // Create empty file file.createNewFile() @@ -52,15 +55,8 @@ object FileUtil { } } - /** - * Get Camera Image Directory - * - * @return File Camera Image Directory - */ - private fun getCameraDirectory(): File { - val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) - return File(dir, "Camera") - } + private fun getFileName() = "IMG_${getTimestamp()}" + //private fun getFileName() = "IMAGE_PICKER" /** * Get Current Time in yyyyMMdd HHmmssSSS format @@ -83,4 +79,89 @@ object FileUtil { val blockSize = stat.blockSizeLong return availBlocks * blockSize } + + /** + * Get Image Width & Height from Uri + * + * @param uri Uri to get Image Size + * @return Int Array, Index 0 has width and Index 1 has height + */ + fun getImageResolution(context: Context, uri: Uri): Pair { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + val stream = context.contentResolver.openInputStream(uri) + BitmapFactory.decodeStream(stream, null, options) + return Pair(options.outWidth, options.outHeight) + } + + /** + * Get Image Width & Height from File + * + * @param file File to get Image Size + * @return Int Array, Index 0 has width and Index 1 has height + */ + fun getImageResolution(file: File): Pair { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(file.absolutePath, options) + return Pair(options.outWidth, options.outHeight) + } + + /** + * Get Image File Size + * + * @param uri Uri to get Image Size + * @return Int Image File Size + */ + fun getImageSize(context: Context, uri: Uri): Long { + return getDocumentFile(context, uri)?.length() ?: 0 + } + + /** + * Create copy of Uri into application specific local path + * + * @param context Application Context + * @param uri Source Uri + * @return File return copy of Uri object + */ + fun getTempFile(context: Context, uri: Uri): File? { + try { + val destination = File(context.cacheDir, "image_picker.png") + + val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r") + val fileDescriptor = parcelFileDescriptor?.fileDescriptor ?: return null + + val src = FileInputStream(fileDescriptor).channel + val dst = FileOutputStream(destination).channel + dst.transferFrom(src, 0, src.size()) + src.close() + dst.close() + + return destination + } catch (ex: IOException) { + ex.printStackTrace() + } + return null + } + + /** + * Get DocumentFile from Uri + * + * @param context Application Context + * @param uri Source Uri + * @return DocumentFile return DocumentFile from Uri + */ + fun getDocumentFile(context: Context, uri: Uri): DocumentFile? { + var file: DocumentFile? = null + if (FileUriUtils.isFileUri(uri)) { + val path = FileUriUtils.getRealPath(context, uri) + if (path != null) { + file = DocumentFile.fromFile(File(path)) + } + } else { + file = DocumentFile.fromSingleUri(context, uri) + } + return file + } + } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ImageUtil.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ImageUtil.kt index 4c183c29..74de5c26 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ImageUtil.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ImageUtil.kt @@ -194,7 +194,10 @@ object ImageUtil { /** * Ref: https://developer.android.com/topic/performance/graphics/manage-memory#kotlin */ - private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean { + private fun canUseForInBitmap( + candidate: Bitmap, + targetOptions: BitmapFactory.Options + ): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // From Android 4.4 (KitKat) onward we can re-use if the byte size of // the new bitmap is smaller than the reusable bitmap candidate diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt index 963f3460..af49f058 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.provider.MediaStore import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile import com.github.dhaval2404.imagepicker.R import java.io.File @@ -67,7 +68,8 @@ object IntentUtils { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // authority = com.github.dhaval2404.imagepicker.provider - val authority = context.packageName + context.getString(R.string.image_picker_provider_authority_suffix) + val authority = + context.packageName + context.getString(R.string.image_picker_provider_authority_suffix) val photoURI = FileProvider.getUriForFile(context, authority, file) intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) } else { @@ -82,7 +84,7 @@ object IntentUtils { * * @return true if Camera App is Available else return false */ - fun isCameraAppAvailable(context: Context) : Boolean { + fun isCameraAppAvailable(context: Context): Boolean { val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) return intent.resolveActivity(context.packageManager) != null } @@ -90,4 +92,32 @@ object IntentUtils { fun isCameraHardwareAvailable(context: Context): Boolean { return context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) } + + + /** + * Get Intent to View Uri backed File + * + * @param context + * @param uri + * @return Intent + */ + fun getUriViewIntent(context: Context, uri: Uri): Intent { + val intent = Intent(Intent.ACTION_VIEW) + val authority = + context.packageName + context.getString(R.string.image_picker_provider_authority_suffix) + + val file = DocumentFile.fromSingleUri(context, uri) + val dataUri = if (file?.canRead() == true) { + uri + } else { + val filePath = FileUriUtils.getRealPath(context, uri)!! + FileProvider.getUriForFile(context, authority, File(filePath)) + } + + intent.setDataAndType(dataUri, "image/*") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + return intent + } + } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/PermissionUtil.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/PermissionUtil.kt index a9926bf1..8513748f 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/PermissionUtil.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/PermissionUtil.kt @@ -48,7 +48,10 @@ object PermissionUtil { * @return true if permission defined in AndroidManifest.xml file, else return false. */ fun isPermissionInManifest(context: Context, permission: String): Boolean { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + val packageInfo = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_PERMISSIONS + ) val permissions = packageInfo.requestedPermissions if (permissions.isNullOrEmpty()) diff --git a/sample/build.gradle b/sample/build.gradle index 220e7922..a1b867c8 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -7,11 +7,11 @@ apply plugin: 'kotlin-android-extensions' apply from: "../ktlint.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { applicationId "com.github.dhaval2404.imagepicker.sample" minSdkVersion 19 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 9 versionName "1.7.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -41,6 +41,7 @@ dependencies { implementation 'androidx.browser:browser:1.2.0' implementation 'com.google.android.material:material:1.1.0' implementation 'com.github.florent37:inline-activity-result-kotlin:1.0.3' + implementation 'androidx.documentfile:documentfile:1.0.1' testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.1' @@ -50,7 +51,7 @@ dependencies { implementation 'com.github.bumptech.glide:glide:4.11.0' //Leakcanary - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0' + //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' implementation project(':imagepicker') diff --git a/sample/src/androidTest/java/com/github/dhaval2404/imagepicker/MainActivityEspressoTest.kt b/sample/src/androidTest/java/com/github/dhaval2404/imagepicker/MainActivityEspressoTest.kt index 10cf077f..24cfe7d8 100644 --- a/sample/src/androidTest/java/com/github/dhaval2404/imagepicker/MainActivityEspressoTest.kt +++ b/sample/src/androidTest/java/com/github/dhaval2404/imagepicker/MainActivityEspressoTest.kt @@ -9,7 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule import com.github.dhaval2404.imagepicker.sample.MainActivity import com.github.dhaval2404.imagepicker.sample.R -import kotlinx.android.synthetic.main.content_gallery_only.* import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index db9503af..622c2439 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -2,8 +2,6 @@ - - // Intercept ImageProvider - Log.d("ImagePicker", "Selected ImageProvider: "+imageProvider.name) + Log.d("ImagePicker", "Selected ImageProvider: " + imageProvider.name) } // Image resolution will be less than 512 x 512 - .maxResultSize(512, 512) + //.maxResultSize(512, 512) + //.saveDir(getExternalFilesDir(null)!!) .start(PROFILE_IMAGE_REQ_CODE) } @@ -86,7 +87,8 @@ class MainActivity : AppCompatActivity() { ) ) // Image resolution will be less than 1080 x 1920 - .maxResultSize(1080, 1920) + //.maxResultSize(360, 420) + //.saveDir(getExternalFilesDir(null)!!) .start(GALLERY_IMAGE_REQ_CODE) } @@ -95,10 +97,8 @@ class MainActivity : AppCompatActivity() { // User can only capture image from Camera .cameraOnly() // Image size will be less than 1024 KB - .compress(1024) - .saveDir(Environment.getExternalStorageDirectory()) - // .saveDir(Environment.getExternalStorageDirectory().absolutePath+File.separator+"ImagePicker") - // .saveDir(getExternalFilesDir(null)!!) + .compress(50) + .saveDir(getExternalFilesDir(null)!!) .start(CAMERA_IMAGE_REQ_CODE) } @@ -106,20 +106,19 @@ class MainActivity : AppCompatActivity() { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { Log.e("TAG", "Path:${ImagePicker.getFilePath(data)}") - // Uri & File object will not be null for RESULT_OK + // Uri object will not be null for RESULT_OK val uri = data?.data!! - val file = ImagePicker.getFile(data)!! when (requestCode) { PROFILE_IMAGE_REQ_CODE -> { - mProfileFile = file + mProfileUri = uri imgProfile.setLocalImage(uri, true) } GALLERY_IMAGE_REQ_CODE -> { - mGalleryFile = file + mGalleryUri = uri imgGallery.setLocalImage(uri) } CAMERA_IMAGE_REQ_CODE -> { - mCameraFile = file + mCameraUri = uri imgCamera.setLocalImage(uri) } } @@ -141,29 +140,29 @@ class MainActivity : AppCompatActivity() { } fun showImage(view: View) { - val file = when (view) { - imgProfile -> mProfileFile - imgCamera -> mCameraFile - imgGallery -> mGalleryFile + val uri = when (view) { + imgProfile -> mProfileUri + imgCamera -> mCameraUri + imgGallery -> mGalleryUri else -> null } - file?.let { - IntentUtil.showImage(this, file) + uri?.let { + startActivity(IntentUtils.getUriViewIntent(this, uri)) } } fun showImageInfo(view: View) { - val file = when (view) { - imgProfileInfo -> mProfileFile - imgCameraInfo -> mCameraFile - imgGalleryInfo -> mGalleryFile + val uri = when (view) { + imgProfileInfo -> mProfileUri + imgCameraInfo -> mCameraUri + imgGalleryInfo -> mGalleryUri else -> null } AlertDialog.Builder(this) .setTitle("Image Info") - .setMessage(FileUtil.getFileInfo(file)) + .setMessage(FileUtil.getFileInfo(this, uri)) .setPositiveButton("Ok", null) .show() } diff --git a/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/util/FileUtil.kt b/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/util/FileUtil.kt index 6b7416d2..83a47998 100644 --- a/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/util/FileUtil.kt +++ b/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/util/FileUtil.kt @@ -1,10 +1,16 @@ package com.github.dhaval2404.imagepicker.sample.util -import android.graphics.BitmapFactory +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import com.github.dhaval2404.imagepicker.util.FileUriUtils +import com.github.dhaval2404.imagepicker.util.FileUtil import java.io.File import java.text.SimpleDateFormat import java.util.Locale + /** * File Utility * @@ -15,17 +21,29 @@ import java.util.Locale object FileUtil { /** - * @param file File + * @param context Context + * @param uri Uri * @return Image Info */ - fun getFileInfo(file: File?): String { - if (file == null || !file.exists()) { + fun getFileInfo(context: Context, uri: Uri?): String { + if (uri == null) { return "Image not found" } - val resolution = getImageResolution(file) + // Get Resolution + val resolution = FileUtil.getImageResolution(context, uri) + + // File Path + val filePath = FileUriUtils.getRealPath(context, uri) + val document = FileUtil.getDocumentFile(context, uri) ?: return "Image not found" + + // Get Last Modified val sdf = SimpleDateFormat("dd/MM/yyyy hh:mm:ss a", Locale.getDefault()) - val modified = sdf.format(file.lastModified()) + val modified = sdf.format(document.lastModified()) + + // File Size + val fileSize = getFileSize(document.length()) + return StringBuilder() .append("Resolution: ") @@ -37,16 +55,23 @@ object FileUtil { .append("\n\n") .append("File Size: ") - .append(getFileSize(file)) + .append(fileSize) + .append("\n\n") + + .append("File Name: ") + .append(getFileName(context.contentResolver, uri)) .append("\n\n") .append("File Path: ") - .append(file.absolutePath) + .append(filePath) + .append("\n\n") + + .append("Uri Path: ") + .append(uri.toString()) .toString() } - private fun getFileSize(file: File): String { - val fileSize = file.length().toFloat() + private fun getFileSize(fileSize: Long): String { val mb = fileSize / (1024 * 1024) val kb = fileSize / (1024) @@ -57,10 +82,30 @@ object FileUtil { } } - private fun getImageResolution(file: File): Pair { - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - BitmapFactory.decodeFile(file.absolutePath, options) - return Pair(options.outWidth, options.outHeight) + fun getFileName(contentResolver: ContentResolver, uri: Uri): String? { + if (ContentResolver.SCHEME_FILE == uri.scheme) { + return File(uri.path).getName() + } else if (ContentResolver.SCHEME_CONTENT == uri.scheme) { + return getCursorContent(contentResolver, uri) + } + return null } + + private fun getCursorContent( + contentResolver: ContentResolver, + uri: Uri + ): String? { + return try { + val cursor = contentResolver.query(uri, null, null, null, null) ?: return null + var fileName: String? = null + if (cursor.moveToFirst()) { + fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + cursor.close() + fileName + } catch (ex: Exception) { + null + } + } + }