From d96805790e314e3ca14e23c30f186e283003c421 Mon Sep 17 00:00:00 2001 From: Sarthak Mishra Date: Mon, 13 Nov 2023 21:33:15 +0530 Subject: [PATCH] Add support for picking documents from the picker. Updated with the Android 13 changes --- .github/workflows/android.yml | 2 +- .../adapter/FolderPickerAdapter.kt | 11 ++++-- .../imagepicker/features/ImagePicker.kt | 5 +++ .../features/ImagePickerActivity.kt | 7 +++- .../features/ImagePickerFragment.kt | 38 ++++++++++++++++++- .../ImagePickerInteractionListener.kt | 2 + .../features/ImagePickerLauncher.kt | 35 ++++++++++++++--- .../features/ImagePickerPresenter.kt | 7 +++- .../esafirm/imagepicker/features/IpCons.kt | 2 + .../fileloader/DefaultImageFileLoader.kt | 21 ++++++++++ .../imagepicker/helper/ImagePickerUtils.kt | 8 ++++ .../com/esafirm/imagepicker/model/Document.kt | 8 ++++ .../com/esafirm/imagepicker/model/Folder.kt | 6 +++ .../com/esafirm/sample/CustomUIActivity.kt | 6 +++ 14 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 imagepicker/src/main/java/com/esafirm/imagepicker/model/Document.kt diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4c015b9e..360f1f73 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,7 +2,7 @@ name: Android CI on: pull_request: - branches: [main] + branches: [ main ] jobs: build: diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/adapter/FolderPickerAdapter.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/adapter/FolderPickerAdapter.kt index faef0af2..9cd41292 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/adapter/FolderPickerAdapter.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/adapter/FolderPickerAdapter.kt @@ -10,6 +10,7 @@ import com.esafirm.imagepicker.features.imageloader.ImageLoader import com.esafirm.imagepicker.features.imageloader.ImageType import com.esafirm.imagepicker.listeners.OnFolderClickListener import com.esafirm.imagepicker.model.Folder +import com.esafirm.imagepicker.model.FolderType class FolderPickerAdapter( context: Context, @@ -31,11 +32,15 @@ class FolderPickerAdapter( override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { val folder = folders.getOrNull(position) ?: return - imageLoader.loadImage(folder.images.first(), holder.image, ImageType.FOLDER) - holder.apply { name.text = folder.folderName - number.text = folder.images.size.toString() + + if (folder.type == FolderType.Local) { + imageLoader.loadImage(folder.images.first(), holder.image, ImageType.FOLDER) + number.text = folder.images.size.toString() + } else { + number.text = "Pick from Google Photos, Dropbox and other storages" + } itemView.setOnClickListener { folderClickListener(folder) } } } diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePicker.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePicker.kt index 169237f0..6e8d0274 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePicker.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePicker.kt @@ -2,6 +2,7 @@ package com.esafirm.imagepicker.features import android.content.Intent import com.esafirm.imagepicker.features.cameraonly.ImagePickerCameraOnly +import com.esafirm.imagepicker.model.Document import com.esafirm.imagepicker.model.Image object ImagePicker { @@ -17,4 +18,8 @@ object ImagePicker { fun getImages(intent: Intent?): List? { return intent?.getParcelableArrayListExtra(IpCons.EXTRA_SELECTED_IMAGES) } + + fun getDocuments(intent: Intent?): List? { + return intent?.getParcelableArrayListExtra(IpCons.EXTRA_SELECTED_DOCUMENTS) + } } \ No newline at end of file diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerActivity.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerActivity.kt index 8ece339f..fa5dd869 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerActivity.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerActivity.kt @@ -179,4 +179,9 @@ class ImagePickerActivity : AppCompatActivity(), ImagePickerInteractionListener setResult(RESULT_OK, result) finish() } -} + + override fun finishPickDocuments(result: Intent?) { + setResult(IpCons.DOCUMENT_PICKED_OK, result) + finish() + } +} \ No newline at end of file diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerFragment.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerFragment.kt index b6eda312..d6fd9219 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerFragment.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerFragment.kt @@ -31,7 +31,9 @@ import com.esafirm.imagepicker.helper.ImagePickerPreferences import com.esafirm.imagepicker.helper.ImagePickerUtils import com.esafirm.imagepicker.helper.IpLogger import com.esafirm.imagepicker.helper.state.fetch +import com.esafirm.imagepicker.model.Document import com.esafirm.imagepicker.model.Folder +import com.esafirm.imagepicker.model.FolderType import com.esafirm.imagepicker.model.Image class ImagePickerFragment : Fragment() { @@ -141,6 +143,7 @@ class ImagePickerFragment : Fragment() { } private fun subscribeToUiState() = presenter.getUiState().observe(this) { state -> + showLoading(state.isLoading) state.error.fetch { @@ -190,9 +193,13 @@ class ImagePickerFragment : Fragment() { resources.configuration.orientation ).apply { val selectListener = { isSelected: Boolean -> selectImage(isSelected) } + val folderClick = { bucket: Folder -> - setImageAdapter(bucket.images) - updateTitle() + if (bucket.type == FolderType.Shared) { + openDocumentsPickerPhotos() + } else { + setImageAdapter(bucket.images) + } } setupAdapters(passedSelectedImages, selectListener, folderClick) @@ -205,6 +212,16 @@ class ImagePickerFragment : Fragment() { } } + private fun openDocumentsPickerPhotos() { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "image/*" + + } + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + startActivityForResult(intent, RC_DOCUMENTS_PICKER) + } + override fun onResume() { super.onResume() loadDataWithPermission() @@ -321,7 +338,23 @@ class ImagePickerFragment : Fragment() { } else if (resultCode == Activity.RESULT_CANCELED) { presenter.abortCaptureImage(requireContext()) } + } else if (requestCode == RC_DOCUMENTS_PICKER) { + handleDocumentsPicker(data) + } + } + + private fun handleDocumentsPicker(data: Intent?) { + val documents = mutableListOf() + data?.data?.also { + documents.add(Document(uri = it)) + } ?: run { + val clipData = data?.clipData ?: return + for (clipIndex in 0 until clipData.itemCount) { + val uriAtIndex = clipData.getItemAt(clipIndex).uri + documents.add(Document(uriAtIndex)) + } } + interactionListener.finishPickDocuments(ImagePickerUtils.createResultIntentDocuments(documents)) } /** @@ -395,6 +428,7 @@ class ImagePickerFragment : Fragment() { private const val STATE_KEY_SELECTED_IMAGES = "Key.SelectedImages" private const val RC_CAPTURE = 2000 + private const val RC_DOCUMENTS_PICKER = 2001 fun newInstance(config: ImagePickerConfig): ImagePickerFragment { val args = Bundle().apply { diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerInteractionListener.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerInteractionListener.kt index a3e427ac..2fcb17f0 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerInteractionListener.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerInteractionListener.kt @@ -11,6 +11,8 @@ interface ImagePickerInteractionListener { // removes Images whose files no longer exist. fun finishPickImages(result: Intent?) + fun finishPickDocuments(result: Intent?) + /** * Called when the user selects or deselects sn image. Also called in onCreateView. * May include Images whose files no longer exist. diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerLauncher.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerLauncher.kt index 8073d701..4924b3f7 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerLauncher.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerLauncher.kt @@ -1,5 +1,6 @@ package com.esafirm.imagepicker.features +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity @@ -10,6 +11,7 @@ import com.esafirm.imagepicker.features.cameraonly.CameraOnlyConfig import com.esafirm.imagepicker.features.common.BaseConfig import com.esafirm.imagepicker.helper.ConfigUtils.checkConfig import com.esafirm.imagepicker.helper.LocaleManager +import com.esafirm.imagepicker.model.Document import com.esafirm.imagepicker.model.Image /* --------------------------------------------------- */ @@ -28,6 +30,7 @@ class ImagePickerLauncher( } typealias ImagePickerCallback = (List) -> Unit +typealias DocumentPickerCallback = (List) -> Unit fun Fragment.registerImagePicker( context: () -> Context = { requireContext() }, @@ -36,11 +39,28 @@ fun Fragment.registerImagePicker( return ImagePickerLauncher(context, createLauncher(callback)) } +/** + * Launcher for image picker. + * Usually you just want to specify the listener when images is picked + * + * ```kotlin + * val launcher = registerImagePicker { + * println(it.images) + * } + * + * launcher.launch(config) + * ``` + * + * @param context The context for the launcher in form of lambda + * @param documentCallback callback when images from document picker is selected + * @param callback callback when images from internal picker is selected + */ fun ComponentActivity.registerImagePicker( context: () -> Context = { this }, - callback: ImagePickerCallback + documentCallback: DocumentPickerCallback? = null, + callback: ImagePickerCallback, ): ImagePickerLauncher { - return ImagePickerLauncher(context, createLauncher(callback)) + return ImagePickerLauncher(context, createLauncher(documentCallback, callback)) } fun createImagePickerIntent(context: Context, config: BaseConfig): Intent { @@ -67,8 +87,13 @@ private fun Fragment.createLauncher(callback: ImagePickerCallback) = callback(images) } -private fun ComponentActivity.createLauncher(callback: ImagePickerCallback) = +private fun ComponentActivity.createLauncher(documentCallback: DocumentPickerCallback?, callback: ImagePickerCallback) = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - val images = ImagePicker.getImages(it.data) ?: emptyList() - callback(images) + if (it.resultCode == RESULT_OK) { + val images = ImagePicker.getImages(it.data) ?: emptyList() + callback(images) + } else if (it.resultCode == IpCons.DOCUMENT_PICKED_OK) { + val documents = ImagePicker.getDocuments(it.data) ?: emptyList() + documentCallback?.invoke(documents) + } } diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerPresenter.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerPresenter.kt index f62e005e..4d128f9e 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerPresenter.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/ImagePickerPresenter.kt @@ -14,6 +14,7 @@ import com.esafirm.imagepicker.helper.state.LiveDataObservableState import com.esafirm.imagepicker.helper.state.ObservableState import com.esafirm.imagepicker.helper.state.asSingleEvent import com.esafirm.imagepicker.model.Folder +import com.esafirm.imagepicker.model.FolderType import com.esafirm.imagepicker.model.Image import java.io.File @@ -42,10 +43,14 @@ internal class ImagePickerPresenter( imageLoader.abortLoadImages() imageLoader.loadDeviceImages(config, object : ImageLoaderListener { override fun onImageLoaded(images: List, folders: List) { + val otherFolder = Folder("More ...") + otherFolder.type = FolderType.Shared + val modifiedFolders = folders.toMutableList() + modifiedFolders.add(otherFolder) setState { ImagePickerState( images = images, - folders = folders, + folders = modifiedFolders, isLoading = false, isFolder = config.isFolderMode.asSingleEvent() ) diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/IpCons.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/IpCons.kt index 2c3f4aa9..21ff9dc1 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/IpCons.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/IpCons.kt @@ -6,4 +6,6 @@ object IpCons { @Deprecated("You should use the new API to start image picker") const val RC_IMAGE_PICKER = 0x229 const val EXTRA_SELECTED_IMAGES = "selectedImages" + const val DOCUMENT_PICKED_OK = 2001 + const val EXTRA_SELECTED_DOCUMENTS = "selectedDocuments" } \ No newline at end of file diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/features/fileloader/DefaultImageFileLoader.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/features/fileloader/DefaultImageFileLoader.kt index bd5ea4d0..a8826e70 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/features/fileloader/DefaultImageFileLoader.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/features/fileloader/DefaultImageFileLoader.kt @@ -226,5 +226,26 @@ class DefaultImageFileLoader(private val context: Context) : ImageFileLoader { null } } + + fun getImageFromURI(context: Context, uri: Uri): Image? { + val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME + ) + + val cursor = context.contentResolver.query( + uri, projection, null, null, null + ) + cursor?.let { + it.moveToFirst() + val idIndex = cursor.getColumnIndex(projection[0]) + val nameIndex = cursor.getColumnIndex(projection[1]) + val id = cursor.getLong(idIndex) + val name = cursor.getString(nameIndex) + it.close() + return Image(id, name, uri.path ?: "") + } + return null + } } } \ No newline at end of file diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/helper/ImagePickerUtils.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/helper/ImagePickerUtils.kt index 807585a2..12bf69d4 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/helper/ImagePickerUtils.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/helper/ImagePickerUtils.kt @@ -12,6 +12,7 @@ import android.webkit.MimeTypeMap import com.esafirm.imagepicker.features.ImagePickerSavePath import com.esafirm.imagepicker.features.IpCons import com.esafirm.imagepicker.helper.IpLogger.d +import com.esafirm.imagepicker.model.Document import com.esafirm.imagepicker.model.Image import java.io.File import java.net.URLConnection @@ -145,4 +146,11 @@ object ImagePickerUtils { data.putParcelableArrayListExtra(IpCons.EXTRA_SELECTED_IMAGES, imageArrayList) return data } + + fun createResultIntentDocuments(documents: List): Intent { + val data = Intent() + val documentsList = ArrayList(documents) + data.putParcelableArrayListExtra(IpCons.EXTRA_SELECTED_DOCUMENTS, documentsList) + return data + } } \ No newline at end of file diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/model/Document.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/model/Document.kt new file mode 100644 index 00000000..8d6111db --- /dev/null +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/model/Document.kt @@ -0,0 +1,8 @@ +package com.esafirm.imagepicker.model + +import android.net.Uri +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class Document(val uri: Uri) : Parcelable diff --git a/imagepicker/src/main/java/com/esafirm/imagepicker/model/Folder.kt b/imagepicker/src/main/java/com/esafirm/imagepicker/model/Folder.kt index 640e8067..6e9b54bc 100644 --- a/imagepicker/src/main/java/com/esafirm/imagepicker/model/Folder.kt +++ b/imagepicker/src/main/java/com/esafirm/imagepicker/model/Folder.kt @@ -1,5 +1,11 @@ package com.esafirm.imagepicker.model +enum class FolderType { + Local, + Shared +} + class Folder(var folderName: String) { var images: MutableList = mutableListOf() + var type: FolderType = FolderType.Local } \ No newline at end of file diff --git a/sample/src/main/java/com/esafirm/sample/CustomUIActivity.kt b/sample/src/main/java/com/esafirm/sample/CustomUIActivity.kt index 3440e9bd..39bf194f 100644 --- a/sample/src/main/java/com/esafirm/sample/CustomUIActivity.kt +++ b/sample/src/main/java/com/esafirm/sample/CustomUIActivity.kt @@ -15,6 +15,7 @@ import androidx.appcompat.widget.Toolbar import com.esafirm.imagepicker.features.ImagePickerConfig import com.esafirm.imagepicker.features.ImagePickerFragment import com.esafirm.imagepicker.features.ImagePickerInteractionListener +import com.esafirm.imagepicker.features.IpCons import com.esafirm.imagepicker.features.cameraonly.CameraOnlyConfig import com.esafirm.imagepicker.helper.ConfigUtils import com.esafirm.imagepicker.helper.IpLogger @@ -166,6 +167,11 @@ class CustomUIActivity : AppCompatActivity() { finish() } + override fun finishPickDocuments(result: Intent?) { + setResult(IpCons.DOCUMENT_PICKED_OK, result) + finish() + } + override fun selectionChanged(imageList: List?) { if (imageList == null) error("Image list is null")