From 70acd7b28166aeb5201680966537926238da5286 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] [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 | 41 ++++++++++--- .../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, 186 insertions(+), 83 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 0a751d47b..1447cd2d7 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 cb32807f8..e016a71ba 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 67dcc789c..93759bdf4 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() - //todo load image thumbnail. + 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 + + 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 ff41048f0..8225ab2dc 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 3b8bee390..972c16fc4 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 e43c0798c..b1cd8ab37 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 cbb3fc442..b575e015b 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 3b5254f86..73b2f1f79 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 000000000..7dd39280a --- /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 e9852f49a..923cc8343 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"/>