diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 9228dc5ac..0a751d47b 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -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() 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) - } - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 5d28a46d1..fb3e49794 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -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(context) { - /** - * Image Loader for loading images. - */ - private val imageLoader = ImageLoader() +) : RecyclerViewAdapter(context) { /** * List of folders. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index a38200463..9029e03bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -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(context) { @@ -48,7 +56,7 @@ class ImageAdapter( private var images: ArrayList = 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. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index ffacde0e7..1d5901c9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -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. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index f4b5c9934..a2de0ed29 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 22da8cbbb..a3ae38e34 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -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 = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = 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 + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java index 01e68c940..c5eb101bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java @@ -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)); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index ff3f63eb8..5ad6952ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -77,7 +77,7 @@ class FileProcessor @Inject constructor( * * @return tags to be redacted */ - private fun getExifTagsToRedact(): Set { + fun getExifTagsToRedact(): Set { val prefManageEXIFTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() val redactTags: Set = @@ -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) { + fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set) { compositeDisposable.add( Observable.fromIterable(redactTags) .flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) }