diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/imagepicker/build.gradle b/imagepicker/build.gradle index 39aa0d0d..fdcbec77 100644 --- a/imagepicker/build.gradle +++ b/imagepicker/build.gradle @@ -6,11 +6,11 @@ plugins { apply from: "../ktlint.gradle" android { - compileSdkVersion 30 + compileSdkVersion 33 defaultConfig { minSdkVersion 19 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 16 versionName "2.1" diff --git a/imagepicker/src/main/AndroidManifest.xml b/imagepicker/src/main/AndroidManifest.xml index 2a14f96a..2c6a94fd 100644 --- a/imagepicker/src/main/AndroidManifest.xml +++ b/imagepicker/src/main/AndroidManifest.xml @@ -1,8 +1,17 @@ + + + + + + + + + + - - - - @@ -38,4 +44,4 @@ - + \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt index fddfbe4c..724d3a52 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt @@ -306,7 +306,8 @@ open class ImagePicker { } }, dismissListener - ) + ,activity) + } else { onResult(createIntent()) } @@ -328,7 +329,7 @@ open class ImagePicker { } }, dismissListener - ) + ,activity ) } /** diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt index 6f0e95bf..38b5336d 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt @@ -1,12 +1,6 @@ package com.github.dhaval2404.imagepicker import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.util.Log -import androidx.appcompat.app.AppCompatActivity import com.github.dhaval2404.imagepicker.constant.ImageProvider import com.github.dhaval2404.imagepicker.provider.CameraProvider import com.github.dhaval2404.imagepicker.provider.CompressionProvider @@ -21,17 +15,25 @@ import com.github.dhaval2404.imagepicker.util.FileUriUtils * @version 1.0 * @since 04 January 2019 */ +import android.Manifest +import android.content.Intent +import android.content.Context +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +/*import com.example.imagepicker.ImagePicker +import com.example.imagepicker.utils.FileUriUtils*/ + class ImagePickerActivity : AppCompatActivity() { companion object { private const val TAG = "image_picker" - - internal fun getCancelledIntent(context: Context): Intent { - val intent = Intent() - val message = context.getString(R.string.error_task_cancelled) - intent.putExtra(ImagePicker.EXTRA_ERROR, message) - return intent - } + private const val REQUEST_CODE_STORAGE_PERMISSION = 100 } private var mGalleryProvider: GalleryProvider? = null @@ -44,19 +46,7 @@ class ImagePickerActivity : AppCompatActivity() { loadBundle(savedInstanceState) } - /** - * Save all appropriate activity state. - */ - public override fun onSaveInstanceState(outState: Bundle) { - mCameraProvider?.onSaveInstanceState(outState) - mCropProvider.onSaveInstanceState(outState) - super.onSaveInstanceState(outState) - } - - /** - * Parse Intent Bundle and initialize variables - */ - private fun loadBundle(savedInstanceState: Bundle?) { + /* private fun loadBundle(savedInstanceState: Bundle?) { // Create Crop Provider mCropProvider = CropProvider(this) mCropProvider.onRestoreInstanceState(savedInstanceState) @@ -64,46 +54,106 @@ class ImagePickerActivity : AppCompatActivity() { // Create Compression Provider mCompressionProvider = CompressionProvider(this) - // Retrieve Image Provider + // Check and request permissions based on Android version + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Marshmallow and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13 (API level 33) and above + // Request permission only for accessing photos + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_MEDIA_IMAGES), REQUEST_CODE_STORAGE_PERMISSION) + } else { + initImageProvider(savedInstanceState) + } + } else { + // Request permission for accessing photos and videos + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE_STORAGE_PERMISSION) + } else { + initImageProvider(savedInstanceState) + } + } + } else { + // For devices below Marshmallow, permissions are granted at installation + initImageProvider(savedInstanceState) + } + }*/ + + private fun loadBundle(savedInstanceState: Bundle?) { + + mCropProvider = CropProvider(this) + mCropProvider.onRestoreInstanceState(savedInstanceState) + mCompressionProvider = CompressionProvider(this) + + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >=33) { + // Android 13 and above photos only + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.READ_MEDIA_IMAGES), + REQUEST_CODE_STORAGE_PERMISSION + ) + } else { + initImageProvider(savedInstanceState) + } + } else { + // Android 12 and below + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + REQUEST_CODE_STORAGE_PERMISSION + ) + } else { + initImageProvider(savedInstanceState) + } + } + } else { + initImageProvider(savedInstanceState) + } + } + + + + + + private fun initImageProvider(savedInstanceState: Bundle?) { val provider: ImageProvider? = intent?.getSerializableExtra(ImagePicker.EXTRA_IMAGE_PROVIDER) as ImageProvider? - // Create Gallery/Camera Provider when (provider) { ImageProvider.GALLERY -> { mGalleryProvider = GalleryProvider(this) - // Pick Gallery Image savedInstanceState ?: mGalleryProvider?.startIntent() } ImageProvider.CAMERA -> { mCameraProvider = CameraProvider(this) - mCameraProvider?.onRestoreInstanceState(savedInstanceState) - // Pick Camera Image savedInstanceState ?: mCameraProvider?.startIntent() } else -> { - // Something went Wrong! This case should never happen - Log.e(TAG, "Image provider can not be null") + Log.e(TAG, "Image provider cannot be null") setError(getString(R.string.error_task_cancelled)) } } } - /** - * Dispatch incoming result to the correct provider. - */ - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - mCameraProvider?.onRequestPermissionsResult(requestCode) + if (requestCode == REQUEST_CODE_STORAGE_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + initImageProvider(null) + } else { + setError("Permission denied") + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + mCameraProvider?.onSaveInstanceState(outState) + mCropProvider.onSaveInstanceState(outState) + super.onSaveInstanceState(outState) } - /** - * Dispatch incoming result to the correct provider. - */ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) mCameraProvider?.onActivityResult(requestCode, resultCode, data) @@ -111,18 +161,10 @@ class ImagePickerActivity : AppCompatActivity() { mCropProvider.onActivityResult(requestCode, resultCode, data) } - /** - * Handle Activity Back Press - */ override fun onBackPressed() { setResultCancel() } - /** - * {@link CameraProvider} and {@link GalleryProvider} Result will be available here. - * - * @param uri Capture/Gallery image Uri - */ fun setImage(uri: Uri) { when { mCropProvider.isCropEnabled() -> mCropProvider.startIntent(uri) @@ -131,16 +173,7 @@ class ImagePickerActivity : AppCompatActivity() { } } - /** - * {@link CropProviders} Result will be available here. - * - * Check if compression is enable/required. If yes then start compression else return result. - * - * @param uri Crop image uri - */ 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(uri)) { @@ -150,29 +183,12 @@ class ImagePickerActivity : AppCompatActivity() { } } - /** - * {@link CompressionProvider} Result will be available here. - * - * @param uri Compressed image Uri - */ fun setCompressedImage(uri: Uri) { - // This is the case when Crop is not enabled - - // 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 mCropProvider.delete() - setResult(uri) } - /** - * Set Result, Image is successfully capture/picked/cropped/compressed. - * - * @param uri final image Uri - */ private fun setResult(uri: Uri) { val intent = Intent() intent.data = uri @@ -181,23 +197,22 @@ class ImagePickerActivity : AppCompatActivity() { finish() } - /** - * User has cancelled the task - */ fun setResultCancel() { setResult(Activity.RESULT_CANCELED, getCancelledIntent(this)) finish() } - /** - * Error occurred while processing image - * - * @param message Error Message - */ fun setError(message: String) { val intent = Intent() intent.putExtra(ImagePicker.EXTRA_ERROR, message) setResult(ImagePicker.RESULT_ERROR, intent) finish() } -} + + internal fun getCancelledIntent(context: Context): Intent { + val intent = Intent() + val message = context.getString(R.string.error_task_cancelled) + intent.putExtra(ImagePicker.EXTRA_ERROR, message) + return intent + } +} \ No newline at end of file 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 58736424..4584198c 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 @@ -13,7 +13,7 @@ import java.io.File * @version 1.0 * @since 04 January 2019 */ -abstract class BaseProvider(protected val activity: ImagePickerActivity) : +abstract class BaseProvider(protected open val activity: ImagePickerActivity) : ContextWrapper(activity) { fun getFileDir(path: String?): File { 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 2451fcf9..b5088fd9 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 @@ -1,9 +1,14 @@ package com.github.dhaval2404.imagepicker.provider +import android.Manifest import android.app.Activity import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.os.Bundle +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePickerActivity import com.github.dhaval2404.imagepicker.R @@ -16,6 +21,89 @@ import com.github.dhaval2404.imagepicker.util.IntentUtils * @version 1.0 * @since 04 January 2019 */ + + +class GalleryProvider(override val activity: ImagePickerActivity) : BaseProvider(activity) { + + companion object { + private const val GALLERY_INTENT_REQ_CODE = 4261 + private const val PERMISSION_REQUEST_CODE = 200 + } + + private val mimeTypes: Array = activity.intent.extras?.getStringArray(ImagePicker.EXTRA_MIME_TYPES) ?: emptyArray() + + fun startIntent() { + if (isPermissionGranted()) { + startGalleryIntent() + } else { + requestPermissions() + } + } + + private fun isPermissionGranted(): Boolean { + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED + } + + private fun requestPermissions() { + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + ActivityCompat.requestPermissions(activity, arrayOf(permission), PERMISSION_REQUEST_CODE) + } + + private fun startGalleryIntent() { + val galleryIntent = IntentUtils.getGalleryIntent(activity, mimeTypes) + activity.startActivityForResult(galleryIntent, GALLERY_INTENT_REQ_CODE) + } + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == GALLERY_INTENT_REQ_CODE && resultCode == Activity.RESULT_OK) { + handleResult(data) + } else { + setResultCancel() + } + } + + private fun handleResult(data: Intent?) { + val uri = data?.data + if (uri != null) { + takePersistableUriPermission(uri) + activity.setImage(uri) + } else { + setError(R.string.error_failed_pick_gallery_image) + } + } + + private fun takePersistableUriPermission(uri: Uri) { + val contentResolver = activity.contentResolver + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } +} + + + + + + + + + + + + + + + + + +/* class GalleryProvider(activity: ImagePickerActivity) : BaseProvider(activity) { @@ -33,28 +121,34 @@ class GalleryProvider(activity: ImagePickerActivity) : mimeTypes = bundle.getStringArray(ImagePicker.EXTRA_MIME_TYPES) ?: emptyArray() } - /** + */ +/** * Start Gallery Capture Intent - */ + *//* + fun startIntent() { startGalleryIntent() } - /** + */ +/** * Start Gallery Intent - */ + *//* + private fun startGalleryIntent() { val galleryIntent = IntentUtils.getGalleryIntent(activity, mimeTypes) activity.startActivityForResult(galleryIntent, GALLERY_INTENT_REQ_CODE) } - /** + */ +/** * Handle Gallery Intent Activity Result * * @param requestCode It must be {@link GalleryProvider#GALLERY_INTENT_REQ_CODE} * @param resultCode For success it should be {@link Activity#RESULT_OK} * @param data Result Intent - */ + *//* + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == GALLERY_INTENT_REQ_CODE) { if (resultCode == Activity.RESULT_OK) { @@ -65,9 +159,11 @@ class GalleryProvider(activity: ImagePickerActivity) : } } - /** + */ +/** * This method will be called when final result fot this provider is enabled. - */ + *//* + private fun handleResult(data: Intent?) { val uri = data?.data if (uri != null) { @@ -78,11 +174,14 @@ class GalleryProvider(activity: ImagePickerActivity) : } } - /** + */ +/** * Take a persistable URI permission grant that has been offered. Once * taken, the permission grant will be remembered across device reboots. - */ + *//* + private fun takePersistableUriPermission(uri: Uri) { contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) } } +*/ diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/DialogHelper.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/DialogHelper.kt index 416a3fa6..bc27c5af 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/DialogHelper.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/DialogHelper.kt @@ -1,9 +1,14 @@ package com.github.dhaval2404.imagepicker.util +import android.app.Activity import android.content.Context +import android.content.pm.PackageManager +import android.os.Build import android.view.LayoutInflater import android.view.View +import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat import com.github.dhaval2404.imagepicker.R import com.github.dhaval2404.imagepicker.constant.ImageProvider import com.github.dhaval2404.imagepicker.listener.DismissListener @@ -18,14 +23,18 @@ import com.github.dhaval2404.imagepicker.listener.ResultListener */ internal object DialogHelper { + const val PERMISSION_CAMERA_REQUEST_CODE = 101 + const val PERMISSION_GALLERY_REQUEST_CODE = 102 + /** - * Show Image Provide Picker Dialog. This will streamline the code to pick/capture image + * Show Image Provider Picker Dialog. This streamlines the code to pick/capture images * */ fun showChooseAppDialog( context: Context, listener: ResultListener, - dismissListener: DismissListener? + dismissListener: DismissListener?, + activity: Activity // Add an Activity reference to handle permissions ) { val layoutInflater = LayoutInflater.from(context) val customView = layoutInflater.inflate(R.layout.dialog_choose_app, null) @@ -46,14 +55,36 @@ internal object DialogHelper { // Handle Camera option click customView.findViewById(R.id.lytCameraPick).setOnClickListener { - listener.onResult(ImageProvider.CAMERA) - dialog.dismiss() + if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + activity, + arrayOf(android.Manifest.permission.CAMERA), + PERMISSION_CAMERA_REQUEST_CODE + ) + } else { + listener.onResult(ImageProvider.CAMERA) + dialog.dismiss() + } } // Handle Gallery option click customView.findViewById(R.id.lytGalleryPick).setOnClickListener { - listener.onResult(ImageProvider.GALLERY) - dialog.dismiss() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // For Android 13 and above, modify this as necessary + Toast.makeText(context, "Limited gallery access in Android 13+", Toast.LENGTH_LONG).show() + } else { + // Request permission before returning the gallery option + if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + activity, + arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSION_GALLERY_REQUEST_CODE + ) + } else { + listener.onResult(ImageProvider.GALLERY) + dialog.dismiss() + } + } } } -} +} \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 694b73ec..28511046 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -7,11 +7,11 @@ plugins { apply from: "../ktlint.gradle" android { - compileSdkVersion 30 + compileSdkVersion 33 defaultConfig { applicationId "com.github.dhaval2404.imagepicker.sample" minSdkVersion 19 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 16 versionName "2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 57062e84..5f20de62 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ + android:theme="@style/AppTheme.NoActionBar" + android:exported="true"> @@ -25,7 +26,8 @@ android:name=".SampleActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar" - tools:ignore="MissingClass"> + tools:ignore="MissingClass" + android:exported="true">