Fliplet.Native.Downloads

Fliplet.Native.Downloads

Fliplet.Native.Downloads is the asset-delivery engine that powers over-the-air (OTA) updates inside a Fliplet native app. It queues every file the app needs, hands transfers to the Cordova BackgroundTransfer plugin, persists queue state in a PV (persistent value) so a download can resume after a cold start, applies incremental DataSource diffs without re-downloading, and reports progress, retries, and errors back to the surrounding Fliplet.Native.Updates flow. The queue is keyed by app ID, so a single device can hold partial download state for multiple Fliplet apps at once.

Native wrapper only. Every method on this page requires the Fliplet native runtime (Cordova BackgroundTransfer, cordova.file, and PV are all required). Feature-detect before calling:

if (Fliplet.Env.get('platform') !== 'native') {
  return;
}

What this namespace does

The boot sequence in Fliplet.Native.init() calls Fliplet.Native.Downloads.init() once on app start. From that point the namespace owns three responsibilities: maintaining the per-app download queue in the apps_downloads PV, running the queue with bounded concurrency through BackgroundTransfer, and handing finished bundles to Fliplet.Native.Maintenance for copy/unzip into the app’s data directory. Application code rarely calls anything other than cancelAppDownloads, getPercentageDownloadAsync, and getAppErrorSummary directly — the rest is wired up by Fliplet.Native.Updates.checkForUpdates().

Initialization and queue

init()

Resolves the platform-specific Cordova file paths, initializes the download_manager_config and apps_downloads PVs, and automatically resumes any download that was in flight when the previous session ended. Called once by Fliplet.Native.init(); app code does not need to call it.

Fliplet.Native.Downloads.init().then(function () {
  console.log('Downloads namespace ready');
});

On Android, init also lowers the concurrent-download cap from 8 to 5 (see Concurrency and platform differences).

startDownloads(appId, hasIncrementalUpdates)

Walks the queued asset list for appId and dispatches transfers up to the concurrency cap. Each asset is treated as one of:

  • action: 'download' (or no action) — queued for transfer if it has not already been marked downloaded.
  • action: 'delete' — slated for removal once the rest of the bundle finishes.

If every asset in the queue is a delete and the update is not incremental, the assets are removed and the queue entry is cleared without any network activity. If hasIncrementalUpdates is true and the asset list is empty, the namespace synthesises a “forceComplete” event so the post-download copy and PV-update steps still run — that is how a pure DataSource diff finalizes.

Fliplet.Native.Downloads.startDownloads(12345, false).then(function () {
  console.log('All transfers settled');
});

startDownloads is called automatically from init() on resume and from Fliplet.Native.Updates.checkForUpdates() after the bundle manifest is fetched.

addAppDownload(app, hasIncrementalUpdates)

Adds (or replaces) an app entry in the apps_downloads PV. The app object must contain at least id and assets; pass hasIncrementalUpdates: true to keep the entry even when assets is empty. Returns a Promise that resolves once the PV is persisted.

Fliplet.Native.Downloads.addAppDownload({
  id: 12345,
  version: 42,
  assets: [
    { url: 'https://api.fliplet.com/v1/widgets/.../file.js', path: 'widgets/abc/file.js' }
  ],
  startingPageId: 67890
}, false);

This is normally called by the Updates module; calling it directly is appropriate when you are wiring a custom update path.

Queue inspection

getAppDownloads(appId)

Returns a Promise that resolves with no value when an entry exists for appId in the queue, and rejects with no value when none does. Use it as a presence check rather than a getter — the queue contents are not exposed by this method.

Fliplet.Native.Downloads.getAppDownloads(12345)
  .then(function () { console.log('App 12345 is downloading'); })
  .catch(function () { console.log('App 12345 is not in the queue'); });

getAllAppsDownloads()

Synchronous. Returns the raw queue array from the apps_downloads PV. Each entry is the full app object that was passed to addAppDownload, with each asset annotated with status ('downloaded' once finished) and nrOfDownloads (count of completed assets).

var queue = Fliplet.Native.Downloads.getAllAppsDownloads();
queue.forEach(function (app) {
  console.log(app.id, app.nrOfDownloads, '/', app.assets.length);
});

setAppsDownloads(apps)

Synchronous. Overwrites the entire queue with apps and persists the PV. Used internally to restore state; calling it directly is a power-user escape hatch — incorrect input will desynchronise the queue from the BackgroundTransfer state.

cancelAppDownloads(appId)

Cancels every in-flight BackgroundTransfer promise tied to appId, removes the queue entry, and resets the internal counters (filesCurrentlyDownloading, downloadQueue, legacyDownload, progress). Returns a Promise that resolves once the cancellation has propagated.

Fliplet.Native.Downloads.cancelAppDownloads(12345).then(function () {
  console.log('Downloads canceled');
});

This is the method to call from a “Cancel update” button in your UI.

Progress

getPercentageDownload(appId)

Synchronous. Returns a number 0–99 representing the current progress for appId:

  • If appId is not in the queue but is already a local app, returns 100.
  • If appId is not in the queue and not installed locally, returns -1 (the DOWNLOAD_CANCEL sentinel).
  • For legacy (V1) zip-based downloads, returns the byte-level progress variable updated by the BackgroundTransfer progress callback.
  • For V2+ asset-based downloads, returns nrOfDownloads / assetsToDownload.length * 99. The cap at 99 reserves the last percent for the post-download copy/unzip step.
var pct = Fliplet.Native.Downloads.getPercentageDownload(12345);
$('.progress-bar').css('width', pct + '%');

getPercentageDownloadAsync(appId)

Returns a Promise that resolves with the same value as getPercentageDownload(appId) unless the app has accumulated severe download errors — in which case it resolves with -1 (the DOWNLOAD_CANCEL sentinel) so the UI can switch to an error state. Use this in any code path where the UI should react to error conditions; use the sync variant only for tight render loops.

Fliplet.Native.Downloads.getPercentageDownloadAsync(12345).then(function (pct) {
  if (pct === -1) {
    showErrorState();
    return;
  }
  updateProgressBar(pct);
});

getNrOfDownloads(appId)

Synchronous. Returns the number of assets queued for download (excluding action: 'delete' entries) for appId, or 0 if the app is not in the queue.

var total = Fliplet.Native.Downloads.getNrOfDownloads(12345);
console.log('Downloading ' + total + ' files');

Single file operations

These three methods are independent of the per-app queue and operate on the Cordova filesystem directly. They are the right primitives for screens that need to fetch or read a single asset outside the OTA update pipeline.

downloadFile(options)

Downloads a single file via BackgroundTransfer.BackgroundDownloader and writes it into the app’s downloads folder (or a subfolder of it). Returns a Promise that resolves with the BackgroundTransfer result object once the file is on disk.

Option Type Description
fileName string Local file name to create.
fileUrl string Remote URL to download from.
dir string Optional. Subfolder under the downloads folder. Created if absent.
onProgress function Optional. Called with the BackgroundTransfer progress object on every update.
onStart function Optional. Called with the download Promise as soon as the transfer starts. Defining this opts the call out of the namespace’s internal completion queue — the caller owns the promise.
Fliplet.Native.Downloads.downloadFile({
  fileName: 'manual.pdf',
  fileUrl: 'https://example.com/manual.pdf',
  dir: 'docs',
  onProgress: function (event) {
    console.log(event.bytesReceived, '/', event.totalBytesToReceive);
  }
}).then(function (result) {
  console.log('Saved to', result.fileData.savedFilePath);
});

getFile(options)

Reads a previously downloaded file from the app’s downloads folder. Returns a Promise that resolves with the Cordova FileEntry.

Option Type Description
fileName string Local file name to read.
dir string Optional. Subfolder under the downloads folder.
Fliplet.Native.Downloads.getFile({ fileName: 'manual.pdf', dir: 'docs' })
  .then(function (fileEntry) {
    console.log(fileEntry.toURL());
  });

getTempDownloadPath()

Synchronous. Returns the full string path of the temporary download directory (<downloadsFolder>/appTempFiles/). Use it when you need to construct a path for Fliplet.Native.Maintenance calls that operate on in-flight downloads.

var tempPath = Fliplet.Native.Downloads.getTempDownloadPath();

Error handling

The namespace records every failed transfer into a per-app summary, persisted in Fliplet.Storage under the downloadsErrorSummary key. Two methods expose it:

getErrorMessage(appId)

Returns a Promise that resolves with the first error entry recorded for appId (or undefined if no errors have been recorded). Each entry is the raw BackgroundTransfer error object — error.fileData.savedFilePath identifies the file that failed.

Fliplet.Native.Downloads.getErrorMessage(12345).then(function (error) {
  if (error) {
    console.error('First failure:', error.fileData.savedFilePath);
  }
});

getAppErrorSummary(appId)

Returns a Promise that resolves with the full summary object { appId, summary: [error, error, ...] } or undefined if nothing has been recorded.

Fliplet.Native.Downloads.getAppErrorSummary(12345).then(function (errorSummary) {
  if (errorSummary) {
    console.log('Total failures:', errorSummary.summary.length);
    errorSummary.summary.forEach(function (e) {
      console.error(e.fileData.savedFilePath, e.fileData.reason);
    });
  }
});

Non-severe error policy

Not every failed file aborts the update. The namespace classifies an error as non-severe when every entry in the summary fails on one of:

  • A path containing entries.json (bundled DataSource — falls back to online).
  • A common image extension (png, jpg, jpeg, gif, svg, webp, and many others — see COMMON_IMG_EXT in the source).

If the summary is non-severe, getPercentageDownloadAsync still reports real progress and the update completes successfully. If it contains anything else (a missing JS file, a missing HTML page, etc.), the update is treated as failed and getPercentageDownloadAsync resolves with -1.

Retry policy

When BackgroundTransfer reports a failure, the namespace retries the same file up to 4 times for these reason codes:

Reason Meaning
502 No reason given
1000 ERROR_UNKNOWN
1004 ERROR_HTTP_DATA_ERROR
1008 ERROR_CANNOT_RESUME
-1001 Request timed out
-1003 Server hostname not found
-1005 Network connection lost
-1200 SSL handshake error

404 is treated as “system will deal with it” (no retry, classified by the non-severe filter above). 1006 (ERROR_INSUFFICIENT_SPACE) immediately rejects with a “storage full” message — no retry, no recovery. Any other reason code skips retry and is recorded in the error summary.

Concurrency and platform differences

Platform Concurrent downloads Downloads folder Local data folder
iOS 8 cordova.file.dataDirectory cordova.file.dataDirectory
Android 5 cordova.file.externalDataDirectory cordova.file.applicationStorageDirectory + 'files/'
Windows 8 cordova.file.dataDirectory cordova.file.dataDirectory

Android caps concurrency lower because the platform throttles parallel BackgroundTransfer operations more aggressively and excessive parallelism causes spurious failures. The cap is read once at init() time.

The temp download path is always <downloadsFolder>/appTempFiles/<appId>/. Files land there first, then Fliplet.Native.Maintenance copies (or unzips, for legacy bundles) them into the per-app folder under the local data folder.

Hooks

The namespace fires two Fliplet.Hooks events on completion. Register handlers via Fliplet.Hooks.on() from anywhere in your app code (global JS is the natural place).

updateCompleted

Fired once every queued asset for an app has been downloaded, copied, and the queue entry has been cleared.

Fliplet.Hooks.on('updateCompleted', function (data) {
  console.log('Update finished for app', data.appId);
});

updateFailed

Fired when the download flow rejects with a severe error.

Fliplet.Hooks.on('updateFailed', function (data) {
  console.error('Update failed for app', data.appId, data.error);
});

Both hooks also trigger a Fliplet.App.Logs entry (update.success or update.failed) so the failure is visible in Studio’s app activity log without any extra wiring.

A DOM-level event is also dispatched on document: flDownloadFinished<appId> (no payload) on success, and flEventNativeDownloadFailed (with event.detail.appId) on each failed file. These are lower-level than the hooks and are used by the built-in update overlay UI; new code should prefer the hooks.

See also