diff --git a/app/build.gradle b/app/build.gradle index 5f0165f7f..fd7f670b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,7 +27,8 @@ dependencies { implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1' implementation 'com.facebook.fresco:fresco:1.10.0' - + implementation 'com.drewnoakes:metadata-extractor:2.11.0' + // UI implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' implementation 'com.github.chrisbanes:PhotoView:2.0.0' diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 9b539c9f7..2d67f6d01 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -3,25 +3,33 @@ package fr.free.nrw.commons.contributions; import android.content.Context; import android.net.Uri; import android.os.Parcel; -import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.StringDef; import java.lang.annotation.Retention; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.Locale; +import java.util.TimeZone; -import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.utils.ConfigUtils; +import fr.free.nrw.commons.utils.StringUtils; import static java.lang.annotation.RetentionPolicy.SOURCE; public class Contribution extends Media { + //{{According to EXIF data|2009-01-09}} + private static final String TEMPLATE_DATE_ACC_TO_EXIF = "|date={{According to EXIF data|%s}}"; + + //{{date|2009|1|9}} → 9 January 2009 + private static final String TEMPLATE_DATA_OTHER_SOURCE = "{{date|%d|%d|%d}}"; + public static Creator CREATOR = new Creator() { @Override public Contribution createFromParcel(Parcel parcel) { @@ -57,6 +65,7 @@ public class Contribution extends Media { private boolean isMultiple; private String wikiDataEntityId; private Uri contentProviderUri; + private String dateCreatedSource; public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated, int state, long dataLength, Date dateUploaded, long transferred, @@ -71,6 +80,7 @@ public class Contribution extends Media { this.width = width; this.height = height; this.license = license; + this.dateCreatedSource = ""; } public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength, @@ -78,6 +88,7 @@ public class Contribution extends Media { super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator); this.decimalCoords = decimalCoords; this.editSummary = editSummary; + this.dateCreatedSource = ""; } public Contribution(Parcel in) { @@ -99,7 +110,13 @@ public class Contribution extends Media { parcel.writeInt(isMultiple ? 1 : 0); } + public String getDateCreatedSource() { + return dateCreatedSource; + } + public void setDateCreatedSource(String dateCreatedSource) { + this.dateCreatedSource = dateCreatedSource; + } public boolean getMultiple() { return isMultiple; @@ -143,20 +160,19 @@ public class Contribution extends Media { public String getPageContents(Context applicationContext) { StringBuilder buffer = new StringBuilder(); - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - buffer .append("== {{int:filedesc}} ==\n") .append("{{Information\n") .append("|description=").append(getDescription()).append("\n") .append("|source=").append("{{own}}\n") .append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n"); - if (dateCreated != null) { - buffer - .append("|date={{According to EXIF data|").append(isoFormat.format(dateCreated)).append("}}\n"); + + String templatizedCreatedDate = getTemplatizedCreatedDate(); + if (!StringUtils.isNullOrWhiteSpace(templatizedCreatedDate)) { + buffer.append("|date=").append(templatizedCreatedDate); } - buffer - .append("}}").append("\n"); + + buffer.append("}}").append("\n"); //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null if (decimalCoords != null) { @@ -178,6 +194,28 @@ public class Contribution extends Media { return buffer.toString(); } + /** + * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE + * @return + */ + private String getTemplatizedCreatedDate() { + if (dateCreated != null) { + if (UploadableFile.DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource)) { + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); + return String.format(Locale.ENGLISH, TEMPLATE_DATE_ACC_TO_EXIF, isoFormat.format(dateCreated)) + "\n"; + } else { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(dateCreated); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + return String.format(Locale.ENGLISH, TEMPLATE_DATA_OTHER_SOURCE, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH)) + "\n"; + } + } + return ""; + } + @Override public void setFilename(String filename) { this.filename = filename; diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java index 1eb46194e..fb4d9ac1a 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java @@ -6,7 +6,16 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.exif.ExifSubIFDDirectory; + import java.io.File; +import java.io.IOException; +import java.util.Date; + +import javax.annotation.Nullable; import fr.free.nrw.commons.upload.FileUtils; @@ -62,16 +71,32 @@ public class UploadableFile implements Parcelable { return 0; } + + /** + * First try to get the file creation date from EXIF else fall back to CP + * @param context + * @return + */ + @Nullable + public DateTimeWithSource getFileCreatedDate(Context context) { + DateTimeWithSource dateTimeFromExif = getDateTimeFromExif(); + if (dateTimeFromExif == null) { + return getFileCreatedDateFromCP(context); + } else { + return dateTimeFromExif; + } + } + /** * Get filePath creation date from uri from all possible content providers * * @return */ - public long getFileCreatedDate(Context context) { + private DateTimeWithSource getFileCreatedDateFromCP(Context context) { try { Cursor cursor = context.getContentResolver().query(contentUri, null, null, null, null); if (cursor == null) { - return -1;//Could not fetch last_modified + return null;//Could not fetch last_modified } //Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app @@ -80,18 +105,69 @@ public class UploadableFile implements Parcelable { } //If both the content providers do not give the data, lets leave it to Jesus if (lastModifiedColumnIndex == -1) { - return -1l; + return null; } cursor.moveToFirst(); - return cursor.getLong(lastModifiedColumnIndex); + return new DateTimeWithSource(cursor.getLong(lastModifiedColumnIndex), DateTimeWithSource.CP_SOURCE); } catch (Exception e) { - return -1;////Could not fetch last_modified + return null;////Could not fetch last_modified } } + /** + * Get filePath creation date from uri from EXIF + * + * @return + */ + private DateTimeWithSource getDateTimeFromExif() { + Metadata metadata; + try { + metadata = ImageMetadataReader.readMetadata(file); + ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); + if (directory!=null && directory.containsTag(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) { + Date date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL); + return new DateTimeWithSource(date, DateTimeWithSource.EXIF_SOURCE); + } + } catch (ImageProcessingException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeParcelable(contentUri, 0); parcel.writeSerializable(file); } + + /** + * This class contains the epochDate along with the source from which it was extracted + */ + public class DateTimeWithSource { + public static final String CP_SOURCE = "contentProvider"; + public static final String EXIF_SOURCE = "exif"; + + private final long epochDate; + private final String source; + + public DateTimeWithSource(long epochDate, String source) { + this.epochDate = epochDate; + this.source = source; + } + + public DateTimeWithSource(Date date, String source) { + this.epochDate = date.getTime(); + this.source = source; + } + + public long getEpochDate() { + return epochDate; + } + + public String getSource() { + return source; + } + } } 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 e9a4ae2df..f90708c65 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 @@ -43,7 +43,7 @@ public class UploadModel { "", GPSExtractor.DUMMY, null, - -1L) { + -1L, "") { }; private final BasicKvStore basicKvStore; private final List licenses; @@ -99,10 +99,16 @@ public class UploadModel { String source, SimilarImageInterface similarImageInterface) { fileProcessor.initFileDetails(Objects.requireNonNull(uploadableFile.getFilePath()), context.getContentResolver()); - long fileCreatedDate = uploadableFile.getFileCreatedDate(context); + UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile.getFileCreatedDate(context); + long fileCreatedDate = -1; + String createdTimestampSource = ""; + if (dateTimeWithSource != null) { + fileCreatedDate = dateTimeWithSource.getEpochDate(); + createdTimestampSource = dateTimeWithSource.getSource(); + } Timber.d("File created date is %d", fileCreatedDate); GPSExtractor gpsExtractor = fileProcessor.processFileCoordinates(similarImageInterface); - return new UploadItem(Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate); + return new UploadItem(Uri.parse(uploadableFile.getFilePath()), uploadableFile.getMimeType(context), source, gpsExtractor, place, fileCreatedDate, createdTimestampSource); } void onItemsProcessed(Place place, List uploadItems) { @@ -284,11 +290,13 @@ public class UploadModel { contribution.setTag("mimeType", item.mimeType); contribution.setSource(item.source); contribution.setContentProviderUri(item.mediaUri); + Timber.d("Created timestamp while building contribution is %s, %s", item.getCreatedTimestamp(), new Date(item.getCreatedTimestamp())); if (item.createdTimestamp != -1L) { contribution.setDateCreated(new Date(item.getCreatedTimestamp())); + contribution.setDateCreatedSource(item.getCreatedTimestampSource()); //Set the date only if you have it, else the upload service is gonna try it the other way } return contribution; @@ -332,10 +340,15 @@ public class UploadModel { private boolean visited; private boolean error; private long createdTimestamp; + private String createdTimestampSource; private BehaviorSubject imageQuality; @SuppressLint("CheckResult") - UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, @Nullable Place place, long createdTimestamp) { + UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, + @Nullable Place place, + long createdTimestamp, + String createdTimestampSource) { + this.createdTimestampSource = createdTimestampSource; title = new Title(); descriptions = new ArrayList<>(); descriptions.add(new Description()); @@ -348,6 +361,10 @@ public class UploadModel { imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); } + public String getCreatedTimestampSource() { + return createdTimestampSource; + } + public String getMimeType() { return mimeType; }