[GSoC] Show uploaded images differently. (#4464)

* uploaded images shown differently

* Loaded images before query

* Handled exceptions, Made ImageLoader injectable, Document and clean code
This commit is contained in:
Aditya-Srivastav 2021-06-22 03:53:30 +05:30 committed by GitHub
parent 746a660028
commit 7a6b24470e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 63 deletions

View file

@ -1,15 +1,20 @@
package fr.free.nrw.commons.customselector.helper package fr.free.nrw.commons.customselector.helper
import android.content.Context
import com.mapbox.android.core.FileUtils
import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.filepicker.Constants
import timber.log.Timber import timber.log.Timber
import java.io.* import java.io.*
import java.math.BigInteger import java.math.BigInteger
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashMap
/** /**
* Image Helper object, includes all the static functions required by custom selector. * Image Helper object, includes all the static functions required by custom selector.
*/ */
@ -68,7 +73,7 @@ object ImageHelper {
val indexes = arrayListOf<Int>() val indexes = arrayListOf<Int>()
for(image in list) { for(image in list) {
val index = getIndex(masterList,image) val index = getIndex(masterList, image)
if (index == -1) { if (index == -1) {
continue continue
} }
@ -76,47 +81,4 @@ object ImageHelper {
} }
return indexes return indexes
} }
/**
* Generates the file sha1 from file input stream.
*/
fun generateSHA1(`is`: InputStream): String {
val digest: MessageDigest = try {
MessageDigest.getInstance("SHA1")
} catch (e: NoSuchAlgorithmException) {
Timber.e(e, "Exception while getting Digest")
return ""
}
val buffer = ByteArray(8192)
var read: Int
return try {
while (`is`.read(buffer).also { read = it } > 0) {
digest.update(buffer, 0, read)
}
val md5sum = digest.digest()
val bigInt = BigInteger(1, md5sum)
var output = bigInt.toString(16)
output = String.format("%40s", output).replace(' ', '0')
Timber.i("File SHA1: %s", output)
output
} catch (e: IOException) {
Timber.e(e, "IO Exception")
""
} finally {
try {
`is`.close()
} catch (e: IOException) {
Timber.e(e, "Exception on closing input stream")
}
}
}
/**
* Gets the file input stream from the file path.
*/
@Throws(FileNotFoundException::class)
fun getFileInputStream(filePath: String?): InputStream {
return FileInputStream(filePath)
}
} }

View file

@ -11,7 +11,6 @@ import com.bumptech.glide.Glide
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
class FolderAdapter( class FolderAdapter(
/** /**
@ -23,12 +22,8 @@ class FolderAdapter(
* Folder Click listener for click events. * Folder Click listener for click events.
*/ */
private val itemClickListener: FolderClickListener private val itemClickListener: FolderClickListener
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
/** ) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
* Image Loader for loading images.
*/
private val imageLoader = ImageLoader()
/** /**
* List of folders. * List of folders.

View file

@ -6,6 +6,7 @@ import fr.free.nrw.commons.R
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.Group import androidx.constraintlayout.widget.Group
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -13,6 +14,7 @@ import com.bumptech.glide.Glide
import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
class ImageAdapter( class ImageAdapter(
/** /**
@ -23,7 +25,13 @@ class ImageAdapter(
/** /**
* Image select listener for click events on image. * Image select listener for click events on image.
*/ */
private var imageSelectListener: ImageSelectListener ): private var imageSelectListener: ImageSelectListener,
/**
* ImageLoader queries images.
*/
private var imageLoader: ImageLoader
):
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context) { RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context) {
@ -48,7 +56,7 @@ class ImageAdapter(
private var images: ArrayList<Image> = ArrayList() private var images: ArrayList<Image> = ArrayList()
/** /**
* create View holder. * Create View holder.
*/ */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false)
@ -69,6 +77,7 @@ class ImageAdapter(
holder.itemUnselected(); holder.itemUnselected();
} }
Glide.with(context).load(image.uri).into(holder.image) Glide.with(context).load(image.uri).into(holder.image)
imageLoader.queryAndSetView(holder,image)
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
selectOrRemoveImage(holder, position) selectOrRemoveImage(holder, position)
} }
@ -87,12 +96,12 @@ class ImageAdapter(
notifyItemChanged(index, ImageSelectedOrUpdated()) notifyItemChanged(index, ImageSelectedOrUpdated())
} }
} else { } else {
/** if(holder.isItemUploaded()){
* TODO Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show()
* Show toast on tapping an uploaded item. } else {
*/
selectedImages.add(images[position]) selectedImages.add(images[position])
notifyItemChanged(position, ImageSelectedOrUpdated()) notifyItemChanged(position, ImageSelectedOrUpdated())
}
} }
imageSelectListener.onSelectedImagesChanged(selectedImages) imageSelectListener.onSelectedImagesChanged(selectedImages)
} }
@ -150,6 +159,9 @@ class ImageAdapter(
uploadedGroup.visibility = View.VISIBLE uploadedGroup.visibility = View.VISIBLE
} }
fun isItemUploaded():Boolean {
return uploadedGroup.visibility == View.VISIBLE
}
/** /**
* Item Not Uploaded view. * Item Not Uploaded view.
*/ */

View file

@ -12,9 +12,10 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.CallbackStatus import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.upload.FileProcessor
import kotlinx.android.synthetic.main.fragment_custom_selector.* import kotlinx.android.synthetic.main.fragment_custom_selector.*
import kotlinx.android.synthetic.main.fragment_custom_selector.view.* import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
import javax.inject.Inject import javax.inject.Inject
@ -32,7 +33,11 @@ class FolderFragment : CommonsDaggerSupportFragment() {
var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null
@Inject set @Inject set
var fileProcessor: FileProcessor? = null
@Inject set
var mediaClient: MediaClient? = null
@Inject set
/** /**
* Folder Adapter. * Folder Adapter.
*/ */

View file

@ -36,6 +36,12 @@ class ImageFragment: CommonsDaggerSupportFragment() {
lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
@Inject set @Inject set
/**
* Image loader for adapter.
*/
var imageLoader: ImageLoader? = null
@Inject set
/** /**
* Image Adapter for recycle view. * Image Adapter for recycle view.
*/ */
@ -84,7 +90,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) val root = inflater.inflate(R.layout.fragment_custom_selector, container, false)
imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener) imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!)
gridLayoutManager = GridLayoutManager(context,getSpanCount()) gridLayoutManager = GridLayoutManager(context,getSpanCount())
with(root.selector_rv){ with(root.selector_rv){
this.layoutManager = gridLayoutManager this.layoutManager = gridLayoutManager
@ -118,6 +124,8 @@ class ImageFragment: CommonsDaggerSupportFragment() {
/** /**
* getSpanCount for GridViewManager. * getSpanCount for GridViewManager.
*
* @return spanCount.
*/ */
private fun getSpanCount(): Int { private fun getSpanCount(): Int {
return 3 return 3

View file

@ -1,7 +1,136 @@
package fr.free.nrw.commons.customselector.ui.selector package fr.free.nrw.commons.customselector.ui.selector
import android.content.Context
import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder
import fr.free.nrw.commons.filepicker.PickedFiles
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.util.*
import javax.inject.Inject
import kotlin.collections.HashMap
/** /**
* Image Loader class, loads images, depending on API results. * Image Loader class, loads images, depending on API results.
*/ */
class ImageLoader { class ImageLoader @Inject constructor(
/**
* MediaClient for SHA1 query.
*/
var mediaClient: MediaClient,
/**
* FileProcessor to pre-process the file.
*/
var fileProcessor: FileProcessor,
/**
* File Utils Wrapper for SHA1
*/
var fileUtilsWrapper: FileUtilsWrapper,
/**
* Context for coroutine.
*/
val context: Context) {
/**
* Maps to facilitate image query.
*/
private var mapImageSHA1: HashMap<Image,String> = HashMap()
private var mapHolderImage : HashMap<ImageViewHolder,Image> = HashMap()
private var mapResult: HashMap<String,Boolean> = HashMap()
/**
* Query image and setUp the view.
*/
fun queryAndSetView(holder: ImageViewHolder, image: Image){
/**
* Recycler view uses same view holder, so we can identify the latest query image from holder.
*/
mapHolderImage[holder] = image
holder.itemNotUploaded()
CoroutineScope(Dispatchers.Main).launch {
var value = false
withContext(Dispatchers.Default) {
if(mapHolderImage[holder] != image) {
// View holder has a new query image, terminate this query.
return@withContext
}
val sha1 = getSHA1(image)
if(mapHolderImage[holder] != image) {
// View holder has a new query image, terminate this query.
return@withContext
}
value = querySHA1(sha1)
}
if(mapHolderImage[holder] == image) {
// View holder and latest query image match, setup the view.
if (value) {
holder.itemUploaded()
} else {
holder.itemNotUploaded()
}
}
}
}
/**
* Query SHA1, return result if previously queried, otherwise start a new query.
*
* @return Query result.
*/
private fun querySHA1(SHA1: String): Boolean {
if(mapResult[SHA1] != null) {
return mapResult[SHA1]!!
}
val isUploaded = mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()
mapResult[SHA1] = isUploaded
return isUploaded
}
/**
* Get SHA1, return SHA1 if available, otherwise generate and store the SHA1.
*
* @return sha1 of the image
*/
private fun getSHA1(image: Image): String{
if(mapImageSHA1[image] != null) {
return mapImageSHA1[image]!!
}
val sha1 = generateModifiedSHA1(image);
mapImageSHA1[image] = sha1;
return sha1;
}
/**
* Generate Modified SHA1 using present Exif settings.
*
* @return modified sha1
*/
private fun generateModifiedSHA1(image: Image) : String {
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
val exifInterface: ExifInterface? = try {
ExifInterface(uploadableFile.file!!)
} catch (e: IOException) {
Timber.e(e)
null
}
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
val sha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath))
uploadableFile.file.delete()
return sha1
}
} }

View file

@ -25,7 +25,7 @@ import java.util.UUID;
import timber.log.Timber; import timber.log.Timber;
class PickedFiles implements Constants { public class PickedFiles implements Constants {
private static String getFolderName(@NonNull Context context) { private static String getFolderName(@NonNull Context context) {
return FilePicker.configuration(context).getFolderName(); return FilePicker.configuration(context).getFolderName();
@ -104,7 +104,7 @@ class PickedFiles implements Constants {
}); });
} }
static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions
InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri); InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri);
File directory = tempImageDirectory(context); File directory = tempImageDirectory(context);
File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri));

View file

@ -77,7 +77,7 @@ class FileProcessor @Inject constructor(
* *
* @return tags to be redacted * @return tags to be redacted
*/ */
private fun getExifTagsToRedact(): Set<String> { fun getExifTagsToRedact(): Set<String> {
val prefManageEXIFTags = val prefManageEXIFTags =
defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet()
val redactTags: Set<String> = val redactTags: Set<String> =
@ -91,7 +91,7 @@ class FileProcessor @Inject constructor(
* @param exifInterface ExifInterface object * @param exifInterface ExifInterface object
* @param redactTags tags to be redacted * @param redactTags tags to be redacted
*/ */
private fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set<String>) { fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set<String>) {
compositeDisposable.add( compositeDisposable.add(
Observable.fromIterable(redactTags) Observable.fromIterable(redactTags)
.flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) } .flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) }