From 560662cad4702627855e3ed16adedbdea168e7a5 Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Tue, 25 May 2021 00:54:55 +0530 Subject: [PATCH 01/28] Initialised xmls, made folder and image item. --- app/build.gradle | 2 +- .../main/res/drawable-ldpi/circle_shape.xml | 4 + app/src/main/res/drawable-ldpi/commons.xml | 62 +++++++++++++ .../res/layout/activity_custom_selector.xml | 7 ++ .../res/layout/fragment_custom_selector.xml | 42 +++++++++ .../layout/item_custom_selector_folder.xml | 84 ++++++++++++++++++ .../res/layout/item_custom_selector_image.xml | 87 +++++++++++++++++++ 7 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable-ldpi/circle_shape.xml create mode 100644 app/src/main/res/drawable-ldpi/commons.xml create mode 100644 app/src/main/res/layout/activity_custom_selector.xml create mode 100644 app/src/main/res/layout/fragment_custom_selector.xml create mode 100644 app/src/main/res/layout/item_custom_selector_folder.xml create mode 100644 app/src/main/res/layout/item_custom_selector_image.xml diff --git a/app/build.gradle b/app/build.gradle index c6f34cc682..68405acce4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -204,7 +204,7 @@ android { } } debug { - minifyEnabled true + minifyEnabled false testCoverageEnabled project.hasProperty('coverage') proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' testProguardFile 'test-proguard-rules.txt' diff --git a/app/src/main/res/drawable-ldpi/circle_shape.xml b/app/src/main/res/drawable-ldpi/circle_shape.xml new file mode 100644 index 0000000000..d581bfb9f3 --- /dev/null +++ b/app/src/main/res/drawable-ldpi/circle_shape.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-ldpi/commons.xml b/app/src/main/res/drawable-ldpi/commons.xml new file mode 100644 index 0000000000..4c2e6cabff --- /dev/null +++ b/app/src/main/res/drawable-ldpi/commons.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml new file mode 100644 index 0000000000..1b6d22e00b --- /dev/null +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_selector.xml b/app/src/main/res/layout/fragment_custom_selector.xml new file mode 100644 index 0000000000..45a174bff2 --- /dev/null +++ b/app/src/main/res/layout/fragment_custom_selector.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml new file mode 100644 index 0000000000..3592255ddb --- /dev/null +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml new file mode 100644 index 0000000000..7252f543ff --- /dev/null +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 4f394ffd71f892c1e75b7c23eae3890634c4498f Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Sat, 5 Jun 2021 05:25:10 +0530 Subject: [PATCH 02/28] xmls done --- app/src/main/res/drawable/ic_arrow_back_black.xml | 10 ++++++++++ app/src/main/res/drawable/ic_done_black.xml | 10 ++++++++++ app/src/main/res/drawable/ic_done_white.xml | 5 +++++ app/src/main/res/layout/custom_selector_toolbar.xml | 6 ++++++ 4 files changed, 31 insertions(+) create mode 100644 app/src/main/res/drawable/ic_arrow_back_black.xml create mode 100644 app/src/main/res/drawable/ic_done_black.xml create mode 100644 app/src/main/res/drawable/ic_done_white.xml create mode 100644 app/src/main/res/layout/custom_selector_toolbar.xml diff --git a/app/src/main/res/drawable/ic_arrow_back_black.xml b/app/src/main/res/drawable/ic_arrow_back_black.xml new file mode 100644 index 0000000000..b5487b3ea4 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_black.xml b/app/src/main/res/drawable/ic_done_black.xml new file mode 100644 index 0000000000..899cbb6840 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_done_white.xml b/app/src/main/res/drawable/ic_done_white.xml new file mode 100644 index 0000000000..2728880b72 --- /dev/null +++ b/app/src/main/res/drawable/ic_done_white.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml new file mode 100644 index 0000000000..28f5b725cc --- /dev/null +++ b/app/src/main/res/layout/custom_selector_toolbar.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From d6472a1fb123151c0b49b4b91abdee72fa3b1bff Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Sat, 5 Jun 2021 05:25:47 +0530 Subject: [PATCH 03/28] xmls completed --- app/src/main/res/drawable/ic_done_black.xml | 2 +- .../res/layout/activity_custom_selector.xml | 23 ++++++++-- .../res/layout/custom_selector_toolbar.xml | 46 +++++++++++++++++-- .../res/layout/fragment_custom_selector.xml | 14 +++--- .../layout/item_custom_selector_folder.xml | 13 +++--- .../res/layout/item_custom_selector_image.xml | 15 +++--- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 4 ++ 10 files changed, 91 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/drawable/ic_done_black.xml b/app/src/main/res/drawable/ic_done_black.xml index 899cbb6840..2d3858a70b 100644 --- a/app/src/main/res/drawable/ic_done_black.xml +++ b/app/src/main/res/drawable/ic_done_black.xml @@ -5,6 +5,6 @@ android:viewportHeight="24" android:tint="?attr/colorControlNormal"> diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index 1b6d22e00b..f90ca51e24 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -1,7 +1,24 @@ - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml index 28f5b725cc..45ebdc9234 100644 --- a/app/src/main/res/layout/custom_selector_toolbar.xml +++ b/app/src/main/res/layout/custom_selector_toolbar.xml @@ -1,6 +1,44 @@ - + - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_custom_selector.xml b/app/src/main/res/layout/fragment_custom_selector.xml index 45a174bff2..8a41cb6dd4 100644 --- a/app/src/main/res/layout/fragment_custom_selector.xml +++ b/app/src/main/res/layout/fragment_custom_selector.xml @@ -6,20 +6,19 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + /> + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ac5d31cf73..b3fb6d1a4d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -58,6 +58,7 @@ 0dp + 2dp 6dp 10dp 20dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0cf7d7901..4e3c8fb040 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -646,5 +646,7 @@ Upload your first media by tapping on the add button. LEARN MORE Wiki Loves Monuments Wiki Loves Monuments is an international photo contest for monuments organised by Wikimedia + Custom Selector + No Images diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 308291dea8..1ae9e0a7cb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -55,6 +55,8 @@ @color/white @color/white @drawable/ic_search_white_24dp + @drawable/ic_done_white + @drawable/ic_arrow_back_white false false @@ -113,6 +115,8 @@ @color/disabled_button_text_color_dark @color/primaryDarkColor @drawable/ic_search_blue_24dp + @drawable/ic_done_black + @drawable/ic_arrow_back_black false false From 63798c00ca023ba642bd9f6d2369f22e471d825b Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Sat, 5 Jun 2021 08:00:07 +0530 Subject: [PATCH 04/28] removed unwanted attribute --- app/src/main/res/layout/item_custom_selector_folder.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml index c13838e923..4dcc78c1ae 100644 --- a/app/src/main/res/layout/item_custom_selector_folder.xml +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -31,7 +31,7 @@ android:layout_height="match_parent" android:id="@+id/album_overlay" android:alpha="0.05" - android:background=""/> + /> <54016427+4D17Y4@users.noreply.github.com> Date: Thu, 10 Jun 2021 13:02:00 +0530 Subject: [PATCH 05/28] Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable --- app/src/main/AndroidManifest.xml | 3 + .../ContributionsListFragment.java | 83 +++++++---- .../listeners/FolderClickListener.kt | 7 + .../listeners/ImageLoaderListener.kt | 8 + .../listeners/ImageSelectListener.kt | 7 + .../customselector/model/CallbackStatus.kt | 18 +++ .../commons/customselector/model/Folder.kt | 44 ++++++ .../nrw/commons/customselector/model/Image.kt | 125 ++++++++++++++++ .../commons/customselector/model/Result.kt | 13 ++ .../ui/adapter/FolderAdapter.kt | 141 ++++++++++++++++++ .../customselector/ui/adapter/ImageAdapter.kt | 135 +++++++++++++++++ .../ui/adapter/RecyclerViewAdapter.kt | 12 ++ .../ui/selector/CustomSelectorActivity.kt | 121 +++++++++++++++ .../ui/selector/CustomSelectorViewModel.kt | 36 +++++ .../CustomSelectorViewModelFactory.kt | 17 +++ .../ui/selector/FolderFragment.kt | 108 ++++++++++++++ .../ui/selector/ImageFileLoader.kt | 38 +++++ .../ui/selector/ImageFragment.kt | 126 ++++++++++++++++ .../customselector/ui/selector/ImageLoader.kt | 7 + .../nrw/commons/di/ActivityBuilderModule.java | 4 + .../commons/di/CommonsApplicationModule.java | 6 + .../nrw/commons/di/FragmentBuilderModule.java | 8 + .../res/layout/activity_custom_selector.xml | 9 +- .../res/layout/custom_selector_toolbar.xml | 7 +- .../layout/fragment_contributions_list.xml | 13 ++ .../res/layout/fragment_custom_selector.xml | 1 + .../layout/item_custom_selector_folder.xml | 45 +++--- .../res/layout/item_custom_selector_image.xml | 23 +-- 28 files changed, 1092 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc6576aad0..1c10683275 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,6 +110,9 @@ + + ) + fun onFailed(throwable: Throwable) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt new file mode 100644 index 0000000000..c29aa21e2e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.customselector.listeners + +import fr.free.nrw.commons.customselector.model.Image + +interface ImageSelectListener { + fun onSelectedImagesChanged(selectedImages: ArrayList) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt new file mode 100644 index 0000000000..257b39a950 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.customselector.model + +sealed class CallbackStatus { + /** + IDLE : The callback is idle , doing nothing. + */ + object IDLE : CallbackStatus() + + /** + FETCHING : Fetching images. + */ + object FETCHING : CallbackStatus() + + /** + SUCCESS : Success fetching images. + */ + object SUCCESS : CallbackStatus() +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt new file mode 100644 index 0000000000..0ce95ec22d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.customselector.model + +data class Folder( + /** + bucketId : Unique directory id, eg 540528482 + */ + var bucketId: Long, + + /** + name : bucket/folder name, eg Camera + */ + var name: String, + + /** + images : folder images, list of all images under this folder. + */ + var images: ArrayList = arrayListOf() + + +) { + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + + if (javaClass != other?.javaClass) { + return false + } + + other as Folder + + if (bucketId != other.bucketId) { + return false + } + if (name != other.name) { + return false + } + if (images != other.images) { + return false + } + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt new file mode 100644 index 0000000000..d6d296f29d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt @@ -0,0 +1,125 @@ +package fr.free.nrw.commons.customselector.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +data class Image( + /** + id : Unique image id, primary key of image in device, eg 104950 + */ + var id: Long, + + /** + name : Name of the image with extension, eg CommonsLogo.jpeg + */ + var name: String, + + /** + uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10) + */ + var uri: Uri, + + /** + path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg + */ + var path: String, + + /** + bucketId : bucketId of folder, eg 540528482 + */ + var bucketId: Long = 0, + + /** + bucketName : name of folder, eg Camera + */ + var bucketName: String = "", + + /** + sha1 : sha1 of original image. + */ + var sha1: String = "" +) : Parcelable { + + /** + default parcelable constructor. + */ + constructor(parcel: Parcel): + this(parcel.readLong(), + parcel.readString()!!, + parcel.readParcelable(Uri::class.java.classLoader)!!, + parcel.readString()!!, + parcel.readLong(), + parcel.readString()!!, + parcel.readString()!! + ) + + /** + Write to parcel method. + */ + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(id) + parcel.writeString(name) + parcel.writeParcelable(uri, flags) + parcel.writeString(path) + parcel.writeLong(bucketId) + parcel.writeString(bucketName) + parcel.writeString(sha1) + } + + /** + * Describe the kinds of special objects contained in this Parcelable + */ + override fun describeContents(): Int { + return 0 + } + + /** + * Indicates whether some other object is "equal to" this one. + */ + override fun equals(other: Any?): Boolean { + + if(javaClass != other?.javaClass) { + return false + } + + other as Image + + if(id != other.id) { + return false; + } + if(name != other.name) { + return false; + } + if(uri != other.uri) { + return false; + } + if(path != other.path) { + return false; + } + if(bucketId != other.bucketId) { + return false; + } + if(bucketName != other.bucketName) { + return false; + } + if(sha1 != other.sha1) { + return false; + } + + return true + } + + /** + * Parcelable companion object + */ + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Image { + return Image(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt new file mode 100644 index 0000000000..0eb4decbd0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.customselector.model + +data class Result( + /** + * CallbackStatus : stores the result status + */ + val status:CallbackStatus, + + /** + * Images : images retrieved + */ + val images: ArrayList) { +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt new file mode 100644 index 0000000000..11450549c9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -0,0 +1,141 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.ui.selector.ImageLoader + +class FolderAdapter( + /** + * Application context. + */ + context: Context, + + /** + * Folder Click listener for click events. + */ + private val itemClickListener: FolderClickListener +) : RecyclerViewAdapter(context) { + + /** + * Image Loader for loading images. + */ + private val imageLoader = ImageLoader() + + /** + * List of folders. + */ + private var folders: MutableList = mutableListOf() + + /** + * Create view holder, returns View holder item. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder { + val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false) + return FolderViewHolder(itemView) + } + + /** + * Bind view holder, setup the item view, title, count and click listener + */ + override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { + val folder = folders[position] + val count = folder.images.size + val previewImage = folder.images[0] + holder.name.text = folder.name + holder.count.text= count.toString() + holder.itemView.setOnClickListener{ + itemClickListener.onFolderClick(folder) + } + + //todo load image thumbnail. + } + + /** + * Initialise the data set. + */ + fun init(newFolders: List) { + val oldFolderList: MutableList = folders + val newFolderList = newFolders.toMutableList() + val diffResult = DiffUtil.calculateDiff( + FoldersDiffCallback(oldFolderList, newFolderList) + ) + folders = newFolderList + diffResult.dispatchUpdatesTo(this) + } + + + /** + * returns item count. + */ + override fun getItemCount(): Int { + return folders.size + } + + /** + * Folder view holder. + */ + class FolderViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) { + + /** + * Folder thumbnail image view. + */ + val image: ImageView = itemView.findViewById(R.id.folder_thumbnail) + + /** + * Folder/album name + */ + val name: TextView = itemView.findViewById(R.id.folder_name) + + /** + * Item count in Folder/Item + */ + val count: TextView = itemView.findViewById(R.id.folder_count) + } + + /** + * DiffUtilCallback. + */ + class FoldersDiffCallback( + var oldFolders: MutableList, + var newFolders: MutableList + ) : DiffUtil.Callback() { + /** + * Returns the size of the old list. + */ + override fun getOldListSize(): Int { + return oldFolders.size + } + + /** + * Returns the size of the new list. + */ + override fun getNewListSize(): Int { + return newFolders.size + } + + /** + * Called by the DiffUtil to decide whether two object represent the same Item. + */ + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId + } + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + */ + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition)) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt new file mode 100644 index 0000000000..b29910c000 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.ViewGroup +import fr.free.nrw.commons.R +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.Group +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.Image + +class ImageAdapter( + /** + * Application Context. + */ + context: Context, + + /** + * Image select listener for click events on image. + */ + private var imageSelectListener: ImageSelectListener ): + + RecyclerViewAdapter(context) { + + /** + * Currently selected images. + */ + private var selectedImages = arrayListOf() + + /** + * List of all images in adapter. + */ + private var images: ArrayList = ArrayList() + + /** + * create View holder. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { + val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) + return ImageViewHolder(itemView) + } + + /** + * Bind View holder, load image, selected view, click listeners. + */ + override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { + val image=images[position] + // todo load image thumbnail, set selected view. + holder.itemView.setOnClickListener { + selectOrRemoveImage(image, position) + } + } + + /** + * Handle click event on an image, update counter on images. + */ + private fun selectOrRemoveImage(image:Image, position:Int){ + // todo select the image if not selected and remove it if already selected + } + + /** + * Initialize the data set. + */ + fun init(newImages:List) { + val oldImageList:ArrayList = images + val newImageList:ArrayList = ArrayList(newImages) + val diffResult = DiffUtil.calculateDiff( + ImagesDiffCallback(oldImageList, newImageList) + ) + images = newImageList + diffResult.dispatchUpdatesTo(this) + } + + /** + * 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 + } + + /** + * Image view holder. + */ + class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + val image: ImageView = itemView.findViewById(R.id.image_thumbnail) + val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) + val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) + val selectedGroup: Group = itemView.findViewById(R.id.selected_group) + } + + /** + * DiffUtilCallback. + */ + class ImagesDiffCallback( + var oldImageList: ArrayList, + var newImageList: ArrayList + ) : DiffUtil.Callback(){ + + /** + * Returns the size of the old list. + */ + override fun getOldListSize(): Int { + return oldImageList.size + } + + /** + * Returns the size of the new list. + */ + override fun getNewListSize(): Int { + return newImageList.size + } + + /** + * Called by the DiffUtil to decide whether two object represent the same Item. + */ + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return newImageList[newItemPosition].id == oldImageList[oldItemPosition].id + } + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + */ + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldImageList[oldItemPosition].equals(newImageList[newItemPosition]) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt new file mode 100644 index 0000000000..75f9353028 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/RecyclerViewAdapter.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import androidx.recyclerview.widget.RecyclerView + +/** + * Generic Recycler view adapter. + */ +abstract class RecyclerViewAdapter(val context: Context): RecyclerView.Adapter() { + val inflater: LayoutInflater = LayoutInflater.from(context) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt new file mode 100644 index 0000000000..1ab30f67ea --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.os.Bundle +import android.widget.ImageButton +import android.widget.TextView +import androidx.lifecycle.ViewModelProvider +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.theme.BaseActivity +import javax.inject.Inject + +class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { + + /** + * View model. + */ + private lateinit var viewModel: CustomSelectorViewModel + + /** + * View Model Factory. + */ + @Inject lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory + + /** + * onCreate Activity, sets theme, initialises the view model, setup view. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_custom_selector) + + viewModel = ViewModelProvider(this,customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + + setupViews() + } + + /** + * Set up view, default folder view. + */ + private fun setupViews() { + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, FolderFragment.newInstance()) + .commit() + fetchData() + setUpToolbar() + + // todo : open image fragment depending on the last user visit. + } + + /** + * Start data fetch in view model. + */ + private fun fetchData() { + viewModel.fetchImages() + } + + /** + * Change the title of the toolbar. + */ + private fun changeTitle(title:String) { + val titleText = findViewById(R.id.title) + if(titleText != null) { + titleText.text = title + } + } + + /** + * Set up the toolbar, back listener, done listener. + */ + private fun setUpToolbar() { + val back : ImageButton = findViewById(R.id.back) + back.setOnClickListener { onBackPressed() } + + // todo done listener. + } + + /** + * override on folder click, change the toolbar title on folder click. + */ + override fun onFolderClick(folder: Folder) { + supportFragmentManager.beginTransaction() + .add(R.id.fragment_container, ImageFragment.newInstance(folder.bucketId)) + .addToBackStack(null) + .commit() + changeTitle(folder.name) + } + + /** + * override Selected Images Change, update view model selected images. + */ + override fun onSelectedImagesChanged(selectedImages: ArrayList) { + // todo update selected images in view model. + } + + /** + * Back pressed. + * Change toolbar title. + */ + override fun onBackPressed() { + super.onBackPressed() + val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) + if(fragment != null && fragment is FolderFragment){ + changeTitle(getString(R.string.custom_selector_title)) + } + } + + + /** + * + * TODO + * Permission check. + * OnDone + * Activity Result. + * + * + */ + + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt new file mode 100644 index 0000000000..a5f7cf6e5a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.model.Result + +class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { + + /** + * Result Live Data + */ + val result = MutableLiveData(Result(CallbackStatus.IDLE, arrayListOf())) + + /** + * Fetch Images and supply to result. + */ + fun fetchImages() { + result.postValue(Result(CallbackStatus.FETCHING, arrayListOf())) + imageFileLoader.abortLoadImage() + imageFileLoader.loadDeviceImages(object: ImageLoaderListener { + + override fun onImageLoaded(images: ArrayList) { + result.postValue(Result(CallbackStatus.SUCCESS, images)) + } + + override fun onFailed(throwable: Throwable) { + result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf())) + } + + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt new file mode 100644 index 0000000000..d7a7d42f40 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelFactory.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject + +/** + * View Model Factory. + */ +class CustomSelectorViewModelFactory @Inject constructor(val context: Context,val imageFileLoader: ImageFileLoader) : ViewModelProvider.Factory { + + override fun create(modelClass: Class) : CustomSelectorViewModel { + return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt new file mode 100644 index 0000000000..a3db475717 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -0,0 +1,108 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.model.Result +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import kotlinx.android.synthetic.main.fragment_custom_selector.* +import kotlinx.android.synthetic.main.fragment_custom_selector.view.* +import javax.inject.Inject + +class FolderFragment : CommonsDaggerSupportFragment() { + + /** + * View Model for images. + */ + private var viewModel: CustomSelectorViewModel? = null + + /** + * View Model Factory. + */ + var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null + @Inject set + + + /** + * Folder Adapter. + */ + private lateinit var folderAdapter: FolderAdapter + + /** + * Grid Layout Manager for recycler view. + */ + private lateinit var gridLayoutManager: GridLayoutManager + + /** + * Companion newInstance. + */ + companion object{ + fun newInstance(): FolderFragment { + return FolderFragment() + } + } + + /** + * OnCreate Fragment, get the view model. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java) + + } + + /** + * OnCreateView. + * Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result. + */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) + folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) + gridLayoutManager = GridLayoutManager(context, columnCount()) + with(root.selector_rv){ + this.layoutManager = gridLayoutManager + setHasFixedSize(true) + this.adapter = folderAdapter + } + viewModel?.result?.observe(viewLifecycleOwner, Observer { + handleResult(it) + }) + return root + } + + /** + * Handle view model result. + * Get folders from images. + * Load adapter. + */ + private fun handleResult(result: Result) { + if(result.status is CallbackStatus.SUCCESS){ + val folders = arrayListOf() + for( i in 1..12) { + folders.add(Folder(i.toLong(), "Folder$i",result.images)) + } + folderAdapter.init(folders) + folderAdapter.notifyDataSetChanged() + selector_rv.visibility = View.VISIBLE + } + loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + } + + /** + * Return Column count ie span count for grid view adapter. + */ + private fun columnCount(): Int { + return 2 + // todo change column count depending on the orientation of the device. + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt new file mode 100644 index 0000000000..738c40e989 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt @@ -0,0 +1,38 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import android.net.Uri +import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener +import fr.free.nrw.commons.customselector.model.Image + +class ImageFileLoader(val context: Context) { + + /** + * Load Device Images. + */ + fun loadDeviceImages(listener: ImageLoaderListener) { + var tempImage = Image(0, "temp", Uri.parse("http://www.google.com"), "path", 0, "bucket", "1223") + var array: ArrayList = ArrayList() + for(i in 1..100) { + array.add(tempImage) + } + listener.onImageLoaded(array) + + // todo load images from device using cursor. + } + + /** + * Abort loading images. + */ + fun abortLoadImage(){ + //todo Abort loading images. + } + + /** + * + * TODO + * Runnable Thread for image loading. + * Sha1 for image (original image). + * + */ +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt new file mode 100644 index 0000000000..c22313d655 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -0,0 +1,126 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.listeners.ImageSelectListener +import fr.free.nrw.commons.customselector.model.CallbackStatus +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 kotlinx.android.synthetic.main.fragment_custom_selector.* +import kotlinx.android.synthetic.main.fragment_custom_selector.view.* +import javax.inject.Inject + +class ImageFragment: CommonsDaggerSupportFragment() { + + /** + * Current bucketId. + */ + private var bucketId: Long? = null + + /** + * View model for images. + */ + private lateinit var viewModel: CustomSelectorViewModel + + /** + * View model Factory. + */ + lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory + @Inject set + + /** + * Image Adapter for recycle view. + */ + private lateinit var imageAdapter: ImageAdapter + + /** + * GridLayoutManager for recycler view. + */ + private lateinit var gridLayoutManager: GridLayoutManager + + + companion object { + + /** + * BucketId args name + */ + const val BUCKET_ID = "BucketId" + + /** + * newInstance from bucketId. + */ + fun newInstance(bucketId: Long): ImageFragment { + val fragment = ImageFragment() + val args = Bundle() + args.putLong(BUCKET_ID, bucketId) + fragment.arguments = args + return fragment + } + } + + /** + * OnCreate + * Get BucketId, view Model. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bucketId = arguments?.getLong(BUCKET_ID) + viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + } + + /** + * OnCreateView + * Init imageAdapter, gridLayoutManger. + * SetUp recycler view. + */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + + val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) + imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener) + gridLayoutManager = GridLayoutManager(context,getSpanCount()) + with(root.selector_rv){ + this.layoutManager = gridLayoutManager + setHasFixedSize(true) + this.adapter = imageAdapter + } + + viewModel.result.observe(viewLifecycleOwner, Observer{ + handleResult(it) + }) + + return root + } + + /** + * Handle view model result. + */ + private fun handleResult(result:Result){ + if(result.status is CallbackStatus.SUCCESS){ + val images = result.images + if(images.isNotEmpty()) { + imageAdapter.init(images) + selector_rv.visibility = View.VISIBLE + } + else{ + selector_rv.visibility = View.GONE + } + } + loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + } + + /** + * getSpanCount for GridViewManager. + */ + private fun getSpanCount(): Int { + return 3 + // todo change span count depending on the device orientation and other factos. + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt new file mode 100644 index 0000000000..22da8cbbb9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.customselector.ui.selector + +/** + * Image Loader class, loads images, depending on API results. + */ +class ImageLoader { +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 28c79c612f..6381bdc8e9 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -8,6 +8,7 @@ import fr.free.nrw.commons.auth.SignupActivity; import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.contributions.MainActivity; +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.notification.NotificationActivity; @@ -34,6 +35,9 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract MainActivity bindContributionsActivity(); + @ContributesAndroidInjector + abstract CustomSelectorActivity bindCustomSelectorActivity(); + @ContributesAndroidInjector abstract SettingsActivity bindSettingsActivity(); diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 1e19de5f4e..bca71de983 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -17,6 +17,7 @@ import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionDao; +import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.kvstore.JsonKvStore; @@ -66,6 +67,11 @@ public CommonsApplicationModule(Context applicationContext) { this.applicationContext = applicationContext; } + @Provides + public ImageFileLoader providesImageFileLoader() { + return new ImageFileLoader(this.applicationContext); + } + @Provides public Context providesApplicationContext() { return this.applicationContext; diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index 3757a21473..f255134eaf 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -8,6 +8,8 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; import fr.free.nrw.commons.contributions.ContributionsFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; +import fr.free.nrw.commons.customselector.ui.selector.FolderFragment; +import fr.free.nrw.commons.customselector.ui.selector.ImageFragment; import fr.free.nrw.commons.explore.ExploreFragment; import fr.free.nrw.commons.explore.ExploreListRootFragment; import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; @@ -49,6 +51,12 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract MediaDetailFragment bindMediaDetailFragment(); + @ContributesAndroidInjector + abstract FolderFragment bindFolderFragment(); + + @ContributesAndroidInjector + abstract ImageFragment bindImageFragment(); + @ContributesAndroidInjector abstract MediaDetailPagerFragment bindMediaDetailPagerFragment(); diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index f90ca51e24..9587e7c0a0 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -15,10 +15,11 @@ app:layout_constraintTop_toTopOf="parent"/> - + app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"/> \ No newline at end of file diff --git a/app/src/main/res/layout/custom_selector_toolbar.xml b/app/src/main/res/layout/custom_selector_toolbar.xml index 45ebdc9234..29b9ab66b3 100644 --- a/app/src/main/res/layout/custom_selector_toolbar.xml +++ b/app/src/main/res/layout/custom_selector_toolbar.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" > - @@ -29,7 +31,7 @@ android:text="@string/custom_selector_title" style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" /> - diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml index 49e4e60c74..e9852f49af 100644 --- a/app/src/main/res/layout/fragment_contributions_list.xml +++ b/app/src/main/res/layout/fragment_contributions_list.xml @@ -69,6 +69,19 @@ app:fabSize="mini" app:srcCompat="@drawable/ic_photo_white_24dp" /> + + diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml index 4dcc78c1ae..077968c6a5 100644 --- a/app/src/main/res/layout/item_custom_selector_folder.xml +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -1,8 +1,8 @@ - @@ -24,49 +23,47 @@ android:id="@+id/folder_thumbnail" android:layout_width="match_parent" android:layout_height="match_parent" - android:scaleType="centerCrop"/> + android:background="@color/black" + android:alpha="0.15" + android:scaleType="centerCrop" /> + android:alpha="0.05" /> + app:layout_constraintBottom_toBottomOf="parent"> + android:textSize="16sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" /> + android:textStyle="bold" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintRight_toRightOf="parent" /> @@ -75,7 +72,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="visible" - app:constraint_referenced_ids="folder_details,album_overlay"/> + app:constraint_referenced_ids="folder_details,album_overlay" /> diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml index e3240e90ee..eec1eb9d99 100644 --- a/app/src/main/res/layout/item_custom_selector_image.xml +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -3,16 +3,16 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" + android:padding="@dimen/dimen_2" android:layout_height="wrap_content"> @@ -38,18 +38,19 @@ android:layout_width="@dimen/dimen_20" android:layout_height="@dimen/dimen_20" app:layout_constraintDimensionRatio="H,1:1" - android:layout_margin="@dimen/dimen_10" - android:gravity="center|center_vertical" - android:includeFontPadding="false" + android:textSize="11sp" 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:background="@drawable/circle_shape" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <54016427+4D17Y4@users.noreply.github.com> Date: Sun, 13 Jun 2021 16:10:04 +0530 Subject: [PATCH 06/28] [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code --- app/build.gradle | 4 + .../contributions/ContributionController.java | 20 ++++ .../ContributionsListFragment.java | 3 +- .../customselector/helper/ImageHelper.kt | 94 +++++++++++++++++++ .../ui/adapter/FolderAdapter.kt | 4 +- .../customselector/ui/adapter/ImageAdapter.kt | 2 + .../ui/selector/CustomSelectorViewModel.kt | 20 +++- .../ui/selector/FolderFragment.kt | 10 +- .../ui/selector/ImageFileLoader.kt | 92 +++++++++++++++--- .../ui/selector/ImageFragment.kt | 6 +- .../layout/item_custom_selector_folder.xml | 17 ++-- .../res/layout/item_custom_selector_image.xml | 6 +- 12 files changed, 239 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 68405acce4..501595fdaa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,6 +142,10 @@ dependencies { def work_version = "2.4.0" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" + + //Glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' } android { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 15c61d8367..778b1afdcc 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -8,6 +8,7 @@ import android.content.Intent; import androidx.annotation.NonNull; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; @@ -58,6 +59,25 @@ public void initiateGalleryPick(final Activity activity, final boolean allowMult initiateGalleryUpload(activity, allowMultipleUploads); } + /** + * Initiate gallery picker with permission + */ + public void initiateCustomGalleryPickWithPermission(final Activity activity) { + boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); + Intent intent = new Intent(activity,CustomSelectorActivity.class); + if (!useExtStorage) { + activity.startActivity(intent); + return; + } + + PermissionUtils.checkPermissionsAndPerformAction(activity, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + () -> activity.startActivity(intent), + R.string.storage_permission_title, + R.string.write_storage_permission_rationale); + } + + /** * Open chooser for gallery uploads */ diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index d0fd857e9b..c2c6c40866 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -271,8 +271,7 @@ private void setListeners() { @OnClick(R.id.fab_custom_gallery) void launchCustomSelector(){ - Intent intent = new Intent(getActivity(), CustomSelectorActivity.class); - startActivity(intent); + controller.initiateCustomGalleryPickWithPermission(getActivity()); } private void animateFAB(final boolean isFabOpen) { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt new file mode 100644 index 0000000000..1b676b6e2c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -0,0 +1,94 @@ +package fr.free.nrw.commons.customselector.helper + +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import timber.log.Timber +import java.io.* +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import kotlin.collections.ArrayList +import kotlin.collections.LinkedHashMap + +/** + * Image Helper object, includes all the static functions required by custom selector + */ + +object ImageHelper { + + /** + * Returns the list of folders from given image list. + */ + fun folderListFromImages(images: List): List { + val folderMap: MutableMap = LinkedHashMap() + for (image in images) { + val bucketId = image.bucketId + val bucketName = image.bucketName + var folder = folderMap[bucketId] + if (folder == null) { + folder = Folder(bucketId, bucketName) + folderMap[bucketId] = folder + } + folder.images.add(image) + } + return ArrayList(folderMap.values) + } + + /** + * Filters the images based on the given bucketId (folder) + */ + fun filterImages(images: ArrayList, bukketId: Long?): ArrayList { + if (bukketId == null) return images + + val filteredImages = arrayListOf() + for (image in images) { + if (image.bucketId == bukketId) { + filteredImages.add(image) + } + } + return filteredImages + } + + /** + * Generates the file sha1 from file input stream. + */ + fun generateSHA1(`is`: InputStream): String { + val digest: MessageDigest = try { + MessageDigest.getInstance("SHA1") + } catch (e: NoSuchAlgorithmException) { + Timber.e(e, "Exception while getting Digest") + return "" + } + val buffer = ByteArray(8192) + var read: Int + return try { + while (`is`.read(buffer).also { read = it } > 0) { + digest.update(buffer, 0, read) + } + val md5sum = digest.digest() + val bigInt = BigInteger(1, md5sum) + var output = bigInt.toString(16) + output = String.format("%40s", output).replace(' ', '0') + Timber.i("File SHA1: %s", output) + output + } catch (e: IOException) { + Timber.e(e, "IO Exception") + "" + } finally { + try { + `is`.close() + } catch (e: IOException) { + Timber.e(e, "Exception on closing input stream") + } + } + } + + /** + * Gets the file input stream from the file path. + */ + @Throws(FileNotFoundException::class) + fun getFileInputStream(filePath: String?): InputStream { + return FileInputStream(filePath) + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 11450549c9..5d28a46d1b 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -7,6 +7,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder @@ -49,8 +50,9 @@ class FolderAdapter( val folder = folders[position] val count = folder.images.size val previewImage = folder.images[0] + Glide.with(context).load(previewImage.uri).into(holder.image) holder.name.text = folder.name - holder.count.text= count.toString() + holder.count.text = count.toString() holder.itemView.setOnClickListener{ itemClickListener.onFolderClick(folder) } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index b29910c000..53de6de773 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -9,6 +9,7 @@ import android.widget.TextView import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -49,6 +50,7 @@ class ImageAdapter( override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val image=images[position] // todo load image thumbnail, set selected view. + Glide.with(context).load(image.uri).into(holder.image) holder.itemView.setOnClickListener { selectOrRemoveImage(image, position) } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt index a5f7cf6e5a..26b8033bac 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -1,14 +1,20 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context +import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel -class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { +class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { + + private val scope = CoroutineScope(Dispatchers.Main) /** * Result Live Data @@ -20,9 +26,8 @@ class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFil */ fun fetchImages() { result.postValue(Result(CallbackStatus.FETCHING, arrayListOf())) - imageFileLoader.abortLoadImage() + scope.cancel() imageFileLoader.loadDeviceImages(object: ImageLoaderListener { - override fun onImageLoaded(images: ArrayList) { result.postValue(Result(CallbackStatus.SUCCESS, images)) } @@ -30,7 +35,14 @@ class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFil override fun onFailed(throwable: Throwable) { result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf())) } + },scope) + } - }) + /** + * Clear the coroutine task linked with context. + */ + override fun onCleared() { + scope.cancel() + super.onCleared() } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index a3db475717..ffacde0e72 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.CallbackStatus @@ -29,7 +30,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { * View Model Factory. */ var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null - @Inject set + @Inject set /** @@ -67,7 +68,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) - folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) + folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener) gridLayoutManager = GridLayoutManager(context, columnCount()) with(root.selector_rv){ this.layoutManager = gridLayoutManager @@ -87,10 +88,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private fun handleResult(result: Result) { if(result.status is CallbackStatus.SUCCESS){ - val folders = arrayListOf() - for( i in 1..12) { - folders.add(Folder(i.toLong(), "Folder$i",result.images)) - } + val folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) folderAdapter.notifyDataSetChanged() selector_rv.visibility = View.VISIBLE diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt index 738c40e989..95cb8233ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt @@ -1,26 +1,97 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.content.ContentUris import android.content.Context -import android.net.Uri +import android.provider.MediaStore import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener import fr.free.nrw.commons.customselector.model.Image +import kotlinx.coroutines.* +import java.io.File +import kotlin.coroutines.CoroutineContext -class ImageFileLoader(val context: Context) { +class ImageFileLoader(val context: Context) : CoroutineScope{ /** - * Load Device Images. + * Coroutine context for fetching images. */ - fun loadDeviceImages(listener: ImageLoaderListener) { - var tempImage = Image(0, "temp", Uri.parse("http://www.google.com"), "path", 0, "bucket", "1223") - var array: ArrayList = ArrayList() - for(i in 1..100) { - array.add(tempImage) + override val coroutineContext: CoroutineContext = Dispatchers.Main + + /** + * Media paramerters required. + */ + private val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.BUCKET_ID, + MediaStore.Images.Media.BUCKET_DISPLAY_NAME) + + /** + * Load Device Images under coroutine. + */ + fun loadDeviceImages(listener: ImageLoaderListener, scope: CoroutineScope) { + launch(Dispatchers.Main) { + withContext(Dispatchers.IO) { + getImages(listener) + } } - listener.onImageLoaded(array) + } + - // todo load images from device using cursor. + /** + * Load the device images using cursor + */ + private fun getImages(listener:ImageLoaderListener) { + val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC") + if (cursor == null) { + listener.onFailed(NullPointerException()) + return + } + + val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID) + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + 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 images = arrayListOf() + if (cursor.moveToFirst()) { + do { + if (Thread.interrupted()) { + listener.onFailed(NullPointerException()) + return + } + val id = cursor.getLong(idColumn) + val name = cursor.getString(nameColumn) + val path = cursor.getString(dataColumn) + val bucketId = cursor.getLong(bucketIdColumn) + val bucketName = cursor.getString(bucketNameColumn) + + val file = + if (path == null || path.isEmpty()) { + null + } else try { + File(path) + } catch (ignored: Exception) { + null + } + + + if (file != null && file.exists()) { + if (id != null && name != null && path != null && bucketId != null && bucketName != null) { + val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) + val image = Image(id, name, uri, path, bucketId, bucketName) + images.add(image) + } + } + + } while (cursor.moveToNext()) + } + cursor.close() + listener.onImageLoaded(images) } + /** * Abort loading images. */ @@ -31,7 +102,6 @@ class ImageFileLoader(val context: Context) { /** * * TODO - * Runnable Thread for image loading. * Sha1 for image (original image). * */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index c22313d655..f4b5c99343 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -4,11 +4,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import fr.free.nrw.commons.R +import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.CallbackStatus import fr.free.nrw.commons.customselector.model.Result @@ -34,7 +34,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { * View model Factory. */ lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory - @Inject set + @Inject set /** * Image Adapter for recycle view. @@ -106,7 +106,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { if(result.status is CallbackStatus.SUCCESS){ val images = result.images if(images.isNotEmpty()) { - imageAdapter.init(images) + imageAdapter.init(ImageHelper.filterImages(images,bucketId)) selector_rv.visibility = View.VISIBLE } else{ diff --git a/app/src/main/res/layout/item_custom_selector_folder.xml b/app/src/main/res/layout/item_custom_selector_folder.xml index 077968c6a5..6d4df43075 100644 --- a/app/src/main/res/layout/item_custom_selector_folder.xml +++ b/app/src/main/res/layout/item_custom_selector_folder.xml @@ -19,30 +19,30 @@ android:background="@color/white" android:layout_height="match_parent"> - + android:background="@color/black" + android:alpha="0.15" /> diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml index eec1eb9d99..021f463bc6 100644 --- a/app/src/main/res/layout/item_custom_selector_image.xml +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -78,7 +78,7 @@ android:id="@+id/uploaded_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="visible" + android:visibility="gone" app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/> From c7dae69ddf45184b6713cb8ea60a78329906bd85 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Thu, 17 Jun 2021 14:59:27 +0530 Subject: [PATCH 07/28] [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> --- .../contributions/ContributionController.java | 10 +-- .../ContributionsListFragment.java | 54 ++++++------- .../customselector/helper/ImageHelper.kt | 30 ++++++- .../customselector/ui/adapter/ImageAdapter.kt | 78 +++++++++++++++++-- .../ui/selector/CustomSelectorActivity.kt | 53 +++++++++---- .../ui/selector/CustomSelectorViewModel.kt | 11 ++- .../nrw/commons/filepicker/Constants.java | 1 + .../nrw/commons/filepicker/FilePicker.java | 48 +++++++++++- 8 files changed, 225 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 778b1afdcc..27cef1c0f5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -8,7 +8,6 @@ import android.content.Intent; import androidx.annotation.NonNull; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; @@ -63,16 +62,11 @@ public void initiateGalleryPick(final Activity activity, final boolean allowMult * Initiate gallery picker with permission */ public void initiateCustomGalleryPickWithPermission(final Activity activity) { - boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true); - Intent intent = new Intent(activity,CustomSelectorActivity.class); - if (!useExtStorage) { - activity.startActivity(intent); - return; - } + setPickerConfiguration(activity,true); PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, - () -> activity.startActivity(intent), + () -> FilePicker.openCustomSelector(activity, 0), R.string.storage_permission_title, R.string.write_storage_permission_rationale); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index c2c6c40866..d2bceae1fe 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -269,34 +269,34 @@ private void setListeners() { }); } - @OnClick(R.id.fab_custom_gallery) - void launchCustomSelector(){ - controller.initiateCustomGalleryPickWithPermission(getActivity()); - } - - private void animateFAB(final boolean isFabOpen) { - this.isFabOpen = !isFabOpen; - if (fabPlus.isShown()) { - if (isFabOpen) { - fabPlus.startAnimation(rotate_backward); - fabCamera.startAnimation(fab_close); - fabGallery.startAnimation(fab_close); - fabCustomGallery.startAnimation(fab_close); - fabCamera.hide(); - fabGallery.hide(); - fabCustomGallery.hide(); - } else { - fabPlus.startAnimation(rotate_forward); - fabCamera.startAnimation(fab_open); - fabGallery.startAnimation(fab_open); - fabCustomGallery.startAnimation(fab_open); - fabCamera.show(); - fabGallery.show(); - fabCustomGallery.show(); - } - this.isFabOpen = !isFabOpen; + @OnClick(R.id.fab_custom_gallery) + void launchCustomSelector(){ + controller.initiateCustomGalleryPickWithPermission(getActivity()); + } + + private void animateFAB(final boolean isFabOpen) { + this.isFabOpen = !isFabOpen; + if (fabPlus.isShown()) { + if (isFabOpen) { + fabPlus.startAnimation(rotate_backward); + fabCamera.startAnimation(fab_close); + fabGallery.startAnimation(fab_close); + fabCustomGallery.startAnimation(fab_close); + fabCamera.hide(); + fabGallery.hide(); + fabCustomGallery.hide(); + } else { + fabPlus.startAnimation(rotate_forward); + fabCamera.startAnimation(fab_open); + fabGallery.startAnimation(fab_open); + fabCustomGallery.startAnimation(fab_open); + fabCamera.show(); + fabGallery.show(); + fabCustomGallery.show(); + } + this.isFabOpen = !isFabOpen; + } } - } /** * Shows welcome message if user has no contributions yet i.e. new user. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 1b676b6e2c..9228dc5acb 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -11,7 +11,7 @@ import kotlin.collections.ArrayList import kotlin.collections.LinkedHashMap /** - * Image Helper object, includes all the static functions required by custom selector + * Image Helper object, includes all the static functions required by custom selector. */ object ImageHelper { @@ -49,6 +49,34 @@ object ImageHelper { return filteredImages } + /** + * getIndex: Returns the index of image in given list. + */ + fun getIndex(list: ArrayList, image: Image): Int { + return list.indexOf(image) + } + + /** + * Gets the list of indices from the master list. + */ + fun getIndexList(list: ArrayList, masterList: ArrayList): ArrayList { + + /** + * TODO + * Can be optimised as masterList is sorted by time. + */ + + val indexes = arrayListOf() + for(image in list) { + val index = getIndex(masterList,image) + if (index == -1) { + continue + } + indexes.add(index) + } + return indexes + } + /** * Generates the file sha1 from file input stream. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 53de6de773..a38200463d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -10,6 +10,7 @@ import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -26,6 +27,16 @@ class ImageAdapter( RecyclerViewAdapter(context) { + /** + * ImageSelectedOrUpdated payload class. + */ + class ImageSelectedOrUpdated + + /** + * ImageUnselected payload class. + */ + class ImageUnselected + /** * Currently selected images. */ @@ -49,18 +60,41 @@ class ImageAdapter( */ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val image=images[position] - // todo load image thumbnail, set selected view. + val selectedIndex = ImageHelper.getIndex(selectedImages,image) + val isSelected = selectedIndex != -1 + if(isSelected){ + holder.itemSelected(selectedIndex+1) + } + else { + holder.itemUnselected(); + } Glide.with(context).load(image.uri).into(holder.image) holder.itemView.setOnClickListener { - selectOrRemoveImage(image, position) + selectOrRemoveImage(holder, position) } } /** * Handle click event on an image, update counter on images. */ - private fun selectOrRemoveImage(image:Image, position:Int){ - // todo select the image if not selected and remove it if already selected + private fun selectOrRemoveImage(holder:ImageViewHolder, position:Int){ + val clickedIndex = ImageHelper.getIndex(selectedImages,images[position]) + if (clickedIndex != -1) { + selectedImages.removeAt(clickedIndex) + notifyItemChanged(position,ImageUnselected()) + val indexes = ImageHelper.getIndexList(selectedImages, images) + for (index in indexes) { + notifyItemChanged(index, ImageSelectedOrUpdated()) + } + } else { + /** + * TODO + * Show toast on tapping an uploaded item. + */ + selectedImages.add(images[position]) + notifyItemChanged(position, ImageSelectedOrUpdated()) + } + imageSelectListener.onSelectedImagesChanged(selectedImages) } /** @@ -90,9 +124,39 @@ class ImageAdapter( */ class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { val image: ImageView = itemView.findViewById(R.id.image_thumbnail) - val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) - val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) - val selectedGroup: Group = itemView.findViewById(R.id.selected_group) + private val selectedNumber: TextView = itemView.findViewById(R.id.selected_count) + private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) + private val selectedGroup: Group = itemView.findViewById(R.id.selected_group) + + /** + * Item selected view. + */ + fun itemSelected(index: Int) { + selectedGroup.visibility = View.VISIBLE + selectedNumber.text = index.toString() + } + + /** + * Item Unselected view. + */ + fun itemUnselected() { + selectedGroup.visibility = View.GONE + } + + /** + * Item Uploaded view. + */ + fun itemUploaded() { + uploadedGroup.visibility = View.VISIBLE + } + + /** + * Item Not Uploaded view. + */ + fun itemNotUploaded() { + uploadedGroup.visibility = View.GONE + } + } /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 1ab30f67ea..099c89a862 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -1,5 +1,7 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.widget.ImageButton import android.widget.TextView @@ -10,6 +12,7 @@ import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.theme.BaseActivity +import java.io.File import javax.inject.Inject class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { @@ -73,7 +76,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL val back : ImageButton = findViewById(R.id.back) back.setOnClickListener { onBackPressed() } - // todo done listener. + val done : ImageButton = findViewById(R.id.done) + done.setOnClickListener { onDone() } } /** @@ -91,9 +95,44 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL * override Selected Images Change, update view model selected images. */ override fun onSelectedImagesChanged(selectedImages: ArrayList) { + viewModel.selectedImages.value = selectedImages // todo update selected images in view model. } + /** + * OnDone clicked. + * Get the selected images. Remove any non existent file, forward the data to finish selector. + */ + fun onDone() { + val selectedImages = viewModel.selectedImages.value + if(selectedImages.isNullOrEmpty()) { + finishPickImages(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++ + } + finishPickImages(selectedImages) + } + + /** + * finishPickImages, Load the data to the intent and set result. + * Finish the activity. + */ + private fun finishPickImages(images: ArrayList) { + val data = Intent() + data.putParcelableArrayListExtra("Images", images) + setResult(Activity.RESULT_OK, data) + finish() + } + /** * Back pressed. * Change toolbar title. @@ -106,16 +145,4 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL } } - - /** - * - * TODO - * Permission check. - * OnDone - * Activity Result. - * - * - */ - - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt index 26b8033bac..4f56a808b5 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context -import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener @@ -14,10 +13,18 @@ import kotlinx.coroutines.cancel class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { + /** + * Scope for coroutine task (image fetch). + */ private val scope = CoroutineScope(Dispatchers.Main) /** - * Result Live Data + * Stores selected images. + */ + var selectedImages: MutableLiveData> = MutableLiveData() + + /** + * Result Live Data. */ val result = MutableLiveData(Result(CallbackStatus.IDLE, arrayListOf())) diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java index 83d838bc2a..4b5b91e684 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java @@ -10,6 +10,7 @@ interface RequestCodes { int FILE_PICKER_IMAGE_IDENTIFICATOR = 0b1101101100; //876 int SOURCE_CHOOSER = 1 << 15; + int PICK_PICTURE_FROM_CUSTOM_SELECTOR = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 10); int PICK_PICTURE_FROM_DOCUMENTS = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 11); int PICK_PICTURE_FROM_GALLERY = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 12); int TAKE_PICTURE = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 13); diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java index 698e2d51f2..6d516abd96 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java @@ -15,6 +15,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import fr.free.nrw.commons.customselector.model.Image; +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; @@ -51,6 +53,11 @@ private static Intent createGalleryIntent(@NonNull Context context, int type) { .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()); } + private static Intent createCustomSelectorIntent(@NonNull Context context, int type) { + storeType(context, type); + return new Intent(context, CustomSelectorActivity.class); + } + private static Intent createCameraForImageIntent(@NonNull Context context, int type) { storeType(context, type); @@ -97,6 +104,14 @@ public static void openGallery(Activity activity, int type) { activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_GALLERY); } + /** + * Opens Custom Selector + */ + public static void openCustomSelector(Activity activity, int type) { + Intent intent = createCustomSelectorIntent(activity, type); + activity.startActivityForResult(intent, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR); + } + /** * Opens the camera app to pick image clicked by user */ @@ -135,12 +150,15 @@ public static void handleActivityResult(int requestCode, int resultCode, Intent if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY || requestCode == RequestCodes.TAKE_PICTURE || requestCode == RequestCodes.CAPTURE_VIDEO || - requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS) { + requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS || + requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) { if (resultCode == Activity.RESULT_OK) { if (requestCode == RequestCodes.PICK_PICTURE_FROM_DOCUMENTS && !isPhoto(data)) { onPictureReturnedFromDocuments(data, activity, callbacks); } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_GALLERY && !isPhoto(data)) { onPictureReturnedFromGallery(data, activity, callbacks); + } else if (requestCode == RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) { + onPictureReturnedFromCustomSelector(data, activity, callbacks); } else if (requestCode == RequestCodes.TAKE_PICTURE) { onPictureReturnedFromCamera(activity, callbacks); } else if (requestCode == RequestCodes.CAPTURE_VIDEO) { @@ -197,6 +215,32 @@ private static void onPictureReturnedFromDocuments(Intent data, Activity activit } } + private static void onPictureReturnedFromCustomSelector(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { + try { + List files = getFilesFromCustomSelector(data, activity); + callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); + } catch (Exception e) { + e.printStackTrace(); + callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); + } + } + + private static List getFilesFromCustomSelector(Intent data, Activity activity) throws IOException, SecurityException { + List files = new ArrayList<>(); + ArrayList images = data.getParcelableArrayListExtra("Images"); + for(Image image : images) { + Uri uri = image.getUri(); + UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); + files.add(file); + } + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, files); + } + + return files; + } + private static void onPictureReturnedFromGallery(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { try { List files = getFilesFromGalleryPictures(data, activity); @@ -301,7 +345,7 @@ public static FilePickerConfiguration configuration(@NonNull Context context) { public enum ImageSource { - GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO + GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR } public interface Callbacks { From 088f66ead066c0184b3325c76315d5dff32ae942 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 22 Jun 2021 03:53:30 +0530 Subject: [PATCH 08/28] [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code --- .../customselector/helper/ImageHelper.kt | 50 +------ .../ui/adapter/FolderAdapter.kt | 7 +- .../customselector/ui/adapter/ImageAdapter.kt | 24 +++- .../ui/selector/FolderFragment.kt | 7 +- .../ui/selector/ImageFragment.kt | 10 +- .../customselector/ui/selector/ImageLoader.kt | 131 +++++++++++++++++- .../nrw/commons/filepicker/PickedFiles.java | 4 +- .../free/nrw/commons/upload/FileProcessor.kt | 4 +- 8 files changed, 174 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 9228dc5acb..0a751d47bc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -1,15 +1,20 @@ package fr.free.nrw.commons.customselector.helper +import android.content.Context +import com.mapbox.android.core.FileUtils import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.filepicker.Constants import timber.log.Timber import java.io.* import java.math.BigInteger import java.security.MessageDigest import java.security.NoSuchAlgorithmException import kotlin.collections.ArrayList +import kotlin.collections.HashMap import kotlin.collections.LinkedHashMap + /** * Image Helper object, includes all the static functions required by custom selector. */ @@ -68,7 +73,7 @@ object ImageHelper { val indexes = arrayListOf() for(image in list) { - val index = getIndex(masterList,image) + val index = getIndex(masterList, image) if (index == -1) { continue } @@ -76,47 +81,4 @@ object ImageHelper { } return indexes } - - /** - * Generates the file sha1 from file input stream. - */ - fun generateSHA1(`is`: InputStream): String { - val digest: MessageDigest = try { - MessageDigest.getInstance("SHA1") - } catch (e: NoSuchAlgorithmException) { - Timber.e(e, "Exception while getting Digest") - return "" - } - val buffer = ByteArray(8192) - var read: Int - return try { - while (`is`.read(buffer).also { read = it } > 0) { - digest.update(buffer, 0, read) - } - val md5sum = digest.digest() - val bigInt = BigInteger(1, md5sum) - var output = bigInt.toString(16) - output = String.format("%40s", output).replace(' ', '0') - Timber.i("File SHA1: %s", output) - output - } catch (e: IOException) { - Timber.e(e, "IO Exception") - "" - } finally { - try { - `is`.close() - } catch (e: IOException) { - Timber.e(e, "Exception on closing input stream") - } - } - } - - /** - * Gets the file input stream from the file path. - */ - @Throws(FileNotFoundException::class) - fun getFileInputStream(filePath: String?): InputStream { - return FileInputStream(filePath) - } - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 5d28a46d1b..fb3e497940 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -11,7 +11,6 @@ import com.bumptech.glide.Glide import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder -import fr.free.nrw.commons.customselector.ui.selector.ImageLoader class FolderAdapter( /** @@ -23,12 +22,8 @@ class FolderAdapter( * Folder Click listener for click events. */ private val itemClickListener: FolderClickListener -) : RecyclerViewAdapter(context) { - /** - * Image Loader for loading images. - */ - private val imageLoader = ImageLoader() +) : RecyclerViewAdapter(context) { /** * List of folders. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index a38200463d..9029e03bc2 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -6,6 +6,7 @@ import fr.free.nrw.commons.R import android.view.View import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -13,6 +14,7 @@ import com.bumptech.glide.Glide import fr.free.nrw.commons.customselector.helper.ImageHelper 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 class ImageAdapter( /** @@ -23,7 +25,13 @@ class ImageAdapter( /** * Image select listener for click events on image. */ - private var imageSelectListener: ImageSelectListener ): + private var imageSelectListener: ImageSelectListener, + + /** + * ImageLoader queries images. + */ + private var imageLoader: ImageLoader +): RecyclerViewAdapter(context) { @@ -48,7 +56,7 @@ class ImageAdapter( private var images: ArrayList = ArrayList() /** - * create View holder. + * Create View holder. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) @@ -69,6 +77,7 @@ class ImageAdapter( holder.itemUnselected(); } Glide.with(context).load(image.uri).into(holder.image) + imageLoader.queryAndSetView(holder,image) holder.itemView.setOnClickListener { selectOrRemoveImage(holder, position) } @@ -87,12 +96,12 @@ class ImageAdapter( notifyItemChanged(index, ImageSelectedOrUpdated()) } } else { - /** - * TODO - * Show toast on tapping an uploaded item. - */ + if(holder.isItemUploaded()){ + Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show() + } else { selectedImages.add(images[position]) notifyItemChanged(position, ImageSelectedOrUpdated()) + } } imageSelectListener.onSelectedImagesChanged(selectedImages) } @@ -150,6 +159,9 @@ class ImageAdapter( uploadedGroup.visibility = View.VISIBLE } + fun isItemUploaded():Boolean { + return uploadedGroup.visibility == View.VISIBLE + } /** * Item Not Uploaded view. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index ffacde0e72..1d5901c9d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -12,9 +12,10 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.CallbackStatus -import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileProcessor import kotlinx.android.synthetic.main.fragment_custom_selector.* import kotlinx.android.synthetic.main.fragment_custom_selector.view.* import javax.inject.Inject @@ -32,7 +33,11 @@ class FolderFragment : CommonsDaggerSupportFragment() { var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null @Inject set + var fileProcessor: FileProcessor? = null + @Inject set + var mediaClient: MediaClient? = null + @Inject set /** * Folder Adapter. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index f4b5c99343..a2de0ed294 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -36,6 +36,12 @@ class ImageFragment: CommonsDaggerSupportFragment() { lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory @Inject set + /** + * Image loader for adapter. + */ + var imageLoader: ImageLoader? = null + @Inject set + /** * Image Adapter for recycle view. */ @@ -84,7 +90,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) - imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener) + imageAdapter = ImageAdapter(requireActivity(), activity as ImageSelectListener, imageLoader!!) gridLayoutManager = GridLayoutManager(context,getSpanCount()) with(root.selector_rv){ this.layoutManager = gridLayoutManager @@ -118,6 +124,8 @@ class ImageFragment: CommonsDaggerSupportFragment() { /** * getSpanCount for GridViewManager. + * + * @return spanCount. */ private fun getSpanCount(): Int { return 3 diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 22da8cbbb9..a3ae38e348 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -1,7 +1,136 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.content.Context +import androidx.exifinterface.media.ExifInterface +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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.IOException +import java.util.* +import javax.inject.Inject +import kotlin.collections.HashMap + /** * Image Loader class, loads images, depending on API results. */ -class ImageLoader { +class ImageLoader @Inject constructor( + + /** + * MediaClient for SHA1 query. + */ + var mediaClient: MediaClient, + + /** + * FileProcessor to pre-process the file. + */ + var fileProcessor: FileProcessor, + + /** + * File Utils Wrapper for SHA1 + */ + var fileUtilsWrapper: FileUtilsWrapper, + + /** + * Context for coroutine. + */ + val context: Context) { + + /** + * Maps to facilitate image query. + */ + private var mapImageSHA1: HashMap = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = HashMap() + + /** + * Query image and setUp the view. + */ + fun queryAndSetView(holder: ImageViewHolder, image: Image){ + + /** + * Recycler view uses same view holder, so we can identify the latest query image from holder. + */ + mapHolderImage[holder] = image + holder.itemNotUploaded() + + CoroutineScope(Dispatchers.Main).launch { + var value = false + withContext(Dispatchers.Default) { + if(mapHolderImage[holder] != image) { + // View holder has a new query image, terminate this query. + return@withContext + } + val sha1 = getSHA1(image) + if(mapHolderImage[holder] != image) { + // View holder has a new query image, terminate this query. + return@withContext + } + value = querySHA1(sha1) + } + if(mapHolderImage[holder] == image) { + // View holder and latest query image match, setup the view. + if (value) { + holder.itemUploaded() + } else { + holder.itemNotUploaded() + } + } + } + } + + /** + * Query SHA1, return result if previously queried, otherwise start a new query. + * + * @return Query result. + */ + private fun querySHA1(SHA1: String): Boolean { + if(mapResult[SHA1] != null) { + return mapResult[SHA1]!! + } + val isUploaded = mediaClient.checkFileExistsUsingSha(SHA1).blockingGet() + mapResult[SHA1] = isUploaded + return isUploaded + } + + /** + * Get SHA1, return SHA1 if available, otherwise generate and store the SHA1. + * + * @return sha1 of the image + */ + private fun getSHA1(image: Image): String{ + if(mapImageSHA1[image] != null) { + return mapImageSHA1[image]!! + } + val sha1 = generateModifiedSHA1(image); + mapImageSHA1[image] = sha1; + return sha1; + } + + /** + * Generate Modified SHA1 using present Exif settings. + * + * @return modified sha1 + */ + private fun generateModifiedSHA1(image: Image) : String { + 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() + return sha1 + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java index 01e68c940a..c5eb101bc3 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java @@ -25,7 +25,7 @@ import timber.log.Timber; -class PickedFiles implements Constants { +public class PickedFiles implements Constants { private static String getFolderName(@NonNull Context context) { return FilePicker.configuration(context).getFolderName(); @@ -104,7 +104,7 @@ static void scanCopiedImages(Context context, List copiedImages) { }); } - static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions + public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri); File directory = tempImageDirectory(context); File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index ff3f63eb86..5ad6952ee0 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -77,7 +77,7 @@ class FileProcessor @Inject constructor( * * @return tags to be redacted */ - private fun getExifTagsToRedact(): Set { + fun getExifTagsToRedact(): Set { val prefManageEXIFTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() val redactTags: Set = @@ -91,7 +91,7 @@ class FileProcessor @Inject constructor( * @param exifInterface ExifInterface object * @param redactTags tags to be redacted */ - private fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set) { + fun redactExifTags(exifInterface: ExifInterface?, redactTags: Set) { compositeDisposable.add( Observable.fromIterable(redactTags) .flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) } From 1749a7b8819aeac4e3f53dfea112fcffd90299df Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 29 Jun 2021 10:09:00 +0530 Subject: [PATCH 09/28] [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success --- .../nrw/commons/contributions/Contribution.kt | 6 +- .../customselector/database/UploadedDao.kt | 88 +++++++++++ .../customselector/database/UploadedStatus.kt | 39 +++++ .../customselector/ui/selector/ImageLoader.kt | 148 ++++++++++++++---- .../fr/free/nrw/commons/db/AppDatabase.kt | 5 +- .../commons/di/CommonsApplicationModule.java | 15 +- .../free/nrw/commons/upload/UploadItem.java | 9 +- .../free/nrw/commons/upload/UploadModel.java | 3 +- .../nrw/commons/upload/worker/UploadWorker.kt | 23 +++ 9 files changed, 297 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt index b46d1087eb..6b895232f9 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt @@ -39,7 +39,8 @@ data class Contribution constructor( var dataLength: Long = 0, var dateCreated: Date? = null, var dateModified: Date? = null, - var hasInvalidLocation : Int = 0 + var hasInvalidLocation : Int = 0, + var contentUri: Uri? = null ) : Parcelable { fun completeWith(media: Media): Contribution { @@ -64,7 +65,8 @@ data class Contribution constructor( decimalCoords = item.gpsCoords.decimalCoords, dateCreatedSource = "", depictedItems = depictedItems, - wikidataPlace = from(item.place) + wikidataPlace = from(item.place), + contentUri = item.contentUri ) /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt new file mode 100644 index 0000000000..d9f2fc55eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -0,0 +1,88 @@ +package fr.free.nrw.commons.customselector.database + +import androidx.room.* +import kotlinx.coroutines.runBlocking +import java.util.* +import kotlinx.coroutines.* + +/** + * UploadedStatusDao for Custom Selector. + */ +@Dao +abstract class UploadedStatusDao { + + /** + * Insert into uploaded status. + */ + @Insert( onConflict = OnConflictStrategy.REPLACE ) + abstract suspend fun insert(uploadedStatus: UploadedStatus) + + /** + * Update uploaded status entry. + */ + @Update + abstract suspend fun update(uploadedStatus: UploadedStatus) + + /** + * Delete uploaded status entry. + */ + @Delete + abstract suspend fun delete(uploadedStatus: UploadedStatus) + + /** + * Get All entries from the uploaded status table. + */ + @Query("SELECT * FROM uploaded_table") + abstract suspend fun getAll() : List + + /** + * Query uploaded status with image sha1. + */ + @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") + abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus + + /** + * Query uploaded status with modified image sha1. + */ + @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") + abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus + + /** + * Asynchronous insert into uploaded status table. + */ + fun insertUploaded(uploadedStatus: UploadedStatus) = runBlocking { + async { + uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + insert(uploadedStatus) + }.await() + } + + /** + * Asynchronous delete from uploaded status table. + */ + fun deleteUploaded(uploadedStatus: UploadedStatus) = runBlocking { + async { delete(uploadedStatus) } + } + + /** + * Asynchronous update entry in uploaded status table. + */ + fun updateUploaded(uploadedStatus: UploadedStatus) = runBlocking { + async { update(uploadedStatus) } + } + + /** + * Asynchronous image sha1 query. + */ + fun getUploadedFromImageSHA1(imageSHA1: String) = runBlocking { + async { getFromImageSHA1(imageSHA1) }.await() + } + + /** + * Asynchronous modified image sha1 query. + */ + fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String) = runBlocking { + async { getFromModifiedImageSHA1(modifiedImageSHA1) }.await() + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt new file mode 100644 index 0000000000..93e4a82437 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatus.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.customselector.database + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import java.util.* + +/** + * Entity class for Uploaded Status. + */ +@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)]) +data class UploadedStatus( + + /** + * Original image sha1. + */ + @PrimaryKey + val imageSHA1 : String, + + /** + * Modified image sha1 (after exif changes). + */ + val modifiedImageSHA1 : String, + + /** + * imageSHA1 query result from API. + */ + var imageResult : Boolean, + + /** + * modifiedImageSHA1 query result from API. + */ + var modifiedImageResult : Boolean, + + /** + * lastUpdated for data validation. + */ + var lastUpdated : Date? = null +) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index a3ae38e348..f2d4d5709f 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -1,7 +1,10 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context +import android.net.Uri import androidx.exifinterface.media.ExifInterface +import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusDao 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 @@ -14,6 +17,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException +import java.net.UnknownHostException import java.util.* import javax.inject.Inject import kotlin.collections.HashMap @@ -38,22 +42,28 @@ class ImageLoader @Inject constructor( */ var fileUtilsWrapper: FileUtilsWrapper, + /** + * UploadedStatusDao for cache query. + */ + var uploadedStatusDao: UploadedStatusDao, + /** * Context for coroutine. */ - val context: Context) { + val context: Context +) { /** * Maps to facilitate image query. */ - private var mapImageSHA1: HashMap = HashMap() - private var mapHolderImage : HashMap = HashMap() - private var mapResult: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = HashMap() /** * Query image and setUp the view. */ - fun queryAndSetView(holder: ImageViewHolder, image: Image){ + fun queryAndSetView(holder: ImageViewHolder, image: Image) { /** * Recycler view uses same view holder, so we can identify the latest query image from holder. @@ -62,26 +72,46 @@ class ImageLoader @Inject constructor( holder.itemNotUploaded() CoroutineScope(Dispatchers.Main).launch { - var value = false + + var result : Result = Result.NOTFOUND withContext(Dispatchers.Default) { - if(mapHolderImage[holder] != image) { - // View holder has a new query image, terminate this query. - return@withContext - } - val sha1 = getSHA1(image) - if(mapHolderImage[holder] != image) { - // View holder has a new query image, terminate this query. - return@withContext + + if (mapHolderImage[holder] == image) { + val imageSHA1 = getImageSHA1(image.uri) + val uploadedStatus = uploadedStatusDao.getUploadedFromImageSHA1(imageSHA1) + + val sha1 = uploadedStatus?.let { + result = getResultFromUploadedStatus(uploadedStatus) + uploadedStatus.modifiedImageSHA1 + } ?: run { + if(mapHolderImage[holder] == image) { + getSHA1(image) + } else { + "" + } + } + + if (mapHolderImage[holder] == image && + result in arrayOf(Result.NOTFOUND, Result.INVALID) && + sha1.isNotEmpty()) { + // Query original image. + result = querySHA1(imageSHA1) + if( result is Result.TRUE ) { + // Original image found. + insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false) + } + else { + // Original image not found, query modified image. + result = querySHA1(sha1) + if (result != Result.ERROR) { + insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE) + } + } + } } - value = querySHA1(sha1) } if(mapHolderImage[holder] == image) { - // View holder and latest query image match, setup the view. - if (value) { - holder.itemUploaded() - } else { - holder.itemNotUploaded() - } + if (result is Result.TRUE) holder.itemUploaded() else holder.itemNotUploaded() } } } @@ -91,13 +121,26 @@ class ImageLoader @Inject constructor( * * @return Query result. */ - private fun querySHA1(SHA1: String): Boolean { - if(mapResult[SHA1] != null) { - return mapResult[SHA1]!! + private fun querySHA1(SHA1: String): Result { + mapResult[SHA1]?.let{ + return it + } + var result : Result = Result.FALSE + try { + if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { + mapResult[SHA1] = Result.TRUE + result = Result.TRUE + } + } catch (e: Exception) { + if (e is UnknownHostException) { + // Handle no network connection. + Timber.e(e, "Network Connection Error") + } + result = Result.ERROR + e.printStackTrace() + } finally { + return result } - val isUploaded = mediaClient.checkFileExistsUsingSha(SHA1).blockingGet() - mapResult[SHA1] = isUploaded - return isUploaded } /** @@ -105,15 +148,45 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private fun getSHA1(image: Image): String{ - if(mapImageSHA1[image] != null) { - return mapImageSHA1[image]!! + private fun getSHA1(image: Image): String { + mapImageSHA1[image]?.let{ + return it } val sha1 = generateModifiedSHA1(image); mapImageSHA1[image] = sha1; return sha1; } + /** + * Insert into uploaded status table. + */ + private fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedImageSha1, imageResult, modifiedImageResult)) + } + + /** + * Get image sha1 from uri, used to retrieve the original image sha1. + */ + private fun getImageSHA1(uri: Uri): String { + return fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + } + + /** + * Get result data from database. + */ + private fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { + if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) { + return Result.TRUE + } else { + uploadedStatus.lastUpdated?.let { + if (it.date >= Calendar.getInstance().time.date - INVALIDATE_DAY_COUNT) { + return Result.FALSE + } + } + } + return Result.INVALID + } + /** * Generate Modified SHA1 using present Exif settings. * @@ -133,4 +206,19 @@ class ImageLoader @Inject constructor( return sha1 } + /** + * Sealed Result class. + */ + sealed class Result { + object TRUE : Result() + object FALSE : Result() + object INVALID : Result() + object NOTFOUND : Result() + object ERROR : Result() + } + + companion object { + const val INVALIDATE_DAY_COUNT: Int = 7 + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt index c408ea5f75..7f6ea70276 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt +++ b/app/src/main/java/fr/free/nrw/commons/db/AppDatabase.kt @@ -5,6 +5,8 @@ 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.upload.depicts.Depicts import fr.free.nrw.commons.upload.depicts.DepictsDao @@ -12,9 +14,10 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao * The database for accessing the respective DAOs * */ -@Database(entities = [Contribution::class, Depicts::class], version = 8, exportSchema = false) +@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class], version = 8, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun contributionDao(): ContributionDao abstract fun DepictsDao(): DepictsDao; + abstract fun UploadedStatusDao(): UploadedStatusDao; } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index bca71de983..7d9c061ffc 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -17,6 +17,7 @@ import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionDao; +import fr.free.nrw.commons.customselector.database.UploadedStatusDao; import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.db.AppDatabase; @@ -68,8 +69,8 @@ public CommonsApplicationModule(Context applicationContext) { } @Provides - public ImageFileLoader providesImageFileLoader() { - return new ImageFileLoader(this.applicationContext); + public ImageFileLoader providesImageFileLoader(Context context) { + return new ImageFileLoader(context); } @Provides @@ -250,13 +251,21 @@ public ContributionDao providesContributionsDao(AppDatabase appDatabase) { } /** - * Get the reference of DepictsDao class + * Get the reference of DepictsDao class. */ @Provides public DepictsDao providesDepictDao(AppDatabase appDatabase) { return appDatabase.DepictsDao(); } + /** + * Get the reference of UploadedStatus class. + */ + @Provides + public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) { + return appDatabase.UploadedStatusDao(); + } + @Provides public ContentResolver providesContentResolver(Context context){ return context.getContentResolver(); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java index 0487fd87fe..bed3e3454f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java @@ -23,6 +23,7 @@ public class UploadItem { private final String createdTimestampSource; private final BehaviorSubject imageQuality; private boolean hasInvalidLocation; + private final Uri contentUri; @SuppressLint("CheckResult") @@ -31,7 +32,8 @@ public class UploadItem { final ImageCoordinates gpsCoords, final Place place, final long createdTimestamp, - final String createdTimestampSource) { + final String createdTimestampSource, + final Uri contentUri) { this.createdTimestampSource = createdTimestampSource; uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail())); this.place = place; @@ -39,6 +41,7 @@ public class UploadItem { this.mimeType = mimeType; this.gpsCoords = gpsCoords; this.createdTimestamp = createdTimestamp; + this.contentUri = contentUri; imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); } @@ -66,8 +69,10 @@ public int getImageQuality() { return imageQuality.getValue(); } + public Uri getContentUri() { return contentUri; } + public void setImageQuality(final int imageQuality) { - this.imageQuality.onNext(imageQuality); + this.imageQuality.onNext(imageQuality); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index cf72fa5d63..1d1b7117fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -106,7 +106,8 @@ private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, final UploadItem uploadItem = new UploadItem( Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, - createdTimestampSource); + createdTimestampSource, + uploadableFile.getContentUri()); if (place != null) { uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place)); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index ad0c08c4cf..8f9bc9504b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -18,9 +18,12 @@ import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.contributions.ChunkInfo 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.di.ApplicationlessInjection import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.upload.StashUploadState import fr.free.nrw.commons.upload.UploadClient import fr.free.nrw.commons.upload.UploadResult @@ -51,12 +54,18 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : @Inject lateinit var contributionDao: ContributionDao + @Inject + lateinit var uploadedStatusDao: UploadedStatusDao + @Inject lateinit var uploadClient: UploadClient @Inject lateinit var mediaClient: MediaClient + @Inject + lateinit var fileUtilsWrapper: FileUtilsWrapper + private val PROCESSING_UPLOADS_NOTIFICATION_TAG = BuildConfig.APPLICATION_ID + " : upload_tag" private val PROCESSING_UPLOADS_NOTIFICATION_ID = 101 @@ -417,6 +426,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : .blockingGet() contributionFromUpload.dateModified=Date() contributionDao.deleteAndSaveContribution(contribution, contributionFromUpload) + + // Upload success, save to uploaded status. + saveIntoUploadedStatus(contribution) + } + + /** + * Save to uploadedStatusDao. + */ + private fun saveIntoUploadedStatus(contribution: Contribution) { + contribution.contentUri?.let { + val imageSha1 = fileUtilsWrapper.getSHA1(appContext.contentResolver.openInputStream(it)) + val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) + uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedSha1, imageSha1 == modifiedSha1, true)); + } } private fun findUniqueFileName(fileName: String): String { From 9488737f3e2ff92289239af485751e9a2c468c9b Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 30 Jun 2021 11:53:04 +0530 Subject: [PATCH 10/28] Image Helper test (#4485) --- .../customselector/helper/ImageHelperTest.kt | 55 +++++++++++++++++++ .../nrw/commons/filepicker/FilePickerTest.kt | 10 ++++ 2 files changed, 65 insertions(+) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt new file mode 100644 index 0000000000..2fe382368f --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/helper/ImageHelperTest.kt @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.customselector.helper + +import android.net.Uri +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import org.junit.jupiter.api.Assertions.* + +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock + +/** + * Custom Selector Image Helper Test + */ +internal class ImageHelperTest { + + var uri: Uri = mock(Uri::class.java) + private val folderImage1 = Image(1, "image1", uri, "abc/abc", 1, "bucket1") + private val folderImage2 = Image(2, "image1", uri, "xyz/xyz", 2, "bucket2") + private val mockImageList = ArrayList(listOf(folderImage1, folderImage2)) + private val folderImageList1 = ArrayList(listOf(folderImage1)) + private val folderImageList2 = ArrayList(listOf(folderImage2)) + + /** + * Test folder list from images. + */ + @Test + fun folderListFromImages() { + val folderList = ArrayList(listOf(Folder(1, "bucket1", folderImageList1), Folder(2, "bucket2", folderImageList2))) + assertEquals(folderList, ImageHelper.folderListFromImages(mockImageList)) + } + + /** + * Test filter images. + */ + @Test + fun filterImages() { + assertEquals(folderImageList1, ImageHelper.filterImages(mockImageList, 1)) + } + + /** + * Test get index from image list. + */ + @Test + fun getIndex() { + assertEquals(1,ImageHelper.getIndex(mockImageList, folderImage2)) + } + + /** + * Test get index list. + */ + @Test + fun getIndexList() { + assertEquals(ArrayList(listOf(0)), ImageHelper.getIndexList(mockImageList, folderImageList2)) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt index b9712df04c..ae841cd1cd 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/FilePickerTest.kt @@ -54,4 +54,14 @@ class FilePickerTest { verify(activity).startActivityForResult(ArgumentMatchers.anyObject(), requestCodeCaptor?.capture()?.toInt()!!) assertEquals(requestCodeCaptor?.value, RequestCodes.TAKE_PICTURE) } + + @Test + fun testOpenCustomSelectorRequestCode() { + `when`(PreferenceManager.getDefaultSharedPreferences(activity)).thenReturn(sharedPref) + `when`(sharedPref.edit()).thenReturn(sharedPreferencesEditor) + `when`(sharedPref.edit().putInt("type", 0)).thenReturn(sharedPreferencesEditor) + FilePicker.openCustomSelector(activity, 0) + verify(activity).startActivityForResult(ArgumentMatchers.anyObject(), requestCodeCaptor?.capture()?.toInt()!!) + assertEquals(requestCodeCaptor?.value, RequestCodes.PICK_PICTURE_FROM_CUSTOM_SELECTOR) + } } \ No newline at end of file From e10e6662b0bd2402d8c95d657b9d7b8807baf71c Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sat, 3 Jul 2021 11:32:54 +0530 Subject: [PATCH 11/28] [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test --- .../ui/adapter/FolderAdapterTest.kt | 81 ++++++++++++ .../ui/adapter/ImageAdapterTest.kt | 121 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt new file mode 100644 index 0000000000..6a6271b96e --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt @@ -0,0 +1,81 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import fr.free.nrw.commons.R +import android.content.Context +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.widget.GridLayout +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.listeners.FolderClickListener +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Custom Selector Folder Adapter Test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class FolderAdapterTest { + + private var uri: Uri = Mockito.mock(Uri::class.java) + private lateinit var activity: CustomSelectorActivity + private lateinit var folderAdapter: FolderAdapter + private lateinit var image: Image + private lateinit var folder: Folder + private lateinit var folderList: ArrayList + + @Before + @Throws(Exception::class) + fun setUp() { + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get() + image = Image(1, "image", uri, "abc/abc", 1, "bucket1") + folder = Folder(1, "bucket1", ArrayList(listOf(image))) + folderList = ArrayList(listOf(folder)) + folderAdapter = FolderAdapter(activity, activity as FolderClickListener) + } + + /** + * Test on create view holder. + */ + @Test + fun onCreateViewHolder() { + folderAdapter.createViewHolder(GridLayout(activity), 0) + } + + /** + * Test on bind view holder. + */ + @Test + fun onBindViewHolder() { + folderAdapter.init(folderList) + val inflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val listItemView: View = inflater.inflate(R.layout.item_custom_selector_folder, null, false) + folderAdapter.onBindViewHolder(FolderAdapter.FolderViewHolder(listItemView), 0) + } + + /** + * Test init. + */ + @Test + fun init() { + folderAdapter.init(folderList) + } + + /** + * Test get item count. + */ + @Test + fun getItemCount() { + assertEquals(0, folderAdapter.itemCount) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt new file mode 100644 index 0000000000..8de08a2d32 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.customselector.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.GridLayout +import fr.free.nrw.commons.R +import fr.free.nrw.commons.TestCommonsApplication +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 org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions +import org.junit.runner.RunWith +import org.mockito.* +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.lang.reflect.Field + +/** + * Custom Selector image adapter test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class ImageAdapterTest { + @Mock + private lateinit var image: Image + @Mock + private lateinit var imageLoader: ImageLoader + @Mock + private lateinit var imageSelectListener: ImageSelectListener + + private lateinit var activity: CustomSelectorActivity + private lateinit var imageAdapter: ImageAdapter + private lateinit var images : ArrayList + private lateinit var holder: ImageAdapter.ImageViewHolder + private lateinit var selectedImageField: Field + + /** + * Set up variables. + */ + @Before + @Throws(Exception::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get() + imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader) + images = ArrayList() + + val inflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val listItemView: View = inflater.inflate(R.layout.item_custom_selector_image, null, false) + holder = ImageAdapter.ImageViewHolder(listItemView) + + selectedImageField = imageAdapter.javaClass.getDeclaredField("selectedImages") + selectedImageField.isAccessible = true + } + + /** + * Test on create view holder. + */ + @Test + fun onCreateViewHolder() { + imageAdapter.createViewHolder(GridLayout(activity), 0) + } + + /** + * Test on bind view holder. + */ + @Test + fun onBindViewHolder() { + // Parameters. + images.add(image) + imageAdapter.init(images) + + // Test conditions. + imageAdapter.onBindViewHolder(holder, 0) + selectedImageField.set(imageAdapter, images) + imageAdapter.onBindViewHolder(holder, 0) + } + + /** + * Test init. + */ + @Test + fun init() { + imageAdapter.init(images) + } + + /** + * Test private function select or remove image. + */ + @Test + fun selectOrRemoveImage() { + // Access function + val func = imageAdapter.javaClass.getDeclaredMethod("selectOrRemoveImage", ImageAdapter.ImageViewHolder::class.java, Int::class.java) + func.isAccessible = true + + // Parameters + images.addAll(listOf(image, image)) + imageAdapter.init(images) + + // Test conditions + holder.itemUploaded() + func.invoke(imageAdapter, holder, 0) + holder.itemNotUploaded() + func.invoke(imageAdapter, holder, 0) + selectedImageField.set(imageAdapter, images) + func.invoke(imageAdapter, holder, 1) + } + + /** + * Test get item count. + */ + @Test + fun getItemCount() { + Assertions.assertEquals(0, imageAdapter.itemCount) + } +} \ No newline at end of file From 86ee96ccdd6f1abadab4cc858797238f10637c1c Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 14 Jul 2021 16:35:08 +0530 Subject: [PATCH 12/28] [GSoC] Master rebase. (#4505) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Fixes #3694 Pre-select places as depictions (#4452) * WikidataEditService: stop automatically adding WikidataPlace as a depiction When the user initiates the upload process from Nearby and also manually adds the place as a depiction, the depiction is added twice. Since this behavior is invisible to the user, it is being removed in preparation for auto-selecting the place as a depiction on the DepictsFragment screen. * DepictsFragment: auto-select place as a depiction Pass the Place reference from UploadActivity to DepictsFragment and select the corresponding DepictedItem. Using the place id, retrieve the corresponding Entity to create and select a DepictedItem. * UploadRepository: use Place from UploadItem to obtain a DepictedItem Instead of passing a Place object from UploadActivity to DepictsFragment and then passing the Place object up the chain to obtain and select a DepictedItem, retrieve the Place object directly within UploadRepository * DepictsFragment: select Place depiction when fragment becomes visible * UploadDepictsAdapter: make adapter aware of selection state Update selection state when recycled list items are automatically selected, preventing automatically selected items from appearing as unselected until they are forced to re-bind (i.e. after scrolling) * DepictsFragment: pre-select place depictions for all UploadItems If several images are selected and set to different places, pre-select all place depictions to reinforce the intended upload workflow philosophy (i.e. all images in a set are intended to be from/of the same place). See discussion in commons-app/apps-android-commons#3694 * DepictsFragment: scroll to the top every time list is updated * Typo fixes (#4461) * Fixed typo on class documentation of TextUtils * corrected comma placement in documentation * Fixed typos in comments * fix-issue-4424 (#4445) Co-authored-by: Pratham2305 * fix edit categories ui (#4414) Co-authored-by: Pratham2305 * Fix doom version issue (#4463) * Update db version * DBOpenHelper version update * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict * Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462) * Edited Project.xml to make indent size 4 * Changed files with 2 space indentation to use 4 space indentation * Edited Project.xml to make indent size 4 * changed files with 2 space indent to 4 space indent * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: neslihanturan * Use more understandable strings (#4470) * Fix #3792 Missing Column Issue (#4468) * Fix Missing Column Issue * Fix tests * Add UploadCategoriesFragment Unit Tests (#4473) * Panorama (#4467) * panoramic images fixed * made requested changes * Minor refactoring Co-authored-by: Aditya Srivastava * Localisation updates from https://translatewiki.net. * Main activity title is sometimes "Contributions", sometimes "Commons" (#4472) Fixes #4438 Replace == with equals() in onRestoreInstanceState * Localisation updates from https://translatewiki.net. * caption and description copyable (#4481) * Removed next button in quiz (#4382) * issues resolved * modification done * warning fixed * issues resolved * Button added * don't know function added * Button added * modification done * modification done * Localisation updates from https://translatewiki.net. * Added option to show and modify location while uploading (#4475) * initial commit * Everything done * minor modification * minor modification * Issues fixed * minor modifications * issue fixed * Issues fixed * Tutorial removed from log out state (#4479) * tutorial removed from log out state * Issue removed * Update changelog.md * Versioning for v3.0.2 * Fix #4482 (#4484) * Fix crash when image resolution is very high (#4483) * Localisation updates from https://translatewiki.net. * Add Contributions Fragment Unit Tests (#4490) * Fix Tests Errors (#4491) * Add UploadMediaDetailFragment Unit Tests (#4492) * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> * [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code * [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success * Image Helper test (#4485) * [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test * merge fix * rebase fix Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Co-authored-by: Brigham Byerly <6891883+byerlyb20@users.noreply.github.com> Co-authored-by: Jamie Brown Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: Nicolas Raoul Co-authored-by: Ashar --- app/src/main/res/values/attrs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f43772fb55..fb61e8d18a 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -51,6 +51,7 @@ + From 732b343b89c29e7e47b5d3b0821fce40289d1b05 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sun, 18 Jul 2021 05:47:01 +0530 Subject: [PATCH 13/28] [GSoC] Custom Selector Tests (#4494) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Fixes #3694 Pre-select places as depictions (#4452) * WikidataEditService: stop automatically adding WikidataPlace as a depiction When the user initiates the upload process from Nearby and also manually adds the place as a depiction, the depiction is added twice. Since this behavior is invisible to the user, it is being removed in preparation for auto-selecting the place as a depiction on the DepictsFragment screen. * DepictsFragment: auto-select place as a depiction Pass the Place reference from UploadActivity to DepictsFragment and select the corresponding DepictedItem. Using the place id, retrieve the corresponding Entity to create and select a DepictedItem. * UploadRepository: use Place from UploadItem to obtain a DepictedItem Instead of passing a Place object from UploadActivity to DepictsFragment and then passing the Place object up the chain to obtain and select a DepictedItem, retrieve the Place object directly within UploadRepository * DepictsFragment: select Place depiction when fragment becomes visible * UploadDepictsAdapter: make adapter aware of selection state Update selection state when recycled list items are automatically selected, preventing automatically selected items from appearing as unselected until they are forced to re-bind (i.e. after scrolling) * DepictsFragment: pre-select place depictions for all UploadItems If several images are selected and set to different places, pre-select all place depictions to reinforce the intended upload workflow philosophy (i.e. all images in a set are intended to be from/of the same place). See discussion in commons-app/apps-android-commons#3694 * DepictsFragment: scroll to the top every time list is updated * Typo fixes (#4461) * Fixed typo on class documentation of TextUtils * corrected comma placement in documentation * Fixed typos in comments * fix-issue-4424 (#4445) Co-authored-by: Pratham2305 * fix edit categories ui (#4414) Co-authored-by: Pratham2305 * Fix doom version issue (#4463) * Update db version * DBOpenHelper version update * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict * Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462) * Edited Project.xml to make indent size 4 * Changed files with 2 space indentation to use 4 space indentation * Edited Project.xml to make indent size 4 * changed files with 2 space indent to 4 space indent * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: neslihanturan * Use more understandable strings (#4470) * Fix #3792 Missing Column Issue (#4468) * Fix Missing Column Issue * Fix tests * Add UploadCategoriesFragment Unit Tests (#4473) * Panorama (#4467) * panoramic images fixed * made requested changes * Minor refactoring Co-authored-by: Aditya Srivastava * Localisation updates from https://translatewiki.net. * Main activity title is sometimes "Contributions", sometimes "Commons" (#4472) Fixes #4438 Replace == with equals() in onRestoreInstanceState * Localisation updates from https://translatewiki.net. * caption and description copyable (#4481) * Removed next button in quiz (#4382) * issues resolved * modification done * warning fixed * issues resolved * Button added * don't know function added * Button added * modification done * modification done * Localisation updates from https://translatewiki.net. * Added option to show and modify location while uploading (#4475) * initial commit * Everything done * minor modification * minor modification * Issues fixed * minor modifications * issue fixed * Issues fixed * Tutorial removed from log out state (#4479) * tutorial removed from log out state * Issue removed * Update changelog.md * Versioning for v3.0.2 * Fix #4482 (#4484) * Fix crash when image resolution is very high (#4483) * Localisation updates from https://translatewiki.net. * Add Contributions Fragment Unit Tests (#4490) * Fix Tests Errors (#4491) * Add UploadMediaDetailFragment Unit Tests (#4492) * Localisation updates from https://translatewiki.net. * Folder Fragment test * Folder Fragment test done * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> * [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code * [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success * Image Helper test (#4485) * [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test * Folder Fragment test * Folder Fragment test done * Fragment test complete * Added Custom Selector View Model Test * ImageFileLoaderTest * Update strings.xml * Custom Selector Activiy test * Image Loader Test Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Co-authored-by: Brigham Byerly <6891883+byerlyb20@users.noreply.github.com> Co-authored-by: Jamie Brown Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: Nicolas Raoul Co-authored-by: Ashar --- .../ui/selector/FolderFragment.kt | 19 +- .../ui/selector/ImageFragment.kt | 27 ++- .../customselector/ui/selector/ImageLoader.kt | 6 +- .../explore/ExploreListRootFragment.java | 2 +- .../ui/selector/CustomSelectorActivityTest.kt | 96 ++++++++ .../selector/CustomSelectorViewModelTest.kt | 41 ++++ .../ui/selector/FolderFragmentTest.kt | 130 +++++++++++ .../ui/selector/ImageFileLoaderTest.kt | 123 ++++++++++ .../ui/selector/ImageFragmentTest.kt | 135 +++++++++++ .../ui/selector/ImageLoaderTest.kt | 218 ++++++++++++++++++ app/src/test/resources/imageLoaderTestFile | 0 11 files changed, 786 insertions(+), 11 deletions(-) create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt create mode 100644 app/src/test/resources/imageLoaderTestFile diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index 1d5901c9d0..e43c0798cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -4,9 +4,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ProgressBar 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.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result @@ -16,7 +18,6 @@ import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.upload.FileProcessor -import kotlinx.android.synthetic.main.fragment_custom_selector.* import kotlinx.android.synthetic.main.fragment_custom_selector.view.* import javax.inject.Inject @@ -27,6 +28,12 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private var viewModel: CustomSelectorViewModel? = null + /** + * View Elements + */ + private var selectorRV: RecyclerView? = null + private var loader: ProgressBar? = null + /** * View Model Factory. */ @@ -75,6 +82,8 @@ class FolderFragment : CommonsDaggerSupportFragment() { val root = inflater.inflate(R.layout.fragment_custom_selector, container, false) folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener) gridLayoutManager = GridLayoutManager(context, columnCount()) + selectorRV = root.selector_rv + loader = root.loader with(root.selector_rv){ this.layoutManager = gridLayoutManager setHasFixedSize(true) @@ -96,9 +105,13 @@ class FolderFragment : CommonsDaggerSupportFragment() { val folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) folderAdapter.notifyDataSetChanged() - selector_rv.visibility = View.VISIBLE + selectorRV?.let { + it.visibility = View.VISIBLE + } + } + loader?.let { + it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } - loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index a2de0ed294..f1583c54fc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -4,9 +4,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ProgressBar 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.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener @@ -28,7 +30,13 @@ class ImageFragment: CommonsDaggerSupportFragment() { /** * View model for images. */ - private lateinit var viewModel: CustomSelectorViewModel + private var viewModel: CustomSelectorViewModel? = null + + /** + * View Elements + */ + private var selectorRV: RecyclerView? = null + private var loader: ProgressBar? = null /** * View model Factory. @@ -98,10 +106,13 @@ class ImageFragment: CommonsDaggerSupportFragment() { this.adapter = imageAdapter } - viewModel.result.observe(viewLifecycleOwner, Observer{ + viewModel?.result?.observe(viewLifecycleOwner, Observer{ handleResult(it) }) + selectorRV = root.selector_rv + loader = root.loader + return root } @@ -113,13 +124,19 @@ class ImageFragment: CommonsDaggerSupportFragment() { val images = result.images if(images.isNotEmpty()) { imageAdapter.init(ImageHelper.filterImages(images,bucketId)) - selector_rv.visibility = View.VISIBLE + selectorRV?.let{ + it.visibility = View.VISIBLE + } } else{ - selector_rv.visibility = View.GONE + selectorRV?.let{ + it.visibility = View.GONE + } } } - loader.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + loader?.let { + it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE + } } /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index f2d4d5709f..5680cc7753 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -19,6 +19,7 @@ import timber.log.Timber import java.io.IOException import java.net.UnknownHostException import java.util.* +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.collections.HashMap @@ -179,7 +180,8 @@ class ImageLoader @Inject constructor( return Result.TRUE } else { uploadedStatus.lastUpdated?.let { - if (it.date >= Calendar.getInstance().time.date - INVALIDATE_DAY_COUNT) { + val duration = Calendar.getInstance().time.time - it.time + if (TimeUnit.MILLISECONDS.toDays(duration) < INVALIDATE_DAY_COUNT) { return Result.FALSE } } @@ -218,7 +220,7 @@ class ImageLoader @Inject constructor( } companion object { - const val INVALIDATE_DAY_COUNT: Int = 7 + const val INVALIDATE_DAY_COUNT: Long = 7 } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java index 32b38fea7e..e88f14b558 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java @@ -40,7 +40,7 @@ public ExploreListRootFragment(Bundle bundle) { featuredArguments.putString("categoryName", title); listFragment.setArguments(featuredArguments); } - + @Nullable @Override public View onCreateView(@NonNull final LayoutInflater inflater, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt new file mode 100644 index 0000000000..6d55a49e23 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt @@ -0,0 +1,96 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.net.Uri +import android.os.Bundle +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Custom Selector Activity Test + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class CustomSelectorActivityTest { + + private lateinit var activity: CustomSelectorActivity + + /** + * Set up the tests. + */ + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java) + .get() + val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java) + onCreate.isAccessible = true + onCreate.invoke(activity, null) + } + + /** + * Test activity not null. + */ + @Test + @Throws(Exception::class) + fun testActivityNotNull() { + assertNotNull(activity) + } + + /** + * Test changeTitle function. + */ + @Test + @Throws(Exception::class) + fun testChangeTitle() { + val func = activity.javaClass.getDeclaredMethod("changeTitle", String::class.java) + func.isAccessible = true + func.invoke(activity, "test") + } + + /** + * Test onFolderClick function. + */ + @Test + @Throws(Exception::class) + fun testOnFolderClick() { + activity.onFolderClick(Folder(1, "test", arrayListOf())); + } + + /** + * Test selectedImagesChanged function. + */ + @Test + @Throws(Exception::class) + fun testOnSelectedImagesChanged() { + activity.onSelectedImagesChanged(ArrayList()) + } + + /** + * Test onDone function. + */ + @Test + @Throws(Exception::class) + fun testOnDone() { + activity.onDone() + activity.onSelectedImagesChanged(ArrayList(arrayListOf(Image(1, "test", Uri.parse("test"), "test", 1)))); + activity.onDone() + } + + /** + * Test onBackPressed Function. + */ + @Test + @Throws(Exception::class) + fun testOnBackPressed() { + activity.onBackPressed() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt new file mode 100644 index 0000000000..309392d4dc --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModelTest.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +/** + * Custom Selector View Model test. + */ +class CustomSelectorViewModelTest { + + private lateinit var viewModel: CustomSelectorViewModel + + @Mock + private lateinit var imageFileLoader: ImageFileLoader + + @Mock + private lateinit var context: Context + + /** + * Set up the test. + */ + @Before + fun setUp(){ + MockitoAnnotations.initMocks(this) + viewModel = CustomSelectorViewModel(context, imageFileLoader); + } + + /** + * Test onCleared(); + */ + @Test + fun testOnCleared(){ + val func = viewModel.javaClass.getDeclaredMethod("onCleared") + func.isAccessible = true + func.invoke(viewModel); + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt new file mode 100644 index 0000000000..53094d6f7b --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt @@ -0,0 +1,130 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import fr.free.nrw.commons.customselector.model.Result +import android.widget.ProgressBar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.soloader.SoLoader +import fr.free.nrw.commons.R +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.ui.adapter.FolderAdapter +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +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.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.wikipedia.AppAdapter +import java.lang.reflect.Field + +/** + * Custom Selector Folder Fragment Test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class FolderFragmentTest { + + private lateinit var fragment: FolderFragment + private lateinit var view: View + private lateinit var selectorRV : RecyclerView + private lateinit var loader : ProgressBar + private lateinit var layoutInflater: LayoutInflater + private lateinit var context: Context + private lateinit var viewModelField:Field + + @Mock + private lateinit var adapter: FolderAdapter + + @Mock + private lateinit var savedInstanceState: Bundle + + /** + * Setup the folder fragment. + */ + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context = RuntimeEnvironment.application.applicationContext + AppAdapter.set(TestAppAdapter()) + SoLoader.setInTestMode() + Fresco.initialize(context) + val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get() + + fragment = FolderFragment.newInstance() + val fragmentManager: FragmentManager = activity.supportFragmentManager + val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commit() + + layoutInflater = LayoutInflater.from(activity) + view = layoutInflater.inflate(R.layout.fragment_custom_selector, null) as View + + selectorRV = view.findViewById(R.id.selector_rv) + loader = view.findViewById(R.id.loader) + + Whitebox.setInternalState(fragment, "folderAdapter", adapter) + Whitebox.setInternalState(fragment, "selectorRV", selectorRV ) + Whitebox.setInternalState(fragment, "loader", loader) + + viewModelField = fragment.javaClass.getDeclaredField("viewModel") + viewModelField.isAccessible = true + } + + /** + * Test onCreateView + */ + @Test + @Throws(Exception::class) + fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + viewModelField.set(fragment, null) + fragment.onCreateView(layoutInflater, null, savedInstanceState) + } + + /** + * Test onCreate + */ + @Test + @Throws(Exception::class) + fun testOnCreate() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onCreate(savedInstanceState) + } + + /** + * Test columnCount. + */ + @Test + fun testColumnCount() { + val func = fragment.javaClass.getDeclaredMethod("columnCount") + func.isAccessible = true + assertEquals(2, func.invoke(fragment)) + } + + /** + * Test handleResult. + */ + @Test + fun testHandleResult() { + val func = fragment.javaClass.getDeclaredMethod("handleResult", Result::class.java) + func.isAccessible = true + func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf())) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt new file mode 100644 index 0000000000..e30d47216c --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoaderTest.kt @@ -0,0 +1,123 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.ContentResolver +import android.content.Context +import android.provider.MediaStore +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.same +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.powermock.reflect.Whitebox +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.robolectric.fakes.RoboCursor +import java.io.File +import kotlin.coroutines.CoroutineContext + +/** + * Custom Selector Image File loader test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ImageFileLoaderTest { + + @Mock + private lateinit var mockContentResolver: ContentResolver + + @Mock + private lateinit var context: Context; + + @Mock + private lateinit var imageLoaderListener: ImageLoaderListener + + @Mock + private lateinit var coroutineScope: CoroutineScope + + private lateinit var imageCursor: RoboCursor + private lateinit var coroutineContext: CoroutineContext + private lateinit var projection: List + private lateinit var imageFileLoader: ImageFileLoader + + /** + * Setup before tests. + */ + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + coroutineContext = Dispatchers.Main + imageCursor = RoboCursor() + imageFileLoader = ImageFileLoader(context) + projection = listOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.BUCKET_ID, + MediaStore.Images.Media.BUCKET_DISPLAY_NAME + ) + + Whitebox.setInternalState(imageFileLoader, "coroutineContext", coroutineContext) + } + + /** + * Test loading device images. + */ + @Test + fun testLoadDeviceImages() { + imageFileLoader.loadDeviceImages(imageLoaderListener, coroutineScope) + } + + /** + * Test get images from the device function. + */ + @Test + fun testGetImages() { + val func = imageFileLoader.javaClass.getDeclaredMethod( + "getImages", + ImageLoaderListener::class.java + ) + func.isAccessible = true + + val image1 = arrayOf(1, "imageLoaderTestFile", "src/test/resources/imageLoaderTestFile", 1, "downloads") + val image2 = arrayOf(2, "imageLoaderTestFile", null, 1, "downloads") + File("src/test/resources/imageLoaderTestFile").createNewFile() + + imageCursor.setColumnNames(projection) + imageCursor.setResults(arrayOf(image1, image2)); + + val contentResolver: ContentResolver = mock { + on { + query( + same(MediaStore.Images.Media.EXTERNAL_CONTENT_URI), + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } doReturn imageCursor; + } + + // test null cursor. + `when`( + context.contentResolver + ).thenReturn(mockContentResolver) + func.invoke(imageFileLoader, imageLoaderListener); + + // test demo cursor. + `when`( + context.contentResolver + ).thenReturn(contentResolver) + func.invoke(imageFileLoader, imageLoaderListener); + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt new file mode 100644 index 0000000000..9794003c82 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.Context +import android.os.Bundle +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.widget.ProgressBar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.soloader.SoLoader +import fr.free.nrw.commons.R +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 fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +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.Shadows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import org.wikipedia.AppAdapter +import java.lang.reflect.Field + +/** + * Custom Selector Image Fragment Test. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ImageFragmentTest { + + private lateinit var fragment: ImageFragment + private lateinit var view: View + private lateinit var selectorRV : RecyclerView + private lateinit var loader : ProgressBar + private lateinit var layoutInflater: LayoutInflater + private lateinit var context: Context + private lateinit var viewModelField: Field + + @Mock + private lateinit var image: Image + + @Mock + private lateinit var adapter: ImageAdapter + + @Mock + private lateinit var savedInstanceState: Bundle + + /** + * Setup the image fragment. + */ + @Before + fun setUp(){ + MockitoAnnotations.initMocks(this) + context = RuntimeEnvironment.application.applicationContext + AppAdapter.set(TestAppAdapter()) + SoLoader.setInTestMode() + Fresco.initialize(context) + val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get() + + fragment = ImageFragment.newInstance(1) + val fragmentManager: FragmentManager = activity.supportFragmentManager + val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.add(fragment, null) + fragmentTransaction.commit() + + layoutInflater = LayoutInflater.from(activity) + view = layoutInflater.inflate(R.layout.fragment_custom_selector, null, false) as View + selectorRV = view.findViewById(R.id.selector_rv) + loader = view.findViewById(R.id.loader) + + Whitebox.setInternalState(fragment, "imageAdapter", adapter) + Whitebox.setInternalState(fragment, "selectorRV", selectorRV ) + Whitebox.setInternalState(fragment, "loader", loader) + + viewModelField = fragment.javaClass.getDeclaredField("viewModel") + viewModelField.isAccessible = true + } + + /** + * Test onCreate + */ + @Test + @Throws(Exception::class) + fun testOnCreate(){ + Shadows.shadowOf(Looper.getMainLooper()).idle() + fragment.onCreate(savedInstanceState); + } + + /** + * Test onCreateView + */ + @Test + @Throws(Exception::class) + fun testOnCreateView() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + viewModelField.set(fragment, null) + fragment.onCreateView(layoutInflater, null, savedInstanceState) + } + + /** + * Test handleResult. + */ + @Test + fun testHandleResult(){ + val func = fragment.javaClass.getDeclaredMethod("handleResult", Result::class.java) + func.isAccessible = true + func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf())) + func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf(image,image))) + } + + /** + * Test getSpanCount. + */ + @Test + fun testGetSpanCount() { + val func = fragment.javaClass.getDeclaredMethod("getSpanCount") + func.isAccessible = true + assertEquals(3, func.invoke(fragment)) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt new file mode 100644 index 0000000000..cb7cf3a501 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt @@ -0,0 +1,218 @@ +package fr.free.nrw.commons.customselector.ui.selector + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.customselector.database.UploadedStatus +import fr.free.nrw.commons.customselector.database.UploadedStatusDao +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.adapter.ImageAdapter +import fr.free.nrw.commons.filepicker.PickedFiles +import fr.free.nrw.commons.filepicker.UploadableFile +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.upload.FileProcessor +import fr.free.nrw.commons.upload.FileUtilsWrapper +import io.reactivex.Single +import junit.framework.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.* +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner +import org.powermock.reflect.Whitebox +import org.robolectric.annotation.Config +import java.io.File +import java.io.FileInputStream +import java.lang.Exception +import java.util.* +import kotlin.collections.HashMap + +/** + * Image Loader Test. + */ +@RunWith(PowerMockRunner::class) +@PrepareForTest(PickedFiles::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +class ImageLoaderTest { + + @Mock + private lateinit var uri:Uri + + @Mock + private lateinit var mediaClient: MediaClient + + @Mock + private lateinit var single: Single + + @Mock + private lateinit var fileProcessor: FileProcessor + + @Mock + private lateinit var fileUtilsWrapper: FileUtilsWrapper + + @Mock + private lateinit var uploadedStatusDao: UploadedStatusDao + + @Mock + private lateinit var holder: ImageAdapter.ImageViewHolder + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var uploadableFile: UploadableFile + + @Mock + private lateinit var inputStream: FileInputStream + + @Mock + private lateinit var contentResolver: ContentResolver + + @Mock + private lateinit var image: Image; + + private lateinit var imageLoader: ImageLoader; + private var mapImageSHA1: HashMap = HashMap() + private var mapHolderImage : HashMap = HashMap() + private var mapResult: HashMap = HashMap() + + /** + * Setup before test. + */ + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + imageLoader = + ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context) + + Whitebox.setInternalState(imageLoader, "mapImageSHA1", mapImageSHA1); + Whitebox.setInternalState(imageLoader, "mapHolderImage", mapHolderImage); + Whitebox.setInternalState(imageLoader, "mapResult", mapResult); + Whitebox.setInternalState(imageLoader, "context", context) + } + + /** + * Test queryAndSetView. + */ + @Test + fun testQueryAndSetView(){ + // TODO + imageLoader.queryAndSetView(holder,image) + } + + /** + * Test querySha1 + */ + @Test + fun testQuerySha1() { + val func = imageLoader.javaClass.getDeclaredMethod( + "querySHA1", + String::class.java + ) + func.isAccessible = true + + Mockito.`when`(single.blockingGet()).thenReturn(true) + Mockito.`when`(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) + Mockito.`when`(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") + + // test without saving in map. + func.invoke(imageLoader, "testSha1"); + + // test with map save. + mapResult["testSha1"] = ImageLoader.Result.FALSE + func.invoke(imageLoader, "testSha1"); + } + + /** + * Test getSha1 + */ + @Test + @Throws (Exception::class) + fun testGetSha1() { + val func = imageLoader.javaClass.getDeclaredMethod( + "getSHA1", + Image::class.java + ) + func.isAccessible = true + + 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") + + Assert.assertEquals("testSha1", func.invoke(imageLoader, image)); + whenever(PickedFiles.pickedExistingPicture(context,Uri.parse("test"))).thenReturn(uploadableFile) + + mapImageSHA1[image] = "testSha2" + Assert.assertEquals("testSha2", func.invoke(imageLoader, image)); + } + + /** + * Test insertIntoUploaded Function. + */ + @Test + @Throws (Exception::class) + fun testInsertIntoUploaded() { + val func = imageLoader.javaClass.getDeclaredMethod( + "insertIntoUploaded", + String::class.java, + String::class.java, + Boolean::class.java, + Boolean::class.java) + func.isAccessible = true + + func.invoke(imageLoader, "", "", true, true) + } + + /** + * Test getImageSha1. + */ + @Test + @Throws (Exception::class) + fun testGetImageSHA1() { + val func = imageLoader.javaClass.getDeclaredMethod( + "getImageSHA1", + Uri::class.java) + func.isAccessible = true + + whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) + whenever(context.contentResolver).thenReturn(contentResolver) + whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") + + Assert.assertEquals("testSha1", func.invoke(imageLoader,uri)) + } + + /** + * Test getResultFromUploadedStatus. + */ + @Test + @Throws (Exception::class) + fun testGetResultFromUploadedStatus() { + val func = imageLoader.javaClass.getDeclaredMethod( + "getResultFromUploadedStatus", + UploadedStatus::class.java) + func.isAccessible = true + + // test Result.TRUE + Assert.assertEquals(ImageLoader.Result.TRUE, + func.invoke(imageLoader, + UploadedStatus("", "", true, true))) + + // test Result.FALSE + Assert.assertEquals(ImageLoader.Result.FALSE, + func.invoke(imageLoader, + UploadedStatus("", "", false, false, Calendar.getInstance().time))) + + // test Result.INVALID + Assert.assertEquals(ImageLoader.Result.INVALID, + func.invoke(imageLoader, UploadedStatus("", "", false, false, Date(0)))) + + } +} \ No newline at end of file diff --git a/app/src/test/resources/imageLoaderTestFile b/app/src/test/resources/imageLoaderTestFile new file mode 100644 index 0000000000..e69de29bb2 From 443dcf445ba37c9e6698dbfe23b77db14cf49483 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 21 Jul 2021 08:57:25 +0530 Subject: [PATCH 14/28] Image Loader Improvements (#4516) --- .../customselector/database/UploadedDao.kt | 24 ++- .../customselector/ui/adapter/ImageAdapter.kt | 6 +- .../ui/selector/ImageFragment.kt | 8 + .../customselector/ui/selector/ImageLoader.kt | 185 +++++++++++------- .../nrw/commons/upload/worker/UploadWorker.kt | 13 +- 5 files changed, 146 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt index d9f2fc55eb..c0282c92c1 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -50,39 +50,37 @@ abstract class UploadedStatusDao { /** * Asynchronous insert into uploaded status table. */ - fun insertUploaded(uploadedStatus: UploadedStatus) = runBlocking { - async { - uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? - insert(uploadedStatus) - }.await() + suspend fun insertUploaded(uploadedStatus: UploadedStatus) { + uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + insert(uploadedStatus) } /** * Asynchronous delete from uploaded status table. */ - fun deleteUploaded(uploadedStatus: UploadedStatus) = runBlocking { - async { delete(uploadedStatus) } + suspend fun deleteUploaded(uploadedStatus: UploadedStatus) { + delete(uploadedStatus) } /** * Asynchronous update entry in uploaded status table. */ - fun updateUploaded(uploadedStatus: UploadedStatus) = runBlocking { - async { update(uploadedStatus) } + suspend fun updateUploaded(uploadedStatus: UploadedStatus) { + update(uploadedStatus) } /** * Asynchronous image sha1 query. */ - fun getUploadedFromImageSHA1(imageSHA1: String) = runBlocking { - async { getFromImageSHA1(imageSHA1) }.await() + suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus { + return getFromImageSHA1(imageSHA1) } /** * Asynchronous modified image sha1 query. */ - fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String) = runBlocking { - async { getFromModifiedImageSHA1(modifiedImageSHA1) }.await() + suspend fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String):UploadedStatus { + return getFromModifiedImageSHA1(modifiedImageSHA1) } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 9029e03bc2..ff41048f04 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -76,7 +76,7 @@ class ImageAdapter( else { holder.itemUnselected(); } - Glide.with(context).load(image.uri).into(holder.image) + Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) imageLoader.queryAndSetView(holder,image) holder.itemView.setOnClickListener { selectOrRemoveImage(holder, position) @@ -99,8 +99,8 @@ class ImageAdapter( if(holder.isItemUploaded()){ Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show() } else { - selectedImages.add(images[position]) - notifyItemChanged(position, ImageSelectedOrUpdated()) + selectedImages.add(images[position]) + notifyItemChanged(position, ImageSelectedOrUpdated()) } } imageSelectListener.onSelectedImagesChanged(selectedImages) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index f1583c54fc..cbb3fc4425 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -148,4 +148,12 @@ class ImageFragment: CommonsDaggerSupportFragment() { return 3 // todo change span count depending on the device orientation and other factos. } + + /** + * OnDestroy Cleanup the imageLoader coroutine. + */ + override fun onDestroy() { + imageLoader?.cleanUP() + super.onDestroy() + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 5680cc7753..a617b2d2a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -11,10 +11,7 @@ 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 kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import timber.log.Timber import java.io.IOException import java.net.UnknownHostException @@ -57,9 +54,17 @@ class ImageLoader @Inject constructor( /** * Maps to facilitate image query. */ - private var mapImageSHA1: HashMap = HashMap() + private var mapModifiedImageSHA1: HashMap = HashMap() private var mapHolderImage : HashMap = HashMap() private var mapResult: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() + + /** + * Coroutine Dispatchers and Scope. + */ + private var defaultDispatcher = Dispatchers.Default + private var ioDispatcher = Dispatchers.IO + private val scope = MainScope() /** * Query image and setUp the view. @@ -72,42 +77,43 @@ class ImageLoader @Inject constructor( mapHolderImage[holder] = image holder.itemNotUploaded() - CoroutineScope(Dispatchers.Main).launch { + scope.launch { - var result : Result = Result.NOTFOUND - withContext(Dispatchers.Default) { + var result: Result = Result.NOTFOUND + if (mapHolderImage[holder] != image) { + return@launch + } + + val imageSHA1 = getImageSHA1(image.uri) + val uploadedStatus = getFromUploaded(imageSHA1) + + val sha1 = uploadedStatus?.let { + result = getResultFromUploadedStatus(uploadedStatus) + uploadedStatus.modifiedImageSHA1 + } ?: run { if (mapHolderImage[holder] == image) { - val imageSHA1 = getImageSHA1(image.uri) - val uploadedStatus = uploadedStatusDao.getUploadedFromImageSHA1(imageSHA1) - - val sha1 = uploadedStatus?.let { - result = getResultFromUploadedStatus(uploadedStatus) - uploadedStatus.modifiedImageSHA1 - } ?: run { - if(mapHolderImage[holder] == image) { - getSHA1(image) - } else { - "" - } - } - - if (mapHolderImage[holder] == image && - result in arrayOf(Result.NOTFOUND, Result.INVALID) && - sha1.isNotEmpty()) { - // Query original image. - result = querySHA1(imageSHA1) - if( result is Result.TRUE ) { - // Original image found. - insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false) - } - else { - // Original image not found, query modified image. - result = querySHA1(sha1) - if (result != Result.ERROR) { - insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE) - } - } + getSHA1(image) + } else { + "" + } + } + + if (mapHolderImage[holder] != image) { + return@launch + } + + if (result in arrayOf(Result.NOTFOUND, Result.INVALID) && sha1.isNotEmpty()) { + // Query original image. + result = querySHA1(imageSHA1) + if (result is Result.TRUE) { + // Original image found. + insertIntoUploaded(imageSHA1, sha1, result is Result.TRUE, false) + } else { + // Original image not found, query modified image. + result = querySHA1(sha1) + if (result != Result.ERROR) { + insertIntoUploaded(imageSHA1, sha1, false, result is Result.TRUE) } } } @@ -122,25 +128,27 @@ class ImageLoader @Inject constructor( * * @return Query result. */ - private fun querySHA1(SHA1: String): Result { - mapResult[SHA1]?.let{ - return it - } - var result : Result = Result.FALSE - try { - if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { - mapResult[SHA1] = Result.TRUE - result = Result.TRUE + + private suspend fun querySHA1(SHA1: String): Result { + return withContext(ioDispatcher) { + mapResult[SHA1]?.let { + return@withContext it } - } catch (e: Exception) { - if (e is UnknownHostException) { - // Handle no network connection. - Timber.e(e, "Network Connection Error") + var result: Result = Result.FALSE + try { + if (mediaClient.checkFileExistsUsingSha(SHA1).blockingGet()) { + mapResult[SHA1] = Result.TRUE + result = Result.TRUE + } + } catch (e: Exception) { + if (e is UnknownHostException) { + // Handle no network connection. + Timber.e(e, "Network Connection Error") + } + result = Result.ERROR + e.printStackTrace() } - result = Result.ERROR - e.printStackTrace() - } finally { - return result + result } } @@ -149,27 +157,48 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private fun getSHA1(image: Image): String { - mapImageSHA1[image]?.let{ + private suspend fun getSHA1(image: Image): String { + mapModifiedImageSHA1[image]?.let{ return it } val sha1 = generateModifiedSHA1(image); - mapImageSHA1[image] = sha1; + mapModifiedImageSHA1[image] = sha1; return sha1; } + /** + * Get the uploaded status entry from the database. + */ + private suspend fun getFromUploaded(imageSha1:String): UploadedStatus?{ + return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1) + } + /** * Insert into uploaded status table. */ - private fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ - uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedImageSha1, imageResult, modifiedImageResult)) + private suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + uploadedStatusDao.insertUploaded( + UploadedStatus( + imageSha1, + modifiedImageSha1, + imageResult, + modifiedImageResult + ) + ) } /** * Get image sha1 from uri, used to retrieve the original image sha1. */ - private fun getImageSHA1(uri: Uri): String { - return fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + private suspend fun getImageSHA1(uri: Uri): String { + return withContext(ioDispatcher) { + mapImageSHA1[uri]?.let{ + return@withContext it + } + val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + mapImageSHA1[uri] = result + result + } } /** @@ -194,18 +223,28 @@ class ImageLoader @Inject constructor( * * @return modified sha1 */ - private fun generateModifiedSHA1(image: Image) : String { - val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri) - val exifInterface: ExifInterface? = try { - ExifInterface(uploadableFile.file!!) - } catch (e: IOException) { - Timber.e(e) - null + 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 } - fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) - val sha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath)) - uploadableFile.file.delete() - return sha1 + } + + /** + * CleanUp function. + */ + fun cleanUP() { + scope.cancel() } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 8f9bc9504b..d46c5e4ac9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -29,9 +29,11 @@ import fr.free.nrw.commons.upload.UploadClient import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.wikidata.WikidataEditService import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException @@ -438,7 +440,16 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : contribution.contentUri?.let { val imageSha1 = fileUtilsWrapper.getSHA1(appContext.contentResolver.openInputStream(it)) val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) - uploadedStatusDao.insertUploaded(UploadedStatus(imageSha1, modifiedSha1, imageSha1 == modifiedSha1, true)); + MainScope().launch { + uploadedStatusDao.insertUploaded( + UploadedStatus( + imageSha1, + modifiedSha1, + imageSha1 == modifiedSha1, + true + ) + ); + } } } From 36a94fea9390fb5326d0979ae59496ee41fa1d9f Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sat, 24 Jul 2021 14:09:59 +0530 Subject: [PATCH 15/28] ImageLoader Test Updated (#4517) --- app/build.gradle | 1 + .../customselector/database/UploadedDao.kt | 38 +--- .../customselector/ui/selector/ImageLoader.kt | 18 +- .../ui/selector/ImageLoaderTest.kt | 162 +++++++++--------- 4 files changed, 96 insertions(+), 123 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 501595fdaa..0ccc38f602 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" testImplementation 'com.facebook.soloader:soloader:0.9.0' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt index c0282c92c1..49a1f61c3b 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -1,9 +1,7 @@ package fr.free.nrw.commons.customselector.database import androidx.room.* -import kotlinx.coroutines.runBlocking import java.util.* -import kotlinx.coroutines.* /** * UploadedStatusDao for Custom Selector. @@ -29,58 +27,30 @@ abstract class UploadedStatusDao { @Delete abstract suspend fun delete(uploadedStatus: UploadedStatus) - /** - * Get All entries from the uploaded status table. - */ - @Query("SELECT * FROM uploaded_table") - abstract suspend fun getAll() : List - /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus + abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus? /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus + abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus? /** * Asynchronous insert into uploaded status table. */ suspend fun insertUploaded(uploadedStatus: UploadedStatus) { - uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + uploadedStatus.lastUpdated = Calendar.getInstance().time insert(uploadedStatus) } - /** - * Asynchronous delete from uploaded status table. - */ - suspend fun deleteUploaded(uploadedStatus: UploadedStatus) { - delete(uploadedStatus) - } - - /** - * Asynchronous update entry in uploaded status table. - */ - suspend fun updateUploaded(uploadedStatus: UploadedStatus) { - update(uploadedStatus) - } - /** * Asynchronous image sha1 query. */ - suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus { + suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? { return getFromImageSHA1(imageSHA1) } - - /** - * Asynchronous modified image sha1 query. - */ - suspend fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String):UploadedStatus { - return getFromModifiedImageSHA1(modifiedImageSHA1) - } - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index a617b2d2a4..3b5254f86d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -62,9 +62,9 @@ class ImageLoader @Inject constructor( /** * Coroutine Dispatchers and Scope. */ - private var defaultDispatcher = Dispatchers.Default - private var ioDispatcher = Dispatchers.IO - private val scope = MainScope() + private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default + private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO + private val scope : CoroutineScope = MainScope() /** * Query image and setUp the view. @@ -129,7 +129,7 @@ class ImageLoader @Inject constructor( * @return Query result. */ - private suspend fun querySHA1(SHA1: String): Result { + suspend fun querySHA1(SHA1: String): Result { return withContext(ioDispatcher) { mapResult[SHA1]?.let { return@withContext it @@ -157,7 +157,7 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private suspend fun getSHA1(image: Image): String { + suspend fun getSHA1(image: Image): String { mapModifiedImageSHA1[image]?.let{ return it } @@ -169,14 +169,14 @@ class ImageLoader @Inject constructor( /** * Get the uploaded status entry from the database. */ - private suspend fun getFromUploaded(imageSha1:String): UploadedStatus?{ + suspend fun getFromUploaded(imageSha1:String): UploadedStatus? { return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1) } /** * Insert into uploaded status table. */ - private suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ uploadedStatusDao.insertUploaded( UploadedStatus( imageSha1, @@ -190,7 +190,7 @@ class ImageLoader @Inject constructor( /** * Get image sha1 from uri, used to retrieve the original image sha1. */ - private suspend fun getImageSHA1(uri: Uri): String { + suspend fun getImageSHA1(uri: Uri): String { return withContext(ioDispatcher) { mapImageSHA1[uri]?.let{ return@withContext it @@ -204,7 +204,7 @@ class ImageLoader @Inject constructor( /** * Get result data from database. */ - private fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { + fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) { return Result.TRUE } else { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt index cb7cf3a501..fe26921e5e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt @@ -3,8 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.ContentResolver import android.content.Context import android.net.Uri -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.customselector.database.UploadedStatus import fr.free.nrw.commons.customselector.database.UploadedStatusDao @@ -17,6 +16,10 @@ import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper import io.reactivex.Single import junit.framework.Assert +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +31,6 @@ import org.powermock.reflect.Whitebox import org.robolectric.annotation.Config import java.io.File import java.io.FileInputStream -import java.lang.Exception import java.util.* import kotlin.collections.HashMap @@ -38,6 +40,7 @@ import kotlin.collections.HashMap @RunWith(PowerMockRunner::class) @PrepareForTest(PickedFiles::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@ExperimentalCoroutinesApi class ImageLoaderTest { @Mock @@ -73,146 +76,145 @@ class ImageLoaderTest { @Mock private lateinit var contentResolver: ContentResolver - @Mock - private lateinit var image: Image; + @ExperimentalCoroutinesApi + private val testDispacher = TestCoroutineDispatcher() private lateinit var imageLoader: ImageLoader; - private var mapImageSHA1: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() private var mapHolderImage : HashMap = HashMap() private var mapResult: HashMap = HashMap() + private var mapModifiedImageSHA1: HashMap = HashMap() + private lateinit var image: Image; + private lateinit var uploadedStatus: UploadedStatus; /** * Setup before test. */ @Before + @ExperimentalCoroutinesApi fun setup() { + Dispatchers.setMain(testDispacher) MockitoAnnotations.initMocks(this) + imageLoader = ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context) + uploadedStatus= UploadedStatus( + "testSha1", + "testSha1", + false, + false, + Calendar.getInstance().time + ) + image = Image(1, "test", uri, "test", 0, "test") Whitebox.setInternalState(imageLoader, "mapImageSHA1", mapImageSHA1); Whitebox.setInternalState(imageLoader, "mapHolderImage", mapHolderImage); + 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) + whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") } /** - * Test queryAndSetView. + * Reset Dispatchers. */ - @Test - fun testQueryAndSetView(){ - // TODO - imageLoader.queryAndSetView(holder,image) + @After + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + testDispacher.cleanupTestCoroutines() } /** - * Test querySha1 + * Test queryAndSetView with upload Status as null. */ @Test - fun testQuerySha1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "querySHA1", - String::class.java - ) - func.isAccessible = true - - Mockito.`when`(single.blockingGet()).thenReturn(true) - Mockito.`when`(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) - Mockito.`when`(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") + fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest { + whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null) + mapModifiedImageSHA1[image] = "testSha1" + mapImageSHA1[uri] = "testSha1" - // test without saving in map. - func.invoke(imageLoader, "testSha1"); + mapResult["testSha1"] = ImageLoader.Result.TRUE + imageLoader.queryAndSetView(holder, image) - // test with map save. mapResult["testSha1"] = ImageLoader.Result.FALSE - func.invoke(imageLoader, "testSha1"); + imageLoader.queryAndSetView(holder, image) } /** - * Test getSha1 + * Test queryAndSetView with upload Status not null (ie retrieved from table) */ @Test - @Throws (Exception::class) - fun testGetSha1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "getSHA1", - Image::class.java - ) - func.isAccessible = true - - 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") - - Assert.assertEquals("testSha1", func.invoke(imageLoader, image)); - whenever(PickedFiles.pickedExistingPicture(context,Uri.parse("test"))).thenReturn(uploadableFile) - - mapImageSHA1[image] = "testSha2" - Assert.assertEquals("testSha2", func.invoke(imageLoader, image)); + fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest { + whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus) + imageLoader.queryAndSetView(holder, image) } /** - * Test insertIntoUploaded Function. + * Test querySha1 */ @Test - @Throws (Exception::class) - fun testInsertIntoUploaded() { - val func = imageLoader.javaClass.getDeclaredMethod( - "insertIntoUploaded", - String::class.java, - String::class.java, - Boolean::class.java, - Boolean::class.java) - func.isAccessible = true + fun testQuerySha1() = testDispacher.runBlockingTest { - func.invoke(imageLoader, "", "", true, true) + whenever(single.blockingGet()).thenReturn(true) + whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) + whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") + + imageLoader.querySHA1("testSha1") } /** - * Test getImageSha1. + * Test getSha1 */ @Test - @Throws (Exception::class) - fun testGetImageSHA1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "getImageSHA1", - Uri::class.java) - func.isAccessible = true + @ExperimentalCoroutinesApi + fun testGetSha1() = testDispacher.runBlockingTest { - whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) - whenever(context.contentResolver).thenReturn(contentResolver) + 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") - Assert.assertEquals("testSha1", func.invoke(imageLoader,uri)) + Assert.assertEquals("testSha1", imageLoader.getSHA1(image)); + whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn( + uploadableFile + ) + + mapModifiedImageSHA1[image] = "testSha2" + Assert.assertEquals("testSha2", imageLoader.getSHA1(image)); } /** * Test getResultFromUploadedStatus. */ @Test - @Throws (Exception::class) fun testGetResultFromUploadedStatus() { val func = imageLoader.javaClass.getDeclaredMethod( "getResultFromUploadedStatus", UploadedStatus::class.java) func.isAccessible = true - // test Result.TRUE - Assert.assertEquals(ImageLoader.Result.TRUE, - func.invoke(imageLoader, - UploadedStatus("", "", true, true))) - - // test Result.FALSE - Assert.assertEquals(ImageLoader.Result.FALSE, - func.invoke(imageLoader, - UploadedStatus("", "", false, false, Calendar.getInstance().time))) - // test Result.INVALID + uploadedStatus.lastUpdated = Date(0); Assert.assertEquals(ImageLoader.Result.INVALID, - func.invoke(imageLoader, UploadedStatus("", "", false, false, Date(0)))) + imageLoader.getResultFromUploadedStatus(uploadedStatus)) + + // test Result.TRUE + uploadedStatus.imageResult = true; + Assert.assertEquals(ImageLoader.Result.TRUE, + imageLoader.getResultFromUploadedStatus(uploadedStatus)) + } + @Test + fun testCleanUP() { + imageLoader.cleanUP() } } \ No newline at end of file From b62247d6005c995eb45d2f4bf11be831c5b7d885 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Mon, 26 Jul 2021 16:31:45 +0530 Subject: [PATCH 16/28] [GSoC] Improvement and bug Fixes (#4522) * Improvement and bug Fixes * fixed ellipsize --- .../ui/selector/CustomSelectorActivity.kt | 5 +- .../res/layout/activity_custom_selector.xml | 14 +-- .../res/layout/custom_selector_toolbar.xml | 85 ++++++++++--------- .../res/layout/item_custom_selector_image.xml | 1 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 58 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 099c89a862..ec7855cb4f 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity import android.content.Intent import android.os.Bundle +import android.view.View import android.widget.ImageButton import android.widget.TextView import androidx.lifecycle.ViewModelProvider @@ -96,7 +97,9 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL */ override fun onSelectedImagesChanged(selectedImages: ArrayList) { viewModel.selectedImages.value = selectedImages - // todo update selected images in view model. + + val done : ImageButton = findViewById(R.id.done) + done.visibility = if (selectedImages.isEmpty()) View.INVISIBLE else View.VISIBLE } /** diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index 9587e7c0a0..d96918feee 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -4,16 +4,10 @@ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> - - - + + - + + - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_selector_image.xml b/app/src/main/res/layout/item_custom_selector_image.xml index 021f463bc6..f04a719223 100644 --- a/app/src/main/res/layout/item_custom_selector_image.xml +++ b/app/src/main/res/layout/item_custom_selector_image.xml @@ -40,6 +40,7 @@ app:layout_constraintDimensionRatio="H,1:1" android:textSize="11sp" android:textStyle="bold" + android:textColor="@color/black" android:layout_margin="@dimen/dimen_6" android:gravity="center|center_vertical" style="@style/TextAppearance.AppCompat.Small" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e3c8fb040..3d71f9b560 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -648,5 +648,7 @@ Upload your first media by tapping on the add button. Wiki Loves Monuments is an international photo contest for monuments organised by Wikimedia Custom Selector No Images + Done + Back From e8f7d0f03b93f253651279b3d1a618432c6d9940 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 27 Jul 2021 05:50:49 +0530 Subject: [PATCH 17/28] Saving selector state (#4526) --- .../listeners/FolderClickListener.kt | 4 +- .../ui/adapter/FolderAdapter.kt | 2 +- .../ui/selector/CustomSelectorActivity.kt | 65 ++++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt index 15b74c57e7..cb32807f83 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt @@ -1,7 +1,5 @@ package fr.free.nrw.commons.customselector.listeners -import fr.free.nrw.commons.customselector.model.Folder - interface FolderClickListener { - fun onFolderClick(folder : Folder) + fun onFolderClick(folderId: Long, folderName: String) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index fb3e497940..67dcc789c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -49,7 +49,7 @@ class FolderAdapter( holder.name.text = folder.name holder.count.text = count.toString() holder.itemView.setOnClickListener{ - itemClickListener.onFolderClick(folder) + itemClickListener.onFolderClick(folder.bucketId, folder.name) } //todo load image thumbnail. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index ec7855cb4f..3b8bee3901 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -2,26 +2,43 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity import android.content.Intent +import android.content.SharedPreferences import android.os.Bundle import android.view.View import android.widget.ImageButton import android.widget.TextView +import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.ImageSelectListener -import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject -class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { +class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener, FragmentManager.OnBackStackChangedListener { /** * View model. */ - private lateinit var viewModel: CustomSelectorViewModel + private lateinit var viewModel: CustomSelectorViewModel + + /** + * isImageFragmentOpen is true when the image fragment is in view. + */ + private var isImageFragmentOpen = false + + /** + * Current ImageFragment attributes. + */ + private var bucketId: Long = 0L + private lateinit var bucketName: String + + /** + * Pref for saving selector state. + */ + private lateinit var prefs: SharedPreferences /** * View Model Factory. @@ -35,9 +52,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL super.onCreate(savedInstanceState) setContentView(R.layout.activity_custom_selector) - viewModel = ViewModelProvider(this,customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE) + viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) setupViews() + + // Open folder if saved in prefs. + if(prefs.contains("FolderId")){ + val lastOpenFolderId: Long = prefs.getLong("FolderId", 0L) + val lastOpenFolderName: String? = prefs.getString("FolderName", null) + lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it) } + } } /** @@ -49,8 +74,6 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL .commit() fetchData() setUpToolbar() - - // todo : open image fragment depending on the last user visit. } /** @@ -63,7 +86,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL /** * Change the title of the toolbar. */ - private fun changeTitle(title:String) { + private fun changeTitle(title: String) { val titleText = findViewById(R.id.title) if(titleText != null) { titleText.text = title @@ -84,12 +107,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL /** * override on folder click, change the toolbar title on folder click. */ - override fun onFolderClick(folder: Folder) { + override fun onFolderClick(folderId: Long, folderName: String) { supportFragmentManager.beginTransaction() - .add(R.id.fragment_container, ImageFragment.newInstance(folder.bucketId)) + .add(R.id.fragment_container, ImageFragment.newInstance(folderId)) .addToBackStack(null) .commit() - changeTitle(folder.name) + + changeTitle(folderName) + + bucketId = folderId + bucketName = folderName + isImageFragmentOpen = true } /** @@ -148,4 +176,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL } } + override fun onDestroy() { + if(isImageFragmentOpen){ + prefs.edit().putLong("FolderId", bucketId).putString("FolderName", bucketName).apply() + } else { + prefs.edit().remove("FolderId").remove("FolderName").apply() + } + super.onDestroy() + } + + /** + * Called whenever the contents of the back stack change. + */ + override fun onBackStackChanged() { + if(supportFragmentManager.backStackEntryCount == 0) { + isImageFragmentOpen = false + } + } } \ No newline at end of file From d78e6deb4eb5ce7f35f585befe3f06e72da64c36 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sun, 8 Aug 2021 18:38:11 +0530 Subject: [PATCH 18/28] [GSoC] Saved Image Fragment Scroll State (#4528) * Saved Image Fragment Scroll State * Fix delete image * Fixed Delete bug * Changed custom selector icon --- .../customselector/helper/ImageHelper.kt | 25 ++++---- .../listeners/FolderClickListener.kt | 2 +- .../ui/adapter/FolderAdapter.kt | 39 ++++++++++--- .../customselector/ui/adapter/ImageAdapter.kt | 54 +++++++++++------- .../ui/selector/CustomSelectorActivity.kt | 36 ++++++------ .../ui/selector/FolderFragment.kt | 13 ++++- .../ui/selector/ImageFragment.kt | 57 +++++++++++++++++-- .../customselector/ui/selector/ImageLoader.kt | 14 ++++- .../res/drawable/ic_custom_image_picker.xml | 3 + .../layout/fragment_contributions_list.xml | 22 +++---- .../layout/item_custom_selector_folder.xml | 2 - 11 files changed, 185 insertions(+), 82 deletions(-) create mode 100644 app/src/main/res/drawable/ic_custom_image_picker.xml diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 0a751d47bc..1447cd2d73 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -1,19 +1,7 @@ package fr.free.nrw.commons.customselector.helper -import android.content.Context -import com.mapbox.android.core.FileUtils import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image -import fr.free.nrw.commons.filepicker.Constants -import timber.log.Timber -import java.io.* -import java.math.BigInteger -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import kotlin.collections.ArrayList -import kotlin.collections.HashMap -import kotlin.collections.LinkedHashMap - /** * Image Helper object, includes all the static functions required by custom selector. @@ -24,7 +12,7 @@ object ImageHelper { /** * Returns the list of folders from given image list. */ - fun folderListFromImages(images: List): List { + fun folderListFromImages(images: List): ArrayList { val folderMap: MutableMap = LinkedHashMap() for (image in images) { val bucketId = image.bucketId @@ -61,6 +49,17 @@ object ImageHelper { return list.indexOf(image) } + /** + * getIndex: Returns the index of image in given list. + */ + fun getIndexFromId(list: ArrayList, imageId: Long): Int { + for(i in list){ + if(i.id == imageId) + return list.indexOf(i) + } + return 0; + } + /** * Gets the list of indices from the master list. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt index cb32807f83..e016a71ba6 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt @@ -1,5 +1,5 @@ package fr.free.nrw.commons.customselector.listeners interface FolderClickListener { - fun onFolderClick(folderId: Long, folderName: String) + fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 67dcc789c5..93759bdf48 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -11,6 +11,7 @@ import com.bumptech.glide.Glide import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder +import fr.free.nrw.commons.customselector.model.Image class FolderAdapter( /** @@ -43,16 +44,38 @@ class FolderAdapter( */ override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { val folder = folders[position] - val count = folder.images.size - val previewImage = folder.images[0] - Glide.with(context).load(previewImage.uri).into(holder.image) - holder.name.text = folder.name - holder.count.text = count.toString() - holder.itemView.setOnClickListener{ - itemClickListener.onFolderClick(folder.bucketId, folder.name) + val toBeRemoved = ArrayList() + + for(image in folder.images) { + // Remove all the top images that do not exist anymore + if(context.contentResolver.getType(image.uri) == null){ + // File not found + toBeRemoved.add(image) + } else { + break + } } + holder.image.setImageDrawable (null) + folder.images.removeAll(toBeRemoved) + val count = folder.images.size - //todo load image thumbnail. + if(count == 0) { + // Folder is empty, remove folder from the adapter. + holder.itemView.post{ + val updatePosition = folders.indexOf(folder) + folders.removeAt(updatePosition) + notifyItemRemoved(updatePosition) + notifyItemRangeChanged(updatePosition, folders.size) + } + } else { + val previewImage = folder.images[0] + Glide.with(context).load(previewImage.uri).into(holder.image) + holder.name.text = folder.name + holder.count.text = count.toString() + holder.itemView.setOnClickListener { + itemClickListener.onFolderClick(folder.bucketId, folder.name, 0) + } + } } /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index ff41048f04..8225ab2dc0 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -1,9 +1,8 @@ package fr.free.nrw.commons.customselector.ui.adapter import android.content.Context -import android.view.ViewGroup -import fr.free.nrw.commons.R import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import android.widget.Toast @@ -11,6 +10,7 @@ import androidx.constraintlayout.widget.Group import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -59,7 +59,7 @@ class ImageAdapter( * Create View holder. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { - val itemView = inflater.inflate(R.layout.item_custom_selector_image,parent, false) + val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false) return ImageViewHolder(itemView) } @@ -68,36 +68,46 @@ class ImageAdapter( */ override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { val image=images[position] - val selectedIndex = ImageHelper.getIndex(selectedImages,image) - val isSelected = selectedIndex != -1 - if(isSelected){ - holder.itemSelected(selectedIndex+1) - } - else { - holder.itemUnselected(); - } - Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) - imageLoader.queryAndSetView(holder,image) - holder.itemView.setOnClickListener { - selectOrRemoveImage(holder, position) + holder.image.setImageDrawable (null) + if (context.contentResolver.getType(image.uri) == null) { + // Image does not exist anymore, update adapter. + holder.itemView.post { + val updatedPosition = images.indexOf(image) + images.remove(image) + notifyItemRemoved(updatedPosition) + notifyItemRangeChanged(updatedPosition, images.size) + } + } else { + val selectedIndex = ImageHelper.getIndex(selectedImages, image) + val isSelected = selectedIndex != -1 + if (isSelected) { + holder.itemSelected(selectedIndex + 1) + } else { + holder.itemUnselected(); + } + Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) + imageLoader.queryAndSetView(holder, image) + holder.itemView.setOnClickListener { + 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]) + private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){ + val clickedIndex = ImageHelper.getIndex(selectedImages, images[position]) if (clickedIndex != -1) { selectedImages.removeAt(clickedIndex) - notifyItemChanged(position,ImageUnselected()) + notifyItemChanged(position, ImageUnselected()) val indexes = ImageHelper.getIndexList(selectedImages, images) for (index in indexes) { notifyItemChanged(index, ImageSelectedOrUpdated()) } } else { if(holder.isItemUploaded()){ - Toast.makeText(context,"Already Uploaded image", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Already Uploaded image", Toast.LENGTH_SHORT).show() } else { selectedImages.add(images[position]) notifyItemChanged(position, ImageSelectedOrUpdated()) @@ -109,7 +119,7 @@ class ImageAdapter( /** * Initialize the data set. */ - fun init(newImages:List) { + fun init(newImages: List) { val oldImageList:ArrayList = images val newImageList:ArrayList = ArrayList(newImages) val diffResult = DiffUtil.calculateDiff( @@ -128,6 +138,10 @@ class ImageAdapter( return images.size } + fun getImageIdAt(position: Int): Long { + return images.get(position).id + } + /** * Image view holder. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 3b8bee3901..972c16fc49 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.View import android.widget.ImageButton import android.widget.TextView -import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.listeners.FolderClickListener @@ -17,7 +16,7 @@ import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject -class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener, FragmentManager.OnBackStackChangedListener { +class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener { /** * View model. @@ -58,10 +57,11 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi setupViews() // Open folder if saved in prefs. - if(prefs.contains("FolderId")){ - val lastOpenFolderId: Long = prefs.getLong("FolderId", 0L) - val lastOpenFolderName: String? = prefs.getString("FolderName", null) - lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it) } + if(prefs.contains(FOLDER_ID)){ + val lastOpenFolderId: Long = prefs.getLong(FOLDER_ID, 0L) + val lastOpenFolderName: String? = prefs.getString(FOLDER_NAME, null) + val lastItemId: Long = prefs.getLong(ITEM_ID, 0) + lastOpenFolderName?.let { onFolderClick(lastOpenFolderId, it, lastItemId) } } } @@ -107,9 +107,9 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi /** * override on folder click, change the toolbar title on folder click. */ - override fun onFolderClick(folderId: Long, folderName: String) { + override fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) { supportFragmentManager.beginTransaction() - .add(R.id.fragment_container, ImageFragment.newInstance(folderId)) + .add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId)) .addToBackStack(null) .commit() @@ -172,25 +172,27 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi super.onBackPressed() val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) if(fragment != null && fragment is FolderFragment){ + isImageFragmentOpen = false changeTitle(getString(R.string.custom_selector_title)) } } + /** + * On activity destroy + * If image fragment is open, overwrite its attributes otherwise discard the values. + */ override fun onDestroy() { if(isImageFragmentOpen){ - prefs.edit().putLong("FolderId", bucketId).putString("FolderName", bucketName).apply() + prefs.edit().putLong(FOLDER_ID, bucketId).putString(FOLDER_NAME, bucketName).apply() } else { - prefs.edit().remove("FolderId").remove("FolderName").apply() + prefs.edit().remove(FOLDER_ID).remove(FOLDER_NAME).apply() } super.onDestroy() } - /** - * Called whenever the contents of the back stack change. - */ - override fun onBackStackChanged() { - if(supportFragmentManager.backStackEntryCount == 0) { - isImageFragmentOpen = false - } + companion object { + const val FOLDER_ID : String = "FolderId" + const val FOLDER_NAME : String = "FolderName" + const val ITEM_ID : String = "ItemId" } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index e43c0798cc..b1cd8ab371 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -14,6 +14,7 @@ import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.model.Result import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.CallbackStatus +import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.media.MediaClient @@ -55,6 +56,11 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private lateinit var gridLayoutManager: GridLayoutManager + /** + * Folder List. + */ + private lateinit var folders : ArrayList + /** * Companion newInstance. */ @@ -102,7 +108,7 @@ class FolderFragment : CommonsDaggerSupportFragment() { */ private fun handleResult(result: Result) { if(result.status is CallbackStatus.SUCCESS){ - val folders = ImageHelper.folderListFromImages(result.images) + folders = ImageHelper.folderListFromImages(result.images) folderAdapter.init(folders) folderAdapter.notifyDataSetChanged() selectorRV?.let { @@ -114,6 +120,11 @@ class FolderFragment : CommonsDaggerSupportFragment() { } } + override fun onResume() { + folderAdapter.notifyDataSetChanged() + super.onResume() + } + /** * Return Column count ie span count for grid view adapter. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index cbb3fc4425..b575e015b6 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -1,6 +1,8 @@ package fr.free.nrw.commons.customselector.ui.selector +import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,11 +15,15 @@ import fr.free.nrw.commons.R import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.listeners.ImageSelectListener 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 kotlinx.android.synthetic.main.fragment_custom_selector.* +import fr.free.nrw.commons.theme.BaseActivity import kotlinx.android.synthetic.main.fragment_custom_selector.view.* +import java.io.File +import java.io.FileInputStream +import java.net.URI import javax.inject.Inject class ImageFragment: CommonsDaggerSupportFragment() { @@ -27,13 +33,18 @@ class ImageFragment: CommonsDaggerSupportFragment() { */ private var bucketId: Long? = null + /** + * Last ImageItem Id. + */ + private var lastItemId: Long? = null + /** * View model for images. */ private var viewModel: CustomSelectorViewModel? = null /** - * View Elements + * View Elements. */ private var selectorRV: RecyclerView? = null private var loader: ProgressBar? = null @@ -67,14 +78,16 @@ class ImageFragment: CommonsDaggerSupportFragment() { * BucketId args name */ const val BUCKET_ID = "BucketId" + const val LAST_ITEM_ID = "LastItemId" /** * newInstance from bucketId. */ - fun newInstance(bucketId: Long): ImageFragment { + fun newInstance(bucketId: Long, lastItemId: Long): ImageFragment { val fragment = ImageFragment() val args = Bundle() args.putLong(BUCKET_ID, bucketId) + args.putLong(LAST_ITEM_ID, lastItemId) fragment.arguments = args return fragment } @@ -87,6 +100,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) bucketId = arguments?.getLong(BUCKET_ID) + lastItemId = arguments?.getLong(LAST_ITEM_ID, 0) viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) } @@ -116,6 +130,8 @@ class ImageFragment: CommonsDaggerSupportFragment() { return root } + lateinit var filteredImages: ArrayList; + /** * Handle view model result. */ @@ -123,9 +139,14 @@ class ImageFragment: CommonsDaggerSupportFragment() { if(result.status is CallbackStatus.SUCCESS){ val images = result.images if(images.isNotEmpty()) { - imageAdapter.init(ImageHelper.filterImages(images,bucketId)) - selectorRV?.let{ + filteredImages = ImageHelper.filterImages(images, bucketId) + imageAdapter.init(filteredImages) + selectorRV?.let { it.visibility = View.VISIBLE + lastItemId?.let { pos -> + (it.layoutManager as GridLayoutManager) + .scrollToPosition(ImageHelper.getIndexFromId(filteredImages, pos)) + } } } else{ @@ -149,11 +170,35 @@ class ImageFragment: CommonsDaggerSupportFragment() { // todo change span count depending on the device orientation and other factos. } + override fun onResume() { + imageAdapter.notifyDataSetChanged() + super.onResume() + } + /** - * OnDestroy Cleanup the imageLoader coroutine. + * OnDestroy + * Cleanup the imageLoader coroutine. + * Save the Image Fragment state. */ override fun onDestroy() { imageLoader?.cleanUP() + + val position = (selectorRV?.layoutManager as GridLayoutManager) + .findFirstVisibleItemPosition() + + // Check for empty RecyclerView. + if (position != -1) { + context?.let { context -> + context.getSharedPreferences( + "CustomSelector", + BaseActivity.MODE_PRIVATE + )?.let { prefs -> + prefs.edit()?.let { editor -> + editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() + } + } + } + } super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 3b5254f86d..73b2f1f79c 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -13,6 +13,7 @@ import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper import kotlinx.coroutines.* import timber.log.Timber +import java.io.FileNotFoundException import java.io.IOException import java.net.UnknownHostException import java.util.* @@ -86,6 +87,8 @@ class ImageLoader @Inject constructor( } val imageSHA1 = getImageSHA1(image.uri) + if(imageSHA1.isEmpty()) + return@launch val uploadedStatus = getFromUploaded(imageSHA1) val sha1 = uploadedStatus?.let { @@ -195,9 +198,14 @@ class ImageLoader @Inject constructor( mapImageSHA1[uri]?.let{ return@withContext it } - val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) - mapImageSHA1[uri] = result - result + try { + val result = fileUtilsWrapper.getSHA1(context.contentResolver.openInputStream(uri)) + mapImageSHA1[uri] = result + result + } catch (e: FileNotFoundException){ + e.printStackTrace() + "" + } } } diff --git a/app/src/main/res/drawable/ic_custom_image_picker.xml b/app/src/main/res/drawable/ic_custom_image_picker.xml new file mode 100644 index 0000000000..7dd39280a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_custom_image_picker.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_contributions_list.xml b/app/src/main/res/layout/fragment_contributions_list.xml index e9852f49af..923cc83431 100644 --- a/app/src/main/res/layout/fragment_contributions_list.xml +++ b/app/src/main/res/layout/fragment_contributions_list.xml @@ -70,17 +70,17 @@ app:srcCompat="@drawable/ic_photo_white_24dp" /> + android:id="@+id/fab_custom_gallery" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:tint="@color/button_blue" + android:visibility="gone" + app:backgroundTint="@color/main_background_light" + app:useCompatPadding="true" + app:elevation="@dimen/tiny_margin" + app:fabSize="mini" + app:srcCompat="@drawable/ic_custom_image_picker" + android:background="@drawable/commons"/> Date: Mon, 9 Aug 2021 16:32:18 +0530 Subject: [PATCH 19/28] rebase fix --- app/src/main/res/values/attrs.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index fb61e8d18a..f43772fb55 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -51,7 +51,6 @@ - From 1b984c8b9b88bbc9922798d086d8d6e91f91c7f6 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 14 Jul 2021 16:35:08 +0530 Subject: [PATCH 20/28] [GSoC] Master rebase. (#4505) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Fixes #3694 Pre-select places as depictions (#4452) * WikidataEditService: stop automatically adding WikidataPlace as a depiction When the user initiates the upload process from Nearby and also manually adds the place as a depiction, the depiction is added twice. Since this behavior is invisible to the user, it is being removed in preparation for auto-selecting the place as a depiction on the DepictsFragment screen. * DepictsFragment: auto-select place as a depiction Pass the Place reference from UploadActivity to DepictsFragment and select the corresponding DepictedItem. Using the place id, retrieve the corresponding Entity to create and select a DepictedItem. * UploadRepository: use Place from UploadItem to obtain a DepictedItem Instead of passing a Place object from UploadActivity to DepictsFragment and then passing the Place object up the chain to obtain and select a DepictedItem, retrieve the Place object directly within UploadRepository * DepictsFragment: select Place depiction when fragment becomes visible * UploadDepictsAdapter: make adapter aware of selection state Update selection state when recycled list items are automatically selected, preventing automatically selected items from appearing as unselected until they are forced to re-bind (i.e. after scrolling) * DepictsFragment: pre-select place depictions for all UploadItems If several images are selected and set to different places, pre-select all place depictions to reinforce the intended upload workflow philosophy (i.e. all images in a set are intended to be from/of the same place). See discussion in commons-app/apps-android-commons#3694 * DepictsFragment: scroll to the top every time list is updated * Typo fixes (#4461) * Fixed typo on class documentation of TextUtils * corrected comma placement in documentation * Fixed typos in comments * fix-issue-4424 (#4445) Co-authored-by: Pratham2305 * fix edit categories ui (#4414) Co-authored-by: Pratham2305 * Fix doom version issue (#4463) * Update db version * DBOpenHelper version update * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict * Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462) * Edited Project.xml to make indent size 4 * Changed files with 2 space indentation to use 4 space indentation * Edited Project.xml to make indent size 4 * changed files with 2 space indent to 4 space indent * fix :Back Pressed Event not work in Explore tab when user not login (#4404) * fix :Back Pressed Event not work in Explore tab * minor changes * fix :Upload count or number of contribution does not get updated when media is successful uploaded (#4399) * * fix:Number of Contributions not updated * Add javadocs * minor changes * made minor changes * String was nonsense and untranslatible, fixed (#4466) * Ability to show captions and descriptions in all entered languages (#4355) * implement Ability to show captions and descriptions in all entered languages *Add Javadoc * handle Back event of fragment(mediaDetailFragment) * fix minor bugs * add internationalization * revert previous changes * fix visibility bug * resolve conflict Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: neslihanturan * Use more understandable strings (#4470) * Fix #3792 Missing Column Issue (#4468) * Fix Missing Column Issue * Fix tests * Add UploadCategoriesFragment Unit Tests (#4473) * Panorama (#4467) * panoramic images fixed * made requested changes * Minor refactoring Co-authored-by: Aditya Srivastava * Localisation updates from https://translatewiki.net. * Main activity title is sometimes "Contributions", sometimes "Commons" (#4472) Fixes #4438 Replace == with equals() in onRestoreInstanceState * Localisation updates from https://translatewiki.net. * caption and description copyable (#4481) * Removed next button in quiz (#4382) * issues resolved * modification done * warning fixed * issues resolved * Button added * don't know function added * Button added * modification done * modification done * Localisation updates from https://translatewiki.net. * Added option to show and modify location while uploading (#4475) * initial commit * Everything done * minor modification * minor modification * Issues fixed * minor modifications * issue fixed * Issues fixed * Tutorial removed from log out state (#4479) * tutorial removed from log out state * Issue removed * Update changelog.md * Versioning for v3.0.2 * Fix #4482 (#4484) * Fix crash when image resolution is very high (#4483) * Localisation updates from https://translatewiki.net. * Add Contributions Fragment Unit Tests (#4490) * Fix Tests Errors (#4491) * Add UploadMediaDetailFragment Unit Tests (#4492) * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Localisation updates from https://translatewiki.net. * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * [GSoC] Image Selection (#4457) * Localisation updates from https://translatewiki.net. * Fixes #4357 After switching to different account, contributions screen shows pictures of previous account (#4421) * Update UploadMediaDetailFragment.java * Update LoginActivity.java Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account * Revert "Update UploadMediaDetailFragment.java" This reverts commit b1b4257f205b022ffaadee9f947357e5fc04c337. Co-authored-by: Obsidian_zero <1198474846@qq.com> * Remove unnecessary whitespace from a message (#4439) * Merge v3.0.1 into master (#4446) * Versioning and changelog for v3.0.0 (#4152) * Versioning for v3.0.0 * Update changelog.md * Handled migration 8-9-10 in BookmarksLocationDao (#4154) * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * #Fixes #4141 - Handled migrations for BookmarkLocationsDao from 8-9-10 * Fixes #4179 (#4180) * Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked() * Updated >ContributionsFragment#backButtonClicked() to handle back press properly * Fixes #4179 (#4181) * Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed() * Cherrypick for hotfix3.1 (#4205) * Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list. * fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146) * fixed bug:app crashes on viewing review in Review Fragment #4135 * Fixed the issue with back button in contribution tab. (#4177) Co-authored-by: Pratham2305 * Fixed the issue with back navigation button on toolbar in explore tab. (#4175) * Fix (#4148) Issues on theme change * fixed themeChange crashes * fixed comments * Overlooked the title bar Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> * Fixes #4173 (#4396) * Fix #4147 Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390) * Update query to fetch descriptions * Make description added to NearbyResultItem * Make string operations to display description and label in a combined way * Fix reviews, remove long description from list and swap label and description texts * Fix repeated information issue * Fix double information issue * fix style issues * Remove douplicated information * Changes made (#4354) * Remove nonexistent method * Fix #4283 IllegalStateException (#4440) * Fix #4283 IllegalStateException * Fix flickering issue * Versioning for v3.0.1 * Update changelog.md Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> * Localisation updates from https://translatewiki.net. * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Added a feature for editing coordinates (#4418) * not * Place Picker added * Pick location and API call linked * minor warnings resolved * Code conventions followed * issue fixed * Wikitext edited properly * minor modification * Location Picker added * Bottom sheet removed * Location picker fully implemented * credit added * credit added * issues fixed * issues fixed * minor issue fixed * Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451) * Localisation updates from https://translatewiki.net. * Fixes 4344 - Duplicate Uploads (#4442) * Fixes 4344 - Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries. * Update states to IN_PROGRESS before uploads are processed * Image selection added * Forwarded activity result to upload wizard * Initialised xmls, made folder and image item. * xmls done * xmls completed * removed unwanted attribute * Created models, adapters and view models (#4441) * created models, adapters and view models * Added Image Fragment * back button linked * Documentation and refractor * spaces * Butterknife annotation * DiffUtil * Added Examples * Extended Custom selector From Base Activity * made view model injectable * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * Image selection added * Forwarded activity result to upload wizard * [GSOC] Added Image Fetch (#4449) * Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code * fixed merge errors * Documented the remaining function Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> * [GSoC] Show uploaded images differently. (#4464) * uploaded images shown differently * Loaded images before query * Handled exceptions, Made ImageLoader injectable, Document and clean code * [GSoC] Added Uploaded status table in room database. (#4476) * added Uploaded status table in room database * Added unique property, minor refractoring * Database intigrated * Database integrated * Handled result null exception * Exceptions handled and refractored * Introduced constants * moved to sealed class * No database insert on network error * queried original image * documented the code * Updated uploaded status on upload success * Image Helper test (#4485) * [GSoC] Adapter Tests (#4488) * Added FolderAdapterTest * Image Adapter Test * merge fix * rebase fix Co-authored-by: translatewiki.net Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com> Co-authored-by: Obsidian_zero <1198474846@qq.com> Co-authored-by: Amir E. Aharoni Co-authored-by: Josephine Lim Co-authored-by: Ashish Co-authored-by: neslihanturan Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com> Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com> Co-authored-by: Pratham2305 Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com> Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com> Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com> Co-authored-by: Brigham Byerly <6891883+byerlyb20@users.noreply.github.com> Co-authored-by: Jamie Brown Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com> Co-authored-by: Nicolas Raoul Co-authored-by: Ashar --- app/src/main/res/values/attrs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f43772fb55..fb61e8d18a 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -51,6 +51,7 @@ + From ea29a726a179f87bba10ec54e5ba6fb23f7a2429 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 10 Aug 2021 07:31:15 +0530 Subject: [PATCH 21/28] Tests updated (#4538) --- .../ui/adapter/FolderAdapter.kt | 2 +- .../customselector/ui/adapter/ImageAdapter.kt | 2 +- .../ui/selector/ImageFragment.kt | 3 +- .../ui/adapter/FolderAdapterTest.kt | 22 +++++++++++-- .../ui/adapter/ImageAdapterTest.kt | 28 ++++++++++++++-- .../ui/selector/CustomSelectorActivityTest.kt | 25 +++++++++++++- .../ui/selector/FolderFragmentTest.kt | 11 +++++++ .../ui/selector/ImageFragmentTest.kt | 33 ++++++++++++++++++- 8 files changed, 116 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 93759bdf48..03790a7d87 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -69,7 +69,7 @@ class FolderAdapter( } } else { val previewImage = folder.images[0] - Glide.with(context).load(previewImage.uri).into(holder.image) + Glide.with(holder.image).load(previewImage.uri).into(holder.image) holder.name.text = folder.name holder.count.text = count.toString() holder.itemView.setOnClickListener { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 8225ab2dc0..2dd97f3e3e 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -85,7 +85,7 @@ class ImageAdapter( } else { holder.itemUnselected(); } - Glide.with(context).load(image.uri).thumbnail(0.3f).into(holder.image) + Glide.with(holder.image).load(image.uri).thumbnail(0.3f).into(holder.image) imageLoader.queryAndSetView(holder, image) holder.itemView.setOnClickListener { selectOrRemoveImage(holder, position) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index b575e015b6..ef49f27e7e 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -48,6 +48,7 @@ class ImageFragment: CommonsDaggerSupportFragment() { */ private var selectorRV: RecyclerView? = null private var loader: ProgressBar? = null + lateinit var filteredImages: ArrayList; /** * View model Factory. @@ -130,8 +131,6 @@ class ImageFragment: CommonsDaggerSupportFragment() { return root } - lateinit var filteredImages: ArrayList; - /** * Handle view model result. */ diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt index 6a6271b96e..1c2a663f38 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapterTest.kt @@ -1,11 +1,14 @@ package fr.free.nrw.commons.customselector.ui.adapter +import android.content.ContentResolver import fr.free.nrw.commons.R import android.content.Context import android.net.Uri import android.view.LayoutInflater import android.view.View import android.widget.GridLayout +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder @@ -15,7 +18,10 @@ import org.junit.Before import org.junit.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.runner.RunWith +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 @@ -34,13 +40,21 @@ class FolderAdapterTest { private lateinit var folder: Folder private lateinit var folderList: ArrayList + @Mock + private lateinit var context: Context + + @Mock + private lateinit var mockContentResolver: ContentResolver + @Before @Throws(Exception::class) fun setUp() { + MockitoAnnotations.initMocks(this) + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get() image = Image(1, "image", uri, "abc/abc", 1, "bucket1") folder = Folder(1, "bucket1", ArrayList(listOf(image))) - folderList = ArrayList(listOf(folder)) + folderList = ArrayList(listOf(folder, folder, folder)) folderAdapter = FolderAdapter(activity, activity as FolderClickListener) } @@ -57,9 +71,13 @@ class FolderAdapterTest { */ @Test fun onBindViewHolder() { - folderAdapter.init(folderList) val inflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val listItemView: View = inflater.inflate(R.layout.item_custom_selector_folder, null, false) + + whenever(context.contentResolver).thenReturn(mockContentResolver) + whenever(mockContentResolver.getType(any())).thenReturn("jpg") + Whitebox.setInternalState(folderAdapter, "context", context) + folderAdapter.init(folderList) folderAdapter.onBindViewHolder(FolderAdapter.FolderViewHolder(listItemView), 0) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt index 8de08a2d32..fac24cb326 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapterTest.kt @@ -1,9 +1,12 @@ package fr.free.nrw.commons.customselector.ui.adapter +import android.content.ContentResolver import android.content.Context +import android.net.Uri import android.view.LayoutInflater import android.view.View import android.widget.GridLayout +import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.customselector.listeners.ImageSelectListener @@ -15,6 +18,7 @@ import org.junit.Test import org.junit.jupiter.api.Assertions import org.junit.runner.RunWith import org.mockito.* +import org.powermock.reflect.Whitebox import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -26,18 +30,23 @@ import java.lang.reflect.Field @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application = TestCommonsApplication::class) class ImageAdapterTest { - @Mock - private lateinit var image: Image @Mock private lateinit var imageLoader: ImageLoader @Mock private lateinit var imageSelectListener: ImageSelectListener + @Mock + private lateinit var context: Context + @Mock + private lateinit var mockContentResolver: ContentResolver private lateinit var activity: CustomSelectorActivity private lateinit var imageAdapter: ImageAdapter private lateinit var images : ArrayList private lateinit var holder: ImageAdapter.ImageViewHolder private lateinit var selectedImageField: Field + private var uri: Uri = Mockito.mock(Uri::class.java) + private lateinit var image: Image + /** * Set up variables. @@ -48,6 +57,7 @@ class ImageAdapterTest { MockitoAnnotations.initMocks(this) activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).get() imageAdapter = ImageAdapter(activity, imageSelectListener, imageLoader) + image = Image(1, "image", uri, "abc/abc", 1, "bucket1") images = ArrayList() val inflater = activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater @@ -71,6 +81,11 @@ class ImageAdapterTest { */ @Test fun onBindViewHolder() { + + whenever(context.contentResolver).thenReturn(mockContentResolver) + whenever(mockContentResolver.getType(uri)).thenReturn("jpg") + Whitebox.setInternalState(imageAdapter, "context", context) + // Parameters. images.add(image) imageAdapter.init(images) @@ -118,4 +133,13 @@ class ImageAdapterTest { fun getItemCount() { Assertions.assertEquals(0, imageAdapter.itemCount) } + + /** + * Test getImageId + */ + @Test + fun getImageIdAt() { + imageAdapter.init(listOf(image)) + Assertions.assertEquals(1, imageAdapter.getImageIdAt(0)) + } } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt index 6d55a49e23..21007daeb5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivityTest.kt @@ -2,7 +2,11 @@ package fr.free.nrw.commons.customselector.ui.selector 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 org.junit.Before @@ -12,7 +16,11 @@ import org.junit.runner.RunWith import org.mockito.MockitoAnnotations 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 /** * Custom Selector Activity Test @@ -29,6 +37,8 @@ class CustomSelectorActivityTest { @Before fun setUp() { MockitoAnnotations.initMocks(this) + AppAdapter.set(TestAppAdapter()) + activity = Robolectric.buildActivity(CustomSelectorActivity::class.java) .get() val onCreate = activity.javaClass.getDeclaredMethod("onCreate", Bundle::class.java) @@ -62,7 +72,7 @@ class CustomSelectorActivityTest { @Test @Throws(Exception::class) fun testOnFolderClick() { - activity.onFolderClick(Folder(1, "test", arrayListOf())); + activity.onFolderClick(1, "test", 0); } /** @@ -93,4 +103,17 @@ class CustomSelectorActivityTest { fun testOnBackPressed() { activity.onBackPressed() } + + /** + * Test onDestroy Function. + */ + @Test + @Throws(Exception::class) + fun testOnDestroy() { + val method: Method = CustomSelectorActivity::class.java.getDeclaredMethod( + "onDestroy" + ) + method.isAccessible = true + method.invoke(activity) + } } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt index 53094d6f7b..c77f243427 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/FolderFragmentTest.kt @@ -127,4 +127,15 @@ class FolderFragmentTest { func.isAccessible = true func.invoke(fragment, Result(CallbackStatus.SUCCESS, arrayListOf())) } + + /** + * Test onResume. + */ + @Test + fun testOnResume() { + val func = fragment.javaClass.getDeclaredMethod("onResume") + func.isAccessible = true + func.invoke(fragment) + } + } \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt index 9794003c82..10ebcc4e8c 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageFragmentTest.kt @@ -3,14 +3,17 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.Context import android.os.Bundle import android.os.Looper +import android.os.Looper.getMainLooper import android.view.LayoutInflater import android.view.View import android.widget.ProgressBar import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.soloader.SoLoader +import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.R import fr.free.nrw.commons.TestAppAdapter import fr.free.nrw.commons.TestCommonsApplication @@ -29,6 +32,7 @@ import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows +import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import org.wikipedia.AppAdapter @@ -50,6 +54,9 @@ class ImageFragmentTest { private lateinit var context: Context private lateinit var viewModelField: Field + @Mock + private lateinit var layoutManager: GridLayoutManager + @Mock private lateinit var image: Image @@ -71,7 +78,7 @@ class ImageFragmentTest { Fresco.initialize(context) val activity = Robolectric.buildActivity(CustomSelectorActivity::class.java).create().get() - fragment = ImageFragment.newInstance(1) + fragment = ImageFragment.newInstance(1,0) val fragmentManager: FragmentManager = activity.supportFragmentManager val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction() fragmentTransaction.add(fragment, null) @@ -132,4 +139,28 @@ class ImageFragmentTest { assertEquals(3, func.invoke(fragment)) } + + /** + * Test onResume. + */ + @Test + fun testOnResume() { + val func = fragment.javaClass.getDeclaredMethod("onResume") + func.isAccessible = true + func.invoke(fragment) + } + + /** + * Test onDestroy. + */ + @Test + fun testOnDestroy() { + shadowOf(getMainLooper()).idle() + selectorRV.layoutManager = layoutManager + whenever(layoutManager.findFirstVisibleItemPosition()).thenReturn(1) + val func = fragment.javaClass.getDeclaredMethod("onDestroy") + func.isAccessible = true + func.invoke(fragment) + } + } \ No newline at end of file From 9333e52e632ff4e389cf5c66de643c7bebab74d0 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 10 Aug 2021 16:18:16 +0530 Subject: [PATCH 22/28] orientation fixed (#4540) --- app/src/main/AndroidManifest.xml | 7 +++++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1c10683275..a2c33778fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,8 +110,11 @@ - + Settings Sign Up Featured Images + Custom Selector Category Peer Review About From 2e2b2860f54f46f07d993a2eec7431951cd89867 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Tue, 10 Aug 2021 17:59:33 +0530 Subject: [PATCH 23/28] refractoring (#4541) --- .../ContributionsListFragment.java | 3 + .../customselector/helper/ImageHelper.kt | 6 +- .../listeners/FolderClickListener.kt | 10 +++ .../listeners/ImageLoaderListener.kt | 14 ++++ .../listeners/ImageSelectListener.kt | 8 +++ .../customselector/model/CallbackStatus.kt | 4 ++ .../commons/customselector/model/Folder.kt | 3 + .../nrw/commons/customselector/model/Image.kt | 3 + .../commons/customselector/model/Result.kt | 3 + .../ui/adapter/FolderAdapter.kt | 6 +- .../customselector/ui/adapter/ImageAdapter.kt | 3 + .../ui/selector/CustomSelectorActivity.kt | 3 + .../ui/selector/CustomSelectorViewModel.kt | 3 + .../ui/selector/FolderFragment.kt | 3 + .../ui/selector/ImageFileLoader.kt | 8 ++- .../commons/di/CommonsApplicationModule.java | 5 ++ .../nrw/commons/filepicker/FilePicker.java | 14 ++++ .../nrw/commons/filepicker/PickedFiles.java | 64 ++++++++++++++++++- .../free/nrw/commons/upload/UploadItem.java | 5 ++ 19 files changed, 157 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index d2bceae1fe..0b739e316b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -269,6 +269,9 @@ private void setListeners() { }); } + /** + * Launch Custom Selector. + */ @OnClick(R.id.fab_custom_gallery) void launchCustomSelector(){ controller.initiateCustomGalleryPickWithPermission(getActivity()); diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt index 1447cd2d73..06ec4c36c8 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt @@ -6,7 +6,6 @@ import fr.free.nrw.commons.customselector.model.Image /** * Image Helper object, includes all the static functions required by custom selector. */ - object ImageHelper { /** @@ -65,10 +64,7 @@ object ImageHelper { */ fun getIndexList(list: ArrayList, masterList: ArrayList): ArrayList { - /** - * TODO - * Can be optimised as masterList is sorted by time. - */ + // Can be optimised as masterList is sorted by time. val indexes = arrayListOf() for(image in list) { diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt index e016a71ba6..bc3bd518dc 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/FolderClickListener.kt @@ -1,5 +1,15 @@ package fr.free.nrw.commons.customselector.listeners +/** + * Custom Selector Folder Click Listener + */ interface FolderClickListener { + + /** + * onFolderClick + * @param folderId : folder id of the folder. + * @param folderName : folder name of the folder. + * @param lastItemId : last scroll position in the folder. + */ fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt index f8540c90a4..5ba43082d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt @@ -2,7 +2,21 @@ package fr.free.nrw.commons.customselector.listeners import fr.free.nrw.commons.customselector.model.Image +/** + * Custom Selector Image Loader Listener + * responds to the device image query. + */ interface ImageLoaderListener { + + /** + * On image loaded + * @param images : queried device images. + */ fun onImageLoaded(images: ArrayList) + + /** + * On failed + * @param throwable : throwable exception on failure. + */ fun onFailed(throwable: Throwable) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt index c29aa21e2e..688c93e649 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt @@ -2,6 +2,14 @@ package fr.free.nrw.commons.customselector.listeners import fr.free.nrw.commons.customselector.model.Image +/** + * Custom selector Image select listener + */ interface ImageSelectListener { + + /** + * onSelectedImagesChanged + * @param selectedImages : new selected images. + */ fun onSelectedImagesChanged(selectedImages: ArrayList) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt index 257b39a950..5cdcfb9bf7 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/CallbackStatus.kt @@ -1,5 +1,9 @@ package fr.free.nrw.commons.customselector.model +/** + * sealed class Callback Status. + * Current status of the device image query. + */ sealed class CallbackStatus { /** IDLE : The callback is idle , doing nothing. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt index 0ce95ec22d..6857589bdf 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Folder.kt @@ -1,5 +1,8 @@ package fr.free.nrw.commons.customselector.model +/** + * Custom selector data class Folder. + */ data class Folder( /** bucketId : Unique directory id, eg 540528482 diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt index d6d296f29d..12e75580db 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Image.kt @@ -4,6 +4,9 @@ import android.net.Uri import android.os.Parcel import android.os.Parcelable +/** + * Custom selector data class Image. + */ data class Image( /** id : Unique image id, primary key of image in device, eg 104950 diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt index 0eb4decbd0..11ed8ef006 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/model/Result.kt @@ -1,5 +1,8 @@ package fr.free.nrw.commons.customselector.model +/** + * Custom selector data class Result. + */ data class Result( /** * CallbackStatus : stores the result status diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt index 03790a7d87..60d2994917 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/FolderAdapter.kt @@ -13,7 +13,10 @@ import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Image -class FolderAdapter( +/** + * Custom selector FolderAdapter. + */ +class FolderAdapter( /** * Application context. */ @@ -91,7 +94,6 @@ class FolderAdapter( diffResult.dispatchUpdatesTo(this) } - /** * returns item count. */ diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 2dd97f3e3e..6712762036 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -16,6 +16,9 @@ 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 +/** + * Custom selector ImageAdapter. + */ class ImageAdapter( /** * Application Context. diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 972c16fc49..2192399746 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -16,6 +16,9 @@ import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject +/** + * Custom Selector Activity. + */ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectListener { /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt index 4f56a808b5..cd7858d1b6 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorViewModel.kt @@ -11,6 +11,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +/** + * Custom Selector view model. + */ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index b1cd8ab371..45237e92d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -22,6 +22,9 @@ import fr.free.nrw.commons.upload.FileProcessor import kotlinx.android.synthetic.main.fragment_custom_selector.view.* import javax.inject.Inject +/** + * Custom selector folder fragment. + */ class FolderFragment : CommonsDaggerSupportFragment() { /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt index 95cb8233ff..12e883a144 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFileLoader.kt @@ -9,6 +9,10 @@ import kotlinx.coroutines.* import java.io.File import kotlin.coroutines.CoroutineContext +/** + * Custom Selector Image File Loader. + * Loads device images. + */ class ImageFileLoader(val context: Context) : CoroutineScope{ /** @@ -39,7 +43,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{ /** - * Load the device images using cursor + * Load Device images using cursor */ private fun getImages(listener:ImageLoaderListener) { val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC") @@ -99,7 +103,7 @@ class ImageFileLoader(val context: Context) : CoroutineScope{ //todo Abort loading images. } - /** + /* * * TODO * Sha1 for image (original image). diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index 7d9c061ffc..bffda8332c 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -68,6 +68,11 @@ public CommonsApplicationModule(Context applicationContext) { this.applicationContext = applicationContext; } + /** + * Provides ImageFileLoader used to fetch device images. + * @param context + * @return + */ @Provides public ImageFileLoader providesImageFileLoader(Context context) { return new ImageFileLoader(context); diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java index 6d516abd96..bc43cb1543 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java @@ -53,6 +53,12 @@ private static Intent createGalleryIntent(@NonNull Context context, int type) { .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()); } + /** + * CreateCustomSectorIntent, creates intent for custom selector activity. + * @param context + * @param type + * @return Custom selector intent + */ private static Intent createCustomSelectorIntent(@NonNull Context context, int type) { storeType(context, type); return new Intent(context, CustomSelectorActivity.class); @@ -215,6 +221,10 @@ private static void onPictureReturnedFromDocuments(Intent data, Activity activit } } + /** + * onPictureReturnedFromCustomSelector. + * Retrieve and forward the images to upload wizard through callback. + */ private static void onPictureReturnedFromCustomSelector(Intent data, Activity activity, @NonNull FilePicker.Callbacks callbacks) { try { List files = getFilesFromCustomSelector(data, activity); @@ -225,6 +235,10 @@ private static void onPictureReturnedFromCustomSelector(Intent data, Activity ac } } + /** + * Get files from custom selector + * Retrieve and process the selected images from the custom selector. + */ private static List getFilesFromCustomSelector(Intent data, Activity activity) throws IOException, SecurityException { List files = new ArrayList<>(); ArrayList images = data.getParcelableArrayListExtra("Images"); diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java index c5eb101bc3..ea59831731 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java @@ -24,19 +24,38 @@ import timber.log.Timber; - +/** + * PickedFiles. + * Process the upload items. + */ public class PickedFiles implements Constants { + /** + * Get Folder Name + * @param context + * @return default application folder name. + */ private static String getFolderName(@NonNull Context context) { return FilePicker.configuration(context).getFolderName(); } + /** + * tempImageDirectory + * @param context + * @return temporary image directory to copy and perform exif changes. + */ private static File tempImageDirectory(@NonNull Context context) { File privateTempDir = new File(context.getCacheDir(), DEFAULT_FOLDER_NAME); if (!privateTempDir.exists()) privateTempDir.mkdirs(); return privateTempDir; } + /** + * writeToFile + * writes inputStream data to the destination file. + * @param in input stream of source file. + * @param file destination file + */ private static void writeToFile(InputStream in, File file) { try { OutputStream out = new FileOutputStream(file); @@ -52,11 +71,24 @@ private static void writeToFile(InputStream in, File file) { } } + /** + * Copy file function. + * Copies source file to destination file. + * @param src source file + * @param dst destination file + * @throws IOException (File input stream exception) + */ private static void copyFile(File src, File dst) throws IOException { InputStream in = new FileInputStream(src); writeToFile(in, dst); } + /** + * Copy files in separate thread. + * Copies all the uploadable files to the temp image folder on background thread. + * @param context + * @param filesToCopy uploadable file list to be copied. + */ static void copyFilesInSeparateThread(final Context context, final List filesToCopy) { new Thread(() -> { List copiedFiles = new ArrayList<>(); @@ -64,7 +96,9 @@ static void copyFilesInSeparateThread(final Context context, final List singleFileList(UploadableFile file) { List list = new ArrayList<>(); list.add(file); return list; } + /** + * ScanCopiedImages + * Scan copied images metadata using media scanner. + * @param context + * @param copiedImages copied images list. + */ static void scanCopiedImages(Context context, List copiedImages) { String[] paths = new String[copiedImages.size()]; for (int i = 0; i < copiedImages.size(); i++) { @@ -104,6 +150,12 @@ static void scanCopiedImages(Context context, List copiedImages) { }); } + /** + * pickedExistingPicture + * convert the image into uploadable file. + * @param photoUri Uri of the image. + * @return Uploadable file ready for tag redaction. + */ public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri); File directory = tempImageDirectory(context); @@ -116,6 +168,9 @@ public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri return new UploadableFile(photoUri, photoFile); } + /** + * getCameraPictureLocation + */ static File getCameraPicturesLocation(@NonNull Context context) throws IOException { File dir = tempImageDirectory(context); return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir); @@ -142,6 +197,11 @@ private static String getMimeType(@NonNull Context context, @NonNull Uri uri) { return extension; } + /** + * GetUriToFile + * @param file get uri of file + * @return uri of requested file. + */ static Uri getUriToFile(@NonNull Context context, @NonNull File file) { String packageName = context.getApplicationContext().getPackageName(); String authority = packageName + ".provider"; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java index bed3e3454f..d7a9c9582b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java @@ -69,6 +69,11 @@ public int getImageQuality() { return imageQuality.getValue(); } + /** + * getContentUri. + * @return Uri of uploadItem + * Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10) + */ public Uri getContentUri() { return contentUri; } public void setImageQuality(final int imageQuality) { From 029f1708037b200c4d66f85c6af8c85e071dbd82 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Thu, 12 Aug 2021 17:19:07 +0530 Subject: [PATCH 24/28] refractoring (#4545) --- .../commons/customselector/ui/selector/FolderFragment.kt | 4 ++++ .../commons/customselector/ui/selector/ImageFragment.kt | 7 +++++++ .../nrw/commons/customselector/ui/selector/ImageLoader.kt | 7 +++++++ .../main/java/fr/free/nrw/commons/upload/UploadItem.java | 5 +++++ 4 files changed, 23 insertions(+) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt index 45237e92d4..456c14831a 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/FolderFragment.kt @@ -123,6 +123,10 @@ class FolderFragment : CommonsDaggerSupportFragment() { } } + /** + * onResume + * notifyDataSetChanged, rebuild the holder views to account for deleted images, folders. + */ override fun onResume() { folderAdapter.notifyDataSetChanged() super.onResume() diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index ef49f27e7e..ef9ae1fceb 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -26,6 +26,9 @@ import java.io.FileInputStream import java.net.URI import javax.inject.Inject +/** + * Custom Selector Image Fragment. + */ class ImageFragment: CommonsDaggerSupportFragment() { /** @@ -169,6 +172,10 @@ class ImageFragment: CommonsDaggerSupportFragment() { // todo change span count depending on the device orientation and other factos. } + /** + * onResume + * notifyDataSetChanged, rebuild the holder views to account for deleted images. + */ override fun onResume() { imageAdapter.notifyDataSetChanged() super.onResume() diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 73b2f1f79c..cf45a87c14 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -266,7 +266,14 @@ class ImageLoader @Inject constructor( object ERROR : Result() } + /** + * Companion Object + */ companion object { + /** + * Invalidate Day count. + * False Database Entries are invalid after INVALIDATE_DAY_COUNT and need to be re-queried. + */ const val INVALIDATE_DAY_COUNT: Long = 7 } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java index d7a9c9582b..1b482717f1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java @@ -23,6 +23,11 @@ public class UploadItem { private final String createdTimestampSource; private final BehaviorSubject imageQuality; private boolean hasInvalidLocation; + + /** + * Uri of uploadItem + * Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10) + */ private final Uri contentUri; From 0ebbc9d1df20883d325fb1546df2cbabdc929534 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Sat, 14 Aug 2021 06:33:26 +0530 Subject: [PATCH 25/28] [GSoC] Welcome Dialog (#4546) * Welcome Dialog * Condition Fix * Orientation, back button Fix --- .../customselector/ui/adapter/ImageAdapter.kt | 2 +- .../ui/selector/CustomSelectorActivity.kt | 25 +++- .../layout/custom_selector_info_dialog.xml | 133 ++++++++++++++++++ app/src/main/res/values/strings.xml | 5 + 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/layout/custom_selector_info_dialog.xml diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index 6712762036..ea7505aebf 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -110,7 +110,7 @@ class ImageAdapter( } } else { if(holder.isItemUploaded()){ - Toast.makeText(context, "Already Uploaded image", Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() } else { selectedImages.add(images[position]) notifyItemChanged(position, ImageSelectedOrUpdated()) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 2192399746..02b0a8b1a7 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -1,10 +1,13 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity +import android.app.Dialog import android.content.Intent import android.content.SharedPreferences 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.lifecycle.ViewModelProvider @@ -16,6 +19,7 @@ import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject + /** * Custom Selector Activity. */ @@ -55,10 +59,18 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi setContentView(R.layout.activity_custom_selector) prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE) - viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get(CustomSelectorViewModel::class.java) + viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get( + CustomSelectorViewModel::class.java + ) setupViews() + if(prefs.getBoolean("customSelectorFirstLaunch", true)) { + // show welcome dialog on first launch + showWelcomeDialog() + prefs.edit().putBoolean("customSelectorFirstLaunch", false).apply() + } + // Open folder if saved in prefs. if(prefs.contains(FOLDER_ID)){ val lastOpenFolderId: Long = prefs.getLong(FOLDER_ID, 0L) @@ -68,6 +80,17 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi } } + /** + * Show Custom Selector Welcome Dialog. + */ + private fun showWelcomeDialog() { + val dialog = Dialog(this) + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + dialog.setContentView(R.layout.custom_selector_info_dialog) + (dialog.findViewById(R.id.btn_ok) as Button).setOnClickListener { dialog.dismiss() } + dialog.show() + } + /** * Set up view, default folder view. */ diff --git a/app/src/main/res/layout/custom_selector_info_dialog.xml b/app/src/main/res/layout/custom_selector_info_dialog.xml new file mode 100644 index 0000000000..a34f247dd3 --- /dev/null +++ b/app/src/main/res/layout/custom_selector_info_dialog.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e43bae7b55..4a04fe47d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -651,5 +651,10 @@ Upload your first media by tapping on the add button. No Images Done Back + Welcome to Custom Picture Selector + This picker shows differently pictures that are already to Commons. + Unlike the picture on the left, the picture on the right has the Commons logo indicating it is already uploaded. + Awesome + This image has already been uploaded to Commons. From 25792e2165b0bf90a3f2e1307153285603d9b636 Mon Sep 17 00:00:00 2001 From: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com> Date: Wed, 18 Aug 2021 05:22:24 +0530 Subject: [PATCH 26/28] [GSoC] Image preview (#4559) * Image preview * refractor --- .../customselector/listeners/ImageSelectListener.kt | 7 +++++++ .../commons/customselector/ui/adapter/ImageAdapter.kt | 6 ++++++ .../ui/selector/CustomSelectorActivity.kt | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt index 688c93e649..1d7310b1d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt @@ -1,5 +1,6 @@ package fr.free.nrw.commons.customselector.listeners +import android.net.Uri import fr.free.nrw.commons.customselector.model.Image /** @@ -12,4 +13,10 @@ interface ImageSelectListener { * @param selectedImages : new selected images. */ fun onSelectedImagesChanged(selectedImages: ArrayList) + + /** + * onLongPress + * @param imageUri : uri of image + */ + fun onLongPress(imageUri: Uri) } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index ea7505aebf..f3fce1cc0e 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -93,6 +93,12 @@ class ImageAdapter( holder.itemView.setOnClickListener { selectOrRemoveImage(holder, position) } + + // launch media preview on long click. + holder.itemView.setOnLongClickListener { + imageSelectListener.onLongPress(image.uri) + true + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 02b0a8b1a7..8dffe73064 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -4,6 +4,7 @@ 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 @@ -15,6 +16,7 @@ import fr.free.nrw.commons.R 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.media.ZoomableActivity import fr.free.nrw.commons.theme.BaseActivity import java.io.File import javax.inject.Inject @@ -156,6 +158,15 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi done.visibility = if (selectedImages.isEmpty()) View.INVISIBLE 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) + } + /** * OnDone clicked. * Get the selected images. Remove any non existent file, forward the data to finish selector. From 50650a169880ff4bc283f24afc6d4e324943b1e9 Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Wed, 18 Aug 2021 16:40:44 +0530 Subject: [PATCH 27/28] update database version --- app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java | 2 +- app/src/main/res/values/attrs.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 315bafe414..f9ba4a5ae3 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -13,7 +13,7 @@ public class DBOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "commons.db"; - private static final int DATABASE_VERSION = 17; + private static final int DATABASE_VERSION = 18; public static final String CONTRIBUTIONS_TABLE = "contributions"; private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s"; diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index fb61e8d18a..6983f67441 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -48,7 +48,6 @@ - From 3f2d929512619e1d4ded3fa993f6674a61bbc012 Mon Sep 17 00:00:00 2001 From: Aditya Srivastava Date: Wed, 18 Aug 2021 17:02:36 +0530 Subject: [PATCH 28/28] remove duplicates --- app/src/main/res/values/attrs.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index d944d2d0bc..f43772fb55 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -48,8 +48,6 @@ - -