Skip to content

Commit cc0b059

Browse files
VitalyVPinchukneslihanturan
authored andcommitted
Image EXIF/XMP metadata removal routine (commons-app#2863)
* [WIP] Added preferences for EXIF tags * [WIP] Added arrays, keys, strings to support EXIF preferences * [WIP] Updated SettingsFragment to setup summary of added preferences(locationAccuracy) * [WIP] Added methods getStringSet()in BasicKvStore, KeyValueStore to support Set<String> data type used in preferences (EXIF tags) * [WIP] Added methods for removing EXIF tags and anonimyzing location coordinates in FileProcessor, GPSExtractor * [WIP] Fixed errors in preferences EXIF tags, added XMP removal routine * [WIP] Removed erroneous location accuracy handling * [WIP] Fixed mistyped GPS Tags * Reverted BasicKvStore. Removed Set<String> support in BasicKvStore as JsonKVStore already has it. * FileProcessor: Replaced throwing runtime exception with warning if EXIF redaction fails. * FileMetadataUtils: Javadoc added * [WIP] FileMetadataUtilsTest added * [WIP] FileMetadataUtilsTest: added javadoc * [WIP] FileMetadataUtilsTest: added javadoc * [WIP] FileProcessor: fixed disposing observables * [WIP] FileMetadataUtils.getTagsFromPref: changed return type from observable to simple array * [WIP] FileProcessorTest: added test for redactExifTags * [WIP] FileProcessorTest: redactExifTags() doesn't work properly
1 parent 5690dd9 commit cc0b059

File tree

13 files changed

+291
-2
lines changed

13 files changed

+291
-2
lines changed

app/src/main/java/fr/free/nrw/commons/settings/Prefs.java

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public class Prefs {
77
public static final String DEFAULT_LICENSE = "defaultLicense";
88
public static final String UPLOADS_SHOWING = "uploadsshowing";
99
public static final String IS_CONTRIBUTION_COUNT_CHANGED = "ccontributionCountChanged";
10+
public static final String MANAGED_EXIF_TAGS = "managedExifTags";
1011

1112
public static class Licenses {
1213
public static final String CC_BY_SA_3 = "CC BY-SA 3.0";

app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java

+14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.net.Uri;
55
import android.os.Bundle;
66
import android.preference.EditTextPreference;
7+
import android.preference.MultiSelectListPreference;
78
import android.preference.Preference;
89
import android.preference.PreferenceFragment;
910
import android.preference.SwitchPreference;
@@ -14,6 +15,11 @@
1415
import com.karumi.dexter.listener.PermissionGrantedResponse;
1516
import com.karumi.dexter.listener.single.BasePermissionListener;
1617

18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
1723
import javax.inject.Inject;
1824
import javax.inject.Named;
1925

@@ -59,6 +65,14 @@ public void onCreate(Bundle savedInstanceState) {
5965
return true;
6066
});
6167

68+
MultiSelectListPreference multiSelectListPref = (MultiSelectListPreference) findPreference("manageExifTags");
69+
if (multiSelectListPref != null) {
70+
multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> {
71+
defaultKvStore.putJson(Prefs.MANAGED_EXIF_TAGS, newValue);
72+
return true;
73+
});
74+
}
75+
6276
final EditTextPreference uploadLimit = (EditTextPreference) findPreference("uploads");
6377
int currentUploadLimit = defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 100);
6478
uploadLimit.setText(Integer.toString(currentUploadLimit));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package fr.free.nrw.commons.ui.LongTitlePreferences;
2+
3+
import android.content.Context;
4+
import android.preference.MultiSelectListPreference;
5+
import android.util.AttributeSet;
6+
import android.view.View;
7+
import android.widget.TextView;
8+
9+
public class LongTitleMultiSelectListPreference extends MultiSelectListPreference {
10+
/*
11+
public LongTitleMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
12+
super(context, attrs, defStyleAttr, defStyleRes);
13+
}
14+
15+
public LongTitleMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
16+
super(context, attrs, defStyleAttr);
17+
}
18+
*/
19+
public LongTitleMultiSelectListPreference(Context context, AttributeSet attrs) {
20+
super(context, attrs);
21+
}
22+
23+
public LongTitleMultiSelectListPreference(Context context) {
24+
super(context);
25+
}
26+
27+
@Override
28+
protected void onBindView(View view)
29+
{
30+
super.onBindView(view);
31+
32+
TextView title= view.findViewById(android.R.id.title);
33+
if (title != null) {
34+
title.setSingleLine(false);
35+
}
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package fr.free.nrw.commons.upload;
2+
3+
import timber.log.Timber;
4+
5+
import static androidx.exifinterface.media.ExifInterface.*;
6+
7+
/**
8+
* Support utils for EXIF metadata handling
9+
*
10+
*/
11+
public class FileMetadataUtils {
12+
13+
/**
14+
* Takes EXIF label from sharedPreferences as input and returns relevant EXIF tags
15+
*
16+
* @param pref EXIF sharedPreference label
17+
* @return EXIF tags
18+
*/
19+
public static String[] getTagsFromPref(String pref) {
20+
Timber.d("Retuning tags for pref:%s", pref);
21+
switch (pref) {
22+
case "Author":
23+
return new String[]{TAG_ARTIST, TAG_CAMARA_OWNER_NAME};
24+
case "Copyright":
25+
return new String[]{TAG_COPYRIGHT};
26+
case "Location":
27+
return new String[]{TAG_GPS_LATITUDE, TAG_GPS_LATITUDE_REF,
28+
TAG_GPS_LONGITUDE, TAG_GPS_LONGITUDE_REF,
29+
TAG_GPS_ALTITUDE, TAG_GPS_ALTITUDE_REF};
30+
case "Camera Model":
31+
return new String[]{TAG_MAKE, TAG_MODEL};
32+
case "Lens Model":
33+
return new String[]{TAG_LENS_MAKE, TAG_LENS_MODEL, TAG_LENS_SPECIFICATION};
34+
case "Serial Numbers":
35+
return new String[]{TAG_BODY_SERIAL_NUMBER, TAG_LENS_SERIAL_NUMBER};
36+
case "Software":
37+
return new String[]{TAG_SOFTWARE};
38+
default:
39+
return new String[]{};
40+
}
41+
}
42+
43+
}

app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java

+65-1
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,34 @@
22

33
import android.annotation.SuppressLint;
44
import android.content.ContentResolver;
5+
import android.content.Context;
56
import android.net.Uri;
67
import androidx.annotation.NonNull;
78

89
import java.io.File;
910
import java.io.IOException;
11+
import java.lang.reflect.Type;
12+
import java.util.Arrays;
13+
import java.util.HashSet;
1014
import java.util.List;
15+
import java.util.Set;
1116

1217
import javax.inject.Inject;
1318
import javax.inject.Named;
1419
import javax.inject.Singleton;
1520

1621
import androidx.exifinterface.media.ExifInterface;
22+
23+
import com.google.gson.reflect.TypeToken;
24+
25+
import fr.free.nrw.commons.R;
1726
import fr.free.nrw.commons.caching.CacheController;
1827
import fr.free.nrw.commons.kvstore.JsonKvStore;
1928
import fr.free.nrw.commons.mwapi.CategoryApi;
29+
import fr.free.nrw.commons.settings.Prefs;
30+
import io.reactivex.Observable;
2031
import io.reactivex.disposables.CompositeDisposable;
32+
import io.reactivex.disposables.Disposable;
2133
import io.reactivex.schedulers.Schedulers;
2234
import timber.log.Timber;
2335

@@ -66,7 +78,10 @@ void initFileDetails(@NonNull String filePath, ContentResolver contentResolver)
6678
/**
6779
* Processes filePath coordinates, either from EXIF data or user location
6880
*/
69-
GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) {
81+
GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface, Context context) {
82+
// Redact EXIF data as indicated in preferences.
83+
redactExifTags(exifInterface, getExifTagsToRedact(context));
84+
7085
Timber.d("Calling GPSExtractor");
7186
imageObj = new GPSExtractor(exifInterface);
7287
decimalCoords = imageObj.getCoords();
@@ -81,6 +96,55 @@ GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface)
8196
return imageObj;
8297
}
8398

99+
/**
100+
* Gets EXIF Tags from preferences to be redacted.
101+
*
102+
* @param context application context
103+
* @return tags to be redacted
104+
*/
105+
private Set<String> getExifTagsToRedact(Context context) {
106+
Type setType = new TypeToken<Set<String>>() {}.getType();
107+
Set<String> prefManageEXIFTags = defaultKvStore.getJson(Prefs.MANAGED_EXIF_TAGS, setType);
108+
109+
Set<String> redactTags = new HashSet<>(Arrays.asList(
110+
context.getResources().getStringArray(R.array.pref_exifTag_values)));
111+
Timber.d(redactTags.toString());
112+
113+
if (prefManageEXIFTags != null) redactTags.removeAll(prefManageEXIFTags);
114+
115+
return redactTags;
116+
}
117+
118+
/**
119+
* Redacts EXIF metadata as indicated in preferences.
120+
*
121+
* @param exifInterface ExifInterface object
122+
* @param redactTags tags to be redacted
123+
*/
124+
public static void redactExifTags(ExifInterface exifInterface, Set<String> redactTags) {
125+
if(redactTags.isEmpty()) return;
126+
127+
Disposable disposable = Observable.fromIterable(redactTags)
128+
.flatMap(tag -> Observable.fromArray(FileMetadataUtils.getTagsFromPref(tag)))
129+
.forEach(tag -> {
130+
Timber.d("Checking for tag: %s", tag);
131+
String oldValue = exifInterface.getAttribute(tag);
132+
if (oldValue != null && !oldValue.isEmpty()) {
133+
Timber.d("Exif tag %s with value %s redacted.", tag, oldValue);
134+
exifInterface.setAttribute(tag, null);
135+
}
136+
});
137+
CompositeDisposable disposables = new CompositeDisposable();
138+
disposables.add(disposable);
139+
disposables.clear();
140+
141+
try {
142+
exifInterface.saveAttributes();
143+
} catch (IOException e) {
144+
Timber.w("EXIF redaction failed: %s", e.toString());
145+
}
146+
}
147+
84148
/**
85149
* Find other images around the same location that were taken within the last 20 sec
86150
* @param similarImageInterface

app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ private UploadItem getUploadItem(UploadableFile uploadableFile,
109109
createdTimestampSource = dateTimeWithSource.getSource();
110110
}
111111
Timber.d("File created date is %d", fileCreatedDate);
112-
GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface);
112+
GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface, context);
113113
return new UploadItem(uploadableFile.getContentUri(), Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, createdTimestampSource);
114114
}
115115

app/src/main/res/values/arrays.xml

+21
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,25 @@
1414
<item>@string/license_pref_cc_by_sa_3_0</item>
1515
<item>@string/license_pref_cc_by_sa_4_0</item>
1616
</array>
17+
18+
<!--TODO add more EXIF tags-->
19+
<array name="pref_exifTag_entries">
20+
<item>@string/exif_tag_name_author</item>
21+
<item>@string/exif_tag_name_copyright</item>
22+
<item>@string/exif_tag_name_location</item>
23+
<item>@string/exif_tag_name_cameraModel</item>
24+
<item>@string/exif_tag_name_lensModel</item>
25+
<item>@string/exif_tag_name_serialNumbers</item>
26+
<item>@string/exif_tag_name_software</item>
27+
</array>
28+
<array name="pref_exifTag_values">
29+
<item>@string/exif_tag_author</item>
30+
<item>@string/exif_tag_copyright</item>
31+
<item>@string/exif_tag_location</item>
32+
<item>@string/exif_tag_cameraModel</item>
33+
<item>@string/exif_tag_lensModel</item>
34+
<item>@string/exif_tag_serialNumbers</item>
35+
<item>@string/exif_tag_software</item>
36+
</array>
37+
1738
</resources>

app/src/main/res/values/keys.xml

+9
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@
55
<string name="license_pref_cc_by_sa_3_0" translatable="false">CC BY-SA 3.0</string>
66
<string name="license_pref_cc_by_4_0" translatable="false">CC BY 4.0</string>
77
<string name="license_pref_cc_by_sa_4_0" translatable="false">CC BY-SA 4.0</string>
8+
9+
<string name="exif_tag_author" translatable="false">Author</string>
10+
<string name="exif_tag_copyright" translatable="false">Copyright</string>
11+
<string name="exif_tag_location" translatable="false">Location</string>
12+
<string name="exif_tag_cameraModel" translatable="false">Camera Model</string>
13+
<string name="exif_tag_lensModel" translatable="false">Lens Model</string>
14+
<string name="exif_tag_serialNumbers" translatable="false">Serial Numbers</string>
15+
<string name="exif_tag_software" translatable="false">Software</string>
16+
817
</resources>

app/src/main/res/values/strings.xml

+13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<string name="preference_category_appearance">Appearance</string>
66
<string name="preference_category_general">General</string>
77
<string name="preference_category_feedback">Feedback</string>
8+
<string name="preference_category_privacy">Privacy</string>
89
<string name="preference_category_location">Location</string>
910
<string name="app_name">Commons</string>
1011
<string name="bullet">&#8226; </string>
@@ -537,6 +538,18 @@ Upload your first media by tapping on the add button.</string>
537538
<string name="welcome_dont_upload_content_description">Examples of images not to upload</string>
538539
<string name="skip_image">SKIP THIS IMAGE</string>
539540
<string name="download_failed_we_cannot_download_the_file_without_storage_permission">Download Failed!!. We cannot download the file without external storage permission.</string>
541+
542+
<string name="manage_exif_tags">Manage EXIF Tags</string>
543+
<string name="manage_exif_tags_summary">Select which EXIF tags to keep in uploads</string>
544+
545+
<string name="exif_tag_name_author">Author</string>
546+
<string name="exif_tag_name_copyright">Copyright</string>
547+
<string name="exif_tag_name_location">Location</string>
548+
<string name="exif_tag_name_cameraModel">Camera Model</string>
549+
<string name="exif_tag_name_lensModel">Lens Model</string>
550+
<string name="exif_tag_name_serialNumbers">Serial Numbers</string>
551+
<string name="exif_tag_name_software">Software</string>
552+
540553
<string name="share_text">Upload photos to Wikimedia Commons on your phone Download the Commons app: %1$s</string>
541554
<string name="share_via">Share app via...</string>
542555
<string name="image_info">Image Info</string>

app/src/main/res/xml/preferences.xml

+12
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@
5959

6060
</fr.free.nrw.commons.ui.LongTitlePreferences.LongTitlePreferenceCategory>
6161

62+
<fr.free.nrw.commons.ui.LongTitlePreferences.LongTitlePreferenceCategory
63+
android:title="@string/preference_category_privacy">
64+
65+
<fr.free.nrw.commons.ui.LongTitlePreferences.LongTitleMultiSelectListPreference
66+
android:entries="@array/pref_exifTag_entries"
67+
android:entryValues="@array/pref_exifTag_values"
68+
android:key="manageExifTags"
69+
android:title="@string/manage_exif_tags"
70+
android:summary="@string/manage_exif_tags_summary"/>
71+
72+
</fr.free.nrw.commons.ui.LongTitlePreferences.LongTitlePreferenceCategory>
73+
6274
<!-- The key 'allowGps' was used before and has since been removed based on the discussion at #1599.
6375
Do not reuse this key unless you revive the same feature with the changes mentioned at #1599.-->
6476

160 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package fr.free.nrw.commons.upload
2+
3+
import androidx.exifinterface.media.ExifInterface.*
4+
import junit.framework.Assert.assertTrue
5+
import org.junit.Test
6+
import java.util.*
7+
8+
/**
9+
* Test cases for FileMetadataUtils
10+
*/
11+
class FileMetadataUtilsTest {
12+
13+
/**
14+
* Test method to verify EXIF tags
15+
*/
16+
@Test
17+
fun getTagsFromPref() {
18+
val author = FileMetadataUtils.getTagsFromPref("Author")
19+
val authorRef = arrayOf(TAG_ARTIST, TAG_CAMARA_OWNER_NAME);
20+
21+
assertTrue(Arrays.deepEquals(author, authorRef))
22+
}
23+
}

0 commit comments

Comments
 (0)