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 778b1afdc..27cef1c0f 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.Context; 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 class ContributionController { * 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 c2c6c4086..d2bceae1f 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 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl }); } - @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 1b676b6e2..9228dc5ac 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 53de6de77..a38200463 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 1ab30f67e..099c89a86 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 26b8033ba..4f56a808b 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 83d838bc2..4b5b91e68 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 @@ public interface Constants { 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 698e2d51f..6d516abd9 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 android.text.TextUtils; 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 @@ public class FilePicker implements Constants { .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 class FilePicker implements Constants { 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 class FilePicker implements Constants { 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 @@ public class FilePicker implements Constants { } } + 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 class FilePicker implements Constants { public enum ImageSource { - GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO + GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR } public interface Callbacks {