mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +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.exifinterface:exifinterface:1.3.2"
|
||||||
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
||||||
implementation "androidx.multidex:multidex:2.0.1"
|
implementation "androidx.multidex:multidex:2.0.1"
|
||||||
|
compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
||||||
|
|
||||||
//swipe_layout
|
//swipe_layout
|
||||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,10 @@
|
||||||
android:finishOnTaskLaunch="true" />
|
android:finishOnTaskLaunch="true" />
|
||||||
|
|
||||||
<activity
|
<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">
|
<activity android:name=".auth.LoginActivity">
|
||||||
<intent-filter>
|
<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)
|
insert(uploadedStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the imageSHA1 is present in database
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ")
|
||||||
|
abstract suspend fun findByImageSHA1(imageSHA1 : String, imageResult: Boolean): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the modifiedImageSHA1 is present in database
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ")
|
||||||
|
abstract suspend fun findByModifiedImageSHA1(modifiedImageSHA1 : String,
|
||||||
|
modifiedImageResult: Boolean): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous image sha1 query.
|
* Asynchronous image sha1 query.
|
||||||
*/
|
*/
|
||||||
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
|
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
|
||||||
return getFromImageSHA1(imageSHA1)
|
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
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Helper object, includes all the static functions required by custom selector.
|
* Image Helper object, includes all the static functions and variables required by custom selector.
|
||||||
*/
|
*/
|
||||||
object ImageHelper {
|
object ImageHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom selector preference key
|
||||||
|
*/
|
||||||
|
const val CUSTOM_SELECTOR_PREFERENCE_KEY: String = "custom_selector"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch state preference key
|
||||||
|
*/
|
||||||
|
const val SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY: String = "show_already_actioned_images"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of folders from given image list.
|
* 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
|
* onSelectedImagesChanged
|
||||||
* @param selectedImages : new selected images.
|
* @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
|
* onLongPress
|
||||||
* @param imageUri : uri of image
|
* @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.
|
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 {
|
) : Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,6 +60,7 @@ data class Image(
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
parcel.readLong(),
|
parcel.readLong(),
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
|
parcel.readString()!!,
|
||||||
parcel.readString()!!
|
parcel.readString()!!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -68,6 +75,7 @@ data class Image(
|
||||||
parcel.writeLong(bucketId)
|
parcel.writeLong(bucketId)
|
||||||
parcel.writeString(bucketName)
|
parcel.writeString(bucketName)
|
||||||
parcel.writeString(sha1)
|
parcel.writeString(sha1)
|
||||||
|
parcel.writeString(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.customselector.ui.adapter
|
package fr.free.nrw.commons.customselector.ui.adapter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
|
@ -10,11 +11,17 @@ import androidx.constraintlayout.widget.Group
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||||
|
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
||||||
|
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom selector ImageAdapter.
|
* Custom selector ImageAdapter.
|
||||||
|
|
@ -36,7 +43,7 @@ class ImageAdapter(
|
||||||
private var imageLoader: ImageLoader
|
private var imageLoader: ImageLoader
|
||||||
):
|
):
|
||||||
|
|
||||||
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context) {
|
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context), FastScrollRecyclerView.SectionedAdapter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageSelectedOrUpdated payload class.
|
* ImageSelectedOrUpdated payload class.
|
||||||
|
|
@ -48,16 +55,58 @@ class ImageAdapter(
|
||||||
*/
|
*/
|
||||||
class ImageUnselected
|
class ImageUnselected
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether addition of all actionable images is done or not
|
||||||
|
*/
|
||||||
|
private var reachedEndOfFolder: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently selected images.
|
* Currently selected images.
|
||||||
*/
|
*/
|
||||||
private var selectedImages = arrayListOf<Image>()
|
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.
|
* List of all images in adapter.
|
||||||
*/
|
*/
|
||||||
private var images: ArrayList<Image> = ArrayList()
|
private var images: ArrayList<Image> = ArrayList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all images
|
||||||
|
*/
|
||||||
|
private var allImages: List<Image> = ArrayList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map to store actionable images
|
||||||
|
*/
|
||||||
|
private var 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.
|
* Create View holder.
|
||||||
*/
|
*/
|
||||||
|
|
@ -70,7 +119,7 @@ class ImageAdapter(
|
||||||
* Bind View holder, load image, selected view, click listeners.
|
* Bind View holder, load image, selected view, click listeners.
|
||||||
*/
|
*/
|
||||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||||
val image=images[position]
|
var image=images[position]
|
||||||
holder.image.setImageDrawable (null)
|
holder.image.setImageDrawable (null)
|
||||||
if (context.contentResolver.getType(image.uri) == null) {
|
if (context.contentResolver.getType(image.uri) == null) {
|
||||||
// Image does not exist anymore, update adapter.
|
// Image does not exist anymore, update adapter.
|
||||||
|
|
@ -81,36 +130,171 @@ class ImageAdapter(
|
||||||
notifyItemRangeChanged(updatedPosition, images.size)
|
notifyItemRangeChanged(updatedPosition, images.size)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val selectedIndex = ImageHelper.getIndex(selectedImages, image)
|
val sharedPreferences: SharedPreferences =
|
||||||
|
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||||
|
val 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
|
val isSelected = selectedIndex != -1
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
holder.itemSelected(selectedIndex + 1)
|
holder.itemSelected(selectedImages.size)
|
||||||
} else {
|
} else {
|
||||||
holder.itemUnselected();
|
holder.itemUnselected()
|
||||||
}
|
}
|
||||||
Glide.with(holder.image).load(image.uri).thumbnail(0.3f).into(holder.image)
|
|
||||||
imageLoader.queryAndSetView(holder, image)
|
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 {
|
holder.itemView.setOnClickListener {
|
||||||
selectOrRemoveImage(holder, position)
|
onThumbnailClicked(position, holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// launch media preview on long click.
|
// launch media preview on long click.
|
||||||
holder.itemView.setOnLongClickListener {
|
holder.itemView.setOnLongClickListener {
|
||||||
imageSelectListener.onLongPress(image.uri)
|
imageSelectListener.onLongPress(images.indexOf(image), images, selectedImages)
|
||||||
true
|
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.
|
* Handle click event on an image, update counter on images.
|
||||||
*/
|
*/
|
||||||
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){
|
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){
|
||||||
val clickedIndex = ImageHelper.getIndex(selectedImages, images[position])
|
val sharedPreferences: SharedPreferences =
|
||||||
|
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||||
|
val 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) {
|
if (clickedIndex != -1) {
|
||||||
selectedImages.removeAt(clickedIndex)
|
selectedImages.removeAt(clickedIndex)
|
||||||
|
if (holder.isItemNotForUpload()) {
|
||||||
|
numberOfSelectedImagesMarkedAsNotForUpload--
|
||||||
|
}
|
||||||
notifyItemChanged(position, ImageUnselected())
|
notifyItemChanged(position, ImageUnselected())
|
||||||
val indexes = ImageHelper.getIndexList(selectedImages, images)
|
|
||||||
|
// Getting index from all images index when switch is on
|
||||||
|
val indexes = if (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) {
|
for (index in indexes) {
|
||||||
notifyItemChanged(index, ImageSelectedOrUpdated())
|
notifyItemChanged(index, ImageSelectedOrUpdated())
|
||||||
}
|
}
|
||||||
|
|
@ -118,19 +302,42 @@ class ImageAdapter(
|
||||||
if (holder.isItemUploaded()) {
|
if (holder.isItemUploaded()) {
|
||||||
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
|
if (holder.isItemNotForUpload()) {
|
||||||
|
numberOfSelectedImagesMarkedAsNotForUpload++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting index from all images index when switch is on
|
||||||
|
val indexes: ArrayList<Int> = if (showAlreadyActionedImages) {
|
||||||
selectedImages.add(images[position])
|
selectedImages.add(images[position])
|
||||||
notifyItemChanged(position, ImageSelectedOrUpdated())
|
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.
|
* Initialize the data set.
|
||||||
*/
|
*/
|
||||||
fun init(newImages: List<Image>) {
|
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>) {
|
||||||
|
allImages = fixedImages
|
||||||
val oldImageList:ArrayList<Image> = images
|
val oldImageList:ArrayList<Image> = images
|
||||||
val newImageList:ArrayList<Image> = ArrayList(newImages)
|
val newImageList:ArrayList<Image> = ArrayList(newImages)
|
||||||
|
actionableImagesMap = emptyMap
|
||||||
|
alreadyAddedPositions = ArrayList()
|
||||||
|
nextImagePosition = 0
|
||||||
|
reachedEndOfFolder = false
|
||||||
|
selectedImages = ArrayList()
|
||||||
|
imagePositionAsPerIncreasingOrder = 0
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
val diffResult = DiffUtil.calculateDiff(
|
||||||
ImagesDiffCallback(oldImageList, newImageList)
|
ImagesDiffCallback(oldImageList, newImageList)
|
||||||
)
|
)
|
||||||
|
|
@ -138,19 +345,63 @@ class ImageAdapter(
|
||||||
diffResult.dispatchUpdatesTo(this)
|
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.
|
* Returns the total number of items in the data set held by the adapter.
|
||||||
*
|
*
|
||||||
* @return The total number of items in this adapter.
|
* @return The total number of items in this adapter.
|
||||||
*/
|
*/
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return images.size
|
val sharedPreferences: SharedPreferences =
|
||||||
|
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
|
||||||
|
val 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 {
|
fun getImageIdAt(position: Int): Long {
|
||||||
return images.get(position).id
|
return images.get(position).id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CleanUp function.
|
||||||
|
*/
|
||||||
|
fun cleanUp() {
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image view holder.
|
* Image view holder.
|
||||||
*/
|
*/
|
||||||
|
|
@ -158,6 +409,7 @@ class ImageAdapter(
|
||||||
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
|
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
|
||||||
private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count)
|
private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count)
|
||||||
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
|
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)
|
private val selectedGroup: Group = itemView.findViewById(R.id.selected_group)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -182,9 +434,24 @@ class ImageAdapter(
|
||||||
uploadedGroup.visibility = View.VISIBLE
|
uploadedGroup.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item is not for upload view
|
||||||
|
*/
|
||||||
|
fun itemNotForUpload() {
|
||||||
|
notForUploadGroup.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
fun isItemUploaded():Boolean {
|
fun isItemUploaded():Boolean {
|
||||||
return uploadedGroup.visibility == View.VISIBLE
|
return uploadedGroup.visibility == View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item is not for upload
|
||||||
|
*/
|
||||||
|
fun isItemNotForUpload():Boolean {
|
||||||
|
return notForUploadGroup.visibility == View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item Not Uploaded view.
|
* Item Not Uploaded view.
|
||||||
*/
|
*/
|
||||||
|
|
@ -192,6 +459,12 @@ class ImageAdapter(
|
||||||
uploadedGroup.visibility = View.GONE
|
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.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import fr.free.nrw.commons.R
|
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.FolderClickListener
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
import fr.free.nrw.commons.filepicker.Constants
|
||||||
import fr.free.nrw.commons.media.ZoomableActivity
|
import fr.free.nrw.commons.media.ZoomableActivity
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
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 java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -53,6 +62,29 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
|
@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.
|
* 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.
|
* Show Custom Selector Welcome Dialog.
|
||||||
*/
|
*/
|
||||||
|
|
@ -102,6 +149,114 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
|
||||||
.commit()
|
.commit()
|
||||||
fetchData()
|
fetchData()
|
||||||
setUpToolbar()
|
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() {
|
private fun setUpToolbar() {
|
||||||
val back : ImageButton = findViewById(R.id.back)
|
val back : ImageButton = findViewById(R.id.back)
|
||||||
back.setOnClickListener { onBackPressed() }
|
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
|
viewModel.selectedImages.value = selectedImages
|
||||||
|
|
||||||
val done : ImageButton = findViewById(R.id.done)
|
if (selectedNotForUploadImages > 0) {
|
||||||
done.visibility = if (selectedImages.isEmpty()) View.INVISIBLE else View.VISIBLE
|
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
|
* onLongPress
|
||||||
* @param imageUri : uri of image
|
* @param imageUri : uri of image
|
||||||
*/
|
*/
|
||||||
override fun onLongPress(imageUri: Uri) {
|
override fun onLongPress(
|
||||||
val intent = Intent(this, ZoomableActivity::class.java).setData(imageUri);
|
position: Int,
|
||||||
startActivity(intent)
|
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.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.text.format.DateFormat
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
|
import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
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.io.File
|
||||||
|
import java.util.*
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,7 +33,9 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
MediaStore.Images.Media.DISPLAY_NAME,
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
MediaStore.Images.Media.DATA,
|
MediaStore.Images.Media.DATA,
|
||||||
MediaStore.Images.Media.BUCKET_ID,
|
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.
|
* Load Device Images under coroutine.
|
||||||
|
|
@ -57,6 +64,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
|
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
|
||||||
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
|
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
|
||||||
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
|
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
|
||||||
|
val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
|
||||||
|
|
||||||
val images = arrayListOf<Image>()
|
val images = arrayListOf<Image>()
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
|
|
@ -70,6 +78,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
val path = cursor.getString(dataColumn)
|
val path = cursor.getString(dataColumn)
|
||||||
val bucketId = cursor.getLong(bucketIdColumn)
|
val bucketId = cursor.getLong(bucketIdColumn)
|
||||||
val bucketName = cursor.getString(bucketNameColumn)
|
val bucketName = cursor.getString(bucketNameColumn)
|
||||||
|
val date = cursor.getLong(dateColumn)
|
||||||
|
|
||||||
val file =
|
val file =
|
||||||
if (path == null || path.isEmpty()) {
|
if (path == null || path.isEmpty()) {
|
||||||
|
|
@ -84,7 +93,22 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||||
if (file != null && file.exists()) {
|
if (file != null && file.exists()) {
|
||||||
if (name != null && path != null && bucketName != null) {
|
if (name != null && path != null && bucketName != null) {
|
||||||
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
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)
|
images.add(image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,47 @@
|
||||||
package fr.free.nrw.commons.customselector.ui.selector
|
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.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.Switch
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||||
|
import fr.free.nrw.commons.customselector.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.ImageSelectListener
|
||||||
|
import fr.free.nrw.commons.customselector.listeners.RefreshUIListener
|
||||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
import fr.free.nrw.commons.customselector.model.Result
|
import fr.free.nrw.commons.customselector.model.Result
|
||||||
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
|
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.upload.FileProcessor
|
||||||
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import kotlinx.android.synthetic.main.fragment_custom_selector.*
|
import kotlinx.android.synthetic.main.fragment_custom_selector.*
|
||||||
import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
|
import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
|
||||||
import java.io.File
|
import java.util.*
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.net.URI
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Selector Image Fragment.
|
* Custom Selector Image Fragment.
|
||||||
*/
|
*/
|
||||||
class ImageFragment: CommonsDaggerSupportFragment() {
|
class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current bucketId.
|
* Current bucketId.
|
||||||
|
|
@ -52,8 +63,14 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
||||||
*/
|
*/
|
||||||
private var selectorRV: RecyclerView? = null
|
private var selectorRV: RecyclerView? = null
|
||||||
private var loader: ProgressBar? = null
|
private var loader: ProgressBar? = null
|
||||||
|
private var switch: Switch? = null
|
||||||
lateinit var filteredImages: ArrayList<Image>;
|
lateinit var filteredImages: ArrayList<Image>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all images
|
||||||
|
*/
|
||||||
|
var allImages: ArrayList<Image> = ArrayList()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View model Factory.
|
* View model Factory.
|
||||||
*/
|
*/
|
||||||
|
|
@ -76,9 +93,48 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
||||||
*/
|
*/
|
||||||
private lateinit var gridLayoutManager: GridLayoutManager
|
private lateinit var gridLayoutManager: GridLayoutManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For showing progress
|
||||||
|
*/
|
||||||
|
private var progressLayout: ConstraintLayout? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotForUploadStatus Dao class for database operations
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var notForUploadStatusDao: NotForUploadStatusDao
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UploadedStatus Dao class for database operations
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var uploadedStatusDao: UploadedStatusDao
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileUtilsWrapper class to get imageSHA1 from uri
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var fileUtilsWrapper: FileUtilsWrapper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileProcessor to pre-process the file.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var fileProcessor: FileProcessor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaClient for SHA1 query.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var mediaClient: MediaClient
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch state
|
||||||
|
*/
|
||||||
|
var showAlreadyActionedImages: Boolean = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BucketId args name
|
* BucketId args name
|
||||||
*/
|
*/
|
||||||
|
|
@ -129,12 +185,54 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
||||||
handleResult(it)
|
handleResult(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
switch = root.switchWidget
|
||||||
|
switch?.visibility = View.VISIBLE
|
||||||
|
switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) }
|
||||||
selectorRV = root.selector_rv
|
selectorRV = root.selector_rv
|
||||||
loader = root.loader
|
loader = root.loader
|
||||||
|
progressLayout = root.progressLayout
|
||||||
|
|
||||||
|
val sharedPreferences: SharedPreferences =
|
||||||
|
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
|
||||||
|
showAlreadyActionedImages = sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
|
||||||
|
switch?.isChecked = showAlreadyActionedImages
|
||||||
|
|
||||||
return root
|
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.
|
* Handle view model result.
|
||||||
*/
|
*/
|
||||||
|
|
@ -143,7 +241,8 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
||||||
val images = result.images
|
val images = result.images
|
||||||
if(images.isNotEmpty()) {
|
if(images.isNotEmpty()) {
|
||||||
filteredImages = ImageHelper.filterImages(images, bucketId)
|
filteredImages = ImageHelper.filterImages(images, bucketId)
|
||||||
imageAdapter.init(filteredImages)
|
allImages = ArrayList(filteredImages)
|
||||||
|
imageAdapter.init(filteredImages, allImages, TreeMap())
|
||||||
selectorRV?.let {
|
selectorRV?.let {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
lastItemId?.let { pos ->
|
lastItemId?.let { pos ->
|
||||||
|
|
@ -191,7 +290,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
||||||
* Save the Image Fragment state.
|
* Save the Image Fragment state.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
imageLoader?.cleanUP()
|
imageAdapter.cleanUp()
|
||||||
|
|
||||||
val position = (selectorRV?.layoutManager as GridLayoutManager)
|
val position = (selectorRV?.layoutManager as GridLayoutManager)
|
||||||
.findFirstVisibleItemPosition()
|
.findFirstVisibleItemPosition()
|
||||||
|
|
@ -211,4 +310,21 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
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
|
package fr.free.nrw.commons.customselector.ui.selector
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder
|
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder
|
||||||
import fr.free.nrw.commons.filepicker.PickedFiles
|
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
import fr.free.nrw.commons.upload.FileProcessor
|
import fr.free.nrw.commons.upload.FileProcessor
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
|
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||||
|
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.UnknownHostException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Loader class, loads images, depending on API results.
|
* Image Loader class, loads images, depending on API results.
|
||||||
|
|
@ -46,6 +44,11 @@ class ImageLoader @Inject constructor(
|
||||||
*/
|
*/
|
||||||
var uploadedStatusDao: UploadedStatusDao,
|
var uploadedStatusDao: UploadedStatusDao,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotForUploadDao for database operations
|
||||||
|
*/
|
||||||
|
var notForUploadStatusDao: NotForUploadStatusDao,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context for coroutine.
|
* Context for coroutine.
|
||||||
*/
|
*/
|
||||||
|
|
@ -61,34 +64,48 @@ class ImageLoader @Inject constructor(
|
||||||
private var mapImageSHA1: HashMap<Uri, String> = HashMap()
|
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()
|
private val scope : CoroutineScope = MainScope()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query image and setUp the view.
|
* 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.
|
* Recycler view uses same view holder, so we can identify the latest query image from holder.
|
||||||
*/
|
*/
|
||||||
mapHolderImage[holder] = image
|
mapHolderImage[holder] = image
|
||||||
holder.itemNotUploaded()
|
holder.itemNotUploaded()
|
||||||
|
holder.itemForUpload()
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|
||||||
var result: Result = Result.NOTFOUND
|
var result: Result = Result.NOTFOUND
|
||||||
|
|
||||||
if (mapHolderImage[holder] != image) {
|
if (mapHolderImage[holder] != image) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageSHA1 = getImageSHA1(image.uri)
|
val imageSHA1: String = when (mapImageSHA1[image.uri] != null) {
|
||||||
if(imageSHA1.isEmpty())
|
true -> mapImageSHA1[image.uri]!!
|
||||||
|
else -> CustomSelectorUtils.getImageSHA1(
|
||||||
|
image.uri,
|
||||||
|
ioDispatcher,
|
||||||
|
fileUtilsWrapper,
|
||||||
|
context.contentResolver
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mapImageSHA1[image.uri] = imageSHA1
|
||||||
|
|
||||||
|
if (imageSHA1.isEmpty()) {
|
||||||
return@launch
|
return@launch
|
||||||
|
}
|
||||||
val uploadedStatus = getFromUploaded(imageSHA1)
|
val uploadedStatus = getFromUploaded(imageSHA1)
|
||||||
|
|
||||||
val sha1 = uploadedStatus?.let {
|
val sha1 = uploadedStatus?.let {
|
||||||
|
|
@ -96,7 +113,7 @@ class ImageLoader @Inject constructor(
|
||||||
uploadedStatus.modifiedImageSHA1
|
uploadedStatus.modifiedImageSHA1
|
||||||
} ?: run {
|
} ?: run {
|
||||||
if (mapHolderImage[holder] == image) {
|
if (mapHolderImage[holder] == image) {
|
||||||
getSHA1(image)
|
getSHA1(image, defaultDispatcher)
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -106,53 +123,137 @@ class ImageLoader @Inject constructor(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val existsInNotForUploadTable = notForUploadStatusDao.find(imageSHA1)
|
||||||
|
|
||||||
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
|
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
|
||||||
|
when {
|
||||||
|
mapResult[imageSHA1] == null -> {
|
||||||
// Query original image.
|
// Query original image.
|
||||||
result = querySHA1(imageSHA1)
|
result = checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||||
|
imageSHA1,
|
||||||
|
ioDispatcher,
|
||||||
|
mediaClient
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
is Result.TRUE -> {
|
||||||
|
mapResult[imageSHA1] = Result.TRUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result = mapResult[imageSHA1]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
if (result is Result.TRUE) {
|
if (result is Result.TRUE) {
|
||||||
// Original image found.
|
// Original image found.
|
||||||
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
|
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
|
||||||
} else {
|
} else {
|
||||||
|
when {
|
||||||
|
mapResult[sha1] == null -> {
|
||||||
// Original image not found, query modified image.
|
// Original image not found, query modified image.
|
||||||
result = querySHA1(sha1)
|
result = checkWhetherFileExistsOnCommonsUsingSHA1(
|
||||||
|
sha1,
|
||||||
|
ioDispatcher,
|
||||||
|
mediaClient
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
is Result.TRUE -> {
|
||||||
|
mapResult[sha1] = Result.TRUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result = mapResult[sha1]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
if (result != Result.ERROR) {
|
if (result != Result.ERROR) {
|
||||||
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
|
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (mapHolderImage[holder] == image) {
|
||||||
if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded()
|
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.
|
* Finds out the next actionable image position
|
||||||
*
|
|
||||||
* @return Query result.
|
|
||||||
*/
|
*/
|
||||||
|
suspend fun nextActionableImage(
|
||||||
|
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
|
||||||
|
defaultDispatcher: CoroutineDispatcher,
|
||||||
|
nextImagePosition: Int
|
||||||
|
): Int {
|
||||||
|
var next: Int
|
||||||
|
|
||||||
suspend fun querySHA1(SHA1: String): Result {
|
// Traversing from given position to the end
|
||||||
return withContext(ioDispatcher) {
|
for (i in nextImagePosition until allImages.size){
|
||||||
mapResult[SHA1]?.let {
|
val it = allImages[i]
|
||||||
return@withContext it
|
val imageSHA1: String = when (mapImageSHA1[it.uri] != null) {
|
||||||
|
true -> mapImageSHA1[it.uri]!!
|
||||||
|
else -> CustomSelectorUtils.getImageSHA1(
|
||||||
|
it.uri,
|
||||||
|
ioDispatcher,
|
||||||
|
fileUtilsWrapper,
|
||||||
|
context.contentResolver
|
||||||
|
)
|
||||||
}
|
}
|
||||||
var result: Result = Result.FALSE
|
next = notForUploadStatusDao.find(imageSHA1)
|
||||||
try {
|
|
||||||
if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) {
|
// After checking the image in the not for upload table, if the image is present then
|
||||||
mapResult[SHA1] = Result.TRUE
|
// skips the image and moves to next image for checking
|
||||||
result = Result.TRUE
|
if(next > 0){
|
||||||
|
continue
|
||||||
|
|
||||||
|
// Otherwise checks in already uploaded 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
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is UnknownHostException) {
|
// If present in the db then skips iteration for the image and moves to the next
|
||||||
// Handle no network connection.
|
// for checking
|
||||||
Timber.e(e, "Network Connection Error")
|
} else {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
result = Result.ERROR
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -160,11 +261,17 @@ class ImageLoader @Inject constructor(
|
||||||
*
|
*
|
||||||
* @return sha1 of the image
|
* @return sha1 of the image
|
||||||
*/
|
*/
|
||||||
suspend fun getSHA1(image: Image): String {
|
suspend fun getSHA1(image: Image, defaultDispatcher: CoroutineDispatcher): String {
|
||||||
mapModifiedImageSHA1[image]?.let{
|
mapModifiedImageSHA1[image]?.let{
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
val sha1 = generateModifiedSHA1(image);
|
val sha1 = CustomSelectorUtils
|
||||||
|
.generateModifiedSHA1(image,
|
||||||
|
defaultDispatcher,
|
||||||
|
context,
|
||||||
|
fileProcessor,
|
||||||
|
fileUtilsWrapper
|
||||||
|
)
|
||||||
mapModifiedImageSHA1[image] = sha1;
|
mapModifiedImageSHA1[image] = sha1;
|
||||||
return sha1;
|
return sha1;
|
||||||
}
|
}
|
||||||
|
|
@ -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.
|
* Get result data from database.
|
||||||
*/
|
*/
|
||||||
|
|
@ -226,35 +314,6 @@ class ImageLoader @Inject constructor(
|
||||||
return Result.INVALID
|
return Result.INVALID
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Modified SHA1 using present Exif settings.
|
|
||||||
*
|
|
||||||
* @return modified sha1
|
|
||||||
*/
|
|
||||||
private suspend fun generateModifiedSHA1(image: Image) : String {
|
|
||||||
return withContext(defaultDispatcher) {
|
|
||||||
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
|
|
||||||
val exifInterface: ExifInterface? = try {
|
|
||||||
ExifInterface(uploadableFile.file!!)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
|
|
||||||
val sha1 =
|
|
||||||
fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath))
|
|
||||||
uploadableFile.file.delete()
|
|
||||||
sha1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CleanUp function.
|
|
||||||
*/
|
|
||||||
fun cleanUP() {
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sealed Result class.
|
* Sealed Result class.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
import fr.free.nrw.commons.customselector.database.*
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
|
||||||
import fr.free.nrw.commons.upload.depicts.Depicts
|
import fr.free.nrw.commons.upload.depicts.Depicts
|
||||||
import fr.free.nrw.commons.upload.depicts.DepictsDao
|
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
|
* 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)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun contributionDao(): ContributionDao
|
abstract fun contributionDao(): ContributionDao
|
||||||
abstract fun DepictsDao(): DepictsDao;
|
abstract fun DepictsDao(): DepictsDao;
|
||||||
abstract fun UploadedStatusDao(): UploadedStatusDao;
|
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.description.DescriptionEditActivity;
|
||||||
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
||||||
import fr.free.nrw.commons.explore.SearchActivity;
|
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.notification.NotificationActivity;
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||||
import fr.free.nrw.commons.review.ReviewActivity;
|
import fr.free.nrw.commons.review.ReviewActivity;
|
||||||
|
|
@ -75,4 +76,7 @@ public abstract class ActivityBuilderModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract DescriptionEditActivity bindDescriptionEditActivity();
|
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.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
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.database.UploadedStatusDao;
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
|
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
|
|
@ -290,6 +291,14 @@ public class CommonsApplicationModule {
|
||||||
return appDatabase.UploadedStatusDao();
|
return appDatabase.UploadedStatusDao();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the reference of NotForUploadStatus class.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) {
|
||||||
|
return appDatabase.NotForUploadStatusDao();
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public ContentResolver providesContentResolver(Context context){
|
public ContentResolver providesContentResolver(Context context){
|
||||||
return context.getContentResolver();
|
return context.getContentResolver();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ public interface Constants {
|
||||||
int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12);
|
int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12);
|
||||||
int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13);
|
int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13);
|
||||||
int CAPTURE_VIDEO = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 14);
|
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:id="@+id/fragment_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toTopOf="@id/bottom_layout"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/>
|
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/>
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/custom_selector_bottom_layout"
|
||||||
|
android:id="@+id/bottom_sheet"
|
||||||
|
/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -23,4 +23,21 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
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>
|
</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:textAlignment="center"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dimen_20"
|
||||||
app:layout_constraintStart_toEndOf="@id/back"
|
app:layout_constraintStart_toEndOf="@id/back"
|
||||||
app:layout_constraintEnd_toStartOf="@id/done"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
|
|
@ -37,20 +38,5 @@
|
||||||
android:text="@string/custom_selector_title"
|
android:text="@string/custom_selector_title"
|
||||||
style="@style/TextAppearance.AppCompat.Widget.ActionBar.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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</merge>
|
</merge>
|
||||||
|
|
@ -3,13 +3,37 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:background="?attr/mainBackground">
|
||||||
|
|
||||||
<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:id="@+id/selector_rv"
|
||||||
android:background="?attr/mainBackground"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:background="?attr/mainBackground"
|
||||||
|
android:layout_height="@dimen/dimen_0"
|
||||||
|
app:fastScrollPopupBgColor="@color/primaryColor"
|
||||||
|
app: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
|
<TextView
|
||||||
|
|
@ -38,4 +62,41 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/progressLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="@dimen/dimen_5"
|
||||||
|
android:background="@color/drawerHeader_background_light"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/selector_rv">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/hiding_already_actioned_pictures"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="@dimen/dimen_5"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
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"
|
android:visibility="gone"
|
||||||
app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/>
|
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.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
<dimen name="progressbar_stroke">3dp</dimen>
|
<dimen name="progressbar_stroke">3dp</dimen>
|
||||||
<dimen name="notification_width">110dp</dimen>
|
<dimen name="notification_width">110dp</dimen>
|
||||||
<dimen name="notification_height">160dp</dimen>
|
<dimen name="notification_height">160dp</dimen>
|
||||||
|
<dimen name="bubble_size">36dp</dimen>
|
||||||
|
|
||||||
<!-- Text sizes -->
|
<!-- Text sizes -->
|
||||||
<dimen name="heading_text_size">24sp</dimen>
|
<dimen name="heading_text_size">24sp</dimen>
|
||||||
|
|
@ -59,11 +60,13 @@
|
||||||
<!-- Commonly used dimensions -->
|
<!-- Commonly used dimensions -->
|
||||||
<dimen name="dimen_0">0dp</dimen>
|
<dimen name="dimen_0">0dp</dimen>
|
||||||
<dimen name="dimen_2">2dp</dimen>
|
<dimen name="dimen_2">2dp</dimen>
|
||||||
|
<dimen name="dimen_5">5dp</dimen>
|
||||||
<dimen name="dimen_6">6dp</dimen>
|
<dimen name="dimen_6">6dp</dimen>
|
||||||
<dimen name="dimen_10">10dp</dimen>
|
<dimen name="dimen_10">10dp</dimen>
|
||||||
<dimen name="dimen_20">20dp</dimen>
|
<dimen name="dimen_20">20dp</dimen>
|
||||||
<dimen name="dimen_40">40dp</dimen>
|
<dimen name="dimen_40">40dp</dimen>
|
||||||
<dimen name="dimen_42">42dp</dimen>
|
<dimen name="dimen_42">42dp</dimen>
|
||||||
|
<dimen name="dimen_50">50dp</dimen>
|
||||||
<dimen name="dimen_250">250dp</dimen>
|
<dimen name="dimen_250">250dp</dimen>
|
||||||
<dimen name="dimen_150">150dp</dimen>
|
<dimen name="dimen_150">150dp</dimen>
|
||||||
<dimen name="dimen_72">72dp</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="error_feedback">Error while sending feedback</string>
|
||||||
<string name="enter_description">What is your feedback?</string>
|
<string name="enter_description">What is your feedback?</string>
|
||||||
<string name="your_feedback">Your feedback</string>
|
<string name="your_feedback">Your feedback</string>
|
||||||
|
<string name="mark_as_not_for_upload">Mark as not for upload</string>
|
||||||
|
<string name="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="menu_view_report">Report</string>
|
||||||
<string name="report_violation">Report violation</string>
|
<string name="report_violation">Report violation</string>
|
||||||
<string name="report_user">Report this user</string>
|
<string name="report_user">Report this user</string>
|
||||||
<string name="report_content">Report this content</string>
|
<string name="report_content">Report this content</string>
|
||||||
<string name="request_user_block">Request to block this user</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>
|
</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.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -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.model.Image
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
|
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
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.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.jupiter.api.Assertions
|
import org.junit.jupiter.api.Assertions
|
||||||
import org.junit.runner.RunWith
|
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.powermock.reflect.Whitebox
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Selector image adapter test.
|
* Custom Selector image adapter test.
|
||||||
*/
|
*/
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(sdk = [21], application = TestCommonsApplication::class)
|
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
class ImageAdapterTest {
|
class ImageAdapterTest {
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var imageLoader: ImageLoader
|
private lateinit var imageLoader: ImageLoader
|
||||||
|
|
@ -38,6 +51,8 @@ class ImageAdapterTest {
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var mockContentResolver: ContentResolver
|
private lateinit var mockContentResolver: ContentResolver
|
||||||
|
@Mock
|
||||||
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
private lateinit var activity: CustomSelectorActivity
|
private lateinit var activity: CustomSelectorActivity
|
||||||
private lateinit var imageAdapter: ImageAdapter
|
private lateinit var imageAdapter: ImageAdapter
|
||||||
|
|
@ -46,6 +61,7 @@ class ImageAdapterTest {
|
||||||
private lateinit var selectedImageField: Field
|
private lateinit var selectedImageField: Field
|
||||||
private var uri: Uri = Mockito.mock(Uri::class.java)
|
private var uri: Uri = Mockito.mock(Uri::class.java)
|
||||||
private lateinit var image: Image
|
private lateinit var image: Image
|
||||||
|
private val testDispatcher = TestCoroutineDispatcher()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,6 +71,7 @@ class ImageAdapterTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
Dispatchers.setMain(testDispatcher)
|
||||||
activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get()
|
activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get()
|
||||||
imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader)
|
imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader)
|
||||||
image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
|
image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
|
||||||
|
|
@ -68,6 +85,12 @@ class ImageAdapterTest {
|
||||||
selectedImageField.isAccessible = true
|
selectedImageField.isAccessible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
testDispatcher.cleanupTestCoroutines()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test on create view holder.
|
* Test on create view holder.
|
||||||
*/
|
*/
|
||||||
|
|
@ -88,20 +111,43 @@ class ImageAdapterTest {
|
||||||
|
|
||||||
// Parameters.
|
// Parameters.
|
||||||
images.add(image)
|
images.add(image)
|
||||||
imageAdapter.init(images)
|
imageAdapter.init(images, images, TreeMap())
|
||||||
|
|
||||||
|
whenever(context.getSharedPreferences("custom_selector", 0))
|
||||||
|
.thenReturn(sharedPreferences)
|
||||||
// Test conditions.
|
// Test conditions.
|
||||||
imageAdapter.onBindViewHolder(holder, 0)
|
imageAdapter.onBindViewHolder(holder, 0)
|
||||||
selectedImageField.set(imageAdapter, images)
|
selectedImageField.set(imageAdapter, images)
|
||||||
imageAdapter.onBindViewHolder(holder, 0)
|
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 init.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun init() {
|
fun init() {
|
||||||
imageAdapter.init(images)
|
imageAdapter.init(images, images, TreeMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,17 +161,37 @@ class ImageAdapterTest {
|
||||||
|
|
||||||
// Parameters
|
// Parameters
|
||||||
images.addAll(listOf(image, image))
|
images.addAll(listOf(image, image))
|
||||||
imageAdapter.init(images)
|
imageAdapter.init(images, images, TreeMap())
|
||||||
|
|
||||||
// Test conditions
|
// Test conditions
|
||||||
holder.itemUploaded()
|
holder.itemUploaded()
|
||||||
func.invoke(imageAdapter, holder, 0)
|
func.invoke(imageAdapter, holder, 0)
|
||||||
holder.itemNotUploaded()
|
holder.itemNotUploaded()
|
||||||
|
holder.itemNotForUpload()
|
||||||
|
func.invoke(imageAdapter, holder, 0)
|
||||||
|
holder.itemNotForUpload()
|
||||||
func.invoke(imageAdapter, holder, 0)
|
func.invoke(imageAdapter, holder, 0)
|
||||||
selectedImageField.set(imageAdapter, images)
|
selectedImageField.set(imageAdapter, images)
|
||||||
func.invoke(imageAdapter, holder, 1)
|
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.
|
* Test get item count.
|
||||||
*/
|
*/
|
||||||
|
|
@ -134,12 +200,47 @@ class ImageAdapterTest {
|
||||||
Assertions.assertEquals(0, imageAdapter.itemCount)
|
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 getImageId
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun getImageIdAt() {
|
fun getImageIdAt() {
|
||||||
imageAdapter.init(listOf(image))
|
imageAdapter.init(listOf(image), listOf(image), TreeMap())
|
||||||
Assertions.assertEquals(1, imageAdapter.getImageIdAt(0))
|
Assertions.assertEquals(1, imageAdapter.getImageIdAt(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
package fr.free.nrw.commons.customselector.ui.selector
|
package fr.free.nrw.commons.customselector.ui.selector
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
|
||||||
import android.os.Looper.getMainLooper
|
|
||||||
import fr.free.nrw.commons.TestAppAdapter
|
import fr.free.nrw.commons.TestAppAdapter
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
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.model.Image
|
||||||
|
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mockito
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
import org.powermock.reflect.Whitebox
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.Shadows
|
|
||||||
import org.robolectric.Shadows.shadowOf
|
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import org.wikipedia.AppAdapter
|
import org.wikipedia.AppAdapter
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
@ -31,6 +29,14 @@ class CustomSelectorActivityTest {
|
||||||
|
|
||||||
private lateinit var activity: CustomSelectorActivity
|
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.
|
* Set up the tests.
|
||||||
*/
|
*/
|
||||||
|
|
@ -44,6 +50,12 @@ class CustomSelectorActivityTest {
|
||||||
val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java)
|
val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java)
|
||||||
onCreate.isAccessible = true
|
onCreate.isAccessible = true
|
||||||
onCreate.invoke(activity, null)
|
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);
|
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 selectedImagesChanged function.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testOnSelectedImagesChanged() {
|
fun testOnSelectedImagesChanged() {
|
||||||
activity.onSelectedImagesChanged(ArrayList())
|
activity.onSelectedImagesChanged(ArrayList(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -91,10 +151,40 @@ class CustomSelectorActivityTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testOnDone() {
|
fun testOnDone() {
|
||||||
activity.onDone()
|
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()
|
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.
|
* Test onBackPressed Function.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ class ImageFileLoaderTest {
|
||||||
MediaStore.Images.Media.DISPLAY_NAME,
|
MediaStore.Images.Media.DISPLAY_NAME,
|
||||||
MediaStore.Images.Media.DATA,
|
MediaStore.Images.Media.DATA,
|
||||||
MediaStore.Images.Media.BUCKET_ID,
|
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)
|
Whitebox.setInternalState(imageFileLoader, "coroutineContext", coroutineContext)
|
||||||
|
|
@ -103,6 +104,7 @@ class ImageFileLoaderTest {
|
||||||
anyOrNull(),
|
anyOrNull(),
|
||||||
anyOrNull(),
|
anyOrNull(),
|
||||||
anyOrNull(),
|
anyOrNull(),
|
||||||
|
anyOrNull(),
|
||||||
anyOrNull()
|
anyOrNull()
|
||||||
)
|
)
|
||||||
} doReturn imageCursor;
|
} doReturn imageCursor;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import java.lang.reflect.Field
|
||||||
class ImageFragmentTest {
|
class ImageFragmentTest {
|
||||||
|
|
||||||
private lateinit var fragment: ImageFragment
|
private lateinit var fragment: ImageFragment
|
||||||
|
private lateinit var activity: CustomSelectorActivity
|
||||||
private lateinit var view: View
|
private lateinit var view: View
|
||||||
private lateinit var selectorRV : RecyclerView
|
private lateinit var selectorRV : RecyclerView
|
||||||
private lateinit var loader : ProgressBar
|
private lateinit var loader : ProgressBar
|
||||||
|
|
@ -76,7 +77,7 @@ class ImageFragmentTest {
|
||||||
AppAdapter.set(TestAppAdapter())
|
AppAdapter.set(TestAppAdapter())
|
||||||
SoLoader.setInTestMode()
|
SoLoader.setInTestMode()
|
||||||
Fresco.initialize(context)
|
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)
|
fragment = ImageFragment.newInstance(1,0)
|
||||||
val fragmentManager: FragmentManager = activity.supportFragmentManager
|
val fragmentManager: FragmentManager = activity.supportFragmentManager
|
||||||
|
|
@ -92,6 +93,7 @@ class ImageFragmentTest {
|
||||||
Whitebox.setInternalState(fragment, "imageAdapter", adapter)
|
Whitebox.setInternalState(fragment, "imageAdapter", adapter)
|
||||||
Whitebox.setInternalState(fragment, "selectorRV", selectorRV )
|
Whitebox.setInternalState(fragment, "selectorRV", selectorRV )
|
||||||
Whitebox.setInternalState(fragment, "loader", loader)
|
Whitebox.setInternalState(fragment, "loader", loader)
|
||||||
|
Whitebox.setInternalState(fragment, "filteredImages", arrayListOf(image,image))
|
||||||
|
|
||||||
viewModelField = fragment.javaClass.getDeclaredField("viewModel")
|
viewModelField = fragment.javaClass.getDeclaredField("viewModel")
|
||||||
viewModelField.isAccessible = true
|
viewModelField.isAccessible = true
|
||||||
|
|
@ -139,6 +141,21 @@ class ImageFragmentTest {
|
||||||
assertEquals(3, func.invoke(fragment))
|
assertEquals(3, func.invoke(fragment))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test onAttach function.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testOnAttach() {
|
||||||
|
fragment.onAttach(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test refresh function.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testRefresh() {
|
||||||
|
fragment.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test onResume.
|
* Test onResume.
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ package fr.free.nrw.commons.customselector.ui.selector
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.nhaarman.mockitokotlin2.*
|
import com.nhaarman.mockitokotlin2.*
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
import fr.free.nrw.commons.TestCommonsApplication
|
||||||
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
import fr.free.nrw.commons.customselector.database.UploadedStatus
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
|
@ -61,6 +63,9 @@ class ImageLoaderTest {
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var uploadedStatusDao: UploadedStatusDao
|
private lateinit var uploadedStatusDao: UploadedStatusDao
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var notForUploadStatusDao: NotForUploadStatusDao
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var holder: ImageAdapter.ImageViewHolder
|
private lateinit var holder: ImageAdapter.ImageViewHolder
|
||||||
|
|
||||||
|
|
@ -97,7 +102,8 @@ class ImageLoaderTest {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
|
||||||
imageLoader =
|
imageLoader =
|
||||||
ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context)
|
ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao,
|
||||||
|
notForUploadStatusDao, context)
|
||||||
uploadedStatus= UploadedStatus(
|
uploadedStatus= UploadedStatus(
|
||||||
"testSha1",
|
"testSha1",
|
||||||
"testSha1",
|
"testSha1",
|
||||||
|
|
@ -112,8 +118,6 @@ class ImageLoaderTest {
|
||||||
Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1);
|
Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1);
|
||||||
Whitebox.setInternalState(imageLoader, "mapResult", mapResult);
|
Whitebox.setInternalState(imageLoader, "mapResult", mapResult);
|
||||||
Whitebox.setInternalState(imageLoader, "context", context)
|
Whitebox.setInternalState(imageLoader, "context", context)
|
||||||
Whitebox.setInternalState(imageLoader, "ioDispatcher", testDispacher)
|
|
||||||
Whitebox.setInternalState(imageLoader, "defaultDispatcher", testDispacher)
|
|
||||||
|
|
||||||
whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream)
|
whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream)
|
||||||
whenever(context.contentResolver).thenReturn(contentResolver)
|
whenever(context.contentResolver).thenReturn(contentResolver)
|
||||||
|
|
@ -136,14 +140,17 @@ class ImageLoaderTest {
|
||||||
@Test
|
@Test
|
||||||
fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest {
|
fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest {
|
||||||
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
|
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
|
||||||
|
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
||||||
mapModifiedImageSHA1[image] = "testSha1"
|
mapModifiedImageSHA1[image] = "testSha1"
|
||||||
mapImageSHA1[uri] = "testSha1"
|
mapImageSHA1[uri] = "testSha1"
|
||||||
|
whenever(context.getSharedPreferences("custom_selector", 0))
|
||||||
|
.thenReturn(Mockito.mock(SharedPreferences::class.java))
|
||||||
|
|
||||||
mapResult["testSha1"] = ImageLoader.Result.TRUE
|
mapResult["testSha1"] = ImageLoader.Result.TRUE
|
||||||
imageLoader.queryAndSetView(holder, image)
|
imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
|
||||||
|
|
||||||
mapResult["testSha1"] = ImageLoader.Result.FALSE
|
mapResult["testSha1"] = ImageLoader.Result.FALSE
|
||||||
imageLoader.queryAndSetView(holder, image)
|
imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -152,20 +159,38 @@ class ImageLoaderTest {
|
||||||
@Test
|
@Test
|
||||||
fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest {
|
fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest {
|
||||||
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus)
|
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus)
|
||||||
imageLoader.queryAndSetView(holder, image)
|
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
||||||
|
whenever(context.getSharedPreferences("custom_selector", 0))
|
||||||
|
.thenReturn(Mockito.mock(SharedPreferences::class.java))
|
||||||
|
imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test querySha1
|
* Test nextActionableImage
|
||||||
*/
|
*/
|
||||||
@Test
|
@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(notForUploadStatusDao.find(any())).thenReturn(1)
|
||||||
whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single)
|
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
|
||||||
whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1")
|
|
||||||
|
|
||||||
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.getFileInputStream("ABC")).thenReturn(inputStream)
|
||||||
whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1")
|
whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1")
|
||||||
|
|
||||||
Assert.assertEquals("testSha1", imageLoader.getSHA1(image));
|
Assert.assertEquals("testSha1", imageLoader.getSHA1(image, testDispacher));
|
||||||
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
|
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
|
||||||
uploadableFile
|
uploadableFile
|
||||||
)
|
)
|
||||||
|
|
||||||
mapModifiedImageSHA1[image] = "testSha2"
|
mapModifiedImageSHA1[image] = "testSha2"
|
||||||
Assert.assertEquals("testSha2", imageLoader.getSHA1(image));
|
Assert.assertEquals("testSha2", imageLoader.getSHA1(image, testDispacher));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -213,8 +238,4 @@ class ImageLoaderTest {
|
||||||
imageLoader.getResultFromUploadedStatus(uploadedStatus))
|
imageLoader.getResultFromUploadedStatus(uploadedStatus))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testCleanUP() {
|
|
||||||
imageLoader.cleanUP()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -5,18 +5,25 @@ import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco
|
import com.facebook.drawee.backends.pipeline.Fresco
|
||||||
import com.facebook.soloader.SoLoader
|
import com.facebook.soloader.SoLoader
|
||||||
|
import fr.free.nrw.commons.TestAppAdapter
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
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.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
import org.powermock.reflect.Whitebox
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.RuntimeEnvironment
|
import org.robolectric.RuntimeEnvironment
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import org.robolectric.annotation.LooperMode
|
import org.robolectric.annotation.LooperMode
|
||||||
|
import org.wikipedia.AppAdapter
|
||||||
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(sdk = [21], application = TestCommonsApplication::class)
|
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||||
|
|
@ -25,18 +32,32 @@ class ZoomableActivityUnitTests {
|
||||||
|
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
private lateinit var activity: ZoomableActivity
|
private lateinit var activity: ZoomableActivity
|
||||||
|
private lateinit var viewModelField: Field
|
||||||
|
private lateinit var image: Image
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var uri: Uri
|
private lateinit var uri: Uri
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var images: ArrayList<Image>
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
AppAdapter.set(TestAppAdapter())
|
||||||
context = RuntimeEnvironment.application.applicationContext
|
context = RuntimeEnvironment.application.applicationContext
|
||||||
SoLoader.setInTestMode()
|
SoLoader.setInTestMode()
|
||||||
Fresco.initialize(context)
|
Fresco.initialize(context)
|
||||||
val intent = Intent().setData(uri)
|
val intent = Intent().setData(uri)
|
||||||
activity = Robolectric.buildActivity(ZoomableActivity::class.java, intent).create().get()
|
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
|
@Test
|
||||||
|
|
@ -45,4 +66,87 @@ class ZoomableActivityUnitTests {
|
||||||
Assert.assertNotNull(activity)
|
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