html2apk 0.4.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
@@ -356,7 +356,7 @@ Retorno:
356
356
 
357
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.
358
358
 
359
- O html2apk injeta `cordova.js` automaticamente no HTML inicial do APK. Se uma funcao nativa for chamada antes do `deviceready`, a bridge espera o Android ficar pronto antes de executar.
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
360
 
361
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.
362
362
 
@@ -418,18 +418,29 @@ No seu JavaScript do app:
418
418
  toast("Mensagem");
419
419
  vibrar(250);
420
420
 
421
+ await notificar({
422
+ titulo: "Pedido aprovado",
423
+ texto: "Toque para abrir o app"
424
+ });
425
+
421
426
  notificar({
422
427
  titulo: "Pedido aprovado",
423
428
  texto: "Toque para abrir os detalhes",
424
429
  acoes: [
425
- { id: "abrir", titulo: "Abrir" },
426
- { 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
+ }
427
442
  ],
428
- aoClicar: {
429
- acao: "abrir-rota",
430
- rota: "/pedido/123",
431
- dados: { id: 123 }
432
- }
443
+ aoClicar: () => abrirForaDoApp("https://exemplo.com/pedido/123")
433
444
  });
434
445
 
435
446
  agendarNotificacao({
@@ -437,8 +448,8 @@ agendarNotificacao({
437
448
  texto: "Hora de abrir o app",
438
449
  quando: Date.now() + 60000,
439
450
  aoClicar: {
440
- acao: "abrir-rota",
441
- rota: "/lembretes"
451
+ funcao: "abrirNoApp",
452
+ argumentos: ["#/lembretes"]
442
453
  }
443
454
  });
444
455
 
@@ -466,8 +477,75 @@ const loop = await agendarLoopNotificacoes({
466
477
  fullscreen(true);
467
478
  ```
468
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
+
469
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.
470
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
+
471
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:
472
550
 
473
551
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html2apk",
3
- "version": "0.4.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": {
@@ -14,6 +14,7 @@ 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";
19
20
  const DEFAULT_APP_ICON_NAME = "html2apk.png";
@@ -63,19 +64,25 @@ function scriptTag(scriptPath) {
63
64
  return `<script src="${scriptPath}"></script>`;
64
65
  }
65
66
 
66
- async function injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js") {
67
+ async function injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js", earlyBridgePath = EARLY_BRIDGE_SCRIPT_NAME) {
67
68
  let html = await fs.readFile(htmlPath, "utf8");
68
- if (/<script\b[^>]*\bsrc=["'][^"']*cordova\.js["'][^>]*>/i.test(html)) {
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) {
69
72
  return false;
70
73
  }
71
74
 
72
- const tag = scriptTag(scriptPath);
75
+ const tags = [
76
+ hasEarlyBridge ? null : scriptTag(earlyBridgePath),
77
+ hasCordova ? null : scriptTag(scriptPath)
78
+ ].filter(Boolean).join("\n ");
79
+
73
80
  if (/<head\b[^>]*>/i.test(html)) {
74
- html = html.replace(/<head\b[^>]*>/i, (match) => `${match}\n ${tag}`);
81
+ html = html.replace(/<head\b[^>]*>/i, (match) => `${match}\n ${tags}`);
75
82
  } else if (/<html\b[^>]*>/i.test(html)) {
76
- html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${tag}`);
83
+ html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${tags}`);
77
84
  } else {
78
- html = `${tag}\n${html}`;
85
+ html = `${tags}\n${html}`;
79
86
  }
80
87
 
81
88
  await fs.writeFile(htmlPath, html, "utf8");
@@ -86,12 +93,16 @@ async function installCordovaRuntimeScript(buildDir, options) {
86
93
  const wwwDir = path.join(buildDir, "www");
87
94
  const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
88
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;
89
99
 
90
100
  if (!isInside(wwwDir, entryHtmlPath)) {
91
101
  throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
92
102
  }
93
103
 
94
- return injectCordovaRuntimeIntoHtml(entryHtmlPath, scriptPath);
104
+ await copyFile(earlyBridgeSource, earlyBridgeTarget);
105
+ return injectCordovaRuntimeIntoHtml(entryHtmlPath, scriptPath, earlyBridgePath);
95
106
  }
96
107
 
97
108
  async function injectScriptIntoHtml(htmlPath, scriptPath) {
@@ -391,11 +391,11 @@ const nativeCodeEntries = [
391
391
  handling: { pt: "Limite duracoes longas e deixe a permissao `VIBRATE` no app; se falhar, siga sem bloquear o fluxo principal.", en: "Keep long durations under control and include the `VIBRATE` permission; if it fails, continue without blocking the main flow." }
392
392
  },
393
393
  {
394
- syntax: { pt: "notificar({ titulo, texto, acoes })", en: "notify({ title, text, actions })" },
394
+ syntax: { pt: "notificar({ titulo, texto, aoClicar?, acoes?, open? })", en: "notify({ title, text, onClick?, actions?, open? })" },
395
395
  java: "notify",
396
- description: { pt: "Cria notificacao Android imediata. `acoes` ou `actions` viram botoes clicaveis.", en: "Creates an immediate Android notification. `actions` or `acoes` become clickable buttons." },
397
- returns: { pt: "Promise<void>; cliques chegam em `aoEvento('notificacao:clicada')` ou `aoClicarNotificacao()`.", en: "Promise<void>; clicks arrive through `onEvent('notification:clicked')` or `onNotificationClick()`." },
398
- handling: { pt: "Ao chamar, o html2apk pede permissao automaticamente no Android 13+. Se o Android nao puder mostrar o pop-up, ele abre as configuracoes e retorna `settingsOpened`.", en: "When called, html2apk automatically requests permission on Android 13+. If Android cannot show the prompt, it opens settings and returns `settingsOpened`." }
396
+ description: { pt: "Cria notificacao Android imediata. So `titulo` e `texto` ja bastam; `aoClicar`, `acoes` e `open` sao opcionais.", en: "Creates an immediate Android notification. `title` and `text` are enough; `onClick`, `actions` and `open` are optional." },
397
+ returns: { pt: "Promise<void>; cliques chegam em `aoEvento('notificacao:clicada')` ou `aoClicarNotificacao()` e tambem podem executar `aoClicar` automaticamente.", en: "Promise<void>; clicks arrive through `onEvent('notification:clicked')` or `onNotificationClick()` and can also run `onClick` automatically." },
398
+ handling: { pt: "Use `aoClicar: () => funcao()` enquanto o app esta vivo. Para agendada ou app fechado, prefira `aoClicar: { funcao, argumentos }`. `open:false` evita abrir a tela; JavaScript so roda assim se o app ainda estiver vivo, mas acoes externas como `abrirForaDoApp` funcionam por fallback nativo.", en: "Use `onClick: () => functionName()` while the app process is alive. For scheduled notifications or a closed app, prefer `onClick: { functionName, args }`. `open:false` avoids opening the screen; JavaScript only runs this way if the app is still alive, but external actions like `openOutsideApp` work through a native fallback." }
399
399
  },
400
400
  {
401
401
  syntax: { pt: "agendarNotificacao({ titulo, texto, quando }) / agendarNotificacoes([...])", en: "scheduleNotification({ title, text, when }) / scheduleNotifications([...])" },
@@ -409,7 +409,7 @@ const nativeCodeEntries = [
409
409
  java: "AlarmManager + NotificationStore",
410
410
  description: { pt: "Cria um loop de notificacoes que continua funcionando com o app fechado e alterna os itens da lista.", en: "Creates a notification loop that keeps working with the app closed and rotates through the list items." },
411
411
  returns: { pt: "{ id, when, repeating, loop }. Guarde o `id` para cancelar depois.", en: "{ id, when, repeating, loop }. Store the `id` to cancel later." },
412
- handling: { pt: "Use `aoClicarNotificacao()` ou `aoEvento('notificacao:clicada')` para chamar funcoes conforme `aoClicar`. Cancele com `cancelarNotificacao(id)`.", en: "Use `onNotificationClick()` or `onEvent('notification:clicked')` to call functions from `onClick`. Cancel with `cancelNotification(id)`." }
412
+ handling: { pt: "Cancele com `cancelarNotificacao(id)`. Se o clique precisar chamar funcao automaticamente mesmo apos o Android reabrir o app, use `funcao` + `argumentos` no `aoClicar`.", en: "Cancel with `cancelNotification(id)`. If the click must call a function automatically after Android reopens the app, use `functionName` + `args` in `onClick`." }
413
413
  },
414
414
  {
415
415
  syntax: { pt: "solicitarPermissaoNotificacoes()", en: "requestNotificationPermission()" },
@@ -668,17 +668,102 @@ const nativeCodeRecipes = [
668
668
  }
669
669
  },
670
670
  {
671
- when: { pt: "Para mostrar uma notificacao agora. A permissao aparece automaticamente se o Android exigir.", en: "To show a notification now. Permission appears automatically if Android requires it." },
671
+ when: { pt: "Para mostrar uma notificacao simples agora. `aoClicar`, `acoes` e `open` sao opcionais.", en: "To show a simple notification now. `onClick`, `actions` and `open` are optional." },
672
+ example: {
673
+ pt: `await notificar({
674
+ titulo: "Pedido aprovado",
675
+ texto: "Toque para abrir o app"
676
+ });`,
677
+ en: `await notify({
678
+ title: "Order approved",
679
+ text: "Tap to open the app"
680
+ });`
681
+ }
682
+ },
683
+ {
684
+ when: { pt: "Para executar algo quando a notificacao for clicada.", en: "To run something when the notification is clicked." },
672
685
  example: {
673
686
  pt: `await notificar({
674
687
  titulo: "Pedido aprovado",
675
688
  texto: "Toque para abrir os detalhes",
676
- aoClicar: { acao: "abrir-pedido", id: 123 }
689
+ aoClicar: () => abrirForaDoApp("https://exemplo.com/pedidos/123")
677
690
  });`,
678
691
  en: `await notify({
679
692
  title: "Order approved",
680
693
  text: "Tap to open details",
681
- onClick: { action: "open-order", id: 123 }
694
+ onClick: () => openOutsideApp("https://example.com/orders/123")
695
+ });`
696
+ }
697
+ },
698
+ {
699
+ when: { pt: "Para colocar botoes na notificacao, cada um chamando uma funcao.", en: "To add buttons to the notification, each one calling a function." },
700
+ example: {
701
+ pt: `window.marcarPedidoLido = (id) => {
702
+ localStorage.setItem("pedido:" + id, "lido");
703
+ };
704
+
705
+ await notificar({
706
+ titulo: "Pedido aprovado",
707
+ texto: "Escolha uma acao",
708
+ acoes: [
709
+ {
710
+ id: "abrir",
711
+ titulo: "Abrir",
712
+ open: true,
713
+ aoClicar: { funcao: "abrirNoApp", argumentos: ["#/pedido/123"] }
714
+ },
715
+ {
716
+ id: "lido",
717
+ titulo: "Marcar lido",
718
+ open: false,
719
+ aoClicar: { funcao: "marcarPedidoLido", argumentos: [123], open: false }
720
+ }
721
+ ]
722
+ });`,
723
+ en: `window.markOrderRead = (id) => {
724
+ localStorage.setItem("order:" + id, "read");
725
+ };
726
+
727
+ await notify({
728
+ title: "Order approved",
729
+ text: "Choose an action",
730
+ actions: [
731
+ {
732
+ id: "open",
733
+ title: "Open",
734
+ open: true,
735
+ onClick: { functionName: "openInApp", args: ["#/order/123"] }
736
+ },
737
+ {
738
+ id: "read",
739
+ title: "Mark read",
740
+ open: false,
741
+ onClick: { functionName: "markOrderRead", args: [123], open: false }
742
+ }
743
+ ]
744
+ });`
745
+ }
746
+ },
747
+ {
748
+ when: { pt: "Para clique de notificacao agendada ou com app fechado.", en: "For scheduled notification clicks or when the app is closed." },
749
+ example: {
750
+ pt: `await agendarNotificacao({
751
+ titulo: "Pedido aprovado",
752
+ texto: "Toque para abrir os detalhes",
753
+ quando: Date.now() + 60 * 1000,
754
+ aoClicar: {
755
+ funcao: "abrirForaDoApp",
756
+ argumentos: ["https://exemplo.com/pedidos/123"]
757
+ }
758
+ });`,
759
+ en: `await scheduleNotification({
760
+ title: "Order approved",
761
+ text: "Tap to open details",
762
+ when: Date.now() + 60 * 1000,
763
+ onClick: {
764
+ functionName: "openOutsideApp",
765
+ args: ["https://example.com/orders/123"]
766
+ }
682
767
  });`
683
768
  }
684
769
  },
@@ -27,6 +27,7 @@
27
27
  <config-file target="AndroidManifest.xml" parent="/manifest/application">
28
28
  <service android:name="dev.html2apk.bridge.FloatingIconService" android:exported="false" />
29
29
  <receiver android:name="dev.html2apk.bridge.NotificationReceiver" android:exported="false" />
30
+ <receiver android:name="dev.html2apk.bridge.NotificationClickReceiver" android:exported="false" />
30
31
  <receiver android:name="dev.html2apk.bridge.BootReceiver" android:enabled="true" android:exported="true">
31
32
  <intent-filter>
32
33
  <action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -38,6 +39,7 @@
38
39
  <source-file src="src/android/Html2ApkBridge.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
39
40
  <source-file src="src/android/FloatingIconService.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
40
41
  <source-file src="src/android/NotificationReceiver.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
42
+ <source-file src="src/android/NotificationClickReceiver.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
41
43
  <source-file src="src/android/BootReceiver.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
42
44
  <source-file src="src/android/NotificationStore.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
43
45
  <framework src="androidx.core:core:1.12.0" />
@@ -73,6 +73,7 @@ public class Html2ApkBridge extends CordovaPlugin {
73
73
  private static final int REQUEST_PICK_FOLDER = 7413;
74
74
  private static final String PREFS_NAME = "html2apk_bridge";
75
75
  private static final String PREF_PERMISSION_PREFIX = "permission_requested_";
76
+ private static Html2ApkBridge activeBridge;
76
77
 
77
78
  private CallbackContext notificationPermissionCallback;
78
79
  private CallbackContext cameraPermissionCallback;
@@ -100,6 +101,7 @@ public class Html2ApkBridge extends CordovaPlugin {
100
101
  @Override
101
102
  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
102
103
  super.initialize(cordova, webView);
104
+ activeBridge = this;
103
105
  handleNotificationIntent(cordova.getActivity().getIntent(), false);
104
106
  handleLinkIntent(cordova.getActivity().getIntent(), false);
105
107
  registerSystemReceiver();
@@ -131,6 +133,9 @@ public class Html2ApkBridge extends CordovaPlugin {
131
133
  stopMicRecorderSilently();
132
134
  unregisterSystemReceiver();
133
135
  dispatchEvent("app:fechado", baseEvent("app:fechado"));
136
+ if (activeBridge == this) {
137
+ activeBridge = null;
138
+ }
134
139
  super.onDestroy();
135
140
  }
136
141
 
@@ -757,11 +762,6 @@ public class Html2ApkBridge extends CordovaPlugin {
757
762
  return;
758
763
  }
759
764
 
760
- if (shouldOpenSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS)) {
761
- callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS, true, false));
762
- return;
763
- }
764
-
765
765
  notificationPermissionCallback = callbackContext;
766
766
  rememberRuntimePermissionRequest(Manifest.permission.POST_NOTIFICATIONS);
767
767
  cordova.requestPermission(this, REQUEST_POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS);
@@ -777,11 +777,6 @@ public class Html2ApkBridge extends CordovaPlugin {
777
777
  return false;
778
778
  }
779
779
 
780
- if (shouldOpenSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS)) {
781
- callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS, true, false));
782
- return true;
783
- }
784
-
785
780
  pendingNotificationOptions = options;
786
781
  pendingNotificationSchedule = schedule;
787
782
  pendingNotificationCallback = callbackContext;
@@ -806,7 +801,7 @@ public class Html2ApkBridge extends CordovaPlugin {
806
801
  .setContentText(text)
807
802
  .setStyle(new NotificationCompat.BigTextStyle().bigText(text))
808
803
  .setAutoCancel(true)
809
- .setContentIntent(createContentIntent(context(), id, detailPayload(options)))
804
+ .setContentIntent(createNotificationClickIntent(context(), id, detailPayload(options)))
810
805
  .setPriority(NotificationCompat.PRIORITY_DEFAULT);
811
806
 
812
807
  addNotificationActions(builder, context(), id, options);
@@ -1069,11 +1064,6 @@ public class Html2ApkBridge extends CordovaPlugin {
1069
1064
  return;
1070
1065
  }
1071
1066
 
1072
- if (shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)) {
1073
- callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, false));
1074
- return;
1075
- }
1076
-
1077
1067
  cameraPermissionCallback = callbackContext;
1078
1068
  rememberRuntimePermissionRequest(Manifest.permission.CAMERA);
1079
1069
  cordova.requestPermission(this, REQUEST_CAMERA, Manifest.permission.CAMERA);
@@ -1092,11 +1082,6 @@ public class Html2ApkBridge extends CordovaPlugin {
1092
1082
  return;
1093
1083
  }
1094
1084
 
1095
- if (shouldOpenSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO)) {
1096
- callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO, true, false));
1097
- return;
1098
- }
1099
-
1100
1085
  microphonePermissionCallback = callbackContext;
1101
1086
  rememberRuntimePermissionRequest(Manifest.permission.RECORD_AUDIO);
1102
1087
  cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
@@ -1131,11 +1116,6 @@ public class Html2ApkBridge extends CordovaPlugin {
1131
1116
  }
1132
1117
 
1133
1118
  if (!hasMicrophonePermission()) {
1134
- if (shouldOpenSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO)) {
1135
- callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO, true, false));
1136
- return;
1137
- }
1138
-
1139
1119
  pendingMicStartCallback = callbackContext;
1140
1120
  rememberRuntimePermissionRequest(Manifest.permission.RECORD_AUDIO);
1141
1121
  cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
@@ -1319,11 +1299,6 @@ public class Html2ApkBridge extends CordovaPlugin {
1319
1299
 
1320
1300
  private void setFlashlightWithPermission(boolean enabled, boolean toggle, CallbackContext callbackContext) throws Exception {
1321
1301
  if (!hasCameraPermission()) {
1322
- if (shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)) {
1323
- callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, false));
1324
- return;
1325
- }
1326
-
1327
1302
  pendingFlashlightCallback = callbackContext;
1328
1303
  pendingFlashlightEnabled = enabled;
1329
1304
  pendingFlashlightToggle = toggle;
@@ -2016,6 +1991,167 @@ public class Html2ApkBridge extends CordovaPlugin {
2016
1991
  );
2017
1992
  }
2018
1993
 
1994
+ static PendingIntent createNotificationClickIntent(Context context, int id, JSONObject detail) {
1995
+ if (shouldOpenAppForNotificationClick(detail)) {
1996
+ return createContentIntent(context, id, detail);
1997
+ }
1998
+
1999
+ Intent intent = new Intent(context, NotificationClickReceiver.class);
2000
+ intent.putExtra(EXTRA_NOTIFICATION_CLICKED, true);
2001
+ intent.putExtra(EXTRA_NOTIFICATION_DETAIL, detail == null ? "{}" : detail.toString());
2002
+
2003
+ return PendingIntent.getBroadcast(
2004
+ context,
2005
+ id,
2006
+ intent,
2007
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
2008
+ );
2009
+ }
2010
+
2011
+ static void handleNotificationClickBroadcast(Context context, JSONObject detail) {
2012
+ if (detail == null) {
2013
+ return;
2014
+ }
2015
+
2016
+ if (activeBridge != null && activeBridge.webView != null) {
2017
+ activeBridge.dispatchNotificationClick(detail);
2018
+ return;
2019
+ }
2020
+
2021
+ handleNativeNotificationAction(context, detail);
2022
+ }
2023
+
2024
+ private static void handleNativeNotificationAction(Context context, JSONObject detail) {
2025
+ JSONObject action = notificationClickAction(detail);
2026
+ if (action == null) {
2027
+ return;
2028
+ }
2029
+
2030
+ String functionName = action.optString("funcao",
2031
+ action.optString("functionName",
2032
+ action.optString("function",
2033
+ action.optString("fn", action.optString("nomeFuncao", "")))));
2034
+
2035
+ if (!isExternalUrlFunction(functionName)) {
2036
+ return;
2037
+ }
2038
+
2039
+ String url = firstActionArgument(action);
2040
+ if (url.length() == 0) {
2041
+ url = action.optString("url", action.optString("href", ""));
2042
+ }
2043
+ if (url.length() == 0) {
2044
+ return;
2045
+ }
2046
+
2047
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
2048
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2049
+ try {
2050
+ context.startActivity(intent);
2051
+ } catch (Exception ignored) {
2052
+ }
2053
+ }
2054
+
2055
+ private static boolean isExternalUrlFunction(String functionName) {
2056
+ return "abrirForaDoApp".equals(functionName)
2057
+ || "abrirUrlExterno".equals(functionName)
2058
+ || "abrirUrl".equals(functionName)
2059
+ || "openOutsideApp".equals(functionName)
2060
+ || "openExternalUrl".equals(functionName)
2061
+ || "openUrl".equals(functionName);
2062
+ }
2063
+
2064
+ private static String firstActionArgument(JSONObject action) {
2065
+ JSONArray args = action.optJSONArray("argumentos");
2066
+ if (args == null) {
2067
+ args = action.optJSONArray("args");
2068
+ }
2069
+ if (args == null) {
2070
+ args = action.optJSONArray("parametros");
2071
+ }
2072
+ if (args == null) {
2073
+ args = action.optJSONArray("params");
2074
+ }
2075
+ if (args == null || args.length() == 0) {
2076
+ return "";
2077
+ }
2078
+ return args.optString(0, "");
2079
+ }
2080
+
2081
+ private static JSONObject notificationClickAction(JSONObject detail) {
2082
+ if (detail == null) {
2083
+ return null;
2084
+ }
2085
+
2086
+ JSONObject action = detail.optJSONObject("action");
2087
+ if (action != null) {
2088
+ JSONObject nested = action.optJSONObject("aoClicar");
2089
+ if (nested == null) {
2090
+ nested = action.optJSONObject("onClick");
2091
+ }
2092
+ return nested == null ? action : nested;
2093
+ }
2094
+
2095
+ action = detail.optJSONObject("acao");
2096
+ if (action != null) {
2097
+ JSONObject nested = action.optJSONObject("aoClicar");
2098
+ if (nested == null) {
2099
+ nested = action.optJSONObject("onClick");
2100
+ }
2101
+ return nested == null ? action : nested;
2102
+ }
2103
+
2104
+ action = detail.optJSONObject("aoClicar");
2105
+ if (action == null) {
2106
+ action = detail.optJSONObject("onClick");
2107
+ }
2108
+ return action;
2109
+ }
2110
+
2111
+ private static boolean shouldOpenAppForNotificationClick(JSONObject detail) {
2112
+ Boolean open = openFlag(detail);
2113
+ JSONObject action;
2114
+
2115
+ if (open != null) {
2116
+ return open.booleanValue();
2117
+ }
2118
+
2119
+ action = detail == null ? null : detail.optJSONObject("action");
2120
+ open = openFlag(action);
2121
+ if (open != null) {
2122
+ return open.booleanValue();
2123
+ }
2124
+
2125
+ action = detail == null ? null : detail.optJSONObject("acao");
2126
+ open = openFlag(action);
2127
+ if (open != null) {
2128
+ return open.booleanValue();
2129
+ }
2130
+
2131
+ action = notificationClickAction(detail);
2132
+ open = openFlag(action);
2133
+ return open == null || open.booleanValue();
2134
+ }
2135
+
2136
+ private static Boolean openFlag(JSONObject object) {
2137
+ if (object == null) {
2138
+ return null;
2139
+ }
2140
+ if (object.has("open")) {
2141
+ return Boolean.valueOf(object.optBoolean("open", true));
2142
+ }
2143
+ if (object.has("abrir")) {
2144
+ return Boolean.valueOf(object.optBoolean("abrir", true));
2145
+ }
2146
+ if (object.has("openApp")) {
2147
+ return Boolean.valueOf(object.optBoolean("openApp", true));
2148
+ }
2149
+ if (object.has("abrirApp")) {
2150
+ return Boolean.valueOf(object.optBoolean("abrirApp", true));
2151
+ }
2152
+ return null;
2153
+ }
2154
+
2019
2155
  static boolean canScheduleExactAlarms(Context context) {
2020
2156
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
2021
2157
  return true;
@@ -2219,9 +2355,13 @@ public class Html2ApkBridge extends CordovaPlugin {
2219
2355
  static JSONObject detailPayload(JSONObject options) throws Exception {
2220
2356
  JSONObject detail = new JSONObject();
2221
2357
  JSONObject click = options.optJSONObject("aoClicar");
2358
+ Boolean open = openFlag(options);
2222
2359
  if (click == null) {
2223
2360
  click = options.optJSONObject("onClick");
2224
2361
  }
2362
+ if (open == null) {
2363
+ open = openFlag(click);
2364
+ }
2225
2365
 
2226
2366
  detail.put("id", notificationId(options));
2227
2367
  detail.put("title", title(options));
@@ -2232,6 +2372,10 @@ public class Html2ApkBridge extends CordovaPlugin {
2232
2372
  detail.put("clickedAt", System.currentTimeMillis());
2233
2373
  detail.put("onClick", click == null ? new JSONObject().put("action", "open-app") : click);
2234
2374
  detail.put("aoClicar", click == null ? new JSONObject().put("acao", "abrir-app") : click);
2375
+ if (open != null) {
2376
+ detail.put("open", open.booleanValue());
2377
+ detail.put("abrir", open.booleanValue());
2378
+ }
2235
2379
  return detail;
2236
2380
  }
2237
2381
 
@@ -2259,7 +2403,7 @@ public class Html2ApkBridge extends CordovaPlugin {
2259
2403
  builder.addAction(
2260
2404
  context.getApplicationInfo().icon,
2261
2405
  title,
2262
- createContentIntent(context, notificationId * 100 + index + 1, detail)
2406
+ createNotificationClickIntent(context, notificationId * 100 + index + 1, detail)
2263
2407
  );
2264
2408
  } catch (Exception ignored) {
2265
2409
  }