Skip to content

Commit

Permalink
Fix stuff (#370)
Browse files Browse the repository at this point in the history
* bump gradle and AGP version

* fix broken snackbar

* add test for fragment picker

* set min sdk to 21

* internal: simple permission requests

* fix blinking folder mode

* fix no camera available

remove blank image

* add test for camera inside picker

* add try catch for video duration label
  • Loading branch information
esafirm committed Jun 20, 2021
1 parent c7f6a2e commit 742d7b3
Show file tree
Hide file tree
Showing 21 changed files with 301 additions and 147 deletions.
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ buildscript {
ext.kotlin_version = '1.4.32'
ext.core_ktx_version = '1.3.2'
repositories {
jcenter()
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
Expand All @@ -16,7 +16,7 @@ ext {
sdk = [
compileSdk: 30,
targetSdk : 30,
minSdk : 14
minSdk : 21
]
}

Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
6 changes: 6 additions & 0 deletions imagepicker/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@

</application>

<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.esafirm.imagepicker.R
import com.esafirm.imagepicker.adapter.ImagePickerAdapter.ImageViewHolder
Expand Down Expand Up @@ -71,7 +70,8 @@ class ImagePickerAdapter(

if (ImagePickerUtils.isVideoFormat(image)) {
if (!videoDurationHolder.containsKey(image.id)) {
val uri = Uri.withAppendedPath(MediaStore.Files.getContentUri("external"), "" + image.id)
val uri =
Uri.withAppendedPath(MediaStore.Files.getContentUri("external"), "" + image.id)
videoDurationHolder[image.id] = ImagePickerUtils.getVideoDurationLabel(
context, uri
)
Expand Down Expand Up @@ -141,7 +141,7 @@ class ImagePickerAdapter(
this.imageSelectedListener = imageSelectedListener
}

fun getItem(position: Int) = listDiffer.currentList.getOrNull(position)
private fun getItem(position: Int) = listDiffer.currentList.getOrNull(position)

class ImageViewHolder(itemView: View) : ViewHolder(itemView) {
val imageView: ImageView = itemView.image_view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package com.esafirm.imagepicker.features

import com.esafirm.imagepicker.helper.state.ObservableState
import com.esafirm.imagepicker.helper.state.SingleEvent
import com.esafirm.imagepicker.helper.state.asSingleEvent
import com.esafirm.imagepicker.model.Folder
import com.esafirm.imagepicker.model.Image

data class ImagePickerState(
val images: List<Image> = emptyList(),
val folders: List<Folder> = emptyList(),
// TODO: handle the transitions between folder and images in the view state as well
val isFolder: SingleEvent<Boolean>? = null,
val isLoading: Boolean = false,
val error: SingleEvent<Throwable>? = null,
val finishPickImage: SingleEvent<List<Image>>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ class ImagePickerActivity : AppCompatActivity(), ImagePickerInteractionListener

if (savedInstanceState != null) {
// The fragment has been restored.
imagePickerFragment = supportFragmentManager.findFragmentById(R.id.ef_imagepicker_fragment_placeholder) as ImagePickerFragment
imagePickerFragment =
supportFragmentManager.findFragmentById(R.id.ef_imagepicker_fragment_placeholder) as ImagePickerFragment
} else {
imagePickerFragment = ImagePickerFragment.newInstance(currentConfig)
val ft = supportFragmentManager.beginTransaction()
Expand Down Expand Up @@ -140,6 +141,7 @@ class ImagePickerActivity : AppCompatActivity(), ImagePickerInteractionListener
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_CANCELED) {
cameraModule.removeImage(this)
setResult(RESULT_CANCELED)
finish()
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
Expand Down Expand Up @@ -45,6 +46,17 @@ class ImagePickerFragment : Fragment() {
requireArguments().getParcelable(ImagePickerConfig::class.java.simpleName)!!
}

private val requestPermissionLauncher =
registerForActivityResult(RequestPermission()) { isGranted ->
if (isGranted) {
IpLogger.d("Write External permission granted")
loadData()
} else {
IpLogger.e("Permission not granted")
interactionListener.cancel()
}
}

private lateinit var presenter: ImagePickerPresenter
private lateinit var interactionListener: ImagePickerInteractionListener

Expand Down Expand Up @@ -127,10 +139,13 @@ class ImagePickerFragment : Fragment() {
return@observe
}

if (config.isFolderMode) {
setFolderAdapter(state.folders)
} else {
setImageAdapter(state.images)
state.isFolder.fetch {
val isFolderMode = this
if (isFolderMode) {
setFolderAdapter(state.folders)
} else {
setImageAdapter(state.images)
}
}

state.finishPickImage.fetch {
Expand Down Expand Up @@ -249,50 +264,20 @@ class ImagePickerFragment : Fragment() {
*/
private fun requestWriteExternalPermission() {
IpLogger.w("Write External permission is not granted. Requesting permission")
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (ActivityCompat.shouldShowRequestPermissionRationale(
requireActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
requestPermissions(permissions, RC_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE)
} else {
val permission = ImagePickerPreferences.PREF_WRITE_EXTERNAL_STORAGE_REQUESTED
if (!preferences.isPermissionRequested(permission)) {
preferences.setPermissionRequested(permission)
requestPermissions(permissions, RC_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE)
} else {
binding?.efSnackbar?.show(R.string.ef_msg_no_write_external_permission) {
openAppSettings()
}
}
}
}

/**
* Handle permission results
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
RC_PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
IpLogger.d("Write External permission granted")
loadData()
return
}
IpLogger.e(
"Permission not granted: results len = " + grantResults.size +
" Result code = " + if (grantResults.isNotEmpty()) grantResults[0] else "(empty)"
)
interactionListener.cancel()
val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE
when {
shouldShowRequestPermissionRationale(permission) -> {
requestPermissionLauncher.launch(permission)
}
else -> {
IpLogger.d("Got unexpected permission result: $requestCode")
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (!preferences.isPermissionRequested()) {
preferences.setPermissionIsRequested()
requestPermissionLauncher.launch(permission)
} else {
binding?.efSnackbar?.show(R.string.ef_msg_no_write_external_permission) {
openAppSettings()
}
}
}
}
}
Expand All @@ -319,7 +304,7 @@ class ImagePickerFragment : Fragment() {
if (resultCode == Activity.RESULT_OK) {
presenter.finishCaptureImage(requireContext(), data, config)
} else if (resultCode == Activity.RESULT_CANCELED) {
presenter.abortCaptureImage()
presenter.abortCaptureImage(requireContext())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ internal class ImagePickerPresenter(

private val cameraModule: CameraModule = ImagePickerComponentsHolder.cameraModule

private val stateObs = LiveDataObservableState(ImagePickerState(isLoading = true), usePostValue = true)
private val stateObs = LiveDataObservableState(
ImagePickerState(isLoading = true),
usePostValue = true
)

private fun setState(newState: ImagePickerState.() -> ImagePickerState) {
stateObs.set(newState(stateObs.get()))
Expand All @@ -43,7 +46,8 @@ internal class ImagePickerPresenter(
ImagePickerState(
images = images,
folders = folders,
isLoading = false
isLoading = false,
isFolder = config.isFolderMode.asSingleEvent()
)
}
}
Expand Down Expand Up @@ -81,7 +85,11 @@ internal class ImagePickerPresenter(
val context = fragment.requireContext().applicationContext
val intent = cameraModule.getCameraIntent(fragment.requireContext(), config)
if (intent == null) {
Toast.makeText(context, context.getString(R.string.ef_error_create_image_file), Toast.LENGTH_LONG).show()
Toast.makeText(
context,
context.getString(R.string.ef_error_create_image_file),
Toast.LENGTH_LONG
).show()
return
}
fragment.startActivityForResult(intent, requestCode)
Expand All @@ -101,7 +109,7 @@ internal class ImagePickerPresenter(
}
}

fun abortCaptureImage() {
cameraModule.removeImage()
fun abortCaptureImage(context: Context) {
cameraModule.removeImage(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import android.widget.Toast
import com.esafirm.imagepicker.R

object CameraHelper {
@JvmStatic
fun checkCameraAvailability(context: Context): Boolean {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val isAvailable = intent.resolveActivity(context.packageManager) != null
if (!isAvailable) {
val appContext = context.applicationContext
Toast.makeText(appContext,
appContext.getString(R.string.ef_error_no_camera), Toast.LENGTH_LONG).show()
Toast.makeText(
appContext,
appContext.getString(R.string.ef_error_no_camera), Toast.LENGTH_LONG
).show()
}
return isAvailable
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ typealias OnImageReadyListener = (List<Image>?) -> Unit
interface CameraModule {
fun getCameraIntent(context: Context, config: BaseConfig): Intent?
fun getImage(context: Context, intent: Intent?, imageReadyListener: OnImageReadyListener)
fun removeImage()
fun removeImage(context: Context)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,34 @@ class DefaultCameraModule : CameraModule {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
}
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val collection =
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
return appContext.contentResolver.insert(collection, values)
}
return UriUtils.uriForFile(appContext, imageFile)
}

override fun getImage(context: Context, intent: Intent?, imageReadyListener: OnImageReadyListener) {
override fun getImage(
context: Context,
intent: Intent?,
imageReadyListener: OnImageReadyListener
) {
if (currentImagePath == null) {
IpLogger.w("currentImagePath null. " +
"This happen if you haven't call #getCameraIntent() or the activity is being recreated")
IpLogger.w(
"currentImagePath null. " +
"This happen if you haven't call #getCameraIntent() or the activity is being recreated"
)
imageReadyListener.invoke(null)
return
}

val imageUri = Uri.parse(currentImagePath)
if (imageUri != null) {
MediaScannerConnection.scanFile(context.applicationContext, arrayOf(imageUri.path), null) { path: String?, uri: Uri? ->
MediaScannerConnection.scanFile(
context.applicationContext,
arrayOf(imageUri.path),
null
) { path: String?, uri: Uri? ->
IpLogger.d("File $path was scanned successfully: $uri")

if (path == null) {
Expand All @@ -82,11 +93,20 @@ class DefaultCameraModule : CameraModule {
}
}

override fun removeImage() {
override fun removeImage(context: Context) {
val imagePath = currentImagePath ?: return
val file = File(imagePath)
if (file.exists()) {
file.delete()
}

// Delete in the media store
try {
val uri = currentUri?.let { Uri.parse(it) } ?: return
context.applicationContext.contentResolver.delete(uri, null, null)
} catch (e: Exception) {
IpLogger.e("Can't delete cancelled uri")
e.printStackTrace()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ class RecyclerViewManager(

/* Init folder and image adapter */
val imageLoader = ImagePickerComponentsHolder.imageLoader
imageAdapter = ImagePickerAdapter(context, imageLoader, selectedImages
?: emptyList(), onImageClick)
imageAdapter = ImagePickerAdapter(
context, imageLoader, selectedImages
?: emptyList(), onImageClick
)
folderAdapter = FolderPickerAdapter(context, imageLoader) {
foldersState = recyclerView.layoutManager?.onSaveInstanceState()
onFolderClick(it)
Expand Down Expand Up @@ -135,7 +137,11 @@ class RecyclerViewManager(
return if (config.limit == IpCons.MAX_LIMIT) {
String.format(context.getString(R.string.ef_selected), imageSize)
} else {
String.format(context.getString(R.string.ef_selected_with_limit), imageSize, config.limit)
String.format(
context.getString(R.string.ef_selected_with_limit),
imageSize,
config.limit
)
}
}

Expand All @@ -155,7 +161,10 @@ class RecyclerViewManager(
}
}

/* --------------------------------------------------- */ /* > Images */ /* --------------------------------------------------- */
/* --------------------------------------------------- */
/* > Images */
/* --------------------------------------------------- */

private fun checkAdapterIsInitialized() {
if (!::imageAdapter.isInitialized) {
error("Must call setupAdapters first!")
Expand Down

0 comments on commit 742d7b3

Please sign in to comment.