From 42977b19bde5d1222ba948183d26f1562b2d20f3 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:51:32 +0530 Subject: [PATCH 1/3] fix(session): restore picker documents and skip intent files tab --- src/handlers/intent.js | 23 ++-- src/lib/editorFile.js | 3 + src/lib/openFile.js | 16 ++- src/lib/saveState.js | 2 +- src/pages/fileBrowser/index.js | 7 +- src/plugins/sdcard/index.d.ts | 1 + src/plugins/sdcard/src/android/SDcard.java | 36 ++++-- tests/unit/sessionPersistence.test.js | 127 +++++++++++++++++++++ 8 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 tests/unit/sessionPersistence.test.js diff --git a/src/handlers/intent.js b/src/handlers/intent.js index fc5f5b9cb1..78c0009ab6 100644 --- a/src/handlers/intent.js +++ b/src/handlers/intent.js @@ -17,11 +17,14 @@ const pendingIntents = []; * @param {Intent} intent */ export default async function HandleIntent(intent = {}) { - const type = intent.action.split(".").slice(-1)[0]; + const type = intent.action?.split(".").slice(-1)[0]; if (["SEND", "VIEW", "EDIT"].includes(type)) { /**@type {string} */ - const url = intent.fileUri || intent.data; + const url = + intent.fileUri || + intent.data || + intent.extras?.["android.intent.extra.STREAM"]; if (!url) return; if (url.startsWith("acode://")) { @@ -72,19 +75,19 @@ export default async function HandleIntent(intent = {}) { return; } + const options = { + mode: "single", + render: true, + persistInSession: false, + }; + if (sessionStorage.getItem("isfilesRestored") === "true") { - await openFile(url, { - mode: "single", - render: true, - }); + await openFile(url, options); } else { // Store the intent for later processing when files are restored pendingIntents.push({ url, - options: { - mode: "single", - render: true, - }, + options, }); } } diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index 2aee9b500f..df7abed821 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -330,6 +330,7 @@ function maybeRecommendLanguageModeExtension(file, modeInfo) { * @property {string} [paneId] target editor pane id * @property {object} [pane] target editor pane * @property {boolean} [isPanePlaceholder] temporary empty tab for an empty pane + * @property {boolean} [persistInSession] restore the tab in a future app session * @property {boolean} [highlightStyles] adopt static CodeMirror highlight CSS into the custom tab shadow root */ @@ -500,6 +501,7 @@ export default class EditorFile { diskMtime = null; hasDiskConflict = false; isPanePlaceholder = false; + persistInSession = true; /** * @@ -513,6 +515,7 @@ export default class EditorFile { this.hideQuickTools = options?.hideQuickTools || false; this.paneId = options?.paneId || options?.pane?.id || null; this.isPanePlaceholder = !!options?.isPanePlaceholder; + this.persistInSession = options?.persistInSession !== false; // if options are passed if (options) { diff --git a/src/lib/openFile.js b/src/lib/openFile.js index bbb5e5812f..bc8445769d 100644 --- a/src/lib/openFile.js +++ b/src/lib/openFile.js @@ -22,6 +22,7 @@ import appSettings from "./settings"; * @property {string} mode * @property {string} uri * @property {string} paneId + * @property {boolean} persistInSession */ /** @@ -37,9 +38,21 @@ export default async function openFile(file, options = {}) { /**@type {EditorFile} */ const existingFile = editorManager.getFile(uri, "uri"); - const { cursorPos, render, onsave, text, mode, encoding, paneId } = options; + const { + cursorPos, + render, + onsave, + text, + mode, + encoding, + paneId, + persistInSession, + } = options; if (existingFile) { + if (persistInSession !== undefined) { + existingFile.persistInSession = persistInSession !== false; + } // If file is already opened and new text is provided const incomingDoc = text != null ? Text.of(String(text).split("\n")) : null; @@ -120,6 +133,7 @@ export default async function openFile(file, options = {}) { savedMtime: helpers.getStatMtime(fileInfo), diskMtime: helpers.getStatMtime(fileInfo), paneId, + persistInSession, }); }; diff --git a/src/lib/saveState.js b/src/lib/saveState.js index 784304d475..7e0ea6790b 100644 --- a/src/lib/saveState.js +++ b/src/lib/saveState.js @@ -14,7 +14,7 @@ export default () => { files.forEach((file) => { if (file.type !== "editor") return; if (file.id === config.DEFAULT_FILE_SESSION) return; - if (file.SAFMode === "single") return; + if (file.persistInSession === false) return; // Selection per file: // - Active file uses live EditorView selection diff --git a/src/pages/fileBrowser/index.js b/src/pages/fileBrowser/index.js index 63becfb329..bd46f597c8 100644 --- a/src/pages/fileBrowser/index.js +++ b/src/pages/fileBrowser/index.js @@ -31,7 +31,7 @@ function FileBrowser(mode, info, doesOpenLast, ...args) { } FileBrowser.openFile = (res) => { - const { url, name, mode } = res; + const { url, name, mode, persistedUriPermission } = res; const createOption = { uri: url, name, @@ -41,8 +41,11 @@ FileBrowser.openFile = (res) => { if (mode) { createOption.mode = mode; } + if (mode === "single") { + createOption.persistInSession = persistedUriPermission !== false; + } - openFile(url, createOption); + return openFile(url, createOption); }; FileBrowser.openFileError = (err) => { diff --git a/src/plugins/sdcard/index.d.ts b/src/plugins/sdcard/index.d.ts index f151eab349..fd6a6b7db8 100644 --- a/src/plugins/sdcard/index.d.ts +++ b/src/plugins/sdcard/index.d.ts @@ -35,6 +35,7 @@ interface DocumentFile { canWrite: boolean; filename: string; length: number; + persistedUriPermission: boolean; type: string; uri: string; } diff --git a/src/plugins/sdcard/src/android/SDcard.java b/src/plugins/sdcard/src/android/SDcard.java index 1b33e7edef..578d33dda3 100644 --- a/src/plugins/sdcard/src/android/SDcard.java +++ b/src/plugins/sdcard/src/android/SDcard.java @@ -266,6 +266,11 @@ public void openDocumentFile(String mimeType, CallbackContext callback) { intent.setAction(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(mimeType); + intent.addFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION | + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + ); activityResultCallback = callback; cordova.startActivityForResult(this, intent, this.OPEN_DOCUMENT); } @@ -409,7 +414,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { return; } - takePermission(uri); + boolean persistedUriPermission = takePermission( + uri, + data.getFlags() + ); DocumentFile file = DocumentFile.fromSingleUri(context, uri); JSONObject res = new JSONObject(); @@ -418,6 +426,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { res.put("filename", file.getName()); res.put("canWrite", canWrite(file.getUri())); res.put("uri", uri.toString()); + res.put("persistedUriPermission", persistedUriPermission); activityResultCallback.success(res); } catch (JSONException e) { activityResultCallback.error(e.toString()); @@ -447,7 +456,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { return; } - takePermission(uri); + takePermission(uri, data.getFlags()); DocumentFile file = DocumentFile.fromTreeUri(context, uri); if (file != null && file.canWrite()) { activityResultCallback.success(uri.toString()); @@ -1152,13 +1161,22 @@ private DocumentFile getFile(String filePath) { return documentFile; } - private void takePermission(Uri uri) { - contentResolver = context.getContentResolver(); - contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | - Intent.FLAG_GRANT_READ_URI_PERMISSION - ); + private boolean takePermission(Uri uri, int intentFlags) { + int permissionFlags = + intentFlags & + (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | + Intent.FLAG_GRANT_READ_URI_PERMISSION); + + if (permissionFlags == 0) return false; + + try { + contentResolver = context.getContentResolver(); + contentResolver.takePersistableUriPermission(uri, permissionFlags); + return true; + } catch (SecurityException | IllegalArgumentException error) { + Log.w("SDcard", "Unable to persist URI permission for " + uri, error); + return false; + } } public boolean canWrite(Uri uri) { diff --git a/tests/unit/sessionPersistence.test.js b/tests/unit/sessionPersistence.test.js new file mode 100644 index 0000000000..50d3699293 --- /dev/null +++ b/tests/unit/sessionPersistence.test.js @@ -0,0 +1,127 @@ +// @vitest-environment happy-dom + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const runtime = vi.hoisted(() => { + const settings = { + value: { + rememberFiles: true, + rememberFolders: false, + }, + }; + + return { + settings, + openFile: vi.fn(async (uri, options = {}) => { + const file = { + id: `file-${globalThis.editorManager.files.length + 1}`, + uri, + type: "editor", + filename: options.name || uri.split("/").pop(), + pinned: false, + isUnsaved: false, + docVersion: 0, + savedVersion: 0, + cacheVersion: 0, + savedMtime: 1, + diskMtime: 1, + hasDiskConflict: false, + readOnly: false, + SAFMode: options.mode, + deletedFile: false, + session: { + selection: { ranges: [{ from: 0, to: 0 }], mainIndex: 0 }, + }, + lastScrollTop: 0, + lastScrollLeft: 0, + editable: true, + encoding: "UTF-8", + persistInSession: options.persistInSession, + }; + + globalThis.editorManager.files.push(file); + globalThis.editorManager.activeFile = file; + }), + }; +}); + +vi.mock("cm/editorUtils", () => ({ + getAllFolds: vi.fn(() => []), + getScrollPosition: vi.fn(() => ({ scrollTop: 0, scrollLeft: 0 })), + getSelection: vi.fn(() => ({ + ranges: [{ from: 0, to: 0 }], + mainIndex: 0, + })), +})); +vi.mock("dialogs/alert", () => ({ default: vi.fn() })); +vi.mock("fileSystem", () => ({ default: vi.fn() })); +vi.mock("lib/auth", () => ({ default: {} })); +vi.mock("lib/config", () => ({ + default: { DEFAULT_FILE_SESSION: "default-session" }, +})); +vi.mock("lib/openFile", () => ({ default: runtime.openFile })); +vi.mock("lib/openFolder", () => ({ + default: vi.fn(), + addedFolder: [], +})); +vi.mock("lib/settings", () => ({ default: runtime.settings })); +vi.mock("lib/startAd", () => ({ + BANNER_SUPPRESSION_REASON: {}, + setBannerSuppressed: vi.fn(), +})); +vi.mock("utils/helpers", () => ({ + default: { error: vi.fn(), errorMessage: vi.fn() }, +})); + +import HandleIntent from "handlers/intent"; +import saveState from "lib/saveState"; +import FileBrowser from "pages/fileBrowser"; + +describe("file session persistence", () => { + beforeEach(() => { + runtime.openFile.mockClear(); + localStorage.clear(); + sessionStorage.clear(); + sessionStorage.setItem("isfilesRestored", "true"); + globalThis.editorManager = { + editor: {}, + files: [], + activeFile: null, + getFile(value, type = "id") { + return this.files.find((file) => file[type] === value); + }, + }; + }); + + it("persists only a picker document with durable URI access", async () => { + const selectedUri = "content://documents/document/selected"; + const unpersistableUri = "content://documents/document/no-grant"; + const intentUri = "content://external-app/shared/temporary"; + + await FileBrowser.openFile({ + type: "file", + url: selectedUri, + name: "selected.js", + mode: "single", + persistedUriPermission: true, + }); + await FileBrowser.openFile({ + type: "file", + url: unpersistableUri, + name: "no-grant.js", + mode: "single", + persistedUriPermission: false, + }); + await HandleIntent({ + action: "android.intent.action.VIEW", + data: intentUri, + }); + + saveState(); + + const restoredUris = JSON.parse(localStorage.files).map( + (file) => file.uri, + ); + expect(restoredUris).toEqual([selectedUri]); + }); +}); From 380c56dca4f7f7fc9f74637a9d8b213a3a0d6d53 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:08:50 +0530 Subject: [PATCH 2/3] feat(file-browser): expose document picker(icon) from nested folders --- src/pages/fileBrowser/fileBrowser.js | 64 ++++++++++++++++++---------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index b26758b189..ef9c59ce28 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -60,6 +60,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const IS_FOLDER_MODE = ["folder", "both"].includes(mode); const IS_FILE_MODE = ["file", "both"].includes(mode); + const SELECT_DOCUMENT_LABEL = "Select document"; const storedState = helpers.parseJSON(localStorage.fileBrowserState) || []; /**@type {Array} */ const state = []; @@ -106,6 +107,16 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { ); const $search = ; + const $selectDocument = ( + + ); const $lead = ; const $page = Page(strings["file browser"].capitalize(), { lead: $lead, @@ -178,8 +189,9 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $content.addEventListener("click", handleClick); $content.addEventListener("contextmenu", handleContextMenu, true); $page.body = $content; + $page.header.append($search); + if (IS_FILE_MODE) $page.header.append($selectDocument); $page.header.append( - $search, $pasteToggler, $selectionModeToggler, $addMenuToggler, @@ -222,6 +234,12 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { }; $pasteToggler.onclick = pasteCopiedItems; + $selectDocument.onclick = selectDocument; + $selectDocument.onkeydown = (event) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + selectDocument(); + }; $fbMenu.onclick = function (e) { $fbMenu.hide(); @@ -653,6 +671,25 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $page.hide(); } + function selectDocument() { + checkFiles.check = false; + sdcard.openDocumentFile( + (res) => { + res.url = res.uri; + resolve({ + type: "file", + ...res, + name: res.filename, + mode: "single", + }); + $page.hide(); + }, + (err) => { + helpers.error(err); + }, + ); + } + /** * @param {string} url */ @@ -872,6 +909,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $addMenuToggler.style.display = "none"; $menuToggler.style.display = "none"; + $selectDocument.style.display = "none"; $selectionMenuToggler.style.display = ""; updatePasteToggler(); @@ -899,6 +937,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { $addMenuToggler.style.display = ""; $menuToggler.style.display = ""; + $selectDocument.style.display = ""; $selectionMenuToggler.style.display = "none"; updatePasteToggler(); @@ -998,7 +1037,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { else if (!$el.hasAttribute("disabled")) file(); break; case "openDoc": - openDoc(); + selectDocument(); break; } @@ -1304,25 +1343,6 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { localStorage.storageList = JSON.stringify(storageList); reload(); } - - function openDoc() { - checkFiles.check = false; - sdcard.openDocumentFile( - (res) => { - res.url = res.uri; - resolve({ - type: "file", - ...res, - name: res.filename, - mode: "single", - }); - $page.hide(); - }, - (err) => { - helpers.error(err); - }, - ); - } } function handleContextMenu(e) { @@ -1431,7 +1451,7 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { } if (IS_FILE_MODE) { - util.pushFolder(allStorages, "Select document", null, { + util.pushFolder(allStorages, SELECT_DOCUMENT_LABEL, null, { openDoc: true, notSelectable: true, }); From dd3a78ff2c19419a9b2f338bce720e1f6d7eb631 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:08:56 +0530 Subject: [PATCH 3/3] fix --- src/lib/fileSessionPersistence.js | 13 ++++ src/lib/openFile.js | 5 +- tests/unit/sessionPersistence.test.js | 100 ++++++++++++++++++-------- 3 files changed, 84 insertions(+), 34 deletions(-) create mode 100644 src/lib/fileSessionPersistence.js diff --git a/src/lib/fileSessionPersistence.js b/src/lib/fileSessionPersistence.js new file mode 100644 index 0000000000..e14c639402 --- /dev/null +++ b/src/lib/fileSessionPersistence.js @@ -0,0 +1,13 @@ +/** + * Promote an open file to durable session persistence when a later open source + * proves that its URI will remain accessible. Less durable sources, such as + * external intents, must not revoke an existing durable grant. + * + * @param {{persistInSession?: boolean}} file + * @param {boolean | undefined} requestedPersistence + */ +export function promoteSessionPersistence(file, requestedPersistence) { + if (requestedPersistence === true) { + file.persistInSession = true; + } +} diff --git a/src/lib/openFile.js b/src/lib/openFile.js index bc8445769d..020c028558 100644 --- a/src/lib/openFile.js +++ b/src/lib/openFile.js @@ -8,6 +8,7 @@ import { reopenWithNewEncoding } from "palettes/changeEncoding"; import { decode, detectEncoding } from "utils/encodings"; import helpers from "utils/helpers"; import EditorFile from "./editorFile"; +import { promoteSessionPersistence } from "./fileSessionPersistence"; import fileTypeHandler from "./fileTypeHandler"; import recents from "./recents"; import appSettings from "./settings"; @@ -50,9 +51,7 @@ export default async function openFile(file, options = {}) { } = options; if (existingFile) { - if (persistInSession !== undefined) { - existingFile.persistInSession = persistInSession !== false; - } + promoteSessionPersistence(existingFile, persistInSession); // If file is already opened and new text is provided const incomingDoc = text != null ? Text.of(String(text).split("\n")) : null; diff --git a/tests/unit/sessionPersistence.test.js b/tests/unit/sessionPersistence.test.js index 50d3699293..938cc63a60 100644 --- a/tests/unit/sessionPersistence.test.js +++ b/tests/unit/sessionPersistence.test.js @@ -12,36 +12,7 @@ const runtime = vi.hoisted(() => { return { settings, - openFile: vi.fn(async (uri, options = {}) => { - const file = { - id: `file-${globalThis.editorManager.files.length + 1}`, - uri, - type: "editor", - filename: options.name || uri.split("/").pop(), - pinned: false, - isUnsaved: false, - docVersion: 0, - savedVersion: 0, - cacheVersion: 0, - savedMtime: 1, - diskMtime: 1, - hasDiskConflict: false, - readOnly: false, - SAFMode: options.mode, - deletedFile: false, - session: { - selection: { ranges: [{ from: 0, to: 0 }], mainIndex: 0 }, - }, - lastScrollTop: 0, - lastScrollLeft: 0, - editable: true, - encoding: "UTF-8", - persistInSession: options.persistInSession, - }; - - globalThis.editorManager.files.push(file); - globalThis.editorManager.activeFile = file; - }), + openFile: vi.fn(), }; }); @@ -74,12 +45,13 @@ vi.mock("utils/helpers", () => ({ })); import HandleIntent from "handlers/intent"; +import { promoteSessionPersistence } from "lib/fileSessionPersistence"; import saveState from "lib/saveState"; import FileBrowser from "pages/fileBrowser"; describe("file session persistence", () => { beforeEach(() => { - runtime.openFile.mockClear(); + runtime.openFile.mockReset(); localStorage.clear(); sessionStorage.clear(); sessionStorage.setItem("isfilesRestored", "true"); @@ -91,6 +63,18 @@ describe("file session persistence", () => { return this.files.find((file) => file[type] === value); }, }; + runtime.openFile.mockImplementation(async (uri, options = {}) => { + const existingFile = globalThis.editorManager.getFile(uri, "uri"); + if (existingFile) { + promoteSessionPersistence(existingFile, options.persistInSession); + globalThis.editorManager.activeFile = existingFile; + return; + } + + const file = createOpenFile(uri, options); + globalThis.editorManager.files.push(file); + globalThis.editorManager.activeFile = file; + }); }); it("persists only a picker document with durable URI access", async () => { @@ -116,6 +100,10 @@ describe("file session persistence", () => { action: "android.intent.action.VIEW", data: intentUri, }); + await HandleIntent({ + action: "android.intent.action.VIEW", + data: selectedUri, + }); saveState(); @@ -124,4 +112,54 @@ describe("file session persistence", () => { ); expect(restoredUris).toEqual([selectedUri]); }); + + it("promotes an intent tab when the same document is later selected", async () => { + const uri = "content://documents/document/shared"; + + await HandleIntent({ + action: "android.intent.action.VIEW", + data: uri, + }); + await FileBrowser.openFile({ + type: "file", + url: uri, + name: "shared.js", + mode: "single", + persistedUriPermission: true, + }); + + saveState(); + + expect(JSON.parse(localStorage.files).map((file) => file.uri)).toEqual([ + uri, + ]); + }); }); + +function createOpenFile(uri, options) { + return { + id: `file-${globalThis.editorManager.files.length + 1}`, + uri, + type: "editor", + filename: options.name || uri.split("/").pop(), + pinned: false, + isUnsaved: false, + docVersion: 0, + savedVersion: 0, + cacheVersion: 0, + savedMtime: 1, + diskMtime: 1, + hasDiskConflict: false, + readOnly: false, + SAFMode: options.mode, + deletedFile: false, + session: { + selection: { ranges: [{ from: 0, to: 0 }], mainIndex: 0 }, + }, + lastScrollTop: 0, + lastScrollLeft: 0, + editable: true, + encoding: "UTF-8", + persistInSession: options.persistInSession, + }; +}