Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Lassi is simplest way to pick media (either image, video, audio or doc)
* Enable/disable camera from LassiOption
* You can open System Default view for file selection by using MediaType.FILE_TYPE_WITH_SYSTEM_VIEW
* Photo Picker feature integration
* Group picked album images with camera capture for editing

# Usage

Expand Down Expand Up @@ -65,7 +66,7 @@ Lassi is simplest way to pick media (either image, video, audio or doc)

```kotlin
val intent = Lassi(this)
.with(LassiOption.CAMERA_AND_GALLERY) // choose Option CAMERA, GALLERY or CAMERA_AND_GALLERY
.with(LassiOption.CAMERA_AND_GALLERY) // choose Option CAMERA, GALLERY, CAMERA_AND_GALLERY or PICKER
.setMaxCount(5)
.setGridSize(3)
.setMediaType(MediaType.VIDEO) // MediaType : VIDEO IMAGE, AUDIO OR DOC
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/java/com/lassi/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
}

R.id.btnPhotoVideoPicker -> {
val intent = lassi.with(LassiOption.CAMERA_AND_GALLERY).setMaxCount(4)
val intent = lassi.with(LassiOption.PICKER).setMaxCount(4)
.setMediaType(MediaType.PHOTO_VIDEO_PICKER)
.setStatusBarColor(R.color.colorPrimaryDark)
.setToolbarColor(R.color.colorPrimary)
Expand All @@ -253,7 +253,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
}

R.id.btnPhotoPicker -> {
val intent = lassi.with(LassiOption.CAMERA_AND_GALLERY).setMaxCount(4)
val intent = lassi.with(LassiOption.PICKER).setMaxCount(4)
.setAscSort(SortingOption.ASCENDING).setGridSize(2)
.setMediaType(MediaType.PHOTO_PICKER)
.setPlaceHolder(R.drawable.ic_image_placeholder)
Expand All @@ -273,7 +273,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
}

R.id.btnVideoMediaPicker -> {
val intent = lassi.with(LassiOption.CAMERA_AND_GALLERY).setMaxCount(4)
val intent = lassi.with(LassiOption.PICKER).setMaxCount(4)
.setMediaType(MediaType.VIDEO_PICKER)
.setStatusBarColor(R.color.colorPrimaryDark)
.setToolbarColor(R.color.colorPrimary)
Expand Down
2 changes: 1 addition & 1 deletion lassi/src/main/java/com/lassi/domain/media/LassiConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ data class LassiConfig(
var maxCount: Int = KeyUtils.DEFAULT_MEDIA_COUNT,
var ascFlag: Int = KeyUtils.DEFAULT_ORDER,
var gridSize: Int = KeyUtils.DEFAULT_GRID_SIZE,
var lassiOption: LassiOption = LassiOption.CAMERA_AND_GALLERY,
var lassiOption: LassiOption = LassiOption.PICKER,
var minTime: Long = KeyUtils.DEFAULT_DURATION,
var maxTime: Long = KeyUtils.DEFAULT_DURATION,
var cropType: CropImageView.CropShape = CropImageView.CropShape.RECTANGLE,
Expand Down
3 changes: 2 additions & 1 deletion lassi/src/main/java/com/lassi/domain/media/LassiOption.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package com.lassi.domain.media
enum class LassiOption {
CAMERA,
GALLERY,
CAMERA_AND_GALLERY
CAMERA_AND_GALLERY,
PICKER
}
3 changes: 3 additions & 0 deletions lassi/src/main/java/com/lassi/presentation/builder/Lassi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ class Lassi(private val context: Context) {
* Allow Media picket to capture/record from camera while multiple media selection
*/
fun with(lassiOption: LassiOption): Lassi {
// Reset config to defaults for every new build chain to avoid leaking
// previous session state (e.g., isCrop from disableCrop()).
lassiConfig = LassiConfig()
lassiConfig.lassiOption = lassiOption
return this
}
Expand Down
184 changes: 127 additions & 57 deletions lassi/src/main/java/com/lassi/presentation/camera/CameraFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -55,43 +56,112 @@ import java.io.File
class CameraFragment : LassiBaseViewModelFragment<CameraViewModel, FragmentCameraBinding>(),
View.OnClickListener {

private val TAG: String = "CameraFragment"
private lateinit var cameraMode: Mode

private val config = LassiConfig.getConfig()
private val cameraViewModel by lazy {
ViewModelProvider(
this, SelectedMediaViewModelFactory(requireContext())
)[SelectedMediaViewModel::class.java]
}

private val folderViewModel by lazy {
ViewModelProvider(
this, FolderViewModelFactory(requireContext())
)[FolderViewModel::class.java]
}
private var currentCropIndex = 0
private var croppedMediaList: ArrayList<MiMedia> = ArrayList()
private var mediaList: ArrayList<MiMedia> = ArrayList()
private var selected =
LassiConfig.getConfig().selectedMedias // this gives the gallery selected images.
private var isFromCropNext = false

private val startVideoContract = registerForActivityResult(StartVideoContract()) { miMedia ->
private val cropImage = registerForActivityResult(CropImageContract()) { miMedia ->
if (LassiConfig.isSingleMediaSelection()) {
isFromCropNext = true
miMedia?.let { setResultOk(arrayListOf(it)) }
return@registerForActivityResult
}

if (miMedia != null) {
// Replace cropped into selected
selected[currentCropIndex] = miMedia

// Compress the cropped image
val compressed = compressSingleMedia(miMedia)
selected[currentCropIndex] = compressed

croppedMediaList.add(compressed)
}
cropNext()
}

private fun getSelectedAndCapturedImages() {
selected = config.selectedMedias
selected.addAll(mediaList)
}

private fun startCroppingSequence() {
if (selected.isNotEmpty()) {
currentCropIndex = 0
croppedMediaList.clear()
val firstPath = selected[0].path
if (!firstPath.isNullOrEmpty()) {
val uri = Uri.fromFile(File(firstPath))
croppingOptions(uri)
} else {
// skip invalid first path
cropImage.launch(null)
}
} else {
LassiConfig.getConfig().selectedMedias.add(miMedia!!)
cameraViewModel.addSelectedMedia(miMedia)
folderViewModel.checkInsert()
if (LassiConfig.getConfig().lassiOption == LassiOption.CAMERA_AND_GALLERY || LassiConfig.getConfig().lassiOption == LassiOption.GALLERY) {
setResultOk(arrayListOf(miMedia))
Log.d(TAG, "startCroppingSequence: selected is empty")
}
}

private fun cropNext() {
currentCropIndex++
if (currentCropIndex < selected.size) {
val path = selected[currentCropIndex].path
if (!path.isNullOrEmpty()) {
val uri = Uri.fromFile(File(path))
croppingOptions(uri)
} else {
cropNext()
}
} else {
isFromCropNext = true
setResultOk(selected)
}
}

private val cropImage = registerForActivityResult(CropImageContract()) { miMedia ->
private fun compressSingleMedia(miMedia: MiMedia): MiMedia {
miMedia.path?.let { path ->
val uri = Uri.fromFile(File(path))
val compressFormat = getCompressFormatForUri(uri, requireContext())
val newUri = writeBitmapToUri(
requireContext(),
decodeUriToBitmap(requireContext(), uri),
compressQuality = LassiConfig.getConfig().compressionRatio,
customOutputUri = null,
compressFormat = compressFormat
)
return miMedia.copy(path = newUri.path)
}
return miMedia // return original if path is null
}

private val folderViewModel by lazy {
ViewModelProvider(
this, FolderViewModelFactory(requireContext())
)[FolderViewModel::class.java]
}

private val startVideoContract = registerForActivityResult(StartVideoContract()) { miMedia ->
if (LassiConfig.isSingleMediaSelection()) {
isFromCropNext = true
miMedia?.let { setResultOk(arrayListOf(it)) }
} else {
LassiConfig.getConfig().selectedMedias.add(miMedia!!)
cameraViewModel.addSelectedMedia(miMedia)
folderViewModel.checkInsert()
if (LassiConfig.getConfig().lassiOption == LassiOption.CAMERA_AND_GALLERY || LassiConfig.getConfig().lassiOption == LassiOption.GALLERY) {
setResultOk(arrayListOf(miMedia))
parentFragmentManager.popBackStack()
}
}
}
Expand Down Expand Up @@ -170,15 +240,13 @@ class CameraFragment : LassiBaseViewModelFragment<CameraViewModel, FragmentCamer
viewModel.startVideoRecord.observe(this, SafeObserver(this::handleVideoRecord))
viewModel.cropImageLiveData.observe(this, SafeObserver { uri ->
val config = LassiConfig.getConfig()
if (config.isCrop && config.maxCount <= 1) {
croppingOptions(uri)
} else {
val mediaList = arrayListOf(createMiMedia(uri.path))
if (config.compressionRatio > 0) {
compressMedia(mediaList)
} else {
setResultOk(mediaList)
}

mediaList = arrayListOf(createMiMedia(uri.path))
croppedMediaList.addAll(config.selectedMedias + mediaList)
if (config.compressionRatio > 0 && !config.isCrop) {
compressMedia(croppedMediaList)
} else { // user has selected the crop option.
setResultOk(croppedMediaList)
}
})
}
Expand Down Expand Up @@ -206,36 +274,27 @@ class CameraFragment : LassiBaseViewModelFragment<CameraViewModel, FragmentCamer
setResultOk(mediaPaths)
}

private fun croppingOptions(
uri: Uri? = null,
includeCamera: Boolean? = false,
includeGallery: Boolean? = false
) {
/**
* Start picker to get image for cropping and then use the image in cropping activity.
*/
cropImage.launch(
includeCamera?.let {
includeGallery?.let { includeGallery ->
CropImageOptions(
imageSourceIncludeCamera = it,
imageSourceIncludeGallery = includeGallery,
cropShape = CropImageView.CropShape.RECTANGLE,
showCropOverlay = true,
guidelines = CropImageView.Guidelines.ON,
multiTouchEnabled = false,
outputCompressQuality = LassiConfig.getConfig().compressionRatio
)
}
}?.let {
CropImageContractOptions(
uri = uri,
cropImageOptions = it,
)
}
private fun croppingOptions(uri: Uri) {
val config = LassiConfig.getConfig()
val aspectX: Int = config.cropAspectRatio?.x ?: return
val aspectY: Int = config.cropAspectRatio?.y ?: return

val cropOptions = CropImageOptions(
imageSourceIncludeCamera = false,
imageSourceIncludeGallery = false,
cropShape = config.cropType,
showCropOverlay = true,
guidelines = CropImageView.Guidelines.ON,
multiTouchEnabled = false,
aspectRatioX = aspectX,
aspectRatioY = aspectY,
fixAspectRatio = config.enableActualCircleCrop,
outputCompressQuality = config.compressionRatio
)
}

val contractOptions = CropImageContractOptions(uri, cropOptions)
cropImage.launch(contractOptions)
}

private fun toggleCamera() {
binding.apply {
Expand All @@ -260,6 +319,7 @@ class CameraFragment : LassiBaseViewModelFragment<CameraViewModel, FragmentCamer
}
}
}

R.id.ivFlipCamera -> toggleCamera()
R.id.ivFlash -> {
//Check whether the flashlight is available or not?
Expand Down Expand Up @@ -408,14 +468,12 @@ class CameraFragment : LassiBaseViewModelFragment<CameraViewModel, FragmentCamer
) != PackageManager.PERMISSION_GRANTED

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {

needsStorage =
ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
} else

if (needsAudio)
needsAudio =
ActivityCompat.checkSelfPermission(
Expand All @@ -427,11 +485,23 @@ class CameraFragment : LassiBaseViewModelFragment<CameraViewModel, FragmentCamer
}

private fun setResultOk(selectedMedia: ArrayList<MiMedia>?) {
val intent = Intent().apply {
putExtra(KeyUtils.SELECTED_MEDIA, selectedMedia)
if (isFromCropNext || !config.isCrop) { // the !config.isCrop condition is given as when the crop is been disabled from the main activity, we don't go to the else part.
/**
* this will be when calling from the cropNext()
*/
val intent = Intent().apply {
putExtra(KeyUtils.SELECTED_MEDIA, selectedMedia)
}
activity?.setResult(Activity.RESULT_OK, intent)
activity?.finish()
isFromCropNext = false
} else {
/**
* everytime else
*/
getSelectedAndCapturedImages()
startCroppingSequence()
}
activity?.setResult(Activity.RESULT_OK, intent)
activity?.finish()
}

private fun requestForPermissions() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ class FolderFragment : LassiBaseViewModelFragment<FolderViewModel, FragmentMedia
override fun onPrepareOptionsMenu(menu: Menu) {
menu.findItem(R.id.menuCamera)?.isVisible =
if (LassiConfig.getConfig().mediaType == MediaType.IMAGE || LassiConfig.getConfig().mediaType == MediaType.VIDEO) {
(LassiConfig.getConfig().lassiOption == LassiOption.CAMERA_AND_GALLERY || LassiConfig.getConfig().lassiOption == LassiOption.CAMERA)
(LassiConfig.getConfig().lassiOption == LassiOption.CAMERA_AND_GALLERY || LassiConfig.getConfig().lassiOption == LassiOption.CAMERA || LassiConfig.getConfig().lassiOption == LassiOption.PICKER)
} else {
false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class LassiMediaPickerActivity :
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menuCamera?.isVisible =
(config.lassiOption == LassiOption.CAMERA ||
config.lassiOption == LassiOption.CAMERA_AND_GALLERY)
config.lassiOption == LassiOption.CAMERA_AND_GALLERY || config.lassiOption == LassiOption.PICKER)
menuDone?.isVisible = !viewModel.selectedMediaLiveData.value.isNullOrEmpty()
return super.onPrepareOptionsMenu(menu)
}
Expand Down