diff --git a/README.md b/README.md
index f8e891c..354ad77 100644
--- a/README.md
+++ b/README.md
@@ -9,64 +9,100 @@ Android library that can be used as quick solution to ImagePicker feature implem
- Camera photo picker
- Gallery single photo picker
- Gallery multiple photo picker
+- Custom gallery picker, supports multiple selection (for old non-AOSP Android ROMs that does not support multiple selection intent)
## Implementation
1. In project-level gradle add new maven repository:
-
+```groovy
allprojects {
repositories {
- ...
maven { url 'https://jitpack.io' }
}
}
-
+```
2. In app-level gradle add new implementation:
-
+```groovy
dependencies {
- implementation 'com.github.ShiftHackZ:ImagePicker:v1.0'
+ implementation 'com.github.ShiftHackZ:ImagePicker:v2.0'
}
-
-
-3. In order to receive images, implement ImagePickerCallback in your Fragment/Activity or as object:
-
-
-public class MainActivity extends AppCompatActivity implements ImagePickerCallback {
- ...
- @Override
- public void onImagesSelected(List files) {
- // Do whatever you want with list of files
- for (int i = 0; i < files.size(); i++) {
- // As example you can process each file inside for-cycle
- }
- }
- ...
+```
+
+3. Create file `provider_path.xml` in `res/xml` folder:
+
+```xml
+
+
+
+
+
+```
+
+4. In your `AndroidManifest.xml` add the file provider inside the `
+
+
+```
+
+5. In order to receive images, implement `ImagePickerCallback` in your Fragment/Activity or as object:
+
+```kotlin
+class MainActivity : AppCompatActivity(), ImagePickerCallback {
+
+ override fun onImagePickerResult(result: PickedResult) {
+ when (result) {
+ PickedResult.Empty -> {
+ // No file was selected, noting to do
+ }
+ is PickedResult.Error -> {
+ val throwable = result.throwable
+ // Some error happened, handle this throwable
+ }
+ is PickedResult.Multiple -> {
+ val pickedImages = result.images
+ val files = pickedImages.map { it.file }
+ // Selected multiple images, do whatever you want with files
+ }
+ is PickedResult.Single -> {
+ val pickedImage = result.image
+ val file = pickedImage.file
+ // Selected one image, do whatever you want with file
+ }
+ }
+ }
}
-
+```
-4. Create an instance of ImagePicker using ImagePicker.Builder(), which require 2 mandatory params: current Activity and ImagePickerCallback:
+6. Create an instance of ImagePicker using ImagePicker.Builder(), which require 2 mandatory params: current Activity and ImagePickerCallback:
-
-ImagePicker imagePicker = new ImagePicker.Builder(activity, callback)
- .useGallery(true)
- .useCamera(true)
- .useMultiSelection(true)
- .build();
-
+```kotlin
+val imagePicker = ImagePicker.Builder(this.packageName + ".provider", this)
+ .useGallery(true) // Use gallery picker if true
+ .useCamera(true) // Use camera picker if true
+ .multipleSelection() // Allow multiple selection in gallery picker
+ .minimumSelectionCount(2) // Defines min count of GallerySelector.CUSTOM multiple selection gallery picker
+ .maximumSelectionCount(3) // Defines max count of GallerySelector.CUSTOM multiple selection gallery picker
+ .gallerySelector(GallerySelector.CUSTOM) // Available values: GallerySelector.NATIVE, GallerySelector.CUSTOM
+ .build()
+```
-List of Builder methods:
-- useGallery(boolean) // Pass 'true' if you want to enable gallery picker
-- useMultiSelection(boolean) // Pass 'true' if you need gallery picker to support multiple photo selection
-- useCamera(boolean) // Pass 'true' if you want to enable camera picker
-5. Finally, launch your ImagePicker:
+7. Finally, launch your ImagePicker:
-
-imagePicker.start();
-
+```kotlin
+imagePicker.launch(context)
+```
## Credits
- Developer: Dmitriy Moroz
diff --git a/app/build.gradle b/app/build.gradle
index 1301d75..8f6de73 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,15 +1,16 @@
plugins {
id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
}
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ compileSdkVersion 33
defaultConfig {
applicationId "com.shz.imagepicker.imagepickerapp"
minSdkVersion 16
- targetSdkVersion 31
+ targetSdkVersion 33
versionCode 1
versionName "1.0"
@@ -21,6 +22,9 @@ android {
minifyEnabled false
}
}
+ buildFeatures {
+ dataBinding true
+ }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -29,9 +33,9 @@ android {
dependencies {
implementation project(':imagepicker')
- implementation 'com.github.bumptech.glide:glide:4.12.0'
- annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
- implementation 'androidx.appcompat:appcompat:1.3.1'
- implementation 'com.google.android.material:material:1.4.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
+ implementation 'com.github.bumptech.glide:glide:4.13.2'
+ kapt 'com.github.bumptech.glide:compiler:4.13.2'
+ implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'com.google.android.material:material:1.6.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bd47d86..0d57f76 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,14 @@
package="com.shz.imagepicker.imagepickerapp">
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/shz/imagepicker/imagepickerapp/ImagesDemoAdapter.kt b/app/src/main/java/com/shz/imagepicker/imagepickerapp/ImagesDemoAdapter.kt
new file mode 100644
index 0000000..502ae05
--- /dev/null
+++ b/app/src/main/java/com/shz/imagepicker/imagepickerapp/ImagesDemoAdapter.kt
@@ -0,0 +1,47 @@
+package com.shz.imagepicker.imagepickerapp
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.shz.imagepicker.imagepicker.model.PickedImage
+import com.shz.imagepicker.imagepickerapp.databinding.ItemDemoImageBinding
+
+class ImagesDemoAdapter : ListAdapter(diff) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
+ ItemDemoImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ )
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class ViewHolder(
+ private val binding: ItemDemoImageBinding,
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: PickedImage) {
+ Glide.with(binding.image)
+ .load(item.file)
+ .centerCrop()
+ .into(binding.image)
+ }
+ }
+
+ companion object {
+ private val diff = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: PickedImage,
+ newItem: PickedImage
+ ): Boolean = oldItem == newItem
+
+ override fun areContentsTheSame(
+ oldItem: PickedImage,
+ newItem: PickedImage
+ ): Boolean = oldItem == newItem
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/shz/imagepicker/imagepickerapp/MainActivity.java b/app/src/main/java/com/shz/imagepicker/imagepickerapp/MainActivity.java
deleted file mode 100644
index 0c560ce..0000000
--- a/app/src/main/java/com/shz/imagepicker/imagepickerapp/MainActivity.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.shz.imagepicker.imagepickerapp;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.bumptech.glide.Glide;
-import com.shz.imagepicker.imagepicker.ImagePicker;
-import com.shz.imagepicker.imagepicker.ImagePickerCallback;
-
-import java.io.File;
-import java.util.List;
-
-public class MainActivity extends AppCompatActivity implements ImagePickerCallback {
-
- private ImageView mResultImage;
- private LinearLayout mResultContainer;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mResultImage = (ImageView) findViewById(R.id.iv_result);
- mResultContainer = (LinearLayout) findViewById(R.id.ll_container);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- }
-
- @Override
- public void onImagesSelected(List files) {
- if (files.size() > 0) {
- if (files.size() == 1) {
- Glide.with(this)
- .load(files.get(0))
- .into(mResultImage);
-
- mResultContainer.setVisibility(View.GONE);
- mResultImage.setVisibility(View.VISIBLE);
- } else {
- mResultContainer.removeAllViews();
- for (int i = 0; i < files.size(); i++) {
- ImageView imageView = new ImageView(this);
-
- Glide.with(this)
- .load(files.get(i))
- .into(imageView);
-
- mResultContainer.addView(imageView);
- imageView.getLayoutParams().height = 300;
- imageView.requestLayout();
- }
- mResultContainer.setVisibility(View.VISIBLE);
- mResultImage.setVisibility(View.GONE);
- }
- }
- }
-
- public void onCameraClick(View v) {
- new ImagePicker.Builder(this, this)
- .useCamera(true)
- .build()
- .start();
- }
-
- public void onGalleryMultipleClick(View v) {
- new ImagePicker.Builder(this, this)
- .useGallery(true)
- .useMultiSelection(true)
- .build()
- .start();
- }
-
- public void onGalleryClick(View v) {
- new ImagePicker.Builder(this, this)
- .useGallery(true)
- .build()
- .start();
- }
-
- public void onGenericMultipleClick(View v) {
- new ImagePicker.Builder(this, this)
- .useGallery(true)
- .useCamera(true)
- .useMultiSelection(true)
- .build()
- .start();
- }
-
- public void onGenericClick(View v) {
- new ImagePicker.Builder(this, this)
- .useGallery(true)
- .useCamera(true)
- .build()
- .start();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/shz/imagepicker/imagepickerapp/MainActivity.kt b/app/src/main/java/com/shz/imagepicker/imagepickerapp/MainActivity.kt
new file mode 100644
index 0000000..38ad354
--- /dev/null
+++ b/app/src/main/java/com/shz/imagepicker/imagepickerapp/MainActivity.kt
@@ -0,0 +1,106 @@
+package com.shz.imagepicker.imagepickerapp
+
+import android.os.Bundle
+import android.util.Log
+import android.widget.ImageView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import androidx.recyclerview.widget.GridLayoutManager
+import com.bumptech.glide.Glide
+import com.shz.imagepicker.imagepicker.ImagePicker
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+import com.shz.imagepicker.imagepicker.ImagePickerLoadDelegate
+import com.shz.imagepicker.imagepicker.model.GalleryPicker
+import com.shz.imagepicker.imagepicker.model.PickedResult
+import com.shz.imagepicker.imagepickerapp.databinding.ActivityMainBinding
+import java.io.File
+
+class MainActivity : AppCompatActivity(), ImagePickerCallback {
+
+ private val imagePicker: ImagePicker.Builder
+ get() = ImagePicker.Builder(this.packageName + ".provider", this)
+
+ private lateinit var binding: ActivityMainBinding
+ private lateinit var adapter: ImagesDemoAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
+
+ adapter = ImagesDemoAdapter()
+
+ binding.rv.adapter = adapter
+ binding.rv.layoutManager = GridLayoutManager(this, 2)
+
+ binding.btnCamera.setOnClickListener {
+ imagePicker
+ .autoRotate(binding.switchAutoRotate.isChecked)
+ .useCamera()
+ .build()
+ .launch(this)
+ }
+ binding.btnGallery.setOnClickListener {
+ imagePicker
+ .autoRotate(binding.switchAutoRotate.isChecked)
+ .useGallery()
+// .multipleSelection(true)
+ .build()
+ .launch(this)
+ }
+ binding.btnGalCustomSingle.setOnClickListener {
+ imagePicker
+ .autoRotate(binding.switchAutoRotate.isChecked)
+ .useGallery()
+ .galleryPicker(GalleryPicker.CUSTOM)
+ .loadDelegate { imageView, file ->
+ Glide.with(this)
+ .load(file)
+ .into(imageView)
+ }
+ .build()
+ .launch(this)
+ }
+ binding.btnGalCustomMultiple.setOnClickListener {
+ imagePicker
+ .autoRotate(binding.switchAutoRotate.isChecked)
+ .useGallery()
+ .multipleSelection()
+ .galleryPicker(GalleryPicker.CUSTOM)
+ .loadDelegate { imageView, file ->
+ Glide.with(this)
+ .load(file)
+ .into(imageView)
+ }
+ .build()
+ .launch(this)
+ }
+ binding.btnDialog.setOnClickListener {
+ imagePicker
+ .autoRotate(binding.switchAutoRotate.isChecked)
+ .useGallery()
+ .useCamera()
+ .multipleSelection()
+ .galleryPicker(GalleryPicker.NATIVE)
+ .build()
+ .launch(this)
+ }
+ }
+
+ override fun onImagePickerResult(result: PickedResult) {
+ Log.d("MainActivity", "result: $result")
+ when (result) {
+ PickedResult.Empty -> {
+ adapter.submitList(emptyList())
+ }
+ is PickedResult.Error -> {
+ result.throwable.printStackTrace()
+ }
+ is PickedResult.Multiple -> {
+ adapter.submitList(result.images)
+ }
+ is PickedResult.Single -> {
+ adapter.submitList(listOf(result.image))
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 0000000..a5d172e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_camera.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_custom.xml b/app/src/main/res/drawable/ic_custom.xml
new file mode 100644
index 0000000..f7ca6f7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_custom.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml
new file mode 100644
index 0000000..3df2119
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gallery.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml
new file mode 100644
index 0000000..bae7be7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_list.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f2d5e70..90c1d27 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,114 +1,139 @@
-
+ tools:ignore="ContentDescription">
-
+ android:layout_height="match_parent">
-
-
-
+
-
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent">
+
-
+
-
+
-
+
-
+
+
+
-
+
+
+
-
\ No newline at end of file
+
+
+
+
diff --git a/app/src/main/res/layout/item_demo_image.xml b/app/src/main/res/layout/item_demo_image.xml
new file mode 100644
index 0000000..0eb8b33
--- /dev/null
+++ b/app/src/main/res/layout/item_demo_image.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/imagepicker/src/main/res/xml/provider_path.xml b/app/src/main/res/xml/provider_path.xml
similarity index 100%
rename from imagepicker/src/main/res/xml/provider_path.xml
rename to app/src/main/res/xml/provider_path.xml
diff --git a/build.gradle b/build.gradle
index e61d8af..2bc9701 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,24 +1,22 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.0.3'
- classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.20.0"
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
+ classpath 'com.android.tools.build:gradle:7.3.1'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10'
+ //classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.20.0"
}
}
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
-}
\ No newline at end of file
+}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d956395..6eeef91 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Feb 15 19:49:44 EET 2021
+#Tue Dec 13 18:15:08 EET 2022
distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
diff --git a/imagepicker/build.gradle b/imagepicker/build.gradle
index 9d27d86..908e286 100644
--- a/imagepicker/build.gradle
+++ b/imagepicker/build.gradle
@@ -1,21 +1,20 @@
plugins {
id 'com.android.library'
+ id 'kotlin-android'
id 'maven-publish'
-
}
def libraryGroupId = 'com.shz.imagepicker'
def libraryArtifactId = 'imagepicker'
-def libraryVersion = '1.0.1'
+def libraryVersion = '2.0'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ compileSdkVersion 33
defaultConfig {
minSdkVersion 16
- targetSdkVersion 31
+ targetSdkVersion 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -33,7 +32,9 @@ android {
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation "androidx.appcompat:appcompat:1.5.1"
+ implementation "androidx.recyclerview:recyclerview:1.2.1"
+ implementation "androidx.exifinterface:exifinterface:1.3.5"
}
publishing {
diff --git a/imagepicker/src/main/AndroidManifest.xml b/imagepicker/src/main/AndroidManifest.xml
index 7fd6b56..1fe0a61 100644
--- a/imagepicker/src/main/AndroidManifest.xml
+++ b/imagepicker/src/main/AndroidManifest.xml
@@ -8,23 +8,19 @@
-
+ android:name=".activity.nativegallery.GalleryMultiPickerNativeActivity"
+ android:theme="@style/ImagePicker.Theme.Transparent" />
-
-
-
-
+ android:name=".activity.nativegallery.GallerySinglePickerActivity"
+ android:theme="@style/ImagePicker.Theme.Transparent" />
+
+
+
-
-
\ No newline at end of file
+
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/CameraPickerActivity.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/CameraPickerActivity.java
deleted file mode 100644
index c00bb9f..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/CameraPickerActivity.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
-
-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 android.provider.MediaStore;
-import android.util.Log;
-
-import com.shz.imagepicker.imagepicker.util.FileOrientationHandler;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-
-public class CameraPickerActivity extends Activity {
-
- public static ImagePickerCallback mCallback;
-
- private static final int PERMISSION_REQUEST_CAMERA = 54560;
- private static final int IMAGE_REQUEST_CAMERA = 54561;
-
- private static final String FILE_PROVIDER_PREFIX = ".provider";
-
- private String mFilename;
- private FileOrientationHandler fileOrientationHandler = new FileOrientationHandler();
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == PERMISSION_REQUEST_CAMERA) {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- startCameraPicker();
- } else {
- finish();
- }
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (resultCode == Activity.RESULT_OK && requestCode == IMAGE_REQUEST_CAMERA) {
- ArrayList files = new ArrayList<>();
- Uri uri = ImagePath.getCaptureImageResultUri(this, data, mFilename);
- Uri uriFile = ImagePath.getNormalizedUri(this, uri);
- File file = new File(uriFile.getPath());
-
- try{
- fileOrientationHandler.rotateAndReWriteImageFile(file);
- }catch (IOException e){
- Log.d(this.getClass().getSimpleName(),"Error rotation image "+e.getLocalizedMessage());
- }
-
- files.add(file);
- mCallback.onImagesSelected(files);
- }
- finish();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- checkCameraPermission();
- }
-
- private void checkCameraPermission() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) {
- ActivityCompat.requestPermissions(
- this,
- new String[] { Manifest.permission.CAMERA },
- PERMISSION_REQUEST_CAMERA
- );
- } else {
- startCameraPicker();
- }
- }
-
- private void startCameraPicker() {
- Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
- mFilename = String.valueOf(System.nanoTime());
- Uri uri = ImagePath.getCaptureImageOutputUri(this, mFilename);
- if (uri != null) {
- File file = new File(uri.getPath());
- if (Build.VERSION.SDK_INT >= 24) {
- cameraIntent.putExtra(
- MediaStore.EXTRA_OUTPUT,
- FileProvider.getUriForFile(
- this,
- this.getPackageName() + FILE_PROVIDER_PREFIX,
- file
- )
- );
- cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- } else {
- cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
- }
- startActivityForResult(cameraIntent, IMAGE_REQUEST_CAMERA);
- }
- }
-
-
-
-}
\ No newline at end of file
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/GalleryMultiPickerActivity.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/GalleryMultiPickerActivity.java
deleted file mode 100644
index 26ca1f7..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/GalleryMultiPickerActivity.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.ClipData;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.provider.MediaStore;
-import android.util.Log;
-
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-import com.shz.imagepicker.imagepicker.util.FileOrientationHandler;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URI;
-import java.util.ArrayList;
-
-public class GalleryMultiPickerActivity extends Activity {
-
- public static ImagePickerCallback mCallback;
-
- private static final int PERMISSION_REQUEST_READ_STORAGE = 54564;
- private static final int IMAGE_REQUEST_GALLERY = 1;
-
- private static final String GALLERY_IMAGE_MIME = "image/jpeg";
- private FileOrientationHandler fileOrientationHandler = new FileOrientationHandler();
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == PERMISSION_REQUEST_READ_STORAGE) {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- startGalleryPicker();
- } else {
- finish();
- }
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- checkGalleryPermission();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (resultCode == Activity.RESULT_OK && requestCode == IMAGE_REQUEST_GALLERY) {
-
-
- if (data != null) {
- ArrayList files = new ArrayList<>();
-
-
- if (data.getExtras() != null) {
- ArrayList fileUris = data.getExtras().getParcelableArrayList("selectedItems");
-
- for (Parcelable uri : fileUris) {
- if (uri instanceof Uri) {
- files.add(new File(getRealPathFromURI((Uri) uri)));
- }
-
- }
-
-
- }
-
- if (data.getData() != null) {
- Log.d("MultiPicker", "data: " + data.getData().toString());
- String filename = ImagePath.getImagePathFromInputStreamUri(this, data.getData());
- File file = new File(filename);
- if (file.exists()) {
- files.add(file);
- }
- }
-
- if (data.getClipData() != null) {
- ClipData clipData = data.getClipData();
-
- for (int i = 0; i < clipData.getItemCount(); i++) {
- ClipData.Item item = clipData.getItemAt(i);
- String filename = ImagePath.getImagePathFromInputStreamUri(this, item.getUri());
- File file = new File(filename);
- if (file.exists()) {
- files.add(file);
- }
- }
- }
-
-
- try {
- for (File file :
- files) {
- fileOrientationHandler.rotateAndReWriteImageFile(file);
- }
- } catch (IOException e) {
- Log.d(this.getClass().getSimpleName(), "Error rotating image " + e.getLocalizedMessage());
- }
-
-
- mCallback.onImagesSelected(files);
- }
- }
- finish();
- }
-
- public String getRealPathFromURI(Uri contentUri) {
- String path = null;
- String[] proj = {MediaStore.MediaColumns.DATA};
- Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
- if (cursor.moveToFirst()) {
- int column_index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
- path = cursor.getString(column_index);
- }
- cursor.close();
- return path;
- }
-
- private void checkGalleryPermission() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED
- || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
- ActivityCompat.requestPermissions(
- this,
- new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
- PERMISSION_REQUEST_READ_STORAGE
- );
- } else {
- startGalleryPicker();
- }
- }
-
- private void startGalleryPicker() {
- Intent galleryIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
- galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
- }
- galleryIntent.putExtra("multi-pick", true);
-
-
- //galleryIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, GALLERY_IMAGE_MIME);
- startActivityForResult(galleryIntent, IMAGE_REQUEST_GALLERY);
-
- }
-}
\ No newline at end of file
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/GallerySinglePickerActivity.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/GallerySinglePickerActivity.java
deleted file mode 100644
index 475312b..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/GallerySinglePickerActivity.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import android.util.Log;
-
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-import com.shz.imagepicker.imagepicker.util.FileOrientationHandler;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-
-public class GallerySinglePickerActivity extends Activity {
-
- public static ImagePickerCallback mCallback;
-
- private static final int PERMISSION_REQUEST_READ_STORAGE = 54562;
- private static final int IMAGE_REQUEST_GALLERY = 54563;
-
- private static final String GALLERY_IMAGE_MIME = "image/jpeg";
- private FileOrientationHandler fileOrientationHandler = new FileOrientationHandler();
-
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- if (requestCode == PERMISSION_REQUEST_READ_STORAGE) {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- startGalleryPicker();
- } else {
- finish();
- }
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (resultCode == Activity.RESULT_OK && requestCode == IMAGE_REQUEST_GALLERY) {
- if (data != null && data.getData() != null) {
- ArrayList files = new ArrayList<>();
- String filename = ImagePath.getImagePathFromInputStreamUri(this, data.getData());
- Log.d("SinglePicker", "data: " + data.getData().toString());
- File file = new File(filename);
- if (file.exists()) {
-
- try{
- fileOrientationHandler.rotateAndReWriteImageFile(file);
- }catch (IOException e){
- Log.d(this.getClass().getSimpleName(),"Error rotation image "+e.getLocalizedMessage());
- }
-
- files.add(file);
- mCallback.onImagesSelected(files);
- }
- }
- }
- finish();
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- checkGalleryPermission();
- }
-
- private void checkGalleryPermission() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED
- || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
- ActivityCompat.requestPermissions(
- this,
- new String[] { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE },
- PERMISSION_REQUEST_READ_STORAGE
- );
- } else {
- startGalleryPicker();
- }
- }
-
- private void startGalleryPicker() {
- Intent galleryIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
- galleryIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, GALLERY_IMAGE_MIME);
- startActivityForResult(galleryIntent, IMAGE_REQUEST_GALLERY);
- }
-}
\ No newline at end of file
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePath.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePath.java
deleted file mode 100644
index c64511b..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePath.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Calendar;
-
-public class ImagePath {
-
- private static final String FILE_EXTENSION_JPEG = ".jpeg";
- private static final String FILE_EXTENSION_JPG = ".jpg";
- private static final String FILE_SCHEMA_CONTENT = "content:";
-
- public static Uri getCaptureImageOutputUri(Context context, String filename) {
- Uri outputUri = null;
- File imageStore = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
- if (imageStore != null) {
- outputUri = Uri.fromFile(new File(imageStore.getPath(), filename + FILE_EXTENSION_JPEG));
- }
- return outputUri;
- }
-
- public static Uri getCaptureImageResultUri(Context context, Intent data, String filename) {
- boolean isCamera = true;
- if (data != null && data.getData() != null) {
- String action = data.getAction();
- isCamera = action != null && action == MediaStore.ACTION_IMAGE_CAPTURE;
- }
- if (isCamera || data.getData() == null) {
- return getCaptureImageOutputUri(context, filename);
- } else {
- return data.getData();
- }
- }
-
- public static Uri getNormalizedUri(Context context, Uri uri) {
- if (uri != null && uri.toString().contains(FILE_SCHEMA_CONTENT)) {
- return Uri.fromFile(getPath(context, uri, MediaStore.Images.Media.DATA));
- } else {
- return uri;
- }
- }
-
- public static String getImagePathFromInputStreamUri(Context context, Uri uri) {
- InputStream inputStream = null;
- String filePath = null;
-
- if (uri.getAuthority() != null) {
- try {
- inputStream = context.getContentResolver().openInputStream(uri);
- File photoFile = createTempFileFrom(context, inputStream);
- filePath = photoFile.getPath();
- } catch (FileNotFoundException ex) {
- ex.printStackTrace();
- } catch (Exception ex) {
- ex.printStackTrace();
- } finally {
- try {
- inputStream.close();
- } catch (IOException ex) {
- ex.printStackTrace();
- }
- }
- }
- return filePath;
- }
-
- private static File createTempFileFrom(Context context, InputStream inputStream) throws IOException {
- File targetFile = null;
-
- if (inputStream != null) {
- int read;
- byte[] buffer = new byte[8 * 1024];
-
- targetFile = createTempFile(context, null);
- FileOutputStream outputStream = new FileOutputStream(targetFile);
-
- while (true) {
- read = inputStream.read(buffer);
- if (read == -1) {
- break;
- }
- outputStream.write(buffer, 0, read);
- }
- outputStream.flush();
-
- try {
- outputStream.close();
- } catch (IOException ex) {
- ex.printStackTrace();
- }
- }
- return targetFile;
- }
-
- private static File createTempFile(Context context, String filePath) {
- String tempFilename;
- if (filePath == null) {
- tempFilename = String.valueOf(Calendar.getInstance().getTimeInMillis());
- } else {
- tempFilename = filePath;
- }
- return new File(context.getExternalCacheDir(), tempFilename + FILE_EXTENSION_JPG);
- }
-
- private static File getPath(Context context, Uri uri, String column) {
- String[] columns = { column };
- Cursor cursor = context.getContentResolver().query(uri, columns, null, null, null);
- int columnIndex = cursor.getColumnIndexOrThrow(column);
- cursor.moveToFirst();
- String path = cursor.getString(columnIndex);
- cursor.close();
- return new File(path);
- }
-}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePicker.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePicker.java
deleted file mode 100644
index c03ac0d..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePicker.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import android.app.Activity;
-import android.content.Intent;
-
-public class ImagePicker {
-
- private static final String IMAGE_PICKER_DIALOG = "ImagePicker";
-
- private Activity mActivity;
- private ImagePickerCallback mCallback;
- private boolean mIsUsingCamera = false;
- private boolean mIsUsingGallery = false;
- private boolean mIsUsingGalleryMultiSelect = false;
-
- private ImagePicker() { }
-
- public static class Builder {
- private final ImagePicker mPicker;
-
- public Builder(Activity activity, ImagePickerCallback callback) {
- mPicker = new ImagePicker();
- mPicker.mActivity = activity;
- mPicker.mCallback = callback;
- }
-
- public Builder useCamera(boolean isUsingCamera) {
- mPicker.mIsUsingCamera = isUsingCamera;
- return this;
- }
-
- public Builder useGallery(boolean isUsingGallery) {
- mPicker.mIsUsingGallery = isUsingGallery;
- return this;
- }
-
- public Builder useMultiSelection(boolean isUsingMultiSelection) {
- mPicker.mIsUsingGalleryMultiSelect = isUsingMultiSelection;
- return this;
- }
-
- public ImagePicker build() {
- return mPicker;
- }
- }
-
- public void start() {
- if (mIsUsingCamera && !mIsUsingGallery) {
- startCamera();
- } else if (mIsUsingGallery && !mIsUsingGalleryMultiSelect && !mIsUsingCamera) {
- startGallerySingle();
- } else if (mIsUsingGallery && mIsUsingGalleryMultiSelect && !mIsUsingCamera) {
- startGalleryMultiple();
- } else if (mIsUsingGallery && !mIsUsingGalleryMultiSelect && mIsUsingCamera) {
- ImagePickerDialog dialog = new ImagePickerDialog(new ImagePickerDialog.ImagePickerDialogListener() {
- @Override
- public void onCamera() {
- startCamera();
- }
-
- @Override
- public void onGallery() {
- startGallerySingle();
- }
- });
- dialog.show(mActivity.getFragmentManager(), IMAGE_PICKER_DIALOG);
- } else if (mIsUsingGallery && mIsUsingGalleryMultiSelect && mIsUsingCamera) {
- ImagePickerDialog dialog = new ImagePickerDialog(new ImagePickerDialog.ImagePickerDialogListener() {
- @Override
- public void onCamera() {
- startCamera();
- }
-
- @Override
- public void onGallery() {
- startGalleryMultiple();
- }
- });
- dialog.show(mActivity.getFragmentManager(), IMAGE_PICKER_DIALOG);
- }
- }
-
- private void startCamera() {
- CameraPickerActivity.mCallback = mCallback;
- mActivity.startActivity(new Intent(mActivity, CameraPickerActivity.class));
- }
-
- private void startGallerySingle() {
- GallerySinglePickerActivity.mCallback = mCallback;
- mActivity.startActivity(new Intent(mActivity, GallerySinglePickerActivity.class));
- }
-
- private void startGalleryMultiple() {
- GalleryMultiPickerActivity.mCallback = mCallback;
- mActivity.startActivity(new Intent(mActivity, GalleryMultiPickerActivity.class));
- }
-}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePicker.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePicker.kt
new file mode 100644
index 0000000..7581a2e
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePicker.kt
@@ -0,0 +1,267 @@
+@file:Suppress("unused", "NOTHING_TO_INLINE")
+
+package com.shz.imagepicker.imagepicker
+
+import android.content.Context
+import android.util.Log
+import android.widget.ImageView
+import com.shz.imagepicker.imagepicker.exception.NothingToLaunchException
+import com.shz.imagepicker.imagepicker.model.GalleryPicker
+import com.shz.imagepicker.imagepicker.model.PickedResult
+import java.io.File
+
+/**
+ * Main **ImagePicker** SDK class.
+ * Contains configuration models and main business logic of.
+ *
+ * @see ImagePicker.Builder
+ *
+ * @author ShiftHackZ (Dmitriy Moroz)
+ */
+class ImagePicker private constructor(
+ private val authority: String,
+ private val callback: ImagePickerCallback,
+ private val useCamera: Boolean,
+ private val useGallery: Boolean,
+ private val autoRotate: Boolean,
+ private val multipleSelection: Boolean,
+ private val minimumSelectionCount: Int,
+ private val maximumSelectionCount: Int,
+ private val galleryPicker: GalleryPicker,
+ private val loadDelegate: ImagePickerLoadDelegate,
+) {
+
+ /**
+ * Checks for access Camera or Media permissions, then
+ * launches **ImagePicker** according to defined configuration.
+ *
+ * Possible use cases:
+ * - When only Builder.useCamera(true) launches Camera Picker;
+ * - When only Builder.useGallery(true) launches Native or Custom Gallery Picker;
+ * - When both Builder.useCamera(true) and Builder.useGallery(true) interactive dialog will be launched;
+ * - Otherwise, will deliver [NothingToLaunchException] as [PickedResult.Error] to [ImagePickerCallback].
+ *
+ * @param context it is recommended to pass Activity [Context] here.
+ *
+ * @see ImagePicker.Builder
+ */
+ fun launch(context: Context) = with(ImagePickerLauncher(context)) {
+ when {
+ useCamera && !useGallery -> launchCameraPicker(authority, callback)
+ !useCamera && useGallery -> launchGalleryPicker(
+ callback = callback,
+ delegate = loadDelegate,
+ multipleSelection = multipleSelection,
+ autoRotate = autoRotate,
+ minimum = minimumSelectionCount,
+ maximum = maximumSelectionCount,
+ galleryPicker = galleryPicker,
+ )
+ useCamera && useGallery -> launchDialog(
+ authority = authority,
+ callback = callback,
+ delegate = loadDelegate,
+ multipleSelection = multipleSelection,
+ autoRotate = autoRotate,
+ minimum = minimumSelectionCount,
+ maximum = maximumSelectionCount,
+ galleryPicker = galleryPicker,
+ )
+ else -> deliverThrowable(callback, NothingToLaunchException())
+ }
+ }
+
+ /**
+ * Builder class that allows to configure and build [ImagePicker] instance.
+ *
+ * @param authority string of your FileProvider, defined in AndroidManifest.xml
+ * @param callback instance of [ImagePickerCallback] that will receive pick-operation result as [PickedResult]
+ *
+ * @see ImagePicker
+ */
+ class Builder(
+ private val authority: String,
+ private val callback: ImagePickerCallback,
+ ) {
+ private var useCamera: Boolean = false
+ private var useGallery: Boolean = false
+ private var autoRotate: Boolean = false
+ private var multipleSelection: Boolean = false
+ private var minimumSelectionCount: Int = 1
+ private var maximumSelectionCount: Int = Int.MAX_VALUE
+ private var galleryPicker: GalleryPicker = GalleryPicker.NATIVE
+ private var loadDelegate: ImagePickerLoadDelegate = defaultImagePickerLoadDelegate
+
+ /**
+ * Must be called after desired parameters were passed to receive [ImagePicker] instance.
+ *
+ * @see ImagePicker
+ *
+ * @return instance of [ImagePicker]
+ * @throws IllegalArgumentException on incorrect minimumSelectionCount, maximumSelectionCount values.
+ */
+ fun build(): ImagePicker {
+ require(minimumSelectionCount >= 1) {
+ "Parameter minimumSelectionCount must be bigger or equal 1"
+ }
+ require(maximumSelectionCount > minimumSelectionCount) {
+ "Parameter maximumSelectionCount must be bigger than minimumSelectionCount"
+ }
+ return ImagePicker(
+ authority = authority,
+ callback = callback,
+ useCamera = useCamera,
+ useGallery = useGallery,
+ autoRotate = autoRotate,
+ multipleSelection = multipleSelection,
+ minimumSelectionCount = minimumSelectionCount,
+ maximumSelectionCount = maximumSelectionCount,
+ galleryPicker = galleryPicker,
+ loadDelegate = loadDelegate,
+ )
+ }
+
+ /**
+ * Configures if Camera Picker is allowed to launch.
+ *
+ * @param useCamera if true will allow launch of Camera Picker.
+ *
+ * @return [ImagePicker.Builder]
+ */
+ fun useCamera(useCamera: Boolean = true) = apply {
+ this.useCamera = useCamera
+ }
+
+ /**
+ * Configures if Gallery Picker is allowed to launch.
+ *
+ * @param useGallery if true will allow launch of Gallery Picker.
+ *
+ * @return [ImagePicker.Builder]
+ */
+ fun useGallery(useGallery: Boolean = true) = apply {
+ this.useGallery = useGallery
+ }
+
+ /**
+ * Configures if Gallery Picker is allowed to auto rotate images with wrong EXIF rotation.
+ * If parameter is set to true, ImagePicker will re-write the original file with rotated one.
+ *
+ * @param autoRotate if true will allow auto rotate on any Gallery Picker.
+ *
+ * @return [ImagePicker.Builder]
+ */
+ fun autoRotate(autoRotate: Boolean = false) = apply {
+ this.autoRotate = autoRotate
+ }
+
+ /**
+ * Configures if multiple image selection is allowed in Gallery Picker.
+ *
+ * @param multipleSelection if true will allow multiple image selection in Gallery Picker.
+ *
+ * @return [ImagePicker.Builder]
+ */
+ fun multipleSelection(multipleSelection: Boolean = true) = apply {
+ this.multipleSelection = multipleSelection
+ }
+
+ /**
+ * Configures minimum amount of images that user will need to select.
+ *
+ * **Affects only** [GalleryPicker.CUSTOM].
+ *
+ * @param minimumSelectionCount minimum allowed selection in [GalleryPicker.CUSTOM].
+ *
+ * @return [ImagePicker.Builder]
+ */
+ @Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+ fun minimumSelectionCount(minimumSelectionCount: Int) = apply {
+ this.minimumSelectionCount = minimumSelectionCount
+ }
+
+ /**
+ * Configures maximum amount of images that user will need to select.
+ *
+ * **Affects only** [GalleryPicker.CUSTOM].
+ *
+ * @param maximumSelectionCount maximum allowed selection in [GalleryPicker.CUSTOM].
+ *
+ * @return [ImagePicker.Builder]
+ */
+ @Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+ fun maximumSelectionCount(maximumSelectionCount: Int) = apply {
+ this.maximumSelectionCount = maximumSelectionCount
+ }
+
+ /**
+ * Defines type of Gallery Picker, represented as [GalleryPicker].
+ *
+ * Possible values:
+ * - [GalleryPicker.NATIVE] will launch Gallery Picker that is build in your Android ROM.
+ * - [GalleryPicker.CUSTOM] will launch Custom Gallery picker, provided by this library.
+ *
+ * @see GalleryPicker
+ *
+ * @param galleryPicker type of desired [GalleryPicker].
+ *
+ * @return [ImagePicker.Builder]
+ */
+ fun galleryPicker(galleryPicker: GalleryPicker) = apply {
+ this.galleryPicker = galleryPicker
+ }
+
+ @Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+ fun loadDelegate(loadDelegate: ImagePickerLoadDelegate) = apply {
+ this.loadDelegate = loadDelegate
+ }
+
+ @Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+ fun loadDelegate(loadDelegate: (ImageView, File) -> Unit) = apply {
+ this.loadDelegate = ImagePickerLoadDelegate { iv, file -> loadDelegate(iv, file) }
+ }
+
+ companion object {
+ /**
+ * Must be called after desired parameters were passed to receive [ImagePicker] instance.
+ *
+ * @param authority string of your FileProvider, defined in AndroidManifest.xml;
+ * @param callback instance of [ImagePickerCallback] that will receive pick-operation result as [PickedResult];
+ * @param block applicable kotlin builder block.
+ *
+ * @see ImagePicker
+ *
+ * @return instance of [ImagePicker]
+ */
+ inline fun build(
+ authority: String,
+ callback: ImagePickerCallback,
+ block: Builder.() -> Unit,
+ ) = Builder(authority, callback)
+ .apply(block)
+ .build()
+ }
+ }
+
+ companion object {
+ private const val TAG = "ShzImagePicker"
+
+ internal inline fun deliverThrowable(clb: ImagePickerCallback, t: Throwable) {
+ t.also(::errorLog)
+ .let(PickedResult::Error)
+ .let(clb::onImagePickerResult)
+ }
+
+ internal inline fun errorLog(t: Throwable) {
+ if (BuildConfig.DEBUG) Log.e(TAG, t.message.toString(), t)
+ }
+
+ internal inline fun errorLog(message: String, t: Throwable? = null) {
+ if (BuildConfig.DEBUG) Log.e(TAG, message, t)
+ }
+
+ internal inline fun debugLog(message: String) {
+ if (BuildConfig.DEBUG) Log.d(TAG, message)
+ }
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerCallback.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerCallback.java
deleted file mode 100644
index 9e73674..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerCallback.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import java.io.File;
-import java.util.List;
-
-public interface ImagePickerCallback {
- void onImagesSelected(List files);
-}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerCallback.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerCallback.kt
new file mode 100644
index 0000000..ac0ea3e
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerCallback.kt
@@ -0,0 +1,26 @@
+package com.shz.imagepicker.imagepicker
+
+import com.shz.imagepicker.imagepicker.model.PickedResult
+
+/**
+ * Contract interface, that allows to receive image-pick result as [PickedResult].
+ *
+ * @see PickedResult
+ */
+fun interface ImagePickerCallback {
+
+ /**
+ * Returns result of image-pick operation.
+ *
+ * @see PickedResult
+ *
+ * @param result actual result of last image-pick operation.
+ *
+ * Possible values:
+ * - [PickedResult.Empty] if nothing was selected;
+ * - [PickedResult.Single] in case one image was selected;
+ * - [PickedResult.Multiple] in case multiple images were selected;
+ * - [PickedResult.Error] in case some error occurred.
+ */
+ fun onImagePickerResult(result: PickedResult)
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerDialog.java b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerDialog.java
deleted file mode 100644
index 326d1dd..0000000
--- a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerDialog.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.shz.imagepicker.imagepicker;
-
-import android.annotation.SuppressLint;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.LinearLayout;
-
-@SuppressLint("ValidFragment")
-public class ImagePickerDialog extends DialogFragment {
-
- public interface ImagePickerDialogListener {
- void onCamera();
- void onGallery();
- }
-
- private ImagePickerDialogListener mListener;
-
- public ImagePickerDialog(ImagePickerDialogListener listener) {
- super();
- this.mListener = listener;
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- Dialog dialog = super.onCreateDialog(savedInstanceState);
- dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
- return dialog;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Dialog dialog = getDialog();
- if (dialog != null) {
- //dialog.getWindow().setLayout(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-
- WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
- //params.horizontalMargin = 0.01f;
- dialog.getWindow().setAttributes(params);
-
- dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.WHITE));
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.layout_image_picker, container, false);
- ((LinearLayout) rootView.findViewById(R.id.btn_camera)).setOnClickListener(v -> {
- mListener.onCamera();
- dismissAllowingStateLoss();
- });
- ((LinearLayout) rootView.findViewById(R.id.btn_gallery)).setOnClickListener(v -> {
- mListener.onGallery();
- dismissAllowingStateLoss();
- });
- return rootView;
- }
-}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerLauncher.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerLauncher.kt
new file mode 100644
index 0000000..74db798
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerLauncher.kt
@@ -0,0 +1,123 @@
+package com.shz.imagepicker.imagepicker
+
+import android.content.Context
+import android.content.Intent
+import com.shz.imagepicker.imagepicker.activity.camera.CameraPickerActivity
+import com.shz.imagepicker.imagepicker.activity.customgallery.GalleryPickerCustomActivity
+import com.shz.imagepicker.imagepicker.activity.dialog.DialogLauncherActivity
+import com.shz.imagepicker.imagepicker.activity.nativegallery.GalleryMultiPickerNativeActivity
+import com.shz.imagepicker.imagepicker.activity.nativegallery.GallerySinglePickerActivity
+import com.shz.imagepicker.imagepicker.core.ImagePickerActivity
+import com.shz.imagepicker.imagepicker.model.GalleryPicker
+
+internal class ImagePickerLauncher(private val context: Context) {
+
+ fun launchCameraPicker(
+ authority: String,
+ callback: ImagePickerCallback,
+ ) {
+ ImagePicker.debugLog("[Launcher] launchCameraPicker")
+ CameraPickerActivity.authority = authority
+ CameraPickerActivity.callback = callback
+ context.startActivity(Intent(context, CameraPickerActivity::class.java))
+ }
+
+ fun launchGalleryPicker(
+ callback: ImagePickerCallback,
+ delegate: ImagePickerLoadDelegate,
+ multipleSelection: Boolean,
+ autoRotate: Boolean,
+ minimum: Int,
+ maximum: Int,
+ galleryPicker: GalleryPicker,
+ ) {
+ ImagePicker.debugLog("[Launcher] launchGalleryPicker")
+ when (galleryPicker) {
+ GalleryPicker.NATIVE -> if (multipleSelection) {
+ launchMultipleSelectionGalleryNative(callback, autoRotate)
+ } else {
+ launchSingleSelectionGallery(callback, autoRotate)
+ }
+ GalleryPicker.CUSTOM -> {
+ launchMultipleSelectionGalleryCustom(
+ callback = callback,
+ delegate = delegate,
+ multipleSelection = multipleSelection,
+ autoRotate = autoRotate,
+ min = minimum,
+ max = maximum
+ )
+ }
+ }
+ }
+
+ fun launchDialog(
+ authority: String,
+ callback: ImagePickerCallback,
+ delegate: ImagePickerLoadDelegate,
+ multipleSelection: Boolean,
+ autoRotate: Boolean,
+ minimum: Int,
+ maximum: Int,
+ galleryPicker: GalleryPicker,
+ ) {
+ ImagePicker.debugLog("[Launcher] launchDialog")
+ DialogLauncherActivity.callback = callback
+ DialogLauncherActivity.loadDelegate = delegate
+ context.startActivity(Intent(context, DialogLauncherActivity::class.java).apply {
+ putExtra(
+ DialogLauncherActivity.BUNDLE_PAYLOAD,
+ DialogLauncherActivity.Payload(
+ authority,
+ multipleSelection,
+ autoRotate,
+ minimum,
+ maximum,
+ galleryPicker,
+ )
+ )
+ })
+ }
+
+ private fun launchSingleSelectionGallery(
+ callback: ImagePickerCallback,
+ autoRotate: Boolean,
+ ) {
+ ImagePicker.debugLog("[Launcher] launchSingleSelectionGallery")
+ GallerySinglePickerActivity.callback = callback
+ context.startActivity(Intent(context, GallerySinglePickerActivity::class.java).apply {
+ putExtra(ImagePickerActivity.BUNDLE_AUTO_ROTATE, autoRotate)
+ })
+ }
+
+ private fun launchMultipleSelectionGalleryNative(
+ callback: ImagePickerCallback,
+ autoRotate: Boolean,
+ ) {
+ ImagePicker.debugLog("[Launcher] launchMultipleSelectionGalleryNative")
+ GalleryMultiPickerNativeActivity.callback = callback
+ context.startActivity(Intent(context, GalleryMultiPickerNativeActivity::class.java).apply {
+ putExtra(ImagePickerActivity.BUNDLE_AUTO_ROTATE, autoRotate)
+ })
+ }
+
+ @Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+ private fun launchMultipleSelectionGalleryCustom(
+ callback: ImagePickerCallback,
+ delegate: ImagePickerLoadDelegate,
+ multipleSelection: Boolean,
+ autoRotate: Boolean,
+ min: Int,
+ max: Int,
+ ) {
+ ImagePicker.debugLog("[Launcher] launchMultipleSelectionGalleryCustom")
+ GalleryPickerCustomActivity.loadDelegate = delegate
+ GalleryPickerCustomActivity.callback = callback
+ context.startActivity(Intent(context, GalleryPickerCustomActivity::class.java).apply {
+ putExtra(ImagePickerActivity.BUNDLE_AUTO_ROTATE, autoRotate)
+ putExtra(GalleryPickerCustomActivity.BUNDLE_MULTI_SELECTION, multipleSelection)
+ putExtra(GalleryPickerCustomActivity.BUNDLE_MINIMUM, min)
+ putExtra(GalleryPickerCustomActivity.BUNDLE_MAXIMUM, max)
+ })
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerLoadDelegate.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerLoadDelegate.kt
new file mode 100644
index 0000000..4167d29
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/ImagePickerLoadDelegate.kt
@@ -0,0 +1,13 @@
+package com.shz.imagepicker.imagepicker
+
+import android.graphics.BitmapFactory
+import android.widget.ImageView
+import java.io.File
+
+fun interface ImagePickerLoadDelegate {
+ fun load(imageView: ImageView, file: File)
+}
+
+internal val defaultImagePickerLoadDelegate = ImagePickerLoadDelegate { iv, file ->
+ iv.setImageBitmap(BitmapFactory.decodeFile(file.path))
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/camera/CameraPickerActivity.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/camera/CameraPickerActivity.kt
new file mode 100644
index 0000000..6122922
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/camera/CameraPickerActivity.kt
@@ -0,0 +1,69 @@
+package com.shz.imagepicker.imagepicker.activity.camera
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import androidx.core.content.FileProvider
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+import com.shz.imagepicker.imagepicker.core.ImagePickerActivity
+import com.shz.imagepicker.imagepicker.model.PickedImage
+import com.shz.imagepicker.imagepicker.model.PickedResult
+import com.shz.imagepicker.imagepicker.model.PickedSource
+import com.shz.imagepicker.imagepicker.utils.checkCameraPermission
+import com.shz.imagepicker.imagepicker.utils.getCaptureImageOutputUri
+import com.shz.imagepicker.imagepicker.utils.getCaptureImageResultUri
+import com.shz.imagepicker.imagepicker.utils.getNormalizedUri
+import java.io.File
+
+internal class CameraPickerActivity : ImagePickerActivity() {
+
+ override val requestCode: Int = 54500
+
+ private var filename: String = ""
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ checkCameraPermission(requestCode, ::startPicker)
+ }
+
+ override fun deliverResult(intent: Intent?) {
+ getCaptureImageResultUri(this, intent, filename)
+ ?.let { uri -> getNormalizedUri(this, uri) }
+ ?.path
+ ?.let(::File)
+ ?.let { file -> PickedImage(PickedSource.CAMERA, file) }
+ ?.let(PickedResult::Single)
+ ?.let(callback::onImagePickerResult)
+ }
+
+ override fun startPicker() {
+ val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ filename = System.nanoTime().toString()
+ val uri = getCaptureImageOutputUri(this, filename)
+ if (filename.isNotEmpty()) uri?.path?.let { path ->
+ val file = File(path)
+ if (Build.VERSION.SDK_INT >= 24) {
+ cameraIntent.putExtra(
+ MediaStore.EXTRA_OUTPUT,
+ FileProvider.getUriForFile(
+ this,
+ authority,
+ file,
+ )
+ )
+ cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ } else {
+ cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
+ }
+ launcher.launch(cameraIntent)
+ }
+ }
+
+ companion object {
+ @JvmField
+ internal var callback: ImagePickerCallback = ImagePickerCallback { }
+ internal var authority: String = ""
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/GalleryPickerCustomActivity.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/GalleryPickerCustomActivity.kt
new file mode 100644
index 0000000..274decf
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/GalleryPickerCustomActivity.kt
@@ -0,0 +1,134 @@
+package com.shz.imagepicker.imagepicker.activity.customgallery
+
+import android.os.Build
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.shz.imagepicker.imagepicker.*
+import com.shz.imagepicker.imagepicker.activity.customgallery.adapter.multiple.GalleryImagesMultipleAdapter
+import com.shz.imagepicker.imagepicker.activity.customgallery.adapter.single.GalleryImagesSingleAdapter
+import com.shz.imagepicker.imagepicker.core.ImagePickerActivity
+import com.shz.imagepicker.imagepicker.defaultImagePickerLoadDelegate
+import com.shz.imagepicker.imagepicker.exception.FeatureNotSupportedException
+import com.shz.imagepicker.imagepicker.model.PickedImage
+import com.shz.imagepicker.imagepicker.model.PickedResult
+import com.shz.imagepicker.imagepicker.model.PickedSource
+import com.shz.imagepicker.imagepicker.utils.checkReadExternalStoragePermission
+import com.shz.imagepicker.imagepicker.utils.getAllImages
+import java.io.File
+
+@Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+internal class GalleryPickerCustomActivity : ImagePickerActivity() {
+
+ override val requestCode: Int = 54503
+
+ private var multipleSelection = false
+ private var max = Int.MAX_VALUE
+ private var min = 1
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (Build.VERSION.SDK_INT >= 33) {
+ val message = "This feature is not supported from Android 13 (SDK 33) due to Android restrictions\n" +
+ "Reference: https://developer.android.com/training/data-storage/shared/media#storage-permission"
+ ImagePicker.deliverThrowable(
+ callback,
+ FeatureNotSupportedException(message),
+ )
+ finish()
+ }
+ setContentView(R.layout.image_picker_activity_gallery_custom)
+
+ multipleSelection = intent.getBooleanExtra(BUNDLE_MULTI_SELECTION, false)
+ max = intent.getIntExtra(BUNDLE_MAXIMUM, 10)
+ min = intent.getIntExtra(BUNDLE_MINIMUM, 1)
+
+ findViewById(R.id.bg_bottom_view)?.isVisible = multipleSelection
+ findViewById(R.id.fl_bottom_view)?.isVisible = multipleSelection
+
+ findViewById(R.id.btn_select)?.setOnClickListener {
+ (findViewById(R.id.rv_images).adapter as? GalleryImagesMultipleAdapter)
+ ?.selectedFiles
+ ?.let(::handleSelection)
+ }
+
+ supportActionBar?.apply {
+ setDisplayHomeAsUpEnabled(true)
+ title = getString(
+ if (multipleSelection) R.string.image_picker_gallery_title_multiple
+ else R.string.image_picker_gallery_title_single
+ )
+ }
+
+ checkReadExternalStoragePermission(requestCode, ::startPicker)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
+ android.R.id.home -> {
+ finish()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ override fun startPicker() {
+ getAllImages(this).let { files ->
+ findViewById(R.id.rv_images).apply {
+ layoutManager = GridLayoutManager(context, 3)
+ adapter = if (!multipleSelection) {
+ GalleryImagesSingleAdapter(loadDelegate) { selectedFile ->
+ handleSelection(listOf(selectedFile))
+ }.apply {
+ submitList(files)
+ }
+ } else {
+ GalleryImagesMultipleAdapter(ArrayList(files), max) { selectedFiles ->
+ onSelectionChanged(selectedFiles.size)
+ }
+ }
+ }
+ }
+ }
+
+ private fun onSelectionChanged(count: Int) {
+ findViewById(R.id.tv_select_cont)?.text = "$count"
+ findViewById(R.id.btn_select)?.isEnabled = count >= min
+ }
+
+ private fun handleSelection(list: List) {
+ when (list.size) {
+ 0 -> PickedResult.Empty
+ 1 -> PickedResult.Single(
+ PickedImage(
+ PickedSource.GALLERY_CUSTOM,
+ list.first().applyConditionalRotation(),
+ )
+ )
+ else -> PickedResult.Multiple(list.map { file ->
+ PickedImage(
+ PickedSource.GALLERY_CUSTOM,
+ file.applyConditionalRotation(),
+ )
+ })
+ }.let(callback::onImagePickerResult)
+ finish()
+ }
+
+ companion object {
+ internal const val BUNDLE_MULTI_SELECTION = "bundle_multi_selection"
+ internal const val BUNDLE_MINIMUM = "bundle_minimum"
+ internal const val BUNDLE_MAXIMUM = "bundle_maximum"
+
+ @JvmField
+ internal var callback: ImagePickerCallback = ImagePickerCallback { }
+
+ @JvmField
+ internal var loadDelegate: ImagePickerLoadDelegate = defaultImagePickerLoadDelegate
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/adapter/multiple/GalleryImagesMultipleAdapter.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/adapter/multiple/GalleryImagesMultipleAdapter.kt
new file mode 100644
index 0000000..65771cd
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/adapter/multiple/GalleryImagesMultipleAdapter.kt
@@ -0,0 +1,80 @@
+package com.shz.imagepicker.imagepicker.activity.customgallery.adapter.multiple
+
+import android.graphics.BitmapFactory
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.ImageView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.shz.imagepicker.imagepicker.R
+import java.io.File
+
+internal class GalleryImagesMultipleAdapter(
+ private val images: ArrayList = arrayListOf(),
+ private val maximum: Int = Int.MAX_VALUE,
+ private val onSelectionChanged: (List) -> Unit = {},
+) : RecyclerView.Adapter() {
+
+ private val selectionIds: ArrayList = arrayListOf()
+
+ val selectedFiles: List
+ get() = images.filter {
+ selectionIds.contains(images.indexOf(it))
+ }
+
+ init {
+ setHasStableIds(true)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.image_picker_item_gallery_image_multiple, parent, false
+ )
+ )
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(images[position], selectionIds.contains(position))
+ }
+
+ override fun getItemCount(): Int = images.size
+
+ override fun getItemId(position: Int): Long = position * 1L
+
+ inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
+
+ fun bind(item: File, selected: Boolean) {
+ view.setOnClickListener {
+ if (selected) {
+ selectionIds.removeAll { it == bindingAdapterPosition }
+ } else if (selectedFiles.size < maximum) {
+ selectionIds.add(bindingAdapterPosition)
+ }
+ notifyItemChanged(bindingAdapterPosition)
+ onSelectionChanged(selectedFiles)
+ }
+ view.findViewById(R.id.image).setImageBitmap(BitmapFactory.decodeFile(item.path))
+ view.findViewById(R.id.checkbox).isChecked = selected
+ }
+ }
+
+ data class Model(
+ val file: File,
+ val isSelected: Boolean,
+ )
+
+ companion object {
+ private val diff = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: File,
+ newItem: File
+ ): Boolean = oldItem.path == newItem.path
+
+ override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
+ TODO("Not yet implemented")
+ }
+
+ }
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/adapter/single/GalleryImagesSingleAdapter.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/adapter/single/GalleryImagesSingleAdapter.kt
new file mode 100644
index 0000000..b3381ec
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/adapter/single/GalleryImagesSingleAdapter.kt
@@ -0,0 +1,51 @@
+package com.shz.imagepicker.imagepicker.activity.customgallery.adapter.single
+
+import android.graphics.BitmapFactory
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.shz.imagepicker.imagepicker.ImagePickerLoadDelegate
+import com.shz.imagepicker.imagepicker.R
+import java.io.File
+
+internal class GalleryImagesSingleAdapter(
+ private val loadDelegate: ImagePickerLoadDelegate,
+ private val onClick: (File) -> Unit = {},
+) : ListAdapter(diff) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.image_picker_item_gallery_image_single, parent, false
+ )
+ )
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class ViewHolder(private val view: View): RecyclerView.ViewHolder(view) {
+
+ fun bind(item: File) {
+ view.setOnClickListener { onClick(getItem(bindingAdapterPosition)) }
+ view.findViewById(R.id.image)?.let { iv -> loadDelegate.load(iv, item) }
+ }
+ }
+
+ companion object {
+ private val diff = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: File,
+ newItem: File,
+ ): Boolean = oldItem.path == newItem.path
+
+ override fun areContentsTheSame(
+ oldItem: File,
+ newItem: File,
+ ): Boolean = oldItem == newItem
+ }
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/view/GalleryImageView.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/view/GalleryImageView.kt
new file mode 100644
index 0000000..77dd29c
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/customgallery/view/GalleryImageView.kt
@@ -0,0 +1,32 @@
+package com.shz.imagepicker.imagepicker.activity.customgallery.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+
+@SuppressLint("AppCompatCustomView")
+internal class GalleryImageView : ImageView {
+
+ private val hwRatio = 1f
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = measuredWidth
+ val height = measuredHeight
+
+ val calculatedHeight: Int = calculateHeightByRatio(width)
+
+ if (calculatedHeight != height) {
+ setMeasuredDimension(width, calculatedHeight)
+ }
+ }
+
+ private fun calculateHeightByRatio(side: Int): Int {
+ return (hwRatio * side.toFloat()).toInt()
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/dialog/DialogLauncherActivity.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/dialog/DialogLauncherActivity.kt
new file mode 100644
index 0000000..ee61854
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/dialog/DialogLauncherActivity.kt
@@ -0,0 +1,79 @@
+package com.shz.imagepicker.imagepicker.activity.dialog
+
+import android.os.Build
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.shz.imagepicker.imagepicker.*
+import com.shz.imagepicker.imagepicker.ImagePickerLauncher
+import com.shz.imagepicker.imagepicker.defaultImagePickerLoadDelegate
+import com.shz.imagepicker.imagepicker.exception.UnableLaunchDialogException
+import com.shz.imagepicker.imagepicker.model.GalleryPicker
+import java.io.Serializable
+
+internal class DialogLauncherActivity : AppCompatActivity(), ImagePickerDialog.Listener {
+
+ private var payload: Payload? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ this.payload = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getSerializableExtra(BUNDLE_PAYLOAD, Payload::class.java)
+ } else {
+ intent.getSerializableExtra(BUNDLE_PAYLOAD) as Payload?
+ }
+
+ this.payload?.let {
+ ImagePickerDialog(this).show(supportFragmentManager, PICKER_DIALOG_TAG)
+ } ?: run {
+ ImagePicker.deliverThrowable(callback, UnableLaunchDialogException())
+ finish()
+ }
+ }
+
+ override fun launchCamera() {
+ ImagePickerLauncher(this).launchCameraPicker(
+ authority = payload?.authority ?: "",
+ callback = callback,
+ )
+ finish()
+ }
+
+ override fun launchGallery() {
+ ImagePickerLauncher(this).launchGalleryPicker(
+ callback = callback,
+ delegate = loadDelegate,
+ multipleSelection = payload?.multipleSelection ?: false,
+ autoRotate = payload?.autoRotate ?: false,
+ galleryPicker = payload?.galleryPicker ?: GalleryPicker.NATIVE,
+ minimum = payload?.minimum ?: 1,
+ maximum = payload?.maximum ?: 10,
+ )
+ finish()
+ }
+
+ override fun onDismiss() {
+ finish()
+ }
+
+ data class Payload(
+ val authority: String,
+ val multipleSelection: Boolean,
+ val autoRotate: Boolean,
+ val minimum: Int,
+ val maximum: Int,
+ val galleryPicker: GalleryPicker,
+ ) : Serializable
+
+ companion object {
+ private const val PICKER_DIALOG_TAG = "picker_dialog_tag"
+
+ internal const val BUNDLE_PAYLOAD = "bundle_payload"
+
+ @JvmField
+ internal var callback: ImagePickerCallback = ImagePickerCallback { }
+
+ @JvmField
+ internal var loadDelegate: ImagePickerLoadDelegate = defaultImagePickerLoadDelegate
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/dialog/ImagePickerDialog.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/dialog/ImagePickerDialog.kt
new file mode 100644
index 0000000..07c903f
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/dialog/ImagePickerDialog.kt
@@ -0,0 +1,53 @@
+package com.shz.imagepicker.imagepicker.activity.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.widget.LinearLayout
+import androidx.fragment.app.DialogFragment
+import com.shz.imagepicker.imagepicker.R
+
+internal class ImagePickerDialog(private val listener: Listener) : DialogFragment() {
+
+ interface Listener {
+ fun launchCamera()
+ fun launchGallery()
+ fun onDismiss()
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
+ super.onCreateDialog(savedInstanceState).apply {
+ window?.requestFeature(Window.FEATURE_NO_TITLE)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.WHITE))
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ listener.onDismiss()
+ super.onDismiss(dialog)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? = inflater.inflate(R.layout.image_picker_dialog_selection, container, false).apply {
+ findViewById(R.id.btn_camera)?.setOnClickListener {
+ listener.launchCamera()
+ dismissAllowingStateLoss()
+ }
+ findViewById(R.id.btn_gallery)?.setOnClickListener {
+ listener.launchGallery()
+ dismissAllowingStateLoss()
+ }
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/nativegallery/GalleryMultiPickerNativeActivity.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/nativegallery/GalleryMultiPickerNativeActivity.kt
new file mode 100644
index 0000000..b3e7bee
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/nativegallery/GalleryMultiPickerNativeActivity.kt
@@ -0,0 +1,86 @@
+package com.shz.imagepicker.imagepicker.activity.nativegallery
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.provider.MediaStore
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+import com.shz.imagepicker.imagepicker.core.ImagePickerActivity
+import com.shz.imagepicker.imagepicker.model.PickedImage
+import com.shz.imagepicker.imagepicker.model.PickedResult
+import com.shz.imagepicker.imagepicker.model.PickedSource
+import com.shz.imagepicker.imagepicker.utils.checkReadExternalStoragePermission
+import com.shz.imagepicker.imagepicker.utils.getImagePathFromInputStreamUri
+import com.shz.imagepicker.imagepicker.utils.getNormalizedUri
+import java.io.File
+
+internal class GalleryMultiPickerNativeActivity : ImagePickerActivity() {
+
+ override val requestCode: Int = 54501
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ checkReadExternalStoragePermission(requestCode, ::startPicker)
+ }
+
+ override fun deliverResult(intent: Intent?) {
+ super.deliverResult(intent)
+ val output = arrayListOf()
+ intent?.clipData?.let { clipData ->
+ for (i in 0 until clipData.itemCount) {
+ getImagePathFromInputStreamUri(this, clipData.getItemAt(i).uri)
+ ?.let(::File)
+ ?.takeIf(File::exists)
+ ?.applyConditionalRotation()
+ ?.let { file -> PickedImage(PickedSource.GALLERY_MULTIPLE, file) }
+ ?.let(output::add)
+ }
+ }
+ val samsungPickerList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent?.extras?.getParcelableArrayList(SAMSUNG_SELECTED_ITEMS, Parcelable::class.java)
+ } else {
+ intent?.extras?.getParcelableArrayList(SAMSUNG_SELECTED_ITEMS)
+ }
+ samsungPickerList?.forEach {
+ (it as? Uri)?.let { uri ->
+ getNormalizedUri(this, uri)?.path
+ ?.let(::File)
+ ?.takeIf(File::exists)
+ ?.applyConditionalRotation()
+ ?.let { file -> PickedImage(PickedSource.GALLERY_MULTIPLE, file) }
+ ?.let(output::add)
+ }
+ }
+ when (output.size) {
+ 0 -> PickedResult.Empty
+ 1 -> PickedResult.Single(output.first())
+ else -> PickedResult.Multiple(output)
+ }.let(callback::onImagePickerResult)
+ }
+
+ override fun startPicker() {
+ launcher.launch(
+ Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
+ }
+ putExtra(SAMSUNG_MULTI_PICK, true);
+ setDataAndType(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ GALLERY_IMAGE_MIME,
+ )
+ }
+ )
+ }
+
+ companion object {
+ private const val GALLERY_IMAGE_MIME = "image/jpeg"
+ private const val SAMSUNG_MULTI_PICK = "multi-pick"
+ private const val SAMSUNG_SELECTED_ITEMS = "selectedItems"
+
+ @JvmField
+ internal var callback: ImagePickerCallback = ImagePickerCallback { }
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/nativegallery/GallerySinglePickerActivity.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/nativegallery/GallerySinglePickerActivity.kt
new file mode 100644
index 0000000..e12fcf8
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/activity/nativegallery/GallerySinglePickerActivity.kt
@@ -0,0 +1,54 @@
+package com.shz.imagepicker.imagepicker.activity.nativegallery
+
+import android.content.Intent
+import android.os.Bundle
+import android.provider.MediaStore
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+import com.shz.imagepicker.imagepicker.core.ImagePickerActivity
+import com.shz.imagepicker.imagepicker.model.PickedImage
+import com.shz.imagepicker.imagepicker.model.PickedResult
+import com.shz.imagepicker.imagepicker.model.PickedSource
+import com.shz.imagepicker.imagepicker.utils.checkReadExternalStoragePermission
+import com.shz.imagepicker.imagepicker.utils.getImagePathFromInputStreamUri
+import java.io.File
+
+internal class GallerySinglePickerActivity : ImagePickerActivity() {
+
+ override val requestCode: Int = 54502
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ checkReadExternalStoragePermission(requestCode, ::startPicker)
+ }
+
+ override fun deliverResult(intent: Intent?) {
+ super.deliverResult(intent)
+ intent
+ ?.data
+ ?.let { uri -> getImagePathFromInputStreamUri(this, uri) }
+ ?.let(::File)
+ ?.takeIf(File::exists)
+ ?.applyConditionalRotation()
+ ?.let { file -> PickedImage(PickedSource.GALLERY_SINGLE, file) }
+ ?.let(PickedResult::Single)
+ ?.let(callback::onImagePickerResult)
+ }
+
+ override fun startPicker() {
+ launcher.launch(
+ Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
+ setDataAndType(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ GALLERY_IMAGE_MIME
+ )
+ }
+ )
+ }
+
+ companion object {
+ private const val GALLERY_IMAGE_MIME = "image/jpeg"
+
+ @JvmField
+ internal var callback: ImagePickerCallback = ImagePickerCallback { }
+ }
+}
\ No newline at end of file
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/core/ImagePickerActivity.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/core/ImagePickerActivity.kt
new file mode 100644
index 0000000..b06933b
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/core/ImagePickerActivity.kt
@@ -0,0 +1,79 @@
+package com.shz.imagepicker.imagepicker.core
+
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.Image
+import android.os.Bundle
+import android.os.PersistableBundle
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import com.shz.imagepicker.imagepicker.ImagePicker
+import com.shz.imagepicker.imagepicker.activity.customgallery.GalleryPickerCustomActivity
+import com.shz.imagepicker.imagepicker.utils.RotationResult
+import com.shz.imagepicker.imagepicker.utils.createTemporaryFile
+import com.shz.imagepicker.imagepicker.utils.rotateImageIfRequired
+import java.io.File
+
+internal abstract class ImagePickerActivity : AppCompatActivity() {
+
+ abstract val requestCode: Int
+
+ private var autoRotate: Boolean = false
+
+ protected val launcher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ deliverResult(result.data)
+ }
+ finish()
+ }
+
+ abstract fun startPicker()
+
+ open fun deliverResult(intent: Intent?) = Unit
+
+ open fun handleMissingPermissions() {
+ finish()
+ }
+
+ protected fun File.applyConditionalRotation(): File {
+ if (!autoRotate) return this
+ return when (val rotationResult = rotateImageIfRequired(this)) {
+ RotationResult.NotRequired -> this
+ is RotationResult.Rotate -> {
+ try {
+ createTemporaryFile(rotationResult.rotatedBitmap)
+ } catch (e: Exception) {
+ ImagePicker.errorLog("Unable to create temporary rotated file", e)
+ this
+ }
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ autoRotate = intent.getBooleanExtra(BUNDLE_AUTO_ROTATE, false)
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == this.requestCode) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ startPicker()
+ } else {
+ handleMissingPermissions()
+ }
+ }
+ }
+
+ companion object {
+ internal const val BUNDLE_AUTO_ROTATE = "bundle_auto_rotate"
+ }
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/FeatureNotSupportedException.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/FeatureNotSupportedException.kt
new file mode 100644
index 0000000..a0c08e7
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/FeatureNotSupportedException.kt
@@ -0,0 +1,12 @@
+package com.shz.imagepicker.imagepicker.exception
+
+import com.shz.imagepicker.imagepicker.ImagePicker
+
+private const val DEFAULT_NOT_SUPPORTED_EXCEPTION_MESSAGE = "This feature is not supported on your configuration device"
+
+/**
+ * Is thrown form [ImagePicker] when some feature is not supported.
+ */
+class FeatureNotSupportedException(
+ override val message: String = DEFAULT_NOT_SUPPORTED_EXCEPTION_MESSAGE,
+) : Throwable()
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/NothingToLaunchException.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/NothingToLaunchException.kt
new file mode 100644
index 0000000..20b54dd
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/NothingToLaunchException.kt
@@ -0,0 +1,12 @@
+package com.shz.imagepicker.imagepicker.exception
+
+import com.shz.imagepicker.imagepicker.ImagePicker
+
+/**
+ * Is thrown form [ImagePicker] when no picker was configuring to launch.
+ *
+ * To fix try to call useGallery(true) or useCamera(true) in [ImagePicker.Builder].
+ *
+ * @see ImagePicker.Builder
+ */
+class NothingToLaunchException : Throwable("Nothing to launch. Make sure that you have set to true useGallery() or useCamera().")
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/UnableLaunchDialogException.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/UnableLaunchDialogException.kt
new file mode 100644
index 0000000..315ff1c
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/exception/UnableLaunchDialogException.kt
@@ -0,0 +1,12 @@
+package com.shz.imagepicker.imagepicker.exception
+
+import com.shz.imagepicker.imagepicker.ImagePicker
+
+/**
+ * Is thrown in some memory leak use cases when launching interactive dialog.
+ *
+ * To fix try to pass Activity context to [ImagePicker.launch] method.
+ *
+ * @see ImagePicker.launch
+ */
+class UnableLaunchDialogException : Throwable("Unable to launch dialog, make sure you are passing Activity context")
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/GalleryPicker.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/GalleryPicker.kt
new file mode 100644
index 0000000..f739d78
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/GalleryPicker.kt
@@ -0,0 +1,27 @@
+package com.shz.imagepicker.imagepicker.model
+
+import android.content.Intent
+import java.io.Serializable
+
+/**
+ * Describes all available types of Gallery Pickers.
+ *
+ * Implements [Serializable].
+ */
+enum class GalleryPicker : Serializable {
+ /**
+ * Matches system Gallery Picker that is implemented in Android ROM.
+ */
+ NATIVE,
+
+ /**
+ * Matches Custom Gallery Picker that is implemented in this library.
+ *
+ * The reason to use [CUSTOM] picker is that many of old and non-AOSP Android ROMs
+ * do not respect [Intent.EXTRA_ALLOW_MULTIPLE] that is passed to launch [Intent].
+ *
+ * Advice: Use this mainly when you need to support multiple images selection on custom ROMs.
+ */
+ @Deprecated("GalleryPicker.CUSTOM is deprecated from Android 13 (SDK 33)")
+ CUSTOM;
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedImage.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedImage.kt
new file mode 100644
index 0000000..88c1196
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedImage.kt
@@ -0,0 +1,52 @@
+@file:Suppress("unused")
+
+package com.shz.imagepicker.imagepicker.model
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+import java.io.File
+import java.io.Serializable
+
+/**
+ * Data model of picked image.
+ *
+ * Implements [Serializable].
+ *
+ * @see ImagePickerCallback
+ */
+data class PickedImage(
+ /**
+ * Describes the source of pick-operation of current image.
+ *
+ * @see PickedSource
+ */
+ val source: PickedSource,
+ /**
+ * Contains instance of [File] that corresponds to picked image.
+ *
+ * @see File
+ */
+ val file: File,
+) : Serializable {
+
+ /**
+ * Field that represents picked image as [Bitmap] instance.
+ *
+ * @see Bitmap
+ */
+ val bitmap: Bitmap
+ get() = BitmapFactory.decodeFile(file.path)
+
+ /**
+ * Field that represents picked image as [Drawable] instance.
+ * This field is nullable.
+ *
+ * @see Drawable
+ */
+ val bitmapDrawable: Drawable?
+ get() = BitmapDrawable.createFromPath(file.path)
+
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedResult.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedResult.kt
new file mode 100644
index 0000000..94e3e4c
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedResult.kt
@@ -0,0 +1,49 @@
+package com.shz.imagepicker.imagepicker.model
+
+import java.io.Serializable
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+
+/**
+ * Describes all available business logic pick-operation result.
+ *
+ * Implements [Serializable].
+ *
+ * @see ImagePickerCallback
+ */
+sealed class PickedResult : Serializable {
+ /**
+ * Member of [PickedResult].
+ * Determines that nothing was selected during pick-operation.
+ */
+ object Empty : PickedResult()
+
+ /**
+ * Member of [PickedResult].
+ * Determines that only one image was selected during pick-operation.
+ *
+ * @param image instance of selected [PickedImage].
+ *
+ * @see PickedImage
+ */
+ data class Single(val image: PickedImage) : PickedResult()
+
+ /**
+ * Member of [PickedResult].
+ * Determines that only multiple images were selected during pick-operation.
+ *
+ * @param images collection of selected [PickedImage].
+ *
+ * @see PickedImage
+ */
+ data class Multiple(val images: List) : PickedResult()
+
+ /**
+ * Member of [PickedResult].
+ * Determines that some error occurred during pick-operation.
+ *
+ * @param throwable issue that happened with operation.
+ *
+ * @see Throwable
+ */
+ data class Error(val throwable: Throwable) : PickedResult()
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedSource.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedSource.kt
new file mode 100644
index 0000000..0578a60
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/model/PickedSource.kt
@@ -0,0 +1,33 @@
+package com.shz.imagepicker.imagepicker.model
+
+import java.io.Serializable
+import com.shz.imagepicker.imagepicker.ImagePickerCallback
+
+/**
+ * Describes the source where particular [PickedImage] was selected from.
+ * Useful to implement your local business logic, when handling result from [ImagePickerCallback].
+ *
+ * @see PickedImage
+ * @see ImagePickerCallback
+ */
+enum class PickedSource : Serializable {
+ /**
+ * Means that image was selected from **single selection** [GalleryPicker.NATIVE].
+ */
+ GALLERY_SINGLE,
+
+ /**
+ * Means that image was selected from **multiple selection** [GalleryPicker.NATIVE].
+ */
+ GALLERY_MULTIPLE,
+
+ /**
+ * Means that image was selected from [GalleryPicker.CUSTOM].
+ */
+ GALLERY_CUSTOM,
+
+ /**
+ * Means that image was selected from **Camera Picker**.
+ */
+ CAMERA;
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/CustomGalleryUtils.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/CustomGalleryUtils.kt
new file mode 100644
index 0000000..48e82ae
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/CustomGalleryUtils.kt
@@ -0,0 +1,53 @@
+package com.shz.imagepicker.imagepicker.utils
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.MediaStore
+import java.io.File
+
+/*fun sortImagesByFolder(files: List): Map> {
+ val resultMap = mutableMapOf>()
+ for (file in files) {
+ (!resultMap.containsKey(file.parentFile)).let { resultMap.put(file.parentFile, mutableListOf()) }
+ resultMap[file.parentFile]?.add(file)
+ }
+ return resultMap.toMap()
+}
+
+fun getImagesFromFolder(context: Context, folder: String): List {
+ val selection = MediaStore.Images.Media.DATA + " LIKE ?"
+ return queryUri(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, arrayOf("%$folder/%"))
+ .use { it?.getResultsFromCursor() ?: listOf() }
+}*/
+
+fun getAllImages(context: Context): List {
+ return queryUri(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null)
+ .use { it?.getResultsFromCursor() ?: listOf() }
+}
+
+
+private fun queryUri(context: Context, uri: Uri, selection: String?, selectionArgs: Array?): Cursor? {
+ return context.contentResolver.query(
+ uri,
+ projection,
+ selection,
+ selectionArgs,
+ null)
+}
+
+private fun Cursor.getResultsFromCursor(): List {
+ val results = mutableListOf()
+ while (this.moveToNext()) {
+ results.add(File(this.getString(this.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA))))
+ }
+ return results
+}
+
+val projection = arrayOf(
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.DATE_ADDED,
+ MediaStore.Files.FileColumns.MIME_TYPE,
+ MediaStore.Files.FileColumns.TITLE,
+)
\ No newline at end of file
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/ImageOrientationUtils.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/ImageOrientationUtils.kt
new file mode 100644
index 0000000..be47739
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/ImageOrientationUtils.kt
@@ -0,0 +1,48 @@
+package com.shz.imagepicker.imagepicker.utils
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Matrix
+import androidx.exifinterface.media.ExifInterface
+import java.io.File
+
+internal sealed class RotationResult {
+ object NotRequired : RotationResult()
+ data class Rotate(
+ val angle: Float,
+ val rotatedBitmap: Bitmap,
+ ) : RotationResult()
+}
+
+internal fun rotateImageIfRequired(inputFile: File): RotationResult {
+ val inputBitmap = BitmapFactory.decodeFile(inputFile.absolutePath)
+ val exifInterface = ExifInterface(inputFile.absolutePath)
+ val orientation: Int = exifInterface.getAttributeInt(
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.ORIENTATION_UNDEFINED,
+ )
+ val angle: Float = when (orientation) {
+ ExifInterface.ORIENTATION_NORMAL -> 0f
+ ExifInterface.ORIENTATION_ROTATE_90 -> 90f
+ ExifInterface.ORIENTATION_ROTATE_180 -> 180f
+ ExifInterface.ORIENTATION_ROTATE_270 -> 270f
+ else -> 0f
+ }
+ return when (angle) {
+ 0f -> RotationResult.NotRequired
+ else -> RotationResult.Rotate(
+ angle,
+ inputBitmap.rotate(angle)
+ )
+ }
+}
+
+internal fun Bitmap.rotate(angle: Float): Bitmap = Bitmap.createBitmap(
+ this,
+ 0,
+ 0,
+ width,
+ height,
+ Matrix().apply { postRotate(angle) },
+ true,
+)
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/ImagePathUtils.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/ImagePathUtils.kt
new file mode 100644
index 0000000..4319587
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/ImagePathUtils.kt
@@ -0,0 +1,109 @@
+package com.shz.imagepicker.imagepicker.utils
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Environment
+import android.provider.MediaStore
+import java.io.*
+import java.lang.Exception
+import java.util.*
+import kotlin.Throws
+
+
+private const val FILE_EXTENSION_JPEG = ".jpeg"
+private const val FILE_EXTENSION_JPG = ".jpg"
+private const val FILE_SCHEMA_CONTENT = "content:"
+
+
+fun getCaptureImageOutputUri(context: Context?, filename: String): Uri? = context
+ ?.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
+ ?.let { file -> Uri.fromFile(File(file.path, filename + FILE_EXTENSION_JPEG)) }
+
+
+fun getCaptureImageResultUri(context: Context, data: Intent?, filename: String): Uri? {
+ var isCamera = true
+ if (data != null && data.data != null) {
+ val action = data.action
+ isCamera = action != null && action === MediaStore.ACTION_IMAGE_CAPTURE
+ }
+ return if (isCamera || data!!.data == null) getCaptureImageOutputUri(context, filename)
+ else data.data
+}
+
+
+fun getNormalizedUri(context: Context, uri: Uri?): Uri? =
+ if (uri != null && uri.toString().contains(FILE_SCHEMA_CONTENT))
+ Uri.fromFile(
+ getPath(
+ context,
+ uri,
+ MediaStore.Images.Media.DATA
+ )
+ )
+ else uri
+
+
+fun getImagePathFromInputStreamUri(context: Context, uri: Uri): String? {
+ var inputStream: InputStream? = null
+ var filePath: String? = null
+ if (uri.authority != null) {
+ try {
+ inputStream = context.contentResolver.openInputStream(uri)
+ val photoFile = createTempFileFrom(context, inputStream)
+ filePath = photoFile!!.path
+ } catch (ex: FileNotFoundException) {
+ ex.printStackTrace()
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ } finally {
+ try {
+ inputStream!!.close()
+ } catch (ex: IOException) {
+ ex.printStackTrace()
+ }
+ }
+ }
+ return filePath
+}
+
+@Throws(IOException::class)
+private fun createTempFileFrom(context: Context, inputStream: InputStream?): File? {
+ var targetFile: File? = null
+ if (inputStream != null) {
+ var read: Int
+ val buffer = ByteArray(8 * 1024)
+ targetFile = createTempFile(context, null)
+ val outputStream = FileOutputStream(targetFile)
+ while (true) {
+ read = inputStream.read(buffer)
+ if (read == -1) {
+ break
+ }
+ outputStream.write(buffer, 0, read)
+ }
+ outputStream.flush()
+ try {
+ outputStream.close()
+ } catch (ex: IOException) {
+ ex.printStackTrace()
+ }
+ }
+ return targetFile
+}
+
+private fun createTempFile(context: Context, filePath: String?): File {
+ val tempFilename: String = filePath ?: Calendar.getInstance().timeInMillis.toString()
+ return File(context.externalCacheDir, tempFilename + FILE_EXTENSION_JPG)
+}
+
+private fun getPath(context: Context, uri: Uri, column: String): File {
+ val columns = arrayOf(column)
+ val cursor = context.contentResolver.query(uri, columns, null, null, null)
+ val columnIndex = cursor!!.getColumnIndexOrThrow(column)
+ cursor.moveToFirst()
+ val path = cursor.getString(columnIndex)
+ cursor.close()
+ return File(path)
+}
+
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/PermissionUtils.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/PermissionUtils.kt
new file mode 100644
index 0000000..9bcb555
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/PermissionUtils.kt
@@ -0,0 +1,45 @@
+package com.shz.imagepicker.imagepicker.utils
+
+import android.Manifest
+import android.app.Activity
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.annotation.ChecksSdkIntAtLeast
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+
+@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU, lambda = 2)
+fun Activity.checkReadExternalStoragePermission(request: Int, action: () -> Unit) {
+ if (Build.VERSION.SDK_INT >= 33) action()
+ else {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ ) == PackageManager.PERMISSION_DENIED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ ),
+ request,
+ )
+ } else {
+ action()
+ }
+ }
+}
+
+fun Activity.checkCameraPermission(request: Int, action: () -> Unit) {
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_DENIED
+ ) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.CAMERA),
+ request,
+ )
+ } else action()
+}
diff --git a/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/StorageUtils.kt b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/StorageUtils.kt
new file mode 100644
index 0000000..a0f2d42
--- /dev/null
+++ b/imagepicker/src/main/java/com/shz/imagepicker/imagepicker/utils/StorageUtils.kt
@@ -0,0 +1,35 @@
+package com.shz.imagepicker.imagepicker.utils
+
+import android.content.Context
+import android.graphics.Bitmap
+import java.io.File
+import java.io.FileOutputStream
+
+private const val STORAGE_ROTATED = "rotated"
+
+internal fun Context.getStorageDirectory(name: String): File {
+ val rootDirectory: File = filesDir
+ if (!rootDirectory.exists()) rootDirectory.mkdir()
+ val directory = File(rootDirectory, name)
+ if (!directory.exists()) directory.mkdir()
+ return directory
+}
+
+internal fun Context.createTemporaryFile(bitmap: Bitmap): File {
+ val directory = getStorageDirectory(STORAGE_ROTATED)
+ val filename = "${System.currentTimeMillis()}.jpg"
+ return File(directory, filename).apply {
+ writeBitmap(bitmap, Bitmap.CompressFormat.JPEG, 100)
+ }
+}
+
+internal fun File.writeBitmap(
+ bitmap: Bitmap,
+ format: Bitmap.CompressFormat,
+ quality: Int = 100,
+) {
+ outputStream().use { out ->
+ bitmap.compress(format, quality, out)
+ out.flush()
+ }
+}
diff --git a/imagepicker/src/main/res/drawable/selector_picker_gallery.xml b/imagepicker/src/main/res/drawable/selector_picker_gallery.xml
new file mode 100644
index 0000000..f063ae1
--- /dev/null
+++ b/imagepicker/src/main/res/drawable/selector_picker_gallery.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/imagepicker/src/main/res/layout/image_picker_activity_gallery_custom.xml b/imagepicker/src/main/res/layout/image_picker_activity_gallery_custom.xml
new file mode 100644
index 0000000..fc581a9
--- /dev/null
+++ b/imagepicker/src/main/res/layout/image_picker_activity_gallery_custom.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/imagepicker/src/main/res/layout/layout_image_picker.xml b/imagepicker/src/main/res/layout/image_picker_dialog_selection.xml
similarity index 88%
rename from imagepicker/src/main/res/layout/layout_image_picker.xml
rename to imagepicker/src/main/res/layout/image_picker_dialog_selection.xml
index 9815a62..63d9ce0 100644
--- a/imagepicker/src/main/res/layout/layout_image_picker.xml
+++ b/imagepicker/src/main/res/layout/image_picker_dialog_selection.xml
@@ -28,7 +28,8 @@
android:gravity="center_vertical"
android:textSize="20sp"
android:textStyle="normal"
- android:text="@string/image_picker_title_camera" />
+ android:textColor="#000000"
+ android:text="@string/image_picker_dialog_camera" />
@@ -52,8 +53,9 @@
android:gravity="center_vertical"
android:textSize="20sp"
android:textStyle="normal"
- android:text="@string/image_picker_title_gallery" />
+ android:textColor="#000000"
+ android:text="@string/image_picker_dialog_gallery" />
-
\ No newline at end of file
+
diff --git a/imagepicker/src/main/res/layout/image_picker_item_gallery_image_multiple.xml b/imagepicker/src/main/res/layout/image_picker_item_gallery_image_multiple.xml
new file mode 100644
index 0000000..5c4753e
--- /dev/null
+++ b/imagepicker/src/main/res/layout/image_picker_item_gallery_image_multiple.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/imagepicker/src/main/res/layout/image_picker_item_gallery_image_single.xml b/imagepicker/src/main/res/layout/image_picker_item_gallery_image_single.xml
new file mode 100644
index 0000000..783e0ac
--- /dev/null
+++ b/imagepicker/src/main/res/layout/image_picker_item_gallery_image_single.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/imagepicker/src/main/res/values/strings.xml b/imagepicker/src/main/res/values/strings.xml
index 4878d9d..156419b 100644
--- a/imagepicker/src/main/res/values/strings.xml
+++ b/imagepicker/src/main/res/values/strings.xml
@@ -1,4 +1,10 @@
- Camera
- Gallery
-
\ No newline at end of file
+ Camera
+ Gallery
+
+ Select image
+ Select images
+
+ Select
+ Selected:
+
diff --git a/imagepicker/src/main/res/values/styles.xml b/imagepicker/src/main/res/values/styles.xml
index 8856c32..2882de8 100644
--- a/imagepicker/src/main/res/values/styles.xml
+++ b/imagepicker/src/main/res/values/styles.xml
@@ -1,6 +1,6 @@
-
-
\ No newline at end of file
+
+
+