From f7840b4f87fd6fc4671a2d3711b93e8f1c5cbd4b Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Mon, 24 Aug 2026 14:19:46 +0530 Subject: [PATCH 1/2] fix: admob issues --- .babelrc | 2 +- package-lock.json | 6 +- package.json | 9 +- src/components/settingsPage.scss | 5 + src/lib/adConsentCoordinator.mjs | 10 +- src/lib/bannerVisibilityController.mjs | 99 ++++++- src/lib/startAd.js | 4 +- .../admob/src/android/cordova/Privacy.kt | 24 +- src/settings/mainSettings.js | 11 +- src/sidebarApps/extensions/index.js | 21 +- tests/admob/adConsentCoordinator.test.js | 73 +++++- .../admob/bannerVisibilityController.test.js | 202 +++++++++++++- utils/scripts/android.sh | 248 ++++++++++++++++++ 13 files changed, 677 insertions(+), 37 deletions(-) create mode 100755 utils/scripts/android.sh diff --git a/.babelrc b/.babelrc index e5efe292f..d50502316 100644 --- a/.babelrc +++ b/.babelrc @@ -15,6 +15,6 @@ "@babel/plugin-transform-runtime", "@babel/plugin-transform-block-scoping" ], - "compact": false, + "compact": true, "sourceMaps": "inline" } diff --git a/package-lock.json b/package-lock.json index 4cdb90b9e..468e8267a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6136,9 +6136,9 @@ "peer": true }, "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", "license": "MIT", "peer": true, "dependencies": { diff --git a/package.json b/package.json index f34c2faf1..7f2e891f8 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "description": "Acode is a code editor for android", "scripts": { "lang": "node ./utils/lang.js", - "build": "sh utils/scripts/build.sh", + "build": "bash utils/scripts/android.sh build", "build:admob": "npm ci --prefix src/plugins/admob --no-audit --no-fund && npm run build --prefix src/plugins/admob", - "start": "sh utils/scripts/start.sh", - "dev": "node utils/scripts/dev.js", - "dev:android": "node utils/scripts/dev.js android", + "start": "bash utils/scripts/android.sh start", + "dev": "bash utils/scripts/android.sh dev", + "dev:android": "bash utils/scripts/android.sh dev android", + "android:check": "bash utils/scripts/android.sh check", "clean": "sh utils/scripts/clean.sh android android", "plugin": "sh utils/scripts/plugin.sh", "setup": "node ./utils/setup.js", diff --git a/src/components/settingsPage.scss b/src/components/settingsPage.scss index bfe722e73..e0e9f34b5 100644 --- a/src/components/settingsPage.scss +++ b/src/components/settingsPage.scss @@ -60,6 +60,11 @@ wc-page.main-settings-page { .main-settings-list>.list-item, .settings-section-card>.list-item { display: flex; + + &[hidden] { + display: none; + } + width: 100%; min-height: 64px; margin: 0; diff --git a/src/lib/adConsentCoordinator.mjs b/src/lib/adConsentCoordinator.mjs index 5a23de173..4391e22aa 100644 --- a/src/lib/adConsentCoordinator.mjs +++ b/src/lib/adConsentCoordinator.mjs @@ -35,6 +35,7 @@ export class AdConsentCoordinator { #state = EMPTY_PRIVACY_STATE; #consentPromise; #adStartPromise; + #privacyOptionsPromise; #listeners = new Set(); constructor({ privacy, initializeAds, onError = console.error }) { @@ -51,7 +52,14 @@ export class AdConsentCoordinator { return (this.#consentPromise ??= this.#gatherAndStart()); } - async showPrivacyOptions() { + showPrivacyOptions() { + this.#privacyOptionsPromise ??= this.#showPrivacyOptions().finally(() => { + this.#privacyOptionsPromise = undefined; + }); + return this.#privacyOptionsPromise; + } + + async #showPrivacyOptions() { const state = await this.#privacy.showOptions(); this.#setState(state); await this.#startAdsIfAllowed(); diff --git a/src/lib/bannerVisibilityController.mjs b/src/lib/bannerVisibilityController.mjs index c02df5b80..517e914d0 100644 --- a/src/lib/bannerVisibilityController.mjs +++ b/src/lib/bannerVisibilityController.mjs @@ -10,6 +10,9 @@ export const BANNER_SUPPRESSION_REASON = Object.freeze({ REWARDED_PASS: "rewarded-pass", }); +const DEFAULT_RETRY_DELAY_MS = 500; +const TRANSIENT_LOAD_ERROR_CODES = new Set([0, 2, 3, 9]); + export class BannerVisibilityController { #banner = null; #registeredPages = new WeakSet(); @@ -20,9 +23,13 @@ export class BannerVisibilityController { #suppressions = new Set(); #nativeVisible = false; #scheduledVisible = false; + #desiredVisible = false; #operation = Promise.resolve(); #revision = 0; #onError; + #retryTimer = null; + #retryUsed = false; + #stopBannerListeners = []; constructor({ getActivePage, @@ -35,9 +42,13 @@ export class BannerVisibilityController { } setBanner(banner) { + this.#stopListeningToBanner(); + this.#resetRetry(); + this.#revision++; this.#banner = banner; this.#nativeVisible = false; this.#scheduledVisible = false; + this.#listenToBanner(banner); this.reconcile(); } @@ -83,6 +94,12 @@ export class BannerVisibilityController { activePage !== null && this.#registeredPages.has(activePage); const shouldShow = pageRequestsBanner && !this.#keyboardVisible; + const eligibilityChanged = shouldShow !== this.#desiredVisible; + this.#desiredVisible = shouldShow; + + if (!shouldShow || eligibilityChanged) { + this.#resetRetry(); + } if (this.#banner) { this.#banner.active = pageRequestsBanner; @@ -98,7 +115,12 @@ export class BannerVisibilityController { dispose() { this.#stopObserving?.(); this.#stopObserving = null; + this.#stopListeningToBanner(); + this.#resetRetry(); this.#banner = null; + this.#desiredVisible = false; + this.#nativeVisible = false; + this.#scheduledVisible = false; this.#revision++; } @@ -107,9 +129,16 @@ export class BannerVisibilityController { this.#stopObserving = this.#observePageChanges(() => this.reconcile()); } - #queueNativeVisibility(shouldShow) { + #queueNativeVisibility(shouldShow, isRetry = false) { const banner = this.#banner; - if (!banner || shouldShow === this.#scheduledVisible) return; + if ( + !banner || + shouldShow === this.#scheduledVisible || + (shouldShow && + (this.#retryTimer !== null || (this.#retryUsed && !isRetry))) + ) { + return; + } this.#scheduledVisible = shouldShow; const revision = ++this.#revision; @@ -124,14 +153,80 @@ export class BannerVisibilityController { } else { await banner.hide?.(); } + if (revision !== this.#revision || banner !== this.#banner) return; this.#nativeVisible = shouldShow; } catch (error) { + if (revision !== this.#revision || banner !== this.#banner) return; this.#nativeVisible = !shouldShow; this.#scheduledVisible = !shouldShow; this.#onError(error); + if (shouldShow) this.#scheduleRetry(); } }); } + + #listenToBanner(banner) { + if (typeof banner?.on !== "function") return; + + for (const [eventName, listener] of [ + ["load", () => this.#handleBannerLoad()], + ["loadfail", (event) => this.#handleBannerLoadFailure(event)], + ]) { + const stopListening = banner.on(eventName, listener); + if (typeof stopListening === "function") { + this.#stopBannerListeners.push(stopListening); + } + } + } + + #stopListeningToBanner() { + for (const stopListening of this.#stopBannerListeners.splice(0)) { + stopListening(); + } + } + + #handleBannerLoad() { + this.#resetRetry(); + if (!this.#desiredVisible) return; + + this.#nativeVisible = true; + this.#scheduledVisible = true; + } + + #handleBannerLoadFailure(event) { + this.#nativeVisible = false; + this.#scheduledVisible = false; + this.#onError(event); + + const errorCode = Number(event?.code); + if (TRANSIENT_LOAD_ERROR_CODES.has(errorCode)) { + this.#scheduleRetry(); + } else { + this.#retryUsed = true; + } + } + + #scheduleRetry() { + if (!this.#desiredVisible || this.#retryTimer !== null || this.#retryUsed) { + return; + } + + this.#retryUsed = true; + const banner = this.#banner; + this.#retryTimer = setTimeout(() => { + this.#retryTimer = null; + if (!this.#desiredVisible || banner !== this.#banner) return; + this.#queueNativeVisibility(true, true); + }, DEFAULT_RETRY_DELAY_MS); + } + + #resetRetry() { + if (this.#retryTimer !== null) { + clearTimeout(this.#retryTimer); + this.#retryTimer = null; + } + this.#retryUsed = false; + } } function getActivePage() { diff --git a/src/lib/startAd.js b/src/lib/startAd.js index a9dd6a74d..757dc6942 100644 --- a/src/lib/startAd.js +++ b/src/lib/startAd.js @@ -63,7 +63,9 @@ export function subscribePrivacyState(listener) { } export async function showPrivacyOptions() { - if (!canUseAdmob()) return getPrivacyState(); + if (!canUseAdmob()) { + throw new Error("AdMob Privacy Choices are unavailable."); + } return getConsentCoordinator().showPrivacyOptions(); } diff --git a/src/plugins/admob/src/android/cordova/Privacy.kt b/src/plugins/admob/src/android/cordova/Privacy.kt index 0d34ccf3c..b2c2264a5 100644 --- a/src/plugins/admob/src/android/cordova/Privacy.kt +++ b/src/plugins/admob/src/android/cordova/Privacy.kt @@ -80,12 +80,26 @@ internal class Privacy(private val plugin: AdMob) { fun showOptions(ctx: ExecuteContext) { plugin.activity.runOnUiThread { - UserMessagingPlatform.showPrivacyOptionsForm(plugin.activity) { formError -> - if (formError != null) { - ctx.reject("UMP ${formError.errorCode}: ${formError.message}") - } else { - ctx.resolve(currentState().toMap()) + val activity = plugin.activity + val state = currentState() + if (!state.privacyOptionsRequired) { + return@runOnUiThread ctx.resolve(state.toMap()) + } + + if (activity.isFinishing || activity.isDestroyed) { + return@runOnUiThread ctx.reject("Privacy Choices are temporarily unavailable") + } + + try { + UserMessagingPlatform.showPrivacyOptionsForm(activity) { formError -> + if (formError != null) { + ctx.reject("UMP ${formError.errorCode}: ${formError.message}") + } else { + ctx.resolve(currentState().toMap()) + } } + } catch (error: Exception) { + ctx.reject(error.message ?: "Unable to open Privacy Choices") } } } diff --git a/src/settings/mainSettings.js b/src/settings/mainSettings.js index 2946f57c7..7ce6cf196 100644 --- a/src/settings/mainSettings.js +++ b/src/settings/mainSettings.js @@ -251,10 +251,19 @@ export default function mainSettings() { break; case "privacyChoices": + loader.create( + strings["privacy choices"] || "Privacy Choices", + strings["loading..."] || "Loading...", + ); try { await showPrivacyOptions(); } catch (error) { - helpers.error(error); + console.warn("Unable to open AdMob Privacy Choices:", error); + helpers.error( + "Unable to open Privacy Choices. Check your connection and try again.", + ); + } finally { + loader.destroy(); } break; diff --git a/src/sidebarApps/extensions/index.js b/src/sidebarApps/extensions/index.js index e83360dcf..72ed4378e 100644 --- a/src/sidebarApps/extensions/index.js +++ b/src/sidebarApps/extensions/index.js @@ -836,11 +836,14 @@ function ListItem({ }); const { default: installPlugin } = await import("lib/installPlugin"); - await installPlugin( - id, - remotePlugin.name, - purchaseToken ? purchaseToken : undefined, - ); + await Promise.all([ + loadAd(), + installPlugin( + id, + remotePlugin.name, + purchaseToken ? purchaseToken : undefined, + ), + ]); const searchInput = container.querySelector('input[name="search-ext"]'); if (searchInput) { @@ -862,6 +865,7 @@ function ListItem({ if (!$installed.collapsed) { $installed.ontoggle(); } + await helpers.showInterstitialIfReady(); async function getPurchase(sku) { const purchases = await helpers.promisify(iap.getPurchases); @@ -910,14 +914,11 @@ function ListItem({ return $el; } -async function loadAd(el) { +async function loadAd() { if (!helpers.canShowAds()) return; try { if (!(await interstitialAd?.isLoaded())) { - const oldText = el.textContent; - el.textContent = strings["loading..."]; await interstitialAd?.load(); - el.textContent = oldText; } } catch (error) { console.error(error); @@ -929,7 +930,7 @@ async function uninstall(id) { const pluginDir = Url.join(PLUGIN_DIR, id); const state = await InstallState.new(id); await Promise.all([ - loadAd(this), + loadAd(), fsOperation(pluginDir).delete(), state.delete(state.storeUrl), ]); diff --git a/tests/admob/adConsentCoordinator.test.js b/tests/admob/adConsentCoordinator.test.js index cb4fe07c7..ecd5a2f7f 100644 --- a/tests/admob/adConsentCoordinator.test.js +++ b/tests/admob/adConsentCoordinator.test.js @@ -9,6 +9,7 @@ function createHarness({ gatherState, previousState = EMPTY_PRIVACY_STATE, gatherError, + showOptions, }) { let gatherCalls = 0; let getStateCalls = 0; @@ -25,6 +26,7 @@ function createHarness({ return previousState; }, async showOptions() { + if (showOptions) return showOptions(); return gatherState; }, }; @@ -141,12 +143,12 @@ test("deduplicates consent collection and ad initialization", async () => { assert.equal(harness.initializeCalls, 1); }); -test("updates privacy-option visibility and keeps ads single-started", async () => { +test("hides a stale privacy-option entry and keeps ads single-started", async () => { const harness = createHarness({ gatherState: { - consentStatus: "notRequired", + consentStatus: "obtained", canRequestAds: true, - privacyOptionsRequired: false, + privacyOptionsRequired: true, }, }); const states = []; @@ -154,15 +156,15 @@ test("updates privacy-option visibility and keeps ads single-started", async () await harness.coordinator.start(); harness.privacy.showOptions = async () => ({ - consentStatus: "obtained", + consentStatus: "notRequired", canRequestAds: true, - privacyOptionsRequired: true, + privacyOptionsRequired: false, }); await harness.coordinator.showPrivacyOptions(); unsubscribe(); assert.equal(harness.initializeCalls, 1); - assert.equal(states.at(-1).privacyOptionsRequired, true); + assert.equal(states.at(-1).privacyOptionsRequired, false); }); test("normalizes an invalid native state instead of starting ads", async () => { @@ -177,3 +179,62 @@ test("normalizes an invalid native state instead of starting ads", async () => { assert.deepEqual(await harness.coordinator.start(), EMPTY_PRIVACY_STATE); assert.equal(harness.initializeCalls, 0); }); + +test("deduplicates concurrent privacy-option requests", async () => { + let resolveOptions; + let showOptionsCalls = 0; + const optionsResult = new Promise((resolve) => { + resolveOptions = resolve; + }); + const harness = createHarness({ + gatherState: { + consentStatus: "obtained", + canRequestAds: true, + privacyOptionsRequired: true, + }, + showOptions() { + showOptionsCalls++; + return optionsResult; + }, + }); + await harness.coordinator.start(); + + const firstRequest = harness.coordinator.showPrivacyOptions(); + const secondRequest = harness.coordinator.showPrivacyOptions(); + assert.equal(firstRequest, secondRequest); + resolveOptions({ + consentStatus: "obtained", + canRequestAds: true, + privacyOptionsRequired: false, + }); + + assert.deepEqual(await firstRequest, { + consentStatus: "obtained", + canRequestAds: true, + privacyOptionsRequired: false, + }); + assert.equal(showOptionsCalls, 1); +}); + +test("preserves consent state when privacy options reject", async () => { + const gatherState = { + consentStatus: "obtained", + canRequestAds: true, + privacyOptionsRequired: true, + }; + const optionsError = new Error("native failure"); + const harness = createHarness({ + gatherState, + showOptions: async () => { + throw optionsError; + }, + }); + await harness.coordinator.start(); + + await assert.rejects( + harness.coordinator.showPrivacyOptions(), + (error) => error === optionsError, + ); + assert.deepEqual(harness.coordinator.state, gatherState); + assert.equal(harness.initializeCalls, 1); +}); diff --git a/tests/admob/bannerVisibilityController.test.js b/tests/admob/bannerVisibilityController.test.js index cb9d52c34..1ad63d396 100644 --- a/tests/admob/bannerVisibilityController.test.js +++ b/tests/admob/bannerVisibilityController.test.js @@ -1,21 +1,34 @@ import assert from "node:assert/strict"; -import { test } from "vitest"; +import { afterEach, test, vi } from "vitest"; import { BANNER_SUPPRESSION_REASON, BannerVisibilityController, } from "../../src/lib/bannerVisibilityController.mjs"; -function createHarness() { +function createHarness({ show, hide } = {}) { let activePage = null; let notifyPageChange = () => {}; const calls = []; + const errors = []; + const listeners = new Map(); const banner = { active: false, async show() { calls.push("show"); + return show?.(); }, async hide() { calls.push("hide"); + return hide?.(); + }, + on(eventName, listener) { + let eventListeners = listeners.get(eventName); + if (!eventListeners) { + eventListeners = new Set(); + listeners.set(eventName, eventListeners); + } + eventListeners.add(listener); + return () => eventListeners.delete(listener); }, }; const controller = new BannerVisibilityController({ @@ -27,7 +40,7 @@ function createHarness() { }; }, onError(error) { - throw error; + errors.push(error); }, }); controller.setBanner(banner); @@ -36,6 +49,10 @@ function createHarness() { banner, calls, controller, + emit(eventName, event = {}) { + for (const listener of listeners.get(eventName) ?? []) listener(event); + }, + errors, changePage(page) { activePage = page; notifyPageChange(); @@ -46,6 +63,19 @@ function createHarness() { }; } +async function createEligibleHarness(options) { + const harness = createHarness(options); + const page = {}; + harness.setActivePage(page); + harness.controller.registerPage(page); + await harness.controller.whenIdle(); + return { harness, page }; +} + +afterEach(() => { + vi.useRealTimers(); +}); + test("shows only for a registered active page and ignores repeated syncs", async () => { const harness = createHarness(); const page = {}; @@ -234,3 +264,169 @@ test("serializes an in-flight show before the latest hide request", async () => assert.equal(banner.active, false); assert.deepEqual(calls, ["show", "hide"]); }); + +test("retries one transient failure after 500ms and caps requests at two", async () => { + vi.useFakeTimers(); + const { harness } = await createEligibleHarness(); + + harness.emit("loadfail", { code: 3 }); + harness.emit("loadfail", { code: 3 }); + await vi.advanceTimersByTimeAsync(499); + assert.deepEqual(harness.calls, ["show"]); + await vi.advanceTimersByTimeAsync(1); + await harness.controller.whenIdle(); + + harness.emit("loadfail", { code: 3 }); + harness.controller.reconcile(); + await vi.advanceTimersByTimeAsync(500); + await harness.controller.whenIdle(); + + assert.deepEqual(harness.calls, ["show", "show"]); + assert.equal(harness.errors.length, 3); +}); + +test("successful load restores the retry allowance", async () => { + vi.useFakeTimers(); + const { harness } = await createEligibleHarness(); + + harness.emit("loadfail", { code: 3 }); + await vi.advanceTimersByTimeAsync(500); + await harness.controller.whenIdle(); + harness.emit("load"); + harness.emit("loadfail", { code: 3 }); + await vi.advanceTimersByTimeAsync(500); + await harness.controller.whenIdle(); + + assert.deepEqual(harness.calls, ["show", "show", "show"]); +}); + +test("cancels a retry on navigation and restores it when the page returns", async () => { + vi.useFakeTimers(); + const { harness, page } = await createEligibleHarness(); + + harness.emit("loadfail", { code: 3 }); + harness.changePage({}); + await harness.controller.whenIdle(); + await vi.advanceTimersByTimeAsync(500); + assert.deepEqual(harness.calls, ["show"]); + + harness.changePage(page); + await harness.controller.whenIdle(); + harness.emit("loadfail", { code: 3 }); + await vi.advanceTimersByTimeAsync(500); + await harness.controller.whenIdle(); + + assert.deepEqual(harness.calls, ["show", "show", "show"]); +}); + +test("does not retry non-transient load failures", async () => { + vi.useFakeTimers(); + const { harness } = await createEligibleHarness(); + + harness.emit("loadfail", { code: 1 }); + harness.controller.reconcile(); + await vi.advanceTimersByTimeAsync(500); + await harness.controller.whenIdle(); + + assert.deepEqual(harness.calls, ["show"]); +}); + +test("retries a rejected native show call", async () => { + vi.useFakeTimers(); + let showCalls = 0; + const { harness } = await createEligibleHarness({ + show() { + showCalls++; + if (showCalls === 1) throw new Error("bridge failure"); + }, + }); + + await vi.advanceTimersByTimeAsync(500); + await harness.controller.whenIdle(); + + assert.deepEqual(harness.calls, ["show", "show"]); + assert.equal(harness.errors.length, 1); +}); + +test("keeps a rejected hide operation eligible for reconciliation", async () => { + let hideCalls = 0; + const { harness } = await createEligibleHarness({ + hide() { + hideCalls++; + if (hideCalls === 1) throw new Error("bridge failure"); + }, + }); + + harness.controller.setKeyboardVisible(true); + await harness.controller.whenIdle(); + harness.controller.reconcile(); + await harness.controller.whenIdle(); + + assert.deepEqual(harness.calls, ["show", "hide", "hide"]); + assert.equal(harness.errors.length, 1); +}); + +test("replacement and disposal cancel retries and detach listeners", async () => { + vi.useFakeTimers(); + const { harness } = await createEligibleHarness(); + harness.emit("loadfail", { code: 3 }); + + const replacementCalls = []; + const replacementListeners = new Map(); + harness.controller.setBanner({ + active: false, + async hide() { + replacementCalls.push("hide"); + }, + on(eventName, listener) { + replacementListeners.set(eventName, listener); + return () => replacementListeners.delete(eventName); + }, + async show() { + replacementCalls.push("show"); + }, + }); + await harness.controller.whenIdle(); + const errorsBeforeStaleEvent = harness.errors.length; + harness.emit("loadfail", { code: 3 }); + replacementListeners.get("loadfail")({ code: 3 }); + harness.controller.dispose(); + replacementListeners.get("loadfail")?.({ code: 3 }); + await vi.advanceTimersByTimeAsync(500); + + assert.deepEqual(replacementCalls, ["show"]); + assert.equal(harness.errors.length, errorsBeforeStaleEvent + 1); +}); + +test("ignores a stale show rejection after replacing the banner", async () => { + let rejectOldShow; + const harness = createHarness({ + show: () => + new Promise((_, reject) => { + rejectOldShow = reject; + }), + }); + const page = {}; + harness.setActivePage(page); + harness.controller.registerPage(page); + await new Promise((resolve) => setImmediate(resolve)); + + const replacementCalls = []; + harness.controller.setBanner({ + active: false, + async hide() { + replacementCalls.push("hide"); + }, + on() { + return () => {}; + }, + async show() { + replacementCalls.push("show"); + }, + }); + rejectOldShow(new Error("stale bridge failure")); + await harness.controller.whenIdle(); + + assert.deepEqual(replacementCalls, ["show"]); + assert.deepEqual(harness.errors, []); +}); diff --git a/utils/scripts/android.sh b/utils/scripts/android.sh new file mode 100755 index 000000000..547c5ed21 --- /dev/null +++ b/utils/scripts/android.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ACTION="${1:-}" + +if [ -z "$ACTION" ]; then + echo "Usage: $0 {build|start|dev|check} [arguments...]" >&2 + exit 2 +fi +shift + +COMMAND=() +NEEDS_ANDROID=true + +case "$ACTION" in + build) + COMMAND=(bash "$ROOT/utils/scripts/build.sh" "$@") + ;; + start) + COMMAND=(bash "$ROOT/utils/scripts/start.sh" "$@") + case "${1:-android}" in + android) ;; + *) NEEDS_ANDROID=false ;; + esac + ;; + dev) + COMMAND=(node "$ROOT/utils/scripts/dev.js" "$@") + for argument in "$@"; do + case "$argument" in + android) break ;; + ios|browser) + NEEDS_ANDROID=false + break + ;; + esac + done + ;; + check) + ;; + *) + echo "Unknown Android action: $ACTION" >&2 + exit 2 + ;; +esac + +if [ "$NEEDS_ANDROID" = false ]; then + exec "${COMMAND[@]}" +fi + +is_java_17() { + local java_home="$1" + [ -x "$java_home/bin/java" ] && + "$java_home/bin/java" -version 2>&1 | head -n 1 | grep -Eq 'version "17([.]|"|$)' +} + +find_java_17() { + local candidate + + if [ -n "${CORDOVA_JAVA_HOME:-}" ]; then + if is_java_17 "$CORDOVA_JAVA_HOME"; then + printf '%s\n' "$CORDOVA_JAVA_HOME" + return + fi + echo "CORDOVA_JAVA_HOME must point to JDK 17: $CORDOVA_JAVA_HOME" >&2 + exit 1 + fi + + if [ -n "${JAVA_HOME:-}" ] && is_java_17 "$JAVA_HOME"; then + printf '%s\n' "$JAVA_HOME" + return + fi + + if [ -x /usr/libexec/java_home ]; then + candidate="$(/usr/libexec/java_home -v 17 2>/dev/null || true)" + if [ -n "$candidate" ] && is_java_17 "$candidate"; then + printf '%s\n' "$candidate" + return + fi + fi + + if [ -n "${HOME:-}" ] && [ -d "$HOME/.sdkman/candidates/java" ]; then + while IFS= read -r candidate; do + if is_java_17 "$candidate"; then + printf '%s\n' "$candidate" + return + fi + done < <(find "$HOME/.sdkman/candidates/java" -maxdepth 1 -type d -name '17*' -print | sort -r) + fi + + if [ -d /Library/Java/JavaVirtualMachines ]; then + for candidate in /Library/Java/JavaVirtualMachines/*/Contents/Home; do + if is_java_17 "$candidate"; then + printf '%s\n' "$candidate" + return + fi + done + fi + + echo "JDK 17 was not found. Install it or set CORDOVA_JAVA_HOME to its home directory." >&2 + exit 1 +} + +find_android_home() { + local candidate + + if [ -n "${ANDROID_HOME:-}" ]; then + if [ -d "$ANDROID_HOME" ]; then + printf '%s\n' "$ANDROID_HOME" + return + fi + echo "ANDROID_HOME points to a missing directory: $ANDROID_HOME" >&2 + exit 1 + fi + + if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT" ]; then + printf '%s\n' "$ANDROID_SDK_ROOT" + return + fi + + if [ -n "${HOME:-}" ]; then + for candidate in \ + "$HOME/Library/Android/sdk" \ + "$HOME/Android/Sdk" \ + "$HOME/Development/android"; do + if [ -d "$candidate" ]; then + printf '%s\n' "$candidate" + return + fi + done + fi + + echo "Android SDK was not found. Set ANDROID_HOME to the SDK directory." >&2 + exit 1 +} + +checksum() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + echo "A SHA-256 utility (shasum or sha256sum) is required." >&2 + return 1 + fi +} + +JAVA_17_HOME="$(find_java_17)" +ANDROID_SDK_HOME="$(find_android_home)" + +export JAVA_HOME="$JAVA_17_HOME" +export CORDOVA_JAVA_HOME="$JAVA_17_HOME" +export ANDROID_HOME="$ANDROID_SDK_HOME" +unset ANDROID_SDK_ROOT +export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:${PATH:-}" + +if ! command -v gradle >/dev/null 2>&1; then + echo "Gradle is required to generate Cordova's wrapper. Install Gradle and retry." >&2 + exit 1 +fi + +CORDOVA_DEFAULTS="$ROOT/node_modules/cordova-android/lib/gradle-config-defaults.js" +if [ ! -f "$CORDOVA_DEFAULTS" ]; then + echo "cordova-android is not installed. Run npm install and retry." >&2 + exit 1 +fi + +GRADLE_VERSION="$(CORDOVA_DEFAULTS="$CORDOVA_DEFAULTS" node -e 'console.log(require(process.env.CORDOVA_DEFAULTS).GRADLE_VERSION)')" +case "$GRADLE_VERSION" in + 8.14.2) + GRADLE_SHA256="7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999" + ;; + *) + echo "Unsupported Cordova Gradle version: $GRADLE_VERSION" >&2 + echo "Update utils/scripts/android.sh with the official checksum before building." >&2 + exit 1 + ;; +esac + +if [ -n "${GRADLE_USER_HOME:-}" ]; then + GRADLE_CACHE_ROOT="$GRADLE_USER_HOME" +elif [ -n "${HOME:-}" ]; then + GRADLE_CACHE_ROOT="$HOME/.gradle" +else + echo "HOME or GRADLE_USER_HOME is required for the Gradle distribution cache." >&2 + exit 1 +fi + +GRADLE_CACHE_DIR="$GRADLE_CACHE_ROOT/acode/distributions" +GRADLE_ZIP="$GRADLE_CACHE_DIR/gradle-$GRADLE_VERSION-bin.zip" +GRADLE_URL="https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip" +mkdir -p "$GRADLE_CACHE_DIR" + +if [ -f "$GRADLE_ZIP" ] && [ "$(checksum "$GRADLE_ZIP")" != "$GRADLE_SHA256" ]; then + echo "[android] Cached Gradle archive is invalid; downloading a fresh copy." + rm -f "$GRADLE_ZIP" +fi + +if [ ! -f "$GRADLE_ZIP" ]; then + if ! command -v curl >/dev/null 2>&1; then + echo "curl is required for the first Gradle download." >&2 + exit 1 + fi + + PARTIAL_ZIP="$GRADLE_ZIP.part.$$" + trap 'rm -f "$PARTIAL_ZIP"' EXIT INT TERM + echo "[android] Downloading Gradle $GRADLE_VERSION once for reliable local builds..." + curl \ + --fail \ + --location \ + --retry 3 \ + --retry-delay 2 \ + --retry-all-errors \ + --connect-timeout 30 \ + --max-time 600 \ + --progress-bar \ + --output "$PARTIAL_ZIP" \ + "$GRADLE_URL" + + if [ "$(checksum "$PARTIAL_ZIP")" != "$GRADLE_SHA256" ]; then + echo "Gradle $GRADLE_VERSION checksum verification failed." >&2 + exit 1 + fi + + mv "$PARTIAL_ZIP" "$GRADLE_ZIP" + trap - EXIT INT TERM +else + echo "[android] Using verified Gradle $GRADLE_VERSION cache." +fi + +export CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL +CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL="$(node -e 'console.log(require("node:url").pathToFileURL(process.argv[1]).href)' "$GRADLE_ZIP")" + +echo "[android] JAVA_HOME=$JAVA_HOME" +echo "[android] ANDROID_HOME=$ANDROID_HOME" + +if [ "$ACTION" = check ]; then + TOOLS_DIR="$ROOT/platforms/android/tools" + if [ ! -d "$TOOLS_DIR" ]; then + echo "Cordova Android platform is missing. Run npm run clean before checking the wrapper." >&2 + exit 1 + fi + gradle -p "$TOOLS_DIR" wrapper --gradle-distribution-url "$CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL" + exit $? +fi + +exec "${COMMAND[@]}" From df3820039d1999996c7f27d1e5986b18a1e409f2 Mon Sep 17 00:00:00 2001 From: Ajit Kumar Date: Mon, 24 Aug 2026 14:29:38 +0530 Subject: [PATCH 2/2] fix: restore Android build commands --- .babelrc | 2 +- package-lock.json | 6 +- package.json | 9 +- utils/scripts/android.sh | 248 --------------------------------------- 4 files changed, 8 insertions(+), 257 deletions(-) delete mode 100755 utils/scripts/android.sh diff --git a/.babelrc b/.babelrc index d50502316..e5efe292f 100644 --- a/.babelrc +++ b/.babelrc @@ -15,6 +15,6 @@ "@babel/plugin-transform-runtime", "@babel/plugin-transform-block-scoping" ], - "compact": true, + "compact": false, "sourceMaps": "inline" } diff --git a/package-lock.json b/package-lock.json index 468e8267a..4cdb90b9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6136,9 +6136,9 @@ "peer": true }, "node_modules/@types/markdown-it": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", - "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", "peer": true, "dependencies": { diff --git a/package.json b/package.json index 7f2e891f8..f34c2faf1 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,11 @@ "description": "Acode is a code editor for android", "scripts": { "lang": "node ./utils/lang.js", - "build": "bash utils/scripts/android.sh build", + "build": "sh utils/scripts/build.sh", "build:admob": "npm ci --prefix src/plugins/admob --no-audit --no-fund && npm run build --prefix src/plugins/admob", - "start": "bash utils/scripts/android.sh start", - "dev": "bash utils/scripts/android.sh dev", - "dev:android": "bash utils/scripts/android.sh dev android", - "android:check": "bash utils/scripts/android.sh check", + "start": "sh utils/scripts/start.sh", + "dev": "node utils/scripts/dev.js", + "dev:android": "node utils/scripts/dev.js android", "clean": "sh utils/scripts/clean.sh android android", "plugin": "sh utils/scripts/plugin.sh", "setup": "node ./utils/setup.js", diff --git a/utils/scripts/android.sh b/utils/scripts/android.sh deleted file mode 100755 index 547c5ed21..000000000 --- a/utils/scripts/android.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -ACTION="${1:-}" - -if [ -z "$ACTION" ]; then - echo "Usage: $0 {build|start|dev|check} [arguments...]" >&2 - exit 2 -fi -shift - -COMMAND=() -NEEDS_ANDROID=true - -case "$ACTION" in - build) - COMMAND=(bash "$ROOT/utils/scripts/build.sh" "$@") - ;; - start) - COMMAND=(bash "$ROOT/utils/scripts/start.sh" "$@") - case "${1:-android}" in - android) ;; - *) NEEDS_ANDROID=false ;; - esac - ;; - dev) - COMMAND=(node "$ROOT/utils/scripts/dev.js" "$@") - for argument in "$@"; do - case "$argument" in - android) break ;; - ios|browser) - NEEDS_ANDROID=false - break - ;; - esac - done - ;; - check) - ;; - *) - echo "Unknown Android action: $ACTION" >&2 - exit 2 - ;; -esac - -if [ "$NEEDS_ANDROID" = false ]; then - exec "${COMMAND[@]}" -fi - -is_java_17() { - local java_home="$1" - [ -x "$java_home/bin/java" ] && - "$java_home/bin/java" -version 2>&1 | head -n 1 | grep -Eq 'version "17([.]|"|$)' -} - -find_java_17() { - local candidate - - if [ -n "${CORDOVA_JAVA_HOME:-}" ]; then - if is_java_17 "$CORDOVA_JAVA_HOME"; then - printf '%s\n' "$CORDOVA_JAVA_HOME" - return - fi - echo "CORDOVA_JAVA_HOME must point to JDK 17: $CORDOVA_JAVA_HOME" >&2 - exit 1 - fi - - if [ -n "${JAVA_HOME:-}" ] && is_java_17 "$JAVA_HOME"; then - printf '%s\n' "$JAVA_HOME" - return - fi - - if [ -x /usr/libexec/java_home ]; then - candidate="$(/usr/libexec/java_home -v 17 2>/dev/null || true)" - if [ -n "$candidate" ] && is_java_17 "$candidate"; then - printf '%s\n' "$candidate" - return - fi - fi - - if [ -n "${HOME:-}" ] && [ -d "$HOME/.sdkman/candidates/java" ]; then - while IFS= read -r candidate; do - if is_java_17 "$candidate"; then - printf '%s\n' "$candidate" - return - fi - done < <(find "$HOME/.sdkman/candidates/java" -maxdepth 1 -type d -name '17*' -print | sort -r) - fi - - if [ -d /Library/Java/JavaVirtualMachines ]; then - for candidate in /Library/Java/JavaVirtualMachines/*/Contents/Home; do - if is_java_17 "$candidate"; then - printf '%s\n' "$candidate" - return - fi - done - fi - - echo "JDK 17 was not found. Install it or set CORDOVA_JAVA_HOME to its home directory." >&2 - exit 1 -} - -find_android_home() { - local candidate - - if [ -n "${ANDROID_HOME:-}" ]; then - if [ -d "$ANDROID_HOME" ]; then - printf '%s\n' "$ANDROID_HOME" - return - fi - echo "ANDROID_HOME points to a missing directory: $ANDROID_HOME" >&2 - exit 1 - fi - - if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT" ]; then - printf '%s\n' "$ANDROID_SDK_ROOT" - return - fi - - if [ -n "${HOME:-}" ]; then - for candidate in \ - "$HOME/Library/Android/sdk" \ - "$HOME/Android/Sdk" \ - "$HOME/Development/android"; do - if [ -d "$candidate" ]; then - printf '%s\n' "$candidate" - return - fi - done - fi - - echo "Android SDK was not found. Set ANDROID_HOME to the SDK directory." >&2 - exit 1 -} - -checksum() { - if command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$1" | awk '{print $1}' - elif command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | awk '{print $1}' - else - echo "A SHA-256 utility (shasum or sha256sum) is required." >&2 - return 1 - fi -} - -JAVA_17_HOME="$(find_java_17)" -ANDROID_SDK_HOME="$(find_android_home)" - -export JAVA_HOME="$JAVA_17_HOME" -export CORDOVA_JAVA_HOME="$JAVA_17_HOME" -export ANDROID_HOME="$ANDROID_SDK_HOME" -unset ANDROID_SDK_ROOT -export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:${PATH:-}" - -if ! command -v gradle >/dev/null 2>&1; then - echo "Gradle is required to generate Cordova's wrapper. Install Gradle and retry." >&2 - exit 1 -fi - -CORDOVA_DEFAULTS="$ROOT/node_modules/cordova-android/lib/gradle-config-defaults.js" -if [ ! -f "$CORDOVA_DEFAULTS" ]; then - echo "cordova-android is not installed. Run npm install and retry." >&2 - exit 1 -fi - -GRADLE_VERSION="$(CORDOVA_DEFAULTS="$CORDOVA_DEFAULTS" node -e 'console.log(require(process.env.CORDOVA_DEFAULTS).GRADLE_VERSION)')" -case "$GRADLE_VERSION" in - 8.14.2) - GRADLE_SHA256="7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999" - ;; - *) - echo "Unsupported Cordova Gradle version: $GRADLE_VERSION" >&2 - echo "Update utils/scripts/android.sh with the official checksum before building." >&2 - exit 1 - ;; -esac - -if [ -n "${GRADLE_USER_HOME:-}" ]; then - GRADLE_CACHE_ROOT="$GRADLE_USER_HOME" -elif [ -n "${HOME:-}" ]; then - GRADLE_CACHE_ROOT="$HOME/.gradle" -else - echo "HOME or GRADLE_USER_HOME is required for the Gradle distribution cache." >&2 - exit 1 -fi - -GRADLE_CACHE_DIR="$GRADLE_CACHE_ROOT/acode/distributions" -GRADLE_ZIP="$GRADLE_CACHE_DIR/gradle-$GRADLE_VERSION-bin.zip" -GRADLE_URL="https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip" -mkdir -p "$GRADLE_CACHE_DIR" - -if [ -f "$GRADLE_ZIP" ] && [ "$(checksum "$GRADLE_ZIP")" != "$GRADLE_SHA256" ]; then - echo "[android] Cached Gradle archive is invalid; downloading a fresh copy." - rm -f "$GRADLE_ZIP" -fi - -if [ ! -f "$GRADLE_ZIP" ]; then - if ! command -v curl >/dev/null 2>&1; then - echo "curl is required for the first Gradle download." >&2 - exit 1 - fi - - PARTIAL_ZIP="$GRADLE_ZIP.part.$$" - trap 'rm -f "$PARTIAL_ZIP"' EXIT INT TERM - echo "[android] Downloading Gradle $GRADLE_VERSION once for reliable local builds..." - curl \ - --fail \ - --location \ - --retry 3 \ - --retry-delay 2 \ - --retry-all-errors \ - --connect-timeout 30 \ - --max-time 600 \ - --progress-bar \ - --output "$PARTIAL_ZIP" \ - "$GRADLE_URL" - - if [ "$(checksum "$PARTIAL_ZIP")" != "$GRADLE_SHA256" ]; then - echo "Gradle $GRADLE_VERSION checksum verification failed." >&2 - exit 1 - fi - - mv "$PARTIAL_ZIP" "$GRADLE_ZIP" - trap - EXIT INT TERM -else - echo "[android] Using verified Gradle $GRADLE_VERSION cache." -fi - -export CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL -CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL="$(node -e 'console.log(require("node:url").pathToFileURL(process.argv[1]).href)' "$GRADLE_ZIP")" - -echo "[android] JAVA_HOME=$JAVA_HOME" -echo "[android] ANDROID_HOME=$ANDROID_HOME" - -if [ "$ACTION" = check ]; then - TOOLS_DIR="$ROOT/platforms/android/tools" - if [ ! -d "$TOOLS_DIR" ]; then - echo "Cordova Android platform is missing. Run npm run clean before checking the wrapper." >&2 - exit 1 - fi - gradle -p "$TOOLS_DIR" wrapper --gradle-distribution-url "$CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL" - exit $? -fi - -exec "${COMMAND[@]}"