From 02acabebf4f9e0b22ec3725ac78adadd987b9ae5 Mon Sep 17 00:00:00 2001 From: Ritika Date: Sat, 24 Jun 2023 18:29:34 +0530 Subject: [PATCH 01/18] fix in-app camera location loss --- .../contributions/ContributionController.java | 121 +++++++++++++++++- .../commons/repository/UploadRepository.java | 4 +- .../free/nrw/commons/upload/FileProcessor.kt | 22 +++- .../nrw/commons/upload/UploadActivity.java | 50 +++++++- .../free/nrw/commons/upload/UploadModel.java | 11 +- .../UploadMediaDetailFragment.java | 29 ++++- .../UploadMediaDetailsContract.java | 5 +- .../mediaDetails/UploadMediaPresenter.java | 8 +- app/src/main/res/values/strings.xml | 6 + 9 files changed, 232 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 0a01ef70c3..1bffb820f2 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -3,9 +3,12 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import android.Manifest; +import android.Manifest.permission; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.provider.Settings; import androidx.annotation.NonNull; import fr.free.nrw.commons.R; import fr.free.nrw.commons.filepicker.DefaultCallback; @@ -13,8 +16,11 @@ import fr.free.nrw.commons.filepicker.FilePicker.ImageSource; import fr.free.nrw.commons.filepicker.UploadableFile; 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.upload.UploadActivity; +import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.ViewUtil; import java.util.ArrayList; @@ -28,7 +34,11 @@ public class ContributionController { public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; private final JsonKvStore defaultKvStore; + private LatLng locationBeforeImageCapture; + private Boolean isLocationInfoPreferred; + @Inject + LocationServiceManager locationManager; @Inject public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { this.defaultKvStore = defaultKvStore; @@ -46,11 +56,105 @@ public void initiateCameraPick(Activity activity) { PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, - () -> initiateCameraUpload(activity), + () -> askUserToAttachLocation(activity), R.string.storage_permission_title, R.string.write_storage_permission_rationale); } + /** + * 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 + * + * If the user selects "No", then the location tag in EXIF is also removed. + * + * @param activity + */ + private void askUserToAttachLocation(Activity activity) { + DialogUtil.showAlertDialog(activity, + activity.getString(R.string.attach_location_info_dialog_title), + activity.getString(R.string.attach_location_info_explanation), + activity.getString(R.string.option_yes_great), + activity.getString(R.string.option_do_not_attach_location), + ()-> { + isLocationInfoPreferred = true; + requestForLocationAccess(activity); + }, + () -> { + isLocationInfoPreferred = false; + initiateCameraUpload(activity); + }, + null, + true); + } + + /** + * Ask for location permission if the user agrees on attaching location with pictures + * and the app does not have the access to location + * + * @param activity + */ + + private void requestForLocationAccess(Activity activity) { + PermissionUtils.checkPermissionsAndPerformAction(activity, + permission.ACCESS_FINE_LOCATION, + () -> { + isLocationInfoPreferred = true; + onLocationPermissionGranted(activity); + }, + () -> isLocationInfoPreferred = false, + R.string.ask_to_turn_location_on, + R.string.in_app_camera_location_permission_rationale); + } + + /** + * Check if apps have access to location even after having individual access + * + * @param activity + */ + private void onLocationPermissionGranted(Activity activity) { + if (!(locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled())) { + showLocationOffDialog(activity); + } else { + initiateCameraUpload(activity); + } + } + + /** + * Ask user to grant location access to apps + * + * @param activity + */ + + private void showLocationOffDialog(Activity activity) { + DialogUtil + .showAlertDialog(activity, + activity.getString(R.string.location_permission_title), + activity.getString(R.string.in_app_camera_needs_location), + activity.getString(R.string.title_app_shortcut_setting), + () -> openLocationSettings(activity), + true); + } + + /** + * Open location source settings so that apps with location access can access it + * + * @param activity + */ + + private void openLocationSettings(Activity activity) { + final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + final PackageManager packageManager = activity.getPackageManager(); + + if (intent.resolveActivity(packageManager)!= null) { + activity.startActivity(intent); + } + } + /** * Initiate gallery picker */ @@ -99,6 +203,12 @@ private void setPickerConfiguration(Activity activity, */ private void initiateCameraUpload(Activity activity) { setPickerConfiguration(activity, false); + defaultKvStore.putBoolean("locationInfoPref", isLocationInfoPreferred); + if (isLocationInfoPreferred) { + locationBeforeImageCapture = locationManager.getLastLocation(); + } else { + locationBeforeImageCapture = null; + } FilePicker.openCameraForImage(activity, 0); } @@ -134,7 +244,8 @@ public List handleExternalImagesPicked(Activity 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, List imagesFiles) { @@ -148,6 +259,12 @@ private Intent handleImagesPicked(Context context, shareIntent.putExtra(PLACE_OBJECT, place); } + if (locationBeforeImageCapture != null) { + shareIntent.putExtra( + UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE, + locationBeforeImageCapture); + } + return shareIntent; } diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index 007e89cb20..5e5f604d05 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -188,9 +188,9 @@ public int getCount() { * @return */ public Observable preProcessImage(UploadableFile uploadableFile, Place place, - SimilarImageInterface similarImageInterface) { + SimilarImageInterface similarImageInterface, LatLng location) { return uploadModel.preProcessImage(uploadableFile, place, - similarImageInterface); + similarImageInterface, location); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index 3d0c62236c..5abcb1b107 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -6,6 +6,7 @@ import android.net.Uri import androidx.exifinterface.media.ExifInterface import fr.free.nrw.commons.R 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.OkHttpJsonApiClient 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 */ - fun processFileCoordinates(similarImageInterface: SimilarImageInterface, filePath: String?) + fun processFileCoordinates(similarImageInterface: SimilarImageInterface, + filePath: String?, location: LatLng?) : ImageCoordinates { val exifInterface: ExifInterface? = try { ExifInterface(filePath!!) @@ -61,11 +63,13 @@ class FileProcessor @Inject constructor( Timber.d("Calling GPSExtractor") val originalImageCoordinates = ImageCoordinates(exifInterface) if (originalImageCoordinates.decimalCoords == null) { - //Find other photos taken around the same time which has gps coordinates - findOtherImages( - File(filePath), - similarImageInterface - ) + if (location == null) { + //Find other photos taken around the same time which has gps coordinates + findOtherImages( + File(filePath), + similarImageInterface + ) + } } else { prePopulateCategoriesAndDepictionsBy(originalImageCoordinates) } @@ -82,6 +86,12 @@ class FileProcessor @Inject constructor( defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() val redactTags: Set = context.resources.getStringArray(R.array.pref_exifTag_values).toSet() + /* Remove EXIF location if user has chosen + "No, do not attach location" while using in-app camera */ + if (!defaultKvStore.getBoolean("locationInfoPref")) { + val locationTag: String = context.getString(R.string.exif_tag_location) + return redactTags - prefManageEXIFTags.minus(locationTag) + } return redactTags - prefManageEXIFTags } 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 7fd4ecce7f..b589cf90e2 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 @@ -8,6 +8,8 @@ import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Intent; +import android.location.Location; +import android.location.LocationManager; import android.os.Bundle; import android.util.DisplayMetrics; import android.view.View; @@ -38,6 +40,8 @@ import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.filepicker.UploadableFile; 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.mwapi.UserClient; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.theme.BaseActivity; @@ -74,6 +78,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, SessionManager sessionManager; @Inject UserClient userClient; + @Inject + LocationServiceManager locationManager; @BindView(R.id.cv_container_top_card) @@ -109,6 +115,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, private ThumbnailsAdapter thumbnailsAdapter; private Place place; + private LatLng prevLocation; + private LatLng currLocation; private List uploadableFiles = Collections.emptyList(); private int currentSelectedPosition = 0; /* @@ -117,6 +125,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, private boolean isMultipleFilesSelected = false; public static final String EXTRA_FILES = "commons_image_exta"; + public static final String LOCATION_BEFORE_IMAGE_CAPTURE = "user_location_before_image_capture"; /** * Stores all nearby places found and related users response for @@ -148,6 +157,11 @@ protected void onCreate(Bundle savedInstanceState) { if (dpi<=321) { 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() { @@ -366,7 +380,21 @@ private void receiveSharedItems() { fragments = new ArrayList<>(); for (UploadableFile uploadableFile : uploadableFiles) { UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); - uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place); + currLocation = locationManager.getLastLocation(); + if (currLocation != null) { + float locationDifference = getLocationDifference(currLocation, prevLocation); + /* Remove location if app has access to it and is recording it but user + has tapped on "No, do not attach location". + 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 (!defaultKvStore.getBoolean("locationInfoPref") + || locationDifference > 100) { + currLocation = null; + } + } + uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place, currLocation); + locationManager.unregisterLocationManager(); uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() { @Override public void deletePictureAtIndex(int index) { @@ -424,6 +452,25 @@ public boolean isWLMUpload() { } } + /** + * 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() { uploadableFiles = contributionController.handleExternalImagesPicked(this, getIntent()); } @@ -438,6 +485,7 @@ private void receiveInternalSharedItems() { Timber.i("Received multiple upload %s", uploadableFiles.size()); place = intent.getParcelableExtra(PLACE_OBJECT); + prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE); resetDirectPrefs(); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 369ce02449..4b5719cdca 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -7,6 +7,7 @@ import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.filepicker.UploadableFile; 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.settings.Prefs; import fr.free.nrw.commons.upload.depicts.DepictsFragment; @@ -86,9 +87,10 @@ public void setSelectedCategories(List selectedCategories) { */ public Observable preProcessImage(final UploadableFile uploadableFile, final Place place, - final SimilarImageInterface similarImageInterface) { + final SimilarImageInterface similarImageInterface, + LatLng location) { return Observable.just( - createAndAddUploadItem(uploadableFile, place, similarImageInterface)); + createAndAddUploadItem(uploadableFile, place, similarImageInterface, location)); } public Single getImageQuality(final UploadItem uploadItem) { @@ -97,7 +99,8 @@ public Single getImageQuality(final UploadItem uploadItem) { private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, final Place place, - final SimilarImageInterface similarImageInterface) { + final SimilarImageInterface similarImageInterface, + LatLng location) { final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile .getFileCreatedDate(context); long fileCreatedDate = -1; @@ -110,7 +113,7 @@ private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, } Timber.d("File created date is %d", fileCreatedDate); final ImageCoordinates imageCoordinates = fileProcessor - .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath()); + .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(), location); final UploadItem uploadItem = new UploadItem( Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, 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 09599667d6..7f404ef716 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 @@ -31,6 +31,8 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.filepicker.UploadableFile; 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.recentlanguages.RecentLanguagesDao; import fr.free.nrw.commons.settings.Prefs; @@ -117,6 +119,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements */ private Place nearbyPlace; private UploadItem uploadItem; + private LatLng location; /** * editableUploadItem : Storing the upload item before going to update the coordinates */ @@ -133,9 +136,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } - public void setImageTobeUploaded(UploadableFile uploadableFile, Place place) { + public void setImageTobeUploaded(UploadableFile uploadableFile, Place place, LatLng location) { this.uploadableFile = uploadableFile; this.place = place; + this.location = location; } @Nullable @@ -160,7 +164,7 @@ private void init() { tooltip.setOnClickListener( v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip)); initPresenter(); - presenter.receiveImage(uploadableFile, place); + presenter.receiveImage(uploadableFile, place, location); initRecyclerView(); if (callback.getIndexInViewFlipper(this) == 0) { @@ -222,7 +226,7 @@ private void showInfoAlert(int titleStringID, int messageStringId) { @OnClick(R.id.btn_next) public void onNextButtonClicked() { - presenter.verifyImageQuality(callback.getIndexInViewFlipper(this)); + presenter.verifyImageQuality(callback.getIndexInViewFlipper(this), location); } @OnClick(R.id.btn_previous) @@ -448,8 +452,12 @@ private void goToLocationPickerActivity(final UploadItem uploadItem) { double defaultLongitude = -122.431297; 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() .getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) { + Timber.d("Image coordinates taken from the EXIF metadata"); defaultLatitude = uploadItem.getGpsCoords() .getDecLatitude(); defaultLongitude = uploadItem.getGpsCoords().getDecLongitude(); @@ -461,6 +469,21 @@ private void goToLocationPickerActivity(final UploadItem uploadItem) { .zoom(defaultZoom).build()) .activityKey("UploadActivity") .build(getActivity()), REQUEST_CODE); + } else if (location != null && location.getLatitude() != 0.0 && location.getLongitude() != 0.0) { + Timber.d("Image coordinates recorded while using the in-app camera"); + defaultLatitude = location.getLatitude(); + defaultLongitude = location.getLongitude(); + + if(defaultKvStore.getString(LAST_ZOOM) != null){ + defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM)); + } + startActivityForResult(new LocationPicker.IntentBuilder() + .defaultLocation(new CameraPosition.Builder() + .target( + new com.mapbox.mapboxsdk.geometry.LatLng(defaultLatitude, defaultLongitude)) + .zoom(defaultZoom).build()) + .activityKey("UploadActivity") + .build(getActivity()), REQUEST_CODE); } else { if (defaultKvStore.getString(LAST_LOCATION) != null) { final String[] locationLatLng 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 e17a6f4daf..159430aa3d 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 @@ -2,6 +2,7 @@ import fr.free.nrw.commons.BasePresenter; 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.upload.ImageCoordinates; import fr.free.nrw.commons.upload.SimilarImageInterface; @@ -43,9 +44,9 @@ interface View extends SimilarImageInterface { interface UserActionListener extends BasePresenter { - void receiveImage(UploadableFile uploadableFile, Place place); + void receiveImage(UploadableFile uploadableFile, Place place, LatLng location); - void verifyImageQuality(int uploadItemIndex); + void verifyImageQuality(int uploadItemIndex, LatLng location); void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper); 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 d8e2538cf4..5ce9c5ce63 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 @@ -87,11 +87,11 @@ public void onDetachView() { * @param place */ @Override - public void receiveImage(final UploadableFile uploadableFile, final Place place) { + public void receiveImage(final UploadableFile uploadableFile, final Place place, LatLng imageLocation) { view.showProgress(true); compositeDisposable.add( repository - .preProcessImage(uploadableFile, place, this) + .preProcessImage(uploadableFile, place, this, imageLocation) .map(uploadItem -> { if(place!=null && place.isMonument()){ if (place.location != null) { @@ -177,10 +177,10 @@ private void checkNearbyPlaces(final UploadItem uploadItem) { * @param uploadItemIndex */ @Override - public void verifyImageQuality(int uploadItemIndex) { + public void verifyImageQuality(int uploadItemIndex, LatLng location) { final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex); - if (uploadItem.getGpsCoords().getDecimalCoords() == null) { + if (uploadItem.getGpsCoords().getDecimalCoords() == null && location == null) { final Runnable onSkipClicked = () -> { view.showProgress(true); compositeDisposable.add( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b285b273e1..9098332977 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -443,6 +443,12 @@ Upload your first media by tapping on the add button. Use GET_CONTENT photo picker Disable if your pictures get uploaded without location Please make sure that this new Android picker does not strip location from your pictures. + Attach location with the image + Location data helps Wiki editors find your picture, making it much more useful. We suggest you let the app attach location information to the pictures taken via this button. Thank you for your contributions! + Yes, great! + No, do not attach location + Please turn on location access from the Settings and try again. + In-app camera needs location permission to attach it to your images in case location is not available in EXIF You won\'t see the campaigns anymore. However, you can re-enable this notification in Settings if you wish. This function requires network connection, please check your connection settings. From d6ac060bbb6e3eff4de7e2e269209fb375179f07 Mon Sep 17 00:00:00 2001 From: Ritika Date: Sat, 24 Jun 2023 19:28:52 +0530 Subject: [PATCH 02/18] fix failing unit tests --- .../commons/upload/UploadMediaPresenterTest.kt | 17 +++++++++++------ .../commons/upload/UploadRepositoryUnitTest.kt | 7 +++++-- .../UploadMediaDetailFragmentUnitTest.kt | 11 ++++++++--- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt index 4aac403eb6..bd5540b29f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt @@ -47,6 +47,9 @@ class UploadMediaPresenterTest { @Mock private lateinit var place: Place + @Mock + private var location: LatLng? = null + @Mock private lateinit var uploadItem: UploadItem @@ -88,10 +91,11 @@ class UploadMediaPresenterTest { repository.preProcessImage( ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(Place::class.java), - ArgumentMatchers.any(UploadMediaPresenter::class.java) + ArgumentMatchers.any(UploadMediaPresenter::class.java), + ArgumentMatchers.any(LatLng::class.java) ) ).thenReturn(testObservableUploadItem) - uploadMediaPresenter.receiveImage(uploadableFile, place) + uploadMediaPresenter.receiveImage(uploadableFile, place, location) verify(view).showProgress(true) testScheduler.triggerActions() verify(view).onImageProcessed( @@ -114,7 +118,7 @@ class UploadMediaPresenterTest { .thenReturn(imageCoordinates) whenever(uploadItem.gpsCoords.decimalCoords) .thenReturn("imageCoordinates") - uploadMediaPresenter.verifyImageQuality(0) + uploadMediaPresenter.verifyImageQuality(0, location) verify(view).showProgress(true) testScheduler.triggerActions() verify(view).showProgress(false) @@ -133,7 +137,7 @@ class UploadMediaPresenterTest { .thenReturn(imageCoordinates) whenever(uploadItem.gpsCoords.decimalCoords) .thenReturn(null) - uploadMediaPresenter.verifyImageQuality(0) + uploadMediaPresenter.verifyImageQuality(0, location) testScheduler.triggerActions() } @@ -264,11 +268,12 @@ class UploadMediaPresenterTest { repository.preProcessImage( ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(Place::class.java), - ArgumentMatchers.any(UploadMediaPresenter::class.java) + ArgumentMatchers.any(UploadMediaPresenter::class.java), + ArgumentMatchers.any(LatLng::class.java) ) ).thenReturn(item) - uploadMediaPresenter.receiveImage(uploadableFile, germanyAsPlace) + uploadMediaPresenter.receiveImage(uploadableFile, germanyAsPlace, location) verify(view).showProgress(true) testScheduler.triggerActions() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt index bcd74d0d98..f1a5a3dff5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt @@ -62,6 +62,9 @@ class UploadRepositoryUnitTest { @Mock private lateinit var place: Place + @Mock + private var location: LatLng? = null + @Mock private lateinit var similarImageInterface: SimilarImageInterface @@ -175,8 +178,8 @@ class UploadRepositoryUnitTest { @Test fun testPreProcessImage() { assertEquals( - repository.preProcessImage(uploadableFile, place, similarImageInterface), - uploadModel.preProcessImage(uploadableFile, place, similarImageInterface) + repository.preProcessImage(uploadableFile, place, similarImageInterface, location), + uploadModel.preProcessImage(uploadableFile, place, similarImageInterface, location) ) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt index c0bc72532a..aaa36a3d1a 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt @@ -103,6 +103,9 @@ class UploadMediaDetailFragmentUnitTest { @Mock private lateinit var imageCoordinates: ImageCoordinates + @Mock + private var location: fr.free.nrw.commons.location.LatLng? = null + private lateinit var activity: UploadActivity @Before @@ -172,7 +175,7 @@ class UploadMediaDetailFragmentUnitTest { @Throws(Exception::class) fun testSetImageTobeUploaded() { Shadows.shadowOf(Looper.getMainLooper()).idle() - fragment.setImageTobeUploaded(null, null) + fragment.setImageTobeUploaded(null, null, location) } @Test @@ -374,7 +377,7 @@ class UploadMediaDetailFragmentUnitTest { `when`(latLng.longitude).thenReturn(0.0) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) 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 @@ -396,7 +399,9 @@ class UploadMediaDetailFragmentUnitTest { `when`(latLng.longitude).thenReturn(0.0) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) fragment.onActivityResult(1211, Activity.RESULT_OK, intent) - Mockito.verify(presenter, Mockito.times(1)).verifyImageQuality(0) + if (location == null) { + Mockito.verify(presenter, Mockito.times(1)).verifyImageQuality(0, location) + } } @Test From 655ff4c25521521c574e0d55e3f22f345c2a1018 Mon Sep 17 00:00:00 2001 From: Ritika Date: Sat, 24 Jun 2023 19:46:28 +0530 Subject: [PATCH 03/18] UploadMediaDetailFragmentUnitTest: modify testOnActivityResultAddLocationDialog to have null location --- .../mediaDetails/UploadMediaDetailFragmentUnitTest.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt index aaa36a3d1a..cb62761235 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt @@ -103,8 +103,6 @@ class UploadMediaDetailFragmentUnitTest { @Mock private lateinit var imageCoordinates: ImageCoordinates - @Mock - private var location: fr.free.nrw.commons.location.LatLng? = null private lateinit var activity: UploadActivity @@ -399,9 +397,7 @@ class UploadMediaDetailFragmentUnitTest { `when`(latLng.longitude).thenReturn(0.0) `when`(uploadItem.gpsCoords).thenReturn(imageCoordinates) fragment.onActivityResult(1211, Activity.RESULT_OK, intent) - if (location == null) { - Mockito.verify(presenter, Mockito.times(1)).verifyImageQuality(0, location) - } + Mockito.verify(presenter, Mockito.times(1)).verifyImageQuality(0, null) } @Test From a8e0c548e5af9d3289991ae6683d96e6b39d6744 Mon Sep 17 00:00:00 2001 From: Ritika Date: Sat, 24 Jun 2023 19:51:18 +0530 Subject: [PATCH 04/18] reintroduce removed variable --- .../upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt index cb62761235..67d8f22293 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt @@ -97,6 +97,9 @@ class UploadMediaDetailFragmentUnitTest { @Mock private lateinit var place: Place + @Mock + private var location: fr.free.nrw.commons.location.LatLng? = null + @Mock private lateinit var defaultKvStore: JsonKvStore From 72080183a0fefd68f64b49dbb6ad54a74639792d Mon Sep 17 00:00:00 2001 From: Ritika Date: Sun, 25 Jun 2023 20:36:08 +0530 Subject: [PATCH 05/18] enable prePopulateCategoriesAndDepictionsBy for current user location --- .../nrw/commons/repository/UploadRepository.java | 4 ++-- .../fr/free/nrw/commons/upload/FileProcessor.kt | 8 +++----- .../fr/free/nrw/commons/upload/FileUtils.java | 5 +++-- .../nrw/commons/upload/FileUtilsWrapper.java | 5 +++-- .../free/nrw/commons/upload/ImageCoordinates.kt | 16 +++++++++++++--- .../commons/upload/ImageProcessingService.java | 9 +++++---- .../fr/free/nrw/commons/upload/UploadModel.java | 4 ++-- .../mediaDetails/UploadMediaPresenter.java | 4 ++-- .../commons/upload/ImageProcessingServiceTest.kt | 16 +++++++++------- .../commons/upload/UploadMediaPresenterTest.kt | 8 ++++---- .../commons/upload/UploadRepositoryUnitTest.kt | 4 ++-- 11 files changed, 48 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index 5e5f604d05..b25c466208 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -199,8 +199,8 @@ public Observable preProcessImage(UploadableFile uploadableFile, Pla * @param uploadItem * @return */ - public Single getImageQuality(UploadItem uploadItem) { - return uploadModel.getImageQuality(uploadItem); + public Single getImageQuality(UploadItem uploadItem, LatLng location) { + return uploadModel.getImageQuality(uploadItem, location); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index 5abcb1b107..b59b66752b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -61,15 +61,13 @@ class FileProcessor @Inject constructor( // Redact EXIF data as indicated in preferences. redactExifTags(exifInterface, getExifTagsToRedact()) Timber.d("Calling GPSExtractor") - val originalImageCoordinates = ImageCoordinates(exifInterface) + val originalImageCoordinates = ImageCoordinates(exifInterface, location) if (originalImageCoordinates.decimalCoords == null) { - if (location == null) { //Find other photos taken around the same time which has gps coordinates findOtherImages( File(filePath), similarImageInterface ) - } } else { prePopulateCategoriesAndDepictionsBy(originalImageCoordinates) } @@ -166,11 +164,11 @@ class FileProcessor @Inject constructor( private fun readImageCoordinates(file: File) = try { - ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!) + ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!, null) } catch (e: IOException) { Timber.e(e) try { - ImageCoordinates(file.absolutePath) + ImageCoordinates(file.absolutePath, null) } catch (ex: IOException) { Timber.e(ex) null diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 10e1577055..52899de711 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -7,6 +7,7 @@ import androidx.exifinterface.media.ExifInterface; +import fr.free.nrw.commons.location.LatLng; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -64,11 +65,11 @@ static String getSHA1(InputStream is) { /** * Get Geolocation of filePath from input filePath path */ - static String getGeolocationOfFile(String filePath) { + static String getGeolocationOfFile(String filePath, LatLng location) { try { ExifInterface exifInterface = new ExifInterface(filePath); - ImageCoordinates imageObj = new ImageCoordinates(exifInterface); + ImageCoordinates imageObj = new ImageCoordinates(exifInterface, location); if (imageObj.getDecimalCoords() != null) { // If image has geolocation information in its EXIF return imageObj.getDecimalCoords(); } else { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java index 51bbd02bff..8f7dfd429e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.upload; import android.content.Context; +import fr.free.nrw.commons.location.LatLng; import io.reactivex.Observable; import java.io.BufferedInputStream; import java.io.File; @@ -37,8 +38,8 @@ public FileInputStream getFileInputStream(String filePath) throws FileNotFoundEx return FileUtils.getFileInputStream(filePath); } - public String getGeolocationOfFile(String filePath) { - return FileUtils.getGeolocationOfFile(filePath); + public String getGeolocationOfFile(String filePath, LatLng location) { + return FileUtils.getGeolocationOfFile(filePath, location); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt index 7486bb2358..1d8354dee1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt @@ -9,8 +9,10 @@ import java.io.InputStream /** * 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. + * 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?, location: LatLng?) { var decLatitude = 0.0 var decLongitude = 0.0 var imageCoordsExists = false @@ -26,13 +28,13 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) { /** * Construct from a stream. */ - internal constructor(stream: InputStream) : this(ExifInterface(stream)) + internal constructor(stream: InputStream, location: LatLng?) : this(ExifInterface(stream), location) /** * Construct from the file path of the image. * @param path file path of the image */ @Throws(IOException::class) - internal constructor(path: String) : this(ExifInterface(path)) + internal constructor(path: String, location: LatLng?) : this(ExifInterface(path), location) init { //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 imageCoordsExists = true Timber.d("EXIF data has location info") + } else if (location != null) { + decLatitude = location.latitude + decLongitude = location.longitude + if (!(decLatitude == 0.0 && decLongitude == 0.0)) { + decimalCoords = "$decLatitude|$decLongitude" + imageCoordsExists = true + Timber.d("Image coordinates recorded while using in-app camera") + } } } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java index 1f31c1690d..be93115bce 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java @@ -5,6 +5,7 @@ import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import android.content.Context; +import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.utils.ImageUtils; @@ -46,7 +47,7 @@ public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper, * Check image quality before upload - checks duplicate image - checks dark image - checks * geolocation for image - check for valid title */ - Single validateImage(UploadItem uploadItem) { + Single validateImage(UploadItem uploadItem, LatLng location) { int currentImageQuality = uploadItem.getImageQuality(); Timber.d("Current image quality is %d", currentImageQuality); if (currentImageQuality == ImageUtils.IMAGE_KEEP) { @@ -57,7 +58,7 @@ Single validateImage(UploadItem uploadItem) { return Single.zip( checkDuplicateImage(filePath), - checkImageGeoLocation(uploadItem.getPlace(), filePath), + checkImageGeoLocation(uploadItem.getPlace(), filePath, location), checkDarkImage(filePath), validateItemTitle(uploadItem), checkFBMD(filePath), @@ -148,13 +149,13 @@ private Single checkDarkImage(String filePath) { * @param filePath file to be checked * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK */ - private Single checkImageGeoLocation(Place place, String filePath) { + private Single checkImageGeoLocation(Place place, String filePath, LatLng location) { Timber.d("Checking for image geolocation %s", filePath); if (place == null || StringUtils.isBlank(place.getWikiDataEntityId())) { return Single.just(ImageUtils.IMAGE_OK); } return Single.fromCallable(() -> filePath) - .map(fileUtilsWrapper::getGeolocationOfFile) + .flatMap(path -> Single.just(fileUtilsWrapper.getGeolocationOfFile(path, location))) .flatMap(geoLocation -> { if (StringUtils.isBlank(geoLocation)) { return Single.just(ImageUtils.IMAGE_OK); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 4b5719cdca..821cf80e3d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -93,8 +93,8 @@ public Observable preProcessImage(final UploadableFile uploadableFil createAndAddUploadItem(uploadableFile, place, similarImageInterface, location)); } - public Single getImageQuality(final UploadItem uploadItem) { - return imageProcessingService.validateImage(uploadItem); + public Single getImageQuality(final UploadItem uploadItem, LatLng location) { + return imageProcessingService.validateImage(uploadItem, location); } private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, 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 5ce9c5ce63..0fb9018344 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 @@ -185,7 +185,7 @@ public void verifyImageQuality(int uploadItemIndex, LatLng location) { view.showProgress(true); compositeDisposable.add( repository - .getImageQuality(uploadItem) + .getImageQuality(uploadItem, location) .observeOn(mainThreadScheduler) .subscribe(imageResult -> { view.showProgress(false); @@ -208,7 +208,7 @@ public void verifyImageQuality(int uploadItemIndex, LatLng location) { view.showProgress(true); compositeDisposable.add( repository - .getImageQuality(uploadItem) + .getImageQuality(uploadItem, location) .observeOn(mainThreadScheduler) .subscribe(imageResult -> { view.showProgress(false); diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt index 5257debf61..2a2ff652b0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt @@ -28,6 +28,8 @@ class u { internal var readEXIF: EXIFReader?=null @Mock internal var mediaClient: MediaClient? = null + @Mock + internal var location: LatLng? = null @InjectMocks var imageProcessingService: ImageProcessingService? = null @@ -62,7 +64,7 @@ class u { `when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java))) .thenReturn("fileSha") - `when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString())) + `when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString(), location)) .thenReturn("latLng") `when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString())) @@ -88,7 +90,7 @@ class u { @Test fun validateImageForKeepImage() { `when`(uploadItem.imageQuality).thenReturn(ImageUtils.IMAGE_KEEP) - val validateImage = imageProcessingService!!.validateImage(uploadItem) + val validateImage = imageProcessingService!!.validateImage(uploadItem, location) assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) } @@ -96,13 +98,13 @@ class u { fun validateImageForDuplicateImage() { `when`(mediaClient!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) .thenReturn(Single.just(true)) - val validateImage = imageProcessingService!!.validateImage(uploadItem) + val validateImage = imageProcessingService!!.validateImage(uploadItem, location) assertEquals(ImageUtils.IMAGE_DUPLICATE, validateImage.blockingGet()) } @Test fun validateImageForOkImage() { - val validateImage = imageProcessingService!!.validateImage(uploadItem) + val validateImage = imageProcessingService!!.validateImage(uploadItem, location) assertEquals(ImageUtils.IMAGE_OK, validateImage.blockingGet()) } @@ -110,7 +112,7 @@ class u { fun validateImageForDarkImage() { `when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString())) .thenReturn(Single.just(ImageUtils.IMAGE_DARK)) - val validateImage = imageProcessingService!!.validateImage(uploadItem) + val validateImage = imageProcessingService!!.validateImage(uploadItem, location) assertEquals(ImageUtils.IMAGE_DARK, validateImage.blockingGet()) } @@ -118,7 +120,7 @@ class u { fun validateImageForWrongGeoLocation() { `when`(imageUtilsWrapper!!.checkImageGeolocationIsDifferent(ArgumentMatchers.anyString(), any(LatLng::class.java))) .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()) } @@ -126,7 +128,7 @@ class u { fun validateImageForFileNameExistsWithCheckTitleOn() { `when`(mediaClient?.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) .thenReturn(Single.just(true)) - val validateImage = imageProcessingService!!.validateImage(uploadItem) + val validateImage = imageProcessingService!!.validateImage(uploadItem, location) assertEquals(ImageUtils.FILE_NAME_EXISTS, validateImage.blockingGet()) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt index bd5540b29f..f75407a3d1 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt @@ -111,7 +111,7 @@ class UploadMediaPresenterTest { @Test fun verifyImageQualityTest() { whenever(repository.uploads).thenReturn(listOf(uploadItem)) - whenever(repository.getImageQuality(uploadItem)) + whenever(repository.getImageQuality(uploadItem, location)) .thenReturn(testSingleImageResult) whenever(uploadItem.imageQuality).thenReturn(0) whenever(uploadItem.gpsCoords) @@ -130,7 +130,7 @@ class UploadMediaPresenterTest { @Test fun `verify ImageQuality Test while coordinates equals to null`() { whenever(repository.uploads).thenReturn(listOf(uploadItem)) - whenever(repository.getImageQuality(uploadItem)) + whenever(repository.getImageQuality(uploadItem, location)) .thenReturn(testSingleImageResult) whenever(uploadItem.imageQuality).thenReturn(0) whenever(uploadItem.gpsCoords) @@ -171,7 +171,7 @@ class UploadMediaPresenterTest { val uploadMediaDetailList: ArrayList = ArrayList() uploadMediaDetailList.add(uploadMediaDetail) uploadItem.setMediaDetails(uploadMediaDetailList) - Mockito.`when`(repository.getImageQuality(uploadItem)).then { + Mockito.`when`(repository.getImageQuality(uploadItem, location)).then { verify(view).showProgress(true) testScheduler.triggerActions() verify(view).showProgress(true) @@ -187,7 +187,7 @@ class UploadMediaPresenterTest { uploadMediaDetail.captionText = "added caption" uploadMediaDetail.languageCode = "eo" uploadItem.setMediaDetails(Collections.singletonList(uploadMediaDetail)) - Mockito.`when`(repository.getImageQuality(uploadItem)).then { + Mockito.`when`(repository.getImageQuality(uploadItem, location)).then { verify(view).showProgress(true) testScheduler.triggerActions() verify(view).showProgress(true) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt index f1a5a3dff5..1f87b0f564 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt @@ -186,8 +186,8 @@ class UploadRepositoryUnitTest { @Test fun testGetImageQuality() { assertEquals( - repository.getImageQuality(uploadItem), - uploadModel.getImageQuality(uploadItem) + repository.getImageQuality(uploadItem, location), + uploadModel.getImageQuality(uploadItem, location) ) } From 3c0339b67c127b1e64ee6e1d75ee4a77fe5f48f9 Mon Sep 17 00:00:00 2001 From: Ritika Date: Sun, 25 Jun 2023 21:00:51 +0530 Subject: [PATCH 06/18] add relevant comment and fix failing test --- .../java/fr/free/nrw/commons/upload/FileProcessor.kt | 12 +++++++----- .../nrw/commons/upload/ImageProcessingServiceTest.kt | 2 +- .../UploadMediaDetailFragmentUnitTest.kt | 1 - 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index b59b66752b..9f18b27995 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -63,11 +63,11 @@ class FileProcessor @Inject constructor( Timber.d("Calling GPSExtractor") val originalImageCoordinates = ImageCoordinates(exifInterface, location) if (originalImageCoordinates.decimalCoords == null) { - //Find other photos taken around the same time which has gps coordinates - findOtherImages( - File(filePath), - similarImageInterface - ) + //Find other photos taken around the same time which has gps coordinates + findOtherImages( + File(filePath), + similarImageInterface + ) } else { prePopulateCategoriesAndDepictionsBy(originalImageCoordinates) } @@ -164,6 +164,8 @@ class FileProcessor @Inject constructor( private fun readImageCoordinates(file: File) = try { + /* 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) { Timber.e(e) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt index 2a2ff652b0..a553eaa1b1 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt @@ -64,7 +64,7 @@ class u { `when`(fileUtilsWrapper!!.getSHA1(any(FileInputStream::class.java))) .thenReturn("fileSha") - `when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString(), location)) + `when`(fileUtilsWrapper!!.getGeolocationOfFile(ArgumentMatchers.anyString(), any(LatLng::class.java))) .thenReturn("latLng") `when`(imageUtilsWrapper?.checkIfImageIsTooDark(ArgumentMatchers.anyString())) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt index 67d8f22293..5bac565912 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt @@ -106,7 +106,6 @@ class UploadMediaDetailFragmentUnitTest { @Mock private lateinit var imageCoordinates: ImageCoordinates - private lateinit var activity: UploadActivity @Before From 5f9ff36ecb594fc0302893b1403526123dc001fb Mon Sep 17 00:00:00 2001 From: Ritika Date: Wed, 28 Jun 2023 21:33:30 +0530 Subject: [PATCH 07/18] modify dialog and disable location tag redaction from EXIF --- .../contributions/ContributionController.java | 61 ++++++++++--------- .../free/nrw/commons/upload/FileProcessor.kt | 5 +- .../nrw/commons/upload/UploadActivity.java | 24 ++++++-- .../UploadMediaDetailFragment.java | 16 ----- app/src/main/res/values/strings.xml | 7 +-- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 1bffb820f2..52a9449fd6 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -35,7 +35,6 @@ public class ContributionController { public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads"; private final JsonKvStore defaultKvStore; private LatLng locationBeforeImageCapture; - private Boolean isLocationInfoPreferred; @Inject LocationServiceManager locationManager; @@ -56,7 +55,14 @@ public void initiateCameraPick(Activity activity) { PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, - () -> askUserToAttachLocation(activity), + () -> { + if (!(PermissionUtils.hasPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) + && isLocationAccessToAppsTurnedOn())) { + askUserToAllowLocationAccess(activity); + } else { + initiateCameraUpload(activity); + } + }, R.string.storage_permission_title, R.string.write_storage_permission_rationale); } @@ -68,26 +74,19 @@ public void initiateCameraPick(Activity activity) { * 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 - * - * If the user selects "No", then the location tag in EXIF is also removed. + * Otherwise, if the EXIF metadata does not have location information, + * then location captured by the app is used * * @param activity */ - private void askUserToAttachLocation(Activity activity) { + private void askUserToAllowLocationAccess(Activity activity) { DialogUtil.showAlertDialog(activity, - activity.getString(R.string.attach_location_info_dialog_title), - activity.getString(R.string.attach_location_info_explanation), - activity.getString(R.string.option_yes_great), - activity.getString(R.string.option_do_not_attach_location), - ()-> { - isLocationInfoPreferred = true; - requestForLocationAccess(activity); - }, - () -> { - isLocationInfoPreferred = false; - initiateCameraUpload(activity); - }, + activity.getString(R.string.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), + ()-> requestForLocationAccess(activity), + () -> initiateCameraUpload(activity), null, true); } @@ -102,11 +101,8 @@ private void askUserToAttachLocation(Activity activity) { private void requestForLocationAccess(Activity activity) { PermissionUtils.checkPermissionsAndPerformAction(activity, permission.ACCESS_FINE_LOCATION, - () -> { - isLocationInfoPreferred = true; - onLocationPermissionGranted(activity); - }, - () -> isLocationInfoPreferred = false, + () -> onLocationPermissionGranted(activity), + () -> {}, R.string.ask_to_turn_location_on, R.string.in_app_camera_location_permission_rationale); } @@ -114,10 +110,20 @@ private void requestForLocationAccess(Activity activity) { /** * Check if apps have access to location even after having individual access * + * @return + */ + private boolean isLocationAccessToAppsTurnedOn() { + return (locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled()); + } + + /** + * Initiate in-app camera if apps have access to location. + * Otherwise, show location-off dialog. + * * @param activity */ private void onLocationPermissionGranted(Activity activity) { - if (!(locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled())) { + if (!isLocationAccessToAppsTurnedOn()) { showLocationOffDialog(activity); } else { initiateCameraUpload(activity); @@ -203,12 +209,7 @@ private void setPickerConfiguration(Activity activity, */ private void initiateCameraUpload(Activity activity) { setPickerConfiguration(activity, false); - defaultKvStore.putBoolean("locationInfoPref", isLocationInfoPreferred); - if (isLocationInfoPreferred) { - locationBeforeImageCapture = locationManager.getLastLocation(); - } else { - locationBeforeImageCapture = null; - } + locationBeforeImageCapture = locationManager.getLastLocation(); FilePicker.openCameraForImage(activity, 0); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index 9f18b27995..f34bb22e76 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -84,12 +84,13 @@ class FileProcessor @Inject constructor( defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() val redactTags: Set = context.resources.getStringArray(R.array.pref_exifTag_values).toSet() + // This will be useful when implementing https://github.com/commons-app/apps-android-commons/issues/5247 /* Remove EXIF location if user has chosen "No, do not attach location" while using in-app camera */ - if (!defaultKvStore.getBoolean("locationInfoPref")) { + /*if (!defaultKvStore.getBoolean("locationInfoPref")) { val locationTag: String = context.getString(R.string.exif_tag_location) return redactTags - prefManageEXIFTags.minus(locationTag) - } + }*/ return redactTags - prefManageEXIFTags } 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 b589cf90e2..7d6297361b 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 @@ -44,6 +44,7 @@ import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.mwapi.UserClient; 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.upload.categories.UploadCategoriesFragment; import fr.free.nrw.commons.upload.depicts.DepictsFragment; @@ -61,6 +62,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Set; import javax.inject.Inject; import javax.inject.Named; import timber.log.Timber; @@ -383,13 +385,13 @@ private void receiveSharedItems() { currLocation = locationManager.getLastLocation(); if (currLocation != null) { float locationDifference = getLocationDifference(currLocation, prevLocation); - /* Remove location if app has access to it and is recording it but user - has tapped on "No, do not attach location". + boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); + /* Remove location if the user has unchecked the Location EXIF tag in the + Manage EXIF Tags setting. 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 (!defaultKvStore.getBoolean("locationInfoPref") - || locationDifference > 100) { + if (isLocationTagUnchecked || locationDifference > 100) { currLocation = null; } } @@ -452,6 +454,20 @@ public boolean isWLMUpload() { } } + /** + * 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 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 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 7f404ef716..f2d3e158e7 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 @@ -457,7 +457,6 @@ private void goToLocationPickerActivity(final UploadItem uploadItem) { Use location of last UploadItem if none of them is available */ if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords() .getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) { - Timber.d("Image coordinates taken from the EXIF metadata"); defaultLatitude = uploadItem.getGpsCoords() .getDecLatitude(); defaultLongitude = uploadItem.getGpsCoords().getDecLongitude(); @@ -469,21 +468,6 @@ private void goToLocationPickerActivity(final UploadItem uploadItem) { .zoom(defaultZoom).build()) .activityKey("UploadActivity") .build(getActivity()), REQUEST_CODE); - } else if (location != null && location.getLatitude() != 0.0 && location.getLongitude() != 0.0) { - Timber.d("Image coordinates recorded while using the in-app camera"); - defaultLatitude = location.getLatitude(); - defaultLongitude = location.getLongitude(); - - if(defaultKvStore.getString(LAST_ZOOM) != null){ - defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM)); - } - startActivityForResult(new LocationPicker.IntentBuilder() - .defaultLocation(new CameraPosition.Builder() - .target( - new com.mapbox.mapboxsdk.geometry.LatLng(defaultLatitude, defaultLongitude)) - .zoom(defaultZoom).build()) - .activityKey("UploadActivity") - .build(getActivity()), REQUEST_CODE); } else { if (defaultKvStore.getString(LAST_LOCATION) != null) { final String[] locationLatLng diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9098332977..10cb0b1233 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -443,10 +443,9 @@ Upload your first media by tapping on the add button. Use GET_CONTENT photo picker Disable if your pictures get uploaded without location Please make sure that this new Android picker does not strip location from your pictures. - Attach location with the image - Location data helps Wiki editors find your picture, making it much more useful. We suggest you let the app attach location information to the pictures taken via this button. Thank you for your contributions! - Yes, great! - No, do not attach location + 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. + Allow + Dismiss Please turn on location access from the Settings and try again. In-app camera needs location permission to attach it to your images in case location is not available in EXIF From e72be00eda462833a2c26348d5441e0707079007 Mon Sep 17 00:00:00 2001 From: Ritika Date: Tue, 4 Jul 2023 19:40:51 +0530 Subject: [PATCH 08/18] modify in-app camera dialog flow and change location to inAppPictureLocation --- .../contributions/ContributionController.java | 20 +++-- .../commons/contributions/MainActivity.java | 1 + .../commons/repository/UploadRepository.java | 4 +- .../commons/settings/SettingsFragment.java | 84 +++++++++++++++++++ .../free/nrw/commons/upload/FileProcessor.kt | 4 +- .../nrw/commons/upload/ImageCoordinates.kt | 8 +- .../nrw/commons/upload/UploadActivity.java | 5 +- .../free/nrw/commons/upload/UploadModel.java | 9 +- .../UploadMediaDetailFragment.java | 14 ++-- .../UploadMediaDetailsContract.java | 2 +- .../mediaDetails/UploadMediaPresenter.java | 5 +- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/preferences.xml | 5 ++ 13 files changed, 136 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 52a9449fd6..1863d38685 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -56,8 +56,8 @@ public void initiateCameraPick(Activity activity) { PermissionUtils.checkPermissionsAndPerformAction(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> { - if (!(PermissionUtils.hasPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) - && isLocationAccessToAppsTurnedOn())) { + if (defaultKvStore.getBoolean("inAppCameraFirstRun")) { + defaultKvStore.putBoolean("inAppCameraFirstRun", false); askUserToAllowLocationAccess(activity); } else { initiateCameraUpload(activity); @@ -81,12 +81,18 @@ && isLocationAccessToAppsTurnedOn())) { */ private void askUserToAllowLocationAccess(Activity activity) { DialogUtil.showAlertDialog(activity, - activity.getString(R.string.location_permission_title), + 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), - ()-> requestForLocationAccess(activity), - () -> initiateCameraUpload(activity), + ()-> { + defaultKvStore.putBoolean("inAppCameraLocationPref", true); + requestForLocationAccess(activity); + }, + () -> { + defaultKvStore.putBoolean("inAppCameraLocationPref", false); + initiateCameraUpload(activity); + }, null, true); } @@ -209,7 +215,9 @@ private void setPickerConfiguration(Activity activity, */ private void initiateCameraUpload(Activity activity) { setPickerConfiguration(activity, false); - locationBeforeImageCapture = locationManager.getLastLocation(); + if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) { + locationBeforeImageCapture = locationManager.getLastLocation(); + } FilePicker.openCameraForImage(activity, 0); } 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 a96f1f37bd..740952664e 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 @@ -403,6 +403,7 @@ protected void onResume() { if ((applicationKvStore.getBoolean("firstrun", true)) && (!applicationKvStore.getBoolean("login_skipped"))) { + defaultKvStore.putBoolean("inAppCameraFirstRun", true); WelcomeActivity.startYourself(this); } } diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java index b25c466208..919c8d2cfc 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java @@ -188,9 +188,9 @@ public int getCount() { * @return */ public Observable preProcessImage(UploadableFile uploadableFile, Place place, - SimilarImageInterface similarImageInterface, LatLng location) { + SimilarImageInterface similarImageInterface, LatLng inAppPictureLocation) { return uploadModel.preProcessImage(uploadableFile, place, - similarImageInterface, location); + similarImageInterface, inAppPictureLocation); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 0846fa9dc4..d0ce46c94d 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -3,13 +3,16 @@ import static android.content.Context.MODE_PRIVATE; import android.Manifest; +import android.Manifest.permission; import android.app.Activity; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; import android.text.Editable; import android.text.TextWatcher; import android.view.View; @@ -37,6 +40,7 @@ import fr.free.nrw.commons.contributions.MainActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.kvstore.JsonKvStore; +import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.logging.CommonsLogSender; import fr.free.nrw.commons.recentlanguages.Language; import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; @@ -65,6 +69,9 @@ public class SettingsFragment extends PreferenceFragmentCompat { @Inject RecentLanguagesDao recentLanguagesDao; + @Inject + LocationServiceManager locationManager; + private ListPreference themeListPreference; private Preference descriptionLanguageListPreference; private Preference appUiLanguageListPreference; @@ -97,6 +104,18 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }); } + Preference inAppCameraLocationPref = findPreference("inAppCameraLocationPref"); + + inAppCameraLocationPref.setOnPreferenceChangeListener( + (preference, newValue) -> { + boolean isInAppCameraLocationTurnedOn = (boolean) newValue; + if (isInAppCameraLocationTurnedOn) { + requestForLocationAccess(); + } + return true; + } + ); + // Gets current language code from shared preferences String languageCode; @@ -175,6 +194,71 @@ public boolean onPreferenceClick(Preference preference) { } } + /** + * Ask for location permission if the user agrees on attaching location with pictures + * and the app does not have the access to location + * + */ + private void requestForLocationAccess() { + PermissionUtils.checkPermissionsAndPerformAction(getActivity(), + permission.ACCESS_FINE_LOCATION, + () -> onLocationPermissionGranted(getActivity()), + () -> {}, + R.string.ask_to_turn_location_on, + R.string.in_app_camera_location_permission_rationale); + } + + /** + * Check if apps have access to location even after having individual access + * + * @return + */ + private boolean isLocationAccessToAppsTurnedOn() { + return (locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled()); + } + + /** + * Initiate in-app camera if apps have access to location. + * Otherwise, show location-off dialog. + * + * @param activity + */ + private void onLocationPermissionGranted(Activity activity) { + if (!isLocationAccessToAppsTurnedOn()) { + showLocationOffDialog(activity); + } + } + + /** + * Ask user to grant location access to apps + * + * @param activity + */ + private void showLocationOffDialog(Activity activity) { + DialogUtil + .showAlertDialog(activity, + activity.getString(R.string.location_permission_title), + activity.getString(R.string.in_app_camera_needs_location), + activity.getString(R.string.title_app_shortcut_setting), + () -> openLocationSettings(activity), + true); + } + + /** + * Open location source settings so that apps with location access can access it + * + * @param activity + */ + + private void openLocationSettings(Activity activity) { + final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + final PackageManager packageManager = activity.getPackageManager(); + + if (intent.resolveActivity(packageManager)!= null) { + activity.startActivity(intent); + } + } + /** * On some devices, the new Photo Picker with GET_CONTENT takeover * redacts location tags from EXIF metadata diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index f34bb22e76..3630b50bd3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -50,7 +50,7 @@ class FileProcessor @Inject constructor( * Processes filePath coordinates, either from EXIF data or user location */ fun processFileCoordinates(similarImageInterface: SimilarImageInterface, - filePath: String?, location: LatLng?) + filePath: String?, inAppPictureLocation: LatLng?) : ImageCoordinates { val exifInterface: ExifInterface? = try { ExifInterface(filePath!!) @@ -61,7 +61,7 @@ class FileProcessor @Inject constructor( // Redact EXIF data as indicated in preferences. redactExifTags(exifInterface, getExifTagsToRedact()) Timber.d("Calling GPSExtractor") - val originalImageCoordinates = ImageCoordinates(exifInterface, location) + val originalImageCoordinates = ImageCoordinates(exifInterface, inAppPictureLocation) if (originalImageCoordinates.decimalCoords == null) { //Find other photos taken around the same time which has gps coordinates findOtherImages( diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt index 1d8354dee1..b2707d5dc9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt @@ -12,7 +12,7 @@ import java.io.InputStream * 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?, location: LatLng?) { +class ImageCoordinates internal constructor(exif: ExifInterface?, inAppPictureLocation: LatLng?) { var decLatitude = 0.0 var decLongitude = 0.0 var imageCoordsExists = false @@ -63,9 +63,9 @@ class ImageCoordinates internal constructor(exif: ExifInterface?, location: LatL //If image has EXIF data, extract image coords imageCoordsExists = true Timber.d("EXIF data has location info") - } else if (location != null) { - decLatitude = location.latitude - decLongitude = location.longitude + } else if (inAppPictureLocation != null) { + decLatitude = inAppPictureLocation.latitude + decLongitude = inAppPictureLocation.longitude if (!(decLatitude == 0.0 && decLongitude == 0.0)) { decimalCoords = "$decLatitude|$decLongitude" imageCoordsExists = true 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 7d6297361b..8182897d5f 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 @@ -387,11 +387,12 @@ private void receiveSharedItems() { 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. + 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) { + if (isLocationTagUnchecked || locationDifference > 100 + || !defaultKvStore.getBoolean("inAppCameraLocationPref")) { currLocation = null; } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 821cf80e3d..f39beecb3c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -88,9 +88,9 @@ public void setSelectedCategories(List selectedCategories) { public Observable preProcessImage(final UploadableFile uploadableFile, final Place place, final SimilarImageInterface similarImageInterface, - LatLng location) { + LatLng inAppPictureLocation) { return Observable.just( - createAndAddUploadItem(uploadableFile, place, similarImageInterface, location)); + createAndAddUploadItem(uploadableFile, place, similarImageInterface, inAppPictureLocation)); } public Single getImageQuality(final UploadItem uploadItem, LatLng location) { @@ -100,7 +100,7 @@ public Single getImageQuality(final UploadItem uploadItem, LatLng locat private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, final Place place, final SimilarImageInterface similarImageInterface, - LatLng location) { + LatLng inAppPictureLocation) { final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile .getFileCreatedDate(context); long fileCreatedDate = -1; @@ -113,7 +113,8 @@ private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, } Timber.d("File created date is %d", fileCreatedDate); final ImageCoordinates imageCoordinates = fileProcessor - .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(), location); + .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath(), + inAppPictureLocation); final UploadItem uploadItem = new UploadItem( Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate, 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 f2d3e158e7..6f7c7c72bb 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 @@ -119,7 +119,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements */ private Place nearbyPlace; private UploadItem uploadItem; - private LatLng location; + /** + * 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 */ @@ -136,10 +140,10 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } - public void setImageTobeUploaded(UploadableFile uploadableFile, Place place, LatLng location) { + public void setImageTobeUploaded(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation) { this.uploadableFile = uploadableFile; this.place = place; - this.location = location; + this.inAppPictureLocation = inAppPictureLocation; } @Nullable @@ -164,7 +168,7 @@ private void init() { tooltip.setOnClickListener( v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip)); initPresenter(); - presenter.receiveImage(uploadableFile, place, location); + presenter.receiveImage(uploadableFile, place, inAppPictureLocation); initRecyclerView(); if (callback.getIndexInViewFlipper(this) == 0) { @@ -226,7 +230,7 @@ private void showInfoAlert(int titleStringID, int messageStringId) { @OnClick(R.id.btn_next) public void onNextButtonClicked() { - presenter.verifyImageQuality(callback.getIndexInViewFlipper(this), location); + presenter.verifyImageQuality(callback.getIndexInViewFlipper(this), inAppPictureLocation); } @OnClick(R.id.btn_previous) 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 159430aa3d..e24b5ad34d 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 @@ -44,7 +44,7 @@ interface View extends SimilarImageInterface { interface UserActionListener extends BasePresenter { - void receiveImage(UploadableFile uploadableFile, Place place, LatLng location); + void receiveImage(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation); void verifyImageQuality(int uploadItemIndex, LatLng location); 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 0fb9018344..7377b40c8e 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 @@ -87,11 +87,12 @@ public void onDetachView() { * @param place */ @Override - public void receiveImage(final UploadableFile uploadableFile, final Place place, LatLng imageLocation) { + public void receiveImage(final UploadableFile uploadableFile, final Place place, + LatLng inAppPictureLocation) { view.showProgress(true); compositeDisposable.add( repository - .preProcessImage(uploadableFile, place, this, imageLocation) + .preProcessImage(uploadableFile, place, this, inAppPictureLocation) .map(uploadItem -> { if(place!=null && place.isMonument()){ if (place.location != null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 10cb0b1233..0f28646893 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -190,6 +190,8 @@ Required permission: Read external storage. App cannot access your gallery without this. Required permission: Write external storage. App cannot access your camera/gallery without this. Requesting Location Permission + Record location for in-app shots + Enable this to record location while using in-app camera OK Warning Duplicate File Name found @@ -443,7 +445,7 @@ Upload your first media by tapping on the add button. Use GET_CONTENT photo picker Disable if your pictures get uploaded without location Please make sure that this new Android picker does not strip location from your pictures. - 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. + 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 Allow Dismiss Please turn on location access from the Settings and try again. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 17360bd2e9..0d4ee1e47c 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -88,6 +88,11 @@ app:singleLineTitle="false" android:summary="@string/manage_exif_tags_summary" android:title="@string/manage_exif_tags" /> + +