1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Add delay before revoking URL on Android (#51299)

This commit is contained in:
Timothy
2026-03-31 12:23:00 +02:00
committed by GitHub
parent 32b9676f97
commit 3581b43336
5 changed files with 84 additions and 3 deletions

View File

@@ -464,7 +464,6 @@ export class HaAutomationTrace extends LitElement {
url,
`trace ${this._entityId} ${this._trace!.timestamp.start}.json`
);
URL.revokeObjectURL(url);
}
private _importTrace() {

View File

@@ -806,7 +806,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
url,
`zwave_js_backup_${new Date().toISOString().replace(/[:.]/g, "-")}.bin`
);
URL.revokeObjectURL(url);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(

View File

@@ -479,7 +479,6 @@ export class HaScriptTrace extends LitElement {
url,
`trace ${this._entityId} ${this._trace!.timestamp.start}.json`
);
URL.revokeObjectURL(url);
}
private _importTrace() {

View File

@@ -1,3 +1,9 @@
// 10 seconds gives the Android WebView download listener enough time
// to open the blob before it is revoked, while still freeing memory
// promptly. Revoking immediately would invalidate the URL before the
// native layer can read it.
const BLOB_REVOKE_DELAY_MS = 10_000;
export const fileDownload = (href: string, filename = ""): void => {
const element = document.createElement("a");
element.target = "_blank";
@@ -7,4 +13,14 @@ export const fileDownload = (href: string, filename = ""): void => {
document.body.appendChild(element);
element.dispatchEvent(new MouseEvent("click"));
document.body.removeChild(element);
if (href.startsWith("blob:")) {
// Revoke blob URLs after a delay on Android so the WebView download
// listener has time to fetch the blob before it becomes invalid.
if (window.externalApp) {
setTimeout(() => URL.revokeObjectURL(href), BLOB_REVOKE_DELAY_MS);
} else {
URL.revokeObjectURL(href);
}
}
};

View File

@@ -0,0 +1,68 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fileDownload } from "../../src/util/file_download";
describe("fileDownload", () => {
let appendChildSpy: ReturnType<typeof vi.spyOn>;
let removeChildSpy: ReturnType<typeof vi.spyOn>;
let dispatchEventSpy: ReturnType<typeof vi.spyOn>;
let createdElement: HTMLAnchorElement;
beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => undefined);
createdElement = document.createElement("a");
vi.spyOn(createdElement, "dispatchEvent");
vi.spyOn(document, "createElement").mockReturnValue(createdElement);
appendChildSpy = vi
.spyOn(document.body, "appendChild")
.mockImplementation((node) => node);
removeChildSpy = vi
.spyOn(document.body, "removeChild")
.mockImplementation((node) => node);
dispatchEventSpy = vi.spyOn(createdElement, "dispatchEvent");
});
afterEach(() => {
vi.restoreAllMocks();
delete (window as any).externalApp;
});
it("sets href, download, and triggers a click", () => {
fileDownload("https://example.com/file.json", "file.json");
expect(createdElement.href).toBe("https://example.com/file.json");
expect(createdElement.download).toBe("file.json");
expect(appendChildSpy).toHaveBeenCalledWith(createdElement);
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(MouseEvent));
expect(removeChildSpy).toHaveBeenCalledWith(createdElement);
});
it("defaults filename to empty string", () => {
fileDownload("https://example.com/file.json");
expect(createdElement.download).toBe("");
});
it("does not revoke non-blob URLs", () => {
fileDownload("https://example.com/file.json", "file.json");
vi.runAllTimers();
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
});
it("revokes blob URLs immediately outside Android", () => {
fileDownload("blob:http://localhost/abc-123", "file.json");
expect(URL.revokeObjectURL).toHaveBeenCalledWith(
"blob:http://localhost/abc-123"
);
});
it("revokes blob URL after delay on Android", () => {
(window as any).externalApp = {};
fileDownload("blob:http://localhost/abc-123", "file.json");
vi.advanceTimersByTime(9_999);
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(URL.revokeObjectURL).toHaveBeenCalledWith(
"blob:http://localhost/abc-123"
);
});
});