mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
[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:
parent
c7dae69ddf
commit
088f66ead0
8 changed files with 174 additions and 63 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue