Create Free APK

JavaScript bridge API

Files: opened with this app

Collecting what another app handed over through "Open with", in chunks the page pulls.

AppMint.getOpenedFile helper#

AppMint.getOpenedFile()

Example

Gives your page the file that was shared to the app, or opened with it ("Open with…"), however late your page starts.

Returns: Promise<object | null> - null when the app was opened normally. The object has token, name, mimeType, size, kind, url, file, getFile(), plus text (text files up to 32 MB) or base64 (other data files up to 8 MB) when cheap. kind is 'image' | 'video' | 'audio' | 'document' | 'data'. Needs: turn on Receive Shares From Other Apps when you build and tick the kinds of files your app accepts.

async function handleStartFile() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;  // not in the app, or feature off
  const f = await AppMint.getOpenedFile();
  if (!f) return;                                  // opened normally
  console.log(f.name, f.mimeType, f.size, f.kind);
  if (f.kind === 'video') {
    document.querySelector('video').src = f.url;   // streams from disk, can seek
  } else if (f.kind === 'image') {
    document.querySelector('img').src = f.url;
  } else if (f.kind === 'document') {
    const file = await f.getFile();                // a real File
    await upload(file);
  } else {
    const text = typeof f.text === 'string' ? f.text : await f.file.text();
    importData(text);
  }
}
handleStartFile();

Or listen - the event (and the optional window.onAppMintFileOpen hook) fires once per new file, with the same object as detail:

window.onAppMintFileOpen = function (f) { console.log('hook:', f.name); };

window.addEventListener('appmint:fileopen', function (e) {
  const f = e.detail;             // {token, name, mimeType, size, kind, url, file, getFile, text?, base64?}
  showFile(f);
});
window.addEventListener('appmint:fileopenerror', function (e) {
  alert('Could not read ' + e.detail.name + ': ' + e.detail.error);   // detail: {name, error}
});

In a React app, a small hook picks up the file however late the component mounts, and keeps following new ones:

import { useEffect, useState } from 'react';

export function useOpenedFile() {
  const [file, setFile] = useState(null);
  useEffect(function () {
    const A = window.AppMint;
    if (!A || typeof A.getOpenedFile !== 'function') return;
    let alive = true;
    A.getOpenedFile().then(function (f) { if (alive && f) setFile(f); });
    function onOpen(e) { setFile(e.detail); }
    window.addEventListener('appmint:fileopen', onOpen);
    return function () { alive = false; window.removeEventListener('appmint:fileopen', onOpen); };
  }, []);
  return file;
}

Notes: Images, videos, audio and any file over 64 MB come only as url (file is null); call f.getFile() when you really need the bytes. Data files and documents also come as f.file. The file is kept for your page until you ask, so an SPA that mounts late still gets it; the latest one is returned each time. When the app is already open and another file arrives, the event fires again. Call AppMint.releaseOpenedFile(f) when done to free the cached copy.

AppMint.getOpenedFiles helper#

AppMint.getOpenedFiles()

Example

Gives your page every file shared to, or opened with, the app during this session - useful when the user shares several photos at once.

Returns: Promise<Array> of the same objects AppMint.getOpenedFile() returns (token, name, mimeType, size, kind, url, file, getFile(), and text / base64 when cheap). An empty array when nothing was received. Needs: turn on Receive Shares From Other Apps when you build and tick the kinds of files your app accepts.

async function importShared() {
  if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
  const files = await AppMint.getOpenedFiles();
  const gallery = document.getElementById('gallery');
  files.forEach(function (f) {
    if (f.kind === 'image') {
      const img = document.createElement('img');
      img.src = f.url;
      img.alt = f.name;
      gallery.appendChild(img);
    } else {
      console.log('received', f.name, f.size + ' bytes');
    }
  });
}
importShared();

New files that arrive while the page is open also fire appmint:fileopen, one event per file:

window.addEventListener('appmint:fileopen', function (e) { addToGallery(e.detail); });

Uploading everything the user shared in one request, as real files:

async function uploadShared() {
  if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
  const files = await AppMint.getOpenedFiles();
  if (!files.length) return;
  const form = new FormData();
  for (const f of files) {
    try {
      form.append('files', await f.getFile(), f.name);   // url-only media is fetched here
    } catch (e) {
      console.warn('skipped', f.name, e.message);        // released earlier
    }
  }
  await fetch('/api/upload', { method: 'POST', body: form });
}

Notes: The list is a copy; it grows as more files arrive. Files you have released with AppMint.releaseOpenedFile(f) stay in the list, but their url no longer works.

AppMint.releaseOpenedFile helper#

AppMint.releaseOpenedFile(file)

Example

Deletes the app's cached copy of a file that was shared to or opened with the app, once your page is done with it.

Returns: nothing. Needs: turn on Receive Shares From Other Apps when you build.

async function importAndForget() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const file = await f.getFile();        // take your own copy of the bytes first
  await saveToMyDatabase(file);
  AppMint.releaseOpenedFile(f);          // then free the cache on disk
}

A shared video plays from f.url, so release it only when playback is over:

async function playShared() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f || f.kind !== 'video') return;
  const video = document.querySelector('video');
  video.src = f.url;
  video.play();
  video.addEventListener('ended', function () {
    video.removeAttribute('src');
    video.load();
    AppMint.releaseOpenedFile(f);        // the url stops working from here
  }, { once: true });
}

After importing a batch of shared photos, free every cached copy at once:

async function importAllPhotos() {
  if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
  const files = await AppMint.getOpenedFiles();
  for (const f of files) {
    if (f.kind !== 'image') continue;
    const file = await f.getFile();      // fetched from f.url
    await savePhoto(file);
    AppMint.releaseOpenedFile(f);
  }
}

Notes: Pass the object you got (it needs its token). A File you already pulled stays valid; the url stops working, and getFile() rejects unless it had already fetched the File. Do not release a video you are still playing from f.url. The app also clears these copies when its screen is closed, so releasing early is about disk space during a long session.

openedFileChunk bridge#

window.WebToApk.openedFileChunk(token: String, offset: String, len: Int): String

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

Described by its group, Files shared to, or opened with, the app, rather than on its own.

Example

Internal plumbing behind AppMint.getOpenedFile(): returns one slice of a shared or opened file's bytes, which the helper joins into a File. Pages should use the public API - f.file, f.getFile() or f.url.

Returns: (sync) a base64 string of up to len bytes starting at offset (the offset is passed as a string, so files over 2 GB still work); "" at the end of the file, for an unknown or released token, or on a read error. Needs: turn on Receive Shares From Other Apps when you build.

The public API (use this):

if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
  AppMint.getOpenedFile().then(function (f) {
    if (!f) return;
    return f.getFile().then(function (file) { console.log('got', file.size, 'bytes'); });
  });
}

What the helper does with it, shown only to explain the arguments:

if (window.WebToApk && typeof WebToApk.openedFileChunk === 'function') {
  const meta = JSON.parse(WebToApk.openedFilesPending() || '[]')[0];
  if (meta) {
    const b64 = WebToApk.openedFileChunk(meta.token, '0', WebToApk.openedFileMaxChunk());
    console.log('first slice is', atob(b64).length, 'bytes of', meta.size);
  }
}

Reading a very large shared file without slices - stream f.url and show progress (here counting CSV rows):

async function countRows() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const res = await fetch(f.url);                // served from disk, no base64 round trips
  if (!res.ok) throw new Error('shared file unavailable (' + res.status + ')');
  const reader = res.body.getReader();
  let seen = 0, rows = 0;
  for (;;) {
    const step = await reader.read();
    if (step.done) break;
    seen += step.value.length;
    for (let i = 0; i < step.value.length; i++) if (step.value[i] === 10) rows++;
    showProgress(seen / f.size);
  }
  console.log(rows + ' rows in ' + f.name);
}

Notes: len is clamped to 1 byte … 4 MB. For media files, streaming f.url is far cheaper than pulling slices.

openedFileCollected bridge#

window.WebToApk.openedFileCollected(token: String)

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

Described by its group, Files shared to, or opened with, the app, rather than on its own.

Example

Internal plumbing behind AppMint.getOpenedFile(): marks a shared or opened file as collected, so it is no longer offered as "new" but its cached copy is kept. Pages should use the public API; there is no reason to call this directly.

Returns: nothing. Needs: turn on Receive Shares From Other Apps when you build.

The public API (use this) - each file is collected once and handed to your page:

if (window.AppMint && typeof AppMint.getOpenedFiles === 'function') {
  AppMint.getOpenedFiles().then(function (files) {
    files.forEach(function (f) { console.log(f.name, f.kind); });
  });
}
window.addEventListener('appmint:fileopen', function (e) { console.log('new file', e.detail.name); });

A file collected at startup is both in the list and announced by the event, so key your own handling by token to process each file once:

const handled = new Set();
function handleOnce(f) {
  if (handled.has(f.token)) return;
  handled.add(f.token);
  addToInbox(f);
}
window.addEventListener('appmint:fileopen', function (e) { handleOnce(e.detail); });
if (window.AppMint && typeof AppMint.getOpenedFiles === 'function') {
  AppMint.getOpenedFiles().then(function (files) { files.forEach(handleOnce); });
}

A file that could not be read is still collected; it is reported once through appmint:fileopenerror instead of appmint:fileopen:

window.addEventListener('appmint:fileopenerror', function (e) {
  const d = e.detail;             // {name, error}
  showBanner('Could not open "' + d.name + '". Try sharing it again.');
  console.warn('opened file failed:', d.error);
});

Notes: Calling it yourself would hide a file from AppMint.getOpenedFile() and the appmint:fileopen event without giving it to anyone. To free the cached copy, use AppMint.releaseOpenedFile(f).

openedFileMaxChunk bridge#

window.WebToApk.openedFileMaxChunk(): Int

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

Described by its group, Files shared to, or opened with, the app, rather than on its own.

Example

Internal plumbing behind AppMint.getOpenedFile(): the largest slice openedFileChunk returns in one call (4 MB). Pages should use the public API, which already reads in slices of this size.

Returns: (sync) a number of bytes (4194304). Needs: turn on Receive Shares From Other Apps when you build.

The public API (use this):

if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
  AppMint.getOpenedFile().then(function (f) {
    if (f && f.file) console.log('pulled in 4 MB slices:', f.file.size, 'bytes');
  });
}

The raw call, if you only want to know the number:

if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
  console.log('slice size', WebToApk.openedFileMaxChunk());
}

Estimating how many bridge calls the helper will make for each waiting file, when profiling a slow import:

if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
  const slice = WebToApk.openedFileMaxChunk();
  JSON.parse(WebToApk.openedFilesPending() || '[]').forEach(function (m) {
    console.log(m.name, (m.size / 1048576).toFixed(1) + ' MB =', Math.ceil(m.size / slice), 'slice(s)');
  });
}

Notes: Same cap as getMaxIoChunkSize() for the file-access bridge.

openedFileRelease bridge#

window.WebToApk.openedFileRelease(token: String)

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

Described by its group, Files shared to, or opened with, the app, rather than on its own.

Example

Internal plumbing behind AppMint.releaseOpenedFile(f): deletes the cached copy of a shared or opened file, by token. Pages should use the public API.

Returns: nothing. Needs: turn on Receive Shares From Other Apps when you build.

The public API (use this):

async function useAndRelease() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const file = await f.getFile();       // keep your own copy first
  await store(file);
  AppMint.releaseOpenedFile(f);         // calls WebToApk.openedFileRelease(f.token)
}

The user dismisses a shared file without importing it - free the cached copy straight away:

async function offerImport() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const box = showImportPrompt(f.name, f.size);   // your own "Import / Discard" UI
  box.querySelector('.discard').addEventListener('click', function () {
    AppMint.releaseOpenedFile(f);
    box.remove();
  });
  box.querySelector('.import').addEventListener('click', async function () {
    await store(await f.getFile());
    AppMint.releaseOpenedFile(f);
    box.remove();
  });
}

A file the app cannot use is released as soon as it is refused:

window.addEventListener('appmint:fileopen', function (e) {
  const f = e.detail;
  if (!/\.(gpx|tcx)$/i.test(f.name)) {
    alert(f.name + ' is not a GPX or TCX activity file.');
    AppMint.releaseOpenedFile(f);
    return;
  }
  importActivity(f.text);                // gpx/tcx arrive as text
});

Notes: After release, f.url returns an error and openedFileChunk returns "" for that token. A File object you already have is not affected.

openedFilesPending bridge#

window.WebToApk.openedFilesPending(): String

A foreign page gets nothing: the files stay parked for the app's own page.

Example

Internal plumbing behind AppMint.getOpenedFile() / AppMint.getOpenedFiles(): lists the shared or opened files the page has not collected yet (metadata only, no bytes). Pages should use the public API.

Returns: (sync) a JSON string: an array of {token, name, mimeType, size, isText, kind, url}. [] when nothing is waiting. Needs: turn on Receive Shares From Other Apps when you build (without it the list is always empty and the AppMint helpers are not installed).

The public API (use this):

if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
  AppMint.getOpenedFile().then(function (f) {
    if (f) console.log('opened with the app:', f.name, f.kind);
  });
}
window.addEventListener('appmint:fileopen', function (e) { console.log('arrived:', e.detail.name); });

Only for debugging - peek at what is waiting without collecting it:

if (window.WebToApk && typeof WebToApk.openedFilesPending === 'function') {
  const waiting = JSON.parse(WebToApk.openedFilesPending() || '[]');
  console.log(waiting.length + ' file(s) waiting', waiting.map(function (m) { return m.name; }));
}

Debugging a file that never reached the page - see what is still waiting and whether it will arrive as f.file or only as f.url:

if (window.WebToApk && typeof WebToApk.openedFilesPending === 'function') {
  const STREAM_LIMIT = 64 * 1024 * 1024;   // the helper's url-only threshold
  JSON.parse(WebToApk.openedFilesPending() || '[]').forEach(function (m) {
    const urlOnly = m.kind === 'image' || m.kind === 'video' || m.kind === 'audio' || m.size > STREAM_LIMIT;
    console.log(m.name, m.mimeType, m.size + ' bytes',
      urlOnly ? 'will come as url only' : 'will be pulled into f.file',
      m.isText ? '(text)' : '');
  });
}

Notes: The helper collects these automatically after load and whenever a new file arrives, so a page rarely sees anything here. Once collected, a file leaves this list but stays readable until released.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-11-04.