5196: Fix in-app camera location loss (#5249)

Merging as this is a great improvement, additional issues/bugs can be filed as GitHub issues.

* fix in-app camera location loss

* fix failing unit tests

* UploadMediaDetailFragmentUnitTest: modify testOnActivityResultAddLocationDialog to have null location

* reintroduce removed variable

* enable prePopulateCategoriesAndDepictionsBy for current user location

* add relevant comment and fix failing test

* modify dialog and disable location tag redaction from EXIF

* modify in-app camera dialog flow and change location to inAppPictureLocation

* change location to inAppPictureLocation

* fix location flow

* preferences.xml: remove redundant default value

* inform users about location loss happening for first upload

* FileProcessor.kt: remove commented-out code

* prevent user location from getting attached to images with no EXIF location in normal and custom selector

* handle onPermissionDenied for location permission

* remove last location when the user turns the GPS off

* disable photo picker and in app camera preferences in settings for logged-out users

* remove debug statements and add toast inside runnables
This commit is contained in:
Ritika Pahwa 2023-09-01 12:15:50 +05:30 committed by GitHub
parent 1cab938d81
commit 5073ca08c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 537 additions and 92 deletions

View file

@ -6,6 +6,7 @@ import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.DefaultCallback;
@ -13,8 +14,14 @@ import fr.free.nrw.commons.filepicker.FilePicker;
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationPermissionsHelper;
import fr.free.nrw.commons.location.LocationPermissionsHelper.Dialog;
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList; import java.util.ArrayList;
@ -28,7 +35,11 @@ public class ContributionController {
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
private final JsonKvStore defaultKvStore; private final JsonKvStore defaultKvStore;
private LatLng locationBeforeImageCapture;
private boolean isInAppCameraUpload;
@Inject
LocationServiceManager locationManager;
@Inject @Inject
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
this.defaultKvStore = defaultKvStore; this.defaultKvStore = defaultKvStore;
@ -46,11 +57,94 @@ public class ContributionController {
PermissionUtils.checkPermissionsAndPerformAction(activity, PermissionUtils.checkPermissionsAndPerformAction(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> initiateCameraUpload(activity), () -> {
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
defaultKvStore.putBoolean("inAppCameraFirstRun", false);
askUserToAllowLocationAccess(activity);
} else if(defaultKvStore.getBoolean("inAppCameraLocationPref")) {
createDialogsAndHandleLocationPermissions(activity);
} else {
initiateCameraUpload(activity);
}
},
R.string.storage_permission_title, R.string.storage_permission_title,
R.string.write_storage_permission_rationale); R.string.write_storage_permission_rationale);
} }
/**
* Asks users to provide location access
*
* @param activity
*/
private void createDialogsAndHandleLocationPermissions(Activity activity) {
LocationPermissionsHelper.Dialog locationAccessDialog = new Dialog(
R.string.location_permission_title,
R.string.in_app_camera_location_permission_rationale
);
LocationPermissionsHelper.Dialog locationOffDialog = new Dialog(
R.string.ask_to_turn_location_on,
R.string.in_app_camera_needs_location
);
LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
activity, locationManager,
new LocationPermissionCallback() {
@Override
public void onLocationPermissionDenied() {
initiateCameraUpload(activity);
}
@Override
public void onLocationPermissionGranted() {
initiateCameraUpload(activity);
}
}
);
locationPermissionsHelper.handleLocationPermissions(
locationAccessDialog,
locationOffDialog
);
}
/**
* Suggest user to attach location information with pictures.
* If the user selects "Yes", then:
*
* Location is taken from the EXIF if the default camera application
* does not redact location tags.
*
* Otherwise, if the EXIF metadata does not have location information,
* then location captured by the app is used
*
* @param activity
*/
private void askUserToAllowLocationAccess(Activity activity) {
DialogUtil.showAlertDialog(activity,
activity.getString(R.string.in_app_camera_location_permission_title),
activity.getString(R.string.in_app_camera_location_access_explanation),
activity.getString(R.string.option_allow),
activity.getString(R.string.option_dismiss),
()-> {
defaultKvStore.putBoolean("inAppCameraLocationPref", true);
createDialogsAndHandleLocationPermissions(activity);
},
() -> {
defaultKvStore.putBoolean("inAppCameraLocationPref", false);
initiateCameraUpload(activity);
},
null,
true);
}
/**
* Check if apps have access to location even after having individual access
*
* @return
*/
private boolean isLocationAccessToAppsTurnedOn() {
return (locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled());
}
/** /**
* Initiate gallery picker * Initiate gallery picker
*/ */
@ -66,9 +160,7 @@ public class ContributionController {
PermissionUtils.checkPermissionsAndPerformAction(activity, PermissionUtils.checkPermissionsAndPerformAction(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> { () -> FilePicker.openCustomSelector(activity, 0),
FilePicker.openCustomSelector(activity, 0);
},
R.string.storage_permission_title, R.string.storage_permission_title,
R.string.write_storage_permission_rationale); R.string.write_storage_permission_rationale);
} }
@ -99,6 +191,10 @@ public class ContributionController {
*/ */
private void initiateCameraUpload(Activity activity) { private void initiateCameraUpload(Activity activity) {
setPickerConfiguration(activity, false); setPickerConfiguration(activity, false);
if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
locationBeforeImageCapture = locationManager.getLastLocation();
}
isInAppCameraUpload = true;
FilePicker.openCameraForImage(activity, 0); FilePicker.openCameraForImage(activity, 0);
} }
@ -134,7 +230,8 @@ public class ContributionController {
/** /**
* Returns intent to be passed to upload activity * Returns intent to be passed to upload activity
* Attaches place object for nearby uploads * Attaches place object for nearby uploads and
* location before image capture if in-app camera is used
*/ */
private Intent handleImagesPicked(Context context, private Intent handleImagesPicked(Context context,
List<UploadableFile> imagesFiles) { List<UploadableFile> imagesFiles) {
@ -148,6 +245,17 @@ public class ContributionController {
shareIntent.putExtra(PLACE_OBJECT, place); shareIntent.putExtra(PLACE_OBJECT, place);
} }
if (locationBeforeImageCapture != null) {
shareIntent.putExtra(
UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
locationBeforeImageCapture);
}
shareIntent.putExtra(
UploadActivity.IN_APP_CAMERA_UPLOAD,
isInAppCameraUpload
);
isInAppCameraUpload = false; // reset the flag for next use
return shareIntent; return shareIntent;
} }

View file

@ -403,6 +403,7 @@ public class MainActivity extends BaseActivity
if ((applicationKvStore.getBoolean("firstrun", true)) && if ((applicationKvStore.getBoolean("firstrun", true)) &&
(!applicationKvStore.getBoolean("login_skipped"))) { (!applicationKvStore.getBoolean("login_skipped"))) {
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
WelcomeActivity.startYourself(this); WelcomeActivity.startYourself(this);
} }
} }

View file

@ -0,0 +1,137 @@
package fr.free.nrw.commons.location;
import android.Manifest.permission;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.provider.Settings;
import android.widget.Toast;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.PermissionUtils;
/**
* Helper class to handle location permissions
*/
public class LocationPermissionsHelper {
Activity activity;
LocationServiceManager locationManager;
LocationPermissionCallback callback;
public LocationPermissionsHelper(Activity activity, LocationServiceManager locationManager,
LocationPermissionCallback callback) {
this.activity = activity;
this.locationManager = locationManager;
this.callback = callback;
}
public static class Dialog {
int dialogTitleResource;
int dialogTextResource;
public Dialog(int dialogTitle, int dialogText) {
dialogTitleResource = dialogTitle;
dialogTextResource = dialogText;
}
}
/**
* Handles the entire location permissions flow
*
* @param locationAccessDialog
* @param locationOffDialog
*/
public void handleLocationPermissions(Dialog locationAccessDialog,
Dialog locationOffDialog) {
requestForLocationAccess(locationAccessDialog, locationOffDialog);
}
/**
* Ask for location permission if the user agrees on attaching location with pictures
* and the app does not have the access to location
*
* @param locationAccessDialog
* @param locationOffDialog
*/
private void requestForLocationAccess(
Dialog locationAccessDialog,
Dialog locationOffDialog
) {
PermissionUtils.checkPermissionsAndPerformAction(activity,
permission.ACCESS_FINE_LOCATION,
() -> {
if(!isLocationAccessToAppsTurnedOn()) {
showLocationOffDialog(locationOffDialog);
} else {
if (callback != null) {
callback.onLocationPermissionGranted();
}
}
},
() -> {
if (callback != null) {
Toast.makeText(
activity,
R.string.in_app_camera_location_permission_denied,
Toast.LENGTH_LONG
).show();
callback.onLocationPermissionDenied();
}
},
locationAccessDialog.dialogTitleResource,
locationAccessDialog.dialogTextResource);
}
/**
* Check if apps have access to location even after having individual access
*
* @return
*/
public boolean isLocationAccessToAppsTurnedOn() {
return (locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled());
}
/**
* Ask user to grant location access to apps
*
*/
private void showLocationOffDialog(Dialog locationOffDialog) {
DialogUtil
.showAlertDialog(activity,
activity.getString(locationOffDialog.dialogTitleResource),
activity.getString(locationOffDialog.dialogTextResource),
activity.getString(R.string.title_app_shortcut_setting),
activity.getString(R.string.cancel),
() -> openLocationSettings(),
() -> {
Toast.makeText(
activity,
R.string.in_app_camera_location_unavailable,
Toast.LENGTH_LONG
).show();
callback.onLocationPermissionDenied();
});
}
/**
* Open location source settings so that apps with location access can access it
*
* TODO: modify it to fix https://github.com/commons-app/apps-android-commons/issues/5255
*/
private void openLocationSettings() {
final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
final PackageManager packageManager = activity.getPackageManager();
if (intent.resolveActivity(packageManager)!= null) {
activity.startActivity(intent);
}
}
/**
* Handle onPermissionDenied within individual classes based on the requirements
*/
public interface LocationPermissionCallback {
void onLocationPermissionDenied();
void onLocationPermissionGranted();
}
}

View file

@ -188,9 +188,9 @@ public class UploadRepository {
* @return * @return
*/ */
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place, public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface, LatLng inAppPictureLocation) {
return uploadModel.preProcessImage(uploadableFile, place, return uploadModel.preProcessImage(uploadableFile, place,
similarImageInterface); similarImageInterface, inAppPictureLocation);
} }
/** /**
@ -199,8 +199,8 @@ public class UploadRepository {
* @param uploadItem * @param uploadItem
* @return * @return
*/ */
public Single<Integer> getImageQuality(UploadItem uploadItem) { public Single<Integer> getImageQuality(UploadItem uploadItem, LatLng location) {
return uploadModel.getImageQuality(uploadItem); return uploadModel.getImageQuality(uploadItem, location);
} }
/** /**

View file

@ -18,6 +18,7 @@ import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference; import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
@ -30,13 +31,15 @@ import androidx.recyclerview.widget.RecyclerView.Adapter;
import com.karumi.dexter.Dexter; import com.karumi.dexter.Dexter;
import com.karumi.dexter.listener.PermissionGrantedResponse; import com.karumi.dexter.listener.PermissionGrantedResponse;
import com.karumi.dexter.listener.single.BasePermissionListener; import com.karumi.dexter.listener.single.BasePermissionListener;
import com.mapbox.mapboxsdk.Mapbox;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.campaigns.CampaignView; import fr.free.nrw.commons.campaigns.CampaignView;
import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationPermissionsHelper;
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.logging.CommonsLogSender; import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.recentlanguages.Language; import fr.free.nrw.commons.recentlanguages.Language;
import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter;
@ -65,6 +68,9 @@ public class SettingsFragment extends PreferenceFragmentCompat {
@Inject @Inject
RecentLanguagesDao recentLanguagesDao; RecentLanguagesDao recentLanguagesDao;
@Inject
LocationServiceManager locationManager;
private ListPreference themeListPreference; private ListPreference themeListPreference;
private Preference descriptionLanguageListPreference; private Preference descriptionLanguageListPreference;
private Preference appUiLanguageListPreference; private Preference appUiLanguageListPreference;
@ -97,6 +103,18 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}); });
} }
Preference inAppCameraLocationPref = findPreference("inAppCameraLocationPref");
inAppCameraLocationPref.setOnPreferenceChangeListener(
(preference, newValue) -> {
boolean isInAppCameraLocationTurnedOn = (boolean) newValue;
if (isInAppCameraLocationTurnedOn) {
createDialogsAndHandleLocationPermissions(getActivity());
}
return true;
}
);
// Gets current language code from shared preferences // Gets current language code from shared preferences
String languageCode; String languageCode;
@ -172,9 +190,45 @@ public class SettingsFragment extends PreferenceFragmentCompat {
findPreference("displayLocationPermissionForCardView").setEnabled(false); findPreference("displayLocationPermissionForCardView").setEnabled(false);
findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE).setEnabled(false); findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE).setEnabled(false);
findPreference("managed_exif_tags").setEnabled(false); findPreference("managed_exif_tags").setEnabled(false);
findPreference("openDocumentPhotoPickerPref").setEnabled(false);
findPreference("inAppCameraLocationPref").setEnabled(false);
} }
} }
/**
* Asks users to provide location access
*
* @param activity
*/
private void createDialogsAndHandleLocationPermissions(Activity activity) {
LocationPermissionsHelper.Dialog locationAccessDialog = new LocationPermissionsHelper.Dialog(
R.string.location_permission_title,
R.string.in_app_camera_location_permission_rationale
);
LocationPermissionsHelper.Dialog locationOffDialog = new LocationPermissionsHelper.Dialog(
R.string.ask_to_turn_location_on,
R.string.in_app_camera_needs_location
);
LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
activity, locationManager, new LocationPermissionCallback() {
@Override
public void onLocationPermissionDenied() {
// dismiss the dialog
}
@Override
public void onLocationPermissionGranted() {
// dismiss the dialog
}
});
locationPermissionsHelper.handleLocationPermissions(
locationAccessDialog,
locationOffDialog
);
}
/** /**
* On some devices, the new Photo Picker with GET_CONTENT takeover * On some devices, the new Photo Picker with GET_CONTENT takeover
* redacts location tags from EXIF metadata * redacts location tags from EXIF metadata

View file

@ -6,6 +6,7 @@ import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.mwapi.CategoryApi import fr.free.nrw.commons.mwapi.CategoryApi
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.settings.Prefs
@ -48,7 +49,8 @@ class FileProcessor @Inject constructor(
/** /**
* Processes filePath coordinates, either from EXIF data or user location * Processes filePath coordinates, either from EXIF data or user location
*/ */
fun processFileCoordinates(similarImageInterface: SimilarImageInterface, filePath: String?) fun processFileCoordinates(similarImageInterface: SimilarImageInterface,
filePath: String?, inAppPictureLocation: LatLng?)
: ImageCoordinates { : ImageCoordinates {
val exifInterface: ExifInterface? = try { val exifInterface: ExifInterface? = try {
ExifInterface(filePath!!) ExifInterface(filePath!!)
@ -59,7 +61,7 @@ class FileProcessor @Inject constructor(
// Redact EXIF data as indicated in preferences. // Redact EXIF data as indicated in preferences.
redactExifTags(exifInterface, getExifTagsToRedact()) redactExifTags(exifInterface, getExifTagsToRedact())
Timber.d("Calling GPSExtractor") Timber.d("Calling GPSExtractor")
val originalImageCoordinates = ImageCoordinates(exifInterface) val originalImageCoordinates = ImageCoordinates(exifInterface, inAppPictureLocation)
if (originalImageCoordinates.decimalCoords == null) { if (originalImageCoordinates.decimalCoords == null) {
//Find other photos taken around the same time which has gps coordinates //Find other photos taken around the same time which has gps coordinates
findOtherImages( findOtherImages(
@ -156,11 +158,13 @@ class FileProcessor @Inject constructor(
private fun readImageCoordinates(file: File) = private fun readImageCoordinates(file: File) =
try { try {
ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!) /* Used null location as location for similar images captured before is not available
in case it is not present in the EXIF. */
ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!, null)
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
try { try {
ImageCoordinates(file.absolutePath) ImageCoordinates(file.absolutePath, null)
} catch (ex: IOException) { } catch (ex: IOException) {
Timber.e(ex) Timber.e(ex)
null null

View file

@ -7,6 +7,7 @@ import android.webkit.MimeTypeMap;
import androidx.exifinterface.media.ExifInterface; import androidx.exifinterface.media.ExifInterface;
import fr.free.nrw.commons.location.LatLng;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -64,11 +65,11 @@ public class FileUtils {
/** /**
* Get Geolocation of filePath from input filePath path * Get Geolocation of filePath from input filePath path
*/ */
static String getGeolocationOfFile(String filePath) { static String getGeolocationOfFile(String filePath, LatLng inAppPictureLocation) {
try { try {
ExifInterface exifInterface = new ExifInterface(filePath); ExifInterface exifInterface = new ExifInterface(filePath);
ImageCoordinates imageObj = new ImageCoordinates(exifInterface); ImageCoordinates imageObj = new ImageCoordinates(exifInterface, inAppPictureLocation);
if (imageObj.getDecimalCoords() != null) { // If image has geolocation information in its EXIF if (imageObj.getDecimalCoords() != null) { // If image has geolocation information in its EXIF
return imageObj.getDecimalCoords(); return imageObj.getDecimalCoords();
} else { } else {

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.content.Context; import android.content.Context;
import fr.free.nrw.commons.location.LatLng;
import io.reactivex.Observable; import io.reactivex.Observable;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
@ -37,8 +38,8 @@ public class FileUtilsWrapper {
return FileUtils.getFileInputStream(filePath); return FileUtils.getFileInputStream(filePath);
} }
public String getGeolocationOfFile(String filePath) { public String getGeolocationOfFile(String filePath, LatLng inAppPictureLocation) {
return FileUtils.getGeolocationOfFile(filePath); return FileUtils.getGeolocationOfFile(filePath, inAppPictureLocation);
} }

View file

@ -9,8 +9,10 @@ import java.io.InputStream
/** /**
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation * Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* is uploaded, extract latitude and longitude from EXIF data of image. * is uploaded, extract latitude and longitude from EXIF data of image.
* Otherwise, if current user location is available while using the in-app camera,
* use it to set image coordinates
*/ */
class ImageCoordinates internal constructor(exif: ExifInterface?) { class ImageCoordinates internal constructor(exif: ExifInterface?, inAppPictureLocation: LatLng?) {
var decLatitude = 0.0 var decLatitude = 0.0
var decLongitude = 0.0 var decLongitude = 0.0
var imageCoordsExists = false var imageCoordsExists = false
@ -26,13 +28,13 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) {
/** /**
* Construct from a stream. * Construct from a stream.
*/ */
internal constructor(stream: InputStream) : this(ExifInterface(stream)) internal constructor(stream: InputStream, inAppPictureLocation: LatLng?) : this(ExifInterface(stream), inAppPictureLocation)
/** /**
* Construct from the file path of the image. * Construct from the file path of the image.
* @param path file path of the image * @param path file path of the image
*/ */
@Throws(IOException::class) @Throws(IOException::class)
internal constructor(path: String) : this(ExifInterface(path)) internal constructor(path: String, inAppPictureLocation: LatLng?) : this(ExifInterface(path), inAppPictureLocation)
init { init {
//If image has no EXIF data and user has enabled GPS setting, get user's location //If image has no EXIF data and user has enabled GPS setting, get user's location
@ -61,6 +63,14 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) {
//If image has EXIF data, extract image coords //If image has EXIF data, extract image coords
imageCoordsExists = true imageCoordsExists = true
Timber.d("EXIF data has location info") Timber.d("EXIF data has location info")
} else if (inAppPictureLocation != null) {
decLatitude = inAppPictureLocation.latitude
decLongitude = inAppPictureLocation.longitude
if (!(decLatitude == 0.0 && decLongitude == 0.0)) {
decimalCoords = "$decLatitude|$decLongitude"
imageCoordsExists = true
Timber.d("Image coordinates recorded while using in-app camera")
}
} }
} }
} }

View file

@ -5,6 +5,7 @@ import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import android.content.Context; import android.content.Context;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
@ -46,7 +47,7 @@ public class ImageProcessingService {
* Check image quality before upload - checks duplicate image - checks dark image - checks * Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title * geolocation for image - check for valid title
*/ */
Single<Integer> validateImage(UploadItem uploadItem) { Single<Integer> validateImage(UploadItem uploadItem, LatLng inAppPictureLocation) {
int currentImageQuality = uploadItem.getImageQuality(); int currentImageQuality = uploadItem.getImageQuality();
Timber.d("Current image quality is %d", currentImageQuality); Timber.d("Current image quality is %d", currentImageQuality);
if (currentImageQuality == ImageUtils.IMAGE_KEEP) { if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
@ -57,7 +58,7 @@ public class ImageProcessingService {
return Single.zip( return Single.zip(
checkDuplicateImage(filePath), checkDuplicateImage(filePath),
checkImageGeoLocation(uploadItem.getPlace(), filePath), checkImageGeoLocation(uploadItem.getPlace(), filePath, inAppPictureLocation),
checkDarkImage(filePath), checkDarkImage(filePath),
validateItemTitle(uploadItem), validateItemTitle(uploadItem),
checkFBMD(filePath), checkFBMD(filePath),
@ -148,13 +149,13 @@ public class ImageProcessingService {
* @param filePath file to be checked * @param filePath file to be checked
* @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK
*/ */
private Single<Integer> checkImageGeoLocation(Place place, String filePath) { private Single<Integer> checkImageGeoLocation(Place place, String filePath, LatLng inAppPictureLocation) {
Timber.d("Checking for image geolocation %s", filePath); Timber.d("Checking for image geolocation %s", filePath);
if (place == null || StringUtils.isBlank(place.getWikiDataEntityId())) { if (place == null || StringUtils.isBlank(place.getWikiDataEntityId())) {
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);
} }
return Single.fromCallable(() -> filePath) return Single.fromCallable(() -> filePath)
.map(fileUtilsWrapper::getGeolocationOfFile) .flatMap(path -> Single.just(fileUtilsWrapper.getGeolocationOfFile(path, inAppPictureLocation)))
.flatMap(geoLocation -> { .flatMap(geoLocation -> {
if (StringUtils.isBlank(geoLocation)) { if (StringUtils.isBlank(geoLocation)) {
return Single.just(ImageUtils.IMAGE_OK); return Single.just(ImageUtils.IMAGE_OK);

View file

@ -8,6 +8,8 @@ import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Intent; import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.View; import android.view.View;
@ -38,8 +40,12 @@ import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationPermissionsHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.UserClient; import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.theme.BaseActivity; import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment; import fr.free.nrw.commons.upload.depicts.DepictsFragment;
@ -57,6 +63,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
@ -74,6 +81,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
SessionManager sessionManager; SessionManager sessionManager;
@Inject @Inject
UserClient userClient; UserClient userClient;
@Inject
LocationServiceManager locationManager;
@BindView(R.id.cv_container_top_card) @BindView(R.id.cv_container_top_card)
@ -109,6 +118,9 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private ThumbnailsAdapter thumbnailsAdapter; private ThumbnailsAdapter thumbnailsAdapter;
private Place place; private Place place;
private LatLng prevLocation;
private LatLng currLocation;
private boolean isInAppCameraUpload;
private List<UploadableFile> uploadableFiles = Collections.emptyList(); private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0; private int currentSelectedPosition = 0;
/* /*
@ -117,6 +129,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private boolean isMultipleFilesSelected = false; private boolean isMultipleFilesSelected = false;
public static final String EXTRA_FILES = "commons_image_exta"; public static final String EXTRA_FILES = "commons_image_exta";
public static final String LOCATION_BEFORE_IMAGE_CAPTURE = "user_location_before_image_capture";
public static final String IN_APP_CAMERA_UPLOAD = "in_app_camera_upload";
/** /**
* Stores all nearby places found and related users response for * Stores all nearby places found and related users response for
@ -148,6 +162,11 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
if (dpi<=321) { if (dpi<=321) {
onRlContainerTitleClicked(); onRlContainerTitleClicked();
} }
if (PermissionUtils.hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
locationManager.registerLocationManager();
}
locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER);
} }
private void init() { private void init() {
@ -366,7 +385,29 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
fragments = new ArrayList<>(); fragments = new ArrayList<>();
for (UploadableFile uploadableFile : uploadableFiles) { for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place);
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;
}
}
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place, currLocation);
locationManager.unregisterLocationManager();
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() { uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() {
@Override @Override
public void deletePictureAtIndex(int index) { public void deletePictureAtIndex(int index) {
@ -424,6 +465,39 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
} }
} }
/**
* Users may uncheck Location tag from the Manage EXIF tags setting any time.
* So, their location must not be shared in this case.
*
* @return
*/
private boolean isLocationTagUncheckedInTheSettings() {
Set<String> prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS);
if (prefExifTags.contains(getString(R.string.exif_tag_location))) {
return false;
}
return true;
}
/**
* Calculate the difference between current location and
* location recorded before capturing the image
*
* @param currLocation
* @param prevLocation
* @return
*/
private float getLocationDifference(LatLng currLocation, LatLng prevLocation) {
if (prevLocation == null) {
return 0.0f;
}
float[] distance = new float[2];
Location.distanceBetween(
currLocation.getLatitude(), currLocation.getLongitude(),
prevLocation.getLatitude(), prevLocation.getLongitude(), distance);
return distance[0];
}
private void receiveExternalSharedItems() { private void receiveExternalSharedItems() {
uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent());
} }
@ -438,6 +512,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
Timber.i("Received multiple upload %s", uploadableFiles.size()); Timber.i("Received multiple upload %s", uploadableFiles.size());
place = intent.getParcelableExtra(PLACE_OBJECT); place = intent.getParcelableExtra(PLACE_OBJECT);
prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE);
isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false);
resetDirectPrefs(); resetDirectPrefs();
} }

View file

@ -7,6 +7,7 @@ import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.depicts.DepictsFragment; import fr.free.nrw.commons.upload.depicts.DepictsFragment;
@ -86,18 +87,20 @@ public class UploadModel {
*/ */
public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile, public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile,
final Place place, final Place place,
final SimilarImageInterface similarImageInterface) { final SimilarImageInterface similarImageInterface,
LatLng inAppPictureLocation) {
return Observable.just( return Observable.just(
createAndAddUploadItem(uploadableFile, place, similarImageInterface)); createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation));
} }
public Single<Integer> getImageQuality(final UploadItem uploadItem) { public Single<Integer> getImageQuality(final UploadItem uploadItem, LatLng inAppPictureLocation) {
return imageProcessingService.validateImage(uploadItem); return imageProcessingService.validateImage(uploadItem, inAppPictureLocation);
} }
private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile,
final Place place, final Place place,
final SimilarImageInterface similarImageInterface) { final SimilarImageInterface similarImageInterface,
LatLng inAppPictureLocation) {
final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
.getFileCreatedDate(context); .getFileCreatedDate(context);
long fileCreatedDate = -1; long fileCreatedDate = -1;
@ -110,7 +113,8 @@ public class UploadModel {
} }
Timber.d("File created date is %d", fileCreatedDate); Timber.d("File created date is %d", fileCreatedDate);
final ImageCoordinates imageCoordinates = fileProcessor final ImageCoordinates imageCoordinates = fileProcessor
.processFileCoordinates(similarImageInterface, uploadableFile.getFilePath()); .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(),
inAppPictureLocation);
final UploadItem uploadItem = new UploadItem( final UploadItem uploadItem = new UploadItem(
Uri.parse(uploadableFile.getFilePath()), Uri.parse(uploadableFile.getFilePath()),
uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,

View file

@ -31,6 +31,8 @@ import fr.free.nrw.commons.LocationPicker.LocationPicker;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
@ -117,6 +119,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
*/ */
private Place nearbyPlace; private Place nearbyPlace;
private UploadItem uploadItem; private UploadItem uploadItem;
/**
* inAppPictureLocation: use location recorded while using the in-app camera if
* device camera does not record it in the EXIF
*/
private LatLng inAppPictureLocation;
/** /**
* editableUploadItem : Storing the upload item before going to update the coordinates * editableUploadItem : Storing the upload item before going to update the coordinates
*/ */
@ -133,9 +140,10 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
public void setImageTobeUploaded(UploadableFile uploadableFile, Place place) { public void setImageTobeUploaded(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation) {
this.uploadableFile = uploadableFile; this.uploadableFile = uploadableFile;
this.place = place; this.place = place;
this.inAppPictureLocation = inAppPictureLocation;
} }
@Nullable @Nullable
@ -160,7 +168,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
tooltip.setOnClickListener( tooltip.setOnClickListener(
v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip)); v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip));
initPresenter(); initPresenter();
presenter.receiveImage(uploadableFile, place); presenter.receiveImage(uploadableFile, place, inAppPictureLocation);
initRecyclerView(); initRecyclerView();
if (callback.getIndexInViewFlipper(this) == 0) { if (callback.getIndexInViewFlipper(this) == 0) {
@ -222,7 +230,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
@OnClick(R.id.btn_next) @OnClick(R.id.btn_next)
public void onNextButtonClicked() { public void onNextButtonClicked() {
presenter.verifyImageQuality(callback.getIndexInViewFlipper(this)); presenter.verifyImageQuality(callback.getIndexInViewFlipper(this), inAppPictureLocation);
} }
@OnClick(R.id.btn_previous) @OnClick(R.id.btn_previous)
@ -448,6 +456,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
double defaultLongitude = -122.431297; double defaultLongitude = -122.431297;
double defaultZoom = 16.0; double defaultZoom = 16.0;
/* Retrieve image location from EXIF if present or
check if user has provided location while using the in-app camera.
Use location of last UploadItem if none of them is available */
if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords() if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords()
.getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) { .getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) {
defaultLatitude = uploadItem.getGpsCoords() defaultLatitude = uploadItem.getGpsCoords()

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload.mediaDetails;
import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.ImageCoordinates; import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageInterface; import fr.free.nrw.commons.upload.SimilarImageInterface;
@ -43,9 +44,9 @@ public interface UploadMediaDetailsContract {
interface UserActionListener extends BasePresenter<View> { interface UserActionListener extends BasePresenter<View> {
void receiveImage(UploadableFile uploadableFile, Place place); void receiveImage(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation);
void verifyImageQuality(int uploadItemIndex); void verifyImageQuality(int uploadItemIndex, LatLng inAppPictureLocation);
void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper); void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper);

View file

@ -87,11 +87,12 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
* @param place * @param place
*/ */
@Override @Override
public void receiveImage(final UploadableFile uploadableFile, final Place place) { public void receiveImage(final UploadableFile uploadableFile, final Place place,
LatLng inAppPictureLocation) {
view.showProgress(true); view.showProgress(true);
compositeDisposable.add( compositeDisposable.add(
repository repository
.preProcessImage(uploadableFile, place, this) .preProcessImage(uploadableFile, place, this, inAppPictureLocation)
.map(uploadItem -> { .map(uploadItem -> {
if(place!=null && place.isMonument()){ if(place!=null && place.isMonument()){
if (place.location != null) { if (place.location != null) {
@ -177,15 +178,15 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
* @param uploadItemIndex * @param uploadItemIndex
*/ */
@Override @Override
public void verifyImageQuality(int uploadItemIndex) { public void verifyImageQuality(int uploadItemIndex, LatLng inAppPictureLocation) {
final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex); final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex);
if (uploadItem.getGpsCoords().getDecimalCoords() == null) { if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null) {
final Runnable onSkipClicked = () -> { final Runnable onSkipClicked = () -> {
view.showProgress(true); view.showProgress(true);
compositeDisposable.add( compositeDisposable.add(
repository repository
.getImageQuality(uploadItem) .getImageQuality(uploadItem, inAppPictureLocation)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(imageResult -> { .subscribe(imageResult -> {
view.showProgress(false); view.showProgress(false);
@ -208,7 +209,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
view.showProgress(true); view.showProgress(true);
compositeDisposable.add( compositeDisposable.add(
repository repository
.getImageQuality(uploadItem) .getImageQuality(uploadItem, inAppPictureLocation)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(imageResult -> { .subscribe(imageResult -> {
view.showProgress(false); view.showProgress(false);

View file

@ -190,6 +190,8 @@
<string name="read_storage_permission_rationale">Required permission: Read external storage. App cannot access your gallery without this.</string> <string name="read_storage_permission_rationale">Required permission: Read external storage. App cannot access your gallery without this.</string>
<string name="write_storage_permission_rationale">Required permission: Write external storage. App cannot access your camera/gallery without this.</string> <string name="write_storage_permission_rationale">Required permission: Write external storage. App cannot access your camera/gallery without this.</string>
<string name="location_permission_title">Requesting Location Permission</string> <string name="location_permission_title">Requesting Location Permission</string>
<string name="in_app_camera_location_permission_title">Record location for in-app shots</string>
<string name="in_app_camera_location_switch_pref_summary">Enable this to record location with in-app shots in case the device camera does not record it</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="duplicate_file_name">Duplicate File Name found</string> <string name="duplicate_file_name">Duplicate File Name found</string>
@ -440,6 +442,13 @@ Upload your first media by tapping on the add button.</string>
<string name="ends_on">Ends on:</string> <string name="ends_on">Ends on:</string>
<string name="display_campaigns">Display campaigns</string> <string name="display_campaigns">Display campaigns</string>
<string name="display_campaigns_explanation">See the ongoing campaigns</string> <string name="display_campaigns_explanation">See the ongoing campaigns</string>
<string name="in_app_camera_location_access_explanation">Allow the app to fetch location in case the camera does not record it. Some device cameras do not record location. In such cases, letting the app fetch and attach location to it makes your contribution more useful. You may change this any time from the Settings</string>
<string name="option_allow">Allow</string>
<string name="option_dismiss">Dismiss</string>
<string name="in_app_camera_needs_location">Please turn on location access from the Settings and try again. \n\nNote: The upload may not have location if app is unable to retrieve location from device within a short interval.</string>
<string name="in_app_camera_location_permission_rationale">In-app camera needs location permission to attach it to your images in case location is not available in EXIF. Please allow the app to access your location and try again.\n\nNote: The upload may not have location if app is unable to retrieve location from device within a short interval.</string>
<string name="in_app_camera_location_permission_denied">The app would not record location along with in-shots due to lack of location permission</string>
<string name="in_app_camera_location_unavailable">The app would not record location along with in-shots as the GPS is turned off</string>
<string name="open_document_photo_picker_title">Use document based photo picker</string> <string name="open_document_photo_picker_title">Use document based photo picker</string>
<string name="open_document_photo_picker_explanation">The new Android photo picker risks losing location information. Enable if you seem to be using it.</string> <string name="open_document_photo_picker_explanation">The new Android photo picker risks losing location information. Enable if you seem to be using it.</string>
<string name="location_loss_warning">Turning this off could trigger the new Android photo picker. It risks losing location information.\n\nTap on \'Read more\' for more information.</string> <string name="location_loss_warning">Turning this off could trigger the new Android photo picker. It risks losing location information.\n\nTap on \'Read more\' for more information.</string>

View file

@ -19,13 +19,6 @@
<PreferenceCategory <PreferenceCategory
android:title="@string/preference_category_general"> android:title="@string/preference_category_general">
<SwitchPreference
android:defaultValue="true"
android:key="useExternalStorage"
app:singleLineTitle="false"
android:summary="@string/use_external_storage_summary"
android:title="@string/use_external_storage" />
<Preference <Preference
android:key="appUiDefaultLanguagePref" android:key="appUiDefaultLanguagePref"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
@ -38,19 +31,6 @@
app:singleLineTitle="false" app:singleLineTitle="false"
android:title="@string/default_description_language" /> android:title="@string/default_description_language" />
<SwitchPreference
android:key="useAuthorName"
app:singleLineTitle="false"
android:summary="@string/preference_author_name_toggle_summary"
android:title="@string/preference_author_name_toggle" />
<EditTextPreference
android:key="authorName"
app:singleLineTitle="false"
app:dependency="useAuthorName"
app:useSimpleSummaryProvider="true"
android:title="@string/preference_author_name" />
<SwitchPreference <SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="displayNearbyCardView" android:key="displayNearbyCardView"
@ -71,11 +51,43 @@
android:summary="@string/display_campaigns_explanation" android:summary="@string/display_campaigns_explanation"
android:title="@string/display_campaigns" /> android:title="@string/display_campaigns" />
</PreferenceCategory>
<PreferenceCategory
android:title="Uploads">
<SwitchPreference
android:defaultValue="true"
android:key="useExternalStorage"
app:singleLineTitle="false"
android:summary="@string/use_external_storage_summary"
android:title="@string/use_external_storage" />
<SwitchPreference
android:key="inAppCameraLocationPref"
android:title="@string/in_app_camera_location_permission_title"
android:summary="@string/in_app_camera_location_switch_pref_summary"/>
<SwitchPreference <SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="openDocumentPhotoPickerPref" android:key="openDocumentPhotoPickerPref"
android:summary="@string/open_document_photo_picker_explanation" android:summary="@string/open_document_photo_picker_explanation"
android:title="@string/open_document_photo_picker_title"/> android:title="@string/open_document_photo_picker_title"/>
<SwitchPreference
android:key="useAuthorName"
app:singleLineTitle="false"
android:summary="@string/preference_author_name_toggle_summary"
android:title="@string/preference_author_name_toggle" />
<EditTextPreference
android:key="authorName"
app:singleLineTitle="false"
app:dependency="useAuthorName"
app:useSimpleSummaryProvider="true"
android:title="@string/preference_author_name" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
@ -88,6 +100,7 @@
app:singleLineTitle="false" app:singleLineTitle="false"
android:summary="@string/manage_exif_tags_summary" android:summary="@string/manage_exif_tags_summary"
android:title="@string/manage_exif_tags" /> android:title="@string/manage_exif_tags" />
</PreferenceCategory> </PreferenceCategory>
<!-- The key 'allowGps' was used before and has since been removed based on the discussion at #1599. <!-- The key 'allowGps' was used before and has since been removed based on the discussion at #1599.

View file

@ -28,6 +28,8 @@ class u {
internal var readEXIF: EXIFReader?=null internal var readEXIF: EXIFReader?=null
@Mock @Mock
internal var mediaClient: MediaClient? = null internal var mediaClient: MediaClient? = null
@Mock
internal var location: LatLng? = null
@InjectMocks @InjectMocks
var imageProcessingService: ImageProcessingService? = null var imageProcessingService: ImageProcessingService? = null
@ -62,7 +64,7 @@ class u {
`when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java))) `when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java)))
.thenReturn("fileSha") .thenReturn("fileSha")
`when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString())) `when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString(), any(LatLng::class.java)))
.thenReturn("latLng") .thenReturn("latLng")
`when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString())) `when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString()))
@ -88,7 +90,7 @@ class u {
@Test @Test
fun validateImageForKeepImage() { fun validateImageForKeepImage() {
`when`(uploadItem.imageQuality).thenReturn(ImageUtils.IMAGE_KEEP) `when`(uploadItem.imageQuality).thenReturn(ImageUtils.IMAGE_KEEP)
val validateImage = imageProcessingService!!.validateImage(uploadItem) val validateImage = imageProcessingService!!.validateImage(uploadItem, location)
assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet())
} }
@ -96,13 +98,13 @@ class u {
fun validateImageForDuplicateImage() { fun validateImageForDuplicateImage() {
`when`(mediaClient!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) `when`(mediaClient!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Single.just(true)) .thenReturn(Single.just(true))
val validateImage = imageProcessingService!!.validateImage(uploadItem) val validateImage = imageProcessingService!!.validateImage(uploadItem, location)
assertEquals(ImageUtils.IMAGE_DUPLICATE, validateImage.blockingGet()) assertEquals(ImageUtils.IMAGE_DUPLICATE, validateImage.blockingGet())
} }
@Test @Test
fun validateImageForOkImage() { fun validateImageForOkImage() {
val validateImage = imageProcessingService!!.validateImage(uploadItem) val validateImage = imageProcessingService!!.validateImage(uploadItem, location)
assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet())
} }
@ -110,7 +112,7 @@ class u {
fun validateImageForDarkImage() { fun validateImageForDarkImage() {
`when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString())) `when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString()))
.thenReturn(Single.just(ImageUtils.IMAGE_DARK)) .thenReturn(Single.just(ImageUtils.IMAGE_DARK))
val validateImage = imageProcessingService!!.validateImage(uploadItem) val validateImage = imageProcessingService!!.validateImage(uploadItem, location)
assertEquals(ImageUtils.IMAGE_DARK, validateImage.blockingGet()) assertEquals(ImageUtils.IMAGE_DARK, validateImage.blockingGet())
} }
@ -118,7 +120,7 @@ class u {
fun validateImageForWrongGeoLocation() { fun validateImageForWrongGeoLocation() {
`when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(ArgumentMatchers.anyString(), any(LatLng::class.java))) `when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(ArgumentMatchers.anyString(), any(LatLng::class.java)))
.thenReturn(Single.just(ImageUtils.IMAGE_GEOLOCATION_DIFFERENT)) .thenReturn(Single.just(ImageUtils.IMAGE_GEOLOCATION_DIFFERENT))
val validateImage = imageProcessingService!!.validateImage(uploadItem) val validateImage = imageProcessingService!!.validateImage(uploadItem, location)
assertEquals(ImageUtils.IMAGE_GEOLOCATION_DIFFERENT, validateImage.blockingGet()) assertEquals(ImageUtils.IMAGE_GEOLOCATION_DIFFERENT, validateImage.blockingGet())
} }
@ -126,7 +128,7 @@ class u {
fun validateImageForFileNameExistsWithCheckTitleOn() { fun validateImageForFileNameExistsWithCheckTitleOn() {
`when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) `when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Single.just(true)) .thenReturn(Single.just(true))
val validateImage = imageProcessingService!!.validateImage(uploadItem) val validateImage = imageProcessingService!!.validateImage(uploadItem, location)
assertEquals(ImageUtils.FILE_NAME_EXISTS, validateImage.blockingGet()) assertEquals(ImageUtils.FILE_NAME_EXISTS, validateImage.blockingGet())
} }
} }

View file

@ -47,6 +47,9 @@ class UploadMediaPresenterTest {
@Mock @Mock
private lateinit var place: Place private lateinit var place: Place
@Mock
private var location: LatLng? = null
@Mock @Mock
private lateinit var uploadItem: UploadItem private lateinit var uploadItem: UploadItem
@ -88,10 +91,11 @@ class UploadMediaPresenterTest {
repository.preProcessImage( repository.preProcessImage(
ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(UploadableFile::class.java),
ArgumentMatchers.any(Place::class.java), ArgumentMatchers.any(Place::class.java),
ArgumentMatchers.any(UploadMediaPresenter::class.java) ArgumentMatchers.any(UploadMediaPresenter::class.java),
ArgumentMatchers.any(LatLng::class.java)
) )
).thenReturn(testObservableUploadItem) ).thenReturn(testObservableUploadItem)
uploadMediaPresenter.receiveImage(uploadableFile, place) uploadMediaPresenter.receiveImage(uploadableFile, place, location)
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
verify(view).onImageProcessed( verify(view).onImageProcessed(
@ -107,14 +111,14 @@ class UploadMediaPresenterTest {
@Test @Test
fun verifyImageQualityTest() { fun verifyImageQualityTest() {
whenever(repository.uploads).thenReturn(listOf(uploadItem)) whenever(repository.uploads).thenReturn(listOf(uploadItem))
whenever(repository.getImageQuality(uploadItem)) whenever(repository.getImageQuality(uploadItem, location))
.thenReturn(testSingleImageResult) .thenReturn(testSingleImageResult)
whenever(uploadItem.imageQuality).thenReturn(0) whenever(uploadItem.imageQuality).thenReturn(0)
whenever(uploadItem.gpsCoords) whenever(uploadItem.gpsCoords)
.thenReturn(imageCoordinates) .thenReturn(imageCoordinates)
whenever(uploadItem.gpsCoords.decimalCoords) whenever(uploadItem.gpsCoords.decimalCoords)
.thenReturn("imageCoordinates") .thenReturn("imageCoordinates")
uploadMediaPresenter.verifyImageQuality(0) uploadMediaPresenter.verifyImageQuality(0, location)
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
verify(view).showProgress(false) verify(view).showProgress(false)
@ -126,14 +130,14 @@ class UploadMediaPresenterTest {
@Test @Test
fun `verify ImageQuality Test while coordinates equals to null`() { fun `verify ImageQuality Test while coordinates equals to null`() {
whenever(repository.uploads).thenReturn(listOf(uploadItem)) whenever(repository.uploads).thenReturn(listOf(uploadItem))
whenever(repository.getImageQuality(uploadItem)) whenever(repository.getImageQuality(uploadItem, location))
.thenReturn(testSingleImageResult) .thenReturn(testSingleImageResult)
whenever(uploadItem.imageQuality).thenReturn(0) whenever(uploadItem.imageQuality).thenReturn(0)
whenever(uploadItem.gpsCoords) whenever(uploadItem.gpsCoords)
.thenReturn(imageCoordinates) .thenReturn(imageCoordinates)
whenever(uploadItem.gpsCoords.decimalCoords) whenever(uploadItem.gpsCoords.decimalCoords)
.thenReturn(null) .thenReturn(null)
uploadMediaPresenter.verifyImageQuality(0) uploadMediaPresenter.verifyImageQuality(0, location)
testScheduler.triggerActions() testScheduler.triggerActions()
} }
@ -167,7 +171,7 @@ class UploadMediaPresenterTest {
val uploadMediaDetailList: ArrayList<UploadMediaDetail> = ArrayList() val uploadMediaDetailList: ArrayList<UploadMediaDetail> = ArrayList()
uploadMediaDetailList.add(uploadMediaDetail) uploadMediaDetailList.add(uploadMediaDetail)
uploadItem.setMediaDetails(uploadMediaDetailList) uploadItem.setMediaDetails(uploadMediaDetailList)
Mockito.`when`(repository.getImageQuality(uploadItem)).then { Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
verify(view).showProgress(true) verify(view).showProgress(true)
@ -183,7 +187,7 @@ class UploadMediaPresenterTest {
uploadMediaDetail.captionText = "added caption" uploadMediaDetail.captionText = "added caption"
uploadMediaDetail.languageCode = "eo" uploadMediaDetail.languageCode = "eo"
uploadItem.setMediaDetails(Collections.singletonList(uploadMediaDetail)) uploadItem.setMediaDetails(Collections.singletonList(uploadMediaDetail))
Mockito.`when`(repository.getImageQuality(uploadItem)).then { Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
verify(view).showProgress(true) verify(view).showProgress(true)
@ -264,11 +268,12 @@ class UploadMediaPresenterTest {
repository.preProcessImage( repository.preProcessImage(
ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(UploadableFile::class.java),
ArgumentMatchers.any(Place::class.java), ArgumentMatchers.any(Place::class.java),
ArgumentMatchers.any(UploadMediaPresenter::class.java) ArgumentMatchers.any(UploadMediaPresenter::class.java),
ArgumentMatchers.any(LatLng::class.java)
) )
).thenReturn(item) ).thenReturn(item)
uploadMediaPresenter.receiveImage(uploadableFile, germanyAsPlace) uploadMediaPresenter.receiveImage(uploadableFile, germanyAsPlace, location)
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()

View file

@ -62,6 +62,9 @@ class UploadRepositoryUnitTest {
@Mock @Mock
private lateinit var place: Place private lateinit var place: Place
@Mock
private var location: LatLng? = null
@Mock @Mock
private lateinit var similarImageInterface: SimilarImageInterface private lateinit var similarImageInterface: SimilarImageInterface
@ -175,16 +178,16 @@ class UploadRepositoryUnitTest {
@Test @Test
fun testPreProcessImage() { fun testPreProcessImage() {
assertEquals( assertEquals(
repository.preProcessImage(uploadableFile, place, similarImageInterface), repository.preProcessImage(uploadableFile, place, similarImageInterface, location),
uploadModel.preProcessImage(uploadableFile, place, similarImageInterface) uploadModel.preProcessImage(uploadableFile, place, similarImageInterface, location)
) )
} }
@Test @Test
fun testGetImageQuality() { fun testGetImageQuality() {
assertEquals( assertEquals(
repository.getImageQuality(uploadItem), repository.getImageQuality(uploadItem, location),
uploadModel.getImageQuality(uploadItem) uploadModel.getImageQuality(uploadItem, location)
) )
} }

View file

@ -97,6 +97,9 @@ class UploadMediaDetailFragmentUnitTest {
@Mock @Mock
private lateinit var place: Place private lateinit var place: Place
@Mock
private var location: fr.free.nrw.commons.location.LatLng? = null
@Mock @Mock
private lateinit var defaultKvStore: JsonKvStore private lateinit var defaultKvStore: JsonKvStore
@ -172,7 +175,7 @@ class UploadMediaDetailFragmentUnitTest {
@Throws(Exception::class) @Throws(Exception::class)
fun testSetImageTobeUploaded() { fun testSetImageTobeUploaded() {
Shadows.shadowOf(Looper.getMainLooper()).idle() Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.setImageTobeUploaded(null, null) fragment.setImageTobeUploaded(null, null, location)
} }
@Test @Test
@ -374,7 +377,7 @@ class UploadMediaDetailFragmentUnitTest {
`when`(latLng.longitude).thenReturn(0.0) `when`(latLng.longitude).thenReturn(0.0)
`when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates)
fragment.onActivityResult(1211, Activity.RESULT_OK, intent) fragment.onActivityResult(1211, Activity.RESULT_OK, intent)
Mockito.verify(presenter, Mockito.times(0)).verifyImageQuality(0) Mockito.verify(presenter, Mockito.times(0)).verifyImageQuality(0, location)
} }
@Test @Test
@ -396,7 +399,7 @@ class UploadMediaDetailFragmentUnitTest {
`when`(latLng.longitude).thenReturn(0.0) `when`(latLng.longitude).thenReturn(0.0)
`when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates)
fragment.onActivityResult(1211, Activity.RESULT_OK, intent) fragment.onActivityResult(1211, Activity.RESULT_OK, intent)
Mockito.verify(presenter, Mockito.times(1)).verifyImageQuality(0) Mockito.verify(presenter, Mockito.times(1)).verifyImageQuality(0, null)
} }
@Test @Test