html2apk 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -252,6 +252,7 @@ Campos principais:
252
252
  | `orientation` | `default`, `vertical`, `horizontal`, `portrait` ou `landscape`. |
253
253
  | `minSdkVersion` | Versao minima do Android em API level. Padrao: `24`. |
254
254
  | `themeColor` | Cor base do tema/splash Android, em hexadecimal. Tambem vira fallback do modo automatico. |
255
+ | `icon` | Icone PNG do app. Se ficar vazio, o html2apk usa automaticamente o icone padrao da ferramenta. |
255
256
  | `themeMode` | `fixed` usa a cor fixa. `auto` adapta as barras Android a cor visivel na tela do APK. Tambem aceita `theme: "auto"`. |
256
257
  | `oneSignalAppId` | Opcional. App ID publico do OneSignal para push remoto. Nao coloque REST API Key no APK. |
257
258
  | `deepLinks` | URLs que podem abrir o APK, como `meuapp://rota` ou `https://meusite.com/produto/1`. |
@@ -355,6 +356,8 @@ Retorno:
355
356
 
356
357
  A v0.1 instala um plugin Cordova local com uma API global simples para recursos Android. Todas as funcoes retornam `Promise`, exceto os ouvintes `aoEvento`/atalhos, que retornam uma funcao para cancelar a escuta.
357
358
 
359
+ O html2apk injeta `html2apk-early-bridge.js` e `cordova.js` automaticamente no HTML inicial do APK. A bridge inicial cria as funcoes interpretadas antes dos scripts do seu projeto; se uma funcao nativa for chamada antes do `deviceready`, ela espera o Android ficar pronto antes de executar.
360
+
358
361
  Cada funcao em portugues tambem tem alias em ingles. A interface grafica mostra a sintaxe PT-BR quando o idioma esta em portugues e mostra a sintaxe em ingles quando o usuario troca o idioma para English.
359
362
 
360
363
  Exemplos de aliases:
@@ -403,30 +406,41 @@ Como tratar retornos:
403
406
 
404
407
  - Use `await` dentro de `try/catch` quando a acao depender de Android, permissao ou outro app instalado.
405
408
  - Seletores de arquivo podem retornar `null`, array vazio ou objeto vazio quando o usuario cancela.
406
- - APIs de permissao retornam objetos com `granted`; se vier `false`, desative o recurso ou mostre uma etapa de permissao.
409
+ - Funcoes que precisam de permissao pedem automaticamente quando sao chamadas, como `lanterna()`, `notificar()` e `ouvirMic()`.
410
+ - Se o Android nao puder mostrar o pop-up de permissao, o html2apk abre a tela correta de configuracoes e retorna `settingsOpened: true`.
411
+ - APIs manuais de permissao continuam existindo para quem quer explicar antes; elas retornam objetos com `granted`, `requiresSettings` e `settingsOpened`.
407
412
  - Eventos retornam uma funcao de cancelamento; guarde essa funcao e chame quando a tela/componente sair.
408
413
  - Medidas de memoria, armazenamento e apps abertos sao diagnosticas. Android moderno pode limitar dados de outros apps por privacidade.
409
414
 
410
415
  No seu JavaScript do app:
411
416
 
412
417
  ```js
413
- await solicitarPermissaoNotificacoes();
414
-
415
418
  toast("Mensagem");
416
419
  vibrar(250);
417
420
 
421
+ await notificar({
422
+ titulo: "Pedido aprovado",
423
+ texto: "Toque para abrir o app"
424
+ });
425
+
418
426
  notificar({
419
427
  titulo: "Pedido aprovado",
420
428
  texto: "Toque para abrir os detalhes",
421
429
  acoes: [
422
- { id: "abrir", titulo: "Abrir" },
423
- { id: "cancelar", titulo: "Cancelar" }
430
+ {
431
+ id: "abrir",
432
+ titulo: "Abrir",
433
+ open: true,
434
+ aoClicar: { funcao: "abrirNoApp", argumentos: ["#/pedido/123"] }
435
+ },
436
+ {
437
+ id: "site",
438
+ titulo: "Ver site",
439
+ open: false,
440
+ aoClicar: { funcao: "abrirForaDoApp", argumentos: ["https://exemplo.com/pedido/123"], open: false }
441
+ }
424
442
  ],
425
- aoClicar: {
426
- acao: "abrir-rota",
427
- rota: "/pedido/123",
428
- dados: { id: 123 }
429
- }
443
+ aoClicar: () => abrirForaDoApp("https://exemplo.com/pedido/123")
430
444
  });
431
445
 
432
446
  agendarNotificacao({
@@ -434,8 +448,8 @@ agendarNotificacao({
434
448
  texto: "Hora de abrir o app",
435
449
  quando: Date.now() + 60000,
436
450
  aoClicar: {
437
- acao: "abrir-rota",
438
- rota: "/lembretes"
451
+ funcao: "abrirNoApp",
452
+ argumentos: ["#/lembretes"]
439
453
  }
440
454
  });
441
455
 
@@ -463,8 +477,75 @@ const loop = await agendarLoopNotificacoes({
463
477
  fullscreen(true);
464
478
  ```
465
479
 
480
+ `notificar()` nao obriga clique, botao nem funcao. So `titulo` e `texto` ja geram uma notificacao normal. `aoClicar`, `acoes`/`actions` e `open` sao opcionais.
481
+
466
482
  `agendarNotificacao()` agenda uma notificacao. Se voce passar um array para ela, ou usar `agendarNotificacoes()`, o html2apk agenda varias em sequencia. Cada item recebe `id` automatico se voce nao informar um.
467
483
 
484
+ Em `aoClicar`, voce pode passar uma funcao diretamente:
485
+
486
+ ```js
487
+ await notificar({
488
+ titulo: "Pedido aprovado",
489
+ texto: "Toque para abrir os detalhes",
490
+ aoClicar: () => abrirForaDoApp("https://exemplo.com/pedido/123")
491
+ });
492
+ ```
493
+
494
+ Nao use `aoClicar: { acao: abrirForaDoApp("https://...") }`, porque os parenteses executam a funcao na hora em que a notificacao e criada. Para notificacao agendada, loop ou app fechado, prefira o formato serializavel:
495
+
496
+ ```js
497
+ await agendarNotificacao({
498
+ titulo: "Pedido aprovado",
499
+ texto: "Toque para abrir os detalhes",
500
+ quando: Date.now() + 60000,
501
+ aoClicar: {
502
+ funcao: "abrirForaDoApp",
503
+ argumentos: ["https://exemplo.com/pedido/123"]
504
+ }
505
+ });
506
+ ```
507
+
508
+ Esse formato tambem aceita funcoes suas, desde que elas existam em `window` quando o app abrir:
509
+
510
+ ```js
511
+ window.abrirPedido = (id) => abrirNoApp("#/pedido/" + id);
512
+
513
+ await notificar({
514
+ titulo: "Pedido aprovado",
515
+ texto: "Toque para abrir os detalhes",
516
+ aoClicar: { funcao: "abrirPedido", argumentos: [123] }
517
+ });
518
+ ```
519
+
520
+ Para botoes na notificacao, use `acoes` ou `actions`. Cada botao tambem aceita `aoClicar`/`onClick`:
521
+
522
+ ```js
523
+ window.marcarPedidoLido = (id) => {
524
+ localStorage.setItem("pedido:" + id, "lido");
525
+ };
526
+
527
+ await notificar({
528
+ titulo: "Pedido aprovado",
529
+ texto: "Escolha uma acao",
530
+ acoes: [
531
+ {
532
+ id: "abrir",
533
+ titulo: "Abrir",
534
+ open: true,
535
+ aoClicar: { funcao: "abrirNoApp", argumentos: ["#/pedido/123"] }
536
+ },
537
+ {
538
+ id: "lido",
539
+ titulo: "Marcar lido",
540
+ open: false,
541
+ aoClicar: { funcao: "marcarPedidoLido", argumentos: [123], open: false }
542
+ }
543
+ ]
544
+ });
545
+ ```
546
+
547
+ `open: false` evita trazer a tela do app para frente. Se o app ainda estiver vivo em segundo plano, o html2apk dispara o evento e executa a funcao JavaScript. Se o Android tiver matado o app, JavaScript de WebView nao consegue rodar sem abrir o app; nesse caso, acoes externas como `{ funcao: "abrirForaDoApp", argumentos: ["https://..."], open: false }` ainda funcionam por fallback nativo.
548
+
468
549
  `agendarLoopNotificacoes()` cria um loop recorrente que funciona com o app fechado. Use `aCada`, `intervalo`, `every` ou `interval` em milissegundos ou como texto (`"30min"`, `"12h"`, `"1d"`). A cada disparo, o Android mostra a proxima notificacao da lista. Para parar:
469
550
 
470
551
  ```js
@@ -532,19 +613,20 @@ O retorno de arquivos tem este formato:
532
613
  Microfone:
533
614
 
534
615
  ```js
535
- await solicitarPermissaoMicrofone();
536
-
537
- await ouvirMic();
538
-
539
- // ... depois, quando quiser parar
540
- const audio = await pararMic();
541
- const audioUrl = `data:${audio.mimeType};base64,${audio.base64}`;
542
-
543
- const player = new Audio(audioUrl);
544
- player.play();
616
+ const inicio = await ouvirMic();
617
+ if (inicio.settingsOpened) {
618
+ console.log("Libere Microfone nas configuracoes e tente novamente");
619
+ } else {
620
+ // ... depois, quando quiser parar
621
+ const audio = await pararMic();
622
+ const audioUrl = `data:${audio.mimeType};base64,${audio.base64}`;
623
+
624
+ const player = new Audio(audioUrl);
625
+ player.play();
626
+ }
545
627
  ```
546
628
 
547
- `ouvirMic()` comeca a gravar e tambem pede permissao se ela ainda nao foi concedida. Para uma experiencia mais clara, prefira chamar `solicitarPermissaoMicrofone()` antes e explicar ao usuario por que o app precisa do microfone.
629
+ `ouvirMic()` comeca a gravar e tambem pede permissao se ela ainda nao foi concedida. Se a permissao estiver bloqueada pelo Android, a tela de configuracoes do app e aberta automaticamente e o retorno traz `settingsOpened: true`.
548
630
 
549
631
  `pararMic()` encerra a gravacao e retorna:
550
632
 
@@ -563,7 +645,6 @@ Para tratar o retorno, use `mimeType` e `base64` juntos em um Data URL quando qu
563
645
  Lanterna, tela, clipboard e intents:
564
646
 
565
647
  ```js
566
- await solicitarPermissaoCamera();
567
648
  const lanternaStatus = await lanterna(true);
568
649
  await alternarLanterna();
569
650
 
@@ -667,9 +748,7 @@ Permissoes e alarmes:
667
748
 
668
749
  ```js
669
750
  const status = await statusPermissaoNotificacoes();
670
- if (!status.granted) {
671
- await solicitarPermissaoNotificacoes();
672
- }
751
+ console.log(status.granted);
673
752
 
674
753
  const podeUsarAlarmeExato = await podeAgendarNotificacaoExata();
675
754
  if (!podeUsarAlarmeExato) {
@@ -677,7 +756,7 @@ if (!podeUsarAlarmeExato) {
677
756
  }
678
757
  ```
679
758
 
680
- A bridge cria canal de notificacao, solicita/consulta `POST_NOTIFICATIONS` no Android 13+, abre o app com payload quando a notificacao e clicada, persiste notificacoes agendadas e tenta reagendar apos reboot ou update do app.
759
+ A bridge cria canal de notificacao, solicita `POST_NOTIFICATIONS` automaticamente quando `notificar()`/`agendarNotificacao()` precisam, abre configuracoes se o Android bloquear o pop-up, abre o app com payload quando a notificacao e clicada, persiste notificacoes agendadas e tenta reagendar apos reboot ou update do app. Se voce usar `exato: true`/`exact: true` em uma notificacao agendada e o Android exigir liberacao manual de alarme exato, o html2apk abre essa tela automaticamente.
681
760
 
682
761
  ## Problemas Comuns
683
762
 
@@ -748,11 +827,15 @@ Fluxo da interface:
748
827
  1. Arraste ou escolha a pasta do projeto.
749
828
  2. O app mostra `verificando ambiente` antes de liberar as proximas etapas.
750
829
  3. Se faltarem pacotes do Android SDK, ele pede permissao e tenta baixar/instalar mostrando logs.
751
- 4. Preencha as configuracoes obrigatorias: nome do app, Package ID, versao, modo e icone PNG.
752
- 5. Revise, clique em `Gerar APK` e acompanhe a barra de progresso.
830
+ 4. Preencha as configuracoes obrigatorias: nome do app, Package ID, versao e modo. Se voce nao escolher icone, o html2apk usa o icone padrao da ferramenta.
831
+ 5. Revise e clique em `Gerar APK` para salvar o APK em `dist`, ou `Testar no USB` para gerar debug, instalar e abrir direto em um celular conectado.
753
832
  6. Ao concluir, a tela final mostra o APK gerado e botoes para abrir a pasta `dist` ou localizar o arquivo.
754
833
 
755
- O PNG escolhido e usado como icone do aplicativo e tambem como imagem da tela inicial do Android, evitando o splash padrao do Cordova.
834
+ O PNG escolhido e usado como icone do aplicativo e tambem como imagem da tela inicial do Android, evitando o splash padrao do Cordova. Quando nenhum PNG e escolhido, o `html2apk.png` da propria ferramenta entra como fallback.
835
+
836
+ Depois que a pasta foi escolhida, a interface acompanha mudancas nela automaticamente. Edicoes em HTML, CSS, JS e assets entram no proximo build sem arrastar a pasta de novo. Se `app.json` ou `config.json` mudar, os campos da tela de configuracoes sao recarregados.
837
+
838
+ Para `Testar no USB`, o celular precisa estar com `Opcoes do desenvolvedor > Depuracao USB` ativa. Ao conectar, desbloqueie o aparelho e aceite a chave RSA. Se o Android aparecer como `unauthorized` ou `offline`, a interface mostra o que fazer nos logs.
756
839
 
757
840
  Os logs podem ser abertos em uma barra inferior durante qualquer etapa pelo botao `Mostrar logs`. Se atrapalhar a visualizacao, use `Ocultar logs` e a area principal volta a ocupar a altura da janela.
758
841
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html2apk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Node CLI and library to turn an HTML/CSS/JS folder into an Android APK through Cordova.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -47,10 +47,28 @@ async function buildAndroid(buildDir, options, buildJsonPath, runner) {
47
47
  });
48
48
  }
49
49
 
50
+ async function runAndroidDevice(buildDir, options, buildJsonPath, runner, deviceId) {
51
+ const args = ["run", "android", "--device", "--debug", "--no-telemetry"];
52
+
53
+ if (deviceId) {
54
+ args.push(`--target=${deviceId}`);
55
+ }
56
+
57
+ if (buildJsonPath) {
58
+ args.push("--buildConfig", buildJsonPath);
59
+ }
60
+
61
+ await runner.run("cordova", args, {
62
+ cwd: buildDir,
63
+ pipeOutput: options.debug
64
+ });
65
+ }
66
+
50
67
  module.exports = {
51
68
  DEFAULT_ANDROID_PLATFORM,
52
69
  createCordovaProject,
53
70
  addCordovaPlugin,
54
71
  addAndroidPlatform,
55
- buildAndroid
72
+ buildAndroid,
73
+ runAndroidDevice
56
74
  };
@@ -5,7 +5,7 @@ const os = require("os");
5
5
  const path = require("path");
6
6
  const { resolveBuildOptions } = require("./config");
7
7
  const { validateEntryFile, validateRequiredOptions } = require("./validation");
8
- const { createCordovaProject, addAndroidPlatform, buildAndroid, addCordovaPlugin } = require("../cordova/project");
8
+ const { createCordovaProject, addAndroidPlatform, buildAndroid, runAndroidDevice, addCordovaPlugin } = require("../cordova/project");
9
9
  const { writeConfigXml } = require("../cordova/config-xml");
10
10
  const { findApk } = require("../cordova/apk-finder");
11
11
  const { copyWebAssets, ensureDir, removePath, copyFile, copyDirectory } = require("../utils/fs-extra");
@@ -14,8 +14,18 @@ const { installBridgePlugin } = require("../bridge/install-bridge");
14
14
  const { getRuntimeEnvironment } = require("../runtime-manager");
15
15
 
16
16
  const AUTO_THEME_SCRIPT_NAME = "html2apk-auto-theme.js";
17
+ const EARLY_BRIDGE_SCRIPT_NAME = "html2apk-early-bridge.js";
17
18
  const ONESIGNAL_SCRIPT_NAME = "html2apk-onesignal.js";
18
19
  const ONESIGNAL_PLUGIN_PACKAGE = "onesignal-cordova-plugin";
20
+ const DEFAULT_APP_ICON_NAME = "html2apk.png";
21
+
22
+ function defaultAppIconPath() {
23
+ return path.resolve(__dirname, "..", "..", DEFAULT_APP_ICON_NAME);
24
+ }
25
+
26
+ function withDefaultAppIcon(assetPath) {
27
+ return String(assetPath || "").trim() || defaultAppIconPath();
28
+ }
19
29
 
20
30
  function isRemoteAsset(assetPath) {
21
31
  return /^https?:\/\//i.test(String(assetPath || ""));
@@ -54,6 +64,47 @@ function scriptTag(scriptPath) {
54
64
  return `<script src="${scriptPath}"></script>`;
55
65
  }
56
66
 
67
+ async function injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js", earlyBridgePath = EARLY_BRIDGE_SCRIPT_NAME) {
68
+ let html = await fs.readFile(htmlPath, "utf8");
69
+ const hasCordova = /<script\b[^>]*\bsrc=["'][^"']*cordova\.js["'][^>]*>/i.test(html);
70
+ const hasEarlyBridge = /<script\b[^>]*\bsrc=["'][^"']*html2apk-early-bridge\.js["'][^>]*>/i.test(html);
71
+ if (hasCordova && hasEarlyBridge) {
72
+ return false;
73
+ }
74
+
75
+ const tags = [
76
+ hasEarlyBridge ? null : scriptTag(earlyBridgePath),
77
+ hasCordova ? null : scriptTag(scriptPath)
78
+ ].filter(Boolean).join("\n ");
79
+
80
+ if (/<head\b[^>]*>/i.test(html)) {
81
+ html = html.replace(/<head\b[^>]*>/i, (match) => `${match}\n ${tags}`);
82
+ } else if (/<html\b[^>]*>/i.test(html)) {
83
+ html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${tags}`);
84
+ } else {
85
+ html = `${tags}\n${html}`;
86
+ }
87
+
88
+ await fs.writeFile(htmlPath, html, "utf8");
89
+ return true;
90
+ }
91
+
92
+ async function installCordovaRuntimeScript(buildDir, options) {
93
+ const wwwDir = path.join(buildDir, "www");
94
+ const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
95
+ const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), path.join(wwwDir, "cordova.js"))) || "cordova.js";
96
+ const earlyBridgeSource = path.resolve(__dirname, "..", "templates", EARLY_BRIDGE_SCRIPT_NAME);
97
+ const earlyBridgeTarget = path.join(wwwDir, EARLY_BRIDGE_SCRIPT_NAME);
98
+ const earlyBridgePath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), earlyBridgeTarget)) || EARLY_BRIDGE_SCRIPT_NAME;
99
+
100
+ if (!isInside(wwwDir, entryHtmlPath)) {
101
+ throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
102
+ }
103
+
104
+ await copyFile(earlyBridgeSource, earlyBridgeTarget);
105
+ return injectCordovaRuntimeIntoHtml(entryHtmlPath, scriptPath, earlyBridgePath);
106
+ }
107
+
57
108
  async function injectScriptIntoHtml(htmlPath, scriptPath) {
58
109
  let html = await fs.readFile(htmlPath, "utf8");
59
110
  if (html.includes(scriptPath)) {
@@ -238,11 +289,147 @@ function outputApkName(options) {
238
289
  return `${safeName}-${options.version}-${flavor}.apk`;
239
290
  }
240
291
 
292
+ function parseAdbDevices(output) {
293
+ return String(output || "")
294
+ .split(/\r?\n/)
295
+ .map((line) => line.trim())
296
+ .filter((line) => line
297
+ && !/^list of devices/i.test(line)
298
+ && !/^\*/.test(line)
299
+ && !/^adb server/i.test(line))
300
+ .map((line) => {
301
+ const parts = line.split(/\s+/);
302
+ return {
303
+ id: parts[0],
304
+ status: parts[1] || "unknown"
305
+ };
306
+ })
307
+ .filter((device) => device.id);
308
+ }
309
+
310
+ function deviceTargetId(device) {
311
+ return device && device.id ? String(device.id) : "";
312
+ }
313
+
314
+ async function ensureUsbDebugDevice(runner) {
315
+ let result;
316
+ try {
317
+ result = await runner.run("adb", ["devices"]);
318
+ } catch (error) {
319
+ throw new Error("ADB nao foi encontrado. Instale Android platform-tools pelo ambiente do html2apk e tente novamente.");
320
+ }
321
+
322
+ const devices = parseAdbDevices(result.stdout);
323
+ const physicalDevices = devices.filter((device) => !/^emulator-/i.test(device.id));
324
+ const ready = physicalDevices.find((device) => device.status === "device");
325
+ if (ready) {
326
+ return ready;
327
+ }
328
+
329
+ if (physicalDevices.some((device) => device.status === "unauthorized")) {
330
+ throw new Error("Celular encontrado, mas a depuracao USB ainda nao foi autorizada. Desbloqueie o celular e aceite a chave RSA de depuracao USB.");
331
+ }
332
+
333
+ if (physicalDevices.some((device) => device.status === "offline")) {
334
+ throw new Error("Celular USB encontrado, mas esta offline. Reconecte o cabo USB, desbloqueie o celular e confirme a depuracao USB.");
335
+ }
336
+
337
+ throw new Error("Nenhum celular USB autorizado foi encontrado. Ative Opcoes do desenvolvedor > Depuracao USB, conecte o celular e aceite a permissao RSA.");
338
+ }
339
+
340
+ async function prepareCordovaProject(projectRoot, buildDir, options, runner, log) {
341
+ await createCordovaProject(buildDir, options, runner);
342
+ const cordovaOptions = { ...options };
343
+ const effectiveIcon = withDefaultAppIcon(options.icon);
344
+ const effectiveSplash = options.splash || effectiveIcon;
345
+ if (!String(options.icon || "").trim()) {
346
+ log("Icon: using the default html2apk icon.");
347
+ }
348
+ cordovaOptions.icon = await copyCordovaAsset(projectRoot, buildDir, effectiveIcon, "icon");
349
+ cordovaOptions.splash = await copyCordovaAsset(projectRoot, buildDir, effectiveSplash, "splash");
350
+ cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
351
+ await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
352
+ await copyWebAssets(path.resolve(projectRoot, options.webRoot || "."), path.join(buildDir, "www"), options, projectRoot);
353
+ if (await installCordovaRuntimeScript(buildDir, options)) {
354
+ log("Cordova runtime: injected cordova.js into the entry HTML.");
355
+ }
356
+ if (await installAutoThemeScript(buildDir, options)) {
357
+ log("Theme mode: auto (system bars follow the visible screen color).");
358
+ }
359
+ if (await installOneSignalScript(buildDir, options)) {
360
+ log("OneSignal: enabled for remote push notifications.");
361
+ }
362
+
363
+ const bridgePluginPath = await installBridgePlugin(buildDir);
364
+ await addCordovaPlugin(buildDir, bridgePluginPath, runner);
365
+
366
+ if (hasOneSignal(options) && !hasPlugin(options.plugins, ONESIGNAL_PLUGIN_PACKAGE)) {
367
+ await addCordovaPlugin(buildDir, await prepareBundledPlugin(buildDir, ONESIGNAL_PLUGIN_PACKAGE), runner);
368
+ }
369
+
370
+ for (const plugin of options.plugins) {
371
+ await addCordovaPlugin(buildDir, plugin, runner);
372
+ }
373
+
374
+ await addAndroidPlatform(buildDir, options, runner);
375
+ }
376
+
377
+ async function copyBuiltApk(projectRoot, buildDir, options) {
378
+ const apkPathInBuild = await findApk(buildDir, options);
379
+ const outputDir = path.resolve(projectRoot, options.outputDir || "dist");
380
+ await ensureDir(outputDir);
381
+
382
+ const apkPath = path.join(outputDir, outputApkName(options));
383
+ await copyFile(apkPathInBuild, apkPath);
384
+ return apkPath;
385
+ }
386
+
387
+ async function ensureBuiltDebugApk(buildDir, options, runner, log) {
388
+ try {
389
+ return await findApk(buildDir, options);
390
+ } catch {
391
+ log("USB debug: building debug APK for direct ADB install.");
392
+ await buildAndroid(buildDir, options, null, runner);
393
+ return findApk(buildDir, options);
394
+ }
395
+ }
396
+
397
+ async function installDebugApkWithAdb(buildDir, options, runner, device, log) {
398
+ const deviceId = deviceTargetId(device);
399
+ const apkPath = await ensureBuiltDebugApk(buildDir, options, runner, log);
400
+ log(`USB debug fallback: installing APK with ADB on ${deviceId}.`);
401
+
402
+ try {
403
+ await runner.run("adb", ["-s", deviceId, "install", "-r", "-d", apkPath]);
404
+ } catch (error) {
405
+ const output = `${error.stdout || ""}\n${error.stderr || ""}\n${error.message || ""}`;
406
+ if (/INSTALL_FAILED_UPDATE_INCOMPATIBLE/i.test(output)) {
407
+ throw new Error("O app ja esta instalado nesse celular com outra assinatura. Desinstale a versao antiga no Android e clique em Testar no USB novamente.");
408
+ }
409
+ throw error;
410
+ }
411
+
412
+ log(`USB debug fallback: opening ${options.packageId}.`);
413
+ await runner.run("adb", [
414
+ "-s",
415
+ deviceId,
416
+ "shell",
417
+ "monkey",
418
+ "-p",
419
+ options.packageId,
420
+ "-c",
421
+ "android.intent.category.LAUNCHER",
422
+ "1"
423
+ ]);
424
+
425
+ return apkPath;
426
+ }
427
+
241
428
  async function buildApk(overrides = {}) {
242
429
  const onLog = typeof overrides.onLog === "function" ? overrides.onLog : null;
243
430
  const { projectRoot, configPath, options } = await resolveBuildOptions(overrides);
244
431
  validateRequiredOptions(options);
245
- const { webRoot } = await validateEntryFile(projectRoot, options);
432
+ await validateEntryFile(projectRoot, options);
246
433
 
247
434
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "html2apk-"));
248
435
  const buildDir = path.join(tempRoot, "cordova-project");
@@ -268,41 +455,86 @@ async function buildApk(overrides = {}) {
268
455
  log(`JAVA_HOME: ${runtime.javaHome}`);
269
456
  }
270
457
 
271
- await createCordovaProject(buildDir, options, runner);
272
- const cordovaOptions = { ...options };
273
- cordovaOptions.icon = await copyCordovaAsset(projectRoot, buildDir, options.icon, "icon");
274
- cordovaOptions.splash = await copyCordovaAsset(projectRoot, buildDir, options.splash || options.icon, "splash");
275
- cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
276
- await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
277
- await copyWebAssets(webRoot, path.join(buildDir, "www"), options, projectRoot);
278
- if (await installAutoThemeScript(buildDir, options)) {
279
- log("Theme mode: auto (system bars follow the visible screen color).");
280
- }
281
- if (await installOneSignalScript(buildDir, options)) {
282
- log("OneSignal: enabled for remote push notifications.");
283
- }
458
+ await prepareCordovaProject(projectRoot, buildDir, options, runner, log);
459
+ const buildJsonPath = await createBuildJson(buildDir, options, projectRoot);
460
+ await buildAndroid(buildDir, options, buildJsonPath, runner);
284
461
 
285
- const bridgePluginPath = await installBridgePlugin(buildDir);
286
- await addCordovaPlugin(buildDir, bridgePluginPath, runner);
462
+ const apkPath = await copyBuiltApk(projectRoot, buildDir, options);
287
463
 
288
- if (hasOneSignal(options) && !hasPlugin(options.plugins, ONESIGNAL_PLUGIN_PACKAGE)) {
289
- await addCordovaPlugin(buildDir, await prepareBundledPlugin(buildDir, ONESIGNAL_PLUGIN_PACKAGE), runner);
464
+ if (!options.debug) {
465
+ await removePath(tempRoot);
466
+ tempCleaned = true;
290
467
  }
291
468
 
292
- for (const plugin of options.plugins) {
293
- await addCordovaPlugin(buildDir, plugin, runner);
469
+ return {
470
+ apkPath,
471
+ buildDir: options.debug ? buildDir : null,
472
+ logs,
473
+ status: "success",
474
+ tempCleaned
475
+ };
476
+ } catch (error) {
477
+ log(`Error: ${error.message}`);
478
+ if (!options.debug) {
479
+ await removePath(tempRoot).catch(() => {});
480
+ tempCleaned = true;
481
+ } else {
482
+ log(`Debug mode enabled. Temporary build kept at: ${buildDir}`);
294
483
  }
295
484
 
296
- await addAndroidPlatform(buildDir, options, runner);
297
- const buildJsonPath = await createBuildJson(buildDir, options, projectRoot);
298
- await buildAndroid(buildDir, options, buildJsonPath, runner);
485
+ error.logs = logs;
486
+ error.buildDir = options.debug ? buildDir : null;
487
+ error.tempCleaned = tempCleaned;
488
+ error.status = "error";
489
+ throw error;
490
+ }
491
+ }
492
+
493
+ async function runDebugUsb(overrides = {}) {
494
+ const onLog = typeof overrides.onLog === "function" ? overrides.onLog : null;
495
+ const resolved = await resolveBuildOptions(overrides);
496
+ const projectRoot = resolved.projectRoot;
497
+ const configPath = resolved.configPath;
498
+ const options = {
499
+ ...resolved.options,
500
+ release: false
501
+ };
502
+ validateRequiredOptions(options);
503
+ await validateEntryFile(projectRoot, options);
504
+
505
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "html2apk-"));
506
+ const buildDir = path.join(tempRoot, "cordova-project");
507
+ const logs = [];
508
+ const runtime = getRuntimeEnvironment();
509
+ const runner = createCommandRunner({ logs, env: runtime.env, onLog });
510
+ let tempCleaned = false;
299
511
 
300
- const apkPathInBuild = await findApk(buildDir, options);
301
- const outputDir = path.resolve(projectRoot, options.outputDir || "dist");
302
- await ensureDir(outputDir);
512
+ function log(line) {
513
+ logs.push(line);
514
+ if (onLog) {
515
+ onLog(line);
516
+ }
517
+ }
518
+
519
+ try {
520
+ log(`Project root: ${projectRoot}`);
521
+ log(configPath ? `Config: ${configPath}` : "Config: defaults only");
522
+ log("USB debug: checking connected Android device.");
523
+ const device = await ensureUsbDebugDevice(runner);
524
+ log(`USB debug device: ${device.id}`);
525
+
526
+ await prepareCordovaProject(projectRoot, buildDir, options, runner, log);
527
+ try {
528
+ await runAndroidDevice(buildDir, options, null, runner, deviceTargetId(device));
529
+ } catch (error) {
530
+ log("USB debug: Cordova run failed. Trying direct ADB install fallback.");
531
+ if (error.stdout || error.stderr) {
532
+ log([error.stdout, error.stderr].filter(Boolean).join("\n").trim().slice(-4000));
533
+ }
534
+ await installDebugApkWithAdb(buildDir, options, runner, device, log);
535
+ }
303
536
 
304
- const apkPath = path.join(outputDir, outputApkName(options));
305
- await copyFile(apkPathInBuild, apkPath);
537
+ const apkPath = await copyBuiltApk(projectRoot, buildDir, options);
306
538
 
307
539
  if (!options.debug) {
308
540
  await removePath(tempRoot);
@@ -312,8 +544,10 @@ async function buildApk(overrides = {}) {
312
544
  return {
313
545
  apkPath,
314
546
  buildDir: options.debug ? buildDir : null,
547
+ device,
315
548
  logs,
316
549
  status: "success",
550
+ usbDebug: true,
317
551
  tempCleaned
318
552
  };
319
553
  } catch (error) {
@@ -334,5 +568,9 @@ async function buildApk(overrides = {}) {
334
568
  }
335
569
 
336
570
  module.exports = {
337
- buildApk
571
+ buildApk,
572
+ defaultAppIconPath,
573
+ injectCordovaRuntimeIntoHtml,
574
+ parseAdbDevices,
575
+ runDebugUsb
338
576
  };