[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:
Ayan Sarkar 2022-09-16 14:44:16 +05:30 committed by GitHub
parent b5ffe7120c
commit 33679eb6b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2560 additions and 284 deletions

View file

@ -122,6 +122,7 @@ dependencies {
implementation "androidx.exifinterface:exifinterface:1.3.2"
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
implementation "androidx.multidex:multidex:2.0.1"
compile 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
//swipe_layout
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'

View file

@ -56,7 +56,10 @@
android:finishOnTaskLaunch="true" />
<activity
android:name=".media.ZoomableActivity" />
android:name=".media.ZoomableActivity"
android:label="Zoomable Activity"
android:configChanges="screenSize|keyboard|orientation"
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
<activity android:name=".auth.LoginActivity">
<intent-filter>

View file

@ -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
}

View file

@ -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
)

View file

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

View file

@ -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"
}

View file

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

View file

@ -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())
}
}

View file

@ -11,12 +11,17 @@ interface ImageSelectListener {
/**
* onSelectedImagesChanged
* @param selectedImages : new selected images.
* @param selectedNotForUploadImages : number of selected not for upload images
*/
fun onSelectedImagesChanged(selectedImages: ArrayList<Image>)
fun onSelectedImagesChanged(selectedImages: ArrayList<Image>, selectedNotForUploadImages: Int)
/**
* onLongPress
* @param imageUri : uri of image
*/
fun onLongPress(imageUri: Uri)
fun onLongPress(
position: Int,
images: ArrayList<Image>,
selectedImages: ArrayList<Image>
)
}

View file

@ -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)
}

View file

@ -0,0 +1,11 @@
package fr.free.nrw.commons.customselector.listeners
/**
* Refresh UI Listener
*/
interface RefreshUIListener {
/**
* Refreshes the data in adapter
*/
fun refresh()
}

View file

@ -41,7 +41,13 @@ data class Image(
/**
sha1 : sha1 of original image.
*/
var sha1: String = ""
var sha1: String = "",
/**
* date: Creation date of the image to show it inside the bubble during bubble scroll.
*/
var date: String = ""
) : Parcelable {
/**
@ -54,6 +60,7 @@ data class Image(
parcel.readString()!!,
parcel.readLong(),
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!
)
@ -68,6 +75,7 @@ data class Image(
parcel.writeLong(bucketId)
parcel.writeString(bucketName)
parcel.writeString(sha1)
parcel.writeString(date)
}
/**

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.customselector.ui.adapter
import android.content.Context
import android.content.SharedPreferences
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
@ -10,11 +11,17 @@ import androidx.constraintlayout.widget.Group
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
import kotlinx.coroutines.*
import java.util.*
import kotlin.collections.ArrayList
/**
* Custom selector ImageAdapter.
@ -36,7 +43,7 @@ class ImageAdapter(
private var imageLoader: ImageLoader
):
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context) {
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context), FastScrollRecyclerView.SectionedAdapter {
/**
* ImageSelectedOrUpdated payload class.
@ -48,16 +55,58 @@ class ImageAdapter(
*/
class ImageUnselected
/**
* Determines whether addition of all actionable images is done or not
*/
private var reachedEndOfFolder: Boolean = false
/**
* Currently selected images.
*/
private var selectedImages = arrayListOf<Image>()
/**
* Number of selected images that are marked as not for upload
*/
private var numberOfSelectedImagesMarkedAsNotForUpload = 0
/**
* List of all images in adapter.
*/
private var images: ArrayList<Image> = ArrayList()
/**
* Stores all images
*/
private var allImages: List<Image> = ArrayList()
/**
* Map to store actionable images
*/
private var actionableImagesMap: TreeMap<Int, Image> = TreeMap()
/**
* Stores already added positions of actionable images
*/
private var alreadyAddedPositions: ArrayList<Int> = ArrayList()
/**
* Next starting index to initiate query to find next actionable image
*/
private var nextImagePosition = 0
/**
* Helps to maintain the increasing sequence of the position. eg- 0, 1, 2, 3
*/
private var imagePositionAsPerIncreasingOrder = 0
/**
* Coroutine Dispatchers and Scope.
*/
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
private val scope : CoroutineScope = MainScope()
/**
* Create View holder.
*/
@ -70,7 +119,7 @@ class ImageAdapter(
* Bind View holder, load image, selected view, click listeners.
*/
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val image=images[position]
var image=images[position]
holder.image.setImageDrawable (null)
if (context.contentResolver.getType(image.uri) == null) {
// Image does not exist anymore, update adapter.
@ -81,56 +130,214 @@ class ImageAdapter(
notifyItemRangeChanged(updatedPosition, images.size)
}
} else {
val selectedIndex = ImageHelper.getIndex(selectedImages, image)
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val showAlreadyActionedImages =
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
// Getting selected index when switch is on
val selectedIndex: Int = if (showAlreadyActionedImages) {
ImageHelper.getIndex(selectedImages, image)
// Getting selected index when switch is off
} else if (actionableImagesMap.size > position) {
ImageHelper
.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
// For any other case return -1
} else {
-1
}
val isSelected = selectedIndex != -1
if (isSelected) {
holder.itemSelected(selectedIndex + 1)
holder.itemSelected(selectedImages.size)
} else {
holder.itemUnselected();
holder.itemUnselected()
}
Glide.with(holder.image).load(image.uri).thumbnail(0.3f).into(holder.image)
imageLoader.queryAndSetView(holder, image)
imageLoader.queryAndSetView(
holder, image, ioDispatcher, defaultDispatcher
)
scope.launch {
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val showAlreadyActionedImages =
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
if (!showAlreadyActionedImages) {
// If the position is not already visited, that means the position is new then
// finds the next actionable image position from all images
if (!alreadyAddedPositions.contains(position)) {
processThumbnailForActionedImage(holder, position)
// If the position is already visited, that means the image is already present
// inside map, so it will fetch the image from the map and load in the holder
} else {
val actionableImages: List<Image> = ArrayList(actionableImagesMap.values)
image = actionableImages[position]
Glide.with(holder.image).load(image.uri)
.thumbnail(0.3f).into(holder.image)
}
// If switch is turned off, it just fetches the image from all images without any
// further operations
} else {
Glide.with(holder.image).load(image.uri)
.thumbnail(0.3f).into(holder.image)
}
}
holder.itemView.setOnClickListener {
selectOrRemoveImage(holder, position)
onThumbnailClicked(position, holder)
}
// launch media preview on long click.
holder.itemView.setOnLongClickListener {
imageSelectListener.onLongPress(image.uri)
imageSelectListener.onLongPress(images.indexOf(image), images, selectedImages)
true
}
}
}
/**
* Process thumbnail for actioned image
*/
suspend fun processThumbnailForActionedImage(
holder: ImageViewHolder,
position: Int
) {
val next = imageLoader.nextActionableImage(
allImages, ioDispatcher, defaultDispatcher,
nextImagePosition
)
// If next actionable image is found, saves it, as the the search for
// finding next actionable image will start from this position
if (next > -1) {
nextImagePosition = next + 1
// If map doesn't contains the next actionable image, that means it's a
// new actionable image, it will put it to the map as actionable images
// and it will load the new image in the view holder
if (!actionableImagesMap.containsKey(next)) {
actionableImagesMap[next] = allImages[next]
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
imagePositionAsPerIncreasingOrder++
Glide.with(holder.image).load(allImages[next].uri)
.thumbnail(0.3f).into(holder.image)
notifyItemInserted(position)
notifyItemRangeChanged(position, itemCount + 1)
}
// If next actionable image is not found, that means searching is
// complete till end, and it will stop searching.
} else {
reachedEndOfFolder = true
notifyItemRemoved(position)
}
}
/**
* Handles click on thumbnail
*/
private fun onThumbnailClicked(
position: Int,
holder: ImageViewHolder
) {
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val switchState =
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
// While switch is turned off, lets user click on image only if the position is
// added inside map
if (!switchState) {
if (actionableImagesMap.size > position) {
selectOrRemoveImage(holder, position)
}
} else {
selectOrRemoveImage(holder, position)
}
}
/**
* Handle click event on an image, update counter on images.
*/
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){
val clickedIndex = ImageHelper.getIndex(selectedImages, images[position])
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val showAlreadyActionedImages =
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
// Getting clicked index from all images index when show_already_actioned_images
// switch is on
val clickedIndex: Int = if(showAlreadyActionedImages) {
ImageHelper.getIndex(selectedImages, images[position])
// Getting clicked index from actionable images when show_already_actioned_images
// switch is off
} else {
ImageHelper.getIndex(selectedImages, ArrayList(actionableImagesMap.values)[position])
}
if (clickedIndex != -1) {
selectedImages.removeAt(clickedIndex)
if (holder.isItemNotForUpload()) {
numberOfSelectedImagesMarkedAsNotForUpload--
}
notifyItemChanged(position, ImageUnselected())
val indexes = ImageHelper.getIndexList(selectedImages, images)
// Getting index from all images index when switch is on
val indexes = if (showAlreadyActionedImages) {
ImageHelper.getIndexList(selectedImages, images)
// Getting index from actionable images when switch is off
} else {
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
}
for (index in indexes) {
notifyItemChanged(index, ImageSelectedOrUpdated())
}
} else {
if(holder.isItemUploaded()){
if (holder.isItemUploaded()) {
Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
} else {
selectedImages.add(images[position])
notifyItemChanged(position, ImageSelectedOrUpdated())
if (holder.isItemNotForUpload()) {
numberOfSelectedImagesMarkedAsNotForUpload++
}
// Getting index from all images index when switch is on
val indexes: ArrayList<Int> = if (showAlreadyActionedImages) {
selectedImages.add(images[position])
ImageHelper.getIndexList(selectedImages, images)
// Getting index from actionable images when switch is off
} else {
selectedImages.add(ArrayList(actionableImagesMap.values)[position])
ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
}
for (index in indexes) {
notifyItemChanged(index, ImageSelectedOrUpdated())
}
}
}
imageSelectListener.onSelectedImagesChanged(selectedImages)
imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
}
/**
* Initialize the data set.
*/
fun init(newImages: List<Image>) {
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>) {
allImages = fixedImages
val oldImageList:ArrayList<Image> = images
val newImageList:ArrayList<Image> = ArrayList(newImages)
actionableImagesMap = emptyMap
alreadyAddedPositions = ArrayList()
nextImagePosition = 0
reachedEndOfFolder = false
selectedImages = ArrayList()
imagePositionAsPerIncreasingOrder = 0
val diffResult = DiffUtil.calculateDiff(
ImagesDiffCallback(oldImageList, newImageList)
)
@ -138,19 +345,63 @@ class ImageAdapter(
diffResult.dispatchUpdatesTo(this)
}
/**
* Set new selected images
*/
fun setSelectedImages(newSelectedImages: ArrayList<Image>){
selectedImages = ArrayList(newSelectedImages)
imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
}
/**
* Refresh the data in the adapter
*/
fun refresh(newImages: List<Image>, fixedImages: List<Image>) {
numberOfSelectedImagesMarkedAsNotForUpload = 0
selectedImages.clear()
images.clear()
selectedImages = arrayListOf()
init(newImages, fixedImages, TreeMap())
notifyDataSetChanged()
}
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
override fun getItemCount(): Int {
return images.size
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val showAlreadyActionedImages =
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
// While switch is on initializes the holder with all images size
return if(showAlreadyActionedImages) {
allImages.size
// While switch is off and searching for next actionable has ended, initializes the holder
// with size of all actionable images
} else if (actionableImagesMap.size == allImages.size || reachedEndOfFolder) {
actionableImagesMap.size
// While switch is off, initializes the holder with and extra view holder so that finding
// and addition of the next actionable image in the adapter can be continued
} else {
actionableImagesMap.size + 1
}
}
fun getImageIdAt(position: Int): Long {
return images.get(position).id
}
/**
* CleanUp function.
*/
fun cleanUp() {
scope.cancel()
}
/**
* Image view holder.
*/
@ -158,6 +409,7 @@ class ImageAdapter(
val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count)
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
private val notForUploadGroup: Group = itemView.findViewById(R.id.not_for_upload_group)
private val selectedGroup: Group = itemView.findViewById(R.id.selected_group)
/**
@ -182,9 +434,24 @@ class ImageAdapter(
uploadedGroup.visibility = View.VISIBLE
}
/**
* Item is not for upload view
*/
fun itemNotForUpload() {
notForUploadGroup.visibility = View.VISIBLE
}
fun isItemUploaded():Boolean {
return uploadedGroup.visibility == View.VISIBLE
}
/**
* Item is not for upload
*/
fun isItemNotForUpload():Boolean {
return notForUploadGroup.visibility == View.VISIBLE
}
/**
* Item Not Uploaded view.
*/
@ -192,6 +459,12 @@ class ImageAdapter(
uploadedGroup.visibility = View.GONE
}
/**
* Item can be uploaded view
*/
fun itemForUpload() {
notForUploadGroup.visibility = View.GONE
}
}
/**
@ -233,4 +506,11 @@ class ImageAdapter(
}
/**
* Returns the text for showing inside the bubble during bubble scroll.
*/
override fun getSectionName(position: Int): String {
return images[position].date
}
}

View file

@ -4,20 +4,29 @@ import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.Window
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.ViewModelProvider
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants.SHOULD_REFRESH
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.filepicker.Constants
import fr.free.nrw.commons.media.ZoomableActivity
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.utils.CustomSelectorUtils
import kotlinx.android.synthetic.main.custom_selector_bottom_layout.*
import kotlinx.coroutines.*
import java.io.File
import javax.inject.Inject
@ -53,6 +62,29 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
*/
@Inject lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
/**
* NotForUploadStatus Dao class for database operations
*/
@Inject
lateinit var notForUploadStatusDao: NotForUploadStatusDao
/**
* FileUtilsWrapper class to get imageSHA1 from uri
*/
@Inject
lateinit var fileUtilsWrapper: FileUtilsWrapper
/**
* Coroutine Dispatchers and Scope.
*/
private val scope : CoroutineScope = MainScope()
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
/**
* Image Fragment instance
*/
var imageFragment: ImageFragment? = null
/**
* onCreate Activity, sets theme, initialises the view model, setup view.
*/
@ -82,6 +114,21 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
}
}
/**
* When data will be send from full screen mode, it will be passed to fragment
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
resultCode == Activity.RESULT_OK) {
val selectedImages: ArrayList<Image> =
data!!
.getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
val shouldRefresh = data.getBooleanExtra(SHOULD_REFRESH, false)
imageFragment!!.passSelectedImages(selectedImages, shouldRefresh)
}
}
/**
* Show Custom Selector Welcome Dialog.
*/
@ -102,6 +149,114 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
.commit()
fetchData()
setUpToolbar()
setUpBottomLayout()
}
/**
* Set up bottom layout
*/
private fun setUpBottomLayout() {
val done : Button = findViewById(R.id.upload)
done.setOnClickListener { onDone() }
val notForUpload : Button = findViewById(R.id.not_for_upload)
notForUpload.setOnClickListener{ onClickNotForUpload() }
}
/**
* Gets selected images and proceed for database operations
*/
private fun onClickNotForUpload() {
val selectedImages = viewModel.selectedImages.value
if(selectedImages.isNullOrEmpty()) {
markAsNotForUpload(arrayListOf())
return
}
var i = 0
while (i < selectedImages.size) {
val path = selectedImages[i].path
val file = File(path)
if (!file.exists()) {
selectedImages.removeAt(i)
i--
}
i++
}
markAsNotForUpload(selectedImages)
}
/**
* Insert selected images in the database
*/
private fun markAsNotForUpload(images: ArrayList<Image>) {
insertIntoNotForUpload(images)
}
/**
* Initializing ImageFragment
*/
fun setOnDataListener(imageFragment: ImageFragment?) {
this.imageFragment = imageFragment
}
/**
* Insert images into not for upload
* Remove images from not for upload
* Refresh the UI
*/
private fun insertIntoNotForUpload(images: ArrayList<Image>) {
scope.launch {
var allImagesAlreadyNotForUpload = true
images.forEach{
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
it.uri,
ioDispatcher,
fileUtilsWrapper,
contentResolver
)
val exists = notForUploadStatusDao.find(imageSHA1)
// If image exists in not for upload table make allImagesAlreadyNotForUpload false
if (exists < 1) {
allImagesAlreadyNotForUpload = false
}
}
// if all images is not already marked as not for upload, insert all images in
// not for upload table
if (!allImagesAlreadyNotForUpload) {
images.forEach {
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
it.uri,
ioDispatcher,
fileUtilsWrapper,
contentResolver
)
notForUploadStatusDao.insert(
NotForUploadStatus(
imageSHA1
)
)
}
// if all images is already marked as not for upload, delete all images from
// not for upload table
} else {
images.forEach {
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
it.uri,
ioDispatcher,
fileUtilsWrapper,
contentResolver
)
notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
}
}
imageFragment!!.refresh()
val bottomLayout : ConstraintLayout = findViewById(R.id.bottom_layout)
bottomLayout.visibility = View.GONE
}
}
/**
@ -127,9 +282,6 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
private fun setUpToolbar() {
val back : ImageButton = findViewById(R.id.back)
back.setOnClickListener { onBackPressed() }
val done : ImageButton = findViewById(R.id.done)
done.setOnClickListener { onDone() }
}
/**
@ -149,22 +301,46 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
}
/**
* override Selected Images Change, update view model selected images.
* override Selected Images Change, update view model selected images and change UI.
*/
override fun onSelectedImagesChanged(selectedImages: ArrayList<Image>) {
override fun onSelectedImagesChanged(selectedImages: ArrayList<Image>,
selectedNotForUploadImages: Int) {
viewModel.selectedImages.value = selectedImages
val done : ImageButton = findViewById(R.id.done)
done.visibility = if (selectedImages.isEmpty()) View.INVISIBLE else View.VISIBLE
if (selectedNotForUploadImages > 0) {
upload.isEnabled = false
upload.alpha = 0.5f
} else {
upload.isEnabled = true
upload.alpha = 1f
}
not_for_upload.text = when (selectedImages.size == selectedNotForUploadImages) {
true -> getString(R.string.unmark_as_not_for_upload)
else -> getString(R.string.mark_as_not_for_upload)
}
val bottomLayout : ConstraintLayout = findViewById(R.id.bottom_layout)
bottomLayout.visibility = if (selectedImages.isEmpty()) View.GONE else View.VISIBLE
}
/**
* onLongPress
* @param imageUri : uri of image
*/
override fun onLongPress(imageUri: Uri) {
val intent = Intent(this, ZoomableActivity::class.java).setData(imageUri);
startActivity(intent)
override fun onLongPress(
position: Int,
images: ArrayList<Image>,
selectedImages: ArrayList<Image>
) {
val intent = Intent(this, ZoomableActivity::class.java)
intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position);
intent.putParcelableArrayListExtra(
CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
selectedImages
)
intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE)
}
/**

View file

@ -3,10 +3,15 @@ package fr.free.nrw.commons.customselector.ui.selector
import android.content.ContentUris
import android.content.Context
import android.provider.MediaStore
import android.text.format.DateFormat
import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
import fr.free.nrw.commons.customselector.model.Image
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
import kotlin.coroutines.CoroutineContext
/**
@ -28,7 +33,9 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
/**
* Load Device Images under coroutine.
@ -57,6 +64,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)
val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
val images = arrayListOf<Image>()
if (cursor.moveToFirst()) {
@ -70,6 +78,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
val path = cursor.getString(dataColumn)
val bucketId = cursor.getLong(bucketIdColumn)
val bucketName = cursor.getString(bucketNameColumn)
val date = cursor.getLong(dateColumn)
val file =
if (path == null || path.isEmpty()) {
@ -84,7 +93,22 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
if (file != null && file.exists()) {
if (name != null && path != null && bucketName != null) {
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
val image = Image(id, name, uri, path, bucketId, bucketName)
val calendar = Calendar.getInstance()
calendar.timeInMillis = date * 1000L
val date: Date = calendar.time
val dateFormat = DateFormat.getMediumDateFormat(context)
val formattedDate = dateFormat.format(date)
val image = Image(
id,
name,
uri,
path,
bucketId,
bucketName,
date = (formattedDate)
)
images.add(image)
}
}

View file

@ -1,36 +1,47 @@
package fr.free.nrw.commons.customselector.ui.selector
import android.net.Uri
import android.app.Activity
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.Switch
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.listeners.PassDataListener
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.helper.ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.listeners.RefreshUIListener
import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
import kotlinx.android.synthetic.main.fragment_custom_selector.*
import kotlinx.android.synthetic.main.fragment_custom_selector.view.*
import java.io.File
import java.io.FileInputStream
import java.net.URI
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
/**
* Custom Selector Image Fragment.
*/
class ImageFragment: CommonsDaggerSupportFragment() {
class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
/**
* Current bucketId.
@ -52,8 +63,14 @@ class ImageFragment: CommonsDaggerSupportFragment() {
*/
private var selectorRV: RecyclerView? = null
private var loader: ProgressBar? = null
private var switch: Switch? = null
lateinit var filteredImages: ArrayList<Image>;
/**
* Stores all images
*/
var allImages: ArrayList<Image> = ArrayList()
/**
* View model Factory.
*/
@ -76,9 +93,48 @@ class ImageFragment: CommonsDaggerSupportFragment() {
*/
private lateinit var gridLayoutManager: GridLayoutManager
/**
* For showing progress
*/
private var progressLayout: ConstraintLayout? = null
/**
* NotForUploadStatus Dao class for database operations
*/
@Inject
lateinit var notForUploadStatusDao: NotForUploadStatusDao
/**
* UploadedStatus Dao class for database operations
*/
@Inject
lateinit var uploadedStatusDao: UploadedStatusDao
/**
* FileUtilsWrapper class to get imageSHA1 from uri
*/
@Inject
lateinit var fileUtilsWrapper: FileUtilsWrapper
/**
* FileProcessor to pre-process the file.
*/
@Inject
lateinit var fileProcessor: FileProcessor
/**
* MediaClient for SHA1 query.
*/
@Inject
lateinit var mediaClient: MediaClient
companion object {
/**
* Switch state
*/
var showAlreadyActionedImages: Boolean = true
/**
* BucketId args name
*/
@ -129,12 +185,54 @@ class ImageFragment: CommonsDaggerSupportFragment() {
handleResult(it)
})
switch = root.switchWidget
switch?.visibility = View.VISIBLE
switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) }
selectorRV = root.selector_rv
loader = root.loader
progressLayout = root.progressLayout
val sharedPreferences: SharedPreferences =
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
showAlreadyActionedImages = sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
switch?.isChecked = showAlreadyActionedImages
return root
}
private fun onChangeSwitchState(checked: Boolean) {
if (checked) {
showAlreadyActionedImages = true
val sharedPreferences: SharedPreferences =
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
editor.apply()
} else {
showAlreadyActionedImages = false
val sharedPreferences: SharedPreferences =
requireContext().getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, false)
editor.apply()
}
imageAdapter.init(allImages, allImages, TreeMap())
imageAdapter.notifyDataSetChanged()
}
/**
* Attaching data listener
*/
override fun onAttach(activity: Activity) {
super.onAttach(activity)
try {
(getActivity() as CustomSelectorActivity).setOnDataListener(this)
} catch (ex: Exception) {
ex.printStackTrace()
}
}
/**
* Handle view model result.
*/
@ -143,7 +241,8 @@ class ImageFragment: CommonsDaggerSupportFragment() {
val images = result.images
if(images.isNotEmpty()) {
filteredImages = ImageHelper.filterImages(images, bucketId)
imageAdapter.init(filteredImages)
allImages = ArrayList(filteredImages)
imageAdapter.init(filteredImages, allImages, TreeMap())
selectorRV?.let {
it.visibility = View.VISIBLE
lastItemId?.let { pos ->
@ -191,7 +290,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
* Save the Image Fragment state.
*/
override fun onDestroy() {
imageLoader?.cleanUP()
imageAdapter.cleanUp()
val position = (selectorRV?.layoutManager as GridLayoutManager)
.findFirstVisibleItemPosition()
@ -211,4 +310,21 @@ class ImageFragment: CommonsDaggerSupportFragment() {
}
super.onDestroy()
}
override fun refresh() {
imageAdapter.refresh(filteredImages, allImages)
}
/**
* Passes selected images and other information from Activity to Fragment and connects it with
* the adapter
*/
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean){
imageAdapter.setSelectedImages(selectedImages)
if (!showAlreadyActionedImages && shouldRefresh) {
imageAdapter.init(filteredImages, allImages, TreeMap())
imageAdapter.setSelectedImages(selectedImages)
}
}
}

View file

@ -1,25 +1,23 @@
package fr.free.nrw.commons.customselector.ui.selector
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter.ImageViewHolder
import fr.free.nrw.commons.filepicker.PickedFiles
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.utils.CustomSelectorUtils
import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1
import kotlinx.coroutines.*
import timber.log.Timber
import java.io.FileNotFoundException
import java.io.IOException
import java.net.UnknownHostException
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.HashMap
/**
* Image Loader class, loads images, depending on API results.
@ -46,6 +44,11 @@ class ImageLoader @Inject constructor(
*/
var uploadedStatusDao: UploadedStatusDao,
/**
* NotForUploadDao for database operations
*/
var notForUploadStatusDao: NotForUploadStatusDao,
/**
* Context for coroutine.
*/
@ -61,34 +64,48 @@ class ImageLoader @Inject constructor(
private var mapImageSHA1: HashMap<Uri, String> = HashMap()
/**
* Coroutine Dispatchers and Scope.
* Coroutine Scope.
*/
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
private val scope : CoroutineScope = MainScope()
/**
* Query image and setUp the view.
*/
fun queryAndSetView(holder: ImageViewHolder, image: Image) {
fun queryAndSetView(
holder: ImageViewHolder,
image: Image,
ioDispatcher: CoroutineDispatcher,
defaultDispatcher: CoroutineDispatcher
) {
/**
* Recycler view uses same view holder, so we can identify the latest query image from holder.
*/
mapHolderImage[holder] = image
holder.itemNotUploaded()
holder.itemForUpload()
scope.launch {
var result: Result = Result.NOTFOUND
if (mapHolderImage[holder] != image) {
return@launch
}
val imageSHA1 = getImageSHA1(image.uri)
if(imageSHA1.isEmpty())
val imageSHA1: String = when (mapImageSHA1[image.uri] != null) {
true -> mapImageSHA1[image.uri]!!
else -> CustomSelectorUtils.getImageSHA1(
image.uri,
ioDispatcher,
fileUtilsWrapper,
context.contentResolver
)
}
mapImageSHA1[image.uri] = imageSHA1
if (imageSHA1.isEmpty()) {
return@launch
}
val uploadedStatus = getFromUploaded(imageSHA1)
val sha1 = uploadedStatus?.let {
@ -96,7 +113,7 @@ class ImageLoader @Inject constructor(
uploadedStatus.modifiedImageSHA1
} ?: run {
if (mapHolderImage[holder] == image) {
getSHA1(image)
getSHA1(image, defaultDispatcher)
} else {
""
}
@ -106,53 +123,137 @@ class ImageLoader @Inject constructor(
return@launch
}
val existsInNotForUploadTable = notForUploadStatusDao.find(imageSHA1)
if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) {
// Query original image.
result = querySHA1(imageSHA1)
when {
mapResult[imageSHA1] == null -> {
// Query original image.
result = checkWhetherFileExistsOnCommonsUsingSHA1(
imageSHA1,
ioDispatcher,
mediaClient
)
when (result) {
is Result.TRUE -> {
mapResult[imageSHA1] = Result.TRUE
}
}
}
else -> {
result = mapResult[imageSHA1]!!
}
}
if (result is Result.TRUE) {
// Original image found.
insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false)
} else {
// Original image not found, query modified image.
result = querySHA1(sha1)
when {
mapResult[sha1] == null -> {
// Original image not found, query modified image.
result = checkWhetherFileExistsOnCommonsUsingSHA1(
sha1,
ioDispatcher,
mediaClient
)
when (result) {
is Result.TRUE -> {
mapResult[sha1] = Result.TRUE
}
}
}
else -> {
result = mapResult[sha1]!!
}
}
if (result != Result.ERROR) {
insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE)
}
}
}
if(mapHolderImage[holder] == image) {
if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded()
val sharedPreferences: SharedPreferences =
context
.getSharedPreferences(ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val showAlreadyActionedImages =
sharedPreferences.getBoolean(
ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
true
)
if (mapHolderImage[holder] == image) {
if ((result is Result.TRUE) && showAlreadyActionedImages) {
holder.itemUploaded()
} else holder.itemNotUploaded()
if ((existsInNotForUploadTable > 0) && showAlreadyActionedImages) {
holder.itemNotForUpload()
} else holder.itemForUpload()
}
}
}
/**
* Query SHA1, return result if previously queried, otherwise start a new query.
*
* @return Query result.
* Finds out the next actionable image position
*/
suspend fun nextActionableImage(
allImages: List<Image>, ioDispatcher: CoroutineDispatcher,
defaultDispatcher: CoroutineDispatcher,
nextImagePosition: Int
): Int {
var next: Int
suspend fun querySHA1(SHA1: String): Result {
return withContext(ioDispatcher) {
mapResult[SHA1]?.let {
return@withContext it
// Traversing from given position to the end
for (i in nextImagePosition until allImages.size){
val it = allImages[i]
val imageSHA1: String = when (mapImageSHA1[it.uri] != null) {
true -> mapImageSHA1[it.uri]!!
else -> CustomSelectorUtils.getImageSHA1(
it.uri,
ioDispatcher,
fileUtilsWrapper,
context.contentResolver
)
}
var result: Result = Result.FALSE
try {
if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) {
mapResult[SHA1] = Result.TRUE
result = Result.TRUE
next = notForUploadStatusDao.find(imageSHA1)
// After checking the image in the not for upload table, if the image is present then
// skips the image and moves to next image for checking
if(next > 0){
continue
// Otherwise checks in already uploaded table
} else {
next = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
// If the image is not present in the already uploaded table, checks for its
// modified SHA1 in already uploaded table
if (next <= 0) {
val modifiedImageSha1 = getSHA1(it, defaultDispatcher)
next = uploadedStatusDao.findByModifiedImageSHA1(
modifiedImageSha1,
true
)
// If the modified image SHA1 is not present in the already uploaded table,
// returns the position as next actionable image position
if (next <= 0) {
return i
// If present in the db then skips iteration for the image and moves to the next
// for checking
} else {
continue
}
// If present in the db then skips iteration for the image and moves to the next
// for checking
} else {
continue
}
} catch (e: Exception) {
if (e is UnknownHostException) {
// Handle no network connection.
Timber.e(e, "Network Connection Error")
}
result = Result.ERROR
e.printStackTrace()
}
result
}
return -1
}
/**
@ -160,11 +261,17 @@ class ImageLoader @Inject constructor(
*
* @return sha1 of the image
*/
suspend fun getSHA1(image: Image): String {
suspend fun getSHA1(image: Image, defaultDispatcher: CoroutineDispatcher): String {
mapModifiedImageSHA1[image]?.let{
return it
}
val sha1 = generateModifiedSHA1(image);
val sha1 = CustomSelectorUtils
.generateModifiedSHA1(image,
defaultDispatcher,
context,
fileProcessor,
fileUtilsWrapper
)
mapModifiedImageSHA1[image] = sha1;
return sha1;
}
@ -190,25 +297,6 @@ class ImageLoader @Inject constructor(
)
}
/**
* Get image sha1 from uri, used to retrieve the original image sha1.
*/
suspend fun getImageSHA1(uri: Uri): String {
return withContext(ioDispatcher) {
mapImageSHA1[uri]?.let{
return@withContext it
}
try {
val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri))
mapImageSHA1[uri] = result
result
} catch (e: FileNotFoundException){
e.printStackTrace()
""
}
}
}
/**
* Get result data from database.
*/
@ -226,35 +314,6 @@ class ImageLoader @Inject constructor(
return Result.INVALID
}
/**
* Generate Modified SHA1 using present Exif settings.
*
* @return modified sha1
*/
private suspend fun generateModifiedSHA1(image: Image) : String {
return withContext(defaultDispatcher) {
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
val exifInterface: ExifInterface? = try {
ExifInterface(uploadableFile.file!!)
} catch (e: IOException) {
Timber.e(e)
null
}
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
val sha1 =
fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath))
uploadableFile.file.delete()
sha1
}
}
/**
* CleanUp function.
*/
fun cleanUP() {
scope.cancel()
}
/**
* Sealed Result class.
*/

View file

@ -5,8 +5,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.database.*
import fr.free.nrw.commons.upload.depicts.Depicts
import fr.free.nrw.commons.upload.depicts.DepictsDao
@ -14,10 +13,11 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
* The database for accessing the respective DAOs
*
*/
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class], version = 13, exportSchema = false)
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class], version = 14, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao
abstract fun DepictsDao(): DepictsDao;
abstract fun UploadedStatusDao(): UploadedStatusDao;
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
}

View file

@ -13,6 +13,7 @@ import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
import fr.free.nrw.commons.description.DescriptionEditActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.media.ZoomableActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
@ -75,4 +76,7 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract DescriptionEditActivity bindDescriptionEditActivity();
@ContributesAndroidInjector
abstract ZoomableActivity bindZoomableActivity();
}

View file

@ -17,6 +17,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
import fr.free.nrw.commons.customselector.database.UploadedStatusDao;
import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader;
import fr.free.nrw.commons.data.DBOpenHelper;
@ -290,6 +291,14 @@ public class CommonsApplicationModule {
return appDatabase.UploadedStatusDao();
}
/**
* Get the reference of NotForUploadStatus class.
*/
@Provides
public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) {
return appDatabase.NotForUploadStatusDao();
}
@Provides
public ContentResolver providesContentResolver(Context context){
return context.getContentResolver();

View file

@ -15,6 +15,8 @@ public interface Constants {
int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12);
int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13);
int CAPTURE_VIDEO = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 14);
int RECEIVE_DATA_FROM_FULL_SCREEN_MODE = 1 << 9;
}
/**

View file

@ -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);
}
}
}

View 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()
}
}

View file

@ -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
}
}
}
}

View 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>

View file

@ -13,7 +13,12 @@
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_layout"
app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/>
<include
layout="@layout/custom_selector_bottom_layout"
android:id="@+id/bottom_sheet"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -23,4 +23,21 @@
app:layout_constraintTop_toTopOf="parent"
/>
<TextView
android:id="@+id/selection_count"
android:layout_width="@dimen/dimen_20"
android:layout_height="@dimen/dimen_20"
app:layout_constraintDimensionRatio="H,1:1"
android:textSize="@dimen/subtitle_text"
android:textStyle="bold"
android:textColor="@color/black"
android:layout_margin="@dimen/dimen_6"
android:gravity="center|center_vertical"
style="@style/TextAppearance.AppCompat.Small"
android:text="12"
android:visibility="gone"
android:background="@drawable/circle_shape"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View 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>

View file

@ -28,8 +28,9 @@
android:textAlignment="center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dimen_20"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintEnd_toStartOf="@id/done"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:singleLine="true"
@ -37,20 +38,5 @@
android:text="@string/custom_selector_title"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
<ImageButton
android:id="@+id/done"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:clickable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:padding="@dimen/standard_gap"
app:srcCompat="?attr/custom_selector_done"
android:visibility="invisible"
android:contentDescription="@string/done" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View file

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

View 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>

View file

@ -82,6 +82,23 @@
android:visibility="gone"
app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/>
<ImageView
android:id="@+id/not_for_upload_overlay_icon"
android:layout_width="@dimen/dimen_50"
android:layout_height="@dimen/dimen_50"
android:paddingBottom="@dimen/dimen_20"
android:paddingEnd="@dimen/dimen_20"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:srcCompat="@drawable/not_for_upload"
/>
<androidx.constraintlayout.widget.Group
android:id="@+id/not_for_upload_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="selected_overlay,not_for_upload_overlay_icon"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View file

@ -41,6 +41,7 @@
<dimen name="progressbar_stroke">3dp</dimen>
<dimen name="notification_width">110dp</dimen>
<dimen name="notification_height">160dp</dimen>
<dimen name="bubble_size">36dp</dimen>
<!-- Text sizes -->
<dimen name="heading_text_size">24sp</dimen>
@ -59,11 +60,13 @@
<!-- Commonly used dimensions -->
<dimen name="dimen_0">0dp</dimen>
<dimen name="dimen_2">2dp</dimen>
<dimen name="dimen_5">5dp</dimen>
<dimen name="dimen_6">6dp</dimen>
<dimen name="dimen_10">10dp</dimen>
<dimen name="dimen_20">20dp</dimen>
<dimen name="dimen_40">40dp</dimen>
<dimen name="dimen_42">42dp</dimen>
<dimen name="dimen_50">50dp</dimen>
<dimen name="dimen_250">250dp</dimen>
<dimen name="dimen_150">150dp</dimen>
<dimen name="dimen_72">72dp</dimen>

View file

@ -734,9 +734,21 @@ Upload your first media by tapping on the add button.</string>
<string name="error_feedback">Error while sending feedback</string>
<string name="enter_description">What is your feedback?</string>
<string name="your_feedback">Your feedback</string>
<string name="mark_as_not_for_upload">Mark as not for upload</string>
<string name="unmark_as_not_for_upload">Unmark as not for upload</string>
<string name="show_already_actioned_pictures">Show already actioned pictures</string>
<string name="hiding_already_actioned_pictures">Hiding already actioned pictures</string>
<string name="no_more_images_found">No more images found</string>
<string name="this_image_is_already_uploaded">This image is already uploaded</string>
<string name="can_not_select_this_image_for_upload">Can not select this image for upload</string>
<string name="image_selected">Image selected</string>
<string name="image_marked_as_not_for_upload">Image marked as not for upload</string>
<string name="menu_view_report">Report</string>
<string name="report_violation">Report violation</string>
<string name="report_user">Report this user</string>
<string name="report_content">Report this content</string>
<string name="request_user_block">Request to block this user</string>
<string name="welcome_to_full_screen_mode_text">Welcome to Full-Screen Selection Mode</string>
<string name="full_screen_mode_zoom_info">Use two fingers to zoom in and out.</string>
<string name="full_screen_mode_features_info">Swipe fast and long to perform these actions: \n- Left/Right: Go to previous/next \n- Up: Select\n- Down: Mark as not for upload.</string>
</resources>

View file

@ -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)
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.customselector.ui.adapter
import android.content.ContentResolver
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
@ -13,22 +14,34 @@ import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.runner.RunWith
import org.mockito.*
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.lang.reflect.Field
import java.util.*
import kotlin.collections.ArrayList
/**
* Custom Selector image adapter test.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@ExperimentalCoroutinesApi
class ImageAdapterTest {
@Mock
private lateinit var imageLoader: ImageLoader
@ -38,6 +51,8 @@ class ImageAdapterTest {
private lateinit var context: Context
@Mock
private lateinit var mockContentResolver: ContentResolver
@Mock
private lateinit var sharedPreferences: SharedPreferences
private lateinit var activity: CustomSelectorActivity
private lateinit var imageAdapter: ImageAdapter
@ -46,6 +61,7 @@ class ImageAdapterTest {
private lateinit var selectedImageField: Field
private var uri: Uri = Mockito.mock(Uri::class.java)
private lateinit var image: Image
private val testDispatcher = TestCoroutineDispatcher()
/**
@ -55,6 +71,7 @@ class ImageAdapterTest {
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
Dispatchers.setMain(testDispatcher)
activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get()
imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader)
image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
@ -68,6 +85,12 @@ class ImageAdapterTest {
selectedImageField.isAccessible = true
}
@After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
/**
* Test on create view holder.
*/
@ -88,20 +111,43 @@ class ImageAdapterTest {
// Parameters.
images.add(image)
imageAdapter.init(images)
imageAdapter.init(images, images, TreeMap())
whenever(context.getSharedPreferences("custom_selector", 0))
.thenReturn(sharedPreferences)
// Test conditions.
imageAdapter.onBindViewHolder(holder, 0)
selectedImageField.set(imageAdapter, images)
imageAdapter.onBindViewHolder(holder, 0)
}
/**
* Test processThumbnailForActionedImage
*/
@Test
fun processThumbnailForActionedImage() = runBlocking {
Whitebox.setInternalState(imageAdapter, "allImages", listOf(image))
whenever(imageLoader.nextActionableImage(listOf(image), Dispatchers.IO, Dispatchers.Default,
0)).thenReturn(0)
imageAdapter.processThumbnailForActionedImage(holder, 0)
}
/**
* Test processThumbnailForActionedImage
*/
@Test
fun `processThumbnailForActionedImage when reached end of the folder`() = runBlocking {
whenever(imageLoader.nextActionableImage(ArrayList(), Dispatchers.IO, Dispatchers.Default,
0)).thenReturn(-1)
imageAdapter.processThumbnailForActionedImage(holder, 0)
}
/**
* Test init.
*/
@Test
fun init() {
imageAdapter.init(images)
imageAdapter.init(images, images, TreeMap())
}
/**
@ -115,17 +161,37 @@ class ImageAdapterTest {
// Parameters
images.addAll(listOf(image, image))
imageAdapter.init(images)
imageAdapter.init(images, images, TreeMap())
// Test conditions
holder.itemUploaded()
func.invoke(imageAdapter, holder, 0)
holder.itemNotUploaded()
holder.itemNotForUpload()
func.invoke(imageAdapter, holder, 0)
holder.itemNotForUpload()
func.invoke(imageAdapter, holder, 0)
selectedImageField.set(imageAdapter, images)
func.invoke(imageAdapter, holder, 1)
}
/**
* Test private function onThumbnailClicked.
*/
@Test
fun onThumbnailClicked() {
images.add(image)
Whitebox.setInternalState(imageAdapter, "images", images)
// Access function
val func = imageAdapter.javaClass.getDeclaredMethod(
"onThumbnailClicked",
Int::class.java,
ImageAdapter.ImageViewHolder::class.java
)
func.isAccessible = true
func.invoke(imageAdapter, 0, holder)
}
/**
* Test get item count.
*/
@ -134,12 +200,47 @@ class ImageAdapterTest {
Assertions.assertEquals(0, imageAdapter.itemCount)
}
/**
* Test setSelectedImages.
*/
@Test
fun setSelectedImages() {
images.add(image)
imageAdapter.setSelectedImages(images)
}
/**
* Test refresh.
*/
@Test
fun refresh() {
imageAdapter.refresh(listOf(image), listOf(image))
}
/**
* Test getSectionName.
*/
@Test
fun getSectionName() {
images.add(image)
Whitebox.setInternalState(imageAdapter, "images", images)
Assertions.assertEquals("", imageAdapter.getSectionName(0))
}
/**
* Test cleanUp.
*/
@Test
fun cleanUp() {
imageAdapter.cleanUp()
}
/**
* Test getImageId
*/
@Test
fun getImageIdAt() {
imageAdapter.init(listOf(image))
imageAdapter.init(listOf(image), listOf(image), TreeMap())
Assertions.assertEquals(1, imageAdapter.getImageIdAt(0))
}
}

View file

@ -1,23 +1,21 @@
package fr.free.nrw.commons.customselector.ui.selector
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Looper
import android.os.Looper.getMainLooper
import fr.free.nrw.commons.TestAppAdapter
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.wikipedia.AppAdapter
import java.lang.reflect.Method
@ -31,6 +29,14 @@ class CustomSelectorActivityTest {
private lateinit var activity: CustomSelectorActivity
private lateinit var imageFragment: ImageFragment
private lateinit var images : java.util.ArrayList<Image>
private var uri: Uri = Mockito.mock(Uri::class.java)
private lateinit var image: Image
/**
* Set up the tests.
*/
@ -44,6 +50,12 @@ class CustomSelectorActivityTest {
val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java)
onCreate.isAccessible = true
onCreate.invoke(activity, null)
imageFragment = ImageFragment.newInstance(1,0)
image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
images = ArrayList()
Whitebox.setInternalState(activity, "imageFragment", imageFragment)
Whitebox.setInternalState(imageFragment, "imageAdapter", Mockito.mock(ImageAdapter::class.java))
}
/**
@ -75,13 +87,61 @@ class CustomSelectorActivityTest {
activity.onFolderClick(1, "test", 0);
}
/**
* Test onActivityResult function.
*/
@Test
@Throws(Exception::class)
fun testOnActivityResult() {
val func = activity.javaClass.getDeclaredMethod(
"onActivityResult",
Int::class.java,
Int::class.java,
Intent::class.java
)
func.isAccessible = true
func.invoke(activity, 512, -1, Mockito.mock(Intent::class.java))
}
/**
* Test showWelcomeDialog function.
*/
@Test
@Throws(Exception::class)
fun testShowWelcomeDialog() {
val func = activity.javaClass.getDeclaredMethod(
"showWelcomeDialog"
)
func.isAccessible = true
func.invoke(activity)
}
/**
* Test onLongPress function.
*/
@Test
@Throws(Exception::class)
fun testOnLongPress() {
val func = activity.javaClass.getDeclaredMethod(
"onLongPress",
Int::class.java,
ArrayList::class.java,
ArrayList::class.java
)
images.add(image)
func.isAccessible = true
func.invoke(activity, 0, images, images)
}
/**
* Test selectedImagesChanged function.
*/
@Test
@Throws(Exception::class)
fun testOnSelectedImagesChanged() {
activity.onSelectedImagesChanged(ArrayList())
activity.onSelectedImagesChanged(ArrayList(), 0)
}
/**
@ -91,10 +151,40 @@ class CustomSelectorActivityTest {
@Throws(Exception::class)
fun testOnDone() {
activity.onDone()
activity.onSelectedImagesChanged(ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1))));
activity.onSelectedImagesChanged(
ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1))),
1
)
activity.onDone()
}
/**
* Test onClickNotForUpload function.
*/
@Test
@Throws(Exception::class)
fun testOnClickNotForUpload() {
val method: Method = CustomSelectorActivity::class.java.getDeclaredMethod(
"onClickNotForUpload"
)
method.isAccessible = true
method.invoke(activity)
activity.onSelectedImagesChanged(
ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1))),
0
)
method.invoke(activity)
}
/**
* Test setOnDataListener Function.
*/
@Test
@Throws(Exception::class)
fun testSetOnDataListener() {
activity.setOnDataListener(imageFragment)
}
/**
* Test onBackPressed Function.
*/

View file

@ -64,7 +64,8 @@ class ImageFileLoaderTest {
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
Whitebox.setInternalState(imageFileLoader, "coroutineContext", coroutineContext)
@ -103,6 +104,7 @@ class ImageFileLoaderTest {
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull()
)
} doReturn imageCursor;

View file

@ -47,6 +47,7 @@ import java.lang.reflect.Field
class ImageFragmentTest {
private lateinit var fragment: ImageFragment
private lateinit var activity: CustomSelectorActivity
private lateinit var view: View
private lateinit var selectorRV : RecyclerView
private lateinit var loader : ProgressBar
@ -76,7 +77,7 @@ class ImageFragmentTest {
AppAdapter.set(TestAppAdapter())
SoLoader.setInTestMode()
Fresco.initialize(context)
val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get()
activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get()
fragment = ImageFragment.newInstance(1,0)
val fragmentManager: FragmentManager = activity.supportFragmentManager
@ -92,6 +93,7 @@ class ImageFragmentTest {
Whitebox.setInternalState(fragment, "imageAdapter", adapter)
Whitebox.setInternalState(fragment, "selectorRV", selectorRV )
Whitebox.setInternalState(fragment, "loader", loader)
Whitebox.setInternalState(fragment, "filteredImages", arrayListOf(image,image))
viewModelField = fragment.javaClass.getDeclaredField("viewModel")
viewModelField.isAccessible = true
@ -139,6 +141,21 @@ class ImageFragmentTest {
assertEquals(3, func.invoke(fragment))
}
/**
* Test onAttach function.
*/
@Test
fun testOnAttach() {
fragment.onAttach(activity)
}
/**
* Test refresh function.
*/
@Test
fun testRefresh() {
fragment.refresh()
}
/**
* Test onResume.

View file

@ -2,9 +2,11 @@ package fr.free.nrw.commons.customselector.ui.selector
import android.content.ContentResolver
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
import fr.free.nrw.commons.customselector.database.UploadedStatus
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.customselector.model.Image
@ -61,6 +63,9 @@ class ImageLoaderTest {
@Mock
private lateinit var uploadedStatusDao: UploadedStatusDao
@Mock
private lateinit var notForUploadStatusDao: NotForUploadStatusDao
@Mock
private lateinit var holder: ImageAdapter.ImageViewHolder
@ -97,7 +102,8 @@ class ImageLoaderTest {
MockitoAnnotations.initMocks(this)
imageLoader =
ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context)
ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao,
notForUploadStatusDao, context)
uploadedStatus= UploadedStatus(
"testSha1",
"testSha1",
@ -112,8 +118,6 @@ class ImageLoaderTest {
Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1);
Whitebox.setInternalState(imageLoader, "mapResult", mapResult);
Whitebox.setInternalState(imageLoader, "context", context)
Whitebox.setInternalState(imageLoader, "ioDispatcher", testDispacher)
Whitebox.setInternalState(imageLoader, "defaultDispatcher", testDispacher)
whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream)
whenever(context.contentResolver).thenReturn(contentResolver)
@ -136,14 +140,17 @@ class ImageLoaderTest {
@Test
fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest {
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
mapModifiedImageSHA1[image] = "testSha1"
mapImageSHA1[uri] = "testSha1"
whenever(context.getSharedPreferences("custom_selector", 0))
.thenReturn(Mockito.mock(SharedPreferences::class.java))
mapResult["testSha1"] = ImageLoader.Result.TRUE
imageLoader.queryAndSetView(holder, image)
imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
mapResult["testSha1"] = ImageLoader.Result.FALSE
imageLoader.queryAndSetView(holder, image)
imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
}
/**
@ -152,20 +159,38 @@ class ImageLoaderTest {
@Test
fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest {
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus)
imageLoader.queryAndSetView(holder, image)
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
whenever(context.getSharedPreferences("custom_selector", 0))
.thenReturn(Mockito.mock(SharedPreferences::class.java))
imageLoader.queryAndSetView(holder, image, testDispacher, testDispacher)
}
/**
* Test querySha1
* Test nextActionableImage
*/
@Test
fun testQuerySha1() = testDispacher.runBlockingTest {
fun testNextActionableImage() = testDispacher.runBlockingTest {
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(0)
whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(0)
PowerMockito.mockStatic(PickedFiles::class.java)
BDDMockito.given(PickedFiles.pickedExistingPicture(context, image.uri))
.willReturn(UploadableFile(uri, File("ABC")))
whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream)
whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1")
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
uploadableFile
)
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
whenever(single.blockingGet()).thenReturn(true)
whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single)
whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1")
whenever(notForUploadStatusDao.find(any())).thenReturn(1)
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
imageLoader.querySHA1("testSha1")
whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(2)
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(2)
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0)
}
/**
@ -183,13 +208,13 @@ class ImageLoaderTest {
whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream)
whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1")
Assert.assertEquals("testSha1", imageLoader.getSHA1(image));
Assert.assertEquals("testSha1", imageLoader.getSHA1(image, testDispacher));
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
uploadableFile
)
mapModifiedImageSHA1[image] = "testSha2"
Assert.assertEquals("testSha2", imageLoader.getSHA1(image));
Assert.assertEquals("testSha2", imageLoader.getSHA1(image, testDispacher));
}
/**
@ -213,8 +238,4 @@ class ImageLoaderTest {
imageLoader.getResultFromUploadedStatus(uploadedStatus))
}
@Test
fun testCleanUP() {
imageLoader.cleanUP()
}
}

View file

@ -5,18 +5,25 @@ import android.content.Intent
import android.net.Uri
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.soloader.SoLoader
import fr.free.nrw.commons.TestAppAdapter
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Image
import fr.free.nrw.commons.customselector.model.Result
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import org.wikipedia.AppAdapter
import java.lang.reflect.Field
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@ -25,18 +32,32 @@ class ZoomableActivityUnitTests {
private lateinit var context: Context
private lateinit var activity: ZoomableActivity
private lateinit var viewModelField: Field
private lateinit var image: Image
@Mock
private lateinit var uri: Uri
@Mock
private lateinit var images: ArrayList<Image>
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
AppAdapter.set(TestAppAdapter())
context = RuntimeEnvironment.application.applicationContext
SoLoader.setInTestMode()
Fresco.initialize(context)
val intent = Intent().setData(uri)
activity = Robolectric.buildActivity(ZoomableActivity::class.java, intent).create().get()
image = Image(1, "image", uri, "abc/abc", 1, "bucket1")
Whitebox.setInternalState(activity, "images", arrayListOf(image))
Whitebox.setInternalState(activity, "selectedImages", arrayListOf(image))
viewModelField = activity.javaClass.getDeclaredField("viewModel")
viewModelField.isAccessible = true
}
@Test
@ -45,4 +66,87 @@ class ZoomableActivityUnitTests {
Assert.assertNotNull(activity)
}
/**
* Test handleResult.
*/
@Test
fun testHandleResult(){
val func = activity.javaClass.getDeclaredMethod("handleResult", Result::class.java)
func.isAccessible = true
func.invoke(activity, Result(CallbackStatus.SUCCESS, arrayListOf()))
func.invoke(activity, Result(CallbackStatus.SUCCESS, arrayListOf(image,image)))
}
/**
* Test onLeftSwiped.
*/
@Test
fun testOnLeftSwiped(){
val func = activity.javaClass.getDeclaredMethod("onLeftSwiped", Boolean::class.java)
func.isAccessible = true
func.invoke(activity, true)
Whitebox.setInternalState(activity, "images", arrayListOf(image, image))
Whitebox.setInternalState(activity, "position", 0)
func.invoke(activity, true)
func.invoke(activity, false)
}
/**
* Test onRightSwiped.
*/
@Test
fun testOnRightSwiped(){
val func = activity.javaClass.getDeclaredMethod("onRightSwiped", Boolean::class.java)
func.isAccessible = true
func.invoke(activity, true)
Whitebox.setInternalState(activity, "images", arrayListOf(image, image))
Whitebox.setInternalState(activity, "position", 1)
func.invoke(activity, true)
func.invoke(activity, false)
}
/**
* Test onUpSwiped.
*/
@Test
fun testOnUpSwiped(){
val func = activity.javaClass.getDeclaredMethod("onUpSwiped")
func.isAccessible = true
func.invoke(activity)
}
/**
* Test onDownSwiped.
*/
@Test
fun testOnDownSwiped(){
val func = activity.javaClass.getDeclaredMethod("onDownSwiped")
func.isAccessible = true
func.invoke(activity)
}
/**
* Test onBackPressed.
*/
@Test
fun testOnBackPressed(){
val func = activity.javaClass.getDeclaredMethod("onBackPressed")
func.isAccessible = true
func.invoke(activity)
}
/**
* Test onDestroy.
*/
@Test
fun testOnDestroy(){
val func = activity.javaClass.getDeclaredMethod("onDestroy")
func.isAccessible = true
func.invoke(activity)
}
}