How to get file creation date in mobile application when downloading it from a gallery or from a file system?
Here is my code:
onFileTypePickerItemTap: function(el, index, target, record) {
var source = record.get("Source");
switch (source) {
case Terrasoft.Configuration.FilesAndLinksDetailFileSources.Gallery:
Terrasoft.Camera.captureFromGallery({
success: this.onCameraCapture,
failure: this.onFail,
scope: this
});
break;
...
}
}
},
onCameraCapture: function(fileName, url) {
Terrasoft.Mask.show({message: Terrasoft.LocalizableStrings.UploadingFile});
if(fileName && url) {
this.saveFile(fileName, url);
} else {
Terrasoft.Mask.hide();
}
}
Like
You can't get the file's real creation date through Terrasoft.Camera. The url your onCameraCapture receives points to a copy the app made in its Downloads folder, and neither platform keeps the original timestamps when copying.
Why:
- Android: the picker returns a
content://link, but that link never reaches JavaScript. The file is copied twice with plain byte copies, and neither copy keeps the original timestamps.
CameraLauncher.getFilePathByUricopies it into the cache (CameraLauncher.java:720-725).Terrasoft.File.copythen copies it into Downloads (LocalFilesystem.java:274-275).
- iOS: the photo's bytes are written into a new file in Downloads, so it also gets a new timestamp.
- JS file API: Cordova's
FileandMetadataobjects only expose modification time and size; there is no creation-time field.Terrasoft.Filehas no metadata method either.
So lastModified on url is simply the moment the user picked the file.
What you can get without changing the app: the EXIF "date taken"
In your call path the copies are byte-for-byte, so a photo's EXIF data survives. You can read DateTimeOriginal (when the photo was taken) from the copy in JS. This held on Android when checked against the source; on iOS it only partly holds (limits below). No EXIF library ships with the mobile web layer, so the code includes a small parser.
onCameraCapture: function(fileName, url) {
if (!fileName || !url) { // cancel: success() is called with no args
Terrasoft.Mask.hide();
return;
}
Terrasoft.Mask.show({message: Terrasoft.LocalizableStrings.UploadingFile});
this.readExifDate(fileName, url, function(originalDate) {
this.saveFile(fileName, url, originalDate); // Date or null
}, this);
},
// Calls back exactly once with a Date (EXIF, device-local time) or null.
readExifDate: function(fileName, url, callback, scope) {
var me = this, called = false;
var done = function(date) {
if (!called) { called = true; Ext.callback(callback, scope, [date || null]); }
};
var onError = function() { done(null); };
var onFile = function(file) { // file is a Terrasoft.File wrapper
try {
file.getFileSystemEntry().file(function(cordovaFile) {
var reader = new FileReader();
reader.onloadend = function(e) {
var date = null;
try { date = e.target.result ? me.parseJpegExifDate(e.target.result) : null; } catch (ex) {}
done(date);
};
reader.readAsArrayBuffer(cordovaFile.slice(0, 131072)); // EXIF sits at the start; avoids loading videos
}, onError);
} catch (ex) { done(null); }
};
var byUrl = function() {
Terrasoft.File.resolveLocalFileSystemURI({uri: url, success: onFile, failure: onError});
};
if (Terrasoft.Platform.isAndroid) {
// The Android copy is always Downloads/<fileName>; opening by name avoids re-encoding issues with '#'/'?'
Terrasoft.File.open({name: Terrasoft.util.getDownloadPath(fileName), success: onFile, failure: byUrl});
} else {
byUrl();
}
},
parseJpegExifDate: function(buffer) {
var view = new DataView(buffer);
if (view.byteLength < 4 || view.getUint16(0) !== 0xFFD8) { return null; } // not a JPEG
var offset = 2;
while (offset + 4 <= view.byteLength) {
var marker = view.getUint16(offset);
if ((marker & 0xFF00) !== 0xFF00 || marker === 0xFFDA) { return null; }
if (marker === 0xFFE1 && view.getUint32(offset + 4) === 0x45786966) { // APP1 "Exif"
return this.readTiffDate(view, offset + 10);
}
offset += 2 + view.getUint16(offset + 2);
}
return null;
},
readTiffDate: function(view, tiff) {
var little = view.getUint16(tiff) === 0x4949;
var u16 = function(o) { return view.getUint16(o, little); };
var u32 = function(o) { return view.getUint32(o, little); };
if (u16(tiff + 2) !== 42) { return null; }
var findTag = function(ifd, tag) {
for (var i = 0, n = u16(ifd); i < n; i++) {
if (u16(ifd + 2 + i * 12) === tag) { return ifd + 2 + i * 12; }
}
return -1;
};
var toDate = function(entry) {
if (entry === -1 || u32(entry + 4) < 19) { return null; }
var start = tiff + u32(entry + 8), s = "";
for (var i = 0; i < 19; i++) { s += String.fromCharCode(view.getUint8(start + i)); }
var m = /^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(s);
if (!m || +m[1] < 1900) { return null; } // "0000:00:00 ..." etc.
return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
};
var ifd0 = tiff + u32(tiff + 4), date = null;
var exifPtr = findTag(ifd0, 0x8769);
if (exifPtr !== -1) {
var exifIfd = tiff + u32(exifPtr + 8);
date = toDate(findTag(exifIfd, 0x9003)) || toDate(findTag(exifIfd, 0x9004)); // DateTimeOriginal, Digitized
}
return date || toDate(findTag(ifd0, 0x0132)); // DateTime (last edit), weakest
}Limits:
- JPEG only: you get
nullfor HEIC (the default iPhone camera format), most PNG screenshots, videos, PDFs, and images whose EXIF was stripped (for example, received through messengers). - Android re-encoding: if you pass
isImageSource: truetogether with a target size, or pick a PNG, Android re-encodes the image and the EXIF is lost. Its EXIF helper copies onlyDateTime, notDateTimeOriginal. - "File system" (multi-select): the stock generator uses
captureFiles, and withUseMobileFileMultiSelecton, the Gallery option does too. The success callback then gets[{fileName, path}]. CallreadExifDate(f.fileName, f.path, ...)for each item and save once all callbacks have returned.
Saving the date:
- Add a Date/Time column, e.g.
UsrOriginalFileDate, to[Entity]File. Don't useCreatedOn; that's when the record was created. - Add the column to the detail's query config (and
SyncColumnsif you use offline mode). - In
saveFile, callrecord.set("UsrOriginalFileDate", date, true)before saving.
If you need a real file date: change the mobile app
This can only be done in engineering/mobile-app, not in configuration:
- Android:
CameraLauncher.getFileName/getFilePathByUri(CameraLauncher.java:690-725) already opens a cursor on the picked file. ReadMediaStore.Images.ImageColumns.DATE_TAKEN, thenDocumentsContract.Document.COLUMN_LAST_MODIFIED, thenDATE_MODIFIED×1000 from it. Pass the value throughprocessResultFromGalleryand add it as a callback argument incamera-provider.js. - iOS: in
CDVCamera.m(resultForImage/resultForVideo, and the PHPicker path), readPHAsset.creationDate. - Even then, Android gives you a "taken" or "modified" time, not a true creation time. It also depends on which app handled the picker.
There is also an iOS-only workaround for single photos: read the Photos asset date from the original assets-library:// link before the copy. I don't recommend it:
- It relies on private Terrasoft members.
- It relies on
ALAssetsLibrary, deprecated since iOS 9. - It needs guards against an unchecked nil in
CDVAssetLibraryFilesystem.m:231-233that can crash the app.