Skip to content

Commit 5073ca0

Browse files
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
1 parent 1cab938 commit 5073ca0

21 files changed

+537
-92
lines changed

app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@
66
import android.app.Activity;
77
import android.content.Context;
88
import android.content.Intent;
9+
import android.widget.Toast;
910
import androidx.annotation.NonNull;
1011
import fr.free.nrw.commons.R;
1112
import fr.free.nrw.commons.filepicker.DefaultCallback;
1213
import fr.free.nrw.commons.filepicker.FilePicker;
1314
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
1415
import fr.free.nrw.commons.filepicker.UploadableFile;
1516
import fr.free.nrw.commons.kvstore.JsonKvStore;
17+
import fr.free.nrw.commons.location.LatLng;
18+
import fr.free.nrw.commons.location.LocationPermissionsHelper;
19+
import fr.free.nrw.commons.location.LocationPermissionsHelper.Dialog;
20+
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
21+
import fr.free.nrw.commons.location.LocationServiceManager;
1622
import fr.free.nrw.commons.nearby.Place;
1723
import fr.free.nrw.commons.upload.UploadActivity;
24+
import fr.free.nrw.commons.utils.DialogUtil;
1825
import fr.free.nrw.commons.utils.PermissionUtils;
1926
import fr.free.nrw.commons.utils.ViewUtil;
2027
import java.util.ArrayList;
@@ -28,7 +35,11 @@ public class ContributionController {
2835

2936
public static final String ACTION_INTERNAL_UPLOADS = "internalImageUploads";
3037
private final JsonKvStore defaultKvStore;
38+
private LatLng locationBeforeImageCapture;
39+
private boolean isInAppCameraUpload;
3140

41+
@Inject
42+
LocationServiceManager locationManager;
3243
@Inject
3344
public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) {
3445
this.defaultKvStore = defaultKvStore;
@@ -46,11 +57,94 @@ public void initiateCameraPick(Activity activity) {
4657

4758
PermissionUtils.checkPermissionsAndPerformAction(activity,
4859
Manifest.permission.WRITE_EXTERNAL_STORAGE,
49-
() -> initiateCameraUpload(activity),
60+
() -> {
61+
if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
62+
defaultKvStore.putBoolean("inAppCameraFirstRun", false);
63+
askUserToAllowLocationAccess(activity);
64+
} else if(defaultKvStore.getBoolean("inAppCameraLocationPref")) {
65+
createDialogsAndHandleLocationPermissions(activity);
66+
} else {
67+
initiateCameraUpload(activity);
68+
}
69+
},
5070
R.string.storage_permission_title,
5171
R.string.write_storage_permission_rationale);
5272
}
5373

74+
/**
75+
* Asks users to provide location access
76+
*
77+
* @param activity
78+
*/
79+
private void createDialogsAndHandleLocationPermissions(Activity activity) {
80+
LocationPermissionsHelper.Dialog locationAccessDialog = new Dialog(
81+
R.string.location_permission_title,
82+
R.string.in_app_camera_location_permission_rationale
83+
);
84+
85+
LocationPermissionsHelper.Dialog locationOffDialog = new Dialog(
86+
R.string.ask_to_turn_location_on,
87+
R.string.in_app_camera_needs_location
88+
);
89+
LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
90+
activity, locationManager,
91+
new LocationPermissionCallback() {
92+
@Override
93+
public void onLocationPermissionDenied() {
94+
initiateCameraUpload(activity);
95+
}
96+
97+
@Override
98+
public void onLocationPermissionGranted() {
99+
initiateCameraUpload(activity);
100+
}
101+
}
102+
);
103+
locationPermissionsHelper.handleLocationPermissions(
104+
locationAccessDialog,
105+
locationOffDialog
106+
);
107+
}
108+
109+
/**
110+
* Suggest user to attach location information with pictures.
111+
* If the user selects "Yes", then:
112+
*
113+
* Location is taken from the EXIF if the default camera application
114+
* does not redact location tags.
115+
*
116+
* Otherwise, if the EXIF metadata does not have location information,
117+
* then location captured by the app is used
118+
*
119+
* @param activity
120+
*/
121+
private void askUserToAllowLocationAccess(Activity activity) {
122+
DialogUtil.showAlertDialog(activity,
123+
activity.getString(R.string.in_app_camera_location_permission_title),
124+
activity.getString(R.string.in_app_camera_location_access_explanation),
125+
activity.getString(R.string.option_allow),
126+
activity.getString(R.string.option_dismiss),
127+
()-> {
128+
defaultKvStore.putBoolean("inAppCameraLocationPref", true);
129+
createDialogsAndHandleLocationPermissions(activity);
130+
},
131+
() -> {
132+
defaultKvStore.putBoolean("inAppCameraLocationPref", false);
133+
initiateCameraUpload(activity);
134+
},
135+
null,
136+
true);
137+
}
138+
139+
/**
140+
* Check if apps have access to location even after having individual access
141+
*
142+
* @return
143+
*/
144+
private boolean isLocationAccessToAppsTurnedOn() {
145+
return (locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled());
146+
}
147+
54148
/**
55149
* Initiate gallery picker
56150
*/
@@ -66,9 +160,7 @@ public void initiateCustomGalleryPickWithPermission(final Activity activity) {
66160

67161
PermissionUtils.checkPermissionsAndPerformAction(activity,
68162
Manifest.permission.WRITE_EXTERNAL_STORAGE,
69-
() -> {
70-
FilePicker.openCustomSelector(activity, 0);
71-
},
163+
() -> FilePicker.openCustomSelector(activity, 0),
72164
R.string.storage_permission_title,
73165
R.string.write_storage_permission_rationale);
74166
}
@@ -99,6 +191,10 @@ private void setPickerConfiguration(Activity activity,
99191
*/
100192
private void initiateCameraUpload(Activity activity) {
101193
setPickerConfiguration(activity, false);
194+
if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
195+
locationBeforeImageCapture = locationManager.getLastLocation();
196+
}
197+
isInAppCameraUpload = true;
102198
FilePicker.openCameraForImage(activity, 0);
103199
}
104200

@@ -134,7 +230,8 @@ public List<UploadableFile> handleExternalImagesPicked(Activity activity,
134230

135231
/**
136232
* Returns intent to be passed to upload activity
137-
* Attaches place object for nearby uploads
233+
* Attaches place object for nearby uploads and
234+
* location before image capture if in-app camera is used
138235
*/
139236
private Intent handleImagesPicked(Context context,
140237
List<UploadableFile> imagesFiles) {
@@ -148,6 +245,17 @@ private Intent handleImagesPicked(Context context,
148245
shareIntent.putExtra(PLACE_OBJECT, place);
149246
}
150247

248+
if (locationBeforeImageCapture != null) {
249+
shareIntent.putExtra(
250+
UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
251+
locationBeforeImageCapture);
252+
}
253+
254+
shareIntent.putExtra(
255+
UploadActivity.IN_APP_CAMERA_UPLOAD,
256+
isInAppCameraUpload
257+
);
258+
isInAppCameraUpload = false; // reset the flag for next use
151259
return shareIntent;
152260
}
153261

app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ protected void onResume() {
403403

404404
if ((applicationKvStore.getBoolean("firstrun", true)) &&
405405
(!applicationKvStore.getBoolean("login_skipped"))) {
406+
defaultKvStore.putBoolean("inAppCameraFirstRun", true);
406407
WelcomeActivity.startYourself(this);
407408
}
408409
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package fr.free.nrw.commons.location;
2+
3+
import android.Manifest.permission;
4+
import android.app.Activity;
5+
import android.content.Intent;
6+
import android.content.pm.PackageManager;
7+
import android.provider.Settings;
8+
import android.widget.Toast;
9+
import fr.free.nrw.commons.R;
10+
import fr.free.nrw.commons.utils.DialogUtil;
11+
import fr.free.nrw.commons.utils.PermissionUtils;
12+
13+
/**
14+
* Helper class to handle location permissions
15+
*/
16+
public class LocationPermissionsHelper {
17+
Activity activity;
18+
LocationServiceManager locationManager;
19+
LocationPermissionCallback callback;
20+
public LocationPermissionsHelper(Activity activity, LocationServiceManager locationManager,
21+
LocationPermissionCallback callback) {
22+
this.activity = activity;
23+
this.locationManager = locationManager;
24+
this.callback = callback;
25+
}
26+
public static class Dialog {
27+
int dialogTitleResource;
28+
int dialogTextResource;
29+
30+
public Dialog(int dialogTitle, int dialogText) {
31+
dialogTitleResource = dialogTitle;
32+
dialogTextResource = dialogText;
33+
}
34+
}
35+
36+
/**
37+
* Handles the entire location permissions flow
38+
*
39+
* @param locationAccessDialog
40+
* @param locationOffDialog
41+
*/
42+
public void handleLocationPermissions(Dialog locationAccessDialog,
43+
Dialog locationOffDialog) {
44+
requestForLocationAccess(locationAccessDialog, locationOffDialog);
45+
}
46+
47+
/**
48+
* Ask for location permission if the user agrees on attaching location with pictures
49+
* and the app does not have the access to location
50+
*
51+
* @param locationAccessDialog
52+
* @param locationOffDialog
53+
*/
54+
private void requestForLocationAccess(
55+
Dialog locationAccessDialog,
56+
Dialog locationOffDialog
57+
) {
58+
PermissionUtils.checkPermissionsAndPerformAction(activity,
59+
permission.ACCESS_FINE_LOCATION,
60+
() -> {
61+
if(!isLocationAccessToAppsTurnedOn()) {
62+
showLocationOffDialog(locationOffDialog);
63+
} else {
64+
if (callback != null) {
65+
callback.onLocationPermissionGranted();
66+
}
67+
}
68+
},
69+
() -> {
70+
if (callback != null) {
71+
Toast.makeText(
72+
activity,
73+
R.string.in_app_camera_location_permission_denied,
74+
Toast.LENGTH_LONG
75+
).show();
76+
callback.onLocationPermissionDenied();
77+
}
78+
},
79+
locationAccessDialog.dialogTitleResource,
80+
locationAccessDialog.dialogTextResource);
81+
}
82+
83+
/**
84+
* Check if apps have access to location even after having individual access
85+
*
86+
* @return
87+
*/
88+
public boolean isLocationAccessToAppsTurnedOn() {
89+
return (locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled());
90+
}
91+
92+
/**
93+
* Ask user to grant location access to apps
94+
*
95+
*/
96+
97+
private void showLocationOffDialog(Dialog locationOffDialog) {
98+
DialogUtil
99+
.showAlertDialog(activity,
100+
activity.getString(locationOffDialog.dialogTitleResource),
101+
activity.getString(locationOffDialog.dialogTextResource),
102+
activity.getString(R.string.title_app_shortcut_setting),
103+
activity.getString(R.string.cancel),
104+
() -> openLocationSettings(),
105+
() -> {
106+
Toast.makeText(
107+
activity,
108+
R.string.in_app_camera_location_unavailable,
109+
Toast.LENGTH_LONG
110+
).show();
111+
callback.onLocationPermissionDenied();
112+
});
113+
}
114+
115+
/**
116+
* Open location source settings so that apps with location access can access it
117+
*
118+
* TODO: modify it to fix https://github.com/commons-app/apps-android-commons/issues/5255
119+
*/
120+
121+
private void openLocationSettings() {
122+
final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
123+
final PackageManager packageManager = activity.getPackageManager();
124+
125+
if (intent.resolveActivity(packageManager)!= null) {
126+
activity.startActivity(intent);
127+
}
128+
}
129+
130+
/**
131+
* Handle onPermissionDenied within individual classes based on the requirements
132+
*/
133+
public interface LocationPermissionCallback {
134+
void onLocationPermissionDenied();
135+
void onLocationPermissionGranted();
136+
}
137+
}

app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ public int getCount() {
188188
* @return
189189
*/
190190
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
191-
SimilarImageInterface similarImageInterface) {
191+
SimilarImageInterface similarImageInterface, LatLng inAppPictureLocation) {
192192
return uploadModel.preProcessImage(uploadableFile, place,
193-
similarImageInterface);
193+
similarImageInterface, inAppPictureLocation);
194194
}
195195

196196
/**
@@ -199,8 +199,8 @@ public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Pla
199199
* @param uploadItem
200200
* @return
201201
*/
202-
public Single<Integer> getImageQuality(UploadItem uploadItem) {
203-
return uploadModel.getImageQuality(uploadItem);
202+
public Single<Integer> getImageQuality(UploadItem uploadItem, LatLng location) {
203+
return uploadModel.getImageQuality(uploadItem, location);
204204
}
205205

206206
/**

0 commit comments

Comments
 (0)