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 <nicolas.raoul@gmail.com>
This commit is contained in:
Evangelos Talos 2024-05-02 14:12:32 +03:00 committed by GitHub
parent 6aa9303d0f
commit c178c5de41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 210 additions and 99 deletions

View file

@ -142,6 +142,7 @@ public class MainActivity extends BaseActivity
} else { } else {
if (applicationKvStore.getBoolean("firstrun", true)) { if (applicationKvStore.getBoolean("firstrun", true)) {
applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false);
applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false);
} }
if(savedInstanceState == null){ if(savedInstanceState == null){
//starting a fresh fragment. //starting a fresh fragment.

View file

@ -20,8 +20,11 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.CheckBox;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;
@ -100,6 +103,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private Place place; private Place place;
private LatLng prevLocation; private LatLng prevLocation;
private LatLng currLocation; private LatLng currLocation;
private static boolean uploadIsOfAPlace = false;
private boolean isInAppCameraUpload; private boolean isInAppCameraUpload;
private List<UploadableFile> uploadableFiles = Collections.emptyList(); private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0; 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 * 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 * should be displayed if permissions are missing and it is first time calling
* `checkStoragePermissions` method. * `checkStoragePermissions` method.
*
* This variable is used in the `checkStoragePermissions` method to determine whether to * 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. * 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, * 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 * a permissions dialog will be displayed to request the required permissions. If set
* to `false`, the dialog won't be shown. * 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() { private void receiveSharedItems() {
thumbnailsAdapter.context=this; thumbnailsAdapter.context=this;
Intent intent = getIntent(); Intent intent = getIntent();
@ -452,8 +463,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
handleNullMedia(); handleNullMedia();
} else { } else {
//Show thumbnails //Show thumbnails
if (uploadableFiles.size() if (uploadableFiles.size() > 1){
> 1) {//If there is only file, no need to show the image thumbnails 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); thumbnailsAdapter.setUploadableFiles(uploadableFiles);
} else { } else {
binding.llContainerTopCard.setVisibility(View.GONE); 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) { for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( if (!uploadIsOfAPlace) {
this, locationManager, null); handleLocation();
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation);
currLocation = locationManager.getLastLocation(); 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() { UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() {
@Override @Override
public void deletePictureAtIndex(int index) { public void deletePictureAtIndex(int index) {
@ -930,4 +887,106 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
this::finish 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;
}
}
}
} }

View file

@ -374,8 +374,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
if (response) { if (response) {
if (callback != null) { if (callback != null) {
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace);
indexOfFragment);
} }
} }
} else { } else {
@ -395,19 +394,41 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null); final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null);
ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage); ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage);
nearbyFoundImage.setImageURI(uploadItem.getMediaUri()); nearbyFoundImage.setImageURI(uploadItem.getMediaUri());
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.upload_nearby_place_found_title), final Activity activity = getActivity();
String.format(Locale.getDefault(),
getString(R.string.upload_nearby_place_found_description), if (activity instanceof UploadActivity) {
place.getName()), final boolean isMultipleFilesSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
() -> {
UploadActivity.nearbyPopupAnswers.put(place, true); // Determine the message based on the selection status
presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment); String message;
}, if (isMultipleFilesSelected) {
() -> { // Use plural message if multiple files are selected
UploadActivity.nearbyPopupAnswers.put(place, false); message = String.format(Locale.getDefault(),
}, getString(R.string.upload_nearby_place_found_description_plural),
customLayout, true); 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 @Override
@ -440,8 +461,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) { if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace); final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
if (response) { if (response) {
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace);
indexOfFragment);
} }
} else { } else {
showNearbyPlaceFound(nearbyPlace); showNearbyPlaceFound(nearbyPlace);

View file

@ -109,7 +109,7 @@ public interface UploadMediaDetailsContract {
void onEditButtonClicked(int indexInViewFlipper); void onEditButtonClicked(int indexInViewFlipper);
void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition); void onUserConfirmedUploadIsOfPlace(Place place);
} }
} }

View file

@ -76,6 +76,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
*/ */
public static boolean isBatteryDialogShowing; public static boolean isBatteryDialogShowing;
public static boolean isCategoriesDialogShowing;
@Inject @Inject
public UploadMediaPresenter(UploadRepository uploadRepository, public UploadMediaPresenter(UploadRepository uploadRepository,
@Named("default_preferences") JsonKvStore defaultKVStore, @Named("default_preferences") JsonKvStore defaultKVStore,
@ -329,18 +331,28 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
view.showEditActivity(repository.getUploads().get(indexInViewFlipper)); 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 @Override
public void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition) { public void onUserConfirmedUploadIsOfPlace(Place place) {
final List<UploadMediaDetail> uploadMediaDetails = repository.getUploads() final List<UploadItem> uploads = repository.getUploads();
.get(uploadItemPosition) for (UploadItem uploadItem : uploads) {
.getUploadMediaDetails(); uploadItem.setPlace(place);
UploadItem uploadItem = repository.getUploads() final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
.get(uploadItemPosition); // Update UploadMediaDetail object for this UploadItem
uploadItem.setPlace(place); uploadMediaDetails.set(0, new UploadMediaDetail(place));
uploadMediaDetails.set(0, new UploadMediaDetail(place)); }
view.updateMediaDetails(uploadMediaDetails); // 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 * Calculates the image quality
* *
@ -410,7 +422,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
} }
if (uploadItemIndex == 0) { if (uploadItemIndex == 0) {
if (!isBatteryDialogShowing) { if (!isBatteryDialogShowing && !isCategoriesDialogShowing) {
// if battery-optimisation dialog is not being shown, call checkImageQuality // if battery-optimisation dialog is not being shown, call checkImageQuality
checkImageQuality(uploadItem, uploadItemIndex); checkImageQuality(uploadItem, uploadItemIndex);
} else { } else {

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/upload_root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<CheckBox
android:id="@+id/categories_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Don't show this message again" />
</LinearLayout>

View file

@ -610,7 +610,8 @@ Upload your first media by tapping on the add button.</string>
<string name="title_for_parent_classes">PARENT CLASSES</string> <string name="title_for_parent_classes">PARENT CLASSES</string>
<string name="upload_nearby_place_found_title">Nearby Place Found</string> <string name="upload_nearby_place_found_title">Nearby Place Found</string>
<string name="upload_nearby_place_found_description">Is this a photo of %1$s?</string> <string name="upload_nearby_place_found_description_plural">Are these pictures of %1$s?</string>
<string name="upload_nearby_place_found_description_singular">Is this a picture of %1$s?</string>
<string name="title_app_shortcut_bookmark">Bookmarks</string> <string name="title_app_shortcut_bookmark">Bookmarks</string>
<string name="title_app_shortcut_setting">Settings</string> <string name="title_app_shortcut_setting">Settings</string>
<string name="remove_bookmark">Removed from bookmarks</string> <string name="remove_bookmark">Removed from bookmarks</string>
@ -815,4 +816,6 @@ Upload your first media by tapping on the add button.</string>
<item quantity="one">%d image selected</item> <item quantity="one">%d image selected</item>
<item quantity="other">%d images selected</item> <item quantity="other">%d images selected</item>
</plurals> </plurals>
<string name="multiple_files_depiction">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.</string>
<string name="multiple_files_depiction_header">Note about multi-uploads</string>
</resources> </resources>