html2apk 0.1.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.
@@ -3,22 +3,46 @@ package dev.html2apk.bridge;
3
3
  import android.Manifest;
4
4
  import android.app.Activity;
5
5
  import android.app.AlarmManager;
6
+ import android.app.ActivityManager;
6
7
  import android.app.NotificationChannel;
7
8
  import android.app.NotificationManager;
8
9
  import android.app.PendingIntent;
10
+ import android.content.ActivityNotFoundException;
11
+ import android.content.BroadcastReceiver;
12
+ import android.content.ClipData;
13
+ import android.content.ClipboardManager;
9
14
  import android.content.Context;
10
15
  import android.content.Intent;
16
+ import android.content.IntentFilter;
17
+ import android.content.pm.ApplicationInfo;
11
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;
12
27
  import android.net.Uri;
28
+ import android.os.BatteryManager;
13
29
  import android.os.Build;
30
+ import android.os.Debug;
31
+ import android.os.Environment;
32
+ import android.os.StatFs;
14
33
  import android.os.VibrationEffect;
15
34
  import android.os.Vibrator;
16
35
  import android.provider.Settings;
36
+ import android.provider.OpenableColumns;
37
+ import android.util.Base64;
17
38
  import android.view.View;
39
+ import android.view.Window;
40
+ import android.view.WindowManager;
18
41
  import android.widget.Toast;
19
42
 
20
43
  import androidx.core.app.NotificationCompat;
21
44
  import androidx.core.app.NotificationManagerCompat;
45
+ import androidx.core.content.ContextCompat;
22
46
 
23
47
  import org.apache.cordova.CallbackContext;
24
48
  import org.apache.cordova.CordovaInterface;
@@ -27,25 +51,78 @@ import org.apache.cordova.CordovaWebView;
27
51
  import org.json.JSONArray;
28
52
  import org.json.JSONObject;
29
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
+
30
62
  public class Html2ApkBridge extends CordovaPlugin {
31
63
  static final String CHANNEL_ID = "html2apk_default";
32
64
  static final String EXTRA_NOTIFICATION_CLICKED = "html2apk_notification_clicked";
33
65
  static final String EXTRA_NOTIFICATION_DETAIL = "html2apk_notification_detail";
34
66
 
35
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;
36
73
 
37
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;
38
82
  private JSONObject initialNotification;
83
+ private JSONObject initialLink;
84
+ private boolean overlaySettingsOpened;
85
+ private boolean torchEnabled;
86
+ private MediaRecorder micRecorder;
87
+ private File micRecordingFile;
88
+ private long micRecordingStartedAt;
89
+ private BroadcastReceiver systemReceiver;
39
90
 
40
91
  @Override
41
92
  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
42
93
  super.initialize(cordova, webView);
43
94
  handleNotificationIntent(cordova.getActivity().getIntent(), false);
95
+ handleLinkIntent(cordova.getActivity().getIntent(), false);
96
+ registerSystemReceiver();
97
+ startFloatingModeIfNeeded();
44
98
  }
45
99
 
46
100
  @Override
47
101
  public void onNewIntent(Intent intent) {
48
102
  handleNotificationIntent(intent, true);
103
+ handleLinkIntent(intent, true);
104
+ }
105
+
106
+ @Override
107
+ public void onResume(boolean multitasking) {
108
+ super.onResume(multitasking);
109
+ dispatchEvent("app:voltou", baseEvent("app:voltou"));
110
+ startFloatingModeIfNeeded();
111
+ }
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();
49
126
  }
50
127
 
51
128
  @Override
@@ -60,7 +137,12 @@ public class Html2ApkBridge extends CordovaPlugin {
60
137
 
61
138
  if ("scheduleNotification".equals(action)) {
62
139
  JSONObject options = args.optJSONObject(0);
63
- 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));
64
146
  callbackContext.success();
65
147
  return true;
66
148
  }
@@ -83,6 +165,167 @@ public class Html2ApkBridge extends CordovaPlugin {
83
165
  return true;
84
166
  }
85
167
 
168
+ if ("keepScreenAwake".equals(action)) {
169
+ keepScreenAwake(args.optBoolean(0, true));
170
+ callbackContext.success();
171
+ return true;
172
+ }
173
+
174
+ if ("setScreenBrightness".equals(action)) {
175
+ setScreenBrightness(args.optDouble(0, -1));
176
+ callbackContext.success();
177
+ return true;
178
+ }
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
+
227
+ if ("copyText".equals(action)) {
228
+ copyText(args.optString(0, ""));
229
+ callbackContext.success();
230
+ return true;
231
+ }
232
+
233
+ if ("readText".equals(action)) {
234
+ callbackContext.success(readText());
235
+ return true;
236
+ }
237
+
238
+ if ("shareText".equals(action)) {
239
+ shareText(args.optString(0, ""));
240
+ callbackContext.success();
241
+ return true;
242
+ }
243
+
244
+ if ("share".equals(action)) {
245
+ share(args.optJSONObject(0));
246
+ callbackContext.success();
247
+ return true;
248
+ }
249
+
250
+ if ("openUrl".equals(action)) {
251
+ openUrl(args.optString(0, ""));
252
+ callbackContext.success();
253
+ return true;
254
+ }
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
+
86
329
  if ("requestNotificationPermission".equals(action)) {
87
330
  requestNotificationPermission(callbackContext);
88
331
  return true;
@@ -104,11 +347,40 @@ public class Html2ApkBridge extends CordovaPlugin {
104
347
  return true;
105
348
  }
106
349
 
350
+ if ("overlayPermissionStatus".equals(action)) {
351
+ callbackContext.success(overlayPermissionStatus());
352
+ return true;
353
+ }
354
+
355
+ if ("requestOverlayPermission".equals(action) || "openOverlaySettings".equals(action)) {
356
+ openOverlaySettings();
357
+ callbackContext.success(overlayPermissionStatus());
358
+ return true;
359
+ }
360
+
361
+ if ("startFloatingIcon".equals(action)) {
362
+ startFloatingIcon();
363
+ callbackContext.success(overlayPermissionStatus());
364
+ return true;
365
+ }
366
+
367
+ if ("stopFloatingIcon".equals(action)) {
368
+ stopFloatingIcon();
369
+ callbackContext.success();
370
+ return true;
371
+ }
372
+
107
373
  if ("getInitialNotification".equals(action)) {
108
374
  callbackContext.success(initialNotification == null ? new JSONObject() : initialNotification);
109
375
  initialNotification = null;
110
376
  return true;
111
377
  }
378
+
379
+ if ("getInitialLink".equals(action)) {
380
+ callbackContext.success(initialLink == null ? new JSONObject() : initialLink);
381
+ initialLink = null;
382
+ return true;
383
+ }
112
384
  } catch (Exception error) {
113
385
  callbackContext.error(error.getMessage());
114
386
  return true;
@@ -119,6 +391,49 @@ public class Html2ApkBridge extends CordovaPlugin {
119
391
 
120
392
  @Override
121
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
+
122
437
  if (requestCode != REQUEST_POST_NOTIFICATIONS || notificationPermissionCallback == null) {
123
438
  return;
124
439
  }
@@ -136,10 +451,90 @@ public class Html2ApkBridge extends CordovaPlugin {
136
451
  }
137
452
  }
138
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
+
139
471
  private Context context() {
140
472
  return this.cordova.getActivity().getApplicationContext();
141
473
  }
142
474
 
475
+ private boolean isFloatingMode() {
476
+ return "floating".equals(preferences.getString("Html2ApkMode", ""));
477
+ }
478
+
479
+ private void startFloatingModeIfNeeded() {
480
+ if (!isFloatingMode()) {
481
+ return;
482
+ }
483
+
484
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
485
+ @Override
486
+ public void run() {
487
+ try {
488
+ if (canDrawOverlays()) {
489
+ startFloatingIcon();
490
+ cordova.getActivity().moveTaskToBack(true);
491
+ } else if (!overlaySettingsOpened) {
492
+ overlaySettingsOpened = true;
493
+ openOverlaySettings();
494
+ }
495
+ } catch (Exception ignored) {
496
+ }
497
+ }
498
+ });
499
+ }
500
+
501
+ private boolean canDrawOverlays() {
502
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context());
503
+ }
504
+
505
+ private JSONObject overlayPermissionStatus() throws Exception {
506
+ JSONObject result = new JSONObject();
507
+ result.put("required", Build.VERSION.SDK_INT >= Build.VERSION_CODES.M);
508
+ result.put("granted", canDrawOverlays());
509
+ result.put("permission", "android.permission.SYSTEM_ALERT_WINDOW");
510
+ return result;
511
+ }
512
+
513
+ private void openOverlaySettings() {
514
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
515
+ return;
516
+ }
517
+
518
+ Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
519
+ intent.setData(Uri.parse("package:" + context().getPackageName()));
520
+ cordova.getActivity().startActivity(intent);
521
+ }
522
+
523
+ private void startFloatingIcon() throws Exception {
524
+ if (!canDrawOverlays()) {
525
+ openOverlaySettings();
526
+ throw new Exception("SYSTEM_ALERT_WINDOW permission is not granted.");
527
+ }
528
+
529
+ Intent intent = new Intent(context(), FloatingIconService.class);
530
+ context().startService(intent);
531
+ }
532
+
533
+ private void stopFloatingIcon() {
534
+ Intent intent = new Intent(context(), FloatingIconService.class);
535
+ context().stopService(intent);
536
+ }
537
+
143
538
  static void ensureNotificationChannel(Context context) {
144
539
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
145
540
  NotificationChannel channel = new NotificationChannel(
@@ -197,27 +592,46 @@ public class Html2ApkBridge extends CordovaPlugin {
197
592
  .setContentIntent(createContentIntent(context(), id, detailPayload(options)))
198
593
  .setPriority(NotificationCompat.PRIORITY_DEFAULT);
199
594
 
595
+ addNotificationActions(builder, context(), id, options);
200
596
  NotificationManagerCompat.from(context()).notify(id, builder.build());
597
+ dispatchEvent("notificacao:recebida", detailPayload(options));
201
598
  }
202
599
 
203
- private void scheduleNotification(JSONObject options) throws Exception {
600
+ private JSONObject scheduleNotification(JSONObject options) throws Exception {
204
601
  long when = options.optLong("quando", options.optLong("when", System.currentTimeMillis() + 60000));
205
602
  if (when < System.currentTimeMillis()) {
206
603
  when = System.currentTimeMillis() + 1000;
207
604
  }
208
605
 
209
606
  int id = notificationId(options);
607
+ options.put("id", id);
608
+ options.put("quando", when);
609
+ options.put("when", when);
210
610
  NotificationStore.save(context(), id, when, options);
211
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;
212
620
  }
213
621
 
214
- private boolean canScheduleExactAlarms() {
215
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
216
- 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.");
217
626
  }
218
627
 
219
- AlarmManager alarmManager = (AlarmManager) context().getSystemService(Context.ALARM_SERVICE);
220
- 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());
221
635
  }
222
636
 
223
637
  private void openExactAlarmSettings() {
@@ -274,61 +688,1045 @@ public class Html2ApkBridge extends CordovaPlugin {
274
688
  });
275
689
  }
276
690
 
277
- private void handleNotificationIntent(Intent intent, boolean dispatchToJs) {
278
- if (intent == null || !intent.getBooleanExtra(EXTRA_NOTIFICATION_CLICKED, false)) {
279
- return;
280
- }
281
-
282
- JSONObject detail = parseDetail(intent.getStringExtra(EXTRA_NOTIFICATION_DETAIL));
283
- initialNotification = detail;
284
-
285
- if (dispatchToJs) {
286
- dispatchNotificationClick(detail);
287
- }
288
-
289
- intent.removeExtra(EXTRA_NOTIFICATION_CLICKED);
290
- intent.removeExtra(EXTRA_NOTIFICATION_DETAIL);
691
+ private void keepScreenAwake(final boolean enabled) {
692
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
693
+ @Override
694
+ public void run() {
695
+ if (enabled) {
696
+ cordova.getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
697
+ } else {
698
+ cordova.getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
699
+ }
700
+ }
701
+ });
291
702
  }
292
703
 
293
- private void dispatchNotificationClick(JSONObject detail) {
294
- if (detail == null || webView == null) {
295
- return;
296
- }
704
+ private void setScreenBrightness(final double value) {
705
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
706
+ @Override
707
+ public void run() {
708
+ WindowManager.LayoutParams attributes = cordova.getActivity().getWindow().getAttributes();
709
+ if (value < 0) {
710
+ attributes.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
711
+ } else {
712
+ attributes.screenBrightness = (float) Math.max(0.01, Math.min(1, value));
713
+ }
714
+ cordova.getActivity().getWindow().setAttributes(attributes);
715
+ }
716
+ });
717
+ }
297
718
 
298
- final String script = "(function(){var detail=" + detail.toString()
299
- + ";window.dispatchEvent(new CustomEvent('html2apk:notification',{detail:detail}));"
300
- + "if(window.Html2ApkNative&&typeof window.Html2ApkNative.__emitNotificationClick==='function'){"
301
- + "window.Html2ApkNative.__emitNotificationClick(detail);}})();";
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);
302
727
 
303
- cordova.getActivity().runOnUiThread(new Runnable() {
728
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
304
729
  @Override
305
730
  public void run() {
306
- webView.getEngine().evaluateJavascript(script, null);
731
+ Window window = cordova.getActivity().getWindow();
732
+ window.setStatusBarColor(statusBarColor);
733
+ window.setNavigationBarColor(navigationBarColor);
734
+ applySystemBarIconContrast(window, darkStatusIcons, darkNavigationIcons);
307
735
  }
308
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;
309
745
  }
310
746
 
311
- private JSONObject parseDetail(String raw) {
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) {
312
761
  try {
313
- if (raw == null || raw.length() == 0) {
314
- return null;
315
- }
316
- return new JSONObject(raw);
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;
317
768
  } catch (Exception ignored) {
318
- return null;
769
+ return false;
319
770
  }
320
771
  }
321
772
 
322
- static PendingIntent createContentIntent(Context context, int id, JSONObject detail) {
323
- Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
324
- if (launchIntent == null) {
325
- launchIntent = new Intent();
326
- launchIntent.setPackage(context.getPackageName());
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
+ }
327
783
  }
328
784
 
329
- launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
330
- launchIntent.putExtra(EXTRA_NOTIFICATION_CLICKED, true);
331
- launchIntent.putExtra(EXTRA_NOTIFICATION_DETAIL, detail == null ? "{}" : detail.toString());
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
+
796
+ private void copyText(String text) {
797
+ ClipboardManager clipboard = (ClipboardManager) context().getSystemService(Context.CLIPBOARD_SERVICE);
798
+ if (clipboard != null) {
799
+ clipboard.setPrimaryClip(ClipData.newPlainText("html2apk", text));
800
+ }
801
+ }
802
+
803
+ private void shareText(String text) {
804
+ Intent intent = new Intent(Intent.ACTION_SEND);
805
+ intent.setType("text/plain");
806
+ intent.putExtra(Intent.EXTRA_TEXT, text);
807
+ cordova.getActivity().startActivity(Intent.createChooser(intent, "Compartilhar"));
808
+ }
809
+
810
+ private void openUrl(String url) throws Exception {
811
+ if (url == null || url.trim().length() == 0) {
812
+ throw new Exception("URL is required.");
813
+ }
814
+
815
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
816
+ cordova.getActivity().startActivity(intent);
817
+ }
818
+
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);
830
+ return;
831
+ }
832
+
833
+ cameraPermissionCallback = callbackContext;
834
+ cordova.requestPermission(this, REQUEST_CAMERA, Manifest.permission.CAMERA);
835
+ }
836
+
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;
848
+ }
849
+
850
+ microphonePermissionCallback = callbackContext;
851
+ cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
852
+ }
853
+
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);
877
+ return;
878
+ }
879
+
880
+ if (!hasMicrophonePermission()) {
881
+ pendingMicStartCallback = callbackContext;
882
+ cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
883
+ return;
884
+ }
885
+
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) {
910
+ }
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);
926
+ }
927
+
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
+
943
+ try {
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) {
951
+ }
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();
1015
+ } catch (Exception ignored) {
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();
1027
+ }
1028
+ }
1029
+
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.");
1034
+ }
1035
+
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
+ }
1043
+
1044
+ throw new Exception("This device does not expose a flashlight.");
1045
+ }
1046
+
1047
+ private void setFlashlight(boolean enabled) throws Exception {
1048
+ if (!hasCameraPermission()) {
1049
+ throw new Exception("CAMERA permission is not granted. Call solicitarPermissaoCamera() first.");
1050
+ }
1051
+
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());
332
1730
 
333
1731
  return PendingIntent.getActivity(
334
1732
  context,
@@ -338,6 +1736,206 @@ public class Html2ApkBridge extends CordovaPlugin {
338
1736
  );
339
1737
  }
340
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
+
341
1939
  static JSONObject detailPayload(JSONObject options) throws Exception {
342
1940
  JSONObject detail = new JSONObject();
343
1941
  JSONObject click = options.optJSONObject("aoClicar");
@@ -357,6 +1955,37 @@ public class Html2ApkBridge extends CordovaPlugin {
357
1955
  return detail;
358
1956
  }
359
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
+
360
1989
  static int notificationId(JSONObject options) {
361
1990
  int id = options.optInt("id", 0);
362
1991
  if (id != 0) {