html2apk 0.2.0 → 0.4.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,49 @@
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.SharedPreferences;
18
+ import android.content.pm.ApplicationInfo;
12
19
  import android.content.pm.PackageManager;
20
+ import android.database.Cursor;
21
+ import android.hardware.camera2.CameraCharacteristics;
22
+ import android.hardware.camera2.CameraManager;
23
+ import android.graphics.Color;
24
+ import android.media.MediaRecorder;
25
+ import android.net.ConnectivityManager;
26
+ import android.net.Network;
27
+ import android.net.NetworkCapabilities;
13
28
  import android.net.Uri;
29
+ import android.os.BatteryManager;
14
30
  import android.os.Build;
31
+ import android.os.Debug;
32
+ import android.os.Environment;
33
+ import android.os.StatFs;
15
34
  import android.os.VibrationEffect;
16
35
  import android.os.Vibrator;
17
36
  import android.provider.Settings;
37
+ import android.provider.OpenableColumns;
38
+ import android.util.Base64;
18
39
  import android.view.View;
40
+ import android.view.Window;
19
41
  import android.view.WindowManager;
20
42
  import android.widget.Toast;
21
43
 
22
44
  import androidx.core.app.NotificationCompat;
23
45
  import androidx.core.app.NotificationManagerCompat;
46
+ import androidx.core.content.ContextCompat;
24
47
 
25
48
  import org.apache.cordova.CallbackContext;
26
49
  import org.apache.cordova.CordovaInterface;
@@ -29,40 +52,96 @@ import org.apache.cordova.CordovaWebView;
29
52
  import org.json.JSONArray;
30
53
  import org.json.JSONObject;
31
54
 
55
+ import java.io.ByteArrayInputStream;
56
+ import java.io.ByteArrayOutputStream;
57
+ import java.io.File;
58
+ import java.io.FileInputStream;
59
+ import java.io.InputStream;
60
+ import java.io.OutputStream;
61
+ import java.util.List;
62
+
32
63
  public class Html2ApkBridge extends CordovaPlugin {
33
64
  static final String CHANNEL_ID = "html2apk_default";
34
65
  static final String EXTRA_NOTIFICATION_CLICKED = "html2apk_notification_clicked";
35
66
  static final String EXTRA_NOTIFICATION_DETAIL = "html2apk_notification_detail";
36
67
 
37
68
  private static final int REQUEST_POST_NOTIFICATIONS = 7311;
69
+ private static final int REQUEST_CAMERA = 7312;
70
+ private static final int REQUEST_RECORD_AUDIO = 7313;
71
+ private static final int REQUEST_PICK_FILE = 7411;
72
+ private static final int REQUEST_SAVE_FILE = 7412;
73
+ private static final int REQUEST_PICK_FOLDER = 7413;
74
+ private static final String PREFS_NAME = "html2apk_bridge";
75
+ private static final String PREF_PERMISSION_PREFIX = "permission_requested_";
38
76
 
39
77
  private CallbackContext notificationPermissionCallback;
78
+ private CallbackContext cameraPermissionCallback;
79
+ private CallbackContext pendingNotificationCallback;
80
+ private CallbackContext pendingFlashlightCallback;
81
+ private CallbackContext microphonePermissionCallback;
82
+ private CallbackContext pendingMicStartCallback;
83
+ private CallbackContext filePickerCallback;
84
+ private CallbackContext saveFileCallback;
85
+ private CallbackContext folderPickerCallback;
86
+ private JSONObject pendingSaveFile;
87
+ private JSONObject pendingNotificationOptions;
40
88
  private JSONObject initialNotification;
89
+ private JSONObject initialLink;
90
+ private Boolean pendingFlashlightEnabled;
91
+ private boolean pendingNotificationSchedule;
92
+ private boolean pendingFlashlightToggle;
41
93
  private boolean overlaySettingsOpened;
94
+ private boolean torchEnabled;
95
+ private MediaRecorder micRecorder;
96
+ private File micRecordingFile;
97
+ private long micRecordingStartedAt;
98
+ private BroadcastReceiver systemReceiver;
42
99
 
43
100
  @Override
44
101
  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
45
102
  super.initialize(cordova, webView);
46
103
  handleNotificationIntent(cordova.getActivity().getIntent(), false);
104
+ handleLinkIntent(cordova.getActivity().getIntent(), false);
105
+ registerSystemReceiver();
47
106
  startFloatingModeIfNeeded();
48
107
  }
49
108
 
50
109
  @Override
51
110
  public void onNewIntent(Intent intent) {
52
111
  handleNotificationIntent(intent, true);
112
+ handleLinkIntent(intent, true);
53
113
  }
54
114
 
55
115
  @Override
56
116
  public void onResume(boolean multitasking) {
57
117
  super.onResume(multitasking);
118
+ dispatchEvent("app:voltou", baseEvent("app:voltou"));
58
119
  startFloatingModeIfNeeded();
59
120
  }
60
121
 
122
+ @Override
123
+ public void onPause(boolean multitasking) {
124
+ dispatchEvent("app:pausado", baseEvent("app:pausado"));
125
+ dispatchEvent("app:background", baseEvent("app:background"));
126
+ super.onPause(multitasking);
127
+ }
128
+
129
+ @Override
130
+ public void onDestroy() {
131
+ stopMicRecorderSilently();
132
+ unregisterSystemReceiver();
133
+ dispatchEvent("app:fechado", baseEvent("app:fechado"));
134
+ super.onDestroy();
135
+ }
136
+
61
137
  @Override
62
138
  public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
63
139
  try {
64
140
  if ("notify".equals(action)) {
65
141
  JSONObject options = args.optJSONObject(0);
142
+ if (requestNotificationPermissionForAction(options == null ? new JSONObject() : options, false, callbackContext)) {
143
+ return true;
144
+ }
66
145
  showNotification(options == null ? new JSONObject() : options);
67
146
  callbackContext.success();
68
147
  return true;
@@ -70,7 +149,15 @@ public class Html2ApkBridge extends CordovaPlugin {
70
149
 
71
150
  if ("scheduleNotification".equals(action)) {
72
151
  JSONObject options = args.optJSONObject(0);
73
- scheduleNotification(options == null ? new JSONObject() : options);
152
+ if (requestNotificationPermissionForAction(options == null ? new JSONObject() : options, true, callbackContext)) {
153
+ return true;
154
+ }
155
+ callbackContext.success(scheduleNotification(options == null ? new JSONObject() : options));
156
+ return true;
157
+ }
158
+
159
+ if ("cancelNotification".equals(action)) {
160
+ cancelNotification(args.opt(0));
74
161
  callbackContext.success();
75
162
  return true;
76
163
  }
@@ -105,24 +192,153 @@ public class Html2ApkBridge extends CordovaPlugin {
105
192
  return true;
106
193
  }
107
194
 
195
+ if ("setSystemBarsColor".equals(action)) {
196
+ callbackContext.success(setSystemBarsColor(args.opt(0)));
197
+ return true;
198
+ }
199
+
200
+ if ("flashlight".equals(action)) {
201
+ setFlashlightWithPermission(args.optBoolean(0, true), false, callbackContext);
202
+ return true;
203
+ }
204
+
205
+ if ("toggleFlashlight".equals(action)) {
206
+ setFlashlightWithPermission(false, true, callbackContext);
207
+ return true;
208
+ }
209
+
210
+ if ("flashlightStatus".equals(action)) {
211
+ callbackContext.success(flashlightStatus());
212
+ return true;
213
+ }
214
+
215
+ if ("requestCameraPermission".equals(action)) {
216
+ requestCameraPermission(callbackContext);
217
+ return true;
218
+ }
219
+
220
+ if ("requestMicrophonePermission".equals(action)) {
221
+ requestMicrophonePermission(callbackContext);
222
+ return true;
223
+ }
224
+
225
+ if ("microphoneStatus".equals(action)) {
226
+ callbackContext.success(microphoneStatus());
227
+ return true;
228
+ }
229
+
230
+ if ("startMic".equals(action)) {
231
+ startMicRecording(callbackContext);
232
+ return true;
233
+ }
234
+
235
+ if ("stopMic".equals(action)) {
236
+ callbackContext.success(stopMicRecording());
237
+ return true;
238
+ }
239
+
108
240
  if ("copyText".equals(action)) {
109
241
  copyText(args.optString(0, ""));
110
242
  callbackContext.success();
111
243
  return true;
112
244
  }
113
245
 
246
+ if ("readText".equals(action)) {
247
+ callbackContext.success(readText());
248
+ return true;
249
+ }
250
+
114
251
  if ("shareText".equals(action)) {
115
252
  shareText(args.optString(0, ""));
116
253
  callbackContext.success();
117
254
  return true;
118
255
  }
119
256
 
257
+ if ("share".equals(action)) {
258
+ share(args.optJSONObject(0));
259
+ callbackContext.success();
260
+ return true;
261
+ }
262
+
120
263
  if ("openUrl".equals(action)) {
121
264
  openUrl(args.optString(0, ""));
122
265
  callbackContext.success();
123
266
  return true;
124
267
  }
125
268
 
269
+ if ("dial".equals(action)) {
270
+ dial(args.optString(0, ""));
271
+ callbackContext.success();
272
+ return true;
273
+ }
274
+
275
+ if ("openMap".equals(action)) {
276
+ openMap(args.optString(0, ""));
277
+ callbackContext.success();
278
+ return true;
279
+ }
280
+
281
+ if ("openWhatsapp".equals(action)) {
282
+ openWhatsapp(args.optString(0, ""), args.optString(1, ""));
283
+ callbackContext.success();
284
+ return true;
285
+ }
286
+
287
+ if ("pickFile".equals(action)) {
288
+ pickFile(args.optJSONObject(0), callbackContext);
289
+ return true;
290
+ }
291
+
292
+ if ("pickFolder".equals(action)) {
293
+ pickFolder(callbackContext);
294
+ return true;
295
+ }
296
+
297
+ if ("saveFile".equals(action)) {
298
+ saveFile(args.optJSONObject(0), callbackContext);
299
+ return true;
300
+ }
301
+
302
+ if ("deviceInfo".equals(action)) {
303
+ callbackContext.success(deviceInfo());
304
+ return true;
305
+ }
306
+
307
+ if ("networkInfo".equals(action)) {
308
+ callbackContext.success(networkInfo());
309
+ return true;
310
+ }
311
+
312
+ if ("batteryInfo".equals(action)) {
313
+ callbackContext.success(batteryInfo());
314
+ return true;
315
+ }
316
+
317
+ if ("memoryInfo".equals(action)) {
318
+ callbackContext.success(memoryInfo());
319
+ return true;
320
+ }
321
+
322
+ if ("storageInfo".equals(action)) {
323
+ callbackContext.success(storageInfo());
324
+ return true;
325
+ }
326
+
327
+ if ("performanceInfo".equals(action)) {
328
+ callbackContext.success(performanceInfo());
329
+ return true;
330
+ }
331
+
332
+ if ("openAppsMemory".equals(action)) {
333
+ callbackContext.success(openAppsMemoryInfo());
334
+ return true;
335
+ }
336
+
337
+ if ("permissionStatus".equals(action)) {
338
+ callbackContext.success(permissionStatus(args.optJSONArray(0)));
339
+ return true;
340
+ }
341
+
126
342
  if ("requestNotificationPermission".equals(action)) {
127
343
  requestNotificationPermission(callbackContext);
128
344
  return true;
@@ -140,7 +356,14 @@ public class Html2ApkBridge extends CordovaPlugin {
140
356
 
141
357
  if ("openExactAlarmSettings".equals(action)) {
142
358
  openExactAlarmSettings();
143
- callbackContext.success();
359
+ JSONObject result = new JSONObject();
360
+ result.put("permission", "android.permission.SCHEDULE_EXACT_ALARM");
361
+ result.put("required", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
362
+ result.put("granted", canScheduleExactAlarms());
363
+ result.put("permissionGranted", canScheduleExactAlarms());
364
+ result.put("requiresSettings", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms());
365
+ result.put("settingsOpened", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExactAlarms());
366
+ callbackContext.success(result);
144
367
  return true;
145
368
  }
146
369
 
@@ -151,11 +374,22 @@ public class Html2ApkBridge extends CordovaPlugin {
151
374
 
152
375
  if ("requestOverlayPermission".equals(action) || "openOverlaySettings".equals(action)) {
153
376
  openOverlaySettings();
154
- callbackContext.success(overlayPermissionStatus());
377
+ JSONObject result = overlayPermissionStatus();
378
+ result.put("requested", true);
379
+ result.put("settingsOpened", result.optBoolean("requiresSettings"));
380
+ callbackContext.success(result);
155
381
  return true;
156
382
  }
157
383
 
158
384
  if ("startFloatingIcon".equals(action)) {
385
+ if (!canDrawOverlays()) {
386
+ openOverlaySettings();
387
+ JSONObject result = overlayPermissionStatus();
388
+ result.put("requested", true);
389
+ result.put("requiresSettings", true);
390
+ callbackContext.success(result);
391
+ return true;
392
+ }
159
393
  startFloatingIcon();
160
394
  callbackContext.success(overlayPermissionStatus());
161
395
  return true;
@@ -172,6 +406,12 @@ public class Html2ApkBridge extends CordovaPlugin {
172
406
  initialNotification = null;
173
407
  return true;
174
408
  }
409
+
410
+ if ("getInitialLink".equals(action)) {
411
+ callbackContext.success(initialLink == null ? new JSONObject() : initialLink);
412
+ initialLink = null;
413
+ return true;
414
+ }
175
415
  } catch (Exception error) {
176
416
  callbackContext.error(error.getMessage());
177
417
  return true;
@@ -182,13 +422,142 @@ public class Html2ApkBridge extends CordovaPlugin {
182
422
 
183
423
  @Override
184
424
  public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
185
- if (requestCode != REQUEST_POST_NOTIFICATIONS || notificationPermissionCallback == null) {
425
+ if (requestCode == REQUEST_CAMERA) {
426
+ boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
427
+ if (pendingFlashlightCallback != null) {
428
+ CallbackContext callback = pendingFlashlightCallback;
429
+ Boolean enabled = pendingFlashlightEnabled;
430
+ boolean toggle = pendingFlashlightToggle;
431
+ pendingFlashlightCallback = null;
432
+ pendingFlashlightEnabled = null;
433
+ pendingFlashlightToggle = false;
434
+
435
+ try {
436
+ if (!granted) {
437
+ JSONObject result = shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)
438
+ ? openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, true)
439
+ : flashlightStatus();
440
+ result.put("requested", true);
441
+ result.put("granted", false);
442
+ callback.success(result);
443
+ return;
444
+ }
445
+
446
+ setFlashlight(toggle ? !torchEnabled : Boolean.TRUE.equals(enabled));
447
+ JSONObject result = flashlightStatus();
448
+ result.put("requested", true);
449
+ result.put("granted", true);
450
+ callback.success(result);
451
+ } catch (Exception error) {
452
+ callback.error(error.getMessage());
453
+ }
454
+ return;
455
+ }
456
+
457
+ if (cameraPermissionCallback == null) {
458
+ return;
459
+ }
460
+
461
+ try {
462
+ JSONObject result = granted
463
+ ? runtimePermissionResult(Manifest.permission.CAMERA, true, true, true)
464
+ : (
465
+ shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)
466
+ ? openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, true)
467
+ : runtimePermissionResult(Manifest.permission.CAMERA, true, true, false)
468
+ );
469
+ cameraPermissionCallback.success(result);
470
+ } catch (Exception error) {
471
+ cameraPermissionCallback.error(error.getMessage());
472
+ } finally {
473
+ cameraPermissionCallback = null;
474
+ }
475
+ return;
476
+ }
477
+
478
+ if (requestCode == REQUEST_RECORD_AUDIO) {
479
+ boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
480
+ CallbackContext callback = pendingMicStartCallback != null ? pendingMicStartCallback : microphonePermissionCallback;
481
+ boolean shouldStartRecording = pendingMicStartCallback != null;
482
+ pendingMicStartCallback = null;
483
+ microphonePermissionCallback = null;
484
+
485
+ if (callback == null) {
486
+ return;
487
+ }
488
+
489
+ try {
490
+ if (shouldStartRecording && granted) {
491
+ startMicRecorder(callback);
492
+ } else {
493
+ JSONObject result = granted
494
+ ? microphoneStatus()
495
+ : (
496
+ shouldOpenSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO)
497
+ ? openSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO, true, true)
498
+ : microphoneStatus()
499
+ );
500
+ result.put("requested", true);
501
+ result.put("granted", granted);
502
+ callback.success(result);
503
+ }
504
+ } catch (Exception error) {
505
+ callback.error(error.getMessage());
506
+ }
507
+ return;
508
+ }
509
+
510
+ if (requestCode != REQUEST_POST_NOTIFICATIONS) {
186
511
  return;
187
512
  }
188
513
 
189
514
  boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
515
+ if (pendingNotificationCallback != null) {
516
+ CallbackContext callback = pendingNotificationCallback;
517
+ JSONObject options = pendingNotificationOptions == null ? new JSONObject() : pendingNotificationOptions;
518
+ boolean schedule = pendingNotificationSchedule;
519
+ pendingNotificationCallback = null;
520
+ pendingNotificationOptions = null;
521
+ pendingNotificationSchedule = false;
522
+
523
+ try {
524
+ if (!granted) {
525
+ JSONObject result = shouldOpenSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS)
526
+ ? openSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS, true, true)
527
+ : notificationPermissionStatus();
528
+ result.put("requested", true);
529
+ result.put("granted", false);
530
+ callback.success(result);
531
+ return;
532
+ }
533
+
534
+ if (schedule) {
535
+ JSONObject result = scheduleNotification(options);
536
+ result.put("requested", true);
537
+ result.put("granted", true);
538
+ callback.success(result);
539
+ } else {
540
+ showNotification(options);
541
+ callback.success();
542
+ }
543
+ } catch (Exception error) {
544
+ callback.error(error.getMessage());
545
+ }
546
+ return;
547
+ }
548
+
549
+ if (notificationPermissionCallback == null) {
550
+ return;
551
+ }
552
+
190
553
  try {
191
- JSONObject result = notificationPermissionStatus();
554
+ JSONObject result = granted
555
+ ? notificationPermissionStatus()
556
+ : (
557
+ shouldOpenSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS)
558
+ ? openSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS, true, true)
559
+ : notificationPermissionStatus()
560
+ );
192
561
  result.put("requested", true);
193
562
  result.put("granted", granted);
194
563
  notificationPermissionCallback.success(result);
@@ -199,10 +568,99 @@ public class Html2ApkBridge extends CordovaPlugin {
199
568
  }
200
569
  }
201
570
 
571
+ @Override
572
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
573
+ if (requestCode == REQUEST_PICK_FILE) {
574
+ handlePickFileResult(resultCode, intent);
575
+ return;
576
+ }
577
+
578
+ if (requestCode == REQUEST_SAVE_FILE) {
579
+ handleSaveFileResult(resultCode, intent);
580
+ return;
581
+ }
582
+
583
+ if (requestCode == REQUEST_PICK_FOLDER) {
584
+ handlePickFolderResult(resultCode, intent);
585
+ }
586
+ }
587
+
202
588
  private Context context() {
203
589
  return this.cordova.getActivity().getApplicationContext();
204
590
  }
205
591
 
592
+ private SharedPreferences preferencesStore() {
593
+ return context().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
594
+ }
595
+
596
+ private boolean wasRuntimePermissionRequested(String permission) {
597
+ return preferencesStore().getBoolean(PREF_PERMISSION_PREFIX + permission, false);
598
+ }
599
+
600
+ private void rememberRuntimePermissionRequest(String permission) {
601
+ preferencesStore().edit().putBoolean(PREF_PERMISSION_PREFIX + permission, true).apply();
602
+ }
603
+
604
+ private boolean shouldOpenSettingsForRuntimePermission(String permission) {
605
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || !wasRuntimePermissionRequested(permission)) {
606
+ return false;
607
+ }
608
+
609
+ return !cordova.getActivity().shouldShowRequestPermissionRationale(permission);
610
+ }
611
+
612
+ private JSONObject runtimePermissionResult(String permission, boolean required, boolean requested, boolean granted) throws Exception {
613
+ JSONObject result = new JSONObject();
614
+ result.put("permission", permission);
615
+ result.put("required", required);
616
+ result.put("requested", requested);
617
+ result.put("granted", granted);
618
+ result.put("permissionGranted", granted);
619
+ result.put("requiresSettings", required && !granted && shouldOpenSettingsForRuntimePermission(permission));
620
+ result.put("settingsOpened", false);
621
+ return result;
622
+ }
623
+
624
+ private JSONObject openSettingsForRuntimePermission(String permission, boolean required, boolean requested) throws Exception {
625
+ JSONObject result = runtimePermissionResult(permission, required, requested, false);
626
+ openPermissionSettings(permission);
627
+ result.put("requiresSettings", true);
628
+ result.put("settingsOpened", true);
629
+ result.put("settingsScreen", Manifest.permission.POST_NOTIFICATIONS.equals(permission) ? "notifications" : "app");
630
+ result.put("telaConfiguracao", result.optString("settingsScreen"));
631
+ return result;
632
+ }
633
+
634
+ private void openPermissionSettings(String permission) {
635
+ if (Manifest.permission.POST_NOTIFICATIONS.equals(permission)) {
636
+ openNotificationSettings();
637
+ return;
638
+ }
639
+
640
+ openAppSettings();
641
+ }
642
+
643
+ private void openAppSettings() {
644
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
645
+ intent.setData(Uri.parse("package:" + context().getPackageName()));
646
+ cordova.getActivity().startActivity(intent);
647
+ }
648
+
649
+ private void openNotificationSettings() {
650
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
651
+ openAppSettings();
652
+ return;
653
+ }
654
+
655
+ Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
656
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, context().getPackageName());
657
+ try {
658
+ cordova.getActivity().startActivity(intent);
659
+ } catch (ActivityNotFoundException error) {
660
+ openAppSettings();
661
+ }
662
+ }
663
+
206
664
  private boolean isFloatingMode() {
207
665
  return "floating".equals(preferences.getString("Html2ApkMode", ""));
208
666
  }
@@ -238,17 +696,24 @@ public class Html2ApkBridge extends CordovaPlugin {
238
696
  result.put("required", Build.VERSION.SDK_INT >= Build.VERSION_CODES.M);
239
697
  result.put("granted", canDrawOverlays());
240
698
  result.put("permission", "android.permission.SYSTEM_ALERT_WINDOW");
699
+ result.put("permissionGranted", canDrawOverlays());
700
+ result.put("requiresSettings", Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !canDrawOverlays());
701
+ result.put("settingsOpened", false);
241
702
  return result;
242
703
  }
243
704
 
244
705
  private void openOverlaySettings() {
245
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
706
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || canDrawOverlays()) {
246
707
  return;
247
708
  }
248
709
 
249
710
  Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
250
711
  intent.setData(Uri.parse("package:" + context().getPackageName()));
251
- cordova.getActivity().startActivity(intent);
712
+ try {
713
+ cordova.getActivity().startActivity(intent);
714
+ } catch (ActivityNotFoundException error) {
715
+ openAppSettings();
716
+ }
252
717
  }
253
718
 
254
719
  private void startFloatingIcon() throws Exception {
@@ -292,21 +757,42 @@ public class Html2ApkBridge extends CordovaPlugin {
292
757
  return;
293
758
  }
294
759
 
760
+ if (shouldOpenSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS)) {
761
+ callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS, true, false));
762
+ return;
763
+ }
764
+
295
765
  notificationPermissionCallback = callbackContext;
766
+ rememberRuntimePermissionRequest(Manifest.permission.POST_NOTIFICATIONS);
296
767
  cordova.requestPermission(this, REQUEST_POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS);
297
768
  }
298
769
 
299
770
  private JSONObject notificationPermissionStatus() throws Exception {
300
- JSONObject result = new JSONObject();
301
- result.put("required", Build.VERSION.SDK_INT >= 33);
302
- result.put("granted", hasNotificationPermission());
303
- result.put("permission", "android.permission.POST_NOTIFICATIONS");
304
- return result;
771
+ boolean required = Build.VERSION.SDK_INT >= 33;
772
+ return runtimePermissionResult(Manifest.permission.POST_NOTIFICATIONS, required, false, hasNotificationPermission());
773
+ }
774
+
775
+ private boolean requestNotificationPermissionForAction(JSONObject options, boolean schedule, CallbackContext callbackContext) throws Exception {
776
+ if (hasNotificationPermission()) {
777
+ return false;
778
+ }
779
+
780
+ if (shouldOpenSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS)) {
781
+ callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.POST_NOTIFICATIONS, true, false));
782
+ return true;
783
+ }
784
+
785
+ pendingNotificationOptions = options;
786
+ pendingNotificationSchedule = schedule;
787
+ pendingNotificationCallback = callbackContext;
788
+ rememberRuntimePermissionRequest(Manifest.permission.POST_NOTIFICATIONS);
789
+ cordova.requestPermission(this, REQUEST_POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS);
790
+ return true;
305
791
  }
306
792
 
307
793
  private void showNotification(JSONObject options) throws Exception {
308
794
  if (!hasNotificationPermission()) {
309
- throw new Exception("POST_NOTIFICATIONS permission is not granted. Call solicitarPermissaoNotificacoes() first.");
795
+ throw new Exception("POST_NOTIFICATIONS permission is not granted.");
310
796
  }
311
797
 
312
798
  ensureNotificationChannel(context());
@@ -323,38 +809,83 @@ public class Html2ApkBridge extends CordovaPlugin {
323
809
  .setContentIntent(createContentIntent(context(), id, detailPayload(options)))
324
810
  .setPriority(NotificationCompat.PRIORITY_DEFAULT);
325
811
 
812
+ addNotificationActions(builder, context(), id, options);
326
813
  NotificationManagerCompat.from(context()).notify(id, builder.build());
814
+ dispatchEvent("notificacao:recebida", detailPayload(options));
327
815
  }
328
816
 
329
- private void scheduleNotification(JSONObject options) throws Exception {
817
+ private JSONObject scheduleNotification(JSONObject options) throws Exception {
330
818
  long when = options.optLong("quando", options.optLong("when", System.currentTimeMillis() + 60000));
331
819
  if (when < System.currentTimeMillis()) {
332
820
  when = System.currentTimeMillis() + 1000;
333
821
  }
334
822
 
335
823
  int id = notificationId(options);
824
+ options.put("id", id);
825
+ options.put("quando", when);
826
+ options.put("when", when);
827
+ boolean exactRequested = wantsExactAlarm(options);
828
+ boolean exactAllowed = canScheduleExactAlarms();
829
+ boolean settingsOpened = false;
830
+ if (exactRequested && !exactAllowed) {
831
+ openExactAlarmSettings();
832
+ settingsOpened = true;
833
+ }
336
834
  NotificationStore.save(context(), id, when, options);
337
- NotificationReceiver.schedule(context(), id, when, options, canScheduleExactAlarms());
835
+ NotificationReceiver.schedule(context(), id, when, options, exactAllowed);
836
+
837
+ JSONObject result = new JSONObject();
838
+ result.put("id", id);
839
+ result.put("when", when);
840
+ result.put("quando", when);
841
+ result.put("repeating", repeatInterval(options) > 0);
842
+ result.put("loop", loopNotifications(options).length() > 0);
843
+ result.put("exactRequested", exactRequested);
844
+ result.put("exatoSolicitado", exactRequested);
845
+ result.put("exactAllowed", exactAllowed);
846
+ result.put("exatoPermitido", exactAllowed);
847
+ result.put("requiresSettings", exactRequested && !exactAllowed);
848
+ result.put("settingsOpened", settingsOpened);
849
+ return result;
338
850
  }
339
851
 
340
- private boolean canScheduleExactAlarms() {
341
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
342
- return true;
852
+ private boolean wantsExactAlarm(JSONObject options) {
853
+ return options.optBoolean("exato",
854
+ options.optBoolean("exact",
855
+ options.optBoolean("alarmeExato",
856
+ options.optBoolean("exactAlarm",
857
+ options.optBoolean("preciso",
858
+ options.optBoolean("precise", false))))));
859
+ }
860
+
861
+ private void cancelNotification(Object input) throws Exception {
862
+ int id = notificationIdFromObject(input);
863
+ if (id == 0) {
864
+ throw new Exception("Notification id is required.");
343
865
  }
344
866
 
345
- AlarmManager alarmManager = (AlarmManager) context().getSystemService(Context.ALARM_SERVICE);
346
- return alarmManager != null && alarmManager.canScheduleExactAlarms();
867
+ NotificationReceiver.cancel(context(), id);
868
+ NotificationStore.remove(context(), id);
869
+ NotificationManagerCompat.from(context()).cancel(id);
870
+ }
871
+
872
+ private boolean canScheduleExactAlarms() {
873
+ return canScheduleExactAlarms(context());
347
874
  }
348
875
 
349
876
  private void openExactAlarmSettings() {
350
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
877
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || canScheduleExactAlarms()) {
351
878
  return;
352
879
  }
353
880
 
354
881
  Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
355
882
  intent.setData(Uri.parse("package:" + context().getPackageName()));
356
883
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
357
- context().startActivity(intent);
884
+ try {
885
+ context().startActivity(intent);
886
+ } catch (ActivityNotFoundException error) {
887
+ openAppSettings();
888
+ }
358
889
  }
359
890
 
360
891
  private void vibrate(long ms) {
@@ -428,6 +959,83 @@ public class Html2ApkBridge extends CordovaPlugin {
428
959
  });
429
960
  }
430
961
 
962
+ private JSONObject setSystemBarsColor(Object input) throws Exception {
963
+ final JSONObject options = normalizeSystemBarsOptions(input);
964
+ final String statusColor = options.optString("statusBarColor", options.optString("color", options.optString("cor", "#126fff")));
965
+ final String navigationColor = options.optString("navigationBarColor", options.optString("navigationColor", statusColor));
966
+ final boolean darkStatusIcons = options.optBoolean("darkIcons", isLightColor(statusColor));
967
+ final boolean darkNavigationIcons = options.optBoolean("darkNavigationIcons", isLightColor(navigationColor));
968
+ final int statusBarColor = Color.parseColor(statusColor);
969
+ final int navigationBarColor = Color.parseColor(navigationColor);
970
+
971
+ this.cordova.getActivity().runOnUiThread(new Runnable() {
972
+ @Override
973
+ public void run() {
974
+ Window window = cordova.getActivity().getWindow();
975
+ window.setStatusBarColor(statusBarColor);
976
+ window.setNavigationBarColor(navigationBarColor);
977
+ applySystemBarIconContrast(window, darkStatusIcons, darkNavigationIcons);
978
+ }
979
+ });
980
+
981
+ JSONObject result = new JSONObject();
982
+ result.put("statusBarColor", statusColor);
983
+ result.put("navigationBarColor", navigationColor);
984
+ result.put("darkIcons", darkStatusIcons);
985
+ result.put("darkNavigationIcons", darkNavigationIcons);
986
+ result.put("applied", true);
987
+ return result;
988
+ }
989
+
990
+ private JSONObject normalizeSystemBarsOptions(Object input) throws Exception {
991
+ if (input instanceof JSONObject) {
992
+ return (JSONObject) input;
993
+ }
994
+
995
+ JSONObject options = new JSONObject();
996
+ String color = input == null ? "#126fff" : String.valueOf(input);
997
+ options.put("color", color);
998
+ options.put("statusBarColor", color);
999
+ options.put("navigationBarColor", color);
1000
+ return options;
1001
+ }
1002
+
1003
+ private boolean isLightColor(String color) {
1004
+ try {
1005
+ int parsed = Color.parseColor(color);
1006
+ int red = Color.red(parsed);
1007
+ int green = Color.green(parsed);
1008
+ int blue = Color.blue(parsed);
1009
+ double luminance = (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
1010
+ return luminance > 158;
1011
+ } catch (Exception ignored) {
1012
+ return false;
1013
+ }
1014
+ }
1015
+
1016
+ private void applySystemBarIconContrast(Window window, boolean darkStatusIcons, boolean darkNavigationIcons) {
1017
+ View decor = window.getDecorView();
1018
+ int flags = decor.getSystemUiVisibility();
1019
+
1020
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1021
+ if (darkStatusIcons) {
1022
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
1023
+ } else {
1024
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
1025
+ }
1026
+ }
1027
+
1028
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1029
+ if (darkNavigationIcons) {
1030
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
1031
+ } else {
1032
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
1033
+ }
1034
+ }
1035
+
1036
+ decor.setSystemUiVisibility(flags);
1037
+ }
1038
+
431
1039
  private void copyText(String text) {
432
1040
  ClipboardManager clipboard = (ClipboardManager) context().getSystemService(Context.CLIPBOARD_SERVICE);
433
1041
  if (clipboard != null) {
@@ -451,33 +1059,926 @@ public class Html2ApkBridge extends CordovaPlugin {
451
1059
  cordova.getActivity().startActivity(intent);
452
1060
  }
453
1061
 
454
- private void handleNotificationIntent(Intent intent, boolean dispatchToJs) {
455
- if (intent == null || !intent.getBooleanExtra(EXTRA_NOTIFICATION_CLICKED, false)) {
1062
+ private boolean hasCameraPermission() {
1063
+ return ContextCompat.checkSelfPermission(context(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
1064
+ }
1065
+
1066
+ private void requestCameraPermission(CallbackContext callbackContext) throws Exception {
1067
+ if (hasCameraPermission()) {
1068
+ callbackContext.success(runtimePermissionResult(Manifest.permission.CAMERA, true, false, true));
456
1069
  return;
457
1070
  }
458
1071
 
459
- JSONObject detail = parseDetail(intent.getStringExtra(EXTRA_NOTIFICATION_DETAIL));
460
- initialNotification = detail;
461
-
462
- if (dispatchToJs) {
463
- dispatchNotificationClick(detail);
1072
+ if (shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)) {
1073
+ callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, false));
1074
+ return;
464
1075
  }
465
1076
 
466
- intent.removeExtra(EXTRA_NOTIFICATION_CLICKED);
467
- intent.removeExtra(EXTRA_NOTIFICATION_DETAIL);
1077
+ cameraPermissionCallback = callbackContext;
1078
+ rememberRuntimePermissionRequest(Manifest.permission.CAMERA);
1079
+ cordova.requestPermission(this, REQUEST_CAMERA, Manifest.permission.CAMERA);
468
1080
  }
469
1081
 
470
- private void dispatchNotificationClick(JSONObject detail) {
471
- if (detail == null || webView == null) {
1082
+ private boolean hasMicrophonePermission() {
1083
+ return ContextCompat.checkSelfPermission(context(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
1084
+ }
1085
+
1086
+ private void requestMicrophonePermission(CallbackContext callbackContext) throws Exception {
1087
+ if (hasMicrophonePermission()) {
1088
+ JSONObject result = microphoneStatus();
1089
+ result.put("requested", false);
1090
+ result.put("granted", true);
1091
+ callbackContext.success(result);
472
1092
  return;
473
1093
  }
474
1094
 
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);}})();";
1095
+ if (shouldOpenSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO)) {
1096
+ callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO, true, false));
1097
+ return;
1098
+ }
479
1099
 
480
- cordova.getActivity().runOnUiThread(new Runnable() {
1100
+ microphonePermissionCallback = callbackContext;
1101
+ rememberRuntimePermissionRequest(Manifest.permission.RECORD_AUDIO);
1102
+ cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
1103
+ }
1104
+
1105
+ private JSONObject microphoneStatus() throws Exception {
1106
+ JSONObject result = new JSONObject();
1107
+ result.put("permission", "android.permission.RECORD_AUDIO");
1108
+ result.put("required", true);
1109
+ result.put("granted", hasMicrophonePermission());
1110
+ result.put("permissionGranted", hasMicrophonePermission());
1111
+ result.put("requiresSettings", !hasMicrophonePermission() && shouldOpenSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO));
1112
+ result.put("settingsOpened", false);
1113
+ result.put("recording", micRecorder != null);
1114
+ result.put("gravando", micRecorder != null);
1115
+ if (micRecorder != null) {
1116
+ result.put("startedAt", micRecordingStartedAt);
1117
+ result.put("iniciadoEm", micRecordingStartedAt);
1118
+ result.put("durationMs", System.currentTimeMillis() - micRecordingStartedAt);
1119
+ result.put("duracaoMs", System.currentTimeMillis() - micRecordingStartedAt);
1120
+ }
1121
+ return result;
1122
+ }
1123
+
1124
+ private void startMicRecording(CallbackContext callbackContext) throws Exception {
1125
+ if (micRecorder != null) {
1126
+ JSONObject result = microphoneStatus();
1127
+ result.put("alreadyRecording", true);
1128
+ result.put("jaGravando", true);
1129
+ callbackContext.success(result);
1130
+ return;
1131
+ }
1132
+
1133
+ if (!hasMicrophonePermission()) {
1134
+ if (shouldOpenSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO)) {
1135
+ callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.RECORD_AUDIO, true, false));
1136
+ return;
1137
+ }
1138
+
1139
+ pendingMicStartCallback = callbackContext;
1140
+ rememberRuntimePermissionRequest(Manifest.permission.RECORD_AUDIO);
1141
+ cordova.requestPermission(this, REQUEST_RECORD_AUDIO, Manifest.permission.RECORD_AUDIO);
1142
+ return;
1143
+ }
1144
+
1145
+ startMicRecorder(callbackContext);
1146
+ }
1147
+
1148
+ private void startMicRecorder(CallbackContext callbackContext) throws Exception {
1149
+ File audioDir = new File(context().getCacheDir(), "html2apk-audio");
1150
+ if (!audioDir.exists() && !audioDir.mkdirs()) {
1151
+ throw new Exception("Could not create audio cache directory.");
1152
+ }
1153
+
1154
+ File audioFile = File.createTempFile("mic-", ".m4a", audioDir);
1155
+ MediaRecorder recorder = new MediaRecorder();
1156
+ try {
1157
+ recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
1158
+ recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
1159
+ recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
1160
+ recorder.setAudioEncodingBitRate(128000);
1161
+ recorder.setAudioSamplingRate(44100);
1162
+ recorder.setOutputFile(audioFile.getAbsolutePath());
1163
+ recorder.prepare();
1164
+ recorder.start();
1165
+ } catch (Exception error) {
1166
+ try {
1167
+ recorder.release();
1168
+ } catch (Exception ignored) {
1169
+ }
1170
+ audioFile.delete();
1171
+ throw error;
1172
+ }
1173
+
1174
+ micRecordingFile = audioFile;
1175
+ micRecordingStartedAt = System.currentTimeMillis();
1176
+ micRecorder = recorder;
1177
+
1178
+ JSONObject result = microphoneStatus();
1179
+ result.put("mimeType", "audio/mp4");
1180
+ result.put("extension", "m4a");
1181
+ result.put("extensao", "m4a");
1182
+ result.put("fileName", micRecordingFile.getName());
1183
+ result.put("nomeArquivo", micRecordingFile.getName());
1184
+ callbackContext.success(result);
1185
+ }
1186
+
1187
+ private JSONObject stopMicRecording() throws Exception {
1188
+ if (micRecorder == null || micRecordingFile == null) {
1189
+ throw new Exception("Microphone is not recording. Call ouvirMic() first.");
1190
+ }
1191
+
1192
+ MediaRecorder recorder = micRecorder;
1193
+ File audioFile = micRecordingFile;
1194
+ long startedAt = micRecordingStartedAt;
1195
+ long endedAt;
1196
+ Exception stopError = null;
1197
+
1198
+ micRecorder = null;
1199
+ micRecordingFile = null;
1200
+ micRecordingStartedAt = 0;
1201
+
1202
+ try {
1203
+ recorder.stop();
1204
+ } catch (RuntimeException error) {
1205
+ stopError = new Exception("Could not finalize audio recording. Wait a little longer before calling pararMic().");
1206
+ } finally {
1207
+ try {
1208
+ recorder.reset();
1209
+ } catch (Exception ignored) {
1210
+ }
1211
+ try {
1212
+ recorder.release();
1213
+ } catch (Exception ignored) {
1214
+ }
1215
+ }
1216
+
1217
+ if (stopError != null) {
1218
+ audioFile.delete();
1219
+ throw stopError;
1220
+ }
1221
+
1222
+ endedAt = System.currentTimeMillis();
1223
+ byte[] audioBytes = readFileBytes(audioFile);
1224
+ String base64 = Base64.encodeToString(audioBytes, Base64.NO_WRAP);
1225
+ audioFile.delete();
1226
+
1227
+ JSONObject result = new JSONObject();
1228
+ result.put("base64", base64);
1229
+ result.put("mimeType", "audio/mp4");
1230
+ result.put("extension", "m4a");
1231
+ result.put("extensao", "m4a");
1232
+ result.put("size", audioBytes.length);
1233
+ result.put("tamanho", audioBytes.length);
1234
+ result.put("startedAt", startedAt);
1235
+ result.put("iniciadoEm", startedAt);
1236
+ result.put("endedAt", endedAt);
1237
+ result.put("finalizadoEm", endedAt);
1238
+ result.put("durationMs", endedAt - startedAt);
1239
+ result.put("duracaoMs", endedAt - startedAt);
1240
+ result.put("recording", false);
1241
+ result.put("gravando", false);
1242
+ return result;
1243
+ }
1244
+
1245
+ private byte[] readFileBytes(File file) throws Exception {
1246
+ InputStream inputStream = new FileInputStream(file);
1247
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1248
+ try {
1249
+ byte[] buffer = new byte[8192];
1250
+ int read;
1251
+ while ((read = inputStream.read(buffer)) != -1) {
1252
+ outputStream.write(buffer, 0, read);
1253
+ }
1254
+ return outputStream.toByteArray();
1255
+ } finally {
1256
+ inputStream.close();
1257
+ outputStream.close();
1258
+ }
1259
+ }
1260
+
1261
+ private void stopMicRecorderSilently() {
1262
+ if (micRecorder == null) {
1263
+ return;
1264
+ }
1265
+
1266
+ MediaRecorder recorder = micRecorder;
1267
+ File audioFile = micRecordingFile;
1268
+ micRecorder = null;
1269
+ micRecordingFile = null;
1270
+ micRecordingStartedAt = 0;
1271
+
1272
+ try {
1273
+ recorder.stop();
1274
+ } catch (Exception ignored) {
1275
+ }
1276
+ try {
1277
+ recorder.reset();
1278
+ } catch (Exception ignored) {
1279
+ }
1280
+ try {
1281
+ recorder.release();
1282
+ } catch (Exception ignored) {
1283
+ }
1284
+ if (audioFile != null) {
1285
+ audioFile.delete();
1286
+ }
1287
+ }
1288
+
1289
+ private String torchCameraId() throws Exception {
1290
+ CameraManager manager = (CameraManager) context().getSystemService(Context.CAMERA_SERVICE);
1291
+ if (manager == null) {
1292
+ throw new Exception("Camera service is not available.");
1293
+ }
1294
+
1295
+ for (String cameraId : manager.getCameraIdList()) {
1296
+ CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
1297
+ Boolean flashAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
1298
+ if (Boolean.TRUE.equals(flashAvailable)) {
1299
+ return cameraId;
1300
+ }
1301
+ }
1302
+
1303
+ throw new Exception("This device does not expose a flashlight.");
1304
+ }
1305
+
1306
+ private void setFlashlight(boolean enabled) throws Exception {
1307
+ if (!hasCameraPermission()) {
1308
+ throw new Exception("CAMERA permission is not granted.");
1309
+ }
1310
+
1311
+ CameraManager manager = (CameraManager) context().getSystemService(Context.CAMERA_SERVICE);
1312
+ if (manager == null) {
1313
+ throw new Exception("Camera service is not available.");
1314
+ }
1315
+
1316
+ manager.setTorchMode(torchCameraId(), enabled);
1317
+ torchEnabled = enabled;
1318
+ }
1319
+
1320
+ private void setFlashlightWithPermission(boolean enabled, boolean toggle, CallbackContext callbackContext) throws Exception {
1321
+ if (!hasCameraPermission()) {
1322
+ if (shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)) {
1323
+ callbackContext.success(openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, false));
1324
+ return;
1325
+ }
1326
+
1327
+ pendingFlashlightCallback = callbackContext;
1328
+ pendingFlashlightEnabled = enabled;
1329
+ pendingFlashlightToggle = toggle;
1330
+ rememberRuntimePermissionRequest(Manifest.permission.CAMERA);
1331
+ cordova.requestPermission(this, REQUEST_CAMERA, Manifest.permission.CAMERA);
1332
+ return;
1333
+ }
1334
+
1335
+ setFlashlight(toggle ? !torchEnabled : enabled);
1336
+ callbackContext.success(flashlightStatus());
1337
+ }
1338
+
1339
+ private JSONObject flashlightStatus() throws Exception {
1340
+ JSONObject result = new JSONObject();
1341
+ result.put("available", context().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH));
1342
+ result.put("enabled", torchEnabled);
1343
+ result.put("permission", "android.permission.CAMERA");
1344
+ result.put("permissionGranted", hasCameraPermission());
1345
+ result.put("requiresSettings", !hasCameraPermission() && shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA));
1346
+ result.put("settingsOpened", false);
1347
+ return result;
1348
+ }
1349
+
1350
+ private String readText() {
1351
+ ClipboardManager clipboard = (ClipboardManager) context().getSystemService(Context.CLIPBOARD_SERVICE);
1352
+ if (clipboard == null || !clipboard.hasPrimaryClip() || clipboard.getPrimaryClip() == null) {
1353
+ return "";
1354
+ }
1355
+
1356
+ ClipData clip = clipboard.getPrimaryClip();
1357
+ if (clip.getItemCount() == 0 || clip.getItemAt(0) == null) {
1358
+ return "";
1359
+ }
1360
+
1361
+ CharSequence text = clip.getItemAt(0).coerceToText(context());
1362
+ return text == null ? "" : text.toString();
1363
+ }
1364
+
1365
+ private void share(JSONObject options) {
1366
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
1367
+ String text = safeOptions.optString("texto", safeOptions.optString("text", ""));
1368
+ String url = safeOptions.optString("url", "");
1369
+ String title = safeOptions.optString("titulo", safeOptions.optString("title", "Compartilhar"));
1370
+ StringBuilder content = new StringBuilder();
1371
+ if (text.length() > 0) {
1372
+ content.append(text);
1373
+ }
1374
+ if (url.length() > 0) {
1375
+ if (content.length() > 0) {
1376
+ content.append("\n");
1377
+ }
1378
+ content.append(url);
1379
+ }
1380
+
1381
+ Intent intent = new Intent(Intent.ACTION_SEND);
1382
+ intent.setType(safeOptions.optString("mimeType", "text/plain"));
1383
+ intent.putExtra(Intent.EXTRA_TEXT, content.toString());
1384
+ intent.putExtra(Intent.EXTRA_TITLE, title);
1385
+ cordova.getActivity().startActivity(Intent.createChooser(intent, title));
1386
+ }
1387
+
1388
+ private void dial(String phone) throws Exception {
1389
+ String cleaned = phone == null ? "" : phone.trim();
1390
+ if (cleaned.length() == 0) {
1391
+ throw new Exception("Phone number is required.");
1392
+ }
1393
+
1394
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + Uri.encode(cleaned)));
1395
+ cordova.getActivity().startActivity(intent);
1396
+ }
1397
+
1398
+ private void openMap(String query) throws Exception {
1399
+ String cleaned = query == null ? "" : query.trim();
1400
+ if (cleaned.length() == 0) {
1401
+ throw new Exception("Map query is required.");
1402
+ }
1403
+
1404
+ Uri uri = cleaned.startsWith("geo:") || cleaned.startsWith("http")
1405
+ ? Uri.parse(cleaned)
1406
+ : Uri.parse("geo:0,0?q=" + Uri.encode(cleaned));
1407
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1408
+ cordova.getActivity().startActivity(intent);
1409
+ }
1410
+
1411
+ private void openWhatsapp(String phone, String message) throws Exception {
1412
+ String digits = phone == null ? "" : phone.replaceAll("[^0-9]", "");
1413
+ if (digits.length() == 0) {
1414
+ throw new Exception("WhatsApp phone number is required.");
1415
+ }
1416
+
1417
+ String url = "https://wa.me/" + digits;
1418
+ if (message != null && message.trim().length() > 0) {
1419
+ url += "?text=" + Uri.encode(message.trim());
1420
+ }
1421
+ openUrl(url);
1422
+ }
1423
+
1424
+ private void pickFile(JSONObject options, CallbackContext callbackContext) {
1425
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
1426
+ String kind = safeOptions.optString("tipo", safeOptions.optString("kind", "file"));
1427
+ boolean multiple = safeOptions.optBoolean("multiplo", safeOptions.optBoolean("multiple", false));
1428
+ String mimeType = mimeTypeForPicker(kind, safeOptions);
1429
+
1430
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
1431
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
1432
+ intent.setType(mimeType);
1433
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple);
1434
+
1435
+ filePickerCallback = callbackContext;
1436
+ cordova.startActivityForResult(this, intent, REQUEST_PICK_FILE);
1437
+ }
1438
+
1439
+ private String mimeTypeForPicker(String kind, JSONObject options) {
1440
+ String type = options.optString("mimeType", options.optString("tipoMime", ""));
1441
+ if (type.length() > 0) {
1442
+ return type;
1443
+ }
1444
+
1445
+ JSONArray types = options.optJSONArray("tipos");
1446
+ if (types == null) {
1447
+ types = options.optJSONArray("types");
1448
+ }
1449
+ if (types != null && types.length() == 1) {
1450
+ return types.optString(0, "*/*");
1451
+ }
1452
+
1453
+ if ("image".equals(kind)) {
1454
+ return "image/*";
1455
+ }
1456
+ if ("video".equals(kind)) {
1457
+ return "video/*";
1458
+ }
1459
+ if ("media".equals(kind)) {
1460
+ return "image/*";
1461
+ }
1462
+ return "*/*";
1463
+ }
1464
+
1465
+ private void pickFolder(CallbackContext callbackContext) {
1466
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
1467
+ folderPickerCallback = callbackContext;
1468
+ cordova.startActivityForResult(this, intent, REQUEST_PICK_FOLDER);
1469
+ }
1470
+
1471
+ private void saveFile(JSONObject options, CallbackContext callbackContext) throws Exception {
1472
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
1473
+ String name = safeOptions.optString("nome", safeOptions.optString("name", "arquivo.txt"));
1474
+ String mimeType = safeOptions.optString("mimeType", "text/plain");
1475
+
1476
+ pendingSaveFile = safeOptions;
1477
+ saveFileCallback = callbackContext;
1478
+
1479
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
1480
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
1481
+ intent.setType(mimeType);
1482
+ intent.putExtra(Intent.EXTRA_TITLE, name);
1483
+ cordova.startActivityForResult(this, intent, REQUEST_SAVE_FILE);
1484
+ }
1485
+
1486
+ private void handlePickFileResult(int resultCode, Intent intent) {
1487
+ CallbackContext callback = filePickerCallback;
1488
+ filePickerCallback = null;
1489
+ if (callback == null) {
1490
+ return;
1491
+ }
1492
+ if (resultCode != Activity.RESULT_OK || intent == null) {
1493
+ callback.success(new JSONArray());
1494
+ return;
1495
+ }
1496
+
1497
+ try {
1498
+ JSONArray items = new JSONArray();
1499
+ if (intent.getClipData() != null) {
1500
+ ClipData clipData = intent.getClipData();
1501
+ for (int index = 0; index < clipData.getItemCount(); index += 1) {
1502
+ items.put(fileInfo(clipData.getItemAt(index).getUri()));
1503
+ }
1504
+ } else if (intent.getData() != null) {
1505
+ items.put(fileInfo(intent.getData()));
1506
+ }
1507
+ callback.success(items);
1508
+ } catch (Exception error) {
1509
+ callback.error(error.getMessage());
1510
+ }
1511
+ }
1512
+
1513
+ private void handlePickFolderResult(int resultCode, Intent intent) {
1514
+ CallbackContext callback = folderPickerCallback;
1515
+ folderPickerCallback = null;
1516
+ if (callback == null) {
1517
+ return;
1518
+ }
1519
+ if (resultCode != Activity.RESULT_OK || intent == null || intent.getData() == null) {
1520
+ callback.success(new JSONObject());
1521
+ return;
1522
+ }
1523
+
1524
+ try {
1525
+ JSONObject result = new JSONObject();
1526
+ result.put("uri", intent.getData().toString());
1527
+ callback.success(result);
1528
+ } catch (Exception error) {
1529
+ callback.error(error.getMessage());
1530
+ }
1531
+ }
1532
+
1533
+ private void handleSaveFileResult(int resultCode, Intent intent) {
1534
+ CallbackContext callback = saveFileCallback;
1535
+ JSONObject options = pendingSaveFile;
1536
+ saveFileCallback = null;
1537
+ pendingSaveFile = null;
1538
+ if (callback == null) {
1539
+ return;
1540
+ }
1541
+ if (resultCode != Activity.RESULT_OK || intent == null || intent.getData() == null) {
1542
+ callback.success(new JSONObject());
1543
+ return;
1544
+ }
1545
+
1546
+ try {
1547
+ Uri uri = intent.getData();
1548
+ writePickedFile(uri, options == null ? new JSONObject() : options);
1549
+ JSONObject result = fileInfo(uri);
1550
+ result.put("saved", true);
1551
+ callback.success(result);
1552
+ } catch (Exception error) {
1553
+ callback.error(error.getMessage());
1554
+ }
1555
+ }
1556
+
1557
+ private JSONObject fileInfo(Uri uri) throws Exception {
1558
+ JSONObject result = new JSONObject();
1559
+ result.put("uri", uri.toString());
1560
+ result.put("mimeType", context().getContentResolver().getType(uri));
1561
+
1562
+ Cursor cursor = context().getContentResolver().query(uri, null, null, null, null);
1563
+ if (cursor != null) {
1564
+ try {
1565
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
1566
+ int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
1567
+ if (cursor.moveToFirst()) {
1568
+ if (nameIndex >= 0) {
1569
+ result.put("name", cursor.getString(nameIndex));
1570
+ result.put("nome", cursor.getString(nameIndex));
1571
+ }
1572
+ if (sizeIndex >= 0) {
1573
+ result.put("size", cursor.getLong(sizeIndex));
1574
+ result.put("tamanho", cursor.getLong(sizeIndex));
1575
+ }
1576
+ }
1577
+ } finally {
1578
+ cursor.close();
1579
+ }
1580
+ }
1581
+ return result;
1582
+ }
1583
+
1584
+ private void writePickedFile(Uri uri, JSONObject options) throws Exception {
1585
+ OutputStream outputStream = context().getContentResolver().openOutputStream(uri);
1586
+ if (outputStream == null) {
1587
+ throw new Exception("Could not open output stream.");
1588
+ }
1589
+
1590
+ try {
1591
+ InputStream inputStream;
1592
+ String base64 = options.optString("base64", "");
1593
+ if (base64.length() > 0) {
1594
+ inputStream = new ByteArrayInputStream(Base64.decode(base64, Base64.DEFAULT));
1595
+ } else {
1596
+ String content = options.optString("conteudo", options.optString("content", ""));
1597
+ inputStream = new ByteArrayInputStream(content.getBytes("UTF-8"));
1598
+ }
1599
+
1600
+ byte[] buffer = new byte[8192];
1601
+ int read;
1602
+ while ((read = inputStream.read(buffer)) != -1) {
1603
+ outputStream.write(buffer, 0, read);
1604
+ }
1605
+ inputStream.close();
1606
+ } finally {
1607
+ outputStream.close();
1608
+ }
1609
+ }
1610
+
1611
+ private JSONObject deviceInfo() throws Exception {
1612
+ JSONObject result = new JSONObject();
1613
+ result.put("manufacturer", Build.MANUFACTURER);
1614
+ result.put("fabricante", Build.MANUFACTURER);
1615
+ result.put("model", Build.MODEL);
1616
+ result.put("modelo", Build.MODEL);
1617
+ result.put("brand", Build.BRAND);
1618
+ result.put("androidVersion", Build.VERSION.RELEASE);
1619
+ result.put("sdkInt", Build.VERSION.SDK_INT);
1620
+ result.put("packageName", context().getPackageName());
1621
+ return result;
1622
+ }
1623
+
1624
+ private JSONObject networkInfo() throws Exception {
1625
+ JSONObject result = new JSONObject();
1626
+ ConnectivityManager manager = (ConnectivityManager) context().getSystemService(Context.CONNECTIVITY_SERVICE);
1627
+ boolean connected = false;
1628
+ String type = "unknown";
1629
+
1630
+ if (manager != null) {
1631
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1632
+ Network network = manager.getActiveNetwork();
1633
+ NetworkCapabilities capabilities = network == null ? null : manager.getNetworkCapabilities(network);
1634
+ connected = capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1635
+ if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
1636
+ type = "wifi";
1637
+ } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
1638
+ type = "cellular";
1639
+ } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
1640
+ type = "ethernet";
1641
+ }
1642
+ } else if (manager.getActiveNetworkInfo() != null) {
1643
+ connected = manager.getActiveNetworkInfo().isConnected();
1644
+ type = manager.getActiveNetworkInfo().getTypeName().toLowerCase();
1645
+ }
1646
+ }
1647
+
1648
+ result.put("online", connected);
1649
+ result.put("connected", connected);
1650
+ result.put("tipo", type);
1651
+ result.put("type", type);
1652
+ return result;
1653
+ }
1654
+
1655
+ private JSONObject batteryInfo() throws Exception {
1656
+ Intent intent = context().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
1657
+ JSONObject result = new JSONObject();
1658
+ if (intent == null) {
1659
+ result.put("level", -1);
1660
+ result.put("nivel", -1);
1661
+ result.put("charging", false);
1662
+ return result;
1663
+ }
1664
+
1665
+ int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
1666
+ int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
1667
+ int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
1668
+ boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING
1669
+ || status == BatteryManager.BATTERY_STATUS_FULL;
1670
+ double percent = scale <= 0 ? -1 : (level * 100.0 / scale);
1671
+
1672
+ result.put("level", percent);
1673
+ result.put("nivel", percent);
1674
+ result.put("charging", charging);
1675
+ result.put("carregando", charging);
1676
+ return result;
1677
+ }
1678
+
1679
+ private JSONObject memoryInfo() throws Exception {
1680
+ ActivityManager manager = (ActivityManager) context().getSystemService(Context.ACTIVITY_SERVICE);
1681
+ ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
1682
+ if (manager != null) {
1683
+ manager.getMemoryInfo(info);
1684
+ }
1685
+
1686
+ Runtime runtime = Runtime.getRuntime();
1687
+ JSONObject result = new JSONObject();
1688
+ result.put("availableBytes", info.availMem);
1689
+ result.put("totalBytes", Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? info.totalMem : -1);
1690
+ result.put("lowMemory", info.lowMemory);
1691
+ result.put("thresholdBytes", info.threshold);
1692
+ result.put("appUsedBytes", runtime.totalMemory() - runtime.freeMemory());
1693
+ result.put("appMaxBytes", runtime.maxMemory());
1694
+ return result;
1695
+ }
1696
+
1697
+ private JSONObject storageInfo() throws Exception {
1698
+ JSONObject result = new JSONObject();
1699
+ result.put("internal", statFsInfo(Environment.getDataDirectory()));
1700
+ result.put("cache", statFsInfo(context().getCacheDir()));
1701
+ if (context().getExternalFilesDir(null) != null) {
1702
+ result.put("appExternal", statFsInfo(context().getExternalFilesDir(null)));
1703
+ }
1704
+ return result;
1705
+ }
1706
+
1707
+ private JSONObject statFsInfo(java.io.File file) throws Exception {
1708
+ StatFs statFs = new StatFs(file.getAbsolutePath());
1709
+ long blockSize = statFs.getBlockSizeLong();
1710
+ long total = statFs.getBlockCountLong() * blockSize;
1711
+ long available = statFs.getAvailableBlocksLong() * blockSize;
1712
+ JSONObject result = new JSONObject();
1713
+ result.put("path", file.getAbsolutePath());
1714
+ result.put("totalBytes", total);
1715
+ result.put("availableBytes", available);
1716
+ result.put("usedBytes", total - available);
1717
+ return result;
1718
+ }
1719
+
1720
+ private JSONObject performanceInfo() throws Exception {
1721
+ JSONObject result = new JSONObject();
1722
+ result.put("timestamp", System.currentTimeMillis());
1723
+ result.put("memory", memoryInfo());
1724
+ result.put("storage", storageInfo());
1725
+ result.put("battery", batteryInfo());
1726
+ result.put("network", networkInfo());
1727
+ return result;
1728
+ }
1729
+
1730
+ private JSONObject openAppsMemoryInfo() throws Exception {
1731
+ ActivityManager manager = (ActivityManager) context().getSystemService(Context.ACTIVITY_SERVICE);
1732
+ JSONArray apps = new JSONArray();
1733
+ JSONObject byName = new JSONObject();
1734
+ JSONObject result = new JSONObject();
1735
+
1736
+ result.put("timestamp", System.currentTimeMillis());
1737
+ result.put("limited", true);
1738
+ result.put("observacao", "Android moderno limita a lista de apps de terceiros por privacidade; o retorno mostra apenas processos visiveis para este app.");
1739
+ result.put("note", "Modern Android limits third-party app process visibility for privacy; this returns only processes visible to this app.");
1740
+
1741
+ if (manager == null) {
1742
+ result.put("apps", apps);
1743
+ result.put("porNome", byName);
1744
+ result.put("byName", byName);
1745
+ return result;
1746
+ }
1747
+
1748
+ List<ActivityManager.RunningAppProcessInfo> processes = manager.getRunningAppProcesses();
1749
+ if (processes == null) {
1750
+ result.put("apps", apps);
1751
+ result.put("porNome", byName);
1752
+ result.put("byName", byName);
1753
+ return result;
1754
+ }
1755
+
1756
+ for (ActivityManager.RunningAppProcessInfo process : processes) {
1757
+ Debug.MemoryInfo[] memoryInfos = manager.getProcessMemoryInfo(new int[] { process.pid });
1758
+ Debug.MemoryInfo memory = memoryInfos != null && memoryInfos.length > 0 ? memoryInfos[0] : null;
1759
+ long pssKb = memory == null ? 0 : memory.getTotalPss();
1760
+ long ramBytes = pssKb * 1024L;
1761
+ String packageName = process.pkgList != null && process.pkgList.length > 0 ? process.pkgList[0] : process.processName;
1762
+ String name = appLabel(packageName, process.processName);
1763
+
1764
+ JSONObject item = new JSONObject();
1765
+ item.put("name", name);
1766
+ item.put("nome", name);
1767
+ item.put("packageName", packageName);
1768
+ item.put("pacote", packageName);
1769
+ item.put("processName", process.processName);
1770
+ item.put("pid", process.pid);
1771
+ item.put("importance", process.importance);
1772
+ item.put("importanceName", importanceName(process.importance));
1773
+ item.put("ramBytes", ramBytes);
1774
+ item.put("ramMb", Math.round((ramBytes / 1024.0 / 1024.0) * 100.0) / 100.0);
1775
+ item.put("pssKb", pssKb);
1776
+ if (memory != null) {
1777
+ item.put("privateDirtyKb", memory.getTotalPrivateDirty());
1778
+ item.put("sharedDirtyKb", memory.getTotalSharedDirty());
1779
+ }
1780
+
1781
+ JSONArray packages = new JSONArray();
1782
+ if (process.pkgList != null) {
1783
+ for (String packageItem : process.pkgList) {
1784
+ packages.put(packageItem);
1785
+ }
1786
+ }
1787
+ item.put("packages", packages);
1788
+ item.put("pacotes", packages);
1789
+ apps.put(item);
1790
+
1791
+ JSONObject summary = new JSONObject();
1792
+ summary.put("ramBytes", ramBytes);
1793
+ summary.put("ramMb", Math.round((ramBytes / 1024.0 / 1024.0) * 100.0) / 100.0);
1794
+ summary.put("packageName", packageName);
1795
+ summary.put("processName", process.processName);
1796
+ byName.put(name, summary);
1797
+ }
1798
+
1799
+ result.put("apps", apps);
1800
+ result.put("porNome", byName);
1801
+ result.put("byName", byName);
1802
+ return result;
1803
+ }
1804
+
1805
+ private String appLabel(String packageName, String fallback) {
1806
+ try {
1807
+ PackageManager packageManager = context().getPackageManager();
1808
+ ApplicationInfo info = packageManager.getApplicationInfo(packageName, 0);
1809
+ CharSequence label = packageManager.getApplicationLabel(info);
1810
+ return label == null ? fallback : label.toString();
1811
+ } catch (Exception ignored) {
1812
+ return fallback == null ? packageName : fallback;
1813
+ }
1814
+ }
1815
+
1816
+ private String importanceName(int importance) {
1817
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
1818
+ return "foreground";
1819
+ }
1820
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
1821
+ return "visible";
1822
+ }
1823
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE) {
1824
+ return "service";
1825
+ }
1826
+ if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED) {
1827
+ return "cached";
1828
+ }
1829
+ return "other";
1830
+ }
1831
+
1832
+ private JSONObject permissionStatus(JSONArray requested) throws Exception {
1833
+ JSONArray permissions = requested == null ? new JSONArray() : requested;
1834
+ JSONObject result = new JSONObject();
1835
+ for (int index = 0; index < permissions.length(); index += 1) {
1836
+ String permission = androidPermissionName(permissions.optString(index, ""));
1837
+ if (permission.length() == 0) {
1838
+ continue;
1839
+ }
1840
+ result.put(permission, ContextCompat.checkSelfPermission(context(), permission) == PackageManager.PERMISSION_GRANTED);
1841
+ }
1842
+ return result;
1843
+ }
1844
+
1845
+ private String androidPermissionName(String permission) {
1846
+ String value = permission == null ? "" : permission.trim();
1847
+ if (value.length() == 0 || value.indexOf('.') >= 0) {
1848
+ return value;
1849
+ }
1850
+ return "android.permission." + value;
1851
+ }
1852
+
1853
+ private JSONObject baseEvent(String type) {
1854
+ JSONObject detail = new JSONObject();
1855
+ try {
1856
+ detail.put("type", type);
1857
+ detail.put("tipo", type);
1858
+ detail.put("timestamp", System.currentTimeMillis());
1859
+ } catch (Exception ignored) {
1860
+ }
1861
+ return detail;
1862
+ }
1863
+
1864
+ private void registerSystemReceiver() {
1865
+ if (systemReceiver != null) {
1866
+ return;
1867
+ }
1868
+
1869
+ systemReceiver = new BroadcastReceiver() {
1870
+ @Override
1871
+ public void onReceive(Context context, Intent intent) {
1872
+ if (intent == null || intent.getAction() == null) {
1873
+ return;
1874
+ }
1875
+
1876
+ try {
1877
+ if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
1878
+ dispatchEvent("bateria:mudou", batteryInfo());
1879
+ } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
1880
+ dispatchEvent("rede:mudou", networkInfo());
1881
+ }
1882
+ } catch (Exception ignored) {
1883
+ }
1884
+ }
1885
+ };
1886
+
1887
+ IntentFilter filter = new IntentFilter();
1888
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
1889
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1890
+ ContextCompat.registerReceiver(context(), systemReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
1891
+ }
1892
+
1893
+ private void unregisterSystemReceiver() {
1894
+ if (systemReceiver == null) {
1895
+ return;
1896
+ }
1897
+
1898
+ try {
1899
+ context().unregisterReceiver(systemReceiver);
1900
+ } catch (Exception ignored) {
1901
+ }
1902
+ systemReceiver = null;
1903
+ }
1904
+
1905
+ private void handleLinkIntent(Intent intent, boolean dispatchToJs) {
1906
+ if (intent == null || intent.getData() == null) {
1907
+ return;
1908
+ }
1909
+
1910
+ JSONObject detail = new JSONObject();
1911
+ try {
1912
+ Uri uri = intent.getData();
1913
+ detail.put("url", uri.toString());
1914
+ detail.put("scheme", uri.getScheme());
1915
+ detail.put("host", uri.getHost());
1916
+ detail.put("path", uri.getPath());
1917
+ detail.put("query", uri.getQuery());
1918
+ detail.put("timestamp", System.currentTimeMillis());
1919
+ } catch (Exception ignored) {
1920
+ }
1921
+
1922
+ initialLink = detail;
1923
+ if (dispatchToJs) {
1924
+ dispatchEvent("link:aberto", detail);
1925
+ }
1926
+ }
1927
+
1928
+ private void dispatchEvent(String type, JSONObject detail) {
1929
+ if (webView == null) {
1930
+ return;
1931
+ }
1932
+
1933
+ JSONObject payload = detail == null ? baseEvent(type) : detail;
1934
+ try {
1935
+ payload.put("type", type);
1936
+ payload.put("tipo", type);
1937
+ if (!payload.has("timestamp")) {
1938
+ payload.put("timestamp", System.currentTimeMillis());
1939
+ }
1940
+ } catch (Exception ignored) {
1941
+ }
1942
+
1943
+ final String script = "(function(){var detail=" + payload.toString()
1944
+ + ";window.dispatchEvent(new CustomEvent('html2apk:event',{detail:detail}));"
1945
+ + "window.dispatchEvent(new CustomEvent('html2apk:'+detail.type,{detail:detail}));})();";
1946
+
1947
+ cordova.getActivity().runOnUiThread(new Runnable() {
1948
+ @Override
1949
+ public void run() {
1950
+ webView.getEngine().evaluateJavascript(script, null);
1951
+ }
1952
+ });
1953
+ }
1954
+
1955
+ private void handleNotificationIntent(Intent intent, boolean dispatchToJs) {
1956
+ if (intent == null || !intent.getBooleanExtra(EXTRA_NOTIFICATION_CLICKED, false)) {
1957
+ return;
1958
+ }
1959
+
1960
+ JSONObject detail = parseDetail(intent.getStringExtra(EXTRA_NOTIFICATION_DETAIL));
1961
+ initialNotification = detail;
1962
+
1963
+ if (dispatchToJs) {
1964
+ dispatchNotificationClick(detail);
1965
+ }
1966
+
1967
+ intent.removeExtra(EXTRA_NOTIFICATION_CLICKED);
1968
+ intent.removeExtra(EXTRA_NOTIFICATION_DETAIL);
1969
+ }
1970
+
1971
+ private void dispatchNotificationClick(JSONObject detail) {
1972
+ if (detail == null || webView == null) {
1973
+ return;
1974
+ }
1975
+
1976
+ dispatchEvent("notificacao:clicada", detail);
1977
+
1978
+ final String script = "(function(){var detail=" + detail.toString()
1979
+ + ";window.dispatchEvent(new CustomEvent('html2apk:notification',{detail:detail}));})();";
1980
+
1981
+ cordova.getActivity().runOnUiThread(new Runnable() {
481
1982
  @Override
482
1983
  public void run() {
483
1984
  webView.getEngine().evaluateJavascript(script, null);
@@ -515,6 +2016,206 @@ public class Html2ApkBridge extends CordovaPlugin {
515
2016
  );
516
2017
  }
517
2018
 
2019
+ static boolean canScheduleExactAlarms(Context context) {
2020
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
2021
+ return true;
2022
+ }
2023
+
2024
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
2025
+ return alarmManager != null && alarmManager.canScheduleExactAlarms();
2026
+ }
2027
+
2028
+ static JSONArray loopNotifications(JSONObject options) {
2029
+ JSONArray notifications = options.optJSONArray("notificacoes");
2030
+ if (notifications == null) {
2031
+ notifications = options.optJSONArray("notifications");
2032
+ }
2033
+ if (notifications == null) {
2034
+ notifications = options.optJSONArray("items");
2035
+ }
2036
+ return notifications == null ? new JSONArray() : notifications;
2037
+ }
2038
+
2039
+ static JSONObject notificationDisplayOptions(JSONObject options) throws Exception {
2040
+ JSONArray notifications = loopNotifications(options);
2041
+ if (notifications.length() == 0) {
2042
+ return options;
2043
+ }
2044
+
2045
+ int index = Math.max(0, options.optInt("loopIndex", options.optInt("indiceLoop", 0)));
2046
+ JSONObject current = notifications.optJSONObject(index % notifications.length());
2047
+ JSONObject displayOptions = new JSONObject(options.toString());
2048
+ if (current != null) {
2049
+ JSONArray names = current.names();
2050
+ if (names != null) {
2051
+ for (int nameIndex = 0; nameIndex < names.length(); nameIndex += 1) {
2052
+ String name = names.optString(nameIndex);
2053
+ displayOptions.put(name, current.opt(name));
2054
+ }
2055
+ }
2056
+ }
2057
+
2058
+ displayOptions.put("id", notificationId(options));
2059
+ displayOptions.put("loopIndex", index);
2060
+ displayOptions.put("indiceLoop", index);
2061
+ displayOptions.put("loopTotal", notifications.length());
2062
+ displayOptions.put("totalLoop", notifications.length());
2063
+ return displayOptions;
2064
+ }
2065
+
2066
+ static long repeatInterval(JSONObject options) {
2067
+ long interval = intervalFromObject(options.opt("intervalo"));
2068
+ if (interval <= 0) {
2069
+ interval = intervalFromObject(options.opt("interval"));
2070
+ }
2071
+ if (interval <= 0) {
2072
+ interval = intervalFromObject(options.opt("aCada"));
2073
+ }
2074
+ if (interval <= 0) {
2075
+ interval = intervalFromObject(options.opt("every"));
2076
+ }
2077
+ if (interval <= 0) {
2078
+ interval = intervalFromObject(options.opt("repetir"));
2079
+ }
2080
+ if (interval <= 0) {
2081
+ interval = intervalFromObject(options.opt("repeat"));
2082
+ }
2083
+ return interval;
2084
+ }
2085
+
2086
+ static long nextRepeatTime(JSONObject options, long after) {
2087
+ long interval = repeatInterval(options);
2088
+ if (interval <= 0) {
2089
+ return 0;
2090
+ }
2091
+
2092
+ long next = options.optLong("when", options.optLong("quando", after)) + interval;
2093
+ while (next <= after) {
2094
+ next += interval;
2095
+ }
2096
+ return next;
2097
+ }
2098
+
2099
+ static JSONObject nextRepeatOptions(JSONObject options, long nextWhen) throws Exception {
2100
+ JSONObject next = new JSONObject(options.toString());
2101
+ JSONArray notifications = loopNotifications(options);
2102
+ int nextIndex = Math.max(0, options.optInt("loopIndex", options.optInt("indiceLoop", 0))) + 1;
2103
+ int runCount = Math.max(0, options.optInt("_runCount", options.optInt("runCount", 0))) + 1;
2104
+
2105
+ next.put("when", nextWhen);
2106
+ next.put("quando", nextWhen);
2107
+ next.put("loopIndex", notifications.length() == 0 ? nextIndex : nextIndex % notifications.length());
2108
+ next.put("indiceLoop", notifications.length() == 0 ? nextIndex : nextIndex % notifications.length());
2109
+ next.put("_runCount", runCount);
2110
+ next.put("runCount", runCount);
2111
+ return next;
2112
+ }
2113
+
2114
+ static boolean shouldStopRepeating(JSONObject options, long nextWhen) {
2115
+ long until = options.optLong("ate", options.optLong("until", 0));
2116
+ if (until > 0 && nextWhen > until) {
2117
+ return true;
2118
+ }
2119
+
2120
+ int maxRuns = options.optInt("vezes", options.optInt("times", options.optInt("count", options.optInt("limit", 0))));
2121
+ int runCount = Math.max(0, options.optInt("_runCount", options.optInt("runCount", 0))) + 1;
2122
+ return maxRuns > 0 && runCount >= maxRuns;
2123
+ }
2124
+
2125
+ static void rescheduleRepeatingNotification(Context context, int id, JSONObject options, boolean exactAllowed) {
2126
+ try {
2127
+ long nextWhen = nextRepeatTime(options, System.currentTimeMillis());
2128
+ if (nextWhen <= 0 || shouldStopRepeating(options, nextWhen)) {
2129
+ NotificationStore.remove(context, id);
2130
+ return;
2131
+ }
2132
+
2133
+ JSONObject nextOptions = nextRepeatOptions(options, nextWhen);
2134
+ nextOptions.put("id", id);
2135
+ NotificationStore.save(context, id, nextWhen, nextOptions);
2136
+ NotificationReceiver.schedule(context, id, nextWhen, nextOptions, exactAllowed);
2137
+ } catch (Exception ignored) {
2138
+ }
2139
+ }
2140
+
2141
+ private static long intervalFromObject(Object value) {
2142
+ if (value == null || value == JSONObject.NULL) {
2143
+ return 0;
2144
+ }
2145
+ if (value instanceof Number) {
2146
+ return Math.max(0, ((Number) value).longValue());
2147
+ }
2148
+ if (value instanceof JSONObject) {
2149
+ JSONObject object = (JSONObject) value;
2150
+ long nested = intervalFromObject(object.opt("intervalo"));
2151
+ if (nested <= 0) {
2152
+ nested = intervalFromObject(object.opt("interval"));
2153
+ }
2154
+ if (nested <= 0) {
2155
+ nested = intervalFromObject(object.opt("aCada"));
2156
+ }
2157
+ if (nested <= 0) {
2158
+ nested = intervalFromObject(object.opt("every"));
2159
+ }
2160
+ return nested;
2161
+ }
2162
+ if (value instanceof Boolean) {
2163
+ return 0;
2164
+ }
2165
+
2166
+ return parseIntervalString(String.valueOf(value));
2167
+ }
2168
+
2169
+ private static long parseIntervalString(String value) {
2170
+ String text = value == null ? "" : value.trim().toLowerCase();
2171
+ if (text.length() == 0) {
2172
+ return 0;
2173
+ }
2174
+
2175
+ int unitStart = text.length();
2176
+ for (int index = 0; index < text.length(); index += 1) {
2177
+ char character = text.charAt(index);
2178
+ if (!(Character.isDigit(character) || character == '.')) {
2179
+ unitStart = index;
2180
+ break;
2181
+ }
2182
+ }
2183
+
2184
+ try {
2185
+ double amount = Double.parseDouble(text.substring(0, unitStart).trim());
2186
+ String unit = text.substring(unitStart).trim();
2187
+ if ("s".equals(unit) || "seg".equals(unit)) {
2188
+ return Math.round(amount * 1000);
2189
+ }
2190
+ if ("m".equals(unit) || "min".equals(unit)) {
2191
+ return Math.round(amount * 60 * 1000);
2192
+ }
2193
+ if ("h".equals(unit) || "hr".equals(unit)) {
2194
+ return Math.round(amount * 60 * 60 * 1000);
2195
+ }
2196
+ if ("d".equals(unit) || "dia".equals(unit) || "dias".equals(unit)) {
2197
+ return Math.round(amount * 24 * 60 * 60 * 1000);
2198
+ }
2199
+ return Math.round(amount);
2200
+ } catch (Exception ignored) {
2201
+ return 0;
2202
+ }
2203
+ }
2204
+
2205
+ private static int notificationIdFromObject(Object input) {
2206
+ if (input instanceof Number) {
2207
+ return ((Number) input).intValue();
2208
+ }
2209
+ if (input instanceof JSONObject) {
2210
+ return notificationId((JSONObject) input);
2211
+ }
2212
+ try {
2213
+ return Integer.parseInt(String.valueOf(input));
2214
+ } catch (Exception ignored) {
2215
+ return 0;
2216
+ }
2217
+ }
2218
+
518
2219
  static JSONObject detailPayload(JSONObject options) throws Exception {
519
2220
  JSONObject detail = new JSONObject();
520
2221
  JSONObject click = options.optJSONObject("aoClicar");
@@ -534,6 +2235,37 @@ public class Html2ApkBridge extends CordovaPlugin {
534
2235
  return detail;
535
2236
  }
536
2237
 
2238
+ static void addNotificationActions(NotificationCompat.Builder builder, Context context, int notificationId, JSONObject options) {
2239
+ JSONArray actions = options.optJSONArray("acoes");
2240
+ if (actions == null) {
2241
+ actions = options.optJSONArray("actions");
2242
+ }
2243
+ if (actions == null) {
2244
+ return;
2245
+ }
2246
+
2247
+ for (int index = 0; index < actions.length(); index += 1) {
2248
+ JSONObject action = actions.optJSONObject(index);
2249
+ if (action == null) {
2250
+ continue;
2251
+ }
2252
+
2253
+ try {
2254
+ String title = action.optString("titulo", action.optString("title", action.optString("id", "Abrir")));
2255
+ JSONObject detail = detailPayload(options);
2256
+ detail.put("notificationAction", action.optString("id", String.valueOf(index)));
2257
+ detail.put("acaoNotificacao", action.optString("id", String.valueOf(index)));
2258
+ detail.put("action", action);
2259
+ builder.addAction(
2260
+ context.getApplicationInfo().icon,
2261
+ title,
2262
+ createContentIntent(context, notificationId * 100 + index + 1, detail)
2263
+ );
2264
+ } catch (Exception ignored) {
2265
+ }
2266
+ }
2267
+ }
2268
+
537
2269
  static int notificationId(JSONObject options) {
538
2270
  int id = options.optInt("id", 0);
539
2271
  if (id != 0) {