html2apk 0.2.0 → 0.3.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.
@@ -1,26 +1,48 @@
1
1
  package dev.html2apk.bridge;
2
2
 
3
3
  import android.Manifest;
4
+ import android.app.Activity;
4
5
  import android.app.AlarmManager;
6
+ import android.app.ActivityManager;
5
7
  import android.app.NotificationChannel;
6
8
  import android.app.NotificationManager;
7
9
  import android.app.PendingIntent;
10
+ import android.content.ActivityNotFoundException;
11
+ import android.content.BroadcastReceiver;
8
12
  import android.content.ClipData;
9
13
  import android.content.ClipboardManager;
10
14
  import android.content.Context;
11
15
  import android.content.Intent;
16
+ import android.content.IntentFilter;
17
+ import android.content.pm.ApplicationInfo;
12
18
  import android.content.pm.PackageManager;
19
+ import android.database.Cursor;
20
+ import android.hardware.camera2.CameraCharacteristics;
21
+ import android.hardware.camera2.CameraManager;
22
+ import android.graphics.Color;
23
+ import android.media.MediaRecorder;
24
+ import android.net.ConnectivityManager;
25
+ import android.net.Network;
26
+ import android.net.NetworkCapabilities;
13
27
  import android.net.Uri;
28
+ import android.os.BatteryManager;
14
29
  import android.os.Build;
30
+ import android.os.Debug;
31
+ import android.os.Environment;
32
+ import android.os.StatFs;
15
33
  import android.os.VibrationEffect;
16
34
  import android.os.Vibrator;
17
35
  import android.provider.Settings;
36
+ import android.provider.OpenableColumns;
37
+ import android.util.Base64;
18
38
  import android.view.View;
39
+ import android.view.Window;
19
40
  import android.view.WindowManager;
20
41
  import android.widget.Toast;
21
42
 
22
43
  import androidx.core.app.NotificationCompat;
23
44
  import androidx.core.app.NotificationManagerCompat;
45
+ import androidx.core.content.ContextCompat;
24
46
 
25
47
  import org.apache.cordova.CallbackContext;
26
48
  import org.apache.cordova.CordovaInterface;
@@ -29,35 +51,80 @@ import org.apache.cordova.CordovaWebView;
29
51
  import org.json.JSONArray;
30
52
  import org.json.JSONObject;
31
53
 
54
+ import java.io.ByteArrayInputStream;
55
+ import java.io.ByteArrayOutputStream;
56
+ import java.io.File;
57
+ import java.io.FileInputStream;
58
+ import java.io.InputStream;
59
+ import java.io.OutputStream;
60
+ import java.util.List;
61
+
32
62
  public class Html2ApkBridge extends CordovaPlugin {
33
63
  static final String CHANNEL_ID = "html2apk_default";
34
64
  static final String EXTRA_NOTIFICATION_CLICKED = "html2apk_notification_clicked";
35
65
  static final String EXTRA_NOTIFICATION_DETAIL = "html2apk_notification_detail";
36
66
 
37
67
  private static final int REQUEST_POST_NOTIFICATIONS = 7311;
68
+ private static final int REQUEST_CAMERA = 7312;
69
+ private static final int REQUEST_RECORD_AUDIO = 7313;
70
+ private static final int REQUEST_PICK_FILE = 7411;
71
+ private static final int REQUEST_SAVE_FILE = 7412;
72
+ private static final int REQUEST_PICK_FOLDER = 7413;
38
73
 
39
74
  private CallbackContext notificationPermissionCallback;
75
+ private CallbackContext cameraPermissionCallback;
76
+ private CallbackContext microphonePermissionCallback;
77
+ private CallbackContext pendingMicStartCallback;
78
+ private CallbackContext filePickerCallback;
79
+ private CallbackContext saveFileCallback;
80
+ private CallbackContext folderPickerCallback;
81
+ private JSONObject pendingSaveFile;
40
82
  private JSONObject initialNotification;
83
+ private JSONObject initialLink;
41
84
  private boolean overlaySettingsOpened;
85
+ private boolean torchEnabled;
86
+ private MediaRecorder micRecorder;
87
+ private File micRecordingFile;
88
+ private long micRecordingStartedAt;
89
+ private BroadcastReceiver systemReceiver;
42
90
 
43
91
  @Override
44
92
  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
45
93
  super.initialize(cordova, webView);
46
94
  handleNotificationIntent(cordova.getActivity().getIntent(), false);
95
+ handleLinkIntent(cordova.getActivity().getIntent(), false);
96
+ registerSystemReceiver();
47
97
  startFloatingModeIfNeeded();
48
98
  }
49
99
 
50
100
  @Override
51
101
  public void onNewIntent(Intent intent) {
52
102
  handleNotificationIntent(intent, true);
103
+ handleLinkIntent(intent, true);
53
104
  }
54
105
 
55
106
  @Override
56
107
  public void onResume(boolean multitasking) {
57
108
  super.onResume(multitasking);
109
+ dispatchEvent("app:voltou", baseEvent("app:voltou"));
58
110
  startFloatingModeIfNeeded();
59
111
  }
60
112
 
113
+ @Override
114
+ public void onPause(boolean multitasking) {
115
+ dispatchEvent("app:pausado", baseEvent("app:pausado"));
116
+ dispatchEvent("app:background", baseEvent("app:background"));
117
+ super.onPause(multitasking);
118
+ }
119
+
120
+ @Override
121
+ public void onDestroy() {
122
+ stopMicRecorderSilently();
123
+ unregisterSystemReceiver();
124
+ dispatchEvent("app:fechado", baseEvent("app:fechado"));
125
+ super.onDestroy();
126
+ }
127
+
61
128
  @Override
62
129
  public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
63
130
  try {
@@ -70,7 +137,12 @@ public class Html2ApkBridge extends CordovaPlugin {
70
137
 
71
138
  if ("scheduleNotification".equals(action)) {
72
139
  JSONObject options = args.optJSONObject(0);
73
- scheduleNotification(options == null ? new JSONObject() : options);
140
+ callbackContext.success(scheduleNotification(options == null ? new JSONObject() : options));
141
+ return true;
142
+ }
143
+
144
+ if ("cancelNotification".equals(action)) {
145
+ cancelNotification(args.opt(0));
74
146
  callbackContext.success();
75
147
  return true;
76
148
  }
@@ -105,24 +177,155 @@ public class Html2ApkBridge extends CordovaPlugin {
105
177
  return true;
106
178
  }
107
179
 
180
+ if ("setSystemBarsColor".equals(action)) {
181
+ callbackContext.success(setSystemBarsColor(args.opt(0)));
182
+ return true;
183
+ }
184
+
185
+ if ("flashlight".equals(action)) {
186
+ setFlashlight(args.optBoolean(0, true));
187
+ callbackContext.success(flashlightStatus());
188
+ return true;
189
+ }
190
+
191
+ if ("toggleFlashlight".equals(action)) {
192
+ setFlashlight(!torchEnabled);
193
+ callbackContext.success(flashlightStatus());
194
+ return true;
195
+ }
196
+
197
+ if ("flashlightStatus".equals(action)) {
198
+ callbackContext.success(flashlightStatus());
199
+ return true;
200
+ }
201
+
202
+ if ("requestCameraPermission".equals(action)) {
203
+ requestCameraPermission(callbackContext);
204
+ return true;
205
+ }
206
+
207
+ if ("requestMicrophonePermission".equals(action)) {
208
+ requestMicrophonePermission(callbackContext);
209
+ return true;
210
+ }
211
+
212
+ if ("microphoneStatus".equals(action)) {
213
+ callbackContext.success(microphoneStatus());
214
+ return true;
215
+ }
216
+
217
+ if ("startMic".equals(action)) {
218
+ startMicRecording(callbackContext);
219
+ return true;
220
+ }
221
+
222
+ if ("stopMic".equals(action)) {
223
+ callbackContext.success(stopMicRecording());
224
+ return true;
225
+ }
226
+
108
227
  if ("copyText".equals(action)) {
109
228
  copyText(args.optString(0, ""));
110
229
  callbackContext.success();
111
230
  return true;
112
231
  }
113
232
 
233
+ if ("readText".equals(action)) {
234
+ callbackContext.success(readText());
235
+ return true;
236
+ }
237
+
114
238
  if ("shareText".equals(action)) {
115
239
  shareText(args.optString(0, ""));
116
240
  callbackContext.success();
117
241
  return true;
118
242
  }
119
243
 
244
+ if ("share".equals(action)) {
245
+ share(args.optJSONObject(0));
246
+ callbackContext.success();
247
+ return true;
248
+ }
249
+
120
250
  if ("openUrl".equals(action)) {
121
251
  openUrl(args.optString(0, ""));
122
252
  callbackContext.success();
123
253
  return true;
124
254
  }
125
255
 
256
+ if ("dial".equals(action)) {
257
+ dial(args.optString(0, ""));
258
+ callbackContext.success();
259
+ return true;
260
+ }
261
+
262
+ if ("openMap".equals(action)) {
263
+ openMap(args.optString(0, ""));
264
+ callbackContext.success();
265
+ return true;
266
+ }
267
+
268
+ if ("openWhatsapp".equals(action)) {
269
+ openWhatsapp(args.optString(0, ""), args.optString(1, ""));
270
+ callbackContext.success();
271
+ return true;
272
+ }
273
+
274
+ if ("pickFile".equals(action)) {
275
+ pickFile(args.optJSONObject(0), callbackContext);
276
+ return true;
277
+ }
278
+
279
+ if ("pickFolder".equals(action)) {
280
+ pickFolder(callbackContext);
281
+ return true;
282
+ }
283
+
284
+ if ("saveFile".equals(action)) {
285
+ saveFile(args.optJSONObject(0), callbackContext);
286
+ return true;
287
+ }
288
+
289
+ if ("deviceInfo".equals(action)) {
290
+ callbackContext.success(deviceInfo());
291
+ return true;
292
+ }
293
+
294
+ if ("networkInfo".equals(action)) {
295
+ callbackContext.success(networkInfo());
296
+ return true;
297
+ }
298
+
299
+ if ("batteryInfo".equals(action)) {
300
+ callbackContext.success(batteryInfo());
301
+ return true;
302
+ }
303
+
304
+ if ("memoryInfo".equals(action)) {
305
+ callbackContext.success(memoryInfo());
306
+ return true;
307
+ }
308
+
309
+ if ("storageInfo".equals(action)) {
310
+ callbackContext.success(storageInfo());
311
+ return true;
312
+ }
313
+
314
+ if ("performanceInfo".equals(action)) {
315
+ callbackContext.success(performanceInfo());
316
+ return true;
317
+ }
318
+
319
+ if ("openAppsMemory".equals(action)) {
320
+ callbackContext.success(openAppsMemoryInfo());
321
+ return true;
322
+ }
323
+
324
+ if ("permissionStatus".equals(action)) {
325
+ callbackContext.success(permissionStatus(args.optJSONArray(0)));
326
+ return true;
327
+ }
328
+
126
329
  if ("requestNotificationPermission".equals(action)) {
127
330
  requestNotificationPermission(callbackContext);
128
331
  return true;
@@ -172,6 +375,12 @@ public class Html2ApkBridge extends CordovaPlugin {
172
375
  initialNotification = null;
173
376
  return true;
174
377
  }
378
+
379
+ if ("getInitialLink".equals(action)) {
380
+ callbackContext.success(initialLink == null ? new JSONObject() : initialLink);
381
+ initialLink = null;
382
+ return true;
383
+ }
175
384
  } catch (Exception error) {
176
385
  callbackContext.error(error.getMessage());
177
386
  return true;
@@ -182,6 +391,49 @@ public class Html2ApkBridge extends CordovaPlugin {
182
391
 
183
392
  @Override
184
393
  public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
394
+ if (requestCode == REQUEST_CAMERA && cameraPermissionCallback != null) {
395
+ boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
396
+ try {
397
+ JSONObject result = new JSONObject();
398
+ result.put("permission", "android.permission.CAMERA");
399
+ result.put("required", true);
400
+ result.put("requested", true);
401
+ result.put("granted", granted);
402
+ cameraPermissionCallback.success(result);
403
+ } catch (Exception error) {
404
+ cameraPermissionCallback.error(error.getMessage());
405
+ } finally {
406
+ cameraPermissionCallback = null;
407
+ }
408
+ return;
409
+ }
410
+
411
+ if (requestCode == REQUEST_RECORD_AUDIO) {
412
+ boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
413
+ CallbackContext callback = pendingMicStartCallback != null ? pendingMicStartCallback : microphonePermissionCallback;
414
+ boolean shouldStartRecording = pendingMicStartCallback != null;
415
+ pendingMicStartCallback = null;
416
+ microphonePermissionCallback = null;
417
+
418
+ if (callback == null) {
419
+ return;
420
+ }
421
+
422
+ try {
423
+ if (shouldStartRecording && granted) {
424
+ startMicRecorder(callback);
425
+ } else {
426
+ JSONObject result = microphoneStatus();
427
+ result.put("requested", true);
428
+ result.put("granted", granted);
429
+ callback.success(result);
430
+ }
431
+ } catch (Exception error) {
432
+ callback.error(error.getMessage());
433
+ }
434
+ return;
435
+ }
436
+
185
437
  if (requestCode != REQUEST_POST_NOTIFICATIONS || notificationPermissionCallback == null) {
186
438
  return;
187
439
  }
@@ -199,6 +451,23 @@ public class Html2ApkBridge extends CordovaPlugin {
199
451
  }
200
452
  }
201
453
 
454
+ @Override
455
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
456
+ if (requestCode == REQUEST_PICK_FILE) {
457
+ handlePickFileResult(resultCode, intent);
458
+ return;
459
+ }
460
+
461
+ if (requestCode == REQUEST_SAVE_FILE) {
462
+ handleSaveFileResult(resultCode, intent);
463
+ return;
464
+ }
465
+
466
+ if (requestCode == REQUEST_PICK_FOLDER) {
467
+ handlePickFolderResult(resultCode, intent);
468
+ }
469
+ }
470
+
202
471
  private Context context() {
203
472
  return this.cordova.getActivity().getApplicationContext();
204
473
  }
@@ -323,27 +592,46 @@ public class Html2ApkBridge extends CordovaPlugin {
323
592
  .setContentIntent(createContentIntent(context(), id, detailPayload(options)))
324
593
  .setPriority(NotificationCompat.PRIORITY_DEFAULT);
325
594
 
595
+ addNotificationActions(builder, context(), id, options);
326
596
  NotificationManagerCompat.from(context()).notify(id, builder.build());
597
+ dispatchEvent("notificacao:recebida", detailPayload(options));
327
598
  }
328
599
 
329
- private void scheduleNotification(JSONObject options) throws Exception {
600
+ private JSONObject scheduleNotification(JSONObject options) throws Exception {
330
601
  long when = options.optLong("quando", options.optLong("when", System.currentTimeMillis() + 60000));
331
602
  if (when < System.currentTimeMillis()) {
332
603
  when = System.currentTimeMillis() + 1000;
333
604
  }
334
605
 
335
606
  int id = notificationId(options);
607
+ options.put("id", id);
608
+ options.put("quando", when);
609
+ options.put("when", when);
336
610
  NotificationStore.save(context(), id, when, options);
337
611
  NotificationReceiver.schedule(context(), id, when, options, canScheduleExactAlarms());
612
+
613
+ JSONObject result = new JSONObject();
614
+ result.put("id", id);
615
+ result.put("when", when);
616
+ result.put("quando", when);
617
+ result.put("repeating", repeatInterval(options) > 0);
618
+ result.put("loop", loopNotifications(options).length() > 0);
619
+ return result;
338
620
  }
339
621
 
340
- private boolean canScheduleExactAlarms() {
341
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
342
- return true;
622
+ private void cancelNotification(Object input) throws Exception {
623
+ int id = notificationIdFromObject(input);
624
+ if (id == 0) {
625
+ throw new Exception("Notification id is required.");
343
626
  }
344
627
 
345
- AlarmManager alarmManager = (AlarmManager) context().getSystemService(Context.ALARM_SERVICE);
346
- return alarmManager != null && alarmManager.canScheduleExactAlarms();
628
+ NotificationReceiver.cancel(context(), id);
629
+ NotificationStore.remove(context(), id);
630
+ NotificationManagerCompat.from(context()).cancel(id);
631
+ }
632
+
633
+ private boolean canScheduleExactAlarms() {
634
+ return canScheduleExactAlarms(context());
347
635
  }
348
636
 
349
637
  private void openExactAlarmSettings() {
@@ -428,6 +716,83 @@ public class Html2ApkBridge extends CordovaPlugin {
428
716
  });
429
717
  }
430
718
 
719
+ private JSONObject setSystemBarsColor(Object input) throws Exception {
720
+ final JSONObject options = normalizeSystemBarsOptions(input);
721
+ final String statusColor = options.optString("statusBarColor", options.optString("color", options.optString("cor", "#126fff")));
722
+ final String navigationColor = options.optString("navigationBarColor", options.optString("navigationColor", statusColor));
723
+ final boolean darkStatusIcons = options.optBoolean("darkIcons", isLightColor(statusColor));
724
+ final boolean darkNavigationIcons = options.optBoolean("darkNavigationIcons", isLightColor(navigationColor));
725
+ final int statusBarColor = Color.parseColor(statusColor);
726
+ final int navigationBarColor = Color.parseColor(navigationColor);
727
+
728
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
729
+ @Override
730
+ public void run() {
731
+ Window window = cordova.getActivity().getWindow();
732
+ window.setStatusBarColor(statusBarColor);
733
+ window.setNavigationBarColor(navigationBarColor);
734
+ applySystemBarIconContrast(window, darkStatusIcons, darkNavigationIcons);
735
+ }
736
+ });
737
+
738
+ JSONObject result = new JSONObject();
739
+ result.put("statusBarColor", statusColor);
740
+ result.put("navigationBarColor", navigationColor);
741
+ result.put("darkIcons", darkStatusIcons);
742
+ result.put("darkNavigationIcons", darkNavigationIcons);
743
+ result.put("applied", true);
744
+ return result;
745
+ }
746
+
747
+ private JSONObject normalizeSystemBarsOptions(Object input) throws Exception {
748
+ if (input instanceof JSONObject) {
749
+ return (JSONObject) input;
750
+ }
751
+
752
+ JSONObject options = new JSONObject();
753
+ String color = input == null ? "#126fff" : String.valueOf(input);
754
+ options.put("color", color);
755
+ options.put("statusBarColor", color);
756
+ options.put("navigationBarColor", color);
757
+ return options;
758
+ }
759
+
760
+ private boolean isLightColor(String color) {
761
+ try {
762
+ int parsed = Color.parseColor(color);
763
+ int red = Color.red(parsed);
764
+ int green = Color.green(parsed);
765
+ int blue = Color.blue(parsed);
766
+ double luminance = (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
767
+ return luminance > 158;
768
+ } catch (Exception ignored) {
769
+ return false;
770
+ }
771
+ }
772
+
773
+ private void applySystemBarIconContrast(Window window, boolean darkStatusIcons, boolean darkNavigationIcons) {
774
+ View decor = window.getDecorView();
775
+ int flags = decor.getSystemUiVisibility();
776
+
777
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
778
+ if (darkStatusIcons) {
779
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
780
+ } else {
781
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
782
+ }
783
+ }
784
+
785
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
786
+ if (darkNavigationIcons) {
787
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
788
+ } else {
789
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
790
+ }
791
+ }
792
+
793
+ decor.setSystemUiVisibility(flags);
794
+ }
795
+
431
796
  private void copyText(String text) {
432
797
  ClipboardManager clipboard = (ClipboardManager) context().getSystemService(Context.CLIPBOARD_SERVICE);
433
798
  if (clipboard != null) {
@@ -451,82 +816,1138 @@ public class Html2ApkBridge extends CordovaPlugin {
451
816
  cordova.getActivity().startActivity(intent);
452
817
  }
453
818
 
454
- private void handleNotificationIntent(Intent intent, boolean dispatchToJs) {
455
- if (intent == null || !intent.getBooleanExtra(EXTRA_NOTIFICATION_CLICKED, false)) {
819
+ private boolean hasCameraPermission() {
820
+ return ContextCompat.checkSelfPermission(context(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
821
+ }
822
+
823
+ private void requestCameraPermission(CallbackContext callbackContext) throws Exception {
824
+ if (hasCameraPermission()) {
825
+ JSONObject result = new JSONObject();
826
+ result.put("permission", "android.permission.CAMERA");
827
+ result.put("required", true);
828
+ result.put("granted", true);
829
+ callbackContext.success(result);
456
830
  return;
457
831
  }
458
832
 
459
- JSONObject detail = parseDetail(intent.getStringExtra(EXTRA_NOTIFICATION_DETAIL));
460
- initialNotification = detail;
833
+ cameraPermissionCallback = callbackContext;
834
+ cordova.requestPermission(this, REQUEST_CAMERA, Manifest.permission.CAMERA);
835
+ }
461
836
 
462
- if (dispatchToJs) {
463
- dispatchNotificationClick(detail);
837
+ private boolean hasMicrophonePermission() {
838
+ return ContextCompat.checkSelfPermission(context(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
839
+ }
840
+
841
+ private void requestMicrophonePermission(CallbackContext callbackContext) throws Exception {
842
+ if (hasMicrophonePermission()) {
843
+ JSONObject result = microphoneStatus();
844
+ result.put("requested", false);
845
+ result.put("granted", true);
846
+ callbackContext.success(result);
847
+ return;
464
848
  }
465
849
 
466
- intent.removeExtra(EXTRA_NOTIFICATION_CLICKED);
467
- intent.removeExtra(EXTRA_NOTIFICATION_DETAIL);
850
+ microphonePermissionCallback = callbackContext;
851
+ cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
468
852
  }
469
853
 
470
- private void dispatchNotificationClick(JSONObject detail) {
471
- if (detail == null || webView == null) {
854
+ private JSONObject microphoneStatus() throws Exception {
855
+ JSONObject result = new JSONObject();
856
+ result.put("permission", "android.permission.RECORD_AUDIO");
857
+ result.put("required", true);
858
+ result.put("granted", hasMicrophonePermission());
859
+ result.put("permissionGranted", hasMicrophonePermission());
860
+ result.put("recording", micRecorder != null);
861
+ result.put("gravando", micRecorder != null);
862
+ if (micRecorder != null) {
863
+ result.put("startedAt", micRecordingStartedAt);
864
+ result.put("iniciadoEm", micRecordingStartedAt);
865
+ result.put("durationMs", System.currentTimeMillis() - micRecordingStartedAt);
866
+ result.put("duracaoMs", System.currentTimeMillis() - micRecordingStartedAt);
867
+ }
868
+ return result;
869
+ }
870
+
871
+ private void startMicRecording(CallbackContext callbackContext) throws Exception {
872
+ if (micRecorder != null) {
873
+ JSONObject result = microphoneStatus();
874
+ result.put("alreadyRecording", true);
875
+ result.put("jaGravando", true);
876
+ callbackContext.success(result);
472
877
  return;
473
878
  }
474
879
 
475
- final String script = "(function(){var detail=" + detail.toString()
476
- + ";window.dispatchEvent(new CustomEvent('html2apk:notification',{detail:detail}));"
477
- + "if(window.Html2ApkNative&&typeof window.Html2ApkNative.__emitNotificationClick==='function'){"
478
- + "window.Html2ApkNative.__emitNotificationClick(detail);}})();";
880
+ if (!hasMicrophonePermission()) {
881
+ pendingMicStartCallback = callbackContext;
882
+ cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
883
+ return;
884
+ }
479
885
 
480
- cordova.getActivity().runOnUiThread(new Runnable() {
481
- @Override
482
- public void run() {
483
- webView.getEngine().evaluateJavascript(script, null);
886
+ startMicRecorder(callbackContext);
887
+ }
888
+
889
+ private void startMicRecorder(CallbackContext callbackContext) throws Exception {
890
+ File audioDir = new File(context().getCacheDir(), "html2apk-audio");
891
+ if (!audioDir.exists() && !audioDir.mkdirs()) {
892
+ throw new Exception("Could not create audio cache directory.");
893
+ }
894
+
895
+ File audioFile = File.createTempFile("mic-", ".m4a", audioDir);
896
+ MediaRecorder recorder = new MediaRecorder();
897
+ try {
898
+ recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
899
+ recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
900
+ recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
901
+ recorder.setAudioEncodingBitRate(128000);
902
+ recorder.setAudioSamplingRate(44100);
903
+ recorder.setOutputFile(audioFile.getAbsolutePath());
904
+ recorder.prepare();
905
+ recorder.start();
906
+ } catch (Exception error) {
907
+ try {
908
+ recorder.release();
909
+ } catch (Exception ignored) {
484
910
  }
485
- });
911
+ audioFile.delete();
912
+ throw error;
913
+ }
914
+
915
+ micRecordingFile = audioFile;
916
+ micRecordingStartedAt = System.currentTimeMillis();
917
+ micRecorder = recorder;
918
+
919
+ JSONObject result = microphoneStatus();
920
+ result.put("mimeType", "audio/mp4");
921
+ result.put("extension", "m4a");
922
+ result.put("extensao", "m4a");
923
+ result.put("fileName", micRecordingFile.getName());
924
+ result.put("nomeArquivo", micRecordingFile.getName());
925
+ callbackContext.success(result);
486
926
  }
487
927
 
488
- private JSONObject parseDetail(String raw) {
928
+ private JSONObject stopMicRecording() throws Exception {
929
+ if (micRecorder == null || micRecordingFile == null) {
930
+ throw new Exception("Microphone is not recording. Call ouvirMic() first.");
931
+ }
932
+
933
+ MediaRecorder recorder = micRecorder;
934
+ File audioFile = micRecordingFile;
935
+ long startedAt = micRecordingStartedAt;
936
+ long endedAt;
937
+ Exception stopError = null;
938
+
939
+ micRecorder = null;
940
+ micRecordingFile = null;
941
+ micRecordingStartedAt = 0;
942
+
489
943
  try {
490
- if (raw == null || raw.length() == 0) {
491
- return null;
944
+ recorder.stop();
945
+ } catch (RuntimeException error) {
946
+ stopError = new Exception("Could not finalize audio recording. Wait a little longer before calling pararMic().");
947
+ } finally {
948
+ try {
949
+ recorder.reset();
950
+ } catch (Exception ignored) {
492
951
  }
493
- return new JSONObject(raw);
952
+ try {
953
+ recorder.release();
954
+ } catch (Exception ignored) {
955
+ }
956
+ }
957
+
958
+ if (stopError != null) {
959
+ audioFile.delete();
960
+ throw stopError;
961
+ }
962
+
963
+ endedAt = System.currentTimeMillis();
964
+ byte[] audioBytes = readFileBytes(audioFile);
965
+ String base64 = Base64.encodeToString(audioBytes, Base64.NO_WRAP);
966
+ audioFile.delete();
967
+
968
+ JSONObject result = new JSONObject();
969
+ result.put("base64", base64);
970
+ result.put("mimeType", "audio/mp4");
971
+ result.put("extension", "m4a");
972
+ result.put("extensao", "m4a");
973
+ result.put("size", audioBytes.length);
974
+ result.put("tamanho", audioBytes.length);
975
+ result.put("startedAt", startedAt);
976
+ result.put("iniciadoEm", startedAt);
977
+ result.put("endedAt", endedAt);
978
+ result.put("finalizadoEm", endedAt);
979
+ result.put("durationMs", endedAt - startedAt);
980
+ result.put("duracaoMs", endedAt - startedAt);
981
+ result.put("recording", false);
982
+ result.put("gravando", false);
983
+ return result;
984
+ }
985
+
986
+ private byte[] readFileBytes(File file) throws Exception {
987
+ InputStream inputStream = new FileInputStream(file);
988
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
989
+ try {
990
+ byte[] buffer = new byte[8192];
991
+ int read;
992
+ while ((read = inputStream.read(buffer)) != -1) {
993
+ outputStream.write(buffer, 0, read);
994
+ }
995
+ return outputStream.toByteArray();
996
+ } finally {
997
+ inputStream.close();
998
+ outputStream.close();
999
+ }
1000
+ }
1001
+
1002
+ private void stopMicRecorderSilently() {
1003
+ if (micRecorder == null) {
1004
+ return;
1005
+ }
1006
+
1007
+ MediaRecorder recorder = micRecorder;
1008
+ File audioFile = micRecordingFile;
1009
+ micRecorder = null;
1010
+ micRecordingFile = null;
1011
+ micRecordingStartedAt = 0;
1012
+
1013
+ try {
1014
+ recorder.stop();
494
1015
  } catch (Exception ignored) {
495
- return null;
1016
+ }
1017
+ try {
1018
+ recorder.reset();
1019
+ } catch (Exception ignored) {
1020
+ }
1021
+ try {
1022
+ recorder.release();
1023
+ } catch (Exception ignored) {
1024
+ }
1025
+ if (audioFile != null) {
1026
+ audioFile.delete();
496
1027
  }
497
1028
  }
498
1029
 
499
- static PendingIntent createContentIntent(Context context, int id, JSONObject detail) {
500
- Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
501
- if (launchIntent == null) {
502
- launchIntent = new Intent();
503
- launchIntent.setPackage(context.getPackageName());
1030
+ private String torchCameraId() throws Exception {
1031
+ CameraManager manager = (CameraManager) context().getSystemService(Context.CAMERA_SERVICE);
1032
+ if (manager == null) {
1033
+ throw new Exception("Camera service is not available.");
504
1034
  }
505
1035
 
506
- launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
507
- launchIntent.putExtra(EXTRA_NOTIFICATION_CLICKED, true);
508
- launchIntent.putExtra(EXTRA_NOTIFICATION_DETAIL, detail == null ? "{}" : detail.toString());
1036
+ for (String cameraId : manager.getCameraIdList()) {
1037
+ CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
1038
+ Boolean flashAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
1039
+ if (Boolean.TRUE.equals(flashAvailable)) {
1040
+ return cameraId;
1041
+ }
1042
+ }
509
1043
 
510
- return PendingIntent.getActivity(
511
- context,
512
- id,
513
- launchIntent,
514
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
515
- );
1044
+ throw new Exception("This device does not expose a flashlight.");
516
1045
  }
517
1046
 
518
- static JSONObject detailPayload(JSONObject options) throws Exception {
519
- JSONObject detail = new JSONObject();
520
- JSONObject click = options.optJSONObject("aoClicar");
521
- if (click == null) {
522
- click = options.optJSONObject("onClick");
1047
+ private void setFlashlight(boolean enabled) throws Exception {
1048
+ if (!hasCameraPermission()) {
1049
+ throw new Exception("CAMERA permission is not granted. Call solicitarPermissaoCamera() first.");
523
1050
  }
524
1051
 
525
- detail.put("id", notificationId(options));
526
- detail.put("title", title(options));
527
- detail.put("titulo", title(options));
528
- detail.put("text", text(options));
529
- detail.put("texto", text(options));
1052
+ CameraManager manager = (CameraManager) context().getSystemService(Context.CAMERA_SERVICE);
1053
+ if (manager == null) {
1054
+ throw new Exception("Camera service is not available.");
1055
+ }
1056
+
1057
+ manager.setTorchMode(torchCameraId(), enabled);
1058
+ torchEnabled = enabled;
1059
+ }
1060
+
1061
+ private JSONObject flashlightStatus() throws Exception {
1062
+ JSONObject result = new JSONObject();
1063
+ result.put("available", context().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH));
1064
+ result.put("enabled", torchEnabled);
1065
+ result.put("permission", "android.permission.CAMERA");
1066
+ result.put("permissionGranted", hasCameraPermission());
1067
+ return result;
1068
+ }
1069
+
1070
+ private String readText() {
1071
+ ClipboardManager clipboard = (ClipboardManager) context().getSystemService(Context.CLIPBOARD_SERVICE);
1072
+ if (clipboard == null || !clipboard.hasPrimaryClip() || clipboard.getPrimaryClip() == null) {
1073
+ return "";
1074
+ }
1075
+
1076
+ ClipData clip = clipboard.getPrimaryClip();
1077
+ if (clip.getItemCount() == 0 || clip.getItemAt(0) == null) {
1078
+ return "";
1079
+ }
1080
+
1081
+ CharSequence text = clip.getItemAt(0).coerceToText(context());
1082
+ return text == null ? "" : text.toString();
1083
+ }
1084
+
1085
+ private void share(JSONObject options) {
1086
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
1087
+ String text = safeOptions.optString("texto", safeOptions.optString("text", ""));
1088
+ String url = safeOptions.optString("url", "");
1089
+ String title = safeOptions.optString("titulo", safeOptions.optString("title", "Compartilhar"));
1090
+ StringBuilder content = new StringBuilder();
1091
+ if (text.length() > 0) {
1092
+ content.append(text);
1093
+ }
1094
+ if (url.length() > 0) {
1095
+ if (content.length() > 0) {
1096
+ content.append("\n");
1097
+ }
1098
+ content.append(url);
1099
+ }
1100
+
1101
+ Intent intent = new Intent(Intent.ACTION_SEND);
1102
+ intent.setType(safeOptions.optString("mimeType", "text/plain"));
1103
+ intent.putExtra(Intent.EXTRA_TEXT, content.toString());
1104
+ intent.putExtra(Intent.EXTRA_TITLE, title);
1105
+ cordova.getActivity().startActivity(Intent.createChooser(intent, title));
1106
+ }
1107
+
1108
+ private void dial(String phone) throws Exception {
1109
+ String cleaned = phone == null ? "" : phone.trim();
1110
+ if (cleaned.length() == 0) {
1111
+ throw new Exception("Phone number is required.");
1112
+ }
1113
+
1114
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + Uri.encode(cleaned)));
1115
+ cordova.getActivity().startActivity(intent);
1116
+ }
1117
+
1118
+ private void openMap(String query) throws Exception {
1119
+ String cleaned = query == null ? "" : query.trim();
1120
+ if (cleaned.length() == 0) {
1121
+ throw new Exception("Map query is required.");
1122
+ }
1123
+
1124
+ Uri uri = cleaned.startsWith("geo:") || cleaned.startsWith("http")
1125
+ ? Uri.parse(cleaned)
1126
+ : Uri.parse("geo:0,0?q=" + Uri.encode(cleaned));
1127
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1128
+ cordova.getActivity().startActivity(intent);
1129
+ }
1130
+
1131
+ private void openWhatsapp(String phone, String message) throws Exception {
1132
+ String digits = phone == null ? "" : phone.replaceAll("[^0-9]", "");
1133
+ if (digits.length() == 0) {
1134
+ throw new Exception("WhatsApp phone number is required.");
1135
+ }
1136
+
1137
+ String url = "https://wa.me/" + digits;
1138
+ if (message != null && message.trim().length() > 0) {
1139
+ url += "?text=" + Uri.encode(message.trim());
1140
+ }
1141
+ openUrl(url);
1142
+ }
1143
+
1144
+ private void pickFile(JSONObject options, CallbackContext callbackContext) {
1145
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
1146
+ String kind = safeOptions.optString("tipo", safeOptions.optString("kind", "file"));
1147
+ boolean multiple = safeOptions.optBoolean("multiplo", safeOptions.optBoolean("multiple", false));
1148
+ String mimeType = mimeTypeForPicker(kind, safeOptions);
1149
+
1150
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
1151
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
1152
+ intent.setType(mimeType);
1153
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple);
1154
+
1155
+ filePickerCallback = callbackContext;
1156
+ cordova.startActivityForResult(this, intent, REQUEST_PICK_FILE);
1157
+ }
1158
+
1159
+ private String mimeTypeForPicker(String kind, JSONObject options) {
1160
+ String type = options.optString("mimeType", options.optString("tipoMime", ""));
1161
+ if (type.length() > 0) {
1162
+ return type;
1163
+ }
1164
+
1165
+ JSONArray types = options.optJSONArray("tipos");
1166
+ if (types == null) {
1167
+ types = options.optJSONArray("types");
1168
+ }
1169
+ if (types != null && types.length() == 1) {
1170
+ return types.optString(0, "*/*");
1171
+ }
1172
+
1173
+ if ("image".equals(kind)) {
1174
+ return "image/*";
1175
+ }
1176
+ if ("video".equals(kind)) {
1177
+ return "video/*";
1178
+ }
1179
+ if ("media".equals(kind)) {
1180
+ return "image/*";
1181
+ }
1182
+ return "*/*";
1183
+ }
1184
+
1185
+ private void pickFolder(CallbackContext callbackContext) {
1186
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
1187
+ folderPickerCallback = callbackContext;
1188
+ cordova.startActivityForResult(this, intent, REQUEST_PICK_FOLDER);
1189
+ }
1190
+
1191
+ private void saveFile(JSONObject options, CallbackContext callbackContext) throws Exception {
1192
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
1193
+ String name = safeOptions.optString("nome", safeOptions.optString("name", "arquivo.txt"));
1194
+ String mimeType = safeOptions.optString("mimeType", "text/plain");
1195
+
1196
+ pendingSaveFile = safeOptions;
1197
+ saveFileCallback = callbackContext;
1198
+
1199
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
1200
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
1201
+ intent.setType(mimeType);
1202
+ intent.putExtra(Intent.EXTRA_TITLE, name);
1203
+ cordova.startActivityForResult(this, intent, REQUEST_SAVE_FILE);
1204
+ }
1205
+
1206
+ private void handlePickFileResult(int resultCode, Intent intent) {
1207
+ CallbackContext callback = filePickerCallback;
1208
+ filePickerCallback = null;
1209
+ if (callback == null) {
1210
+ return;
1211
+ }
1212
+ if (resultCode != Activity.RESULT_OK || intent == null) {
1213
+ callback.success(new JSONArray());
1214
+ return;
1215
+ }
1216
+
1217
+ try {
1218
+ JSONArray items = new JSONArray();
1219
+ if (intent.getClipData() != null) {
1220
+ ClipData clipData = intent.getClipData();
1221
+ for (int index = 0; index < clipData.getItemCount(); index += 1) {
1222
+ items.put(fileInfo(clipData.getItemAt(index).getUri()));
1223
+ }
1224
+ } else if (intent.getData() != null) {
1225
+ items.put(fileInfo(intent.getData()));
1226
+ }
1227
+ callback.success(items);
1228
+ } catch (Exception error) {
1229
+ callback.error(error.getMessage());
1230
+ }
1231
+ }
1232
+
1233
+ private void handlePickFolderResult(int resultCode, Intent intent) {
1234
+ CallbackContext callback = folderPickerCallback;
1235
+ folderPickerCallback = null;
1236
+ if (callback == null) {
1237
+ return;
1238
+ }
1239
+ if (resultCode != Activity.RESULT_OK || intent == null || intent.getData() == null) {
1240
+ callback.success(new JSONObject());
1241
+ return;
1242
+ }
1243
+
1244
+ try {
1245
+ JSONObject result = new JSONObject();
1246
+ result.put("uri", intent.getData().toString());
1247
+ callback.success(result);
1248
+ } catch (Exception error) {
1249
+ callback.error(error.getMessage());
1250
+ }
1251
+ }
1252
+
1253
+ private void handleSaveFileResult(int resultCode, Intent intent) {
1254
+ CallbackContext callback = saveFileCallback;
1255
+ JSONObject options = pendingSaveFile;
1256
+ saveFileCallback = null;
1257
+ pendingSaveFile = null;
1258
+ if (callback == null) {
1259
+ return;
1260
+ }
1261
+ if (resultCode != Activity.RESULT_OK || intent == null || intent.getData() == null) {
1262
+ callback.success(new JSONObject());
1263
+ return;
1264
+ }
1265
+
1266
+ try {
1267
+ Uri uri = intent.getData();
1268
+ writePickedFile(uri, options == null ? new JSONObject() : options);
1269
+ JSONObject result = fileInfo(uri);
1270
+ result.put("saved", true);
1271
+ callback.success(result);
1272
+ } catch (Exception error) {
1273
+ callback.error(error.getMessage());
1274
+ }
1275
+ }
1276
+
1277
+ private JSONObject fileInfo(Uri uri) throws Exception {
1278
+ JSONObject result = new JSONObject();
1279
+ result.put("uri", uri.toString());
1280
+ result.put("mimeType", context().getContentResolver().getType(uri));
1281
+
1282
+ Cursor cursor = context().getContentResolver().query(uri, null, null, null, null);
1283
+ if (cursor != null) {
1284
+ try {
1285
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
1286
+ int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
1287
+ if (cursor.moveToFirst()) {
1288
+ if (nameIndex >= 0) {
1289
+ result.put("name", cursor.getString(nameIndex));
1290
+ result.put("nome", cursor.getString(nameIndex));
1291
+ }
1292
+ if (sizeIndex >= 0) {
1293
+ result.put("size", cursor.getLong(sizeIndex));
1294
+ result.put("tamanho", cursor.getLong(sizeIndex));
1295
+ }
1296
+ }
1297
+ } finally {
1298
+ cursor.close();
1299
+ }
1300
+ }
1301
+ return result;
1302
+ }
1303
+
1304
+ private void writePickedFile(Uri uri, JSONObject options) throws Exception {
1305
+ OutputStream outputStream = context().getContentResolver().openOutputStream(uri);
1306
+ if (outputStream == null) {
1307
+ throw new Exception("Could not open output stream.");
1308
+ }
1309
+
1310
+ try {
1311
+ InputStream inputStream;
1312
+ String base64 = options.optString("base64", "");
1313
+ if (base64.length() > 0) {
1314
+ inputStream = new ByteArrayInputStream(Base64.decode(base64, Base64.DEFAULT));
1315
+ } else {
1316
+ String content = options.optString("conteudo", options.optString("content", ""));
1317
+ inputStream = new ByteArrayInputStream(content.getBytes("UTF-8"));
1318
+ }
1319
+
1320
+ byte[] buffer = new byte[8192];
1321
+ int read;
1322
+ while ((read = inputStream.read(buffer)) != -1) {
1323
+ outputStream.write(buffer, 0, read);
1324
+ }
1325
+ inputStream.close();
1326
+ } finally {
1327
+ outputStream.close();
1328
+ }
1329
+ }
1330
+
1331
+ private JSONObject deviceInfo() throws Exception {
1332
+ JSONObject result = new JSONObject();
1333
+ result.put("manufacturer", Build.MANUFACTURER);
1334
+ result.put("fabricante", Build.MANUFACTURER);
1335
+ result.put("model", Build.MODEL);
1336
+ result.put("modelo", Build.MODEL);
1337
+ result.put("brand", Build.BRAND);
1338
+ result.put("androidVersion", Build.VERSION.RELEASE);
1339
+ result.put("sdkInt", Build.VERSION.SDK_INT);
1340
+ result.put("packageName", context().getPackageName());
1341
+ return result;
1342
+ }
1343
+
1344
+ private JSONObject networkInfo() throws Exception {
1345
+ JSONObject result = new JSONObject();
1346
+ ConnectivityManager manager = (ConnectivityManager) context().getSystemService(Context.CONNECTIVITY_SERVICE);
1347
+ boolean connected = false;
1348
+ String type = "unknown";
1349
+
1350
+ if (manager != null) {
1351
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1352
+ Network network = manager.getActiveNetwork();
1353
+ NetworkCapabilities capabilities = network == null ? null : manager.getNetworkCapabilities(network);
1354
+ connected = capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1355
+ if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
1356
+ type = "wifi";
1357
+ } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
1358
+ type = "cellular";
1359
+ } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
1360
+ type = "ethernet";
1361
+ }
1362
+ } else if (manager.getActiveNetworkInfo() != null) {
1363
+ connected = manager.getActiveNetworkInfo().isConnected();
1364
+ type = manager.getActiveNetworkInfo().getTypeName().toLowerCase();
1365
+ }
1366
+ }
1367
+
1368
+ result.put("online", connected);
1369
+ result.put("connected", connected);
1370
+ result.put("tipo", type);
1371
+ result.put("type", type);
1372
+ return result;
1373
+ }
1374
+
1375
+ private JSONObject batteryInfo() throws Exception {
1376
+ Intent intent = context().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
1377
+ JSONObject result = new JSONObject();
1378
+ if (intent == null) {
1379
+ result.put("level", -1);
1380
+ result.put("nivel", -1);
1381
+ result.put("charging", false);
1382
+ return result;
1383
+ }
1384
+
1385
+ int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
1386
+ int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
1387
+ int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
1388
+ boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING
1389
+ || status == BatteryManager.BATTERY_STATUS_FULL;
1390
+ double percent = scale <= 0 ? -1 : (level * 100.0 / scale);
1391
+
1392
+ result.put("level", percent);
1393
+ result.put("nivel", percent);
1394
+ result.put("charging", charging);
1395
+ result.put("carregando", charging);
1396
+ return result;
1397
+ }
1398
+
1399
+ private JSONObject memoryInfo() throws Exception {
1400
+ ActivityManager manager = (ActivityManager) context().getSystemService(Context.ACTIVITY_SERVICE);
1401
+ ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
1402
+ if (manager != null) {
1403
+ manager.getMemoryInfo(info);
1404
+ }
1405
+
1406
+ Runtime runtime = Runtime.getRuntime();
1407
+ JSONObject result = new JSONObject();
1408
+ result.put("availableBytes", info.availMem);
1409
+ result.put("totalBytes", Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? info.totalMem : -1);
1410
+ result.put("lowMemory", info.lowMemory);
1411
+ result.put("thresholdBytes", info.threshold);
1412
+ result.put("appUsedBytes", runtime.totalMemory() - runtime.freeMemory());
1413
+ result.put("appMaxBytes", runtime.maxMemory());
1414
+ return result;
1415
+ }
1416
+
1417
+ private JSONObject storageInfo() throws Exception {
1418
+ JSONObject result = new JSONObject();
1419
+ result.put("internal", statFsInfo(Environment.getDataDirectory()));
1420
+ result.put("cache", statFsInfo(context().getCacheDir()));
1421
+ if (context().getExternalFilesDir(null) != null) {
1422
+ result.put("appExternal", statFsInfo(context().getExternalFilesDir(null)));
1423
+ }
1424
+ return result;
1425
+ }
1426
+
1427
+ private JSONObject statFsInfo(java.io.File file) throws Exception {
1428
+ StatFs statFs = new StatFs(file.getAbsolutePath());
1429
+ long blockSize = statFs.getBlockSizeLong();
1430
+ long total = statFs.getBlockCountLong() * blockSize;
1431
+ long available = statFs.getAvailableBlocksLong() * blockSize;
1432
+ JSONObject result = new JSONObject();
1433
+ result.put("path", file.getAbsolutePath());
1434
+ result.put("totalBytes", total);
1435
+ result.put("availableBytes", available);
1436
+ result.put("usedBytes", total - available);
1437
+ return result;
1438
+ }
1439
+
1440
+ private JSONObject performanceInfo() throws Exception {
1441
+ JSONObject result = new JSONObject();
1442
+ result.put("timestamp", System.currentTimeMillis());
1443
+ result.put("memory", memoryInfo());
1444
+ result.put("storage", storageInfo());
1445
+ result.put("battery", batteryInfo());
1446
+ result.put("network", networkInfo());
1447
+ return result;
1448
+ }
1449
+
1450
+ private JSONObject openAppsMemoryInfo() throws Exception {
1451
+ ActivityManager manager = (ActivityManager) context().getSystemService(Context.ACTIVITY_SERVICE);
1452
+ JSONArray apps = new JSONArray();
1453
+ JSONObject byName = new JSONObject();
1454
+ JSONObject result = new JSONObject();
1455
+
1456
+ result.put("timestamp", System.currentTimeMillis());
1457
+ result.put("limited", true);
1458
+ result.put("observacao", "Android moderno limita a lista de apps de terceiros por privacidade; o retorno mostra apenas processos visiveis para este app.");
1459
+ result.put("note", "Modern Android limits third-party app process visibility for privacy; this returns only processes visible to this app.");
1460
+
1461
+ if (manager == null) {
1462
+ result.put("apps", apps);
1463
+ result.put("porNome", byName);
1464
+ result.put("byName", byName);
1465
+ return result;
1466
+ }
1467
+
1468
+ List<ActivityManager.RunningAppProcessInfo> processes = manager.getRunningAppProcesses();
1469
+ if (processes == null) {
1470
+ result.put("apps", apps);
1471
+ result.put("porNome", byName);
1472
+ result.put("byName", byName);
1473
+ return result;
1474
+ }
1475
+
1476
+ for (ActivityManager.RunningAppProcessInfo process : processes) {
1477
+ Debug.MemoryInfo[] memoryInfos = manager.getProcessMemoryInfo(new int[] { process.pid });
1478
+ Debug.MemoryInfo memory = memoryInfos != null && memoryInfos.length > 0 ? memoryInfos[0] : null;
1479
+ long pssKb = memory == null ? 0 : memory.getTotalPss();
1480
+ long ramBytes = pssKb * 1024L;
1481
+ String packageName = process.pkgList != null && process.pkgList.length > 0 ? process.pkgList[0] : process.processName;
1482
+ String name = appLabel(packageName, process.processName);
1483
+
1484
+ JSONObject item = new JSONObject();
1485
+ item.put("name", name);
1486
+ item.put("nome", name);
1487
+ item.put("packageName", packageName);
1488
+ item.put("pacote", packageName);
1489
+ item.put("processName", process.processName);
1490
+ item.put("pid", process.pid);
1491
+ item.put("importance", process.importance);
1492
+ item.put("importanceName", importanceName(process.importance));
1493
+ item.put("ramBytes", ramBytes);
1494
+ item.put("ramMb", Math.round((ramBytes / 1024.0 / 1024.0) * 100.0) / 100.0);
1495
+ item.put("pssKb", pssKb);
1496
+ if (memory != null) {
1497
+ item.put("privateDirtyKb", memory.getTotalPrivateDirty());
1498
+ item.put("sharedDirtyKb", memory.getTotalSharedDirty());
1499
+ }
1500
+
1501
+ JSONArray packages = new JSONArray();
1502
+ if (process.pkgList != null) {
1503
+ for (String packageItem : process.pkgList) {
1504
+ packages.put(packageItem);
1505
+ }
1506
+ }
1507
+ item.put("packages", packages);
1508
+ item.put("pacotes", packages);
1509
+ apps.put(item);
1510
+
1511
+ JSONObject summary = new JSONObject();
1512
+ summary.put("ramBytes", ramBytes);
1513
+ summary.put("ramMb", Math.round((ramBytes / 1024.0 / 1024.0) * 100.0) / 100.0);
1514
+ summary.put("packageName", packageName);
1515
+ summary.put("processName", process.processName);
1516
+ byName.put(name, summary);
1517
+ }
1518
+
1519
+ result.put("apps", apps);
1520
+ result.put("porNome", byName);
1521
+ result.put("byName", byName);
1522
+ return result;
1523
+ }
1524
+
1525
+ private String appLabel(String packageName, String fallback) {
1526
+ try {
1527
+ PackageManager packageManager = context().getPackageManager();
1528
+ ApplicationInfo info = packageManager.getApplicationInfo(packageName, 0);
1529
+ CharSequence label = packageManager.getApplicationLabel(info);
1530
+ return label == null ? fallback : label.toString();
1531
+ } catch (Exception ignored) {
1532
+ return fallback == null ? packageName : fallback;
1533
+ }
1534
+ }
1535
+
1536
+ private String importanceName(int importance) {
1537
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
1538
+ return "foreground";
1539
+ }
1540
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
1541
+ return "visible";
1542
+ }
1543
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE) {
1544
+ return "service";
1545
+ }
1546
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED) {
1547
+ return "cached";
1548
+ }
1549
+ return "other";
1550
+ }
1551
+
1552
+ private JSONObject permissionStatus(JSONArray requested) throws Exception {
1553
+ JSONArray permissions = requested == null ? new JSONArray() : requested;
1554
+ JSONObject result = new JSONObject();
1555
+ for (int index = 0; index < permissions.length(); index += 1) {
1556
+ String permission = androidPermissionName(permissions.optString(index, ""));
1557
+ if (permission.length() == 0) {
1558
+ continue;
1559
+ }
1560
+ result.put(permission, ContextCompat.checkSelfPermission(context(), permission) == PackageManager.PERMISSION_GRANTED);
1561
+ }
1562
+ return result;
1563
+ }
1564
+
1565
+ private String androidPermissionName(String permission) {
1566
+ String value = permission == null ? "" : permission.trim();
1567
+ if (value.length() == 0 || value.indexOf('.') >= 0) {
1568
+ return value;
1569
+ }
1570
+ return "android.permission." + value;
1571
+ }
1572
+
1573
+ private JSONObject baseEvent(String type) {
1574
+ JSONObject detail = new JSONObject();
1575
+ try {
1576
+ detail.put("type", type);
1577
+ detail.put("tipo", type);
1578
+ detail.put("timestamp", System.currentTimeMillis());
1579
+ } catch (Exception ignored) {
1580
+ }
1581
+ return detail;
1582
+ }
1583
+
1584
+ private void registerSystemReceiver() {
1585
+ if (systemReceiver != null) {
1586
+ return;
1587
+ }
1588
+
1589
+ systemReceiver = new BroadcastReceiver() {
1590
+ @Override
1591
+ public void onReceive(Context context, Intent intent) {
1592
+ if (intent == null || intent.getAction() == null) {
1593
+ return;
1594
+ }
1595
+
1596
+ try {
1597
+ if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
1598
+ dispatchEvent("bateria:mudou", batteryInfo());
1599
+ } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
1600
+ dispatchEvent("rede:mudou", networkInfo());
1601
+ }
1602
+ } catch (Exception ignored) {
1603
+ }
1604
+ }
1605
+ };
1606
+
1607
+ IntentFilter filter = new IntentFilter();
1608
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
1609
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1610
+ ContextCompat.registerReceiver(context(), systemReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
1611
+ }
1612
+
1613
+ private void unregisterSystemReceiver() {
1614
+ if (systemReceiver == null) {
1615
+ return;
1616
+ }
1617
+
1618
+ try {
1619
+ context().unregisterReceiver(systemReceiver);
1620
+ } catch (Exception ignored) {
1621
+ }
1622
+ systemReceiver = null;
1623
+ }
1624
+
1625
+ private void handleLinkIntent(Intent intent, boolean dispatchToJs) {
1626
+ if (intent == null || intent.getData() == null) {
1627
+ return;
1628
+ }
1629
+
1630
+ JSONObject detail = new JSONObject();
1631
+ try {
1632
+ Uri uri = intent.getData();
1633
+ detail.put("url", uri.toString());
1634
+ detail.put("scheme", uri.getScheme());
1635
+ detail.put("host", uri.getHost());
1636
+ detail.put("path", uri.getPath());
1637
+ detail.put("query", uri.getQuery());
1638
+ detail.put("timestamp", System.currentTimeMillis());
1639
+ } catch (Exception ignored) {
1640
+ }
1641
+
1642
+ initialLink = detail;
1643
+ if (dispatchToJs) {
1644
+ dispatchEvent("link:aberto", detail);
1645
+ }
1646
+ }
1647
+
1648
+ private void dispatchEvent(String type, JSONObject detail) {
1649
+ if (webView == null) {
1650
+ return;
1651
+ }
1652
+
1653
+ JSONObject payload = detail == null ? baseEvent(type) : detail;
1654
+ try {
1655
+ payload.put("type", type);
1656
+ payload.put("tipo", type);
1657
+ if (!payload.has("timestamp")) {
1658
+ payload.put("timestamp", System.currentTimeMillis());
1659
+ }
1660
+ } catch (Exception ignored) {
1661
+ }
1662
+
1663
+ final String script = "(function(){var detail=" + payload.toString()
1664
+ + ";window.dispatchEvent(new CustomEvent('html2apk:event',{detail:detail}));"
1665
+ + "window.dispatchEvent(new CustomEvent('html2apk:'+detail.type,{detail:detail}));})();";
1666
+
1667
+ cordova.getActivity().runOnUiThread(new Runnable() {
1668
+ @Override
1669
+ public void run() {
1670
+ webView.getEngine().evaluateJavascript(script, null);
1671
+ }
1672
+ });
1673
+ }
1674
+
1675
+ private void handleNotificationIntent(Intent intent, boolean dispatchToJs) {
1676
+ if (intent == null || !intent.getBooleanExtra(EXTRA_NOTIFICATION_CLICKED, false)) {
1677
+ return;
1678
+ }
1679
+
1680
+ JSONObject detail = parseDetail(intent.getStringExtra(EXTRA_NOTIFICATION_DETAIL));
1681
+ initialNotification = detail;
1682
+
1683
+ if (dispatchToJs) {
1684
+ dispatchNotificationClick(detail);
1685
+ }
1686
+
1687
+ intent.removeExtra(EXTRA_NOTIFICATION_CLICKED);
1688
+ intent.removeExtra(EXTRA_NOTIFICATION_DETAIL);
1689
+ }
1690
+
1691
+ private void dispatchNotificationClick(JSONObject detail) {
1692
+ if (detail == null || webView == null) {
1693
+ return;
1694
+ }
1695
+
1696
+ dispatchEvent("notificacao:clicada", detail);
1697
+
1698
+ final String script = "(function(){var detail=" + detail.toString()
1699
+ + ";window.dispatchEvent(new CustomEvent('html2apk:notification',{detail:detail}));})();";
1700
+
1701
+ cordova.getActivity().runOnUiThread(new Runnable() {
1702
+ @Override
1703
+ public void run() {
1704
+ webView.getEngine().evaluateJavascript(script, null);
1705
+ }
1706
+ });
1707
+ }
1708
+
1709
+ private JSONObject parseDetail(String raw) {
1710
+ try {
1711
+ if (raw == null || raw.length() == 0) {
1712
+ return null;
1713
+ }
1714
+ return new JSONObject(raw);
1715
+ } catch (Exception ignored) {
1716
+ return null;
1717
+ }
1718
+ }
1719
+
1720
+ static PendingIntent createContentIntent(Context context, int id, JSONObject detail) {
1721
+ Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
1722
+ if (launchIntent == null) {
1723
+ launchIntent = new Intent();
1724
+ launchIntent.setPackage(context.getPackageName());
1725
+ }
1726
+
1727
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1728
+ launchIntent.putExtra(EXTRA_NOTIFICATION_CLICKED, true);
1729
+ launchIntent.putExtra(EXTRA_NOTIFICATION_DETAIL, detail == null ? "{}" : detail.toString());
1730
+
1731
+ return PendingIntent.getActivity(
1732
+ context,
1733
+ id,
1734
+ launchIntent,
1735
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
1736
+ );
1737
+ }
1738
+
1739
+ static boolean canScheduleExactAlarms(Context context) {
1740
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
1741
+ return true;
1742
+ }
1743
+
1744
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
1745
+ return alarmManager != null && alarmManager.canScheduleExactAlarms();
1746
+ }
1747
+
1748
+ static JSONArray loopNotifications(JSONObject options) {
1749
+ JSONArray notifications = options.optJSONArray("notificacoes");
1750
+ if (notifications == null) {
1751
+ notifications = options.optJSONArray("notifications");
1752
+ }
1753
+ if (notifications == null) {
1754
+ notifications = options.optJSONArray("items");
1755
+ }
1756
+ return notifications == null ? new JSONArray() : notifications;
1757
+ }
1758
+
1759
+ static JSONObject notificationDisplayOptions(JSONObject options) throws Exception {
1760
+ JSONArray notifications = loopNotifications(options);
1761
+ if (notifications.length() == 0) {
1762
+ return options;
1763
+ }
1764
+
1765
+ int index = Math.max(0, options.optInt("loopIndex", options.optInt("indiceLoop", 0)));
1766
+ JSONObject current = notifications.optJSONObject(index % notifications.length());
1767
+ JSONObject displayOptions = new JSONObject(options.toString());
1768
+ if (current != null) {
1769
+ JSONArray names = current.names();
1770
+ if (names != null) {
1771
+ for (int nameIndex = 0; nameIndex < names.length(); nameIndex += 1) {
1772
+ String name = names.optString(nameIndex);
1773
+ displayOptions.put(name, current.opt(name));
1774
+ }
1775
+ }
1776
+ }
1777
+
1778
+ displayOptions.put("id", notificationId(options));
1779
+ displayOptions.put("loopIndex", index);
1780
+ displayOptions.put("indiceLoop", index);
1781
+ displayOptions.put("loopTotal", notifications.length());
1782
+ displayOptions.put("totalLoop", notifications.length());
1783
+ return displayOptions;
1784
+ }
1785
+
1786
+ static long repeatInterval(JSONObject options) {
1787
+ long interval = intervalFromObject(options.opt("intervalo"));
1788
+ if (interval <= 0) {
1789
+ interval = intervalFromObject(options.opt("interval"));
1790
+ }
1791
+ if (interval <= 0) {
1792
+ interval = intervalFromObject(options.opt("aCada"));
1793
+ }
1794
+ if (interval <= 0) {
1795
+ interval = intervalFromObject(options.opt("every"));
1796
+ }
1797
+ if (interval <= 0) {
1798
+ interval = intervalFromObject(options.opt("repetir"));
1799
+ }
1800
+ if (interval <= 0) {
1801
+ interval = intervalFromObject(options.opt("repeat"));
1802
+ }
1803
+ return interval;
1804
+ }
1805
+
1806
+ static long nextRepeatTime(JSONObject options, long after) {
1807
+ long interval = repeatInterval(options);
1808
+ if (interval <= 0) {
1809
+ return 0;
1810
+ }
1811
+
1812
+ long next = options.optLong("when", options.optLong("quando", after)) + interval;
1813
+ while (next <= after) {
1814
+ next += interval;
1815
+ }
1816
+ return next;
1817
+ }
1818
+
1819
+ static JSONObject nextRepeatOptions(JSONObject options, long nextWhen) throws Exception {
1820
+ JSONObject next = new JSONObject(options.toString());
1821
+ JSONArray notifications = loopNotifications(options);
1822
+ int nextIndex = Math.max(0, options.optInt("loopIndex", options.optInt("indiceLoop", 0))) + 1;
1823
+ int runCount = Math.max(0, options.optInt("_runCount", options.optInt("runCount", 0))) + 1;
1824
+
1825
+ next.put("when", nextWhen);
1826
+ next.put("quando", nextWhen);
1827
+ next.put("loopIndex", notifications.length() == 0 ? nextIndex : nextIndex % notifications.length());
1828
+ next.put("indiceLoop", notifications.length() == 0 ? nextIndex : nextIndex % notifications.length());
1829
+ next.put("_runCount", runCount);
1830
+ next.put("runCount", runCount);
1831
+ return next;
1832
+ }
1833
+
1834
+ static boolean shouldStopRepeating(JSONObject options, long nextWhen) {
1835
+ long until = options.optLong("ate", options.optLong("until", 0));
1836
+ if (until > 0 && nextWhen > until) {
1837
+ return true;
1838
+ }
1839
+
1840
+ int maxRuns = options.optInt("vezes", options.optInt("times", options.optInt("count", options.optInt("limit", 0))));
1841
+ int runCount = Math.max(0, options.optInt("_runCount", options.optInt("runCount", 0))) + 1;
1842
+ return maxRuns > 0 && runCount >= maxRuns;
1843
+ }
1844
+
1845
+ static void rescheduleRepeatingNotification(Context context, int id, JSONObject options, boolean exactAllowed) {
1846
+ try {
1847
+ long nextWhen = nextRepeatTime(options, System.currentTimeMillis());
1848
+ if (nextWhen <= 0 || shouldStopRepeating(options, nextWhen)) {
1849
+ NotificationStore.remove(context, id);
1850
+ return;
1851
+ }
1852
+
1853
+ JSONObject nextOptions = nextRepeatOptions(options, nextWhen);
1854
+ nextOptions.put("id", id);
1855
+ NotificationStore.save(context, id, nextWhen, nextOptions);
1856
+ NotificationReceiver.schedule(context, id, nextWhen, nextOptions, exactAllowed);
1857
+ } catch (Exception ignored) {
1858
+ }
1859
+ }
1860
+
1861
+ private static long intervalFromObject(Object value) {
1862
+ if (value == null || value == JSONObject.NULL) {
1863
+ return 0;
1864
+ }
1865
+ if (value instanceof Number) {
1866
+ return Math.max(0, ((Number) value).longValue());
1867
+ }
1868
+ if (value instanceof JSONObject) {
1869
+ JSONObject object = (JSONObject) value;
1870
+ long nested = intervalFromObject(object.opt("intervalo"));
1871
+ if (nested <= 0) {
1872
+ nested = intervalFromObject(object.opt("interval"));
1873
+ }
1874
+ if (nested <= 0) {
1875
+ nested = intervalFromObject(object.opt("aCada"));
1876
+ }
1877
+ if (nested <= 0) {
1878
+ nested = intervalFromObject(object.opt("every"));
1879
+ }
1880
+ return nested;
1881
+ }
1882
+ if (value instanceof Boolean) {
1883
+ return 0;
1884
+ }
1885
+
1886
+ return parseIntervalString(String.valueOf(value));
1887
+ }
1888
+
1889
+ private static long parseIntervalString(String value) {
1890
+ String text = value == null ? "" : value.trim().toLowerCase();
1891
+ if (text.length() == 0) {
1892
+ return 0;
1893
+ }
1894
+
1895
+ int unitStart = text.length();
1896
+ for (int index = 0; index < text.length(); index += 1) {
1897
+ char character = text.charAt(index);
1898
+ if (!(Character.isDigit(character) || character == '.')) {
1899
+ unitStart = index;
1900
+ break;
1901
+ }
1902
+ }
1903
+
1904
+ try {
1905
+ double amount = Double.parseDouble(text.substring(0, unitStart).trim());
1906
+ String unit = text.substring(unitStart).trim();
1907
+ if ("s".equals(unit) || "seg".equals(unit)) {
1908
+ return Math.round(amount * 1000);
1909
+ }
1910
+ if ("m".equals(unit) || "min".equals(unit)) {
1911
+ return Math.round(amount * 60 * 1000);
1912
+ }
1913
+ if ("h".equals(unit) || "hr".equals(unit)) {
1914
+ return Math.round(amount * 60 * 60 * 1000);
1915
+ }
1916
+ if ("d".equals(unit) || "dia".equals(unit) || "dias".equals(unit)) {
1917
+ return Math.round(amount * 24 * 60 * 60 * 1000);
1918
+ }
1919
+ return Math.round(amount);
1920
+ } catch (Exception ignored) {
1921
+ return 0;
1922
+ }
1923
+ }
1924
+
1925
+ private static int notificationIdFromObject(Object input) {
1926
+ if (input instanceof Number) {
1927
+ return ((Number) input).intValue();
1928
+ }
1929
+ if (input instanceof JSONObject) {
1930
+ return notificationId((JSONObject) input);
1931
+ }
1932
+ try {
1933
+ return Integer.parseInt(String.valueOf(input));
1934
+ } catch (Exception ignored) {
1935
+ return 0;
1936
+ }
1937
+ }
1938
+
1939
+ static JSONObject detailPayload(JSONObject options) throws Exception {
1940
+ JSONObject detail = new JSONObject();
1941
+ JSONObject click = options.optJSONObject("aoClicar");
1942
+ if (click == null) {
1943
+ click = options.optJSONObject("onClick");
1944
+ }
1945
+
1946
+ detail.put("id", notificationId(options));
1947
+ detail.put("title", title(options));
1948
+ detail.put("titulo", title(options));
1949
+ detail.put("text", text(options));
1950
+ detail.put("texto", text(options));
530
1951
  detail.put("when", options.optLong("quando", options.optLong("when", System.currentTimeMillis())));
531
1952
  detail.put("clickedAt", System.currentTimeMillis());
532
1953
  detail.put("onClick", click == null ? new JSONObject().put("action", "open-app") : click);
@@ -534,6 +1955,37 @@ public class Html2ApkBridge extends CordovaPlugin {
534
1955
  return detail;
535
1956
  }
536
1957
 
1958
+ static void addNotificationActions(NotificationCompat.Builder builder, Context context, int notificationId, JSONObject options) {
1959
+ JSONArray actions = options.optJSONArray("acoes");
1960
+ if (actions == null) {
1961
+ actions = options.optJSONArray("actions");
1962
+ }
1963
+ if (actions == null) {
1964
+ return;
1965
+ }
1966
+
1967
+ for (int index = 0; index < actions.length(); index += 1) {
1968
+ JSONObject action = actions.optJSONObject(index);
1969
+ if (action == null) {
1970
+ continue;
1971
+ }
1972
+
1973
+ try {
1974
+ String title = action.optString("titulo", action.optString("title", action.optString("id", "Abrir")));
1975
+ JSONObject detail = detailPayload(options);
1976
+ detail.put("notificationAction", action.optString("id", String.valueOf(index)));
1977
+ detail.put("acaoNotificacao", action.optString("id", String.valueOf(index)));
1978
+ detail.put("action", action);
1979
+ builder.addAction(
1980
+ context.getApplicationInfo().icon,
1981
+ title,
1982
+ createContentIntent(context, notificationId * 100 + index + 1, detail)
1983
+ );
1984
+ } catch (Exception ignored) {
1985
+ }
1986
+ }
1987
+ }
1988
+
537
1989
  static int notificationId(JSONObject options) {
538
1990
  int id = options.optInt("id", 0);
539
1991
  if (id != 0) {