html2apk 0.7.0 → 0.9.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.
@@ -4,9 +4,11 @@ import android.Manifest;
4
4
  import android.app.Activity;
5
5
  import android.app.AlarmManager;
6
6
  import android.app.ActivityManager;
7
+ import android.app.KeyguardManager;
7
8
  import android.app.NotificationChannel;
8
9
  import android.app.NotificationManager;
9
10
  import android.app.PendingIntent;
11
+ import android.app.WallpaperManager;
10
12
  import android.content.ActivityNotFoundException;
11
13
  import android.content.BroadcastReceiver;
12
14
  import android.content.ClipData;
@@ -20,7 +22,13 @@ import android.content.pm.PackageManager;
20
22
  import android.database.Cursor;
21
23
  import android.hardware.camera2.CameraCharacteristics;
22
24
  import android.hardware.camera2.CameraManager;
25
+ import android.hardware.biometrics.BiometricPrompt;
26
+ import android.graphics.Bitmap;
27
+ import android.graphics.BitmapFactory;
23
28
  import android.graphics.Color;
29
+ import android.location.Location;
30
+ import android.location.LocationListener;
31
+ import android.location.LocationManager;
24
32
  import android.media.MediaRecorder;
25
33
  import android.net.ConnectivityManager;
26
34
  import android.net.Network;
@@ -28,13 +36,20 @@ import android.net.NetworkCapabilities;
28
36
  import android.net.Uri;
29
37
  import android.os.BatteryManager;
30
38
  import android.os.Build;
39
+ import android.os.Bundle;
40
+ import android.os.CancellationSignal;
31
41
  import android.os.Debug;
32
42
  import android.os.Environment;
43
+ import android.os.Handler;
44
+ import android.os.Looper;
33
45
  import android.os.StatFs;
34
46
  import android.os.VibrationEffect;
35
47
  import android.os.Vibrator;
36
48
  import android.provider.Settings;
49
+ import android.provider.MediaStore;
37
50
  import android.provider.OpenableColumns;
51
+ import android.security.keystore.KeyGenParameterSpec;
52
+ import android.security.keystore.KeyProperties;
38
53
  import android.util.Base64;
39
54
  import android.view.View;
40
55
  import android.view.Window;
@@ -44,6 +59,7 @@ import android.widget.Toast;
44
59
  import androidx.core.app.NotificationCompat;
45
60
  import androidx.core.app.NotificationManagerCompat;
46
61
  import androidx.core.content.ContextCompat;
62
+ import androidx.core.content.FileProvider;
47
63
 
48
64
  import org.apache.cordova.CallbackContext;
49
65
  import org.apache.cordova.CordovaInterface;
@@ -55,10 +71,24 @@ import org.json.JSONObject;
55
71
  import java.io.ByteArrayInputStream;
56
72
  import java.io.ByteArrayOutputStream;
57
73
  import java.io.File;
74
+ import java.io.FileOutputStream;
58
75
  import java.io.FileInputStream;
59
76
  import java.io.InputStream;
60
77
  import java.io.OutputStream;
78
+ import java.net.HttpURLConnection;
79
+ import java.net.URL;
80
+ import java.security.KeyStore;
81
+ import java.util.HashMap;
82
+ import java.util.Iterator;
61
83
  import java.util.List;
84
+ import java.util.Map;
85
+ import java.util.UUID;
86
+ import java.util.concurrent.Executor;
87
+
88
+ import javax.crypto.Cipher;
89
+ import javax.crypto.KeyGenerator;
90
+ import javax.crypto.SecretKey;
91
+ import javax.crypto.spec.GCMParameterSpec;
62
92
 
63
93
  public class Html2ApkBridge extends CordovaPlugin {
64
94
  static final String CHANNEL_ID = "html2apk_default";
@@ -68,11 +98,21 @@ public class Html2ApkBridge extends CordovaPlugin {
68
98
  private static final int REQUEST_POST_NOTIFICATIONS = 7311;
69
99
  private static final int REQUEST_CAMERA = 7312;
70
100
  private static final int REQUEST_RECORD_AUDIO = 7313;
101
+ private static final int REQUEST_LOCATION = 7314;
71
102
  private static final int REQUEST_PICK_FILE = 7411;
72
103
  private static final int REQUEST_SAVE_FILE = 7412;
73
104
  private static final int REQUEST_PICK_FOLDER = 7413;
105
+ private static final int REQUEST_CAPTURE_PHOTO = 7414;
106
+ private static final int REQUEST_CAPTURE_VIDEO = 7415;
74
107
  private static final String PREFS_NAME = "html2apk_bridge";
75
108
  private static final String PREF_PERMISSION_PREFIX = "permission_requested_";
109
+ private static final String STORED_FILES_DIR = "html2apk-files";
110
+ private static final String STORED_FILE_META_SUFFIX = ".html2apk-meta.json";
111
+ private static final String SECURE_PREFS_NAME = "html2apk_secure_storage";
112
+ private static final String SECURE_KEY_ALIAS = "html2apk_secure_storage_key";
113
+ private static final String SECURE_VALUE_PREFIX = "value:";
114
+ private static final String SECURE_IV_PREFIX = "iv:";
115
+ private static final String SECURE_TYPE_PREFIX = "type:";
76
116
  private static Html2ApkBridge activeBridge;
77
117
 
78
118
  private CallbackContext notificationPermissionCallback;
@@ -84,19 +124,33 @@ public class Html2ApkBridge extends CordovaPlugin {
84
124
  private CallbackContext filePickerCallback;
85
125
  private CallbackContext saveFileCallback;
86
126
  private CallbackContext folderPickerCallback;
127
+ private CallbackContext mediaCaptureCallback;
128
+ private CallbackContext pendingLocationCallback;
129
+ private CallbackContext biometricCallback;
130
+ private CallbackContext pendingDownloadCallback;
87
131
  private JSONObject pendingSaveFile;
132
+ private JSONObject pendingMediaCaptureOptions;
133
+ private JSONObject pendingLocationOptions;
88
134
  private JSONObject pendingNotificationOptions;
135
+ private JSONObject pendingDownloadOptions;
89
136
  private JSONObject initialNotification;
90
137
  private JSONObject initialLink;
138
+ private File pendingMediaCaptureFile;
139
+ private Uri pendingMediaCaptureUri;
140
+ private String pendingMediaCaptureKind;
91
141
  private Boolean pendingFlashlightEnabled;
92
142
  private boolean pendingNotificationSchedule;
93
143
  private boolean pendingFlashlightToggle;
144
+ private boolean pendingLocationWatch;
94
145
  private boolean overlaySettingsOpened;
95
146
  private boolean torchEnabled;
147
+ private int locationWatchCounter;
96
148
  private MediaRecorder micRecorder;
97
149
  private File micRecordingFile;
98
150
  private long micRecordingStartedAt;
99
151
  private BroadcastReceiver systemReceiver;
152
+ private CancellationSignal biometricCancellationSignal;
153
+ private final Map<String, LocationListener> locationListeners = new HashMap<String, LocationListener>();
100
154
 
101
155
  @Override
102
156
  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
@@ -131,6 +185,8 @@ public class Html2ApkBridge extends CordovaPlugin {
131
185
  @Override
132
186
  public void onDestroy() {
133
187
  stopMicRecorderSilently();
188
+ stopAllLocationWatches();
189
+ cancelBiometricPrompt();
134
190
  unregisterSystemReceiver();
135
191
  dispatchEvent("app:fechado", baseEvent("app:fechado"));
136
192
  if (activeBridge == this) {
@@ -217,6 +273,16 @@ public class Html2ApkBridge extends CordovaPlugin {
217
273
  return true;
218
274
  }
219
275
 
276
+ if ("capturePhoto".equals(action)) {
277
+ captureMedia("photo", args.optJSONObject(0), callbackContext);
278
+ return true;
279
+ }
280
+
281
+ if ("captureVideo".equals(action)) {
282
+ captureMedia("video", args.optJSONObject(0), callbackContext);
283
+ return true;
284
+ }
285
+
220
286
  if ("requestCameraPermission".equals(action)) {
221
287
  requestCameraPermission(callbackContext);
222
288
  return true;
@@ -304,6 +370,63 @@ public class Html2ApkBridge extends CordovaPlugin {
304
370
  return true;
305
371
  }
306
372
 
373
+ if ("saveStoredFile".equals(action)) {
374
+ callbackContext.success(saveStoredFile(args.optJSONObject(0)));
375
+ return true;
376
+ }
377
+
378
+ if ("readStoredFile".equals(action)) {
379
+ callbackContext.success(readStoredFile(args.optJSONObject(0)));
380
+ return true;
381
+ }
382
+
383
+ if ("deleteStoredFile".equals(action)) {
384
+ callbackContext.success(deleteStoredFile(args.optJSONObject(0)));
385
+ return true;
386
+ }
387
+
388
+ if ("storedFileInfo".equals(action)) {
389
+ callbackContext.success(storedFileInfo(args.optJSONObject(0)));
390
+ return true;
391
+ }
392
+
393
+ if ("listStoredFiles".equals(action)) {
394
+ callbackContext.success(listStoredFiles());
395
+ return true;
396
+ }
397
+
398
+ if ("openStoredFile".equals(action)) {
399
+ openStoredFile(args.optJSONObject(0));
400
+ callbackContext.success();
401
+ return true;
402
+ }
403
+
404
+ if ("shareStoredFile".equals(action)) {
405
+ shareStoredFile(args.optJSONObject(0));
406
+ callbackContext.success();
407
+ return true;
408
+ }
409
+
410
+ if ("downloadFile".equals(action)) {
411
+ downloadFile(args.optJSONObject(0), callbackContext);
412
+ return true;
413
+ }
414
+
415
+ if ("setWallpaper".equals(action)) {
416
+ callbackContext.success(setWallpaper(args.optJSONObject(0)));
417
+ return true;
418
+ }
419
+
420
+ if ("wallpaperInfo".equals(action)) {
421
+ callbackContext.success(wallpaperInfo());
422
+ return true;
423
+ }
424
+
425
+ if ("openWallpaperSettings".equals(action)) {
426
+ callbackContext.success(openWallpaperSettings());
427
+ return true;
428
+ }
429
+
307
430
  if ("deviceInfo".equals(action)) {
308
431
  callbackContext.success(deviceInfo());
309
432
  return true;
@@ -339,6 +462,21 @@ public class Html2ApkBridge extends CordovaPlugin {
339
462
  return true;
340
463
  }
341
464
 
465
+ if ("getLocation".equals(action)) {
466
+ getLocation(args.optJSONObject(0), callbackContext);
467
+ return true;
468
+ }
469
+
470
+ if ("watchLocation".equals(action)) {
471
+ watchLocation(args.optJSONObject(0), callbackContext);
472
+ return true;
473
+ }
474
+
475
+ if ("stopLocationWatch".equals(action)) {
476
+ callbackContext.success(stopLocationWatch(args.optString(0, "")));
477
+ return true;
478
+ }
479
+
342
480
  if ("permissionStatus".equals(action)) {
343
481
  callbackContext.success(permissionStatus(args.optJSONArray(0)));
344
482
  return true;
@@ -417,6 +555,36 @@ public class Html2ApkBridge extends CordovaPlugin {
417
555
  initialLink = null;
418
556
  return true;
419
557
  }
558
+
559
+ if ("authenticateBiometric".equals(action)) {
560
+ authenticateBiometric(args.optJSONObject(0), callbackContext);
561
+ return true;
562
+ }
563
+
564
+ if ("saveSecureItem".equals(action)) {
565
+ callbackContext.success(saveSecureItem(args.optJSONObject(0)));
566
+ return true;
567
+ }
568
+
569
+ if ("readSecureItem".equals(action)) {
570
+ callbackContext.success(readSecureItem(args.optJSONObject(0)));
571
+ return true;
572
+ }
573
+
574
+ if ("deleteSecureItem".equals(action)) {
575
+ callbackContext.success(deleteSecureItem(args.optJSONObject(0)));
576
+ return true;
577
+ }
578
+
579
+ if ("listSecureKeys".equals(action)) {
580
+ callbackContext.success(listSecureKeys());
581
+ return true;
582
+ }
583
+
584
+ if ("clearSecureStorage".equals(action)) {
585
+ callbackContext.success(clearSecureStorage());
586
+ return true;
587
+ }
420
588
  } catch (Exception error) {
421
589
  callbackContext.error(error.getMessage());
422
590
  return true;
@@ -430,11 +598,11 @@ public class Html2ApkBridge extends CordovaPlugin {
430
598
  }
431
599
 
432
600
  private boolean hasPendingNotificationPermissionRequest() {
433
- return pendingNotificationCallback != null || notificationPermissionCallback != null;
601
+ return pendingNotificationCallback != null || notificationPermissionCallback != null || pendingDownloadCallback != null;
434
602
  }
435
603
 
436
604
  private boolean hasPendingCameraPermissionRequest() {
437
- return pendingFlashlightCallback != null || cameraPermissionCallback != null;
605
+ return pendingFlashlightCallback != null || cameraPermissionCallback != null || mediaCaptureCallback != null;
438
606
  }
439
607
 
440
608
  private boolean hasPendingMicrophonePermissionRequest() {
@@ -475,6 +643,32 @@ public class Html2ApkBridge extends CordovaPlugin {
475
643
  return;
476
644
  }
477
645
 
646
+ if (mediaCaptureCallback != null && pendingMediaCaptureKind != null) {
647
+ CallbackContext callback = mediaCaptureCallback;
648
+ String kind = pendingMediaCaptureKind;
649
+ JSONObject options = pendingMediaCaptureOptions == null ? new JSONObject() : pendingMediaCaptureOptions;
650
+ mediaCaptureCallback = null;
651
+ pendingMediaCaptureKind = null;
652
+ pendingMediaCaptureOptions = null;
653
+
654
+ try {
655
+ if (!granted) {
656
+ JSONObject result = shouldOpenSettingsForRuntimePermission(Manifest.permission.CAMERA)
657
+ ? openSettingsForRuntimePermission(Manifest.permission.CAMERA, true, true)
658
+ : runtimePermissionResult(Manifest.permission.CAMERA, true, true, false);
659
+ result.put("requested", true);
660
+ result.put("granted", false);
661
+ callback.success(result);
662
+ return;
663
+ }
664
+
665
+ startMediaCapture(kind, options, callback);
666
+ } catch (Exception error) {
667
+ callback.error(error.getMessage());
668
+ }
669
+ return;
670
+ }
671
+
478
672
  if (cameraPermissionCallback == null) {
479
673
  return;
480
674
  }
@@ -528,6 +722,42 @@ public class Html2ApkBridge extends CordovaPlugin {
528
722
  return;
529
723
  }
530
724
 
725
+ if (requestCode == REQUEST_LOCATION) {
726
+ boolean granted = hasLocationPermission();
727
+ CallbackContext callback = pendingLocationCallback;
728
+ JSONObject options = pendingLocationOptions == null ? new JSONObject() : pendingLocationOptions;
729
+ boolean shouldWatch = pendingLocationWatch;
730
+ pendingLocationCallback = null;
731
+ pendingLocationOptions = null;
732
+ pendingLocationWatch = false;
733
+
734
+ if (callback == null) {
735
+ return;
736
+ }
737
+
738
+ try {
739
+ if (granted) {
740
+ if (shouldWatch) {
741
+ startLocationWatch(options, callback);
742
+ } else {
743
+ resolveCurrentLocation(options, callback);
744
+ }
745
+ return;
746
+ }
747
+
748
+ String permission = locationPermissionName(options);
749
+ JSONObject result = shouldOpenSettingsForRuntimePermission(permission)
750
+ ? openSettingsForRuntimePermission(permission, true, true)
751
+ : runtimePermissionResult(permission, true, true, false);
752
+ result.put("requested", true);
753
+ result.put("granted", false);
754
+ callback.success(result);
755
+ } catch (Exception error) {
756
+ callback.error(error.getMessage());
757
+ }
758
+ return;
759
+ }
760
+
531
761
  if (requestCode != REQUEST_POST_NOTIFICATIONS) {
532
762
  return;
533
763
  }
@@ -567,6 +797,24 @@ public class Html2ApkBridge extends CordovaPlugin {
567
797
  return;
568
798
  }
569
799
 
800
+ if (pendingDownloadCallback != null) {
801
+ CallbackContext callback = pendingDownloadCallback;
802
+ JSONObject options = pendingDownloadOptions == null ? new JSONObject() : pendingDownloadOptions;
803
+ pendingDownloadCallback = null;
804
+ pendingDownloadOptions = null;
805
+
806
+ try {
807
+ options.put("notificationPermissionRequested", true);
808
+ options.put("permissaoNotificacaoSolicitada", true);
809
+ options.put("notificationPermissionGranted", granted);
810
+ options.put("permissaoNotificacaoConcedida", granted);
811
+ startDownloadFile(options, callback);
812
+ } catch (Exception error) {
813
+ callback.error(error.getMessage());
814
+ }
815
+ return;
816
+ }
817
+
570
818
  if (notificationPermissionCallback == null) {
571
819
  return;
572
820
  }
@@ -603,6 +851,11 @@ public class Html2ApkBridge extends CordovaPlugin {
603
851
 
604
852
  if (requestCode == REQUEST_PICK_FOLDER) {
605
853
  handlePickFolderResult(resultCode, intent);
854
+ return;
855
+ }
856
+
857
+ if (requestCode == REQUEST_CAPTURE_PHOTO || requestCode == REQUEST_CAPTURE_VIDEO) {
858
+ handleMediaCaptureResult(resultCode, intent);
606
859
  }
607
860
  }
608
861
 
@@ -1362,6 +1615,127 @@ public class Html2ApkBridge extends CordovaPlugin {
1362
1615
  return result;
1363
1616
  }
1364
1617
 
1618
+ private void captureMedia(String kind, JSONObject options, CallbackContext callbackContext) throws Exception {
1619
+ if (mediaCaptureCallback != null) {
1620
+ rejectBusyCallback(callbackContext, "Camera capture");
1621
+ return;
1622
+ }
1623
+
1624
+ if (!hasCameraPermission()) {
1625
+ mediaCaptureCallback = callbackContext;
1626
+ pendingMediaCaptureKind = kind;
1627
+ pendingMediaCaptureOptions = options == null ? new JSONObject() : options;
1628
+ rememberRuntimePermissionRequest(Manifest.permission.CAMERA);
1629
+ cordova.requestPermission(this, REQUEST_CAMERA, Manifest.permission.CAMERA);
1630
+ return;
1631
+ }
1632
+
1633
+ startMediaCapture(kind, options == null ? new JSONObject() : options, callbackContext);
1634
+ }
1635
+
1636
+ private void startMediaCapture(String kind, JSONObject options, CallbackContext callbackContext) throws Exception {
1637
+ boolean video = "video".equals(kind);
1638
+ Intent intent = new Intent(video ? MediaStore.ACTION_VIDEO_CAPTURE : MediaStore.ACTION_IMAGE_CAPTURE);
1639
+ if (intent.resolveActivity(context().getPackageManager()) == null) {
1640
+ throw new Exception(video ? "No video camera app is available." : "No camera app is available.");
1641
+ }
1642
+
1643
+ File outputFile = createMediaCaptureFile(video ? "video" : "photo");
1644
+ Uri outputUri = fileProviderUri(outputFile);
1645
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
1646
+ intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
1647
+
1648
+ if (video) {
1649
+ if (options.has("durationSeconds") || options.has("duracaoSegundos")) {
1650
+ intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, Math.max(1, options.optInt("durationSeconds", options.optInt("duracaoSegundos", 0))));
1651
+ }
1652
+ if (options.has("quality") || options.has("qualidade")) {
1653
+ intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, Math.max(0, Math.min(1, options.optInt("quality", options.optInt("qualidade", 1)))));
1654
+ }
1655
+ }
1656
+
1657
+ pendingMediaCaptureFile = outputFile;
1658
+ pendingMediaCaptureUri = outputUri;
1659
+ pendingMediaCaptureKind = video ? "video" : "photo";
1660
+ pendingMediaCaptureOptions = options;
1661
+ mediaCaptureCallback = callbackContext;
1662
+ cordova.startActivityForResult(this, intent, video ? REQUEST_CAPTURE_VIDEO : REQUEST_CAPTURE_PHOTO);
1663
+ }
1664
+
1665
+ private File createMediaCaptureFile(String kind) throws Exception {
1666
+ File baseDir = "video".equals(kind)
1667
+ ? context().getExternalFilesDir(Environment.DIRECTORY_MOVIES)
1668
+ : context().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
1669
+ if (baseDir == null) {
1670
+ baseDir = new File(context().getCacheDir(), "html2apk-media");
1671
+ }
1672
+ if (!baseDir.exists() && !baseDir.mkdirs()) {
1673
+ throw new Exception("Could not create media directory.");
1674
+ }
1675
+
1676
+ String extension = "video".equals(kind) ? ".mp4" : ".jpg";
1677
+ return File.createTempFile("html2apk-" + kind + "-", extension, baseDir);
1678
+ }
1679
+
1680
+ private Uri fileProviderUri(File file) {
1681
+ return FileProvider.getUriForFile(context(), context().getPackageName() + ".html2apk.fileprovider", file);
1682
+ }
1683
+
1684
+ private void handleMediaCaptureResult(int resultCode, Intent intent) {
1685
+ CallbackContext callback = mediaCaptureCallback;
1686
+ JSONObject options = pendingMediaCaptureOptions == null ? new JSONObject() : pendingMediaCaptureOptions;
1687
+ File outputFile = pendingMediaCaptureFile;
1688
+ String kind = pendingMediaCaptureKind;
1689
+ mediaCaptureCallback = null;
1690
+ pendingMediaCaptureOptions = null;
1691
+ pendingMediaCaptureFile = null;
1692
+ pendingMediaCaptureUri = null;
1693
+ pendingMediaCaptureKind = null;
1694
+
1695
+ if (callback == null) {
1696
+ return;
1697
+ }
1698
+ if (resultCode != Activity.RESULT_OK) {
1699
+ if (outputFile != null && outputFile.exists()) {
1700
+ outputFile.delete();
1701
+ }
1702
+ callback.success(new JSONObject());
1703
+ return;
1704
+ }
1705
+
1706
+ try {
1707
+ JSONObject result;
1708
+ if (outputFile != null && outputFile.exists() && outputFile.length() > 0) {
1709
+ result = storedFileResult(outputFile, mediaMimeType(kind), kind);
1710
+ if (options.optBoolean("base64", false)) {
1711
+ result.put("base64", Base64.encodeToString(readFileBytes(outputFile), Base64.NO_WRAP));
1712
+ }
1713
+ } else if (intent != null && intent.getData() != null) {
1714
+ result = fileInfo(intent.getData());
1715
+ if (options.optBoolean("base64", false)) {
1716
+ result.put("base64", Base64.encodeToString(readUriBytes(intent.getData()), Base64.NO_WRAP));
1717
+ }
1718
+ } else {
1719
+ if (outputFile != null && outputFile.exists()) {
1720
+ outputFile.delete();
1721
+ }
1722
+ result = new JSONObject();
1723
+ }
1724
+
1725
+ result.put("kind", kind);
1726
+ result.put("tipo", kind);
1727
+ result.put("captured", true);
1728
+ result.put("capturado", true);
1729
+ callback.success(result);
1730
+ } catch (Exception error) {
1731
+ callback.error(error.getMessage());
1732
+ }
1733
+ }
1734
+
1735
+ private String mediaMimeType(String kind) {
1736
+ return "video".equals(kind) ? "video/mp4" : "image/jpeg";
1737
+ }
1738
+
1365
1739
  private String readText() {
1366
1740
  ClipboardManager clipboard = (ClipboardManager) context().getSystemService(Context.CLIPBOARD_SERVICE);
1367
1741
  if (clipboard == null || !clipboard.hasPrimaryClip() || clipboard.getPrimaryClip() == null) {
@@ -1638,109 +2012,1286 @@ public class Html2ApkBridge extends CordovaPlugin {
1638
2012
  }
1639
2013
  }
1640
2014
 
1641
- private JSONObject deviceInfo() throws Exception {
1642
- JSONObject result = new JSONObject();
1643
- result.put("manufacturer", Build.MANUFACTURER);
1644
- result.put("fabricante", Build.MANUFACTURER);
1645
- result.put("model", Build.MODEL);
1646
- result.put("modelo", Build.MODEL);
1647
- result.put("brand", Build.BRAND);
1648
- result.put("androidVersion", Build.VERSION.RELEASE);
1649
- result.put("sdkInt", Build.VERSION.SDK_INT);
1650
- result.put("packageName", context().getPackageName());
1651
- return result;
1652
- }
2015
+ private JSONObject saveStoredFile(JSONObject options) throws Exception {
2016
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2017
+ File file = storedFileFromOptions(safeOptions);
2018
+ boolean existed = file.exists();
2019
+ StoredContent content = storedContentFromOptions(safeOptions);
1653
2020
 
1654
- private JSONObject networkInfo() throws Exception {
1655
- JSONObject result = new JSONObject();
1656
- ConnectivityManager manager = (ConnectivityManager) context().getSystemService(Context.CONNECTIVITY_SERVICE);
1657
- boolean connected = false;
1658
- String type = "unknown";
2021
+ File parent = file.getParentFile();
2022
+ if (parent != null && !parent.exists() && !parent.mkdirs()) {
2023
+ throw new Exception("Could not create storage directory.");
2024
+ }
1659
2025
 
1660
- if (manager != null) {
1661
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1662
- Network network = manager.getActiveNetwork();
1663
- NetworkCapabilities capabilities = network == null ? null : manager.getNetworkCapabilities(network);
1664
- connected = capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1665
- if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
1666
- type = "wifi";
1667
- } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
1668
- type = "cellular";
1669
- } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
1670
- type = "ethernet";
1671
- }
1672
- } else if (manager.getActiveNetworkInfo() != null) {
1673
- connected = manager.getActiveNetworkInfo().isConnected();
1674
- type = manager.getActiveNetworkInfo().getTypeName().toLowerCase();
1675
- }
2026
+ FileOutputStream outputStream = new FileOutputStream(file);
2027
+ try {
2028
+ outputStream.write(content.bytes);
2029
+ } finally {
2030
+ outputStream.close();
1676
2031
  }
1677
2032
 
1678
- result.put("online", connected);
1679
- result.put("connected", connected);
1680
- result.put("tipo", type);
1681
- result.put("type", type);
2033
+ JSONObject meta = new JSONObject();
2034
+ meta.put("name", file.getName());
2035
+ meta.put("nome", file.getName());
2036
+ meta.put("mimeType", safeOptions.optString("mimeType", content.mimeType));
2037
+ meta.put("type", content.type);
2038
+ meta.put("tipo", content.type);
2039
+ meta.put("updatedAt", System.currentTimeMillis());
2040
+ meta.put("atualizadoEm", meta.optLong("updatedAt"));
2041
+ writeStoredFileMeta(file, meta);
2042
+
2043
+ JSONObject result = storedFileResult(file, meta.optString("mimeType", content.mimeType), content.type);
2044
+ result.put("saved", true);
2045
+ result.put("salvo", true);
2046
+ result.put("created", !existed);
2047
+ result.put("criado", !existed);
2048
+ result.put("updated", existed);
2049
+ result.put("atualizado", existed);
1682
2050
  return result;
1683
2051
  }
1684
2052
 
1685
- private JSONObject batteryInfo() throws Exception {
1686
- Intent intent = context().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
1687
- JSONObject result = new JSONObject();
1688
- if (intent == null) {
1689
- result.put("level", -1);
1690
- result.put("nivel", -1);
1691
- result.put("charging", false);
2053
+ private JSONObject readStoredFile(JSONObject options) throws Exception {
2054
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2055
+ File file = storedFileFromOptions(safeOptions);
2056
+ if (!file.exists()) {
2057
+ JSONObject result = storedFileResult(file, safeOptions.optString("mimeType", "text/plain"), "missing");
2058
+ result.put("exists", false);
2059
+ result.put("existe", false);
1692
2060
  return result;
1693
2061
  }
1694
2062
 
1695
- int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
1696
- int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
1697
- int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
1698
- boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING
1699
- || status == BatteryManager.BATTERY_STATUS_FULL;
1700
- double percent = scale <= 0 ? -1 : (level * 100.0 / scale);
2063
+ JSONObject meta = readStoredFileMeta(file);
2064
+ String type = meta.optString("type", "string");
2065
+ String mimeType = meta.optString("mimeType", safeOptions.optString("mimeType", "text/plain"));
2066
+ byte[] bytes = readFileBytes(file);
2067
+ JSONObject result = storedFileResult(file, mimeType, type);
2068
+ result.put("exists", true);
2069
+ result.put("existe", true);
1701
2070
 
1702
- result.put("level", percent);
1703
- result.put("nivel", percent);
1704
- result.put("charging", charging);
1705
- result.put("carregando", charging);
2071
+ if ("base64".equals(type) || safeOptions.optBoolean("base64", false)) {
2072
+ result.put("base64", Base64.encodeToString(bytes, Base64.NO_WRAP));
2073
+ return result;
2074
+ }
2075
+
2076
+ String content = new String(bytes, "UTF-8");
2077
+ result.put("content", content);
2078
+ result.put("conteudo", content);
2079
+ result.put("value", storedValueFromContent(content, type));
2080
+ result.put("valor", result.opt("value"));
1706
2081
  return result;
1707
2082
  }
1708
2083
 
1709
- private JSONObject memoryInfo() throws Exception {
1710
- ActivityManager manager = (ActivityManager) context().getSystemService(Context.ACTIVITY_SERVICE);
1711
- ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
1712
- if (manager != null) {
1713
- manager.getMemoryInfo(info);
2084
+ private JSONObject deleteStoredFile(JSONObject options) throws Exception {
2085
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2086
+ File file = storedFileFromOptions(safeOptions);
2087
+ boolean existed = file.exists();
2088
+ boolean deleted = !file.exists() || file.delete();
2089
+ File meta = storedFileMeta(file);
2090
+ if (meta.exists()) {
2091
+ meta.delete();
1714
2092
  }
1715
2093
 
1716
- Runtime runtime = Runtime.getRuntime();
1717
2094
  JSONObject result = new JSONObject();
1718
- result.put("availableBytes", info.availMem);
1719
- result.put("totalBytes", Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? info.totalMem : -1);
1720
- result.put("lowMemory", info.lowMemory);
1721
- result.put("thresholdBytes", info.threshold);
1722
- result.put("appUsedBytes", runtime.totalMemory() - runtime.freeMemory());
1723
- result.put("appMaxBytes", runtime.maxMemory());
2095
+ result.put("name", file.getName());
2096
+ result.put("nome", file.getName());
2097
+ result.put("existsBefore", existed);
2098
+ result.put("existiaAntes", existed);
2099
+ result.put("deleted", deleted);
2100
+ result.put("excluido", deleted);
1724
2101
  return result;
1725
2102
  }
1726
2103
 
1727
- private JSONObject storageInfo() throws Exception {
1728
- JSONObject result = new JSONObject();
1729
- result.put("internal", statFsInfo(Environment.getDataDirectory()));
1730
- result.put("cache", statFsInfo(context().getCacheDir()));
1731
- if (context().getExternalFilesDir(null) != null) {
1732
- result.put("appExternal", statFsInfo(context().getExternalFilesDir(null)));
1733
- }
2104
+ private JSONObject storedFileInfo(JSONObject options) throws Exception {
2105
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2106
+ File file = storedFileFromOptions(safeOptions);
2107
+ JSONObject meta = readStoredFileMeta(file);
2108
+ JSONObject result = storedFileResult(file, meta.optString("mimeType", safeOptions.optString("mimeType", guessMimeType(file.getName()))), meta.optString("type", "string"));
2109
+ result.put("exists", file.exists());
2110
+ result.put("existe", file.exists());
1734
2111
  return result;
1735
2112
  }
1736
2113
 
1737
- private JSONObject statFsInfo(java.io.File file) throws Exception {
1738
- StatFs statFs = new StatFs(file.getAbsolutePath());
1739
- long blockSize = statFs.getBlockSizeLong();
1740
- long total = statFs.getBlockCountLong() * blockSize;
1741
- long available = statFs.getAvailableBlocksLong() * blockSize;
1742
- JSONObject result = new JSONObject();
1743
- result.put("path", file.getAbsolutePath());
2114
+ private JSONArray listStoredFiles() throws Exception {
2115
+ File dir = storedFilesDir();
2116
+ JSONArray files = new JSONArray();
2117
+ File[] items = dir.listFiles();
2118
+ if (items == null) {
2119
+ return files;
2120
+ }
2121
+
2122
+ for (File item : items) {
2123
+ if (!item.isFile() || item.getName().endsWith(STORED_FILE_META_SUFFIX)) {
2124
+ continue;
2125
+ }
2126
+ JSONObject meta = readStoredFileMeta(item);
2127
+ JSONObject info = storedFileResult(item, meta.optString("mimeType", guessMimeType(item.getName())), meta.optString("type", "string"));
2128
+ info.put("exists", true);
2129
+ info.put("existe", true);
2130
+ files.put(info);
2131
+ }
2132
+ return files;
2133
+ }
2134
+
2135
+ private void openStoredFile(JSONObject options) throws Exception {
2136
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2137
+ File file = storedFileFromOptions(safeOptions);
2138
+ if (!file.exists()) {
2139
+ throw new Exception("Stored file does not exist.");
2140
+ }
2141
+
2142
+ String mimeType = readStoredFileMeta(file).optString("mimeType", safeOptions.optString("mimeType", guessMimeType(file.getName())));
2143
+ Intent intent = new Intent(Intent.ACTION_VIEW);
2144
+ intent.setDataAndType(fileProviderUri(file), mimeType);
2145
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
2146
+ cordova.getActivity().startActivity(intent);
2147
+ }
2148
+
2149
+ private void shareStoredFile(JSONObject options) throws Exception {
2150
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2151
+ File file = storedFileFromOptions(safeOptions);
2152
+ if (!file.exists()) {
2153
+ throw new Exception("Stored file does not exist.");
2154
+ }
2155
+
2156
+ String mimeType = readStoredFileMeta(file).optString("mimeType", safeOptions.optString("mimeType", guessMimeType(file.getName())));
2157
+ String title = safeOptions.optString("titulo", safeOptions.optString("title", "Compartilhar"));
2158
+ Intent intent = new Intent(Intent.ACTION_SEND);
2159
+ intent.setType(mimeType);
2160
+ intent.putExtra(Intent.EXTRA_STREAM, fileProviderUri(file));
2161
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
2162
+ cordova.getActivity().startActivity(Intent.createChooser(intent, title));
2163
+ }
2164
+
2165
+ private void downloadFile(JSONObject options, CallbackContext callbackContext) {
2166
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2167
+ try {
2168
+ if (shouldRequestDownloadNotificationPermission(safeOptions)) {
2169
+ if (hasPendingNotificationPermissionRequest()) {
2170
+ rejectBusyCallback(callbackContext, "POST_NOTIFICATIONS permission");
2171
+ return;
2172
+ }
2173
+ pendingDownloadCallback = callbackContext;
2174
+ pendingDownloadOptions = safeOptions;
2175
+ rememberRuntimePermissionRequest(Manifest.permission.POST_NOTIFICATIONS);
2176
+ cordova.requestPermission(this, REQUEST_POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS);
2177
+ return;
2178
+ }
2179
+ startDownloadFile(safeOptions, callbackContext);
2180
+ } catch (Exception error) {
2181
+ callbackContext.error(error.getMessage());
2182
+ }
2183
+ }
2184
+
2185
+ private boolean shouldRequestDownloadNotificationPermission(JSONObject options) {
2186
+ if (!wantsDownloadNotification(options) || hasNotificationPermission()) {
2187
+ return false;
2188
+ }
2189
+ return options.optBoolean("pedirPermissaoNotificacao",
2190
+ options.optBoolean("requestNotificationPermission",
2191
+ options.optBoolean("solicitarPermissaoNotificacao", true)));
2192
+ }
2193
+
2194
+ private void startDownloadFile(final JSONObject options, final CallbackContext callbackContext) {
2195
+ final JSONObject safeOptions = options == null ? new JSONObject() : options;
2196
+ cordova.getThreadPool().execute(new Runnable() {
2197
+ @Override
2198
+ public void run() {
2199
+ DownloadSource source = null;
2200
+ InputStream inputStream = null;
2201
+ FileOutputStream outputStream = null;
2202
+ boolean notificationShown = false;
2203
+ int downloadNotificationId = downloadNotificationId(safeOptions);
2204
+ try {
2205
+ source = openDownloadSource(safeOptions);
2206
+ String name = downloadDestinationName(safeOptions, source);
2207
+ File file = storedFile(name);
2208
+ File parent = file.getParentFile();
2209
+ if (parent != null && !parent.exists() && !parent.mkdirs()) {
2210
+ throw new Exception("Could not create storage directory.");
2211
+ }
2212
+
2213
+ if (source.file != null && sameFile(source.file, file)) {
2214
+ JSONObject result = storedFileInfo(new JSONObject().put("name", file.getName()));
2215
+ result.put("downloaded", true);
2216
+ result.put("baixado", true);
2217
+ result.put("sourceType", source.type);
2218
+ result.put("tipoOrigem", source.type);
2219
+ result.put("progressNotification", wantsDownloadNotification(safeOptions));
2220
+ result.put("notificacaoProgresso", wantsDownloadNotification(safeOptions));
2221
+ result.put("notificationShown", false);
2222
+ result.put("notificacaoMostrada", false);
2223
+ result.put("notificationPermissionGranted", hasNotificationPermission());
2224
+ result.put("permissaoNotificacaoConcedida", hasNotificationPermission());
2225
+ result.put("notificationPermissionRequested", safeOptions.optBoolean("notificationPermissionRequested", false));
2226
+ result.put("permissaoNotificacaoSolicitada", safeOptions.optBoolean("permissaoNotificacaoSolicitada", false));
2227
+ callbackContext.success(result);
2228
+ return;
2229
+ }
2230
+
2231
+ boolean progressNotification = wantsDownloadNotification(safeOptions);
2232
+ boolean notificationPermissionGranted = hasNotificationPermission();
2233
+ String notificationTitle = downloadNotificationTitle(safeOptions);
2234
+ String progressText = downloadProgressText(safeOptions, name);
2235
+ if (progressNotification && notificationPermissionGranted) {
2236
+ notificationShown = notifyDownloadProgress(downloadNotificationId, notificationTitle, progressText, source.totalBytes, 0, false, null);
2237
+ }
2238
+
2239
+ inputStream = source.inputStream;
2240
+ outputStream = new FileOutputStream(file);
2241
+ long size = 0;
2242
+ int lastPercent = -1;
2243
+ long lastNotifyAt = 0;
2244
+ byte[] buffer = new byte[8192];
2245
+ int read;
2246
+ while ((read = inputStream.read(buffer)) != -1) {
2247
+ outputStream.write(buffer, 0, read);
2248
+ size += read;
2249
+ if (notificationShown) {
2250
+ int percent = source.totalBytes > 0 ? (int) Math.min(100, (size * 100) / source.totalBytes) : -1;
2251
+ long now = System.currentTimeMillis();
2252
+ if (percent != lastPercent && (percent < 0 || percent - lastPercent >= 2 || now - lastNotifyAt > 700)) {
2253
+ notifyDownloadProgress(downloadNotificationId, notificationTitle, progressText, source.totalBytes, size, false, null);
2254
+ lastPercent = percent;
2255
+ lastNotifyAt = now;
2256
+ }
2257
+ }
2258
+ }
2259
+ outputStream.getFD().sync();
2260
+
2261
+ String mimeType = source.mimeType;
2262
+ if (mimeType == null || mimeType.length() == 0) {
2263
+ mimeType = guessMimeType(file.getName());
2264
+ }
2265
+ JSONObject meta = new JSONObject();
2266
+ meta.put("name", file.getName());
2267
+ meta.put("nome", file.getName());
2268
+ meta.put("mimeType", mimeType);
2269
+ meta.put("type", "base64");
2270
+ meta.put("tipo", "base64");
2271
+ meta.put("sourceType", source.type);
2272
+ meta.put("tipoOrigem", source.type);
2273
+ if (source.url.length() > 0) {
2274
+ meta.put("sourceUrl", source.url);
2275
+ meta.put("urlOrigem", source.url);
2276
+ }
2277
+ meta.put("updatedAt", System.currentTimeMillis());
2278
+ writeStoredFileMeta(file, meta);
2279
+
2280
+ JSONObject result = storedFileResult(file, mimeType, "base64");
2281
+ result.put("downloaded", true);
2282
+ result.put("baixado", true);
2283
+ result.put("sourceType", source.type);
2284
+ result.put("tipoOrigem", source.type);
2285
+ if (source.url.length() > 0) {
2286
+ result.put("sourceUrl", source.url);
2287
+ result.put("urlOrigem", source.url);
2288
+ }
2289
+ result.put("size", size);
2290
+ result.put("tamanho", size);
2291
+ result.put("totalBytes", source.totalBytes);
2292
+ result.put("totalBytesOrigem", source.totalBytes);
2293
+ result.put("progressNotification", progressNotification);
2294
+ result.put("notificacaoProgresso", progressNotification);
2295
+ result.put("notificationShown", notificationShown);
2296
+ result.put("notificacaoMostrada", notificationShown);
2297
+ result.put("notificationId", downloadNotificationId);
2298
+ result.put("idNotificacao", downloadNotificationId);
2299
+ result.put("notificationPermissionGranted", notificationPermissionGranted);
2300
+ result.put("permissaoNotificacaoConcedida", notificationPermissionGranted);
2301
+ result.put("notificationPermissionRequested", safeOptions.optBoolean("notificationPermissionRequested", false));
2302
+ result.put("permissaoNotificacaoSolicitada", safeOptions.optBoolean("permissaoNotificacaoSolicitada", false));
2303
+ if (notificationShown) {
2304
+ notifyDownloadProgress(downloadNotificationId, notificationTitle, downloadCompleteText(safeOptions, name), source.totalBytes, size, true, null);
2305
+ }
2306
+ callbackContext.success(result);
2307
+ } catch (Exception error) {
2308
+ if (notificationShown) {
2309
+ notifyDownloadProgress(downloadNotificationId, downloadNotificationTitle(safeOptions), null, 0, 0, true, error);
2310
+ }
2311
+ callbackContext.error(error.getMessage());
2312
+ } finally {
2313
+ closeSilently(outputStream);
2314
+ closeSilently(inputStream);
2315
+ if (source != null && source.inputStream != inputStream) {
2316
+ closeSilently(source.inputStream);
2317
+ }
2318
+ if (source != null && source.connection != null) {
2319
+ source.connection.disconnect();
2320
+ }
2321
+ }
2322
+ }
2323
+ });
2324
+ }
2325
+
2326
+ private DownloadSource openDownloadSource(JSONObject options) throws Exception {
2327
+ String url = options.optString("url", "");
2328
+ if (url.trim().length() > 0) {
2329
+ URL parsedUrl = new URL(url);
2330
+ HttpURLConnection connection = (HttpURLConnection) parsedUrl.openConnection();
2331
+ connection.setConnectTimeout(Math.max(1000, options.optInt("connectTimeoutMs", 15000)));
2332
+ connection.setReadTimeout(Math.max(1000, options.optInt("readTimeoutMs", 30000)));
2333
+ connection.connect();
2334
+ int status = connection.getResponseCode();
2335
+ if (status < 200 || status >= 300) {
2336
+ connection.disconnect();
2337
+ throw new Exception("Download failed with HTTP " + status + ".");
2338
+ }
2339
+ String mimeType = options.optString("mimeType", options.optString("tipoMime", connection.getContentType()));
2340
+ return new DownloadSource(connection.getInputStream(), connection, connection.getContentLengthLong(), mimeType, "url", url, downloadedFileName(parsedUrl), null);
2341
+ }
2342
+
2343
+ String base64 = options.optString("base64", "");
2344
+ if (base64.length() > 0) {
2345
+ byte[] bytes = Base64.decode(stripDataUrlBase64(base64), Base64.DEFAULT);
2346
+ String mimeType = options.optString("mimeType", options.optString("tipoMime", dataUrlMimeType(base64)));
2347
+ if (mimeType.length() == 0) {
2348
+ mimeType = "application/octet-stream";
2349
+ }
2350
+ return new DownloadSource(new ByteArrayInputStream(bytes), null, bytes.length, mimeType, "base64", "", "", null);
2351
+ }
2352
+
2353
+ String uriText = options.optString("contentUri", options.optString("uri", ""));
2354
+ if (uriText.length() > 0) {
2355
+ Uri uri = Uri.parse(uriText);
2356
+ InputStream inputStream = context().getContentResolver().openInputStream(uri);
2357
+ if (inputStream == null) {
2358
+ throw new Exception("Could not open file URI.");
2359
+ }
2360
+ JSONObject info = fileInfo(uri);
2361
+ long size = info.optLong("size", info.optLong("tamanho", -1));
2362
+ String mimeType = options.optString("mimeType", options.optString("tipoMime", info.optString("mimeType", context().getContentResolver().getType(uri))));
2363
+ String name = info.optString("name", info.optString("nome", ""));
2364
+ return new DownloadSource(inputStream, null, size, mimeType, "file", "", name, null);
2365
+ }
2366
+
2367
+ String pathText = options.optString("caminho", options.optString("path", ""));
2368
+ if (pathText.length() > 0) {
2369
+ File sourceFile = new File(pathText);
2370
+ if (!sourceFile.exists() || !sourceFile.isFile()) {
2371
+ throw new Exception("Source file does not exist.");
2372
+ }
2373
+ String mimeType = options.optString("mimeType", options.optString("tipoMime", guessMimeType(sourceFile.getName())));
2374
+ return new DownloadSource(new FileInputStream(sourceFile), null, sourceFile.length(), mimeType, "file", "", sourceFile.getName(), sourceFile);
2375
+ }
2376
+
2377
+ String sourceName = options.optString("arquivoOrigem",
2378
+ options.optString("sourceName",
2379
+ options.optString("sourceFile",
2380
+ options.optString("nomeArquivoOrigem", ""))));
2381
+ if (sourceName.length() > 0) {
2382
+ File sourceFile = storedFile(sourceName);
2383
+ if (!sourceFile.exists() || !sourceFile.isFile()) {
2384
+ throw new Exception("Stored source file does not exist.");
2385
+ }
2386
+ String mimeType = readStoredFileMeta(sourceFile).optString("mimeType", options.optString("mimeType", guessMimeType(sourceFile.getName())));
2387
+ return new DownloadSource(new FileInputStream(sourceFile), null, sourceFile.length(), mimeType, "stored-file", "", sourceFile.getName(), sourceFile);
2388
+ }
2389
+
2390
+ throw new Exception("Download source is required. Use url, base64, uri, path or sourceName.");
2391
+ }
2392
+
2393
+ private String downloadDestinationName(JSONObject options, DownloadSource source) throws Exception {
2394
+ String name = options.optString("nome",
2395
+ options.optString("name",
2396
+ options.optString("fileName",
2397
+ options.optString("nomeArquivo", ""))));
2398
+ if (name.trim().length() == 0 && source != null) {
2399
+ name = source.defaultName;
2400
+ }
2401
+ if (name == null || name.trim().length() == 0) {
2402
+ name = "download-" + System.currentTimeMillis() + ".bin";
2403
+ }
2404
+ return safeStoredFileName(name);
2405
+ }
2406
+
2407
+ private boolean wantsDownloadNotification(JSONObject options) {
2408
+ return options.optBoolean("notificacao",
2409
+ options.optBoolean("notification",
2410
+ options.optBoolean("notificacaoProgresso",
2411
+ options.optBoolean("progressNotification",
2412
+ options.optBoolean("mostrarNotificacao", true)))));
2413
+ }
2414
+
2415
+ private int downloadNotificationId(JSONObject options) {
2416
+ int id = options.optInt("notificationId", options.optInt("idNotificacao", 0));
2417
+ if (id != 0) {
2418
+ return id;
2419
+ }
2420
+ return (int) ((System.currentTimeMillis() + Math.abs(UUID.randomUUID().hashCode())) & 0x0fffffff);
2421
+ }
2422
+
2423
+ private String downloadNotificationTitle(JSONObject options) {
2424
+ return options.optString("tituloNotificacao",
2425
+ options.optString("notificationTitle",
2426
+ options.optString("titulo", options.optString("title", "Baixando arquivo"))));
2427
+ }
2428
+
2429
+ private String downloadProgressText(JSONObject options, String name) {
2430
+ return options.optString("textoNotificacao",
2431
+ options.optString("notificationText", "Baixando " + name));
2432
+ }
2433
+
2434
+ private String downloadCompleteText(JSONObject options, String name) {
2435
+ return options.optString("textoConcluido",
2436
+ options.optString("completeText", "Download concluido: " + name));
2437
+ }
2438
+
2439
+ private boolean notifyDownloadProgress(int id, String title, String text, long totalBytes, long currentBytes, boolean done, Exception error) {
2440
+ if (!hasNotificationPermission()) {
2441
+ return false;
2442
+ }
2443
+
2444
+ try {
2445
+ String errorText = error == null || error.getMessage() == null || error.getMessage().length() == 0
2446
+ ? "Download falhou."
2447
+ : error.getMessage();
2448
+ String bodyText = error == null ? text : errorText;
2449
+ ensureNotificationChannel(context());
2450
+ JSONObject detail = new JSONObject();
2451
+ detail.put("type", "download");
2452
+ detail.put("tipo", "download");
2453
+ detail.put("id", id);
2454
+ detail.put("title", title);
2455
+ detail.put("titulo", title);
2456
+ detail.put("text", bodyText);
2457
+ detail.put("texto", bodyText);
2458
+
2459
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context(), CHANNEL_ID)
2460
+ .setSmallIcon(context().getApplicationInfo().icon)
2461
+ .setContentTitle(error == null ? title : "Download falhou")
2462
+ .setContentText(bodyText == null || bodyText.length() == 0 ? title : bodyText)
2463
+ .setOnlyAlertOnce(true)
2464
+ .setOngoing(!done && error == null)
2465
+ .setAutoCancel(done)
2466
+ .setPriority(NotificationCompat.PRIORITY_LOW)
2467
+ .setContentIntent(createNotificationClickIntent(context(), id, detail));
2468
+
2469
+ if (error != null) {
2470
+ builder.setStyle(new NotificationCompat.BigTextStyle().bigText(errorText));
2471
+ builder.setProgress(0, 0, false);
2472
+ } else if (!done && totalBytes > 0) {
2473
+ int progress = (int) Math.min(100, (currentBytes * 100) / totalBytes);
2474
+ builder.setProgress(100, progress, false);
2475
+ } else if (!done) {
2476
+ builder.setProgress(0, 0, true);
2477
+ } else {
2478
+ builder.setProgress(0, 0, false);
2479
+ }
2480
+
2481
+ NotificationManagerCompat.from(context()).notify(id, builder.build());
2482
+ return true;
2483
+ } catch (Exception ignored) {
2484
+ return false;
2485
+ }
2486
+ }
2487
+
2488
+ private boolean sameFile(File first, File second) {
2489
+ try {
2490
+ return first.getCanonicalPath().equals(second.getCanonicalPath());
2491
+ } catch (Exception ignored) {
2492
+ return false;
2493
+ }
2494
+ }
2495
+
2496
+ private void closeSilently(InputStream inputStream) {
2497
+ if (inputStream == null) {
2498
+ return;
2499
+ }
2500
+ try {
2501
+ inputStream.close();
2502
+ } catch (Exception ignored) {
2503
+ }
2504
+ }
2505
+
2506
+ private void closeSilently(OutputStream outputStream) {
2507
+ if (outputStream == null) {
2508
+ return;
2509
+ }
2510
+ try {
2511
+ outputStream.close();
2512
+ } catch (Exception ignored) {
2513
+ }
2514
+ }
2515
+
2516
+ private JSONObject setWallpaper(JSONObject options) throws Exception {
2517
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
2518
+ String target = wallpaperTarget(safeOptions);
2519
+ String mimeType = wallpaperMimeType(safeOptions);
2520
+
2521
+ if ("call".equals(target)) {
2522
+ return unsupportedWallpaperResult(safeOptions, target, mimeType, "Call screen backgrounds are controlled by the phone/dialer app and do not have a stable public Android API.");
2523
+ }
2524
+
2525
+ if (mimeType.toLowerCase().startsWith("video/")) {
2526
+ return unsupportedWallpaperResult(safeOptions, target, mimeType, "Video wallpapers require an Android live wallpaper flow. Open the wallpaper settings and let the user choose a live wallpaper app.");
2527
+ }
2528
+
2529
+ Bitmap bitmap = decodeWallpaperBitmap(safeOptions);
2530
+ if (bitmap == null) {
2531
+ throw new Exception("Could not decode wallpaper image.");
2532
+ }
2533
+
2534
+ WallpaperManager manager = WallpaperManager.getInstance(context());
2535
+ boolean wantsSystem = "system".equals(target) || "both".equals(target);
2536
+ boolean wantsLock = "lock".equals(target) || "both".equals(target);
2537
+ boolean systemApplied = false;
2538
+ boolean lockApplied = false;
2539
+ String systemError = "";
2540
+ String lockError = "";
2541
+
2542
+ try {
2543
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
2544
+ if (wantsSystem) {
2545
+ try {
2546
+ manager.setBitmap(bitmap, null, true, WallpaperManager.FLAG_SYSTEM);
2547
+ systemApplied = true;
2548
+ } catch (Exception error) {
2549
+ systemError = error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage();
2550
+ }
2551
+ }
2552
+ if (wantsLock) {
2553
+ try {
2554
+ manager.setBitmap(bitmap, null, true, WallpaperManager.FLAG_LOCK);
2555
+ lockApplied = true;
2556
+ } catch (Exception error) {
2557
+ lockError = error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage();
2558
+ }
2559
+ }
2560
+ } else if (wantsLock && !wantsSystem) {
2561
+ lockError = "Lock screen wallpaper is supported only on Android 7.0+.";
2562
+ } else {
2563
+ try {
2564
+ manager.setBitmap(bitmap);
2565
+ systemApplied = true;
2566
+ if (wantsLock) {
2567
+ lockError = "Lock screen wallpaper is supported only on Android 7.0+.";
2568
+ }
2569
+ } catch (Exception error) {
2570
+ systemError = error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage();
2571
+ }
2572
+ }
2573
+ } finally {
2574
+ bitmap.recycle();
2575
+ }
2576
+
2577
+ JSONObject result = wallpaperInfo();
2578
+ boolean applied = systemApplied || lockApplied;
2579
+ result.put("target", target);
2580
+ result.put("alvo", target);
2581
+ result.put("mimeType", mimeType);
2582
+ result.put("applied", applied);
2583
+ result.put("aplicado", applied);
2584
+ result.put("requiresUserAction", !applied);
2585
+ result.put("precisaAcaoUsuario", !applied);
2586
+ result.put("systemApplied", systemApplied);
2587
+ result.put("inicioAplicado", systemApplied);
2588
+ result.put("homeApplied", systemApplied);
2589
+ result.put("lockApplied", lockApplied);
2590
+ result.put("bloqueioAplicado", lockApplied);
2591
+ result.put("settingsOpened", false);
2592
+ result.put("configuracaoAberta", false);
2593
+ if (systemError.length() > 0) {
2594
+ result.put("systemError", systemError);
2595
+ result.put("erroInicio", systemError);
2596
+ }
2597
+ if (lockError.length() > 0) {
2598
+ result.put("lockError", lockError);
2599
+ result.put("erroBloqueio", lockError);
2600
+ }
2601
+ return result;
2602
+ }
2603
+
2604
+ private JSONObject wallpaperInfo() throws Exception {
2605
+ boolean lockSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
2606
+ JSONObject result = new JSONObject();
2607
+ result.put("permission", "android.permission.SET_WALLPAPER");
2608
+ result.put("supported", true);
2609
+ result.put("suportado", true);
2610
+ result.put("imageSupported", true);
2611
+ result.put("imagemSuportada", true);
2612
+ result.put("lockSupported", lockSupported);
2613
+ result.put("bloqueioSuportado", lockSupported);
2614
+ result.put("videoSupported", false);
2615
+ result.put("videoSuportado", false);
2616
+ result.put("callBackgroundSupported", false);
2617
+ result.put("fundoChamadasSuportado", false);
2618
+ result.put("requiresUserAction", false);
2619
+ result.put("precisaAcaoUsuario", false);
2620
+ result.put("requiresUserActionForVideo", true);
2621
+ result.put("videoPrecisaAcaoUsuario", true);
2622
+ result.put("settingsOpened", false);
2623
+ result.put("configuracaoAberta", false);
2624
+ return result;
2625
+ }
2626
+
2627
+ private JSONObject openWallpaperSettings() throws Exception {
2628
+ JSONObject result = wallpaperInfo();
2629
+ Intent settingsIntent = new Intent("android.settings.WALLPAPER_SETTINGS");
2630
+ boolean opened = tryOpenWallpaperIntent(settingsIntent);
2631
+ String screen = opened ? "wallpaper-settings" : "";
2632
+
2633
+ if (!opened) {
2634
+ Intent chooserIntent = new Intent(Intent.ACTION_SET_WALLPAPER);
2635
+ opened = tryOpenWallpaperIntent(chooserIntent);
2636
+ screen = opened ? "set-wallpaper" : "";
2637
+ }
2638
+
2639
+ result.put("settingsOpened", opened);
2640
+ result.put("configuracaoAberta", opened);
2641
+ result.put("settingsScreen", screen);
2642
+ result.put("telaConfiguracao", screen);
2643
+ return result;
2644
+ }
2645
+
2646
+ private boolean tryOpenWallpaperIntent(Intent intent) {
2647
+ if (intent.resolveActivity(context().getPackageManager()) == null) {
2648
+ return false;
2649
+ }
2650
+ try {
2651
+ cordova.getActivity().startActivity(intent);
2652
+ return true;
2653
+ } catch (ActivityNotFoundException error) {
2654
+ return false;
2655
+ }
2656
+ }
2657
+
2658
+ private JSONObject unsupportedWallpaperResult(JSONObject options, String target, String mimeType, String message) throws Exception {
2659
+ JSONObject result = wallpaperInfo();
2660
+ result.put("target", target);
2661
+ result.put("alvo", target);
2662
+ result.put("mimeType", mimeType);
2663
+ result.put("applied", false);
2664
+ result.put("aplicado", false);
2665
+ result.put("supported", false);
2666
+ result.put("suportado", false);
2667
+ result.put("requiresUserAction", true);
2668
+ result.put("precisaAcaoUsuario", true);
2669
+ result.put("message", message);
2670
+ result.put("mensagem", message);
2671
+ if (wallpaperShouldOpenSettings(options)) {
2672
+ JSONObject settings = openWallpaperSettings();
2673
+ result.put("settingsOpened", settings.optBoolean("settingsOpened", false));
2674
+ result.put("configuracaoAberta", settings.optBoolean("configuracaoAberta", false));
2675
+ result.put("settingsScreen", settings.optString("settingsScreen", ""));
2676
+ result.put("telaConfiguracao", settings.optString("telaConfiguracao", ""));
2677
+ }
2678
+ return result;
2679
+ }
2680
+
2681
+ private boolean wallpaperShouldOpenSettings(JSONObject options) {
2682
+ return options.optBoolean("abrirConfiguracao",
2683
+ options.optBoolean("openSettings",
2684
+ options.optBoolean("abrirAjustes", false)));
2685
+ }
2686
+
2687
+ private String wallpaperTarget(JSONObject options) {
2688
+ String target = options.optString("alvo",
2689
+ options.optString("target",
2690
+ options.optString("tela",
2691
+ options.optString("screen", "inicio"))));
2692
+ String lower = target == null ? "" : target.toLowerCase();
2693
+ if (lower.indexOf("call") >= 0 || lower.indexOf("chamada") >= 0) {
2694
+ return "call";
2695
+ }
2696
+ if (lower.indexOf("lock") >= 0 || lower.indexOf("bloque") >= 0) {
2697
+ return "lock";
2698
+ }
2699
+ if (lower.indexOf("both") >= 0 || lower.indexOf("amb") >= 0 || lower.indexOf("all") >= 0 || lower.indexOf("tudo") >= 0) {
2700
+ return "both";
2701
+ }
2702
+ return "system";
2703
+ }
2704
+
2705
+ private String wallpaperMimeType(JSONObject options) {
2706
+ String mimeType = options.optString("mimeType", options.optString("tipoMime", ""));
2707
+ if (mimeType.indexOf('/') > 0) {
2708
+ return mimeType;
2709
+ }
2710
+
2711
+ String base64 = options.optString("base64", "");
2712
+ if (base64.startsWith("data:")) {
2713
+ int end = base64.indexOf(';');
2714
+ int comma = base64.indexOf(',');
2715
+ if (end < 0 || (comma >= 0 && comma < end)) {
2716
+ end = comma;
2717
+ }
2718
+ if (end > 5) {
2719
+ return base64.substring(5, end);
2720
+ }
2721
+ }
2722
+
2723
+ String uriText = options.optString("contentUri", options.optString("uri", ""));
2724
+ if (uriText.length() > 0) {
2725
+ try {
2726
+ String uriType = context().getContentResolver().getType(Uri.parse(uriText));
2727
+ if (uriType != null && uriType.length() > 0) {
2728
+ return uriType;
2729
+ }
2730
+ } catch (Exception ignored) {
2731
+ }
2732
+ }
2733
+
2734
+ String name = options.optString("nomeArquivo", options.optString("fileName", options.optString("nome", options.optString("name", ""))));
2735
+ if (name.length() > 0) {
2736
+ return guessMimeType(name);
2737
+ }
2738
+ String path = options.optString("caminho", options.optString("path", ""));
2739
+ if (path.length() > 0) {
2740
+ return guessMimeType(path);
2741
+ }
2742
+ return "application/octet-stream";
2743
+ }
2744
+
2745
+ private Bitmap decodeWallpaperBitmap(JSONObject options) throws Exception {
2746
+ InputStream inputStream = wallpaperInputStream(options);
2747
+ try {
2748
+ return BitmapFactory.decodeStream(inputStream);
2749
+ } finally {
2750
+ inputStream.close();
2751
+ }
2752
+ }
2753
+
2754
+ private InputStream wallpaperInputStream(JSONObject options) throws Exception {
2755
+ String base64 = options.optString("base64", "");
2756
+ if (base64.length() > 0) {
2757
+ return new ByteArrayInputStream(Base64.decode(stripDataUrlBase64(base64), Base64.DEFAULT));
2758
+ }
2759
+
2760
+ String uriText = options.optString("contentUri", options.optString("uri", ""));
2761
+ if (uriText.length() > 0) {
2762
+ Uri uri = Uri.parse(uriText);
2763
+ InputStream inputStream = context().getContentResolver().openInputStream(uri);
2764
+ if (inputStream == null) {
2765
+ throw new Exception("Could not open wallpaper URI.");
2766
+ }
2767
+ return inputStream;
2768
+ }
2769
+
2770
+ String path = options.optString("caminho", options.optString("path", ""));
2771
+ if (path.length() > 0) {
2772
+ return new FileInputStream(new File(path));
2773
+ }
2774
+
2775
+ String name = options.optString("nomeArquivo", options.optString("fileName", options.optString("nome", options.optString("name", ""))));
2776
+ if (name.length() > 0) {
2777
+ File file = storedFile(name);
2778
+ if (!file.exists()) {
2779
+ throw new Exception("Stored wallpaper file does not exist.");
2780
+ }
2781
+ return new FileInputStream(file);
2782
+ }
2783
+
2784
+ throw new Exception("Wallpaper image source is required.");
2785
+ }
2786
+
2787
+ private String stripDataUrlBase64(String value) {
2788
+ int comma = value.indexOf(',');
2789
+ if (value.startsWith("data:") && comma >= 0) {
2790
+ return value.substring(comma + 1);
2791
+ }
2792
+ return value;
2793
+ }
2794
+
2795
+ private String dataUrlMimeType(String value) {
2796
+ if (value == null || !value.startsWith("data:")) {
2797
+ return "";
2798
+ }
2799
+ int comma = value.indexOf(',');
2800
+ int semicolon = value.indexOf(';');
2801
+ int end = semicolon >= 0 ? semicolon : comma;
2802
+ if (end > 5) {
2803
+ return value.substring(5, end);
2804
+ }
2805
+ return "";
2806
+ }
2807
+
2808
+ private File storedFilesDir() throws Exception {
2809
+ File dir = context().getExternalFilesDir(STORED_FILES_DIR);
2810
+ if (dir == null) {
2811
+ dir = new File(context().getFilesDir(), STORED_FILES_DIR);
2812
+ }
2813
+ if (!dir.exists() && !dir.mkdirs()) {
2814
+ throw new Exception("Could not create stored files directory.");
2815
+ }
2816
+ return dir;
2817
+ }
2818
+
2819
+ private File storedFileFromOptions(JSONObject options) throws Exception {
2820
+ String name = options.optString("nome", options.optString("name", options.optString("fileName", options.optString("nomeArquivo", ""))));
2821
+ return storedFile(name);
2822
+ }
2823
+
2824
+ private File storedFile(String name) throws Exception {
2825
+ String safeName = safeStoredFileName(name);
2826
+ File dir = storedFilesDir();
2827
+ File file = new File(dir, safeName);
2828
+ String dirPath = dir.getCanonicalPath();
2829
+ String filePath = file.getCanonicalPath();
2830
+ if (!filePath.equals(dirPath) && !filePath.startsWith(dirPath + File.separator)) {
2831
+ throw new Exception("Invalid stored file name.");
2832
+ }
2833
+ return file;
2834
+ }
2835
+
2836
+ private String safeStoredFileName(String name) throws Exception {
2837
+ String value = name == null ? "" : name.trim();
2838
+ if (value.length() == 0) {
2839
+ throw new Exception("File name is required.");
2840
+ }
2841
+ if (value.indexOf('/') >= 0 || value.indexOf('\\') >= 0 || value.indexOf(':') >= 0 || value.contains("..")) {
2842
+ throw new Exception("File name must not contain path separators.");
2843
+ }
2844
+ return value;
2845
+ }
2846
+
2847
+ private StoredContent storedContentFromOptions(JSONObject options) throws Exception {
2848
+ if (options.has("base64")) {
2849
+ return new StoredContent(Base64.decode(options.optString("base64", ""), Base64.DEFAULT), "base64", options.optString("mimeType", "application/octet-stream"));
2850
+ }
2851
+
2852
+ Object value = options.has("value") ? options.opt("value") : options.opt("valor");
2853
+ if (value == null || value == JSONObject.NULL) {
2854
+ value = options.optString("conteudo", options.optString("content", ""));
2855
+ }
2856
+
2857
+ if (value instanceof JSONObject || value instanceof JSONArray) {
2858
+ return new StoredContent(String.valueOf(value).getBytes("UTF-8"), "json", options.optString("mimeType", "application/json"));
2859
+ }
2860
+ if (value instanceof Number) {
2861
+ return new StoredContent(String.valueOf(value).getBytes("UTF-8"), "number", options.optString("mimeType", "text/plain"));
2862
+ }
2863
+ if (value instanceof Boolean) {
2864
+ return new StoredContent(String.valueOf(value).getBytes("UTF-8"), "boolean", options.optString("mimeType", "text/plain"));
2865
+ }
2866
+ if (options.optBoolean("json", false)) {
2867
+ return new StoredContent(String.valueOf(value).getBytes("UTF-8"), "json", options.optString("mimeType", "application/json"));
2868
+ }
2869
+ return new StoredContent(String.valueOf(value).getBytes("UTF-8"), "string", options.optString("mimeType", "text/plain"));
2870
+ }
2871
+
2872
+ private Object storedValueFromContent(String content, String type) {
2873
+ try {
2874
+ if ("json".equals(type)) {
2875
+ String text = content.trim();
2876
+ if (text.startsWith("[")) {
2877
+ return new JSONArray(text);
2878
+ }
2879
+ if (text.startsWith("{")) {
2880
+ return new JSONObject(text);
2881
+ }
2882
+ return text;
2883
+ }
2884
+ if ("number".equals(type)) {
2885
+ return Double.valueOf(content);
2886
+ }
2887
+ if ("boolean".equals(type)) {
2888
+ return Boolean.valueOf(content);
2889
+ }
2890
+ } catch (Exception ignored) {
2891
+ }
2892
+ return content;
2893
+ }
2894
+
2895
+ private JSONObject storedFileResult(File file, String mimeType, String type) throws Exception {
2896
+ JSONObject result = new JSONObject();
2897
+ result.put("name", file.getName());
2898
+ result.put("nome", file.getName());
2899
+ result.put("uri", fileProviderUri(file).toString());
2900
+ result.put("contentUri", result.optString("uri"));
2901
+ result.put("path", file.getAbsolutePath());
2902
+ result.put("caminho", file.getAbsolutePath());
2903
+ result.put("mimeType", mimeType == null || mimeType.length() == 0 ? guessMimeType(file.getName()) : mimeType);
2904
+ result.put("type", type);
2905
+ result.put("tipo", type);
2906
+ result.put("size", file.exists() ? file.length() : 0);
2907
+ result.put("tamanho", file.exists() ? file.length() : 0);
2908
+ result.put("lastModified", file.exists() ? file.lastModified() : 0);
2909
+ result.put("modificadoEm", file.exists() ? file.lastModified() : 0);
2910
+ return result;
2911
+ }
2912
+
2913
+ private File storedFileMeta(File file) {
2914
+ return new File(file.getParentFile(), file.getName() + STORED_FILE_META_SUFFIX);
2915
+ }
2916
+
2917
+ private void writeStoredFileMeta(File file, JSONObject meta) throws Exception {
2918
+ File metaFile = storedFileMeta(file);
2919
+ FileOutputStream outputStream = new FileOutputStream(metaFile);
2920
+ try {
2921
+ outputStream.write(meta.toString().getBytes("UTF-8"));
2922
+ } finally {
2923
+ outputStream.close();
2924
+ }
2925
+ }
2926
+
2927
+ private JSONObject readStoredFileMeta(File file) {
2928
+ File metaFile = storedFileMeta(file);
2929
+ if (!metaFile.exists()) {
2930
+ return new JSONObject();
2931
+ }
2932
+ try {
2933
+ return new JSONObject(new String(readFileBytes(metaFile), "UTF-8"));
2934
+ } catch (Exception ignored) {
2935
+ return new JSONObject();
2936
+ }
2937
+ }
2938
+
2939
+ private byte[] readUriBytes(Uri uri) throws Exception {
2940
+ InputStream inputStream = context().getContentResolver().openInputStream(uri);
2941
+ if (inputStream == null) {
2942
+ throw new Exception("Could not open input stream.");
2943
+ }
2944
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
2945
+ try {
2946
+ byte[] buffer = new byte[8192];
2947
+ int read;
2948
+ while ((read = inputStream.read(buffer)) != -1) {
2949
+ outputStream.write(buffer, 0, read);
2950
+ }
2951
+ return outputStream.toByteArray();
2952
+ } finally {
2953
+ inputStream.close();
2954
+ outputStream.close();
2955
+ }
2956
+ }
2957
+
2958
+ private String downloadedFileName(URL url) {
2959
+ String path = url.getPath();
2960
+ int slash = path == null ? -1 : path.lastIndexOf('/');
2961
+ String name = slash >= 0 ? path.substring(slash + 1) : path;
2962
+ if (name == null || name.trim().length() == 0) {
2963
+ return "download-" + System.currentTimeMillis();
2964
+ }
2965
+ return name;
2966
+ }
2967
+
2968
+ private String guessMimeType(String name) {
2969
+ String lower = name == null ? "" : name.toLowerCase();
2970
+ if (lower.endsWith(".html") || lower.endsWith(".htm")) {
2971
+ return "text/html";
2972
+ }
2973
+ if (lower.endsWith(".json")) {
2974
+ return "application/json";
2975
+ }
2976
+ if (lower.endsWith(".pdf")) {
2977
+ return "application/pdf";
2978
+ }
2979
+ if (lower.endsWith(".png")) {
2980
+ return "image/png";
2981
+ }
2982
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) {
2983
+ return "image/jpeg";
2984
+ }
2985
+ if (lower.endsWith(".mp4")) {
2986
+ return "video/mp4";
2987
+ }
2988
+ if (lower.endsWith(".mp3")) {
2989
+ return "audio/mpeg";
2990
+ }
2991
+ if (lower.endsWith(".txt") || lower.endsWith(".log")) {
2992
+ return "text/plain";
2993
+ }
2994
+ return "application/octet-stream";
2995
+ }
2996
+
2997
+ private static class StoredContent {
2998
+ final byte[] bytes;
2999
+ final String type;
3000
+ final String mimeType;
3001
+
3002
+ StoredContent(byte[] bytes, String type, String mimeType) {
3003
+ this.bytes = bytes;
3004
+ this.type = type;
3005
+ this.mimeType = mimeType;
3006
+ }
3007
+ }
3008
+
3009
+ private static class DownloadSource {
3010
+ final InputStream inputStream;
3011
+ final HttpURLConnection connection;
3012
+ final long totalBytes;
3013
+ final String mimeType;
3014
+ final String type;
3015
+ final String url;
3016
+ final String defaultName;
3017
+ final File file;
3018
+
3019
+ DownloadSource(InputStream inputStream, HttpURLConnection connection, long totalBytes, String mimeType, String type, String url, String defaultName, File file) {
3020
+ this.inputStream = inputStream;
3021
+ this.connection = connection;
3022
+ this.totalBytes = totalBytes;
3023
+ this.mimeType = mimeType == null ? "" : mimeType;
3024
+ this.type = type == null ? "file" : type;
3025
+ this.url = url == null ? "" : url;
3026
+ this.defaultName = defaultName == null ? "" : defaultName;
3027
+ this.file = file;
3028
+ }
3029
+ }
3030
+
3031
+ private JSONObject saveSecureItem(JSONObject options) throws Exception {
3032
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
3033
+ String key = secureKeyFromOptions(safeOptions);
3034
+ StoredContent content = storedContentFromOptions(safeOptions);
3035
+ EncryptedValue encrypted = encryptSecureText(new String(content.bytes, "UTF-8"));
3036
+ SharedPreferences.Editor editor = secureStore().edit();
3037
+ editor.putString(SECURE_VALUE_PREFIX + key, encrypted.cipherText);
3038
+ editor.putString(SECURE_IV_PREFIX + key, encrypted.iv);
3039
+ editor.putString(SECURE_TYPE_PREFIX + key, content.type);
3040
+ editor.apply();
3041
+
3042
+ JSONObject result = new JSONObject();
3043
+ result.put("key", key);
3044
+ result.put("chave", key);
3045
+ result.put("saved", true);
3046
+ result.put("salvo", true);
3047
+ result.put("type", content.type);
3048
+ result.put("tipo", content.type);
3049
+ return result;
3050
+ }
3051
+
3052
+ private JSONObject readSecureItem(JSONObject options) throws Exception {
3053
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
3054
+ String key = secureKeyFromOptions(safeOptions);
3055
+ SharedPreferences store = secureStore();
3056
+ JSONObject result = new JSONObject();
3057
+ result.put("key", key);
3058
+ result.put("chave", key);
3059
+ if (!store.contains(SECURE_VALUE_PREFIX + key)) {
3060
+ result.put("exists", false);
3061
+ result.put("existe", false);
3062
+ return result;
3063
+ }
3064
+
3065
+ String content = decryptSecureText(store.getString(SECURE_VALUE_PREFIX + key, ""), store.getString(SECURE_IV_PREFIX + key, ""));
3066
+ String type = store.getString(SECURE_TYPE_PREFIX + key, "string");
3067
+ result.put("exists", true);
3068
+ result.put("existe", true);
3069
+ result.put("type", type);
3070
+ result.put("tipo", type);
3071
+ result.put("value", storedValueFromContent(content, type));
3072
+ result.put("valor", result.opt("value"));
3073
+ return result;
3074
+ }
3075
+
3076
+ private JSONObject deleteSecureItem(JSONObject options) throws Exception {
3077
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
3078
+ String key = secureKeyFromOptions(safeOptions);
3079
+ SharedPreferences store = secureStore();
3080
+ boolean existed = store.contains(SECURE_VALUE_PREFIX + key);
3081
+ store.edit()
3082
+ .remove(SECURE_VALUE_PREFIX + key)
3083
+ .remove(SECURE_IV_PREFIX + key)
3084
+ .remove(SECURE_TYPE_PREFIX + key)
3085
+ .apply();
3086
+
3087
+ JSONObject result = new JSONObject();
3088
+ result.put("key", key);
3089
+ result.put("chave", key);
3090
+ result.put("existsBefore", existed);
3091
+ result.put("existiaAntes", existed);
3092
+ result.put("deleted", true);
3093
+ result.put("excluido", true);
3094
+ return result;
3095
+ }
3096
+
3097
+ private JSONArray listSecureKeys() {
3098
+ JSONArray result = new JSONArray();
3099
+ Map<String, ?> values = secureStore().getAll();
3100
+ for (String key : values.keySet()) {
3101
+ if (key.startsWith(SECURE_VALUE_PREFIX)) {
3102
+ result.put(key.substring(SECURE_VALUE_PREFIX.length()));
3103
+ }
3104
+ }
3105
+ return result;
3106
+ }
3107
+
3108
+ private JSONObject clearSecureStorage() {
3109
+ SharedPreferences store = secureStore();
3110
+ Map<String, ?> values = store.getAll();
3111
+ SharedPreferences.Editor editor = store.edit();
3112
+ int count = 0;
3113
+ for (String key : values.keySet()) {
3114
+ if (key.startsWith(SECURE_VALUE_PREFIX) || key.startsWith(SECURE_IV_PREFIX) || key.startsWith(SECURE_TYPE_PREFIX)) {
3115
+ editor.remove(key);
3116
+ count += key.startsWith(SECURE_VALUE_PREFIX) ? 1 : 0;
3117
+ }
3118
+ }
3119
+ editor.apply();
3120
+
3121
+ JSONObject result = new JSONObject();
3122
+ try {
3123
+ result.put("cleared", true);
3124
+ result.put("limpo", true);
3125
+ result.put("count", count);
3126
+ result.put("total", count);
3127
+ } catch (Exception ignored) {
3128
+ }
3129
+ return result;
3130
+ }
3131
+
3132
+ private SharedPreferences secureStore() {
3133
+ return context().getSharedPreferences(SECURE_PREFS_NAME, Context.MODE_PRIVATE);
3134
+ }
3135
+
3136
+ private String secureKeyFromOptions(JSONObject options) throws Exception {
3137
+ String key = options.optString("chave", options.optString("key", options.optString("name", options.optString("nome", ""))));
3138
+ if (key.trim().length() == 0 || key.indexOf('/') >= 0 || key.indexOf('\\') >= 0 || key.contains("..")) {
3139
+ throw new Exception("Secure storage key is invalid.");
3140
+ }
3141
+ return key.trim();
3142
+ }
3143
+
3144
+ private EncryptedValue encryptSecureText(String value) throws Exception {
3145
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
3146
+ cipher.init(Cipher.ENCRYPT_MODE, secureStorageSecretKey());
3147
+ byte[] encrypted = cipher.doFinal(value.getBytes("UTF-8"));
3148
+ return new EncryptedValue(
3149
+ Base64.encodeToString(encrypted, Base64.NO_WRAP),
3150
+ Base64.encodeToString(cipher.getIV(), Base64.NO_WRAP)
3151
+ );
3152
+ }
3153
+
3154
+ private String decryptSecureText(String cipherText, String iv) throws Exception {
3155
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
3156
+ GCMParameterSpec spec = new GCMParameterSpec(128, Base64.decode(iv, Base64.DEFAULT));
3157
+ cipher.init(Cipher.DECRYPT_MODE, secureStorageSecretKey(), spec);
3158
+ byte[] plain = cipher.doFinal(Base64.decode(cipherText, Base64.DEFAULT));
3159
+ return new String(plain, "UTF-8");
3160
+ }
3161
+
3162
+ private SecretKey secureStorageSecretKey() throws Exception {
3163
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
3164
+ keyStore.load(null);
3165
+ if (!keyStore.containsAlias(SECURE_KEY_ALIAS)) {
3166
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
3167
+ keyGenerator.init(new KeyGenParameterSpec.Builder(
3168
+ SECURE_KEY_ALIAS,
3169
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
3170
+ )
3171
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
3172
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
3173
+ .setRandomizedEncryptionRequired(true)
3174
+ .build());
3175
+ return keyGenerator.generateKey();
3176
+ }
3177
+
3178
+ KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) keyStore.getEntry(SECURE_KEY_ALIAS, null);
3179
+ return entry.getSecretKey();
3180
+ }
3181
+
3182
+ private static class EncryptedValue {
3183
+ final String cipherText;
3184
+ final String iv;
3185
+
3186
+ EncryptedValue(String cipherText, String iv) {
3187
+ this.cipherText = cipherText;
3188
+ this.iv = iv;
3189
+ }
3190
+ }
3191
+
3192
+ private JSONObject deviceInfo() throws Exception {
3193
+ JSONObject result = new JSONObject();
3194
+ result.put("manufacturer", Build.MANUFACTURER);
3195
+ result.put("fabricante", Build.MANUFACTURER);
3196
+ result.put("model", Build.MODEL);
3197
+ result.put("modelo", Build.MODEL);
3198
+ result.put("brand", Build.BRAND);
3199
+ result.put("androidVersion", Build.VERSION.RELEASE);
3200
+ result.put("sdkInt", Build.VERSION.SDK_INT);
3201
+ result.put("packageName", context().getPackageName());
3202
+ return result;
3203
+ }
3204
+
3205
+ private JSONObject networkInfo() throws Exception {
3206
+ JSONObject result = new JSONObject();
3207
+ ConnectivityManager manager = (ConnectivityManager) context().getSystemService(Context.CONNECTIVITY_SERVICE);
3208
+ boolean connected = false;
3209
+ String type = "unknown";
3210
+
3211
+ if (manager != null) {
3212
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
3213
+ Network network = manager.getActiveNetwork();
3214
+ NetworkCapabilities capabilities = network == null ? null : manager.getNetworkCapabilities(network);
3215
+ connected = capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
3216
+ if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
3217
+ type = "wifi";
3218
+ } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
3219
+ type = "cellular";
3220
+ } else if (capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
3221
+ type = "ethernet";
3222
+ }
3223
+ } else if (manager.getActiveNetworkInfo() != null) {
3224
+ connected = manager.getActiveNetworkInfo().isConnected();
3225
+ type = manager.getActiveNetworkInfo().getTypeName().toLowerCase();
3226
+ }
3227
+ }
3228
+
3229
+ result.put("online", connected);
3230
+ result.put("connected", connected);
3231
+ result.put("tipo", type);
3232
+ result.put("type", type);
3233
+ return result;
3234
+ }
3235
+
3236
+ private JSONObject batteryInfo() throws Exception {
3237
+ Intent intent = context().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
3238
+ JSONObject result = new JSONObject();
3239
+ if (intent == null) {
3240
+ result.put("level", -1);
3241
+ result.put("nivel", -1);
3242
+ result.put("charging", false);
3243
+ return result;
3244
+ }
3245
+
3246
+ int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
3247
+ int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
3248
+ int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
3249
+ boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING
3250
+ || status == BatteryManager.BATTERY_STATUS_FULL;
3251
+ double percent = scale <= 0 ? -1 : (level * 100.0 / scale);
3252
+
3253
+ result.put("level", percent);
3254
+ result.put("nivel", percent);
3255
+ result.put("charging", charging);
3256
+ result.put("carregando", charging);
3257
+ return result;
3258
+ }
3259
+
3260
+ private JSONObject memoryInfo() throws Exception {
3261
+ ActivityManager manager = (ActivityManager) context().getSystemService(Context.ACTIVITY_SERVICE);
3262
+ ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
3263
+ if (manager != null) {
3264
+ manager.getMemoryInfo(info);
3265
+ }
3266
+
3267
+ Runtime runtime = Runtime.getRuntime();
3268
+ JSONObject result = new JSONObject();
3269
+ result.put("availableBytes", info.availMem);
3270
+ result.put("totalBytes", Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? info.totalMem : -1);
3271
+ result.put("lowMemory", info.lowMemory);
3272
+ result.put("thresholdBytes", info.threshold);
3273
+ result.put("appUsedBytes", runtime.totalMemory() - runtime.freeMemory());
3274
+ result.put("appMaxBytes", runtime.maxMemory());
3275
+ return result;
3276
+ }
3277
+
3278
+ private JSONObject storageInfo() throws Exception {
3279
+ JSONObject result = new JSONObject();
3280
+ result.put("internal", statFsInfo(Environment.getDataDirectory()));
3281
+ result.put("cache", statFsInfo(context().getCacheDir()));
3282
+ if (context().getExternalFilesDir(null) != null) {
3283
+ result.put("appExternal", statFsInfo(context().getExternalFilesDir(null)));
3284
+ }
3285
+ return result;
3286
+ }
3287
+
3288
+ private JSONObject statFsInfo(java.io.File file) throws Exception {
3289
+ StatFs statFs = new StatFs(file.getAbsolutePath());
3290
+ long blockSize = statFs.getBlockSizeLong();
3291
+ long total = statFs.getBlockCountLong() * blockSize;
3292
+ long available = statFs.getAvailableBlocksLong() * blockSize;
3293
+ JSONObject result = new JSONObject();
3294
+ result.put("path", file.getAbsolutePath());
1744
3295
  result.put("totalBytes", total);
1745
3296
  result.put("availableBytes", available);
1746
3297
  result.put("usedBytes", total - available);
@@ -1859,6 +3410,428 @@ public class Html2ApkBridge extends CordovaPlugin {
1859
3410
  return "other";
1860
3411
  }
1861
3412
 
3413
+ private boolean hasLocationPermission() {
3414
+ return ContextCompat.checkSelfPermission(context(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
3415
+ || ContextCompat.checkSelfPermission(context(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
3416
+ }
3417
+
3418
+ private String locationPermissionName(JSONObject options) {
3419
+ boolean highAccuracy = options != null && options.optBoolean("altaPrecisao", options.optBoolean("highAccuracy", false));
3420
+ return highAccuracy ? Manifest.permission.ACCESS_FINE_LOCATION : Manifest.permission.ACCESS_COARSE_LOCATION;
3421
+ }
3422
+
3423
+ private void getLocation(JSONObject options, CallbackContext callbackContext) throws Exception {
3424
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
3425
+ if (!hasLocationPermission()) {
3426
+ requestLocationPermission(safeOptions, false, callbackContext);
3427
+ return;
3428
+ }
3429
+ resolveCurrentLocation(safeOptions, callbackContext);
3430
+ }
3431
+
3432
+ private void watchLocation(JSONObject options, CallbackContext callbackContext) throws Exception {
3433
+ JSONObject safeOptions = options == null ? new JSONObject() : options;
3434
+ if (!hasLocationPermission()) {
3435
+ requestLocationPermission(safeOptions, true, callbackContext);
3436
+ return;
3437
+ }
3438
+ startLocationWatch(safeOptions, callbackContext);
3439
+ }
3440
+
3441
+ private void requestLocationPermission(JSONObject options, boolean watch, CallbackContext callbackContext) throws Exception {
3442
+ if (pendingLocationCallback != null) {
3443
+ rejectBusyCallback(callbackContext, "Location permission");
3444
+ return;
3445
+ }
3446
+
3447
+ pendingLocationCallback = callbackContext;
3448
+ pendingLocationOptions = options;
3449
+ pendingLocationWatch = watch;
3450
+ rememberRuntimePermissionRequest(Manifest.permission.ACCESS_FINE_LOCATION);
3451
+ rememberRuntimePermissionRequest(Manifest.permission.ACCESS_COARSE_LOCATION);
3452
+ cordova.requestPermissions(this, REQUEST_LOCATION, new String[] {
3453
+ Manifest.permission.ACCESS_FINE_LOCATION,
3454
+ Manifest.permission.ACCESS_COARSE_LOCATION
3455
+ });
3456
+ }
3457
+
3458
+ private void resolveCurrentLocation(final JSONObject options, final CallbackContext callbackContext) throws Exception {
3459
+ final LocationManager manager = locationManager();
3460
+ Location lastKnown = bestLastKnownLocation(manager, options);
3461
+ if (lastKnown != null) {
3462
+ callbackContext.success(locationResult(lastKnown, "lastKnown"));
3463
+ return;
3464
+ }
3465
+
3466
+ final String provider = bestLocationProvider(manager, options);
3467
+ if (provider.length() == 0) {
3468
+ callbackContext.success(locationUnavailableResult("No location provider is enabled."));
3469
+ return;
3470
+ }
3471
+
3472
+ final boolean[] finished = new boolean[] { false };
3473
+ final LocationListener listener = new LocationListener() {
3474
+ @Override
3475
+ public void onLocationChanged(Location location) {
3476
+ if (finished[0]) {
3477
+ return;
3478
+ }
3479
+ finished[0] = true;
3480
+ try {
3481
+ manager.removeUpdates(this);
3482
+ } catch (Exception ignored) {
3483
+ }
3484
+ try {
3485
+ callbackContext.success(locationResult(location, "singleUpdate"));
3486
+ } catch (Exception error) {
3487
+ callbackContext.error(error.getMessage());
3488
+ }
3489
+ }
3490
+
3491
+ @Override
3492
+ public void onStatusChanged(String provider, int status, Bundle extras) {
3493
+ }
3494
+
3495
+ @Override
3496
+ public void onProviderEnabled(String provider) {
3497
+ }
3498
+
3499
+ @Override
3500
+ public void onProviderDisabled(String provider) {
3501
+ }
3502
+ };
3503
+
3504
+ manager.requestSingleUpdate(provider, listener, Looper.getMainLooper());
3505
+ new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
3506
+ @Override
3507
+ public void run() {
3508
+ if (finished[0]) {
3509
+ return;
3510
+ }
3511
+ finished[0] = true;
3512
+ try {
3513
+ manager.removeUpdates(listener);
3514
+ } catch (Exception ignored) {
3515
+ }
3516
+ try {
3517
+ callbackContext.success(locationUnavailableResult("Location timeout."));
3518
+ } catch (Exception error) {
3519
+ callbackContext.error(error.getMessage());
3520
+ }
3521
+ }
3522
+ }, Math.max(1000, options.optLong("timeoutMs", options.optLong("tempoLimiteMs", 10000))));
3523
+ }
3524
+
3525
+ private void startLocationWatch(final JSONObject options, CallbackContext callbackContext) throws Exception {
3526
+ final LocationManager manager = locationManager();
3527
+ final String provider = bestLocationProvider(manager, options);
3528
+ if (provider.length() == 0) {
3529
+ callbackContext.success(locationUnavailableResult("No location provider is enabled."));
3530
+ return;
3531
+ }
3532
+
3533
+ final String id = "loc-" + System.currentTimeMillis() + "-" + (++locationWatchCounter);
3534
+ final LocationListener listener = new LocationListener() {
3535
+ @Override
3536
+ public void onLocationChanged(Location location) {
3537
+ try {
3538
+ JSONObject detail = locationResult(location, "watch");
3539
+ detail.put("watchId", id);
3540
+ detail.put("idObservador", id);
3541
+ dispatchEvent("localizacao:mudou", detail);
3542
+ } catch (Exception ignored) {
3543
+ }
3544
+ }
3545
+
3546
+ @Override
3547
+ public void onStatusChanged(String provider, int status, Bundle extras) {
3548
+ }
3549
+
3550
+ @Override
3551
+ public void onProviderEnabled(String provider) {
3552
+ }
3553
+
3554
+ @Override
3555
+ public void onProviderDisabled(String provider) {
3556
+ try {
3557
+ JSONObject detail = locationUnavailableResult("Location provider disabled.");
3558
+ detail.put("watchId", id);
3559
+ detail.put("idObservador", id);
3560
+ dispatchEvent("localizacao:mudou", detail);
3561
+ } catch (Exception ignored) {
3562
+ }
3563
+ }
3564
+ };
3565
+
3566
+ long minTime = Math.max(1000, options.optLong("intervaloMs", options.optLong("intervalMs", 5000)));
3567
+ float minDistance = (float) Math.max(0, options.optDouble("distanciaMetros", options.optDouble("distanceMeters", 0)));
3568
+ manager.requestLocationUpdates(provider, minTime, minDistance, listener, Looper.getMainLooper());
3569
+ locationListeners.put(id, listener);
3570
+
3571
+ Location lastKnown = bestLastKnownLocation(manager, options);
3572
+ if (lastKnown != null) {
3573
+ JSONObject detail = locationResult(lastKnown, "lastKnown");
3574
+ detail.put("watchId", id);
3575
+ detail.put("idObservador", id);
3576
+ dispatchEvent("localizacao:mudou", detail);
3577
+ }
3578
+
3579
+ JSONObject result = new JSONObject();
3580
+ result.put("id", id);
3581
+ result.put("watchId", id);
3582
+ result.put("idObservador", id);
3583
+ result.put("watching", true);
3584
+ result.put("observando", true);
3585
+ result.put("provider", provider);
3586
+ result.put("provedor", provider);
3587
+ callbackContext.success(result);
3588
+ }
3589
+
3590
+ private JSONObject stopLocationWatch(String id) throws Exception {
3591
+ JSONObject result = new JSONObject();
3592
+ if (id == null || id.trim().length() == 0) {
3593
+ int count = stopAllLocationWatches();
3594
+ result.put("stopped", count);
3595
+ result.put("parados", count);
3596
+ return result;
3597
+ }
3598
+
3599
+ LocationListener listener = locationListeners.remove(id);
3600
+ boolean stopped = false;
3601
+ if (listener != null) {
3602
+ locationManager().removeUpdates(listener);
3603
+ stopped = true;
3604
+ }
3605
+ result.put("id", id);
3606
+ result.put("watchId", id);
3607
+ result.put("stopped", stopped);
3608
+ result.put("parado", stopped);
3609
+ return result;
3610
+ }
3611
+
3612
+ private int stopAllLocationWatches() {
3613
+ int count = 0;
3614
+ try {
3615
+ LocationManager manager = locationManager();
3616
+ for (LocationListener listener : locationListeners.values()) {
3617
+ try {
3618
+ manager.removeUpdates(listener);
3619
+ count += 1;
3620
+ } catch (Exception ignored) {
3621
+ }
3622
+ }
3623
+ } catch (Exception ignored) {
3624
+ }
3625
+ locationListeners.clear();
3626
+ return count;
3627
+ }
3628
+
3629
+ private LocationManager locationManager() throws Exception {
3630
+ LocationManager manager = (LocationManager) context().getSystemService(Context.LOCATION_SERVICE);
3631
+ if (manager == null) {
3632
+ throw new Exception("Location service is not available.");
3633
+ }
3634
+ return manager;
3635
+ }
3636
+
3637
+ private String bestLocationProvider(LocationManager manager, JSONObject options) {
3638
+ boolean highAccuracy = options != null && options.optBoolean("altaPrecisao", options.optBoolean("highAccuracy", false));
3639
+ if (highAccuracy && manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
3640
+ return LocationManager.GPS_PROVIDER;
3641
+ }
3642
+ if (manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
3643
+ return LocationManager.NETWORK_PROVIDER;
3644
+ }
3645
+ if (manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
3646
+ return LocationManager.GPS_PROVIDER;
3647
+ }
3648
+ if (manager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)) {
3649
+ return LocationManager.PASSIVE_PROVIDER;
3650
+ }
3651
+ return "";
3652
+ }
3653
+
3654
+ private Location bestLastKnownLocation(LocationManager manager, JSONObject options) {
3655
+ Location best = null;
3656
+ String[] providers = new String[] {
3657
+ LocationManager.GPS_PROVIDER,
3658
+ LocationManager.NETWORK_PROVIDER,
3659
+ LocationManager.PASSIVE_PROVIDER
3660
+ };
3661
+ for (String provider : providers) {
3662
+ try {
3663
+ if (!manager.isProviderEnabled(provider)) {
3664
+ continue;
3665
+ }
3666
+ Location location = manager.getLastKnownLocation(provider);
3667
+ if (location == null) {
3668
+ continue;
3669
+ }
3670
+ if (best == null || location.getTime() > best.getTime() || location.getAccuracy() < best.getAccuracy()) {
3671
+ best = location;
3672
+ }
3673
+ } catch (Exception ignored) {
3674
+ }
3675
+ }
3676
+ return best;
3677
+ }
3678
+
3679
+ private JSONObject locationResult(Location location, String source) throws Exception {
3680
+ JSONObject result = new JSONObject();
3681
+ result.put("available", true);
3682
+ result.put("disponivel", true);
3683
+ result.put("permission", "android.permission.ACCESS_FINE_LOCATION");
3684
+ result.put("permissionGranted", hasLocationPermission());
3685
+ result.put("latitude", location.getLatitude());
3686
+ result.put("longitude", location.getLongitude());
3687
+ result.put("accuracy", location.hasAccuracy() ? location.getAccuracy() : JSONObject.NULL);
3688
+ result.put("precisao", location.hasAccuracy() ? location.getAccuracy() : JSONObject.NULL);
3689
+ result.put("altitude", location.hasAltitude() ? location.getAltitude() : JSONObject.NULL);
3690
+ result.put("speed", location.hasSpeed() ? location.getSpeed() : JSONObject.NULL);
3691
+ result.put("velocidade", location.hasSpeed() ? location.getSpeed() : JSONObject.NULL);
3692
+ result.put("provider", location.getProvider());
3693
+ result.put("provedor", location.getProvider());
3694
+ result.put("timestamp", location.getTime());
3695
+ result.put("source", source);
3696
+ result.put("fonte", source);
3697
+ result.put("requiresSettings", false);
3698
+ result.put("settingsOpened", false);
3699
+ return result;
3700
+ }
3701
+
3702
+ private JSONObject locationUnavailableResult(String message) throws Exception {
3703
+ JSONObject result = new JSONObject();
3704
+ result.put("available", false);
3705
+ result.put("disponivel", false);
3706
+ result.put("permission", "android.permission.ACCESS_FINE_LOCATION");
3707
+ result.put("permissionGranted", hasLocationPermission());
3708
+ result.put("requiresSettings", true);
3709
+ result.put("settingsOpened", false);
3710
+ result.put("message", message);
3711
+ result.put("mensagem", message);
3712
+ result.put("timestamp", System.currentTimeMillis());
3713
+ return result;
3714
+ }
3715
+
3716
+ private void authenticateBiometric(final JSONObject options, final CallbackContext callbackContext) throws Exception {
3717
+ if (biometricCallback != null) {
3718
+ rejectBusyCallback(callbackContext, "Biometric authentication");
3719
+ return;
3720
+ }
3721
+
3722
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
3723
+ callbackContext.success(biometricUnavailableResult("BiometricPrompt requires Android 9 or newer."));
3724
+ return;
3725
+ }
3726
+
3727
+ KeyguardManager keyguardManager = (KeyguardManager) context().getSystemService(Context.KEYGUARD_SERVICE);
3728
+ if (keyguardManager == null || !keyguardManager.isDeviceSecure()) {
3729
+ callbackContext.success(biometricUnavailableResult("Device secure lock screen is not configured."));
3730
+ return;
3731
+ }
3732
+
3733
+ biometricCallback = callbackContext;
3734
+ biometricCancellationSignal = new CancellationSignal();
3735
+
3736
+ final JSONObject safeOptions = options == null ? new JSONObject() : options;
3737
+ final Activity activity = cordova.getActivity();
3738
+ activity.runOnUiThread(new Runnable() {
3739
+ @Override
3740
+ public void run() {
3741
+ try {
3742
+ Executor executor = ContextCompat.getMainExecutor(activity);
3743
+ BiometricPrompt prompt = new BiometricPrompt.Builder(activity)
3744
+ .setTitle(safeOptions.optString("titulo", safeOptions.optString("title", "Autenticacao")))
3745
+ .setSubtitle(safeOptions.optString("subtitulo", safeOptions.optString("subtitle", "")))
3746
+ .setDescription(safeOptions.optString("descricao", safeOptions.optString("description", "")))
3747
+ .setNegativeButton(
3748
+ safeOptions.optString("cancelar", safeOptions.optString("cancel", "Cancelar")),
3749
+ executor,
3750
+ new android.content.DialogInterface.OnClickListener() {
3751
+ @Override
3752
+ public void onClick(android.content.DialogInterface dialog, int which) {
3753
+ finishBiometric(false, true, "Canceled by user.");
3754
+ }
3755
+ }
3756
+ )
3757
+ .build();
3758
+
3759
+ prompt.authenticate(
3760
+ biometricCancellationSignal,
3761
+ executor,
3762
+ new BiometricPrompt.AuthenticationCallback() {
3763
+ @Override
3764
+ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
3765
+ finishBiometric(true, false, "");
3766
+ }
3767
+
3768
+ @Override
3769
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
3770
+ String message = errString == null ? "" : errString.toString();
3771
+ finishBiometric(false, message.toLowerCase().contains("cancel"), message);
3772
+ }
3773
+
3774
+ @Override
3775
+ public void onAuthenticationFailed() {
3776
+ dispatchEvent("biometria:falhou", baseEvent("biometria:falhou"));
3777
+ }
3778
+ }
3779
+ );
3780
+ } catch (Exception error) {
3781
+ finishBiometric(false, false, error.getMessage());
3782
+ }
3783
+ }
3784
+ });
3785
+ }
3786
+
3787
+ private void finishBiometric(boolean authenticated, boolean canceled, String message) {
3788
+ CallbackContext callback = biometricCallback;
3789
+ biometricCallback = null;
3790
+ biometricCancellationSignal = null;
3791
+ if (callback == null) {
3792
+ return;
3793
+ }
3794
+
3795
+ try {
3796
+ JSONObject result = new JSONObject();
3797
+ result.put("supported", true);
3798
+ result.put("suportado", true);
3799
+ result.put("authenticated", authenticated);
3800
+ result.put("autenticado", authenticated);
3801
+ result.put("canceled", canceled);
3802
+ result.put("cancelado", canceled);
3803
+ result.put("message", message == null ? "" : message);
3804
+ result.put("mensagem", message == null ? "" : message);
3805
+ callback.success(result);
3806
+ } catch (Exception error) {
3807
+ callback.error(error.getMessage());
3808
+ }
3809
+ }
3810
+
3811
+ private JSONObject biometricUnavailableResult(String message) throws Exception {
3812
+ JSONObject result = new JSONObject();
3813
+ result.put("supported", false);
3814
+ result.put("suportado", false);
3815
+ result.put("authenticated", false);
3816
+ result.put("autenticado", false);
3817
+ result.put("requiresSettings", true);
3818
+ result.put("settingsOpened", false);
3819
+ result.put("message", message);
3820
+ result.put("mensagem", message);
3821
+ return result;
3822
+ }
3823
+
3824
+ private void cancelBiometricPrompt() {
3825
+ if (biometricCancellationSignal != null) {
3826
+ try {
3827
+ biometricCancellationSignal.cancel();
3828
+ } catch (Exception ignored) {
3829
+ }
3830
+ }
3831
+ biometricCancellationSignal = null;
3832
+ biometricCallback = null;
3833
+ }
3834
+
1862
3835
  private JSONObject permissionStatus(JSONArray requested) throws Exception {
1863
3836
  JSONArray permissions = requested == null ? new JSONArray() : requested;
1864
3837
  JSONObject result = new JSONObject();