mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
[GSoC 2022] Improve custom picker (all features) (#5035)
* Project Initiated by creating helper classes for database operations * Database created * Rest of the work and documentation * Requested changes done * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * [GSoC] Insert and Remove Images from not for upload (#4999) * Inserted and marked images as not for upload * Documentation added * Test delete * Implemented remove from not for upload * Test fixed * Requested changes done * Added tests for new lines in existing classes * [GSoC] Added Bubble Scroll (#5023) * Library added * Bubble scroll implemented * Left and right swipe * Requested changes * [GSoC] Hide/Unhide actioned pictures and change numbering (#5012) * Changed numbering of marked images * Hide Unhide implemented * Test fixed * Improved speed for database operation * Improved speed for database operation * Changed progress dialog * Improved hiding speed * Test fixed * Fixed bug * Fixed bug and improved performance * Fixed bug and improved performance * Test fixed * Bug fixed * Bug fixed * Bug fixed * Bug fixed * Bug fixed * Code clean up * Test hiding images * Test hiding images * Test hiding images * Code clean up and test fixed * Fixed layout * Fixed bug * Bug fixed * Renamed method * Documentation added explaining logic * Documentation added explaining logic * [GSoC] Full Screen Mode (#5032) * Gesture detection implemented * Left and right swipe * Selection implemented * onDown implemented * onDown implemented * FS mode implemented * OnSwipe doc * Scope cancel * Added label in Manifest * Merged two features * Requested changes * Image uploaded bug fixed * Increased DB version * Made requested changes * Made requested changes * Made requested changes * Made requested changes * Solved image flashing bug * Solved image flashing bug * Requested changes * Requested changes * Changed name of a function * Fixed transaction failure on large number of images * Tested with isIdentity * Tested with isIdentity * Increased the threshold * Added info dialog * Minor changes * ImageAdapter Test * CustomSelectorActivity Test * Requested changes * Test for ZoomableActivity * Test for ZoomableActivity * Test for ImageLoader * Test for OnSwipeTouchListener * Test for rest * Reverted some test changes * Added more tests for ImageAdapter * Added more tests for ImageAdapter and swipe gesture Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
This commit is contained in:
parent
b5ffe7120c
commit
33679eb6b4
41 changed files with 2560 additions and 284 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -56,7 +56,10 @@
|
|||
android:finishOnTaskLaunch="true" />
|
||||
|
||||
<activity
|
||||
android:name=".media.ZoomableActivity" />
|
||||
android:name=".media.ZoomableActivity"
|
||||
android:label="Zoomable Activity"
|
||||
android:configChanges="screenSize|keyboard|orientation"
|
||||
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||
|
||||
<activity android:name=".auth.LoginActivity">
|
||||
<intent-filter>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<Int, Int> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Image>)
|
||||
fun onSelectedImagesChanged(selectedImages: ArrayList<Image>, selectedNotForUploadImages: Int)
|
||||
|
||||
/**
|
||||
* onLongPress
|
||||
* @param imageUri : uri of image
|
||||
*/
|
||||
fun onLongPress(imageUri: Uri)
|
||||
fun onLongPress(
|
||||
position: Int,
|
||||
images: ArrayList<Image>,
|
||||
selectedImages: ArrayList<Image>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Image>, shouldRefresh: Boolean)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package fr.free.nrw.commons.customselector.listeners
|
||||
|
||||
/**
|
||||
* Refresh UI Listener
|
||||
*/
|
||||
interface RefreshUIListener {
|
||||
/**
|
||||
* Refreshes the data in adapter
|
||||
*/
|
||||
fun refresh()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<ImageAdapter.ImageViewHolder>(context) {
|
||||
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(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<Image>()
|
||||
|
||||
/**
|
||||
* 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<Image> = ArrayList()
|
||||
|
||||
/**
|
||||
* Stores all images
|
||||
*/
|
||||
private var allImages: List<Image> = ArrayList()
|
||||
|
||||
/**
|
||||
* Map to store actionable images
|
||||
*/
|
||||
private var actionableImagesMap: TreeMap<Int, Image> = TreeMap()
|
||||
|
||||
/**
|
||||
* Stores already added positions of actionable images
|
||||
*/
|
||||
private var alreadyAddedPositions: ArrayList<Int> = ArrayList()
|
||||
|
||||
/**
|
||||
* Next starting index to initiate query to find next actionable image
|
||||
*/
|
||||
private var 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<Image> = 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<Int> = 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<Image>) {
|
||||
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>) {
|
||||
allImages = fixedImages
|
||||
val oldImageList:ArrayList<Image> = images
|
||||
val newImageList:ArrayList<Image> = 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<Image>){
|
||||
selectedImages = ArrayList(newSelectedImages)
|
||||
imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
|
||||
}
|
||||
/**
|
||||
* Refresh the data in the adapter
|
||||
*/
|
||||
fun refresh(newImages: List<Image>, fixedImages: List<Image>) {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Image> =
|
||||
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<Image>) {
|
||||
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<Image>) {
|
||||
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<Image>) {
|
||||
override fun onSelectedImagesChanged(selectedImages: ArrayList<Image>,
|
||||
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<Image>,
|
||||
selectedImages: ArrayList<Image>
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<Image>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Image>;
|
||||
|
||||
/**
|
||||
* Stores all images
|
||||
*/
|
||||
var allImages: ArrayList<Image> = 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<Image>, shouldRefresh: Boolean){
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
|
||||
if (!showAlreadyActionedImages && shouldRefresh) {
|
||||
imageAdapter.init(filteredImages, allImages, TreeMap())
|
||||
imageAdapter.setSelectedImages(selectedImages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Uri, String> = 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<Image>, 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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<ImageInfo>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
653
app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt
Normal file
653
app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt
Normal file
|
|
@ -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<Image>? = null
|
||||
|
||||
/**
|
||||
* Total selected images present in folder
|
||||
*/
|
||||
private var selectedImages: ArrayList<Image>? = 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: 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<ImageInfo?> =
|
||||
object : BaseControllerListener<ImageInfo?>() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
app/src/main/res/drawable/not_for_upload.xml
Normal file
6
app/src/main/res/drawable/not_for_upload.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#D50000" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/>
|
||||
|
||||
</vector>
|
||||
|
|
@ -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"/>
|
||||
|
||||
<include
|
||||
layout="@layout/custom_selector_bottom_layout"
|
||||
android:id="@+id/bottom_sheet"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -23,4 +23,21 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/selection_count"
|
||||
android:layout_width="@dimen/dimen_20"
|
||||
android:layout_height="@dimen/dimen_20"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
android:textSize="@dimen/subtitle_text"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/black"
|
||||
android:layout_margin="@dimen/dimen_6"
|
||||
android:gravity="center|center_vertical"
|
||||
style="@style/TextAppearance.AppCompat.Small"
|
||||
android:text="12"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/circle_shape"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
49
app/src/main/res/layout/custom_selector_bottom_layout.xml
Normal file
49
app/src/main/res/layout/custom_selector_bottom_layout.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottom_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/not_for_upload"
|
||||
android:layout_width="@dimen/dimen_0"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mark_as_not_for_upload"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="visible"
|
||||
android:layout_margin="@dimen/dimen_5"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottom_layout"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toStartOf="@id/bottom_layout"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_layout" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/upload"
|
||||
android:layout_width="@dimen/dimen_0"
|
||||
android:layout_height="@dimen/dimen_0"
|
||||
android:text="@string/upload"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="visible"
|
||||
android:layout_margin="@dimen/dimen_5"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bottom_layout"
|
||||
app:layout_constraintEnd_toEndOf="@id/bottom_layout"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_layout" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</merge>
|
||||
|
|
@ -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" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/done"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:clickable="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/standard_gap"
|
||||
app:srcCompat="?attr/custom_selector_done"
|
||||
android:visibility="invisible"
|
||||
android:contentDescription="@string/done" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</merge>
|
||||
|
|
@ -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">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
<Switch
|
||||
android:id="@+id/switchWidget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:text="@string/show_already_actioned_pictures"
|
||||
android:padding="@dimen/dimen_10"
|
||||
android:checked="true" />
|
||||
|
||||
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
android:id="@+id/selector_rv"
|
||||
android:background="?attr/mainBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/mainBackground"
|
||||
android:layout_height="@dimen/dimen_0"
|
||||
app:fastScrollPopupBgColor="@color/primaryColor"
|
||||
app:fastScrollPopupTextColor="@android:color/primary_text_dark"
|
||||
app:fastScrollPopupTextSize="@dimen/subheading_text_size"
|
||||
app:fastScrollPopupBackgroundSize="@dimen/bubble_size"
|
||||
app:fastScrollThumbColor="@color/primaryColor"
|
||||
app:fastScrollTrackColor="@color/upload_overlay_background_light"
|
||||
app:fastScrollPopupPosition="adjacent"
|
||||
app:layout_constraintBottom_toTopOf="@id/progressLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/switchWidget"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
|
|
@ -38,4 +62,41 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/progressLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/dimen_5"
|
||||
android:background="@color/drawerHeader_background_light"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/selector_rv">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hiding_already_actioned_pictures"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/dimen_5"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@id/text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
54
app/src/main/res/layout/full_screen_mode_info_dialog.xml
Normal file
54
app/src/main/res/layout/full_screen_mode_info_dialog.xml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_margin="@dimen/dimen_10">
|
||||
|
||||
<ScrollView
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
||||
android:text="@string/welcome_to_full_screen_mode_text"
|
||||
android:textSize="@dimen/normal_text"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/dimen_10"
|
||||
android:text="@string/full_screen_mode_zoom_info"
|
||||
android:textSize="@dimen/description_text_size"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/dimen_10"
|
||||
android:layout_marginEnd="@dimen/dimen_10"
|
||||
android:layout_marginStart="@dimen/dimen_10"
|
||||
android:text="@string/full_screen_mode_features_info"
|
||||
android:textSize="@dimen/description_text_size"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btn_ok"
|
||||
android:layout_marginHorizontal="@dimen/dimen_40"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:text="@string/welcome_custom_selector_ok"
|
||||
/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -82,6 +82,23 @@
|
|||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/not_for_upload_overlay_icon"
|
||||
android:layout_width="@dimen/dimen_50"
|
||||
android:layout_height="@dimen/dimen_50"
|
||||
android:paddingBottom="@dimen/dimen_20"
|
||||
android:paddingEnd="@dimen/dimen_20"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:srcCompat="@drawable/not_for_upload"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/not_for_upload_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="selected_overlay,not_for_upload_overlay_icon"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
<dimen name="progressbar_stroke">3dp</dimen>
|
||||
<dimen name="notification_width">110dp</dimen>
|
||||
<dimen name="notification_height">160dp</dimen>
|
||||
<dimen name="bubble_size">36dp</dimen>
|
||||
|
||||
<!-- Text sizes -->
|
||||
<dimen name="heading_text_size">24sp</dimen>
|
||||
|
|
@ -59,11 +60,13 @@
|
|||
<!-- Commonly used dimensions -->
|
||||
<dimen name="dimen_0">0dp</dimen>
|
||||
<dimen name="dimen_2">2dp</dimen>
|
||||
<dimen name="dimen_5">5dp</dimen>
|
||||
<dimen name="dimen_6">6dp</dimen>
|
||||
<dimen name="dimen_10">10dp</dimen>
|
||||
<dimen name="dimen_20">20dp</dimen>
|
||||
<dimen name="dimen_40">40dp</dimen>
|
||||
<dimen name="dimen_42">42dp</dimen>
|
||||
<dimen name="dimen_50">50dp</dimen>
|
||||
<dimen name="dimen_250">250dp</dimen>
|
||||
<dimen name="dimen_150">150dp</dimen>
|
||||
<dimen name="dimen_72">72dp</dimen>
|
||||
|
|
|
|||
|
|
@ -734,9 +734,21 @@ Upload your first media by tapping on the add button.</string>
|
|||
<string name="error_feedback">Error while sending feedback</string>
|
||||
<string name="enter_description">What is your feedback?</string>
|
||||
<string name="your_feedback">Your feedback</string>
|
||||
<string name="mark_as_not_for_upload">Mark as not for upload</string>
|
||||
<string name="unmark_as_not_for_upload">Unmark as not for upload</string>
|
||||
<string name="show_already_actioned_pictures">Show already actioned pictures</string>
|
||||
<string name="hiding_already_actioned_pictures">Hiding already actioned pictures</string>
|
||||
<string name="no_more_images_found">No more images found</string>
|
||||
<string name="this_image_is_already_uploaded">This image is already uploaded</string>
|
||||
<string name="can_not_select_this_image_for_upload">Can not select this image for upload</string>
|
||||
<string name="image_selected">Image selected</string>
|
||||
<string name="image_marked_as_not_for_upload">Image marked as not for upload</string>
|
||||
<string name="menu_view_report">Report</string>
|
||||
<string name="report_violation">Report violation</string>
|
||||
<string name="report_user">Report this user</string>
|
||||
<string name="report_content">Report this content</string>
|
||||
<string name="request_user_block">Request to block this user</string>
|
||||
<string name="welcome_to_full_screen_mode_text">Welcome to Full-Screen Selection Mode</string>
|
||||
<string name="full_screen_mode_zoom_info">Use two fingers to zoom in and out.</string>
|
||||
<string name="full_screen_mode_features_info">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.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Image>
|
||||
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Image>
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue