From c178c5de419296e8abad3fead8174bacb9369cdd Mon Sep 17 00:00:00 2001 From: Evangelos Talos <115378448+vtalos@users.noreply.github.com> Date: Thu, 2 May 2024 14:12:32 +0300 Subject: [PATCH] Enhancing Multi-Upload Functionality for Consistent Depiction Categorization (#5700) * Add AlertDialog for categories and modularize receiveSharedItems * Improve nearby-place search function for a multi-upload Enhance the depiction consistency of a multi-upload by ensuring that it corresponds to a single place * Add javadoc * Update strings.xml * Renamed setImageTobeUploaded to setImageToBeUploaded * Make uploadIsOnPlace private & add a setter * Rename uploadIsOnPlace to uploadIsOfAPlace * Use singular when there is only one picture * Add a 'Do not show again' checkbox on the dialog * Update strings.xml --------- Co-authored-by: Giannis Karyotakis <110292528+karyotakisg@users.noreply.github.com> Co-authored-by: Nicolas Raoul --- .../commons/contributions/MainActivity.java | 1 + .../nrw/commons/upload/UploadActivity.java | 199 ++++++++++++------ .../UploadMediaDetailFragment.java | 54 +++-- .../UploadMediaDetailsContract.java | 2 +- .../mediaDetails/UploadMediaPresenter.java | 32 ++- .../activity_upload_categories_dialog.xml | 16 ++ app/src/main/res/values/strings.xml | 5 +- 7 files changed, 210 insertions(+), 99 deletions(-) create mode 100644 app/src/main/res/layout/activity_upload_categories_dialog.xml diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 7862493fd..63bde1be9 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -142,6 +142,7 @@ public class MainActivity extends BaseActivity } else { if (applicationKvStore.getBoolean("firstrun", true)) { applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); + applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); } if(savedInstanceState == null){ //starting a fresh fragment. diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 6b46a103f..707bf1363 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -20,8 +20,11 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.provider.Settings; import android.util.DisplayMetrics; +import android.view.LayoutInflater; import android.view.View; +import android.widget.CheckBox; import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; @@ -100,6 +103,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, private Place place; private LatLng prevLocation; private LatLng currLocation; + private static boolean uploadIsOfAPlace = false; private boolean isInAppCameraUpload; private List uploadableFiles = Collections.emptyList(); private int currentSelectedPosition = 0; @@ -123,10 +127,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * when necessary. Initially, it is set to `true`, indicating that the permissions dialog * should be displayed if permissions are missing and it is first time calling * `checkStoragePermissions` method. - * * This variable is used in the `checkStoragePermissions` method to determine whether to * show a permissions dialog to the user if the required permissions are not granted. - * * If `showPermissionsDialog` is set to `true` and the necessary permissions are missing, * a permissions dialog will be displayed to request the required permissions. If set * to `false`, the dialog won't be shown. @@ -438,6 +440,15 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } } + /** + * Sets the flag indicating whether the upload is of a specific place. + * + * @param uploadOfAPlace a boolean value indicating whether the upload is of place. + */ + public static void setUploadIsOfAPlace(boolean uploadOfAPlace) { + uploadIsOfAPlace = uploadOfAPlace; + } + private void receiveSharedItems() { thumbnailsAdapter.context=this; Intent intent = getIntent(); @@ -452,8 +463,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, handleNullMedia(); } else { //Show thumbnails - if (uploadableFiles.size() - > 1) {//If there is only file, no need to show the image thumbnails + if (uploadableFiles.size() > 1){ + if(!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")){//If there is only file, no need to show the image thumbnails + showAlertDialogForCategories(); + } + if (uploadableFiles.size() > 3 && + !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")){ + showAlertForBattery(); + } thumbnailsAdapter.setUploadableFiles(uploadableFiles); } else { binding.llContainerTopCard.setVisibility(View.GONE); @@ -467,77 +484,17 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } - /* Suggest users to turn battery optimisation off when uploading more than a few files. - That's because we have noticed that many-files uploads have - a much higher probability of failing than uploads with less files. - - Show the dialog for Android 6 and above as - the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS intent was added in API level 23 - */ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (uploadableFiles.size() > 3 - && !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")) { - // When battery-optimisation dialog is shown don't show the image quality dialog - UploadMediaPresenter.isBatteryDialogShowing = true; - DialogUtil.showAlertDialog( - this, - getString(R.string.unrestricted_battery_mode), - getString(R.string.suggest_unrestricted_mode), - getString(R.string.title_activity_settings), - getString(R.string.cancel), - () -> { - /* Since opening the right settings page might be device dependent, using - https://github.com/WaseemSabir/BatteryPermissionHelper - directly appeared like a promising idea. - However, this simply closed the popup and did not make - the settings page appear on a Pixel as well as a Xiaomi device. - - Used the standard intent instead of using this library as - it shows a list of all the apps on the device and allows users to - turn battery optimisation off. - */ - Intent batteryOptimisationSettingsIntent = new Intent( - Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); - startActivity(batteryOptimisationSettingsIntent); - // calling checkImageQuality after battery dialog is interacted with - // so that 2 dialogs do not pop up simultaneously - presenter.checkImageQuality(0); - UploadMediaPresenter.isBatteryDialogShowing = false; - }, - () -> { - presenter.checkImageQuality(0); - UploadMediaPresenter.isBatteryDialogShowing = false; - } - ); - defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true); - } - } for (UploadableFile uploadableFile : uploadableFiles) { UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); - LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( - this, locationManager, null); - if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { - currLocation = locationManager.getLastLocation(); + if (!uploadIsOfAPlace) { + handleLocation(); + uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); + locationManager.unregisterLocationManager(); + } else { + uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); } - if (currLocation != null) { - float locationDifference = getLocationDifference(currLocation, prevLocation); - boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); - /* Remove location if the user has unchecked the Location EXIF tag in the - Manage EXIF Tags setting or turned "Record location for in-app shots" off. - Also, location information is discarded if the difference between - current location and location recorded just before capturing the image - is greater than 100 meters */ - if (isLocationTagUnchecked || locationDifference > 100 - || !defaultKvStore.getBoolean("inAppCameraLocationPref") - || !isInAppCameraUpload) { - currLocation = null; - } - } - uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); - locationManager.unregisterLocationManager(); - UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() { @Override public void deletePictureAtIndex(int index) { @@ -930,4 +887,106 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, this::finish ); } + + /** + * If the user uploads more than 1 file informs that + * depictions/categories apply to all pictures of a multi upload. + * This method takes no arguments and does not return any value. + * It shows the AlertDialog and continues the flow of uploads. + */ + private void showAlertDialogForCategories() { + UploadMediaPresenter.isCategoriesDialogShowing = true; + // Inflate the custom layout + LayoutInflater inflater = getLayoutInflater(); + View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null); + CheckBox checkBox = view.findViewById(R.id.categories_checkbox); + // Create the alert dialog + AlertDialog alertDialog = new AlertDialog.Builder(this) + .setView(view) + .setTitle(getString(R.string.multiple_files_depiction_header)) + .setMessage(getString(R.string.multiple_files_depiction)) + .setPositiveButton("OK", (dialog, which) -> { + if (checkBox.isChecked()) { + // Save the user's choice to not show the dialog again + defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true); + } + presenter.checkImageQuality(0); + + UploadMediaPresenter.isCategoriesDialogShowing = false; + }) + .setNegativeButton("", null) + .create(); + alertDialog.show(); + } + + + /** Suggest users to turn battery optimisation off when uploading + * more than a few files. That's because we have noticed that + * many-files uploads have a much higher probability of failing + * than uploads with less files. Show the dialog for Android 6 + * and above as the ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + * intent was added in API level 23 + */ + private void showAlertForBattery(){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // When battery-optimisation dialog is shown don't show the image quality dialog + UploadMediaPresenter.isBatteryDialogShowing = true; + DialogUtil.showAlertDialog( + this, + getString(R.string.unrestricted_battery_mode), + getString(R.string.suggest_unrestricted_mode), + getString(R.string.title_activity_settings), + getString(R.string.cancel), + () -> { + /* Since opening the right settings page might be device dependent, using + https://github.com/WaseemSabir/BatteryPermissionHelper + directly appeared like a promising idea. + However, this simply closed the popup and did not make + the settings page appear on a Pixel as well as a Xiaomi device. + Used the standard intent instead of using this library as + it shows a list of all the apps on the device and allows users to + turn battery optimisation off. + */ + Intent batteryOptimisationSettingsIntent = new Intent( + Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); + startActivity(batteryOptimisationSettingsIntent); + // calling checkImageQuality after battery dialog is interacted with + // so that 2 dialogs do not pop up simultaneously + + UploadMediaPresenter.isBatteryDialogShowing = false; + }, + () -> { + UploadMediaPresenter.isBatteryDialogShowing = false; + } + ); + defaultKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", true); + } + } + + /** + * If the permission for Location is turned on and certain + * conditions are met, returns current location of the user. + */ + private void handleLocation(){ + LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( + this, locationManager, null); + if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { + currLocation = locationManager.getLastLocation(); + } + + if (currLocation != null) { + float locationDifference = getLocationDifference(currLocation, prevLocation); + boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); + /* Remove location if the user has unchecked the Location EXIF tag in the + Manage EXIF Tags setting or turned "Record location for in-app shots" off. + Also, location information is discarded if the difference between + current location and location recorded just before capturing the image + is greater than 100 meters */ + if (isLocationTagUnchecked || locationDifference > 100 + || !defaultKvStore.getBoolean("inAppCameraLocationPref") + || !isInAppCameraUpload) { + currLocation = null; + } + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java index 3d1305023..2cf284705 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java @@ -374,8 +374,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); if (response) { if (callback != null) { - presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, - indexOfFragment); + presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace); } } } else { @@ -395,19 +394,41 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null); ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage); nearbyFoundImage.setImageURI(uploadItem.getMediaUri()); - DialogUtil.showAlertDialog(getActivity(), - getString(R.string.upload_nearby_place_found_title), - String.format(Locale.getDefault(), - getString(R.string.upload_nearby_place_found_description), - place.getName()), - () -> { - UploadActivity.nearbyPopupAnswers.put(place, true); - presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment); - }, - () -> { - UploadActivity.nearbyPopupAnswers.put(place, false); - }, - customLayout, true); + + final Activity activity = getActivity(); + + if (activity instanceof UploadActivity) { + final boolean isMultipleFilesSelected = ((UploadActivity) activity).getIsMultipleFilesSelected(); + + // Determine the message based on the selection status + String message; + if (isMultipleFilesSelected) { + // Use plural message if multiple files are selected + message = String.format(Locale.getDefault(), + getString(R.string.upload_nearby_place_found_description_plural), + place.getName()); + } else { + // Use singular message if only one file is selected + message = String.format(Locale.getDefault(), + getString(R.string.upload_nearby_place_found_description_singular), + place.getName()); + } + + // Show the AlertDialog with the determined message + DialogUtil.showAlertDialog(getActivity(), + getString(R.string.upload_nearby_place_found_title), + message, + () -> { + // Execute when user confirms the upload is of the specified place + UploadActivity.nearbyPopupAnswers.put(place, true); + presenter.onUserConfirmedUploadIsOfPlace(place); + }, + () -> { + // Execute when user cancels the upload of the specified place + UploadActivity.nearbyPopupAnswers.put(place, false); + }, + customLayout, true); + } } @Override @@ -440,8 +461,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) { final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); if (response) { - presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, - indexOfFragment); + presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace); } } else { showNearbyPlaceFound(nearbyPlace); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java index 21c7eae9f..9b789e046 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java @@ -109,7 +109,7 @@ public interface UploadMediaDetailsContract { void onEditButtonClicked(int indexInViewFlipper); - void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition); + void onUserConfirmedUploadIsOfPlace(Place place); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java index 4f9fe7d7c..7152d4d8f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java @@ -76,6 +76,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt */ public static boolean isBatteryDialogShowing; + public static boolean isCategoriesDialogShowing; + @Inject public UploadMediaPresenter(UploadRepository uploadRepository, @Named("default_preferences") JsonKvStore defaultKVStore, @@ -329,18 +331,28 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt view.showEditActivity(repository.getUploads().get(indexInViewFlipper)); } + /** + * Updates the information regarding the specified place for uploads + * when the user confirms the suggested nearby place. + * + * @param place The place to be associated with the uploads. + */ @Override - public void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition) { - final List uploadMediaDetails = repository.getUploads() - .get(uploadItemPosition) - .getUploadMediaDetails(); - UploadItem uploadItem = repository.getUploads() - .get(uploadItemPosition); - uploadItem.setPlace(place); - uploadMediaDetails.set(0, new UploadMediaDetail(place)); - view.updateMediaDetails(uploadMediaDetails); + public void onUserConfirmedUploadIsOfPlace(Place place) { + final List uploads = repository.getUploads(); + for (UploadItem uploadItem : uploads) { + uploadItem.setPlace(place); + final List uploadMediaDetails = uploadItem.getUploadMediaDetails(); + // Update UploadMediaDetail object for this UploadItem + uploadMediaDetails.set(0, new UploadMediaDetail(place)); + } + // Now that all UploadItems and their associated UploadMediaDetail objects have been updated, + // update the view with the modified media details of the first upload item + view.updateMediaDetails(uploads.get(0).getUploadMediaDetails()); + UploadActivity.setUploadIsOfAPlace(true); } + /** * Calculates the image quality * @@ -410,7 +422,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt } if (uploadItemIndex == 0) { - if (!isBatteryDialogShowing) { + if (!isBatteryDialogShowing && !isCategoriesDialogShowing) { // if battery-optimisation dialog is not being shown, call checkImageQuality checkImageQuality(uploadItem, uploadItemIndex); } else { diff --git a/app/src/main/res/layout/activity_upload_categories_dialog.xml b/app/src/main/res/layout/activity_upload_categories_dialog.xml new file mode 100644 index 000000000..fd1620f6e --- /dev/null +++ b/app/src/main/res/layout/activity_upload_categories_dialog.xml @@ -0,0 +1,16 @@ + + + + + + \ 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 04aa6af9b..017404d2c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -610,7 +610,8 @@ Upload your first media by tapping on the add button. PARENT CLASSES Nearby Place Found - Is this a photo of %1$s? + Are these pictures of %1$s? + Is this a picture of %1$s? Bookmarks Settings Removed from bookmarks @@ -815,4 +816,6 @@ Upload your first media by tapping on the add button. %d image selected %d images selected + Please remember that all images in a multi-upload get the same categories and depictions. If the images do not share depictions and categories, please perform several separate uploads. + Note about multi-uploads