[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 Aditya Srivastava
parent c7dae69ddf
commit 088f66ead0
8 changed files with 174 additions and 63 deletions

View file

@ -1,15 +1,20 @@
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.Image
import fr.free.nrw.commons.filepicker.Constants
import timber.log.Timber
import java.io.*
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.LinkedHashMap
/**
* Image Helper object, includes all the static functions required by custom selector.
*/
@ -68,7 +73,7 @@ object ImageHelper {
val indexes = arrayListOf<Int>()
for(image in list) {
val index = getIndex(masterList,image)
val index = getIndex(masterList, image)
if (index == -1) {
continue
}
@ -76,47 +81,4 @@ object ImageHelper {
}
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.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
class FolderAdapter(
/**
@ -23,12 +22,8 @@ class FolderAdapter(
* Folder Click listener for click events.
*/
private val itemClickListener: FolderClickListener
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
/**
* Image Loader for loading images.
*/
private val imageLoader = ImageLoader()
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
/**
* List of folders.

View file

@ -6,6 +6,7 @@ import fr.free.nrw.commons.R
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.Group
import androidx.recyclerview.widget.DiffUtil
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.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
class ImageAdapter(
/**
@ -23,7 +25,13 @@ class ImageAdapter(
/**
* 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) {
@ -48,7 +56,7 @@ class ImageAdapter(
private var images: ArrayList<Image> = ArrayList()
/**
* create View holder.
* Create View holder.
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false)
@ -69,6 +77,7 @@ class ImageAdapter(
holder.itemUnselected();
}
Glide.with(context).load(image.uri).into(holder.image)
imageLoader.queryAndSetView(holder,image)
holder.itemView.setOnClickListener {
selectOrRemoveImage(holder, position)
}
@ -87,12 +96,12 @@ class ImageAdapter(
notifyItemChanged(index, ImageSelectedOrUpdated())
}
} else {
/**
* TODO
* Show toast on tapping an uploaded item.
*/
if(holder.isItemUploaded()){
Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show()
} else {
selectedImages.add(images[position])
notifyItemChanged(position, ImageSelectedOrUpdated())
}
}
imageSelectListener.onSelectedImagesChanged(selectedImages)
}
@ -150,6 +159,9 @@ class ImageAdapter(
uploadedGroup.visibility = View.VISIBLE
}
fun isItemUploaded():Boolean {
return uploadedGroup.visibility == View.VISIBLE
}
/**
* 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.listeners.FolderClickListener
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.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.view.*
import javax.inject.Inject
@ -32,7 +33,11 @@ class FolderFragment : CommonsDaggerSupportFragment() {
var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null
@Inject set
var fileProcessor: FileProcessor? = null
@Inject set
var mediaClient: MediaClient? = null
@Inject set
/**
* Folder Adapter.
*/

View file

@ -36,6 +36,12 @@ class ImageFragment: CommonsDaggerSupportFragment() {
lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
@Inject set
/**
* Image loader for adapter.
*/
var imageLoader: ImageLoader? = null
@Inject set
/**
* Image Adapter for recycle view.
*/
@ -84,7 +90,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
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())
with(root.selector_rv){
this.layoutManager = gridLayoutManager
@ -118,6 +124,8 @@ class ImageFragment: CommonsDaggerSupportFragment() {
/**
* getSpanCount for GridViewManager.
*
* @return spanCount.
*/
private fun getSpanCount(): Int {
return 3

View file

@ -1,7 +1,136 @@
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.
*/
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;
class PickedFiles implements Constants {
public class PickedFiles implements Constants {
private static String getFolderName(@NonNull Context context) {
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);
File directory = tempImageDirectory(context);
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
*/
private fun getExifTagsToRedact(): Set<String> {
fun getExifTagsToRedact(): Set<String> {
val prefManageEXIFTags =
defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet()
val redactTags: Set<String> =
@ -91,7 +91,7 @@ class FileProcessor @Inject constructor(
* @param exifInterface ExifInterface object
* @param redactTags tags to be redacted
*/
private fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set<String>) {
fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set<String>) {
compositeDisposable.add(
Observable.fromIterable(redactTags)
.flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) }