How to get file info from mobile app

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 0

Like

1 comments

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.getFilePathByUri copies it into the cache (CameraLauncher.java:720-725).
    • Terrasoft.File.copy then 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 File and Metadata objects only expose modification time and size; there is no creation-time field. Terrasoft.File has 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 null for 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: true together with a target size, or pick a PNG, Android re-encodes the image and the EXIF is lost. Its EXIF helper copies only DateTime, not DateTimeOriginal.
  • "File system" (multi-select): the stock generator uses captureFiles, and with UseMobileFileMultiSelect on, the Gallery option does too. The success callback then gets [{fileName, path}]. Call readExifDate(f.fileName, f.path, ...) for each item and save once all callbacks have returned.

Saving the date:

  1. Add a Date/Time column, e.g. UsrOriginalFileDate, to [Entity]File. Don't use CreatedOn; that's when the record was created.
  2. Add the column to the detail's query config (and SyncColumns if you use offline mode).
  3. In saveFile, call record.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. Read MediaStore.Images.ImageColumns.DATE_TAKEN, then DocumentsContract.Document.COLUMN_LAST_MODIFIED, then DATE_MODIFIED×1000 from it. Pass the value through processResultFromGallery and add it as a callback argument in camera-provider.js.
  • iOS: in CDVCamera.m (resultForImage/resultForVideo, and the PHPicker path), read PHAsset.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-233 that can crash the app.
Show all comments