diff --git a/app/build.gradle b/app/build.gradle
index 684ad65df..ddd786bac 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -122,6 +122,7 @@ dependencies {
implementation "androidx.exifinterface:exifinterface:1.3.2"
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
implementation "androidx.multidex:multidex:2.0.1"
+ compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
//swipe_layout
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cf245ed7c..803945f9a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -56,7 +56,10 @@
android:finishOnTaskLaunch="true" />
+ android:name=".media.ZoomableActivity"
+ android:label="Zoomable Activity"
+ android:configChanges="screenSize|keyboard|orientation"
+ android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadDao.kt
new file mode 100644
index 000000000..d6c9235ce
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadDao.kt
@@ -0,0 +1,57 @@
+package fr.free.nrw.commons.customselector.database
+
+import androidx.room.*
+
+
+/**
+ * Dao class for Not For Upload
+ */
+@Dao
+abstract class NotForUploadStatusDao {
+
+ /**
+ * Insert into Not For Upload status.
+ */
+ @Insert( onConflict = OnConflictStrategy.REPLACE )
+ abstract suspend fun insert(notForUploadStatus: NotForUploadStatus)
+
+ /**
+ * Delete Not For Upload status entry.
+ */
+ @Delete
+ abstract suspend fun delete(notForUploadStatus: NotForUploadStatus)
+
+ /**
+ * Query Not For Upload status with image sha1.
+ */
+ @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
+ abstract suspend fun getFromImageSHA1(imageSHA1 : String) : NotForUploadStatus?
+
+ /**
+ * Asynchronous image sha1 query.
+ */
+ suspend fun getNotForUploadFromImageSHA1(imageSHA1: String):NotForUploadStatus? {
+ return getFromImageSHA1(imageSHA1)
+ }
+
+ /**
+ * Deletion Not For Upload status with image sha1.
+ */
+ @Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
+ abstract suspend fun deleteWithImageSHA1(imageSHA1 : String)
+
+ /**
+ * Asynchronous image sha1 deletion.
+ */
+ suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) {
+ return deleteWithImageSHA1(imageSHA1)
+ }
+
+ /**
+ * Check whether the imageSHA1 is present in database
+ */
+ @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
+ abstract suspend fun find(imageSHA1 : String): Int
+}
+
+
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt
new file mode 100644
index 000000000..31d48af97
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatus.kt
@@ -0,0 +1,16 @@
+package fr.free.nrw.commons.customselector.database
+
+import androidx.room.*
+
+/**
+ * Entity class for Not For Upload status.
+ */
+@Entity(tableName = "images_not_for_upload_table")
+data class NotForUploadStatus(
+
+ /**
+ * Original image sha1.
+ */
+ @PrimaryKey
+ val imageSHA1 : String
+)
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt
index 49a1f61c3..70454cc9e 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt
@@ -47,10 +47,24 @@ abstract class UploadedStatusDao {
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.
*/
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
return getFromImageSHA1(imageSHA1)
}
+
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/CustomSelectorConstants.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/CustomSelectorConstants.kt
new file mode 100644
index 000000000..f28a1c613
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/CustomSelectorConstants.kt
@@ -0,0 +1,15 @@
+package fr.free.nrw.commons.customselector.helper
+
+/**
+ * Stores constants related to custom image selector
+ */
+object CustomSelectorConstants {
+
+ const val BUCKET_ID = "bucket_id"
+ const val TOTAL_SELECTED_IMAGES = "total_selected_images"
+ const val PRESENT_POSITION = "present_position"
+ const val NEW_SELECTED_IMAGES = "new_selected_images"
+ const val SHOULD_REFRESH = "should_refresh"
+ const val FULL_SCREEN_MODE_FIRST_LUNCH = "full_screen_mode_first_launch"
+
+}
\ No newline at end of file
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 06ec4c36c..f59d8a844 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
@@ -4,10 +4,20 @@ import fr.free.nrw.commons.customselector.model.Folder
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 {
+ /**
+ * Custom selector preference key
+ */
+ const val CUSTOM_SELECTOR_PREFERENCE_KEY: String = "custom_selector"
+
+ /**
+ * Switch state preference key
+ */
+ const val SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY: String = "show_already_actioned_images"
+
/**
* Returns the list of folders from given image list.
*/
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt
new file mode 100644
index 000000000..89cbb8fb4
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListener.kt
@@ -0,0 +1,103 @@
+package fr.free.nrw.commons.customselector.helper
+
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.*
+import kotlin.math.abs
+
+/**
+ * Class for detecting swipe gestures
+ */
+open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
+
+ private val gestureDetector: GestureDetector
+
+ private val SWIPE_THRESHOLD_HEIGHT = (getScreenResolution(context!!)).second / 3
+ private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
+ private val SWIPE_VELOCITY_THRESHOLD = 1000
+
+ override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
+ return gestureDetector.onTouchEvent(motionEvent)
+ }
+
+ fun getScreenResolution(context: Context): Pair {
+ val wm: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val display: Display = wm.getDefaultDisplay()
+ val metrics = DisplayMetrics()
+ display.getMetrics(metrics)
+ val width: Int = metrics.widthPixels
+ val height: Int = metrics.heightPixels
+ return width to height
+ }
+
+ inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
+
+ override fun onDown(e: MotionEvent?): Boolean {
+ return true
+ }
+
+ /**
+ * Detects the gestures
+ */
+ override fun onFling(
+ event1: MotionEvent,
+ event2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ try {
+ val diffY: Float = event2.y - event1.y
+ val diffX: Float = event2.x - event1.x
+ if (abs(diffX) > abs(diffY)) {
+ if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) >
+ SWIPE_VELOCITY_THRESHOLD) {
+ if (diffX > 0) {
+ onSwipeRight()
+ } else {
+ onSwipeLeft()
+ }
+ }
+ } else {
+ if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT && abs(velocityY) >
+ SWIPE_VELOCITY_THRESHOLD) {
+ if (diffY > 0) {
+ onSwipeDown()
+ } else {
+ onSwipeUp()
+ }
+ }
+ }
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ return false
+ }
+ }
+
+ /**
+ * Swipe right to view previous image
+ */
+ open fun onSwipeRight() {}
+
+ /**
+ * Swipe left to view next image
+ */
+ open fun onSwipeLeft() {}
+
+ /**
+ * Swipe up to select the picture (the equivalent of tapping it in non-fullscreen mode)
+ * and show the next picture skipping pictures that have either already been uploaded or
+ * marked as not for upload
+ */
+ open fun onSwipeUp() {}
+
+ /**
+ * Swipe down to mark that picture as "Not for upload" (the equivalent of selecting it then
+ * tapping "Mark as not for upload" in non-fullscreen mode), and show the next picture.
+ */
+ open fun onSwipeDown() {}
+
+ init {
+ gestureDetector = GestureDetector(context, GestureListener())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
index 1d7310b1d..d6349eb49 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
@@ -11,12 +11,17 @@ interface ImageSelectListener {
/**
* onSelectedImagesChanged
* @param selectedImages : new selected images.
+ * @param selectedNotForUploadImages : number of selected not for upload images
*/
- fun onSelectedImagesChanged(selectedImages: ArrayList)
+ fun onSelectedImagesChanged(selectedImages: ArrayList, selectedNotForUploadImages: Int)
/**
* onLongPress
* @param imageUri : uri of image
*/
- fun onLongPress(imageUri: Uri)
+ fun onLongPress(
+ position: Int,
+ images: ArrayList,
+ selectedImages: ArrayList
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/PassDataListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/PassDataListener.kt
new file mode 100644
index 000000000..d2110d208
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/PassDataListener.kt
@@ -0,0 +1,10 @@
+package fr.free.nrw.commons.customselector.listeners
+
+import fr.free.nrw.commons.customselector.model.Image
+
+/**
+ * Interface to pass data between fragment and activity
+ */
+interface PassDataListener {
+ fun passSelectedImages(selectedImages: ArrayList, shouldRefresh: Boolean)
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/RefreshUIListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/RefreshUIListener.kt
new file mode 100644
index 000000000..d271c1d0b
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/RefreshUIListener.kt
@@ -0,0 +1,11 @@
+package fr.free.nrw.commons.customselector.listeners
+
+/**
+ * Refresh UI Listener
+ */
+interface RefreshUIListener {
+ /**
+ * Refreshes the data in adapter
+ */
+ fun refresh()
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt
index 12e75580d..38e7b6b85 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt
@@ -41,7 +41,13 @@ data class Image(
/**
sha1 : sha1 of original image.
*/
- var sha1: String = ""
+ var sha1: String = "",
+
+ /**
+ * date: Creation date of the image to show it inside the bubble during bubble scroll.
+ */
+ var date: String = ""
+
) : Parcelable {
/**
@@ -54,6 +60,7 @@ data class Image(
parcel.readString()!!,
parcel.readLong(),
parcel.readString()!!,
+ parcel.readString()!!,
parcel.readString()!!
)
@@ -68,6 +75,7 @@ data class Image(
parcel.writeLong(bucketId)
parcel.writeString(bucketName)
parcel.writeString(sha1)
+ parcel.writeString(date)
}
/**
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 f3fce1cc0..4180f0082 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
@@ -1,6 +1,7 @@
package fr.free.nrw.commons.customselector.ui.adapter
import android.content.Context
+import android.content.SharedPreferences
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
@@ -10,11 +11,17 @@ import androidx.constraintlayout.widget.Group
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
+import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import fr.free.nrw.commons.R
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.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
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
+import kotlinx.coroutines.*
+import java.util.*
+import kotlin.collections.ArrayList
/**
* Custom selector ImageAdapter.
@@ -36,7 +43,7 @@ class ImageAdapter(
private var imageLoader: ImageLoader
):
- RecyclerViewAdapter(context) {
+ RecyclerViewAdapter(context), FastScrollRecyclerView.SectionedAdapter {
/**
* ImageSelectedOrUpdated payload class.
@@ -48,16 +55,58 @@ class ImageAdapter(
*/
class ImageUnselected
+ /**
+ * Determines whether addition of all actionable images is done or not
+ */
+ private var reachedEndOfFolder: Boolean = false
+
/**
* Currently selected images.
*/
private var selectedImages = arrayListOf()
+ /**
+ * Number of selected images that are marked as not for upload
+ */
+ private var numberOfSelectedImagesMarkedAsNotForUpload = 0
+
/**
* List of all images in adapter.
*/
private var images: ArrayList = ArrayList()
+ /**
+ * Stores all images
+ */
+ private var allImages: List = ArrayList()
+
+ /**
+ * Map to store actionable images
+ */
+ private var actionableImagesMap: TreeMap = TreeMap()
+
+ /**
+ * Stores already added positions of actionable images
+ */
+ private var alreadyAddedPositions: ArrayList = ArrayList()
+
+ /**
+ * Next starting index to initiate query to find next actionable image
+ */
+ private var nextImagePosition = 0
+
+ /**
+ * Helps to maintain the increasing sequence of the position. eg- 0, 1, 2, 3
+ */
+ private var imagePositionAsPerIncreasingOrder = 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.
*/
@@ -70,7 +119,7 @@ class ImageAdapter(
* Bind View holder, load image, selected view, click listeners.
*/
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
- val image=images[position]
+ var image=images[position]
holder.image.setImageDrawable (null)
if (context.contentResolver.getType(image.uri) == null) {
// Image does not exist anymore, update adapter.
@@ -81,56 +130,214 @@ class ImageAdapter(
notifyItemRangeChanged(updatedPosition, images.size)
}
} else {
- val selectedIndex = ImageHelper.getIndex(selectedImages, image)
+ val sharedPreferences: SharedPreferences =
+ context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
+ val showAlreadyActionedImages =
+ sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+
+ // Getting selected index when switch is on
+ val selectedIndex: Int = if (showAlreadyActionedImages) {
+ ImageHelper.getIndex(selectedImages, image)
+
+ // Getting selected index when switch is off
+ } else if (actionableImagesMap.size > position) {
+ ImageHelper
+ .getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
+
+ // For any other case return -1
+ } else {
+ -1
+ }
+
val isSelected = selectedIndex != -1
if (isSelected) {
- holder.itemSelected(selectedIndex + 1)
+ holder.itemSelected(selectedImages.size)
} else {
- holder.itemUnselected();
+ holder.itemUnselected()
}
- Glide.with(holder.image).load(image.uri).thumbnail(0.3f).into(holder.image)
- imageLoader.queryAndSetView(holder, image)
+
+ imageLoader.queryAndSetView(
+ holder, image, ioDispatcher, defaultDispatcher
+ )
+ scope.launch {
+ val sharedPreferences: SharedPreferences =
+ context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
+ val showAlreadyActionedImages =
+ sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+ if (!showAlreadyActionedImages) {
+ // 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)) {
+ processThumbnailForActionedImage(holder, 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 = ArrayList(actionableImagesMap.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 {
- selectOrRemoveImage(holder, position)
+ onThumbnailClicked(position, holder)
}
// launch media preview on long click.
holder.itemView.setOnLongClickListener {
- imageSelectListener.onLongPress(image.uri)
+ imageSelectListener.onLongPress(images.indexOf(image), images, selectedImages)
true
}
}
}
+ /**
+ * Process thumbnail for actioned image
+ */
+ suspend fun processThumbnailForActionedImage(
+ holder: ImageViewHolder,
+ position: Int
+ ) {
+ val next = imageLoader.nextActionableImage(
+ allImages, ioDispatcher, defaultDispatcher,
+ nextImagePosition
+ )
+
+ // 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) {
+ nextImagePosition = next + 1
+
+ // If map doesn't contains the next actionable image, that means it's a
+ // new actionable image, it will put it to the map as actionable images
+ // and it will load the new image in the view holder
+ if (!actionableImagesMap.containsKey(next)) {
+ actionableImagesMap[next] = allImages[next]
+ alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
+ imagePositionAsPerIncreasingOrder++
+ 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 {
+ reachedEndOfFolder = true
+ notifyItemRemoved(position)
+ }
+ }
+
+ /**
+ * Handles click on thumbnail
+ */
+ private fun onThumbnailClicked(
+ position: Int,
+ holder: ImageViewHolder
+ ) {
+ val sharedPreferences: SharedPreferences =
+ context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
+ val switchState =
+ sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+
+ // While switch is turned off, lets user click on image only if the position is
+ // added inside map
+ if (!switchState) {
+ if (actionableImagesMap.size > position) {
+ selectOrRemoveImage(holder, position)
+ }
+ } else {
+ selectOrRemoveImage(holder, position)
+ }
+ }
+
/**
* Handle click event on an image, update counter on images.
*/
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 showAlreadyActionedImages =
+ sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+
+ // Getting clicked index from all images index when show_already_actioned_images
+ // switch is on
+ val clickedIndex: Int = if(showAlreadyActionedImages) {
+ ImageHelper.getIndex(selectedImages, images[position])
+
+ // Getting clicked index from actionable images when show_already_actioned_images
+ // switch is off
+ } else {
+ ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
+ }
+
if (clickedIndex != -1) {
selectedImages.removeAt(clickedIndex)
+ if (holder.isItemNotForUpload()) {
+ numberOfSelectedImagesMarkedAsNotForUpload--
+ }
notifyItemChanged(position, ImageUnselected())
- val indexes = ImageHelper.getIndexList(selectedImages, images)
+
+ // Getting index from all images index when switch is on
+ val indexes = if (showAlreadyActionedImages) {
+ ImageHelper.getIndexList(selectedImages, images)
+
+ // Getting index from actionable images when switch is off
+ } else {
+ ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
+ }
for (index in indexes) {
notifyItemChanged(index, ImageSelectedOrUpdated())
}
} else {
- if(holder.isItemUploaded()){
+ if (holder.isItemUploaded()) {
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
} else {
- selectedImages.add(images[position])
- notifyItemChanged(position, ImageSelectedOrUpdated())
+ if (holder.isItemNotForUpload()) {
+ numberOfSelectedImagesMarkedAsNotForUpload++
+ }
+
+ // Getting index from all images index when switch is on
+ val indexes: ArrayList = if (showAlreadyActionedImages) {
+ selectedImages.add(images[position])
+ ImageHelper.getIndexList(selectedImages, images)
+
+ // Getting index from actionable images when switch is off
+ } else {
+ selectedImages.add(ArrayList(actionableImagesMap.values)[position])
+ ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
+ }
+
+ for (index in indexes) {
+ notifyItemChanged(index, ImageSelectedOrUpdated())
+ }
}
}
- imageSelectListener.onSelectedImagesChanged(selectedImages)
+ imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
}
/**
* Initialize the data set.
*/
- fun init(newImages: List) {
+ fun init(newImages: List, fixedImages: List, emptyMap: TreeMap) {
+ allImages = fixedImages
val oldImageList:ArrayList = images
val newImageList:ArrayList = ArrayList(newImages)
+ actionableImagesMap = emptyMap
+ alreadyAddedPositions = ArrayList()
+ nextImagePosition = 0
+ reachedEndOfFolder = false
+ selectedImages = ArrayList()
+ imagePositionAsPerIncreasingOrder = 0
val diffResult = DiffUtil.calculateDiff(
ImagesDiffCallback(oldImageList, newImageList)
)
@@ -138,19 +345,63 @@ class ImageAdapter(
diffResult.dispatchUpdatesTo(this)
}
+ /**
+ * Set new selected images
+ */
+ fun setSelectedImages(newSelectedImages: ArrayList){
+ selectedImages = ArrayList(newSelectedImages)
+ imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
+ }
+ /**
+ * Refresh the data in the adapter
+ */
+ fun refresh(newImages: List, fixedImages: List) {
+ numberOfSelectedImagesMarkedAsNotForUpload = 0
+ selectedImages.clear()
+ images.clear()
+ selectedImages = arrayListOf()
+ init(newImages, fixedImages, TreeMap())
+ notifyDataSetChanged()
+ }
+
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
override fun getItemCount(): Int {
- return images.size
+ val sharedPreferences: SharedPreferences =
+ context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
+ val showAlreadyActionedImages =
+ sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+
+ // While switch is on initializes the holder with all images size
+ return if(showAlreadyActionedImages) {
+ allImages.size
+
+ // While switch is off and searching for next actionable has ended, initializes the holder
+ // with size of all actionable images
+ } else if (actionableImagesMap.size == allImages.size || reachedEndOfFolder) {
+ actionableImagesMap.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 {
+ actionableImagesMap.size + 1
+ }
}
fun getImageIdAt(position: Int): Long {
return images.get(position).id
}
+ /**
+ * CleanUp function.
+ */
+ fun cleanUp() {
+ scope.cancel()
+ }
+
/**
* Image view holder.
*/
@@ -158,6 +409,7 @@ class ImageAdapter(
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count)
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
+ private val notForUploadGroup: Group = itemView.findViewById(R.id.not_for_upload_group)
private val selectedGroup: Group = itemView.findViewById(R.id.selected_group)
/**
@@ -182,9 +434,24 @@ class ImageAdapter(
uploadedGroup.visibility = View.VISIBLE
}
+ /**
+ * Item is not for upload view
+ */
+ fun itemNotForUpload() {
+ notForUploadGroup.visibility = View.VISIBLE
+ }
+
fun isItemUploaded():Boolean {
return uploadedGroup.visibility == View.VISIBLE
}
+
+ /**
+ * Item is not for upload
+ */
+ fun isItemNotForUpload():Boolean {
+ return notForUploadGroup.visibility == View.VISIBLE
+ }
+
/**
* Item Not Uploaded view.
*/
@@ -192,6 +459,12 @@ class ImageAdapter(
uploadedGroup.visibility = View.GONE
}
+ /**
+ * Item can be uploaded view
+ */
+ fun itemForUpload() {
+ notForUploadGroup.visibility = View.GONE
+ }
}
/**
@@ -233,4 +506,11 @@ class ImageAdapter(
}
+ /**
+ * Returns the text for showing inside the bubble during bubble scroll.
+ */
+ override fun getSectionName(position: Int): String {
+ return images[position].date
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
index 8dffe7306..f0c8cb947 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
@@ -4,20 +4,29 @@ import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
-import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.ViewModelProvider
import fr.free.nrw.commons.R
+import fr.free.nrw.commons.customselector.database.NotForUploadStatus
+import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
+import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
+import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants.SHOULD_REFRESH
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.filepicker.Constants
import fr.free.nrw.commons.media.ZoomableActivity
import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.upload.FileUtilsWrapper
+import fr.free.nrw.commons.utils.CustomSelectorUtils
+import kotlinx.android.synthetic.main.custom_selector_bottom_layout.*
+import kotlinx.coroutines.*
import java.io.File
import javax.inject.Inject
@@ -53,6 +62,29 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
*/
@Inject lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
+ /**
+ * NotForUploadStatus Dao class for database operations
+ */
+ @Inject
+ lateinit var notForUploadStatusDao: NotForUploadStatusDao
+
+ /**
+ * FileUtilsWrapper class to get imageSHA1 from uri
+ */
+ @Inject
+ lateinit var fileUtilsWrapper: FileUtilsWrapper
+
+ /**
+ * Coroutine Dispatchers and Scope.
+ */
+ private val scope : CoroutineScope = MainScope()
+ private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
+
+ /**
+ * Image Fragment instance
+ */
+ var imageFragment: ImageFragment? = null
+
/**
* onCreate Activity, sets theme, initialises the view model, setup view.
*/
@@ -82,6 +114,21 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
}
}
+ /**
+ * When data will be send from full screen mode, it will be passed to fragment
+ */
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
+ resultCode == Activity.RESULT_OK) {
+ val selectedImages: ArrayList =
+ data!!
+ .getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
+ val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false)
+ imageFragment!!.passSelectedImages(selectedImages, shouldRefresh)
+ }
+ }
+
/**
* Show Custom Selector Welcome Dialog.
*/
@@ -102,6 +149,114 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
.commit()
fetchData()
setUpToolbar()
+ setUpBottomLayout()
+ }
+
+ /**
+ * Set up bottom layout
+ */
+ private fun setUpBottomLayout() {
+ val done : Button = findViewById(R.id.upload)
+ done.setOnClickListener { onDone() }
+
+ val notForUpload : Button = findViewById(R.id.not_for_upload)
+ notForUpload.setOnClickListener{ onClickNotForUpload() }
+ }
+
+ /**
+ * Gets selected images and proceed for database operations
+ */
+ private fun onClickNotForUpload() {
+ val selectedImages = viewModel.selectedImages.value
+ if(selectedImages.isNullOrEmpty()) {
+ markAsNotForUpload(arrayListOf())
+ return
+ }
+ var i = 0
+ while (i < selectedImages.size) {
+ val path = selectedImages[i].path
+ val file = File(path)
+ if (!file.exists()) {
+ selectedImages.removeAt(i)
+ i--
+ }
+ i++
+ }
+ markAsNotForUpload(selectedImages)
+ }
+
+ /**
+ * Insert selected images in the database
+ */
+ private fun markAsNotForUpload(images: ArrayList) {
+ insertIntoNotForUpload(images)
+ }
+
+ /**
+ * Initializing ImageFragment
+ */
+ fun setOnDataListener(imageFragment: ImageFragment?) {
+ this.imageFragment = imageFragment
+ }
+
+ /**
+ * Insert images into not for upload
+ * Remove images from not for upload
+ * Refresh the UI
+ */
+ private fun insertIntoNotForUpload(images: ArrayList) {
+ scope.launch {
+ var allImagesAlreadyNotForUpload = true
+ images.forEach{
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ it.uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ val exists = notForUploadStatusDao.find(imageSHA1)
+
+ // If image exists in not for upload table make allImagesAlreadyNotForUpload false
+ if (exists < 1) {
+ allImagesAlreadyNotForUpload = false
+ }
+ }
+
+ // if all images is not already marked as not for upload, insert all images in
+ // not for upload table
+ if (!allImagesAlreadyNotForUpload) {
+ images.forEach {
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ it.uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ notForUploadStatusDao.insert(
+ NotForUploadStatus(
+ imageSHA1
+ )
+ )
+ }
+
+ // if all images is already marked as not for upload, delete all images from
+ // not for upload table
+ } else {
+ images.forEach {
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ it.uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
+ }
+ }
+
+ imageFragment!!.refresh()
+ val bottomLayout : ConstraintLayout = findViewById(R.id.bottom_layout)
+ bottomLayout.visibility = View.GONE
+ }
}
/**
@@ -127,9 +282,6 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
private fun setUpToolbar() {
val back : ImageButton = findViewById(R.id.back)
back.setOnClickListener { onBackPressed() }
-
- val done : ImageButton = findViewById(R.id.done)
- done.setOnClickListener { onDone() }
}
/**
@@ -149,22 +301,46 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
}
/**
- * override Selected Images Change, update view model selected images.
+ * override Selected Images Change, update view model selected images and change UI.
*/
- override fun onSelectedImagesChanged(selectedImages: ArrayList) {
+ override fun onSelectedImagesChanged(selectedImages: ArrayList,
+ selectedNotForUploadImages: Int) {
viewModel.selectedImages.value = selectedImages
- val done : ImageButton = findViewById(R.id.done)
- done.visibility = if (selectedImages.isEmpty()) View.INVISIBLE else View.VISIBLE
+ if (selectedNotForUploadImages > 0) {
+ upload.isEnabled = false
+ upload.alpha = 0.5f
+ } else {
+ upload.isEnabled = true
+ upload.alpha = 1f
+ }
+
+ not_for_upload.text = when (selectedImages.size == selectedNotForUploadImages) {
+ true -> getString(R.string.unmark_as_not_for_upload)
+ else -> getString(R.string.mark_as_not_for_upload)
+ }
+
+ val bottomLayout : ConstraintLayout = findViewById(R.id.bottom_layout)
+ bottomLayout.visibility = if (selectedImages.isEmpty()) View.GONE else View.VISIBLE
}
/**
* onLongPress
* @param imageUri : uri of image
*/
- override fun onLongPress(imageUri: Uri) {
- val intent = Intent(this, ZoomableActivity::class.java).setData(imageUri);
- startActivity(intent)
+ override fun onLongPress(
+ position: Int,
+ images: ArrayList,
+ selectedImages: ArrayList
+ ) {
+ val intent = Intent(this, ZoomableActivity::class.java)
+ intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position);
+ intent.putParcelableArrayListExtra(
+ CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
+ selectedImages
+ )
+ intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
+ startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE)
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt
index 38b5cf0a4..8e22574e3 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt
@@ -3,10 +3,15 @@ package fr.free.nrw.commons.customselector.ui.selector
import android.content.ContentUris
import android.content.Context
import android.provider.MediaStore
+import android.text.format.DateFormat
import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
import fr.free.nrw.commons.customselector.model.Image
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.File
+import java.util.*
import kotlin.coroutines.CoroutineContext
/**
@@ -28,7 +33,9 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID,
- MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
+ MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
+ MediaStore.Images.Media.DATE_ADDED
+ )
/**
* Load Device Images under coroutine.
@@ -57,6 +64,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
+ val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
val images = arrayListOf()
if (cursor.moveToFirst()) {
@@ -70,6 +78,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
val path = cursor.getString(dataColumn)
val bucketId = cursor.getLong(bucketIdColumn)
val bucketName = cursor.getString(bucketNameColumn)
+ val date = cursor.getLong(dateColumn)
val file =
if (path == null || path.isEmpty()) {
@@ -84,7 +93,22 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
if (file != null && file.exists()) {
if (name != null && path != null && bucketName != null) {
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
- val image = Image(id, name, uri, path, bucketId, bucketName)
+
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = date * 1000L
+ val date: Date = calendar.time
+ val dateFormat = DateFormat.getMediumDateFormat(context)
+ val formattedDate = dateFormat.format(date)
+
+ val image = Image(
+ id,
+ name,
+ uri,
+ path,
+ bucketId,
+ bucketName,
+ date = (formattedDate)
+ )
images.add(image)
}
}
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 36f298e6b..986e331e9 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
@@ -1,36 +1,47 @@
package fr.free.nrw.commons.customselector.ui.selector
-import android.net.Uri
+import android.app.Activity
+import android.content.Context.MODE_PRIVATE
+import android.content.SharedPreferences
import android.os.Bundle
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
+import android.widget.Switch
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.listeners.PassDataListener
+import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
+import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
+import fr.free.nrw.commons.customselector.listeners.RefreshUIListener
import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
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.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.view.*
-import java.io.File
-import java.io.FileInputStream
-import java.net.URI
+import java.util.*
import javax.inject.Inject
+import kotlin.collections.ArrayList
/**
* Custom Selector Image Fragment.
*/
-class ImageFragment: CommonsDaggerSupportFragment() {
+class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
/**
* Current bucketId.
@@ -52,8 +63,14 @@ class ImageFragment: CommonsDaggerSupportFragment() {
*/
private var selectorRV: RecyclerView? = null
private var loader: ProgressBar? = null
+ private var switch: Switch? = null
lateinit var filteredImages: ArrayList;
+ /**
+ * Stores all images
+ */
+ var allImages: ArrayList = ArrayList()
+
/**
* View model Factory.
*/
@@ -76,9 +93,48 @@ class ImageFragment: CommonsDaggerSupportFragment() {
*/
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 {
+ /**
+ * Switch state
+ */
+ var showAlreadyActionedImages: Boolean = true
+
/**
* BucketId args name
*/
@@ -129,12 +185,54 @@ class ImageFragment: CommonsDaggerSupportFragment() {
handleResult(it)
})
+ switch = root.switchWidget
+ switch?.visibility = View.VISIBLE
+ switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) }
selectorRV = root.selector_rv
loader = root.loader
+ progressLayout = root.progressLayout
+
+ val sharedPreferences: SharedPreferences =
+ requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
+ showAlreadyActionedImages = sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+ switch?.isChecked = showAlreadyActionedImages
return root
}
+ private fun onChangeSwitchState(checked: Boolean) {
+ if (checked) {
+ showAlreadyActionedImages = true
+ val sharedPreferences: SharedPreferences =
+ requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
+ val editor = sharedPreferences.edit()
+ editor.putBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+ editor.apply()
+ } else {
+ showAlreadyActionedImages = false
+ val sharedPreferences: SharedPreferences =
+ requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
+ val editor = sharedPreferences.edit()
+ editor.putBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, false)
+ editor.apply()
+ }
+
+ imageAdapter.init(allImages, allImages, TreeMap())
+ imageAdapter.notifyDataSetChanged()
+ }
+
+ /**
+ * Attaching data listener
+ */
+ override fun onAttach(activity: Activity) {
+ super.onAttach(activity)
+ try {
+ (getActivity() as CustomSelectorActivity).setOnDataListener(this)
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ }
+ }
+
/**
* Handle view model result.
*/
@@ -143,7 +241,8 @@ class ImageFragment: CommonsDaggerSupportFragment() {
val images = result.images
if(images.isNotEmpty()) {
filteredImages = ImageHelper.filterImages(images, bucketId)
- imageAdapter.init(filteredImages)
+ allImages = ArrayList(filteredImages)
+ imageAdapter.init(filteredImages, allImages, TreeMap())
selectorRV?.let {
it.visibility = View.VISIBLE
lastItemId?.let { pos ->
@@ -191,7 +290,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
* Save the Image Fragment state.
*/
override fun onDestroy() {
- imageLoader?.cleanUP()
+ imageAdapter.cleanUp()
val position = (selectorRV?.layoutManager as GridLayoutManager)
.findFirstVisibleItemPosition()
@@ -211,4 +310,21 @@ class ImageFragment: CommonsDaggerSupportFragment() {
}
super.onDestroy()
}
+
+ override fun refresh() {
+ imageAdapter.refresh(filteredImages, allImages)
+ }
+
+ /**
+ * Passes selected images and other information from Activity to Fragment and connects it with
+ * the adapter
+ */
+ override fun passSelectedImages(selectedImages: ArrayList, shouldRefresh: Boolean){
+ imageAdapter.setSelectedImages(selectedImages)
+
+ if (!showAlreadyActionedImages && shouldRefresh) {
+ imageAdapter.init(filteredImages, allImages, TreeMap())
+ imageAdapter.setSelectedImages(selectedImages)
+ }
+ }
}
\ No newline at end of file
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 cf45a87c1..b5ce07fc3 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,25 +1,23 @@
package fr.free.nrw.commons.customselector.ui.selector
import android.content.Context
+import android.content.SharedPreferences
import android.net.Uri
-import androidx.exifinterface.media.ExifInterface
+import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
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.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 fr.free.nrw.commons.utils.CustomSelectorUtils
+import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
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.concurrent.TimeUnit
import javax.inject.Inject
-import kotlin.collections.HashMap
/**
* Image Loader class, loads images, depending on API results.
@@ -46,6 +44,11 @@ class ImageLoader @Inject constructor(
*/
var uploadedStatusDao: UploadedStatusDao,
+ /**
+ * NotForUploadDao for database operations
+ */
+ var notForUploadStatusDao: NotForUploadStatusDao,
+
/**
* Context for coroutine.
*/
@@ -61,34 +64,48 @@ class ImageLoader @Inject constructor(
private var mapImageSHA1: HashMap = HashMap()
/**
- * Coroutine Dispatchers and Scope.
+ * Coroutine Scope.
*/
- private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
- private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
private val scope : CoroutineScope = MainScope()
/**
* Query image and setUp the view.
*/
- fun queryAndSetView(holder: ImageViewHolder, image: Image) {
+ 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.
*/
mapHolderImage[holder] = image
holder.itemNotUploaded()
+ holder.itemForUpload()
scope.launch {
-
var result: Result = Result.NOTFOUND
if (mapHolderImage[holder] != image) {
return@launch
}
- val imageSHA1 = getImageSHA1(image.uri)
- if(imageSHA1.isEmpty())
+ val imageSHA1: String = when (mapImageSHA1[image.uri] != null) {
+ true -> mapImageSHA1[image.uri]!!
+ else -> CustomSelectorUtils.getImageSHA1(
+ image.uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ context.contentResolver
+ )
+ }
+ mapImageSHA1[image.uri] = imageSHA1
+
+ if (imageSHA1.isEmpty()) {
return@launch
+ }
val uploadedStatus = getFromUploaded(imageSHA1)
val sha1 = uploadedStatus?.let {
@@ -96,7 +113,7 @@ class ImageLoader @Inject constructor(
uploadedStatus.modifiedImageSHA1
} ?: run {
if (mapHolderImage[holder] == image) {
- getSHA1(image)
+ getSHA1(image, defaultDispatcher)
} else {
""
}
@@ -106,53 +123,137 @@ class ImageLoader @Inject constructor(
return@launch
}
+ val existsInNotForUploadTable = notForUploadStatusDao.find(imageSHA1)
+
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
- // Query original image.
- result = querySHA1(imageSHA1)
+ when {
+ mapResult[imageSHA1] == null -> {
+ // Query original image.
+ result = checkWhetherFileExistsOnCommonsUsingSHA1(
+ imageSHA1,
+ ioDispatcher,
+ mediaClient
+ )
+ when (result) {
+ is Result.TRUE -> {
+ mapResult[imageSHA1] = Result.TRUE
+ }
+ }
+ }
+ else -> {
+ result = mapResult[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)
+ when {
+ mapResult[sha1] == null -> {
+ // Original image not found, query modified image.
+ result = checkWhetherFileExistsOnCommonsUsingSHA1(
+ 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)
}
}
}
- if(mapHolderImage[holder] == image) {
- if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded()
+
+ val sharedPreferences: SharedPreferences =
+ context
+ .getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
+ val showAlreadyActionedImages =
+ sharedPreferences.getBoolean(
+ ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
+ true
+ )
+
+ if (mapHolderImage[holder] == image) {
+ if ((result is Result.TRUE) && showAlreadyActionedImages) {
+ holder.itemUploaded()
+ } else holder.itemNotUploaded()
+
+ if ((existsInNotForUploadTable > 0) && showAlreadyActionedImages) {
+ holder.itemNotForUpload()
+ } else holder.itemForUpload()
}
}
}
/**
- * Query SHA1, return result if previously queried, otherwise start a new query.
- *
- * @return Query result.
+ * Finds out the next actionable image position
*/
+ suspend fun nextActionableImage(
+ allImages: List, ioDispatcher: CoroutineDispatcher,
+ defaultDispatcher: CoroutineDispatcher,
+ nextImagePosition: Int
+ ): Int {
+ var next: Int
- suspend fun querySHA1(SHA1: String): Result {
- return withContext(ioDispatcher) {
- mapResult[SHA1]?.let {
- return@withContext it
+ // Traversing from given position to the end
+ for (i in nextImagePosition until allImages.size){
+ val it = allImages[i]
+ 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
- try {
- if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) {
- mapResult[SHA1] = Result.TRUE
- result = Result.TRUE
+ next = notForUploadStatusDao.find(imageSHA1)
+
+ // After checking the image in the not for upload table, if the image is present then
+ // skips the image and moves to next image for checking
+ if(next > 0){
+ continue
+
+ // Otherwise checks in already uploaded table
+ } else {
+ next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
+
+ // If the image is not present in the already uploaded table, checks for its
+ // modified SHA1 in already uploaded table
+ 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 table,
+ // returns the position as next actionable image position
+ if (next <= 0) {
+ return i
+
+ // If present in the db then skips iteration for the image and moves to the next
+ // for checking
+ } else {
+ continue
+ }
+
+ // If present in the 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
}
/**
@@ -160,11 +261,17 @@ class ImageLoader @Inject constructor(
*
* @return sha1 of the image
*/
- suspend fun getSHA1(image: Image): String {
+ suspend fun getSHA1(image: Image, defaultDispatcher: CoroutineDispatcher): String {
mapModifiedImageSHA1[image]?.let{
return it
}
- val sha1 = generateModifiedSHA1(image);
+ val sha1 = CustomSelectorUtils
+ .generateModifiedSHA1(image,
+ defaultDispatcher,
+ context,
+ fileProcessor,
+ fileUtilsWrapper
+ )
mapModifiedImageSHA1[image] = sha1;
return sha1;
}
@@ -190,25 +297,6 @@ class ImageLoader @Inject constructor(
)
}
- /**
- * Get image sha1 from uri, used to retrieve the original image sha1.
- */
- suspend fun getImageSHA1(uri: Uri): String {
- return withContext(ioDispatcher) {
- mapImageSHA1[uri]?.let{
- return@withContext it
- }
- try {
- val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri))
- mapImageSHA1[uri] = result
- result
- } catch (e: FileNotFoundException){
- e.printStackTrace()
- ""
- }
- }
- }
-
/**
* Get result data from database.
*/
@@ -226,35 +314,6 @@ class ImageLoader @Inject constructor(
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.
*/
diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt
index a877e14c4..a0991c4ba 100644
--- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt
+++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt
@@ -5,8 +5,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
-import fr.free.nrw.commons.customselector.database.UploadedStatus
-import fr.free.nrw.commons.customselector.database.UploadedStatusDao
+import fr.free.nrw.commons.customselector.database.*
import fr.free.nrw.commons.upload.depicts.Depicts
import fr.free.nrw.commons.upload.depicts.DepictsDao
@@ -14,10 +13,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
* The database for accessing the respective DAOs
*
*/
-@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class], version = 13, exportSchema = false)
+@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class], version = 14, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao
abstract fun DepictsDao(): DepictsDao;
abstract fun UploadedStatusDao(): UploadedStatusDao;
+ abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
}
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index 31412236d..6a9277906 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -13,6 +13,7 @@ import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
import fr.free.nrw.commons.description.DescriptionEditActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
+import fr.free.nrw.commons.media.ZoomableActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
@@ -75,4 +76,7 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract DescriptionEditActivity bindDescriptionEditActivity();
+
+ @ContributesAndroidInjector
+ abstract ZoomableActivity bindZoomableActivity();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
index 443dd8433..9185d01ec 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
@@ -17,6 +17,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionDao;
+import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
import fr.free.nrw.commons.data.DBOpenHelper;
@@ -290,6 +291,14 @@ public class CommonsApplicationModule {
return appDatabase.UploadedStatusDao();
}
+ /**
+ * Get the reference of NotForUploadStatus class.
+ */
+ @Provides
+ public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) {
+ return appDatabase.NotForUploadStatusDao();
+ }
+
@Provides
public ContentResolver providesContentResolver(Context context){
return context.getContentResolver();
diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java
index 4b5b91e68..c25461740 100644
--- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java
+++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java
@@ -15,6 +15,8 @@ public interface Constants {
int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12);
int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13);
int CAPTURE_VIDEO = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 14);
+
+ int RECEIVE_DATA_FROM_FULL_SCREEN_MODE = 1 << 9;
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.java b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.java
deleted file mode 100644
index 286384ff1..000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package fr.free.nrw.commons.media;
-
-import android.graphics.drawable.Animatable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.ProgressBar;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.drawee.controller.BaseControllerListener;
-import com.facebook.drawee.controller.ControllerListener;
-import com.facebook.drawee.drawable.ProgressBarDrawable;
-import com.facebook.drawee.drawable.ScalingUtils;
-import com.facebook.drawee.generic.GenericDraweeHierarchy;
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import com.facebook.drawee.interfaces.DraweeController;
-import com.facebook.imagepipeline.image.ImageInfo;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.media.zoomControllers.zoomable.DoubleTapGestureListener;
-import fr.free.nrw.commons.media.zoomControllers.zoomable.ZoomableDraweeView;
-import timber.log.Timber;
-
-
-public class ZoomableActivity extends AppCompatActivity {
- private Uri imageUri;
-
- @BindView(R.id.zoomable)
- ZoomableDraweeView photo;
- @BindView(R.id.zoom_progress_bar)
- ProgressBar spinner;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- imageUri = getIntent().getData();
- if (null == imageUri) {
- throw new IllegalArgumentException("No data to display");
- }
- Timber.d("URl = " + imageUri);
-
- setContentView(R.layout.activity_zoomable);
- ButterKnife.bind(this);
- init();
- }
-
- /**
- * Two types of loading indicators have been added to the zoom activity:
- * 1. An Indeterminate spinner for showing the time lapsed between dispatch of the image request
- * and starting to receiving the image.
- * 2. ProgressBarDrawable that reflects how much image has been downloaded
- */
- private final ControllerListener loadingListener = new BaseControllerListener() {
- @Override
- public void onSubmit(String id, Object callerContext) {
- // Sometimes the spinner doesn't appear when rapidly switching between images, this fixes that
- spinner.setVisibility(View.VISIBLE);
- }
-
- @Override
- public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
- spinner.setVisibility(View.GONE);
- }
- @Override
- public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
- spinner.setVisibility(View.GONE);
- }
- };
- private void init() {
- if( imageUri != null ) {
- GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(getResources())
- .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
- .setProgressBarImage(new ProgressBarDrawable())
- .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
- .build();
- photo.setHierarchy(hierarchy);
- photo.setAllowTouchInterceptionWhileZoomed(true);
- photo.setIsLongpressEnabled(false);
- photo.setTapListener(new DoubleTapGestureListener(photo));
- DraweeController controller = Fresco.newDraweeControllerBuilder()
- .setUri(imageUri)
- .setControllerListener(loadingListener)
- .build();
- photo.setController(controller);
- }
- }
-
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt
new file mode 100644
index 000000000..41f37d1a6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt
@@ -0,0 +1,653 @@
+package fr.free.nrw.commons.media
+
+import android.app.Activity
+import android.app.Dialog
+import android.content.Intent
+import android.content.SharedPreferences
+import android.graphics.drawable.Animatable
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.view.Window
+import android.widget.Button
+import android.widget.ProgressBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.lifecycle.ViewModelProvider
+import butterknife.BindView
+import butterknife.ButterKnife
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.controller.BaseControllerListener
+import com.facebook.drawee.controller.ControllerListener
+import com.facebook.drawee.drawable.ProgressBarDrawable
+import com.facebook.drawee.drawable.ScalingUtils
+import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
+import com.facebook.drawee.interfaces.DraweeController
+import com.facebook.imagepipeline.image.ImageInfo
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.customselector.database.NotForUploadStatus
+import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
+import fr.free.nrw.commons.customselector.database.UploadedStatusDao
+import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
+import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants.SHOULD_REFRESH
+import fr.free.nrw.commons.customselector.helper.ImageHelper
+import fr.free.nrw.commons.customselector.helper.OnSwipeTouchListener
+import fr.free.nrw.commons.customselector.model.CallbackStatus
+import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.customselector.model.Result
+import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorViewModel
+import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorViewModelFactory
+import fr.free.nrw.commons.media.zoomControllers.zoomable.DoubleTapGestureListener
+import fr.free.nrw.commons.media.zoomControllers.zoomable.ZoomableDraweeView
+import fr.free.nrw.commons.theme.BaseActivity
+import fr.free.nrw.commons.upload.FileProcessor
+import fr.free.nrw.commons.upload.FileUtilsWrapper
+import fr.free.nrw.commons.utils.CustomSelectorUtils
+import kotlinx.coroutines.*
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.collections.ArrayList
+
+/**
+ * Activity for helping to view an image in full-screen mode with some other features
+ * like zoom, and swap gestures
+ */
+class ZoomableActivity : BaseActivity() {
+
+ private lateinit var imageUri: Uri
+
+ /**
+ * View model.
+ */
+ private lateinit var viewModel: CustomSelectorViewModel
+
+ /**
+ * Pref for saving states.
+ */
+ private lateinit var prefs: SharedPreferences
+
+ @JvmField
+ @BindView(R.id.zoomable)
+ var photo: ZoomableDraweeView? = null
+
+ @JvmField
+ @BindView(R.id.zoom_progress_bar)
+ var spinner: ProgressBar? = null
+
+ @JvmField
+ @BindView(R.id.selection_count)
+ var selectedCount: TextView? = null
+
+ /**
+ * Total images present in folder
+ */
+ private var images: ArrayList? = null
+
+ /**
+ * Total selected images present in folder
+ */
+ private var selectedImages: ArrayList? = null
+
+ /**
+ * Present position of the image
+ */
+ private var position = 0
+
+ /**
+ * Present bucket ID
+ */
+ private var bucketId: Long = 0L
+
+ /**
+ * Determines whether the adapter should refresh
+ */
+ private var shouldRefresh = false
+
+ /**
+ * FileUtilsWrapper class to get imageSHA1 from uri
+ */
+ @Inject
+ lateinit var fileUtilsWrapper: FileUtilsWrapper
+
+ /**
+ * FileProcessor to pre-process the file.
+ */
+ @Inject
+ lateinit var fileProcessor: FileProcessor
+
+ /**
+ * NotForUploadStatus Dao class for database operations
+ */
+ @Inject
+ lateinit var notForUploadStatusDao: NotForUploadStatusDao
+
+ /**
+ * UploadedStatus Dao class for database operations
+ */
+ @Inject
+ lateinit var uploadedStatusDao: UploadedStatusDao
+
+ /**
+ * View Model Factory.
+ */
+ @Inject
+ lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
+
+ /**
+ * Coroutine Dispatchers and Scope.
+ */
+ private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
+ private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
+ private val scope : CoroutineScope = MainScope()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_zoomable)
+ ButterKnife.bind(this)
+
+ prefs = applicationContext.getSharedPreferences(
+ ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY,
+ MODE_PRIVATE
+ )
+
+ selectedImages = intent.getParcelableArrayListExtra(
+ CustomSelectorConstants.TOTAL_SELECTED_IMAGES
+ )
+ position = intent.getIntExtra(CustomSelectorConstants.PRESENT_POSITION, 0)
+ bucketId = intent.getLongExtra(CustomSelectorConstants.BUCKET_ID, 0L)
+ viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(
+ CustomSelectorViewModel::class.java
+ )
+ viewModel.fetchImages()
+ viewModel.result.observe(this) {
+ handleResult(it)
+ }
+
+ if(prefs.getBoolean(CustomSelectorConstants.FULL_SCREEN_MODE_FIRST_LUNCH, true)) {
+ // show welcome dialog on first launch
+ showWelcomeDialog()
+ prefs.edit().putBoolean(
+ CustomSelectorConstants.FULL_SCREEN_MODE_FIRST_LUNCH,
+ false
+ ).apply()
+ }
+ }
+
+ /**
+ * Show Full Screen Mode Welcome Dialog.
+ */
+ private fun showWelcomeDialog() {
+ val dialog = Dialog(this)
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ dialog.setContentView(R.layout.full_screen_mode_info_dialog)
+ (dialog.findViewById(R.id.btn_ok) as Button).setOnClickListener { dialog.dismiss() }
+ dialog.show()
+ }
+
+ /**
+ * Handle view model result.
+ */
+ private fun handleResult(result: Result){
+ if(result.status is CallbackStatus.SUCCESS){
+ val images = result.images
+ if(images.isNotEmpty()) {
+ this@ZoomableActivity.images = ImageHelper.filterImages(images, bucketId)
+ imageUri = if (this@ZoomableActivity.images.isNullOrEmpty()) {
+ intent.data as Uri
+ } else {
+ this@ZoomableActivity.images!![position].uri
+ }
+ Timber.d("URL = $imageUri")
+ init(imageUri)
+ onSwipe()
+ }
+ }
+ spinner?.let {
+ it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE
+ }
+ }
+
+ /**
+ * Handle swap gestures. Ex. onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown
+ */
+ private fun onSwipe() {
+ val sharedPreferences: SharedPreferences =
+ getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
+ val showAlreadyActionedImages =
+ sharedPreferences.getBoolean(ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
+
+ if (!images.isNullOrEmpty()) {
+ photo!!.setOnTouchListener(object : OnSwipeTouchListener(this) {
+ // Swipe left to view next image in the folder. (if available)
+ override fun onSwipeLeft() {
+ super.onSwipeLeft()
+ onLeftSwiped(showAlreadyActionedImages)
+ }
+
+ // Swipe right to view previous image in the folder. (if available)
+ override fun onSwipeRight() {
+ super.onSwipeRight()
+ onRightSwiped(showAlreadyActionedImages)
+ }
+
+ // Swipe up to select the picture (the equivalent of tapping it in non-fullscreen mode)
+ // and show the next picture skipping pictures that have either already been uploaded or
+ // marked as not for upload
+ override fun onSwipeUp() {
+ super.onSwipeUp()
+ onUpSwiped()
+ }
+
+ // Swipe down to mark that picture as "Not for upload" (the equivalent of selecting it then
+ // tapping "Mark as not for upload" in non-fullscreen mode), and show the next picture.
+ override fun onSwipeDown() {
+ super.onSwipeDown()
+ onDownSwiped()
+ }
+ })
+ }
+ }
+
+ /**
+ * Handles down swipe action
+ */
+ private fun onDownSwiped() {
+ if (photo?.zoomableController?.isIdentity == false)
+ return
+
+ scope.launch {
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ images!![position].uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ var isUploaded = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
+ if (isUploaded > 0) {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.this_image_is_already_uploaded),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ val imageModifiedSHA1 = CustomSelectorUtils.generateModifiedSHA1(
+ images!![position],
+ defaultDispatcher,
+ this@ZoomableActivity,
+ fileProcessor,
+ fileUtilsWrapper
+ )
+ isUploaded = uploadedStatusDao.findByModifiedImageSHA1(
+ imageModifiedSHA1,
+ true
+ )
+ if (isUploaded > 0) {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.this_image_is_already_uploaded),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ insertInNotForUpload(images!![position])
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.image_marked_as_not_for_upload),
+ Toast.LENGTH_SHORT
+ ).show()
+ shouldRefresh = true
+ if (position < images!!.size - 1) {
+ position++
+ init(images!![position].uri)
+ } else {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.no_more_images_found),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles up swipe action
+ */
+ private fun onUpSwiped() {
+ if (photo?.zoomableController?.isIdentity == false)
+ return
+
+ scope.launch {
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ images!![position].uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ var isNonActionable = notForUploadStatusDao.find(imageSHA1)
+ if (isNonActionable > 0) {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.can_not_select_this_image_for_upload),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ isNonActionable =
+ uploadedStatusDao.findByImageSHA1(imageSHA1, true)
+ if (isNonActionable > 0) {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.this_image_is_already_uploaded),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ val imageModifiedSHA1 = CustomSelectorUtils.generateModifiedSHA1(
+ images!![position],
+ defaultDispatcher,
+ this@ZoomableActivity,
+ fileProcessor,
+ fileUtilsWrapper
+ )
+ isNonActionable = uploadedStatusDao.findByModifiedImageSHA1(
+ imageModifiedSHA1,
+ true
+ )
+ if (isNonActionable > 0) {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.this_image_is_already_uploaded),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ if (!selectedImages!!.contains(images!![position])) {
+ selectedImages!!.add(images!![position])
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.image_selected),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ position = getNextActionableImage(position + 1)
+ init(images!![position].uri)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles right swipe action
+ */
+ private fun onRightSwiped(showAlreadyActionedImages: Boolean) {
+ if (photo?.zoomableController?.isIdentity == false)
+ return
+
+ if (showAlreadyActionedImages) {
+ if (position > 0) {
+ position--
+ init(images!![position].uri)
+ } else {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.no_more_images_found),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ } else {
+ if (position > 0) {
+ scope.launch {
+ position = getPreviousActionableImage(position - 1)
+ init(images!![position].uri)
+ }
+ } else {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.no_more_images_found),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+
+ /**
+ * Handles left swipe action
+ */
+ private fun onLeftSwiped(showAlreadyActionedImages: Boolean) {
+ if (photo?.zoomableController?.isIdentity == false)
+ return
+
+ if (showAlreadyActionedImages) {
+ if (position < images!!.size - 1) {
+ position++
+ init(images!![position].uri)
+ } else {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.no_more_images_found),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ } else {
+ if (position < images!!.size - 1) {
+ scope.launch {
+ position = getNextActionableImage(position + 1)
+ init(images!![position].uri)
+ }
+ } else {
+ Toast.makeText(
+ this@ZoomableActivity,
+ getString(R.string.no_more_images_found),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+
+ /**
+ * Gets next actionable image.
+ * Iterates from an index to the end of the folder and check whether the current image is
+ * present in already uploaded table or in not for upload table,
+ * and returns the first actionable image it can find.
+ */
+ private suspend fun getNextActionableImage(index: Int): Int {
+ var nextPosition = position
+ for(i in index until images!!.size){
+ nextPosition = i
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ images!![i].uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ var isNonActionable = notForUploadStatusDao.find(imageSHA1)
+ if (isNonActionable <= 0) {
+ isNonActionable = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
+ if (isNonActionable <= 0) {
+ val imageModifiedSHA1 = CustomSelectorUtils.generateModifiedSHA1(
+ images!![i],
+ defaultDispatcher,
+ this@ZoomableActivity,
+ fileProcessor,
+ fileUtilsWrapper
+ )
+ isNonActionable = uploadedStatusDao.findByModifiedImageSHA1(
+ imageModifiedSHA1,
+ true
+ )
+ if (isNonActionable <= 0) {
+ return i
+ } else {
+ continue
+ }
+ } else {
+ continue
+ }
+ } else {
+ continue
+ }
+ }
+ return nextPosition
+ }
+
+ /**
+ * Gets previous actionable image.
+ * Iterates from an index to the first image of the folder and check whether the current image
+ * is present in already uploaded table or in not for upload table,
+ * and returns the first actionable image it can find
+ */
+ private suspend fun getPreviousActionableImage(index: Int): Int {
+ var previousPosition = position
+ for(i in index downTo 0){
+ previousPosition = i
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ images!![i].uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ var isNonActionable = notForUploadStatusDao.find(imageSHA1)
+ if (isNonActionable <= 0) {
+ isNonActionable = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
+ if (isNonActionable <= 0) {
+ val imageModifiedSHA1 = CustomSelectorUtils.generateModifiedSHA1(
+ images!![i],
+ defaultDispatcher,
+ this@ZoomableActivity,
+ fileProcessor,
+ fileUtilsWrapper
+ )
+ isNonActionable = uploadedStatusDao.findByModifiedImageSHA1(
+ imageModifiedSHA1,
+ true
+ )
+ if (isNonActionable <= 0) {
+ return i
+ } else {
+ continue
+ }
+ } else {
+ continue
+ }
+ } else {
+ continue
+ }
+ }
+ return previousPosition
+ }
+
+ /**
+ * Unselect item UI
+ */
+ private fun itemUnselected() {
+ selectedCount!!.visibility = View.INVISIBLE
+ }
+
+ /**
+ * Select item UI
+ */
+ private fun itemSelected(i: Int) {
+ selectedCount!!.visibility = View.VISIBLE
+ selectedCount!!.text = i.toString()
+ }
+
+ /**
+ * Get position of an image from list
+ */
+ private fun getImagePosition(list: ArrayList?, image: Image): Int {
+ return list!!.indexOf(image)
+ }
+
+ /**
+ * Two types of loading indicators have been added to the zoom activity:
+ * 1. An Indeterminate spinner for showing the time lapsed between dispatch of the image request
+ * and starting to receiving the image.
+ * 2. ProgressBarDrawable that reflects how much image has been downloaded
+ */
+ private val loadingListener: ControllerListener =
+ object : BaseControllerListener() {
+ override fun onSubmit(id: String, callerContext: Any) {
+ // Sometimes the spinner doesn't appear when rapidly switching between images, this fixes that
+ spinner!!.visibility = View.VISIBLE
+ }
+
+ override fun onIntermediateImageSet(id: String, imageInfo: ImageInfo?) {
+ spinner!!.visibility = View.GONE
+ }
+
+ override fun onFinalImageSet(
+ id: String,
+ imageInfo: ImageInfo?,
+ animatable: Animatable?
+ ) {
+ spinner!!.visibility = View.GONE
+ }
+ }
+
+ private fun init(imageUri: Uri?) {
+ if (imageUri != null) {
+ val hierarchy = GenericDraweeHierarchyBuilder.newInstance(resources)
+ .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
+ .setProgressBarImage(ProgressBarDrawable())
+ .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
+ .build()
+ photo!!.hierarchy = hierarchy
+ photo!!.setAllowTouchInterceptionWhileZoomed(true)
+ photo!!.setIsLongpressEnabled(false)
+ photo!!.setTapListener(DoubleTapGestureListener(photo))
+ val controller: DraweeController = Fresco.newDraweeControllerBuilder()
+ .setUri(imageUri)
+ .setControllerListener(loadingListener)
+ .build()
+ photo!!.controller = controller
+
+ if (!images.isNullOrEmpty()) {
+ val selectedIndex = getImagePosition(selectedImages, images!![position])
+ val isSelected = selectedIndex != -1
+ if (isSelected) {
+ itemSelected(selectedImages!!.size)
+ } else {
+ itemUnselected()
+ }
+ }
+ }
+ }
+
+ /**
+ * Inserts an image in Not For Upload table
+ */
+ private suspend fun insertInNotForUpload(it: Image) {
+ val imageSHA1 = CustomSelectorUtils.getImageSHA1(
+ it.uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ notForUploadStatusDao.insert(
+ NotForUploadStatus(
+ imageSHA1
+ )
+ )
+ }
+
+ /**
+ * Send selected images in fragment
+ */
+ override fun onBackPressed() {
+ if (!images.isNullOrEmpty()) {
+ val returnIntent = Intent()
+ returnIntent.putParcelableArrayListExtra(
+ CustomSelectorConstants.NEW_SELECTED_IMAGES,
+ selectedImages
+ )
+ returnIntent.putExtra(SHOULD_REFRESH, shouldRefresh)
+ setResult(Activity.RESULT_OK, returnIntent)
+ finish()
+ }
+ super.onBackPressed()
+ }
+
+ override fun onDestroy() {
+ scope.cancel()
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt
new file mode 100644
index 000000000..daae40f61
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt
@@ -0,0 +1,103 @@
+package fr.free.nrw.commons.utils
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import androidx.exifinterface.media.ExifInterface
+import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.filepicker.PickedFiles
+import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
+import fr.free.nrw.commons.media.MediaClient
+import fr.free.nrw.commons.upload.FileProcessor
+import fr.free.nrw.commons.upload.FileUtilsWrapper
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.net.UnknownHostException
+
+/**
+ * Util Class for Custom Selector
+ */
+class CustomSelectorUtils {
+ companion object {
+
+ /**
+ * Get image sha1 from uri, used to retrieve the original image sha1.
+ */
+ suspend fun getImageSHA1(
+ uri: Uri,
+ ioDispatcher: CoroutineDispatcher,
+ fileUtilsWrapper: FileUtilsWrapper,
+ contentResolver: ContentResolver
+ ): String {
+ return withContext(ioDispatcher) {
+
+ try {
+ val result = fileUtilsWrapper.getSHA1(contentResolver.openInputStream(uri))
+ result
+ } catch (e: FileNotFoundException) {
+ e.printStackTrace()
+ ""
+ }
+ }
+ }
+
+ /**
+ * Generates modified SHA1 of an image
+ */
+ 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 true if the image exists on Commons, false otherwise.
+ */
+ suspend fun checkWhetherFileExistsOnCommonsUsingSHA1(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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/not_for_upload.xml b/app/src/main/res/drawable/not_for_upload.xml
new file mode 100644
index 000000000..a882eec7b
--- /dev/null
+++ b/app/src/main/res/drawable/not_for_upload.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml
index d96918fee..b7b3c70dc 100644
--- a/app/src/main/res/layout/activity_custom_selector.xml
+++ b/app/src/main/res/layout/activity_custom_selector.xml
@@ -13,7 +13,12 @@
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/bottom_layout"
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/>
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_zoomable.xml b/app/src/main/res/layout/activity_zoomable.xml
index d022199e2..b0dd45aac 100644
--- a/app/src/main/res/layout/activity_zoomable.xml
+++ b/app/src/main/res/layout/activity_zoomable.xml
@@ -23,4 +23,21 @@
app:layout_constraintTop_toTopOf="parent"
/>
+
+
diff --git a/app/src/main/res/layout/custom_selector_bottom_layout.xml b/app/src/main/res/layout/custom_selector_bottom_layout.xml
new file mode 100644
index 000000000..45439c29c
--- /dev/null
+++ b/app/src/main/res/layout/custom_selector_bottom_layout.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml
index 72ab6386a..f23bfe0e0 100644
--- a/app/src/main/res/layout/custom_selector_toolbar.xml
+++ b/app/src/main/res/layout/custom_selector_toolbar.xml
@@ -28,8 +28,9 @@
android:textAlignment="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/dimen_20"
app:layout_constraintStart_toEndOf="@id/back"
- app:layout_constraintEnd_toStartOf="@id/done"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:singleLine="true"
@@ -37,20 +38,5 @@
android:text="@string/custom_selector_title"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_custom_selector.xml b/app/src/main/res/layout/fragment_custom_selector.xml
index b0b4e6c28..bbc4c0a07 100644
--- a/app/src/main/res/layout/fragment_custom_selector.xml
+++ b/app/src/main/res/layout/fragment_custom_selector.xml
@@ -3,13 +3,37 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="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">
-
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/full_screen_mode_info_dialog.xml b/app/src/main/res/layout/full_screen_mode_info_dialog.xml
new file mode 100644
index 000000000..459ea42c4
--- /dev/null
+++ b/app/src/main/res/layout/full_screen_mode_info_dialog.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml
index f04a71922..4a44dbb98 100644
--- a/app/src/main/res/layout/item_custom_selector_image.xml
+++ b/app/src/main/res/layout/item_custom_selector_image.xml
@@ -82,6 +82,23 @@
android:visibility="gone"
app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/>
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index b3fb6d1a4..44c381a39 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -41,6 +41,7 @@
3dp
110dp
160dp
+ 36dp
24sp
@@ -59,11 +60,13 @@
0dp
2dp
+ 5dp
6dp
10dp
20dp
40dp
42dp
+ 50dp
250dp
150dp
72dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 524892ced..64988501e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -734,9 +734,21 @@ Upload your first media by tapping on the add button.
Error while sending feedback
What is your feedback?
Your feedback
+ Mark as not for upload
+ Unmark as not for upload
+ Show already actioned pictures
+ Hiding already actioned pictures
+ No more images found
+ This image is already uploaded
+ Can not select this image for upload
+ Image selected
+ Image marked as not for upload
Report
Report violation
Report this user
Report this content
Request to block this user
+ Welcome to Full-Screen Selection Mode
+ Use two fingers to zoom in and out.
+ Swipe fast and long to perform these actions: \n- Left/Right: Go to previous/next \n- Up: Select\n- Down: Mark as not for upload.
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListenerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListenerTest.kt
new file mode 100644
index 000000000..79de8c4d7
--- /dev/null
+++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/OnSwipeTouchListenerTest.kt
@@ -0,0 +1,144 @@
+package fr.free.nrw.commons.customselector.helper
+
+import android.content.Context
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import com.nhaarman.mockitokotlin2.whenever
+import fr.free.nrw.commons.TestAppAdapter
+import fr.free.nrw.commons.TestCommonsApplication
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.powermock.reflect.Whitebox
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import org.wikipedia.AppAdapter
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [21], application = TestCommonsApplication::class)
+internal class OnSwipeTouchListenerTest {
+
+ private lateinit var context: Context
+ private lateinit var onSwipeTouchListener: OnSwipeTouchListener
+ private lateinit var gesListener: OnSwipeTouchListener.GestureListener
+
+ @Mock
+ private lateinit var gestureDetector: GestureDetector
+
+ @Mock
+ private lateinit var view: View
+
+ @Mock
+ private lateinit var motionEvent1: MotionEvent
+
+ @Mock
+ private lateinit var motionEvent2: MotionEvent
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ AppAdapter.set(TestAppAdapter())
+
+ context = RuntimeEnvironment.application.applicationContext
+ onSwipeTouchListener = OnSwipeTouchListener(context)
+ gesListener = OnSwipeTouchListener(context).GestureListener()
+
+ Whitebox.setInternalState(onSwipeTouchListener, "gestureDetector", gestureDetector)
+
+ }
+
+ /**
+ * Test onTouch
+ */
+ @Test
+ fun onTouch() {
+ val func = onSwipeTouchListener.javaClass.getDeclaredMethod("onTouch", View::class.java, MotionEvent::class.java)
+ func.isAccessible = true
+ func.invoke(onSwipeTouchListener, view, motionEvent1)
+ }
+
+
+ /**
+ * Test onSwipeRight
+ */
+ @Test
+ fun onSwipeRight() {
+ onSwipeTouchListener.onSwipeRight()
+ }
+
+ /**
+ * Test onSwipeLeft
+ */
+ @Test
+ fun onSwipeLeft() {
+ onSwipeTouchListener.onSwipeLeft()
+ }
+
+ /**
+ * Test onSwipeUp
+ */
+ @Test
+ fun onSwipeUp() {
+ onSwipeTouchListener.onSwipeUp()
+ }
+
+ /**
+ * Test onSwipeDown
+ */
+ @Test
+ fun onSwipeDown() {
+ onSwipeTouchListener.onSwipeDown()
+ }
+
+ /**
+ * Test onDown
+ */
+ @Test
+ fun onDown() {
+ gesListener.onDown(motionEvent1)
+ }
+
+ /**
+ * Test onFling for onSwipeRight
+ */
+ @Test
+ fun `Test onFling for onSwipeRight`() {
+ whenever(motionEvent1.x).thenReturn(1f)
+ whenever(motionEvent2.x).thenReturn(110f)
+ gesListener.onFling(motionEvent1, motionEvent2, 2000f, 0f)
+ }
+
+ /**
+ * Test onFling for onSwipeLeft
+ */
+ @Test
+ fun `Test onFling for onSwipeLeft`() {
+ whenever(motionEvent1.x).thenReturn(110f)
+ whenever(motionEvent2.x).thenReturn(1f)
+ gesListener.onFling(motionEvent1, motionEvent2, 2000f, 0f)
+ }
+
+ /**
+ * Test onFling for onSwipeDown
+ */
+ @Test
+ fun `Test onFling for onSwipeDown`() {
+ whenever(motionEvent1.y).thenReturn(1f)
+ whenever(motionEvent2.y).thenReturn(110f)
+ gesListener.onFling(motionEvent1, motionEvent2, 0f, 2000f)
+ }
+
+ /**
+ * Test onFling for onSwipeUp
+ */
+ @Test
+ fun `Test onFling for onSwipeUp`() {
+ whenever(motionEvent1.y).thenReturn(110f)
+ whenever(motionEvent2.y).thenReturn(1f)
+ gesListener.onFling(motionEvent1, motionEvent2, 0f, 2000f)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt
index fac24cb32..61808c9bb 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt
@@ -2,6 +2,7 @@ package fr.free.nrw.commons.customselector.ui.adapter
import android.content.ContentResolver
import android.content.Context
+import android.content.SharedPreferences
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
@@ -13,22 +14,34 @@ import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.runner.RunWith
-import org.mockito.*
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.lang.reflect.Field
+import java.util.*
+import kotlin.collections.ArrayList
/**
* Custom Selector image adapter test.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
+@ExperimentalCoroutinesApi
class ImageAdapterTest {
@Mock
private lateinit var imageLoader: ImageLoader
@@ -38,6 +51,8 @@ class ImageAdapterTest {
private lateinit var context: Context
@Mock
private lateinit var mockContentResolver: ContentResolver
+ @Mock
+ private lateinit var sharedPreferences: SharedPreferences
private lateinit var activity: CustomSelectorActivity
private lateinit var imageAdapter: ImageAdapter
@@ -46,6 +61,7 @@ class ImageAdapterTest {
private lateinit var selectedImageField: Field
private var uri: Uri = Mockito.mock(Uri::class.java)
private lateinit var image: Image
+ private val testDispatcher = TestCoroutineDispatcher()
/**
@@ -55,6 +71,7 @@ class ImageAdapterTest {
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
+ Dispatchers.setMain(testDispatcher)
activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get()
imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader)
image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
@@ -68,6 +85,12 @@ class ImageAdapterTest {
selectedImageField.isAccessible = true
}
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ testDispatcher.cleanupTestCoroutines()
+ }
+
/**
* Test on create view holder.
*/
@@ -88,20 +111,43 @@ class ImageAdapterTest {
// Parameters.
images.add(image)
- imageAdapter.init(images)
+ imageAdapter.init(images, images, TreeMap())
+ whenever(context.getSharedPreferences("custom_selector", 0))
+ .thenReturn(sharedPreferences)
// Test conditions.
imageAdapter.onBindViewHolder(holder, 0)
selectedImageField.set(imageAdapter, images)
imageAdapter.onBindViewHolder(holder, 0)
}
+ /**
+ * Test processThumbnailForActionedImage
+ */
+ @Test
+ fun processThumbnailForActionedImage() = runBlocking {
+ Whitebox.setInternalState(imageAdapter, "allImages", listOf(image))
+ whenever(imageLoader.nextActionableImage(listOf(image), Dispatchers.IO, Dispatchers.Default,
+ 0)).thenReturn(0)
+ imageAdapter.processThumbnailForActionedImage(holder, 0)
+ }
+
+ /**
+ * Test processThumbnailForActionedImage
+ */
+ @Test
+ fun `processThumbnailForActionedImage when reached end of the folder`() = runBlocking {
+ whenever(imageLoader.nextActionableImage(ArrayList(), Dispatchers.IO, Dispatchers.Default,
+ 0)).thenReturn(-1)
+ imageAdapter.processThumbnailForActionedImage(holder, 0)
+ }
+
/**
* Test init.
*/
@Test
fun init() {
- imageAdapter.init(images)
+ imageAdapter.init(images, images, TreeMap())
}
/**
@@ -115,17 +161,37 @@ class ImageAdapterTest {
// Parameters
images.addAll(listOf(image, image))
- imageAdapter.init(images)
+ imageAdapter.init(images, images, TreeMap())
// Test conditions
holder.itemUploaded()
func.invoke(imageAdapter, holder, 0)
holder.itemNotUploaded()
+ holder.itemNotForUpload()
+ func.invoke(imageAdapter, holder, 0)
+ holder.itemNotForUpload()
func.invoke(imageAdapter, holder, 0)
selectedImageField.set(imageAdapter, images)
func.invoke(imageAdapter, holder, 1)
}
+ /**
+ * Test private function onThumbnailClicked.
+ */
+ @Test
+ fun onThumbnailClicked() {
+ images.add(image)
+ Whitebox.setInternalState(imageAdapter, "images", images)
+ // Access function
+ val func = imageAdapter.javaClass.getDeclaredMethod(
+ "onThumbnailClicked",
+ Int::class.java,
+ ImageAdapter.ImageViewHolder::class.java
+ )
+ func.isAccessible = true
+ func.invoke(imageAdapter, 0, holder)
+ }
+
/**
* Test get item count.
*/
@@ -134,12 +200,47 @@ class ImageAdapterTest {
Assertions.assertEquals(0, imageAdapter.itemCount)
}
+ /**
+ * Test setSelectedImages.
+ */
+ @Test
+ fun setSelectedImages() {
+ images.add(image)
+ imageAdapter.setSelectedImages(images)
+ }
+
+ /**
+ * Test refresh.
+ */
+ @Test
+ fun refresh() {
+ imageAdapter.refresh(listOf(image), listOf(image))
+ }
+
+ /**
+ * Test getSectionName.
+ */
+ @Test
+ fun getSectionName() {
+ images.add(image)
+ Whitebox.setInternalState(imageAdapter, "images", images)
+ Assertions.assertEquals("", imageAdapter.getSectionName(0))
+ }
+
+ /**
+ * Test cleanUp.
+ */
+ @Test
+ fun cleanUp() {
+ imageAdapter.cleanUp()
+ }
+
/**
* Test getImageId
*/
@Test
fun getImageIdAt() {
- imageAdapter.init(listOf(image))
+ imageAdapter.init(listOf(image), listOf(image), TreeMap())
Assertions.assertEquals(1, imageAdapter.getImageIdAt(0))
}
}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt
index 21007daeb..7d6d2f9e8 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt
@@ -1,23 +1,21 @@
package fr.free.nrw.commons.customselector.ui.selector
+import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.os.Looper
-import android.os.Looper.getMainLooper
import fr.free.nrw.commons.TestAppAdapter
import fr.free.nrw.commons.TestCommonsApplication
-import fr.free.nrw.commons.contributions.MainActivity
-import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.runner.RunWith
+import org.mockito.Mockito
import org.mockito.MockitoAnnotations
+import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
-import org.robolectric.Shadows
-import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.wikipedia.AppAdapter
import java.lang.reflect.Method
@@ -31,6 +29,14 @@ class CustomSelectorActivityTest {
private lateinit var activity: CustomSelectorActivity
+ private lateinit var imageFragment: ImageFragment
+
+ private lateinit var images : java.util.ArrayList
+
+ private var uri: Uri = Mockito.mock(Uri::class.java)
+
+ private lateinit var image: Image
+
/**
* Set up the tests.
*/
@@ -44,6 +50,12 @@ class CustomSelectorActivityTest {
val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java)
onCreate.isAccessible = true
onCreate.invoke(activity, null)
+ imageFragment = ImageFragment.newInstance(1,0)
+ image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
+ images = ArrayList()
+
+ Whitebox.setInternalState(activity, "imageFragment", imageFragment)
+ Whitebox.setInternalState(imageFragment, "imageAdapter", Mockito.mock(ImageAdapter::class.java))
}
/**
@@ -75,13 +87,61 @@ class CustomSelectorActivityTest {
activity.onFolderClick(1, "test", 0);
}
+ /**
+ * Test onActivityResult function.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun testOnActivityResult() {
+ val func = activity.javaClass.getDeclaredMethod(
+ "onActivityResult",
+ Int::class.java,
+ Int::class.java,
+ Intent::class.java
+ )
+ func.isAccessible = true
+ func.invoke(activity, 512, -1, Mockito.mock(Intent::class.java))
+ }
+
+ /**
+ * Test showWelcomeDialog function.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun testShowWelcomeDialog() {
+ val func = activity.javaClass.getDeclaredMethod(
+ "showWelcomeDialog"
+ )
+ func.isAccessible = true
+ func.invoke(activity)
+ }
+
+
+ /**
+ * Test onLongPress function.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun testOnLongPress() {
+ val func = activity.javaClass.getDeclaredMethod(
+ "onLongPress",
+ Int::class.java,
+ ArrayList::class.java,
+ ArrayList::class.java
+ )
+ images.add(image)
+
+ func.isAccessible = true
+ func.invoke(activity, 0, images, images)
+ }
+
/**
* Test selectedImagesChanged function.
*/
@Test
@Throws(Exception::class)
fun testOnSelectedImagesChanged() {
- activity.onSelectedImagesChanged(ArrayList())
+ activity.onSelectedImagesChanged(ArrayList(), 0)
}
/**
@@ -91,10 +151,40 @@ class CustomSelectorActivityTest {
@Throws(Exception::class)
fun testOnDone() {
activity.onDone()
- activity.onSelectedImagesChanged(ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1))));
+ activity.onSelectedImagesChanged(
+ ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1))),
+ 1
+ )
activity.onDone()
}
+ /**
+ * Test onClickNotForUpload function.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun testOnClickNotForUpload() {
+ val method: Method = CustomSelectorActivity::class.java.getDeclaredMethod(
+ "onClickNotForUpload"
+ )
+ method.isAccessible = true
+ method.invoke(activity)
+ activity.onSelectedImagesChanged(
+ ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1))),
+ 0
+ )
+ method.invoke(activity)
+ }
+
+ /**
+ * Test setOnDataListener Function.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun testSetOnDataListener() {
+ activity.setOnDataListener(imageFragment)
+ }
+
/**
* Test onBackPressed Function.
*/
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt
index 15ee8eca0..3a2d6e683 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt
@@ -64,7 +64,8 @@ class ImageFileLoaderTest {
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID,
- MediaStore.Images.Media.BUCKET_DISPLAY_NAME
+ MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
+ MediaStore.Images.Media.DATE_ADDED
)
Whitebox.setInternalState(imageFileLoader, "coroutineContext", coroutineContext)
@@ -103,6 +104,7 @@ class ImageFileLoaderTest {
anyOrNull(),
anyOrNull(),
anyOrNull(),
+ anyOrNull(),
anyOrNull()
)
} doReturn imageCursor;
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt
index 10ebcc4e8..86323e672 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt
@@ -47,6 +47,7 @@ import java.lang.reflect.Field
class ImageFragmentTest {
private lateinit var fragment: ImageFragment
+ private lateinit var activity: CustomSelectorActivity
private lateinit var view: View
private lateinit var selectorRV : RecyclerView
private lateinit var loader : ProgressBar
@@ -76,7 +77,7 @@ class ImageFragmentTest {
AppAdapter.set(TestAppAdapter())
SoLoader.setInTestMode()
Fresco.initialize(context)
- val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get()
+ activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get()
fragment = ImageFragment.newInstance(1,0)
val fragmentManager: FragmentManager = activity.supportFragmentManager
@@ -92,6 +93,7 @@ class ImageFragmentTest {
Whitebox.setInternalState(fragment, "imageAdapter", adapter)
Whitebox.setInternalState(fragment, "selectorRV", selectorRV )
Whitebox.setInternalState(fragment, "loader", loader)
+ Whitebox.setInternalState(fragment, "filteredImages", arrayListOf(image,image))
viewModelField = fragment.javaClass.getDeclaredField("viewModel")
viewModelField.isAccessible = true
@@ -139,6 +141,21 @@ class ImageFragmentTest {
assertEquals(3, func.invoke(fragment))
}
+ /**
+ * Test onAttach function.
+ */
+ @Test
+ fun testOnAttach() {
+ fragment.onAttach(activity)
+ }
+
+ /**
+ * Test refresh function.
+ */
+ @Test
+ fun testRefresh() {
+ fragment.refresh()
+ }
/**
* Test onResume.
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt
index 0c3a7de3a..162ae5fc1 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt
@@ -2,9 +2,11 @@ package fr.free.nrw.commons.customselector.ui.selector
import android.content.ContentResolver
import android.content.Context
+import android.content.SharedPreferences
import android.net.Uri
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.TestCommonsApplication
+import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.model.Image
@@ -61,6 +63,9 @@ class ImageLoaderTest {
@Mock
private lateinit var uploadedStatusDao: UploadedStatusDao
+ @Mock
+ private lateinit var notForUploadStatusDao: NotForUploadStatusDao
+
@Mock
private lateinit var holder: ImageAdapter.ImageViewHolder
@@ -97,7 +102,8 @@ class ImageLoaderTest {
MockitoAnnotations.initMocks(this)
imageLoader =
- ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context)
+ ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao,
+ notForUploadStatusDao, context)
uploadedStatus= UploadedStatus(
"testSha1",
"testSha1",
@@ -112,8 +118,6 @@ class ImageLoaderTest {
Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1);
Whitebox.setInternalState(imageLoader, "mapResult", mapResult);
Whitebox.setInternalState(imageLoader, "context", context)
- Whitebox.setInternalState(imageLoader, "ioDispatcher", testDispacher)
- Whitebox.setInternalState(imageLoader, "defaultDispatcher", testDispacher)
whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream)
whenever(context.contentResolver).thenReturn(contentResolver)
@@ -136,14 +140,17 @@ class ImageLoaderTest {
@Test
fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest {
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
+ whenever(notForUploadStatusDao.find(any())).thenReturn(0)
mapModifiedImageSHA1[image] = "testSha1"
mapImageSHA1[uri] = "testSha1"
+ whenever(context.getSharedPreferences("custom_selector", 0))
+ .thenReturn(Mockito.mock(SharedPreferences::class.java))
mapResult["testSha1"] = ImageLoader.Result.TRUE
- imageLoader.queryAndSetView(holder, image)
+ imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
mapResult["testSha1"] = ImageLoader.Result.FALSE
- imageLoader.queryAndSetView(holder, image)
+ imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
}
/**
@@ -152,20 +159,38 @@ class ImageLoaderTest {
@Test
fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest {
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 nextActionableImage
*/
@Test
- fun testQuerySha1() = testDispacher.runBlockingTest {
+ fun testNextActionableImage() = testDispacher.runBlockingTest {
+ whenever(notForUploadStatusDao.find(any())).thenReturn(0)
+ whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(0)
+ whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(0)
+ PowerMockito.mockStatic(PickedFiles::class.java)
+ BDDMockito.given(PickedFiles.pickedExistingPicture(context, image.uri))
+ .willReturn(UploadableFile(uri, File("ABC")))
+ whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream)
+ whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1")
+ whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
+ uploadableFile
+ )
+ imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
- whenever(single.blockingGet()).thenReturn(true)
- whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single)
- whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1")
+ whenever(notForUploadStatusDao.find(any())).thenReturn(1)
+ imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
- imageLoader.querySHA1("testSha1")
+ whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(2)
+ imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
+
+ whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(2)
+ imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
}
/**
@@ -183,13 +208,13 @@ class ImageLoaderTest {
whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream)
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(
uploadableFile
)
mapModifiedImageSHA1[image] = "testSha2"
- Assert.assertEquals("testSha2", imageLoader.getSHA1(image));
+ Assert.assertEquals("testSha2", imageLoader.getSHA1(image, testDispacher));
}
/**
@@ -213,8 +238,4 @@ class ImageLoaderTest {
imageLoader.getResultFromUploadedStatus(uploadedStatus))
}
- @Test
- fun testCleanUP() {
- imageLoader.cleanUP()
- }
}
\ No newline at end of file
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt
index 14b395973..4be134836 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/media/ZoomableActivityUnitTests.kt
@@ -5,18 +5,25 @@ import android.content.Intent
import android.net.Uri
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.soloader.SoLoader
+import fr.free.nrw.commons.TestAppAdapter
import fr.free.nrw.commons.TestCommonsApplication
+import fr.free.nrw.commons.customselector.model.CallbackStatus
+import fr.free.nrw.commons.customselector.model.Image
+import fr.free.nrw.commons.customselector.model.Result
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import org.wikipedia.AppAdapter
+import java.lang.reflect.Field
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@@ -25,18 +32,32 @@ class ZoomableActivityUnitTests {
private lateinit var context: Context
private lateinit var activity: ZoomableActivity
+ private lateinit var viewModelField: Field
+ private lateinit var image: Image
@Mock
private lateinit var uri: Uri
+ @Mock
+ private lateinit var images: ArrayList
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ AppAdapter.set(TestAppAdapter())
context = RuntimeEnvironment.application.applicationContext
SoLoader.setInTestMode()
Fresco.initialize(context)
val intent = Intent().setData(uri)
activity = Robolectric.buildActivity(ZoomableActivity::class.java, intent).create().get()
+
+ image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
+
+ Whitebox.setInternalState(activity, "images", arrayListOf(image))
+ Whitebox.setInternalState(activity, "selectedImages", arrayListOf(image))
+
+ viewModelField = activity.javaClass.getDeclaredField("viewModel")
+ viewModelField.isAccessible = true
}
@Test
@@ -45,4 +66,87 @@ class ZoomableActivityUnitTests {
Assert.assertNotNull(activity)
}
+ /**
+ * Test handleResult.
+ */
+ @Test
+ fun testHandleResult(){
+ val func = activity.javaClass.getDeclaredMethod("handleResult", Result::class.java)
+ func.isAccessible = true
+ func.invoke(activity, Result(CallbackStatus.SUCCESS, arrayListOf()))
+ func.invoke(activity, Result(CallbackStatus.SUCCESS, arrayListOf(image,image)))
+ }
+
+ /**
+ * Test onLeftSwiped.
+ */
+ @Test
+ fun testOnLeftSwiped(){
+ val func = activity.javaClass.getDeclaredMethod("onLeftSwiped", Boolean::class.java)
+ func.isAccessible = true
+ func.invoke(activity, true)
+
+ Whitebox.setInternalState(activity, "images", arrayListOf(image, image))
+ Whitebox.setInternalState(activity, "position", 0)
+ func.invoke(activity, true)
+
+ func.invoke(activity, false)
+ }
+
+ /**
+ * Test onRightSwiped.
+ */
+ @Test
+ fun testOnRightSwiped(){
+ val func = activity.javaClass.getDeclaredMethod("onRightSwiped", Boolean::class.java)
+ func.isAccessible = true
+ func.invoke(activity, true)
+
+ Whitebox.setInternalState(activity, "images", arrayListOf(image, image))
+ Whitebox.setInternalState(activity, "position", 1)
+ func.invoke(activity, true)
+
+ func.invoke(activity, false)
+ }
+
+ /**
+ * Test onUpSwiped.
+ */
+ @Test
+ fun testOnUpSwiped(){
+ val func = activity.javaClass.getDeclaredMethod("onUpSwiped")
+ func.isAccessible = true
+ func.invoke(activity)
+ }
+
+ /**
+ * Test onDownSwiped.
+ */
+ @Test
+ fun testOnDownSwiped(){
+ val func = activity.javaClass.getDeclaredMethod("onDownSwiped")
+ func.isAccessible = true
+ func.invoke(activity)
+ }
+
+ /**
+ * Test onBackPressed.
+ */
+ @Test
+ fun testOnBackPressed(){
+ val func = activity.javaClass.getDeclaredMethod("onBackPressed")
+ func.isAccessible = true
+ func.invoke(activity)
+ }
+
+
+ /**
+ * Test onDestroy.
+ */
+ @Test
+ fun testOnDestroy(){
+ val func = activity.javaClass.getDeclaredMethod("onDestroy")
+ func.isAccessible = true
+ func.invoke(activity)
+ }
}
\ No newline at end of file