[GSoC] Hide/Unhide actioned pictures and change numbering (#5012)

* Changed numbering of marked images

* Hide Unhide implemented

* Test fixed

* Improved speed for database operation

* Improved speed for database operation

* Changed progress dialog

* Improved hiding speed

* Test fixed

* Fixed bug

* Fixed bug and improved performance

* Fixed bug and improved performance

* Test fixed

* Bug fixed

* Bug fixed

* Bug fixed

* Bug fixed

* Bug fixed

* Code clean up

* Test hiding images

* Test hiding images

* Test hiding images

* Code clean up and test fixed

* Fixed layout

* Fixed bug

* Bug fixed

* Renamed method

* Documentation added explaining logic

* Documentation added explaining logic
This commit is contained in:
Ayan Sarkar 2022-08-20 14:49:27 +05:30 committed by GitHub
parent 5559282c1a
commit a6c51a75a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 638 additions and 163 deletions

View file

@ -47,6 +47,19 @@ abstract class UploadedStatusDao {
insert(uploadedStatus) insert(uploadedStatus)
} }
/**
* Check whether the imageSHA1 is present in database
*/
@Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ")
abstract suspend fun findByImageSHA1(imageSHA1 : String, imageResult: Boolean): Int
/**
* Check whether the modifiedImageSHA1 is present in database
*/
@Query("SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ")
abstract suspend fun findByModifiedImageSHA1(modifiedImageSHA1 : String,
modifiedImageResult: Boolean): Int
/** /**
* Asynchronous image sha1 query. * Asynchronous image sha1 query.
*/ */

View file

@ -4,10 +4,20 @@ import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Image
/** /**
* Image Helper object, includes all the static functions required by custom selector. * Image Helper object, includes all the static functions and variables required by custom selector.
*/ */
object ImageHelper { object ImageHelper {
/**
* Custom selector preference key
*/
const val CUSTOM_SELECTOR_PREFERENCE_KEY: String = "custom_selector"
/**
* Switch state preference key
*/
const val SWITCH_STATE_PREFERENCE_KEY: String = "switch_state"
/** /**
* Returns the list of folders from given image list. * Returns the list of folders from given image list.
*/ */

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.customselector.ui.adapter package fr.free.nrw.commons.customselector.ui.adapter
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
@ -13,9 +14,14 @@ import com.bumptech.glide.Glide
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.helper.ImageHelper.SWITCH_STATE_PREFERENCE_KEY
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 import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
import kotlinx.coroutines.*
import java.util.*
import kotlin.collections.ArrayList
/** /**
* Custom selector ImageAdapter. * Custom selector ImageAdapter.
@ -49,6 +55,8 @@ class ImageAdapter(
*/ */
class ImageUnselected class ImageUnselected
private var stopAddition: Boolean = false
/** /**
* Currently selected images. * Currently selected images.
*/ */
@ -64,6 +72,35 @@ class ImageAdapter(
*/ */
private var images: ArrayList<Image> = ArrayList() private var images: ArrayList<Image> = ArrayList()
/**
* Stores all images
*/
private var allImages: List<Image> = ArrayList()
/**
* Map to store actionable images
*/
private var mapActionableImages: TreeMap<Int, Image> = TreeMap()
/**
* Stores already added positions of actionable images
*/
private var alreadyAddedPositions: ArrayList<Int> = ArrayList()
/**
* Next starting index to initiate query to find next actionable image
*/
private var nextImage = 0
private var count = 0
/**
* Coroutine Dispatchers and Scope.
*/
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
private val scope : CoroutineScope = MainScope()
/** /**
* Create View holder. * Create View holder.
*/ */
@ -76,7 +113,7 @@ class ImageAdapter(
* Bind View holder, load image, selected view, click listeners. * Bind View holder, load image, selected view, click listeners.
*/ */
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val image=images[position] var image=images[position]
holder.image.setImageDrawable (null) holder.image.setImageDrawable (null)
if (context.contentResolver.getType(image.uri) == null) { if (context.contentResolver.getType(image.uri) == null) {
// Image does not exist anymore, update adapter. // Image does not exist anymore, update adapter.
@ -87,17 +124,106 @@ class ImageAdapter(
notifyItemRangeChanged(updatedPosition, images.size) notifyItemRangeChanged(updatedPosition, images.size)
} }
} else { } else {
val selectedIndex = ImageHelper.getIndex(selectedImages, image) val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
// Getting selected index when switch is on
val selectedIndex: Int = if (switchState) {
ImageHelper.getIndex(selectedImages, image)
// Getting selected index when switch is off
} else if (mapActionableImages.size > position) {
ImageHelper
.getIndex(selectedImages, ArrayList(mapActionableImages.values)[position])
// For any other case return -1
} else {
-1
}
val isSelected = selectedIndex != -1 val isSelected = selectedIndex != -1
if (isSelected) { if (isSelected) {
holder.itemSelected(selectedIndex + 1) holder.itemSelected(selectedImages.size)
} else { } else {
holder.itemUnselected(); holder.itemUnselected()
} }
Glide.with(holder.image).load(image.uri).thumbnail(0.3f).into(holder.image)
imageLoader.queryAndSetView(holder, image) scope.launch {
imageLoader.queryAndSetView(
holder, image, ioDispatcher, defaultDispatcher
)
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
if (!switchState) {
// If the position is not already visited, that means the position is new then
// finds the next actionable image position from all images
if(!alreadyAddedPositions.contains(position)) {
val next = imageLoader.nextActionableImage(
allImages, ioDispatcher, defaultDispatcher,
nextImage
)
// If next actionable image is found, saves it, as the the search for
// finding next actionable image will start from this position
if (next > -1) {
nextImage = next+1
// If map doesn't contains the next actionable image, that means it's a
// new actionable image, if will put it to the map as actionable images
// and it will load the new image in the view holder
if (!mapActionableImages.containsKey(next)) {
mapActionableImages[next] = allImages[next]
alreadyAddedPositions.add(count)
count++
Glide.with(holder.image).load(allImages[next].uri)
.thumbnail(0.3f).into(holder.image)
notifyItemInserted(position)
notifyItemRangeChanged(position, itemCount+1)
}
// If next actionable image is not found, that means searching is
// complete till end, and it will stop searching.
} else {
stopAddition = true
notifyItemRemoved(position)
}
// If the position is already visited, that means the image is already present
// inside map, so it will fetch the image from the map and load in the holder
} else {
val actionableImages: List<Image> = ArrayList(mapActionableImages.values)
image = actionableImages[position]
Glide.with(holder.image).load(image.uri)
.thumbnail(0.3f).into(holder.image)
}
// If switch is turned off, it just fetches the image from all images without any
// further operations
} else {
Glide.with(holder.image).load(image.uri)
.thumbnail(0.3f).into(holder.image)
}
}
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
selectOrRemoveImage(holder, position) val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
// While switch is turned off, lets user click on image only if the position is
// added inside map
if (!switchState) {
if (mapActionableImages.size > position) {
selectOrRemoveImage(holder, position)
}
} else {
selectOrRemoveImage(holder, position)
}
} }
// launch media preview on long click. // launch media preview on long click.
@ -112,26 +238,60 @@ class ImageAdapter(
* Handle click event on an image, update counter on images. * Handle click event on an image, update counter on images.
*/ */
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){ private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){
val clickedIndex = ImageHelper.getIndex(selectedImages, images[position]) val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
// Getting clicked index from all images index when switch is on
val clickedIndex: Int = if(switchState) {
ImageHelper.getIndex(selectedImages, images[position])
// Getting clicked index from actionable images when switch is off
} else {
ImageHelper.getIndex(selectedImages, ArrayList(mapActionableImages.values)[position])
}
if (clickedIndex != -1) { if (clickedIndex != -1) {
selectedImages.removeAt(clickedIndex) selectedImages.removeAt(clickedIndex)
if (holder.isItemNotForUpload()) { if (holder.isItemNotForUpload()) {
selectedNotForUploadImages-- selectedNotForUploadImages--
} }
notifyItemChanged(position, ImageUnselected()) notifyItemChanged(position, ImageUnselected())
val indexes = ImageHelper.getIndexList(selectedImages, images)
// Getting index from all images index when switch is on
val indexes = if (switchState) {
ImageHelper.getIndexList(selectedImages, images)
// Getting index from actionable images when switch is off
} else {
ImageHelper.getIndexList(selectedImages, ArrayList(mapActionableImages.values))
}
for (index in indexes) { for (index in indexes) {
notifyItemChanged(index, ImageSelectedOrUpdated()) notifyItemChanged(index, ImageSelectedOrUpdated())
} }
} else { } else {
if(holder.isItemUploaded()){ if (holder.isItemUploaded()) {
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
} else { } else {
if (holder.isItemNotForUpload()) { if (holder.isItemNotForUpload()) {
selectedNotForUploadImages++ selectedNotForUploadImages++
} }
selectedImages.add(images[position])
notifyItemChanged(position, ImageSelectedOrUpdated()) // Getting index from all images index when switch is on
val indexes: ArrayList<Int> = if (switchState) {
selectedImages.add(images[position])
ImageHelper.getIndexList(selectedImages, images)
// Getting index from actionable images when switch is off
} else {
selectedImages.add(ArrayList(mapActionableImages.values)[position])
ImageHelper.getIndexList(selectedImages, ArrayList(mapActionableImages.values))
}
for (index in indexes) {
notifyItemChanged(index, ImageSelectedOrUpdated())
}
} }
} }
imageSelectListener.onSelectedImagesChanged(selectedImages, selectedNotForUploadImages) imageSelectListener.onSelectedImagesChanged(selectedImages, selectedNotForUploadImages)
@ -140,9 +300,16 @@ class ImageAdapter(
/** /**
* Initialize the data set. * Initialize the data set.
*/ */
fun init(newImages: List<Image>) { fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>) {
allImages = fixedImages
val oldImageList:ArrayList<Image> = images val oldImageList:ArrayList<Image> = images
val newImageList:ArrayList<Image> = ArrayList(newImages) val newImageList:ArrayList<Image> = ArrayList(newImages)
mapActionableImages = emptyMap
alreadyAddedPositions = ArrayList()
nextImage = 0
stopAddition = false
selectedImages = ArrayList()
count = 0
val diffResult = DiffUtil.calculateDiff( val diffResult = DiffUtil.calculateDiff(
ImagesDiffCallback(oldImageList, newImageList) ImagesDiffCallback(oldImageList, newImageList)
) )
@ -153,12 +320,12 @@ class ImageAdapter(
/** /**
* Refresh the data in the adapter * Refresh the data in the adapter
*/ */
fun refresh(newImages: List<Image>) { fun refresh(newImages: List<Image>, fixedImages: List<Image>) {
selectedNotForUploadImages = 0 selectedNotForUploadImages = 0
selectedImages.clear() selectedImages.clear()
images.clear() images.clear()
selectedImages = arrayListOf() selectedImages = arrayListOf()
init(newImages) init(newImages, fixedImages, TreeMap())
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -168,13 +335,38 @@ class ImageAdapter(
* @return The total number of items in this adapter. * @return The total number of items in this adapter.
*/ */
override fun getItemCount(): Int { override fun getItemCount(): Int {
return images.size val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
// While switch is on initializes the holder with all images size
return if(switchState) {
allImages.size
// While switch is off and searching for next actionable has ended, initializes the holder
// with size of all actionable images
} else if (mapActionableImages.size == allImages.size || stopAddition) {
mapActionableImages.size
// While switch is off, initializes the holder with and extra view holder so that finding
// and addition of the next actionable image in the adapter can be continued
} else {
mapActionableImages.size + 1
}
} }
fun getImageIdAt(position: Int): Long { fun getImageIdAt(position: Int): Long {
return images.get(position).id return images.get(position).id
} }
/**
* CleanUp function.
*/
fun cleanUp() {
scope.cancel()
}
/** /**
* Image view holder. * Image view holder.
*/ */

View file

@ -1,19 +1,26 @@
package fr.free.nrw.commons.customselector.ui.selector package fr.free.nrw.commons.customselector.ui.selector
import android.app.Activity import android.app.Activity
import android.net.Uri import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Switch
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.helper.ImageHelper.SWITCH_STATE_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.listeners.RefreshUIListener import fr.free.nrw.commons.customselector.listeners.RefreshUIListener
import fr.free.nrw.commons.customselector.model.CallbackStatus import fr.free.nrw.commons.customselector.model.CallbackStatus
@ -21,13 +28,16 @@ import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
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.theme.BaseActivity import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
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 java.io.File import kotlinx.coroutines.*
import java.io.FileInputStream import java.util.*
import java.net.URI
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
/** /**
* Custom Selector Image Fragment. * Custom Selector Image Fragment.
@ -54,8 +64,14 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
*/ */
private var selectorRV: RecyclerView? = null private var selectorRV: RecyclerView? = null
private var loader: ProgressBar? = null private var loader: ProgressBar? = null
private var switch: Switch? = null
lateinit var filteredImages: ArrayList<Image>; lateinit var filteredImages: ArrayList<Image>;
/**
* Stores all images
*/
var allImages: ArrayList<Image> = ArrayList()
/** /**
* View model Factory. * View model Factory.
*/ */
@ -78,9 +94,48 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
*/ */
private lateinit var gridLayoutManager: GridLayoutManager private lateinit var gridLayoutManager: GridLayoutManager
/**
* For showing progress
*/
private var progressLayout: ConstraintLayout? = null
/**
* NotForUploadStatus Dao class for database operations
*/
@Inject
lateinit var notForUploadStatusDao: NotForUploadStatusDao
/**
* UploadedStatus Dao class for database operations
*/
@Inject
lateinit var uploadedStatusDao: UploadedStatusDao
/**
* FileUtilsWrapper class to get imageSHA1 from uri
*/
@Inject
lateinit var fileUtilsWrapper: FileUtilsWrapper
/**
* FileProcessor to pre-process the file.
*/
@Inject
lateinit var fileProcessor: FileProcessor
/**
* MediaClient for SHA1 query.
*/
@Inject
lateinit var mediaClient: MediaClient
companion object { companion object {
/**
* Switch state
*/
var switchState: Boolean = true
/** /**
* BucketId args name * BucketId args name
*/ */
@ -131,12 +186,50 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
handleResult(it) handleResult(it)
}) })
switch = root.switchWidget
switch?.visibility = View.VISIBLE
switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) }
selectorRV = root.selector_rv selectorRV = root.selector_rv
loader = root.loader loader = root.loader
progressLayout = root.progressLayout
val sharedPreferences: SharedPreferences =
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
switchState = sharedPreferences.getBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
switch?.isChecked = switchState
switch?.text =
if (switchState) getString(R.string.hide_already_actioned_pictures)
else getString(R.string.show_already_actioned_pictures)
return root return root
} }
private fun onChangeSwitchState(checked: Boolean) {
if (checked) {
switchState = true
val sharedPreferences: SharedPreferences =
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putBoolean(SWITCH_STATE_PREFERENCE_KEY, true)
editor.apply()
switch?.text = getString(R.string.hide_already_actioned_pictures)
imageAdapter.init(allImages, allImages, TreeMap())
imageAdapter.notifyDataSetChanged()
} else {
switchState = false
val sharedPreferences: SharedPreferences =
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putBoolean(SWITCH_STATE_PREFERENCE_KEY, false)
editor.apply()
switch?.text = getString(R.string.show_already_actioned_pictures)
imageAdapter.init(allImages, allImages, TreeMap())
imageAdapter.notifyDataSetChanged()
}
}
/** /**
* Attaching data listener * Attaching data listener
*/ */
@ -157,7 +250,12 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
val images = result.images val images = result.images
if(images.isNotEmpty()) { if(images.isNotEmpty()) {
filteredImages = ImageHelper.filterImages(images, bucketId) filteredImages = ImageHelper.filterImages(images, bucketId)
imageAdapter.init(filteredImages) allImages = ArrayList(filteredImages)
if(switchState) {
imageAdapter.init(filteredImages, allImages, TreeMap())
} else {
imageAdapter.init(filteredImages, allImages, TreeMap())
}
selectorRV?.let { selectorRV?.let {
it.visibility = View.VISIBLE it.visibility = View.VISIBLE
lastItemId?.let { pos -> lastItemId?.let { pos ->
@ -205,7 +303,7 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
* Save the Image Fragment state. * Save the Image Fragment state.
*/ */
override fun onDestroy() { override fun onDestroy() {
imageLoader?.cleanUP() imageAdapter.cleanUp()
val position = (selectorRV?.layoutManager as GridLayoutManager) val position = (selectorRV?.layoutManager as GridLayoutManager)
.findFirstVisibleItemPosition() .findFirstVisibleItemPosition()
@ -227,6 +325,6 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
} }
override fun refresh() { override fun refresh() {
imageAdapter.refresh(filteredImages) imageAdapter.refresh(filteredImages, allImages)
} }
} }

View file

@ -1,27 +1,23 @@
package fr.free.nrw.commons.customselector.ui.selector package fr.free.nrw.commons.customselector.ui.selector
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatus import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder 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.media.MediaClient
import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.utils.CustomSelectorUtils import fr.free.nrw.commons.utils.CustomSelectorUtils
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.querySHA1
import kotlinx.coroutines.* import kotlinx.coroutines.*
import timber.log.Timber
import java.io.FileNotFoundException
import java.io.IOException
import java.net.UnknownHostException
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject 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.
@ -67,106 +63,179 @@ class ImageLoader @Inject constructor(
private var mapResult: HashMap<String, Result> = HashMap() private var mapResult: HashMap<String, Result> = HashMap()
private var mapImageSHA1: HashMap<Uri, String> = HashMap() private var mapImageSHA1: HashMap<Uri, String> = HashMap()
/**
* Coroutine Dispatchers and Scope.
*/
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
private val scope : CoroutineScope = MainScope()
/** /**
* Query image and setUp the view. * Query image and setUp the view.
*/ */
fun queryAndSetView(holder: ImageViewHolder, image: Image) { suspend fun queryAndSetView(
holder: ImageViewHolder,
image: Image,
ioDispatcher: CoroutineDispatcher,
defaultDispatcher: CoroutineDispatcher
) {
/** /**
* Recycler view uses same view holder, so we can identify the latest query image from holder. * Recycler view uses same view holder, so we can identify the latest query image from holder.
*/ */
mapHolderImage[holder] = image mapHolderImage[holder] = image
holder.itemNotUploaded() holder.itemNotUploaded()
holder.itemForUpload()
scope.launch { var result: Result = Result.NOTFOUND
var result: Result = Result.NOTFOUND if (mapHolderImage[holder] != image) {
return
}
if (mapHolderImage[holder] != image) { val imageSHA1: String = when(mapImageSHA1[image.uri] != null) {
return@launch true -> mapImageSHA1[image.uri]!!
else -> CustomSelectorUtils.getImageSHA1(
image.uri,
ioDispatcher,
fileUtilsWrapper,
context.contentResolver
)
}
mapImageSHA1[image.uri] = imageSHA1
if(imageSHA1.isEmpty()) {
return
}
val uploadedStatus = getFromUploaded(imageSHA1)
val sha1 = uploadedStatus?.let {
result = getResultFromUploadedStatus(uploadedStatus)
uploadedStatus.modifiedImageSHA1
} ?: run {
if (mapHolderImage[holder] == image) {
getSHA1(image, defaultDispatcher)
} else {
""
} }
}
val imageSHA1: String = when(mapImageSHA1[image.uri] != null) { if (mapHolderImage[holder] != image) {
true -> mapImageSHA1[image.uri]!! return
else -> CustomSelectorUtils.getImageSHA1(image.uri, ioDispatcher, fileUtilsWrapper, context.contentResolver) }
}
if(imageSHA1.isEmpty()) val exists = notForUploadStatusDao.find(imageSHA1)
return@launch
val uploadedStatus = getFromUploaded(imageSHA1)
val sha1 = uploadedStatus?.let { if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
result = getResultFromUploadedStatus(uploadedStatus) when {
uploadedStatus.modifiedImageSHA1 mapResult[imageSHA1] == null -> {
} ?: run { // Query original image.
if (mapHolderImage[holder] == image) { result = querySHA1(imageSHA1, ioDispatcher, mediaClient)
getSHA1(image) when (result) {
} else { is Result.TRUE -> {
"" mapResult[imageSHA1] = Result.TRUE
} }
}
if (mapHolderImage[holder] != image) {
return@launch
}
val exists = notForUploadStatusDao.find(imageSHA1)
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
// Query original image.
result = querySHA1(imageSHA1)
if (result is Result.TRUE) {
// Original image found.
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
} else {
// Original image not found, query modified image.
result = querySHA1(sha1)
if (result != Result.ERROR) {
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
} }
} }
else -> {
result = mapResult[imageSHA1]!!
}
} }
if(mapHolderImage[holder] == image) { if (result is Result.TRUE) {
if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded() // Original image found.
if (exists > 0) holder.itemNotForUpload() else holder.itemForUpload() insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
} else {
when {
mapResult[sha1] == null -> {
// Original image not found, query modified image.
result = querySHA1(sha1, ioDispatcher, mediaClient)
when (result) {
is Result.TRUE -> {
mapResult[sha1] = Result.TRUE
}
}
}
else -> {
result = mapResult[sha1]!!
}
}
if (result != Result.ERROR) {
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
}
} }
} }
val sharedPreferences: SharedPreferences =
context
.getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(ImageHelper.SWITCH_STATE_PREFERENCE_KEY, true)
if(mapHolderImage[holder] == image) {
if (result is Result.TRUE) {
if (switchState) holder.itemUploaded() else holder.itemNotUploaded()
} else holder.itemNotUploaded()
if (exists > 0) {
if (switchState) holder.itemNotForUpload() else holder.itemForUpload()
} else holder.itemForUpload()
}
} }
/** /**
* Query SHA1, return result if previously queried, otherwise start a new query. * Finds out the next actionable image position
*
* @return Query result.
*/ */
suspend fun nextActionableImage(
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
defaultDispatcher: CoroutineDispatcher,
nextImagePosition: Int
): Int {
var next = -1
suspend fun querySHA1(SHA1: String): Result { // Traversing from given position to the end
return withContext(ioDispatcher) { for (i in nextImagePosition until allImages.size){
mapResult[SHA1]?.let { val it = allImages[i]
return@withContext it val imageSHA1: String = when (mapImageSHA1[it.uri] != null) {
true -> mapImageSHA1[it.uri]!!
else -> CustomSelectorUtils.getImageSHA1(
it.uri,
ioDispatcher,
fileUtilsWrapper,
context.contentResolver
)
} }
var result: Result = Result.FALSE next = notForUploadStatusDao.find(imageSHA1)
try {
if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { // After checking the image in the not for upload database, if the image is present then
mapResult[SHA1] = Result.TRUE // skips the image and moves to next image for checking
result = Result.TRUE if(next > 0){
continue
// Otherwise checks in already uploaded database
} else {
next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
// If the image is not present in the already uploaded database, checks for it's
// modified SHA1 in already uploaded database
if (next <= 0) {
val modifiedImageSha1 = getSHA1(it, defaultDispatcher)
next = uploadedStatusDao.findByModifiedImageSHA1(
modifiedImageSha1,
true
)
// If the modified image SHA1 is not present in the already uploaded database,
// returns the position as next actionable image position
if (next <= 0) {
return i
// If present in tha db then skips iteration for the image and moves to the next
// for checking
} else {
continue
}
// If present in tha db then skips iteration for the image and moves to the next
// for checking
} else {
continue
} }
} catch (e: Exception) {
if (e is UnknownHostException) {
// Handle no network connection.
Timber.e(e, "Network Connection Error")
}
result = Result.ERROR
e.printStackTrace()
} }
result
} }
return -1
} }
/** /**
@ -174,11 +243,17 @@ class ImageLoader @Inject constructor(
* *
* @return sha1 of the image * @return sha1 of the image
*/ */
suspend fun getSHA1(image: Image): String { suspend fun getSHA1(image: Image, defaultDispatcher: CoroutineDispatcher): String {
mapModifiedImageSHA1[image]?.let{ mapModifiedImageSHA1[image]?.let{
return it return it
} }
val sha1 = generateModifiedSHA1(image); val sha1 = CustomSelectorUtils
.generateModifiedSHA1(image,
defaultDispatcher,
context,
fileProcessor,
fileUtilsWrapper
)
mapModifiedImageSHA1[image] = sha1; mapModifiedImageSHA1[image] = sha1;
return sha1; return sha1;
} }
@ -221,35 +296,6 @@ class ImageLoader @Inject constructor(
return Result.INVALID return Result.INVALID
} }
/**
* Generate Modified SHA1 using present Exif settings.
*
* @return modified sha1
*/
private suspend fun generateModifiedSHA1(image: Image) : String {
return withContext(defaultDispatcher) {
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()
sha1
}
}
/**
* CleanUp function.
*/
fun cleanUP() {
scope.cancel()
}
/** /**
* Sealed Result class. * Sealed Result class.
*/ */

View file

@ -1,17 +1,28 @@
package fr.free.nrw.commons.utils package fr.free.nrw.commons.utils
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
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 fr.free.nrw.commons.upload.FileUtilsWrapper
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException
import java.net.UnknownHostException
/** /**
* Util Class for Custom Selector * Util Class for Custom Selector
*/ */
class CustomSelectorUtils { class CustomSelectorUtils {
companion object { companion object {
/** /**
* Get image sha1 from uri, used to retrieve the original image sha1. * Get image sha1 from uri, used to retrieve the original image sha1.
*/ */
@ -31,5 +42,60 @@ class CustomSelectorUtils {
} }
} }
} }
/**
* Generate Modified SHA1 using present Exif settings.
*
* @return modified sha1
*/
suspend fun generateModifiedSHA1(image: Image,
defaultDispatcher : CoroutineDispatcher,
context: Context,
fileProcessor: FileProcessor,
fileUtilsWrapper: FileUtilsWrapper
) : String {
return withContext(defaultDispatcher) {
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()
sha1
}
}
/**
* Query SHA1, return result if previously queried, otherwise start a new query.
*
* @return Query result.
*/
suspend fun querySHA1(SHA1: String,
ioDispatcher : CoroutineDispatcher,
mediaClient: MediaClient
): ImageLoader.Result {
return withContext(ioDispatcher) {
var result: ImageLoader.Result = ImageLoader.Result.FALSE
try {
if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) {
result = ImageLoader.Result.TRUE
}
} catch (e: Exception) {
if (e is UnknownHostException) {
// Handle no network connection.
Timber.e(e, "Network Connection Error")
}
result = ImageLoader.Result.ERROR
e.printStackTrace()
}
result
}
}
} }
} }

View file

@ -3,13 +3,26 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/mainBackground">
<Switch
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/hide_already_actioned_pictures"
android:padding="@dimen/dimen_10"
android:checked="true" />
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
android:id="@+id/selector_rv" android:id="@+id/selector_rv"
android:background="?attr/mainBackground"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:background="?attr/mainBackground"
android:layout_height="@dimen/dimen_0"
app:fastScrollPopupBgColor="@color/primaryColor" app:fastScrollPopupBgColor="@color/primaryColor"
app:fastScrollPopupTextColor="@android:color/primary_text_dark" app:fastScrollPopupTextColor="@android:color/primary_text_dark"
app:fastScrollPopupTextSize="@dimen/subheading_text_size" app:fastScrollPopupTextSize="@dimen/subheading_text_size"
@ -17,6 +30,10 @@
app:fastScrollThumbColor="@color/primaryColor" app:fastScrollThumbColor="@color/primaryColor"
app:fastScrollTrackColor="@color/upload_overlay_background_light" app:fastScrollTrackColor="@color/upload_overlay_background_light"
app:fastScrollPopupPosition="adjacent" app:fastScrollPopupPosition="adjacent"
app:layout_constraintBottom_toTopOf="@id/progressLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/switchWidget"
/> />
<TextView <TextView
@ -45,4 +62,41 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
/> />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/progressLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/dimen_5"
android:background="@color/drawerHeader_background_light"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/selector_rv">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hiding_already_actioned_pictures"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center"
android:padding="@dimen/dimen_5"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -736,4 +736,7 @@ Upload your first media by tapping on the add button.</string>
<string name="your_feedback">Your feedback</string> <string name="your_feedback">Your feedback</string>
<string name="mark_as_not_for_upload">Mark as not for upload</string> <string name="mark_as_not_for_upload">Mark as not for upload</string>
<string name="unmark_as_not_for_upload">Unmark as not for upload</string> <string name="unmark_as_not_for_upload">Unmark as not for upload</string>
<string name="show_already_actioned_pictures">Show already actioned pictures</string>
<string name="hide_already_actioned_pictures">Hide already actioned pictures</string>
<string name="hiding_already_actioned_pictures">Hiding already actioned pictures</string>
</resources> </resources>

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.customselector.ui.adapter
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -23,6 +24,8 @@ import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import java.lang.reflect.Field import java.lang.reflect.Field
import java.util.*
import kotlin.collections.ArrayList
/** /**
* Custom Selector image adapter test. * Custom Selector image adapter test.
@ -88,8 +91,10 @@ class ImageAdapterTest {
// Parameters. // Parameters.
images.add(image) images.add(image)
imageAdapter.init(images) imageAdapter.init(images, images, TreeMap())
whenever(context.getSharedPreferences("custom_selector", 0))
.thenReturn(Mockito.mock(SharedPreferences::class.java))
// Test conditions. // Test conditions.
imageAdapter.onBindViewHolder(holder, 0) imageAdapter.onBindViewHolder(holder, 0)
selectedImageField.set(imageAdapter, images) selectedImageField.set(imageAdapter, images)
@ -101,7 +106,7 @@ class ImageAdapterTest {
*/ */
@Test @Test
fun init() { fun init() {
imageAdapter.init(images) imageAdapter.init(images, images, TreeMap())
} }
/** /**
@ -115,7 +120,7 @@ class ImageAdapterTest {
// Parameters // Parameters
images.addAll(listOf(image, image)) images.addAll(listOf(image, image))
imageAdapter.init(images) imageAdapter.init(images, images, TreeMap())
// Test conditions // Test conditions
holder.itemUploaded() holder.itemUploaded()
@ -142,7 +147,7 @@ class ImageAdapterTest {
*/ */
@Test @Test
fun getImageIdAt() { fun getImageIdAt() {
imageAdapter.init(listOf(image)) imageAdapter.init(listOf(image), listOf(image), TreeMap())
Assertions.assertEquals(1, imageAdapter.getImageIdAt(0)) Assertions.assertEquals(1, imageAdapter.getImageIdAt(0))
} }
} }

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.customselector.ui.selector
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.TestCommonsApplication
@ -117,8 +118,6 @@ class ImageLoaderTest {
Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1); Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1);
Whitebox.setInternalState(imageLoader, "mapResult", mapResult); Whitebox.setInternalState(imageLoader, "mapResult", mapResult);
Whitebox.setInternalState(imageLoader, "context", context) Whitebox.setInternalState(imageLoader, "context", context)
Whitebox.setInternalState(imageLoader, "ioDispatcher", testDispacher)
Whitebox.setInternalState(imageLoader, "defaultDispatcher", testDispacher)
whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream)
whenever(context.contentResolver).thenReturn(contentResolver) whenever(context.contentResolver).thenReturn(contentResolver)
@ -141,14 +140,17 @@ class ImageLoaderTest {
@Test @Test
fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest { fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest {
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null) whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
mapModifiedImageSHA1[image] = "testSha1" mapModifiedImageSHA1[image] = "testSha1"
mapImageSHA1[uri] = "testSha1" mapImageSHA1[uri] = "testSha1"
whenever(context.getSharedPreferences("custom_selector", 0))
.thenReturn(Mockito.mock(SharedPreferences::class.java))
mapResult["testSha1"] = ImageLoader.Result.TRUE mapResult["testSha1"] = ImageLoader.Result.TRUE
imageLoader.queryAndSetView(holder, image) imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
mapResult["testSha1"] = ImageLoader.Result.FALSE mapResult["testSha1"] = ImageLoader.Result.FALSE
imageLoader.queryAndSetView(holder, image) imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
} }
/** /**
@ -157,20 +159,10 @@ class ImageLoaderTest {
@Test @Test
fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest { fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest {
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus) whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus)
imageLoader.queryAndSetView(holder, image) whenever(notForUploadStatusDao.find(any())).thenReturn(0)
} whenever(context.getSharedPreferences("custom_selector", 0))
.thenReturn(Mockito.mock(SharedPreferences::class.java))
/** imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
* Test querySha1
*/
@Test
fun testQuerySha1() = testDispacher.runBlockingTest {
whenever(single.blockingGet()).thenReturn(true)
whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single)
whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1")
imageLoader.querySHA1("testSha1")
} }
/** /**
@ -188,13 +180,13 @@ class ImageLoaderTest {
whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream) whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream)
whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1")
Assert.assertEquals("testSha1", imageLoader.getSHA1(image)); Assert.assertEquals("testSha1", imageLoader.getSHA1(image, testDispacher));
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn( whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
uploadableFile uploadableFile
) )
mapModifiedImageSHA1[image] = "testSha2" mapModifiedImageSHA1[image] = "testSha2"
Assert.assertEquals("testSha2", imageLoader.getSHA1(image)); Assert.assertEquals("testSha2", imageLoader.getSHA1(image, testDispacher));
} }
/** /**
@ -218,8 +210,4 @@ class ImageLoaderTest {
imageLoader.getResultFromUploadedStatus(uploadedStatus)) imageLoader.getResultFromUploadedStatus(uploadedStatus))
} }
@Test
fun testCleanUP() {
imageLoader.cleanUP()
}
} }