diff --git a/bun.lock b/bun.lock index caba5e279..894453012 100644 --- a/bun.lock +++ b/bun.lock @@ -49,6 +49,8 @@ "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", + "com.foxdebug.acode.rk.auth": "file:src/plugins/auth", + "com.foxdebug.acode.rk.customtabs": "file:src/plugins/custom-tabs", "com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot", "com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal", "cordova-android": "^14.0.1", @@ -570,6 +572,10 @@ "colorette": ["colorette@2.0.19", "", {}, ""], + "com.foxdebug.acode.rk.auth": ["com.foxdebug.acode.rk.auth@file:src/plugins/auth", {}], + + "com.foxdebug.acode.rk.customtabs": ["com.foxdebug.acode.rk.customtabs@file:src/plugins/custom-tabs", {}], + "com.foxdebug.acode.rk.exec.proot": ["com.foxdebug.acode.rk.exec.proot@file:src/plugins/proot", {}], "com.foxdebug.acode.rk.exec.terminal": ["com.foxdebug.acode.rk.exec.terminal@file:src/plugins/terminal", {}], @@ -1344,8 +1350,6 @@ "cacache/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], - "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "cordova/cordova-common": ["cordova-common@6.0.0", "", { "dependencies": { "@netflix/nerror": "^1.1.3", "ansi": "^0.3.1", "bplist-parser": "^0.3.2", "elementtree": "^0.1.7", "endent": "^2.1.0", "fast-glob": "^3.3.3", "plist": "^3.1.0" } }, "sha512-16WPC1DuxVdshV3RoQUXqhcJVdhxWGwiFysA4TkYuboqoev6mgt0JuIJFxmQbzR/DuyuONaVe0L0O0Hf1C08Mg=="], "cordova/nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": "bin/nopt.js" }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], @@ -1360,8 +1364,6 @@ "cordova-fetch/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "cordova-fetch/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - "cordova-lib/cordova-common": ["cordova-common@6.0.0", "", { "dependencies": { "@netflix/nerror": "^1.1.3", "ansi": "^0.3.1", "bplist-parser": "^0.3.2", "elementtree": "^0.1.7", "endent": "^2.1.0", "fast-glob": "^3.3.3", "plist": "^3.1.0" } }, "sha512-16WPC1DuxVdshV3RoQUXqhcJVdhxWGwiFysA4TkYuboqoev6mgt0JuIJFxmQbzR/DuyuONaVe0L0O0Hf1C08Mg=="], "cordova-lib/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -1426,8 +1428,6 @@ "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, ""], - "string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -1436,26 +1436,12 @@ "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "@npmcli/arborist/nopt/abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], - "@npmcli/git/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - - "@npmcli/promise-spawn/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - - "@npmcli/run-script/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - "@tufjs/models/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "bin-links/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "cliui/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "cordova-fetch/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - "cordova-lib/write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "cordova/nopt/abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], @@ -1464,8 +1450,6 @@ "css-loader/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, ""], - "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "htmlparser2/readable-stream/isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], "htmlparser2/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], @@ -1478,8 +1462,6 @@ "node-gyp/nopt/abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], - "node-gyp/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, ""], "postcss-loader/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, ""], @@ -1488,19 +1470,13 @@ "raw-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, ""], - "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, ""], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "css-loader/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, ""], + "css-loader/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, ""], - "postcss-loader/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, ""], + "postcss-loader/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "raw-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""], diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index 8f3d82111..07225370c 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -610,26 +610,43 @@ export default class EditorFile { } const protocol = Url.getProtocol(this.#uri); - let fs; + const text = this.session.getValue(); + if (/s?ftp:/.test(protocol)) { // if file is a ftp or sftp file, get file content from cached file. // remove ':' from protocol because cache file of remote files are // stored as ftp102525465N i.e. protocol + id + // Cache files are local file:// URIs, so native file reading works const cacheFilename = protocol.slice(0, -1) + this.id; - const cacheFile = Url.join(CACHE_STORAGE, cacheFilename); - fs = fsOperation(cacheFile); - } else { - fs = fsOperation(this.uri); + const cacheFileUri = Url.join(CACHE_STORAGE, cacheFilename); + + try { + return await system.compareFileText(cacheFileUri, this.encoding, text); + } catch (error) { + console.error("Native compareFileText failed:", error); + return false; + } + } + + if (/^(file|content):/.test(protocol)) { + // file:// and content:// URIs can be handled by native Android code + // Native reads file AND compares in background thread + try { + return await system.compareFileText(this.uri, this.encoding, text); + } catch (error) { + console.error("Native compareFileText failed:", error); + return false; + } } try { + const fs = fsOperation(this.uri); const oldText = await fs.readFile(this.encoding); - const text = this.session.getValue(); - if (oldText.length !== text.length) return true; - return oldText !== text; + // Offload string comparison to background thread + return await system.compareTexts(oldText, text); } catch (error) { - window.log("error", error); + console.error(error); return false; } } diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java index 517550fc4..520653b2c 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -78,6 +78,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; import android.os.Build; import android.os.Environment; @@ -169,6 +171,8 @@ public boolean execute( case "decode": case "encode": case "copyToUri": + case "compare-file-text": + case "compare-texts": break; case "get-configuration": getConfiguration(callbackContext); @@ -505,6 +509,12 @@ public void run() { case "encode": encode(arg1, arg2, callbackContext); break; + case "compare-file-text": + compareFileText(arg1, arg2, arg3, callbackContext); + break; + case "compare-texts": + compareTexts(arg1, arg2, callbackContext); + break; default: break; } @@ -616,6 +626,146 @@ private void encode( } } + /** + * Compares file content with provided text. + * This method runs in a background thread to avoid blocking the UI. + * + * @param fileUri The URI of the file to read (file:// or content://) + * @param encoding The character encoding to use when reading the file + * @param currentText The text to compare against the file content + * @param callback Returns 1 if texts are different, 0 if same + */ + private void compareFileText( + String fileUri, + String encoding, + String currentText, + CallbackContext callback + ) { + try { + if (fileUri == null || fileUri.isEmpty()) { + callback.error("File URI is required"); + return; + } + + if (encoding == null || encoding.isEmpty()) { + encoding = "UTF-8"; + } + + if (!Charset.isSupported(encoding)) { + callback.error("Charset not supported: " + encoding); + return; + } + + Uri uri = Uri.parse(fileUri); + Charset charset = Charset.forName(encoding); + String fileContent; + + // Handle file:// URIs + if ("file".equalsIgnoreCase(uri.getScheme())) { + File file = new File(uri.getPath()); + + // Validate file + if (!file.exists()) { + callback.error("File does not exist"); + return; + } + if (!file.isFile()) { + callback.error("Path is not a file"); + return; + } + if (!file.canRead()) { + callback.error("File is not readable"); + return; + } + + Path path = file.toPath(); + fileContent = new String(Files.readAllBytes(path), charset); + + } else { + // Handle content:// URIs + InputStream inputStream = null; + try { + inputStream = context.getContentResolver().openInputStream(uri); + + if (inputStream == null) { + callback.error("Cannot open file"); + return; + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, charset))) { + char[] buffer = new char[8192]; + int charsRead; + while ((charsRead = reader.read(buffer)) != -1) { + sb.append(buffer, 0, charsRead); + } + } + fileContent = sb.toString(); + + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + } + } + + // check length first + if (fileContent.length() != currentText.length()) { + callback.success(1); // Changed + return; + } + + // Full comparison + if (fileContent.equals(currentText)) { + callback.success(0); // Not changed + } else { + callback.success(1); // Changed + } + + } catch (Exception e) { + callback.error(e.toString()); + } + } + + /** + * Compares two text strings. + * This method runs in a background thread to avoid blocking the UI + * for large string comparisons. + * + * @param text1 First text to compare + * @param text2 Second text to compare + * @param callback Returns 1 if texts are different, 0 if same + */ + private void compareTexts( + String text1, + String text2, + CallbackContext callback + ) { + try { + if (text1 == null) text1 = ""; + if (text2 == null) text2 = ""; + + // check length first + if (text1.length() != text2.length()) { + callback.success(1); // Changed + return; + } + + // Full comparison + if (text1.equals(text2)) { + callback.success(0); // Not changed + } else { + callback.success(1); // Changed + } + + } catch (Exception e) { + callback.error(e.toString()); + } + } + private void getAvailableEncodings(CallbackContext callback) { try { Map < String, Charset > charsets = Charset.availableCharsets(); diff --git a/src/plugins/system/www/plugin.js b/src/plugins/system/www/plugin.js index d63af6ddc..7831b3b09 100644 --- a/src/plugins/system/www/plugin.js +++ b/src/plugins/system/www/plugin.js @@ -155,5 +155,44 @@ module.exports = { }, getGlobalSetting: function (key, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'get-global-setting', [key]); + }, + /** + * Compare file content with provided text in a background thread. + * @param {string} fileUri - The URI of the file to read + * @param {string} encoding - The character encoding to use + * @param {string} currentText - The text to compare against + * @returns {Promise} - Resolves to true if content differs, false if same + */ + compareFileText: function (fileUri, encoding, currentText) { + return new Promise((resolve, reject) => { + cordova.exec( + function(result) { + resolve(result === 1); + }, + reject, + 'System', + 'compare-file-text', + [fileUri, encoding, currentText] + ); + }); + }, + /** + * Compare two text strings in a background thread. + * @param {string} text1 - First text to compare + * @param {string} text2 - Second text to compare + * @returns {Promise} - Resolves to true if texts differ, false if same + */ + compareTexts: function (text1, text2) { + return new Promise((resolve, reject) => { + cordova.exec( + function(result) { + resolve(result === 1); + }, + reject, + 'System', + 'compare-texts', + [text1, text2] + ); + }); } }; \ No newline at end of file