Spaces:
Running
Running
Upload native_features.py
Browse files- native_features.py +131 -16
native_features.py
CHANGED
|
@@ -1091,13 +1091,30 @@ _NATIVE_BRIDGE_JS = r"""// capacitor-native-bridge.js — Auto-injected by APK C
|
|
| 1091 |
// Starts progress UI immediately and streams network chunks in real time!
|
| 1092 |
// ═══════════════════════════════════════════════
|
| 1093 |
var origFetch = window.fetch;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
window.fetch = async function(resource, init) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
var url = '';
|
| 1096 |
if (typeof resource === 'string') {
|
| 1097 |
url = resource;
|
| 1098 |
} else if (resource && resource.url) {
|
| 1099 |
url = resource.url;
|
| 1100 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1101 |
|
| 1102 |
var isDownloadCandidate = false;
|
| 1103 |
var now = Date.now();
|
|
@@ -1252,7 +1269,9 @@ _NATIVE_BRIDGE_JS = r"""// capacitor-native-bridge.js — Auto-injected by APK C
|
|
| 1252 |
if (!blob && tf.localPath && window.Capacitor && window.Capacitor.convertFileSrc) {
|
| 1253 |
try {
|
| 1254 |
var webUrl = window.Capacitor.convertFileSrc(tf.localPath);
|
| 1255 |
-
|
|
|
|
|
|
|
| 1256 |
if (resp.ok) {
|
| 1257 |
blob = await resp.blob();
|
| 1258 |
}
|
|
@@ -1520,9 +1539,23 @@ _NATIVE_BRIDGE_JS = r"""// capacitor-native-bridge.js — Auto-injected by APK C
|
|
| 1520 |
// ═══════════════════════════════════════════════
|
| 1521 |
// 2. ROBUST CHUNKED DOWNLOAD & SAVE TO DEVICE
|
| 1522 |
// Intercepts <a download> and blob:/data: URLs.
|
| 1523 |
-
//
|
| 1524 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1525 |
// ═══════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1526 |
async function saveDownloadToDevice(url, suggestedFilename) {
|
| 1527 |
console.log('[NativeBridge] Intercepted download:', suggestedFilename, (url || '').slice(0, 50));
|
| 1528 |
var filename = (suggestedFilename || '').trim();
|
|
@@ -1549,28 +1582,110 @@ _NATIVE_BRIDGE_JS = r"""// capacitor-native-bridge.js — Auto-injected by APK C
|
|
| 1549 |
|
| 1550 |
try {
|
| 1551 |
// 2. Fetch if not in blobStore (handles data: or remote http/https)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1552 |
if (!blob) {
|
| 1553 |
-
|
|
|
|
|
|
|
|
|
|
| 1554 |
if (!resp.ok) throw new Error('Fetch failed with status ' + resp.status);
|
| 1555 |
var totalBytes = parseInt(resp.headers.get('content-length') || '0', 10);
|
| 1556 |
-
var
|
| 1557 |
ui.update(0, totalBytes);
|
| 1558 |
|
| 1559 |
if (resp.body && typeof resp.body.getReader === 'function') {
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
| 1565 |
-
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1570 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1571 |
}
|
| 1572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1573 |
} else {
|
|
|
|
|
|
|
| 1574 |
blob = await resp.blob();
|
| 1575 |
ui.update(blob.size, blob.size);
|
| 1576 |
}
|
|
|
|
| 1091 |
// Starts progress UI immediately and streams network chunks in real time!
|
| 1092 |
// ═══════════════════════════════════════════════
|
| 1093 |
var origFetch = window.fetch;
|
| 1094 |
+
// Bridge-internal bypass: saveDownloadToDevice / nativeEntryToFile must use
|
| 1095 |
+
// origFetch directly so their network reads never re-enter this wrapper
|
| 1096 |
+
// (prevents double DownloadUI cards). Exposed for the onCapReady closure.
|
| 1097 |
+
window.__nativeOrigFetch = origFetch;
|
| 1098 |
window.fetch = async function(resource, init) {
|
| 1099 |
+
// 0.3.0 INTERNAL BYPASS — never treat the bridge's own reads as downloads.
|
| 1100 |
+
if (init && init.__nativeBridgeInternal) {
|
| 1101 |
+
var cleanInit = {};
|
| 1102 |
+
for (var _k in init) {
|
| 1103 |
+
if (Object.prototype.hasOwnProperty.call(init, _k) && _k !== '__nativeBridgeInternal') cleanInit[_k] = init[_k];
|
| 1104 |
+
}
|
| 1105 |
+
return origFetch.call(this, resource, cleanInit);
|
| 1106 |
+
}
|
| 1107 |
var url = '';
|
| 1108 |
if (typeof resource === 'string') {
|
| 1109 |
url = resource;
|
| 1110 |
} else if (resource && resource.url) {
|
| 1111 |
url = resource.url;
|
| 1112 |
}
|
| 1113 |
+
// Internal Capacitor cache reads are never user downloads — skip wrapper entirely.
|
| 1114 |
+
// (convertFileSrc() URLs look like http://localhost/_capacitor_file_/...)
|
| 1115 |
+
if (url && (url.indexOf('/_capacitor_file_') !== -1 || url.indexOf('capacitor://') === 0)) {
|
| 1116 |
+
return origFetch.apply(this, arguments);
|
| 1117 |
+
}
|
| 1118 |
|
| 1119 |
var isDownloadCandidate = false;
|
| 1120 |
var now = Date.now();
|
|
|
|
| 1269 |
if (!blob && tf.localPath && window.Capacitor && window.Capacitor.convertFileSrc) {
|
| 1270 |
try {
|
| 1271 |
var webUrl = window.Capacitor.convertFileSrc(tf.localPath);
|
| 1272 |
+
// Bridge-internal read: bypass download-detection wrapper (no spurious DownloadUI).
|
| 1273 |
+
var _of = window.__nativeOrigFetch || origFetch || window.fetch;
|
| 1274 |
+
var resp = await _of.call(window, webUrl);
|
| 1275 |
if (resp.ok) {
|
| 1276 |
blob = await resp.blob();
|
| 1277 |
}
|
|
|
|
| 1539 |
// ═══════════════════════════════════════════════
|
| 1540 |
// 2. ROBUST CHUNKED DOWNLOAD & SAVE TO DEVICE
|
| 1541 |
// Intercepts <a download> and blob:/data: URLs.
|
| 1542 |
+
// PUMP-AND-FORWARD: network bytes stream straight to native in
|
| 1543 |
+
// 512KB stages (never materialize a full Blob for http(s)).
|
| 1544 |
+
// Binder limit 1MB: 512KB raw -> ~683KB base64 + JSON < 800KB, safe.
|
| 1545 |
+
// Chunks are awaited sequentially (Java has no seq num, appends in
|
| 1546 |
+
// arrival order — parallel sends would corrupt file order).
|
| 1547 |
+
// Auto-renames duplicates, real-time progress UI, single card per download.
|
| 1548 |
// ═══════════════════════════════════════════════
|
| 1549 |
+
// Binder-safe Uint8Array -> base64 without Blob/FileReader.
|
| 1550 |
+
// Chunked fromCharCode avoids arg-stack overflow on 512KB arrays.
|
| 1551 |
+
function uint8ToBase64(u8) {
|
| 1552 |
+
var s = '';
|
| 1553 |
+
var CH = 0x8000;
|
| 1554 |
+
for (var i = 0; i < u8.length; i += CH) {
|
| 1555 |
+
s += String.fromCharCode.apply(null, u8.subarray(i, i + CH));
|
| 1556 |
+
}
|
| 1557 |
+
return btoa(s);
|
| 1558 |
+
}
|
| 1559 |
async function saveDownloadToDevice(url, suggestedFilename) {
|
| 1560 |
console.log('[NativeBridge] Intercepted download:', suggestedFilename, (url || '').slice(0, 50));
|
| 1561 |
var filename = (suggestedFilename || '').trim();
|
|
|
|
| 1582 |
|
| 1583 |
try {
|
| 1584 |
// 2. Fetch if not in blobStore (handles data: or remote http/https)
|
| 1585 |
+
// PUMP-AND-FORWARD: http(s) bytes go network -> native directly in
|
| 1586 |
+
// 512KB stages. Max JS accumulation = 512KB (plus one net read,
|
| 1587 |
+
// typically 16-64KB). Never builds a full Blob for remote URLs,
|
| 1588 |
+
// so a 30/100MB file peaks <1MB in JS instead of 2x file size.
|
| 1589 |
if (!blob) {
|
| 1590 |
+
// Bridge-internal fetch: bypasses the window.fetch download wrapper
|
| 1591 |
+
// above, so this read creates NO second DownloadUI card.
|
| 1592 |
+
var _of2 = window.__nativeOrigFetch || origFetch || window.fetch;
|
| 1593 |
+
var resp = await _of2.call(window, url);
|
| 1594 |
if (!resp.ok) throw new Error('Fetch failed with status ' + resp.status);
|
| 1595 |
var totalBytes = parseInt(resp.headers.get('content-length') || '0', 10);
|
| 1596 |
+
var streamMime = resp.headers.get('content-type') || 'application/octet-stream';
|
| 1597 |
ui.update(0, totalBytes);
|
| 1598 |
|
| 1599 |
if (resp.body && typeof resp.body.getReader === 'function') {
|
| 1600 |
+
// Fail-fast on permission before pulling bytes over the network.
|
| 1601 |
+
if (window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()) {
|
| 1602 |
+
try {
|
| 1603 |
+
var _perm = await callNative('NativeFolderHelper', 'checkStoragePermission', {});
|
| 1604 |
+
if (_perm && _perm.granted === false) {
|
| 1605 |
+
ui.error({ message: 'File was not saved: Storage permission denied' });
|
| 1606 |
+
return null;
|
| 1607 |
+
}
|
| 1608 |
+
} catch (_pe) {}
|
| 1609 |
+
}
|
| 1610 |
+
var streamDownloadId = 'dl_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7);
|
| 1611 |
+
var STAGE_MAX = 512 * 1024; // 512KB raw -> ~683KB b64, well under 1MB binder
|
| 1612 |
+
var staging = [];
|
| 1613 |
+
var stagedLen = 0;
|
| 1614 |
+
var streamedTotal = 0;
|
| 1615 |
+
var streamFirst = true;
|
| 1616 |
+
var streamLastRes = null;
|
| 1617 |
+
var netReader = resp.body.getReader();
|
| 1618 |
+
|
| 1619 |
+
async function flushStage(isLast) {
|
| 1620 |
+
if (stagedLen === 0) {
|
| 1621 |
+
if (!isLast || !streamFirst) return;
|
| 1622 |
+
// Empty file: still create the MediaStore entry.
|
| 1623 |
+
streamLastRes = await callNative('NativeFolderHelper', 'saveFileChunk', {
|
| 1624 |
+
downloadId: streamDownloadId,
|
| 1625 |
+
filename: filename,
|
| 1626 |
+
mimeType: streamMime,
|
| 1627 |
+
chunk: '',
|
| 1628 |
+
isFirst: true,
|
| 1629 |
+
isLast: true,
|
| 1630 |
+
totalSize: totalBytes || 0
|
| 1631 |
+
});
|
| 1632 |
+
streamFirst = false;
|
| 1633 |
+
return;
|
| 1634 |
+
}
|
| 1635 |
+
var combined = new Uint8Array(stagedLen);
|
| 1636 |
+
var _pos = 0;
|
| 1637 |
+
for (var _si = 0; _si < staging.length; _si++) {
|
| 1638 |
+
combined.set(staging[_si], _pos);
|
| 1639 |
+
_pos += staging[_si].length;
|
| 1640 |
}
|
| 1641 |
+
staging = [];
|
| 1642 |
+
stagedLen = 0;
|
| 1643 |
+
var b64 = uint8ToBase64(combined);
|
| 1644 |
+
// `combined`/ staging refs dropped here so GC can reclaim before next await.
|
| 1645 |
+
streamLastRes = await callNative('NativeFolderHelper', 'saveFileChunk', {
|
| 1646 |
+
downloadId: streamDownloadId,
|
| 1647 |
+
filename: filename,
|
| 1648 |
+
mimeType: streamMime,
|
| 1649 |
+
chunk: b64,
|
| 1650 |
+
isFirst: streamFirst,
|
| 1651 |
+
isLast: !!isLast,
|
| 1652 |
+
totalSize: totalBytes || 0
|
| 1653 |
+
});
|
| 1654 |
+
streamFirst = false;
|
| 1655 |
}
|
| 1656 |
+
|
| 1657 |
+
try {
|
| 1658 |
+
while (true) {
|
| 1659 |
+
var nr = await netReader.read();
|
| 1660 |
+
if (nr.done) break;
|
| 1661 |
+
if (nr.value) {
|
| 1662 |
+
staging.push(nr.value);
|
| 1663 |
+
stagedLen += nr.value.length;
|
| 1664 |
+
streamedTotal += nr.value.length;
|
| 1665 |
+
ui.update(streamedTotal, totalBytes || streamedTotal);
|
| 1666 |
+
// Flush as soon as we hit 512KB — data passes through immediately,
|
| 1667 |
+
// accumulation never exceeds STAGE_MAX + one net read.
|
| 1668 |
+
if (stagedLen >= STAGE_MAX) {
|
| 1669 |
+
await flushStage(false);
|
| 1670 |
+
}
|
| 1671 |
+
}
|
| 1672 |
+
}
|
| 1673 |
+
await flushStage(true);
|
| 1674 |
+
} catch (streamErr) {
|
| 1675 |
+
// Partial MediaStore entry (IS_PENDING=1) may remain open in Java;
|
| 1676 |
+
// there is currently no abortDownload() to delete it (see Part B).
|
| 1677 |
+
try { if (netReader && netReader.cancel) await netReader.cancel(); } catch (_c) {}
|
| 1678 |
+
throw streamErr;
|
| 1679 |
+
}
|
| 1680 |
+
|
| 1681 |
+
var _sUri = (streamLastRes && streamLastRes.uri) || '';
|
| 1682 |
+
var _sName = (streamLastRes && streamLastRes.filename) || filename;
|
| 1683 |
+
var _sFolder = (streamLastRes && streamLastRes.folder) || 'Downloads';
|
| 1684 |
+
ui.success(_sName, _sFolder, _sUri);
|
| 1685 |
+
return streamLastRes;
|
| 1686 |
} else {
|
| 1687 |
+
// No ReadableStream (blob:/data: in old WebViews): must materialize.
|
| 1688 |
+
// Uses bypassed fetch's raw .blob() so no second DownloadUI card.
|
| 1689 |
blob = await resp.blob();
|
| 1690 |
ui.update(blob.size, blob.size);
|
| 1691 |
}
|