cordova-plugin-mserviceinterface-2.0 1.0.103 → 1.0.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-mserviceinterface-2.0",
3
- "version": "1.0.103",
3
+ "version": "1.0.104",
4
4
  "description": "This plugin establishes the connection to the native interface and performs the requested task.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/plugin.xml CHANGED
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
3
- xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-mserviceinterface-2.0" version="1.0.103">
3
+ xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-mserviceinterface-2.0" version="1.0.104">
4
4
  <name>mserviceinterface-2.0</name>
5
5
  <description>This plugin establishes the connection to the native interface and performs the requested task.</description>
6
6
  <author>selfservit@gmail.com</author>
@@ -46,6 +46,7 @@
46
46
  <config-file target="AndroidManifest.xml" parent="/manifest">
47
47
  <uses-feature android:name="android.hardware.camera" android:required="true" />
48
48
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
49
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
49
50
  <uses-permission android:name="android.permission.INTERNET" />
50
51
  <uses-permission android:name="android.permission.CAMERA" />
51
52
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -63,23 +64,20 @@
63
64
  </queries>
64
65
  </config-file>
65
66
  <config-file target="AndroidManifest.xml" parent="/manifest/application">
66
- <activity android:name="com.selfservit.util.CameraActivity" android:configChanges="orientation|screenSize" android:screenOrientation="portrait" android:exported="true" android:theme="@style/CameraTheme" />
67
- <service android:name="com.selfservit.util.mInterfaceService" android:foregroundServiceType="location" android:enabled="true" android:exported="true"/>
67
+ <activity android:name="com.selfservit.util.CameraActivity" android:configChanges="orientation|screenSize" android:screenOrientation="portrait" android:exported="false" android:theme="@style/CameraTheme" />
68
+ <service android:name="com.selfservit.util.mInterfaceService" android:foregroundServiceType="location" android:enabled="true" android:exported="false"/>
68
69
  <receiver android:name="com.selfservit.util.mInterfaceReceiver" android:enabled="true" android:exported="true">
69
70
  <intent-filter>
70
71
  <action android:name="android.intent.action.BOOT_COMPLETED"/>
71
- <action android:name="com.example.checkservice" />
72
72
  </intent-filter>
73
73
  </receiver>
74
- <receiver android:enabled="true" android:exported="true" android:name="com.selfservit.util.GeofenceBroadcastReceiver">
75
- <intent-filter>
76
- <action android:name="com.google.android.gms.location.GeofenceEvent" />
77
- </intent-filter>
74
+ <receiver android:enabled="true" android:exported="false" android:name="com.selfservit.util.GeofenceBroadcastReceiver">
78
75
  </receiver>
79
76
  <provider android:name="com.selfservit.util.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true">
80
77
  <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/opener_paths" />
81
78
  </provider>
82
79
  </config-file>
80
+ <source-file src="src/android/AttachmentFileIO.java" target-dir="src/com/selfservit/util/" />
83
81
  <source-file src="src/android/mInterfaceService.java" target-dir="src/com/selfservit/util/" />
84
82
  <source-file src="src/android/mInterfaceReceiver.java" target-dir="src/com/selfservit/util/" />
85
83
  <source-file src="src/android/mInterface.java" target-dir="src/com/selfservit/util/" />
@@ -166,6 +166,58 @@ function setActivityAttribute(openingTag, attributeName, attributeValue) {
166
166
  );
167
167
  }
168
168
 
169
+ function isMatchingAndroidAppLink(filter, configuration) {
170
+ function attributes(tag) {
171
+ var values = {};
172
+ var pattern = /([\w:.-]+)\s*=\s*(["'])([\s\S]*?)\2/g;
173
+ var match;
174
+ while ((match = pattern.exec(tag)) !== null) {
175
+ values[match[1]] = match[3].replace(
176
+ /&(#x[0-9a-f]+|#\d+|amp|quot|apos|lt|gt);/gi,
177
+ function (entity, value) {
178
+ if (value.charAt(0) === "#") {
179
+ return String.fromCodePoint(value.charAt(1).toLowerCase() === "x"
180
+ ? parseInt(value.slice(2), 16) : parseInt(value.slice(1), 10));
181
+ }
182
+ return {amp: "&", quot: '"', apos: "'", lt: "<", gt: ">"}[value];
183
+ }
184
+ );
185
+ }
186
+ return values;
187
+ }
188
+
189
+ var content = filter.replace(/<!--[\s\S]*?-->/g, "");
190
+ var opening = content.match(/<intent-filter\b[^>]*>/i);
191
+ var filterAttributes = opening ? attributes(opening[0]) : {};
192
+ // Preserve filters with extra behavior, such as priority or additional data.
193
+ if (Object.keys(filterAttributes).some(function (key) { return key !== "android:autoVerify"; })) {
194
+ return false;
195
+ }
196
+ var body = content.replace(/^\s*<intent-filter\b[^>]*>/i, "").replace(/<\/intent-filter>\s*$/i, "");
197
+ var tags = body.match(/<(?:action|category|data)\b[^>]*\/\s*>/gi) || [];
198
+ if (tags.length !== 4 || body.replace(/<(?:action|category|data)\b[^>]*\/\s*>/gi, "").trim() !== "") {
199
+ return false;
200
+ }
201
+ var actions = [], categories = [], data = [];
202
+ tags.forEach(function (tag) {
203
+ var values = attributes(tag);
204
+ if (/^<data\b/i.test(tag)) {
205
+ data.push(values);
206
+ } else if (Object.keys(values).length === 1 && values["android:name"]) {
207
+ if (/^<action\b/i.test(tag)) actions.push(values["android:name"]);
208
+ else categories.push(values["android:name"]);
209
+ }
210
+ });
211
+ return actions.length === 1 && actions[0] === "android.intent.action.VIEW"
212
+ && categories.length === 2
213
+ && categories.indexOf("android.intent.category.DEFAULT") !== -1
214
+ && categories.indexOf("android.intent.category.BROWSABLE") !== -1
215
+ && data.length === 1 && Object.keys(data[0]).length === 3
216
+ && data[0]["android:scheme"] === configuration.scheme
217
+ && data[0]["android:host"] === configuration.host
218
+ && data[0]["android:path"] === configuration.path;
219
+ }
220
+
169
221
  function configureAndroidAppLink(projectRoot, configuration) {
170
222
  var manifestPath;
171
223
  var manifestContent;
@@ -243,22 +295,21 @@ function configureAndroidAppLink(projectRoot, configuration) {
243
295
  " <!-- MService App Link - END -->\n";
244
296
 
245
297
  markerPattern =
246
- /\s*<!-- MService App Link - START -->[\s\S]*?<!-- MService App Link - END -->\s*/i;
247
-
248
- /*
249
- * Replace an existing hook-generated filter when configuration changes.
250
- */
251
- if (markerPattern.test(updatedActivityBlock)) {
252
- updatedActivityBlock = updatedActivityBlock.replace(
253
- markerPattern,
254
- intentFilter
255
- );
256
- } else {
257
- updatedActivityBlock = updatedActivityBlock.replace(
258
- /<\/activity>\s*$/i,
259
- intentFilter + " </activity>"
260
- );
261
- }
298
+ /\s*<!-- MService App Link - START -->[\s\S]*?<!-- MService App Link - END -->\s*/gi;
299
+
300
+ // Cordova can discard comments when rewriting XML. Remove both marked
301
+ // blocks and equivalent unmarked filters before inserting one definition.
302
+ updatedActivityBlock = updatedActivityBlock.replace(markerPattern, "\n");
303
+ updatedActivityBlock = updatedActivityBlock.replace(
304
+ /\s*<intent-filter\b[^>]*>[\s\S]*?<\/intent-filter>/gi,
305
+ function (filter) {
306
+ return isMatchingAndroidAppLink(filter, configuration) ? "" : filter;
307
+ }
308
+ );
309
+ updatedActivityBlock = updatedActivityBlock.replace(
310
+ /\s*<\/activity>\s*$/i,
311
+ intentFilter + " </activity>"
312
+ );
262
313
 
263
314
  manifestContent = manifestContent.replace(
264
315
  activityPattern,
@@ -180,10 +180,6 @@ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
180
180
  .setAutoCancel(true)
181
181
  .setColor(context.getResources().getColor(notification_color_id))
182
182
  .setContentIntent(pendingIntent);
183
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
184
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT);
185
- notificationManager.createNotificationChannel(channel);
186
- }
187
183
  int notificationId = UUID.randomUUID().hashCode();
188
184
  notificationManager.notify(notificationId, notificationBuilder.build());
189
185
  } catch (ClassNotFoundException e) {
@@ -3,8 +3,4 @@ android {
3
3
  multiDexEnabled true
4
4
 
5
5
  }
6
- compileOptions {
7
- sourceCompatibility JavaVersion.VERSION_1_8
8
- targetCompatibility JavaVersion.VERSION_1_8
9
- }
10
6
  }
@@ -26,6 +26,7 @@ import android.graphics.Color;
26
26
  import android.graphics.Matrix;
27
27
  import android.graphics.Paint;
28
28
  import android.location.Location;
29
+ import android.location.LocationManager;
29
30
  import android.media.MediaMetadataRetriever;
30
31
  import android.media.ThumbnailUtils;
31
32
  import android.net.ConnectivityManager;
@@ -35,6 +36,9 @@ import android.os.AsyncTask;
35
36
  import android.os.Build;
36
37
  import android.os.Bundle;
37
38
  import android.os.Environment;
39
+ import android.os.Handler;
40
+ import android.os.Looper;
41
+ import android.os.SystemClock;
38
42
  import android.provider.MediaStore;
39
43
  import android.provider.OpenableColumns;
40
44
  import android.provider.Settings;
@@ -50,9 +54,13 @@ import androidx.annotation.NonNull;
50
54
  import androidx.annotation.Nullable;
51
55
  import androidx.annotation.RequiresApi;
52
56
  import androidx.core.app.ActivityCompat;
57
+ import androidx.core.location.LocationManagerCompat;
53
58
  import androidx.exifinterface.media.ExifInterface;
54
59
 
55
60
  import com.google.android.gms.common.ConnectionResult;
61
+ import com.google.android.gms.common.GoogleApiAvailability;
62
+ import com.google.android.gms.location.CurrentLocationRequest;
63
+ import com.google.android.gms.tasks.CancellationTokenSource;
56
64
  import com.google.android.gms.common.api.GoogleApiClient;
57
65
  import com.google.android.gms.common.api.PendingResult;
58
66
  import com.google.android.gms.common.api.ResultCallback;
@@ -140,7 +148,6 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
140
148
  private static final String PORTRAIT = "portrait";
141
149
  private static final String LANDSCAPE = "landscape";
142
150
  private static final String ANDROID_PLATFORM = "Android";
143
- String currentLocationInd = "";
144
151
  Location mCurrentLocation1;
145
152
  LocationRequest mLocationRequest1;
146
153
  GoogleApiClient mGoogleApiClient1;
@@ -174,6 +181,8 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
174
181
 
175
182
  @Override
176
183
  public void onDestroy() {
184
+ locationPluginDestroyed = true;
185
+ cancelCurrentLocationRequest();
177
186
  mInterfaceService.stopIfNotTracking(cordova.getActivity());
178
187
  if (mGoogleApiClient1 != null) {
179
188
  if (mGoogleApiClient1.isConnected()) {
@@ -184,6 +193,12 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
184
193
  super.onDestroy();
185
194
  }
186
195
 
196
+ @Override
197
+ public void onReset() {
198
+ cancelCurrentLocationRequest();
199
+ super.onReset();
200
+ }
201
+
187
202
  @RequiresApi(api = Build.VERSION_CODES.O)
188
203
  @Override
189
204
  public boolean execute(final String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
@@ -489,6 +504,10 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
489
504
  String str = "false";
490
505
  if (isServiceRunning(mInterfaceService.class)) {
491
506
  str = "true";
507
+ // Legacy JS skips StartService when this returns true.
508
+ // Recheck an existing queue-only/tracking service here,
509
+ // preserving the callback contract and eligibility guards.
510
+ mInterfaceService.start(cordova.getActivity());
492
511
  System.out.println("service check is: " + str + " " + isServiceRunning(mInterfaceService.class));
493
512
  }
494
513
  callbackContext.success(str);
@@ -622,19 +641,8 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
622
641
  callbackContext.error(e.getMessage());
623
642
  }
624
643
  } else if ("GetCurrentLocation".equals(action)) {
625
- try {
626
- currentLocationInd = "true";
627
- callback = callbackContext;
628
- createLocationRequest1();
629
- this.mGoogleApiClient1 = new GoogleApiClient.Builder(cordova.getActivity())
630
- .addConnectionCallbacks(this)
631
- .addOnConnectionFailedListener(this)
632
- .addApi(LocationServices.API)
633
- .build();
634
- mGoogleApiClient1.connect();
635
- } catch (Exception e) {
636
- callbackContext.error(e.getMessage());
637
- }
644
+ getCurrentLocation(callbackContext);
645
+ return true;
638
646
  } else if (action.equals("GetSyncIndicator")) {
639
647
  try {
640
648
  File newQueueFolder, oldQueueFolder;
@@ -1494,6 +1502,122 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
1494
1502
  return false;
1495
1503
  }
1496
1504
 
1505
+ private static final long CURRENT_LOCATION_TIMEOUT_MS = 10_000L;
1506
+ private static final String CURRENT_LOCATION_TIMEOUT_ERROR = "Unable to obtain current location within 10 seconds";
1507
+ private final Handler currentLocationHandler = new Handler(Looper.getMainLooper());
1508
+ private CurrentLocationOperation currentLocationOperation;
1509
+ private volatile boolean locationPluginDestroyed;
1510
+
1511
+ private void getCurrentLocation(final CallbackContext resultCallback) {
1512
+ final long deadline = SystemClock.elapsedRealtime() + CURRENT_LOCATION_TIMEOUT_MS;
1513
+ currentLocationHandler.post(() -> {
1514
+ if (locationPluginDestroyed) {
1515
+ resultCallback.error("Current location request cancelled: plugin destroyed");
1516
+ return;
1517
+ }
1518
+ if (currentLocationOperation != null) {
1519
+ resultCallback.error("A current location request is already in progress");
1520
+ return;
1521
+ }
1522
+ CurrentLocationOperation operation = new CurrentLocationOperation(resultCallback, deadline);
1523
+ currentLocationOperation = operation;
1524
+ operation.start();
1525
+ });
1526
+ }
1527
+
1528
+ private void cancelCurrentLocationRequest() {
1529
+ currentLocationHandler.post(() -> {
1530
+ if (currentLocationOperation != null) {
1531
+ currentLocationOperation.finish(null, "Current location request cancelled");
1532
+ }
1533
+ });
1534
+ }
1535
+
1536
+ // All request state and completion listeners run on the main looper. This
1537
+ // callback is separate from the shared camera/scanner/GPS-settings callback.
1538
+ private final class CurrentLocationOperation {
1539
+ private final CallbackContext resultCallback;
1540
+ private final long deadline;
1541
+ private final CancellationTokenSource cancellation = new CancellationTokenSource();
1542
+ private final Runnable timeout = () -> finish(null, CURRENT_LOCATION_TIMEOUT_ERROR);
1543
+ private boolean completed;
1544
+
1545
+ CurrentLocationOperation(CallbackContext resultCallback, long deadline) {
1546
+ this.resultCallback = resultCallback;
1547
+ this.deadline = deadline;
1548
+ }
1549
+
1550
+ void start() {
1551
+ long remaining = deadline - SystemClock.elapsedRealtime();
1552
+ if (remaining <= 0) {
1553
+ finish(null, CURRENT_LOCATION_TIMEOUT_ERROR);
1554
+ return;
1555
+ }
1556
+ currentLocationHandler.postDelayed(timeout, remaining);
1557
+ try {
1558
+ Context context = cordova.getActivity().getApplicationContext();
1559
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
1560
+ && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
1561
+ finish(null, "Location permission not granted");
1562
+ return;
1563
+ }
1564
+ LocationManager manager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
1565
+ if (manager == null || !LocationManagerCompat.isLocationEnabled(manager)) {
1566
+ finish(null, "Device location is disabled");
1567
+ return;
1568
+ }
1569
+ if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
1570
+ finish(null, "Google Play services is unavailable");
1571
+ return;
1572
+ }
1573
+ remaining = deadline - SystemClock.elapsedRealtime();
1574
+ if (remaining <= 0) {
1575
+ finish(null, CURRENT_LOCATION_TIMEOUT_ERROR);
1576
+ return;
1577
+ }
1578
+ CurrentLocationRequest request = new CurrentLocationRequest.Builder()
1579
+ .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
1580
+ .setMaxUpdateAgeMillis(0)
1581
+ .setDurationMillis(remaining)
1582
+ .build();
1583
+ // No network precondition: a satellite fix can be available offline.
1584
+ LocationServices.getFusedLocationProviderClient(context)
1585
+ .getCurrentLocation(request, cancellation.getToken())
1586
+ .addOnSuccessListener(location -> finish(location,
1587
+ location == null ? "Current location is unavailable" : null))
1588
+ .addOnFailureListener(error -> finish(null,
1589
+ "Unable to obtain current location: " + error.getMessage()))
1590
+ .addOnCanceledListener(() -> finish(null, "Current location request cancelled"));
1591
+ } catch (Exception error) {
1592
+ finish(null, "Unable to obtain current location: " + error.getMessage());
1593
+ }
1594
+ }
1595
+
1596
+ void finish(Location location, String error) {
1597
+ if (completed) return;
1598
+ completed = true;
1599
+ currentLocationHandler.removeCallbacks(timeout);
1600
+ if (currentLocationOperation == this) currentLocationOperation = null;
1601
+ // Cancels outstanding acquisition on timeout/reset and releases the
1602
+ // one-shot request on every terminal path. Late results are ignored.
1603
+ try {
1604
+ cancellation.cancel();
1605
+ } catch (RuntimeException cancellationError) {
1606
+ Log.w(TAG, "Unable to cancel current location acquisition", cancellationError);
1607
+ }
1608
+ if (SystemClock.elapsedRealtime() >= deadline) {
1609
+ location = null;
1610
+ error = CURRENT_LOCATION_TIMEOUT_ERROR;
1611
+ }
1612
+ if (location != null) {
1613
+ resultCallback.success(location.getLatitude() + "," + location.getLongitude());
1614
+ } else {
1615
+ resultCallback.error(error);
1616
+ }
1617
+ }
1618
+ }
1619
+
1620
+
1497
1621
  public void createLocationRequest1() {
1498
1622
  try {
1499
1623
  mLocationRequest1 = new LocationRequest();
@@ -2585,36 +2709,31 @@ public class mInterface extends CordovaPlugin implements com.google.android.gms.
2585
2709
  final Status status = locationSettingsResult.getStatus();
2586
2710
  switch (status.getStatusCode()) {
2587
2711
  case LocationSettingsStatusCodes.SUCCESS:
2712
+ callback.success();
2588
2713
  break;
2589
2714
  case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
2590
2715
  try {
2591
2716
  this.cordova.setActivityResultCallback(this);
2592
2717
  status.startResolutionForResult(this.cordova.getActivity(), LOCATION_ACCURACY_REQUEST_NEW);
2593
2718
  } catch (IntentSender.SendIntentException e) {
2594
- System.out.println("PendingIntent unable to execute request: ");
2719
+ callback.error("Unable to open location settings: " + e.getMessage());
2595
2720
  }
2596
2721
  break;
2597
2722
  case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
2598
- System.out.println("Location settings are inadequate, and cannot be fixed here. Dialog not created.");
2723
+ callback.error("Location settings cannot be changed on this device");
2724
+ break;
2725
+ default:
2726
+ callback.error("Unable to check location settings: " + status.getStatusCode());
2599
2727
  break;
2600
2728
  }
2601
2729
  } catch (Exception e) {
2602
- System.out.println("LocationSettingsResult failed, exception is : " + e.getMessage());
2730
+ callback.error("LocationSettingsResult failed, exception is : " + e.getMessage());
2603
2731
  }
2604
2732
  }
2605
2733
 
2606
2734
  @Override
2607
2735
  public void onLocationChanged(@NonNull Location location) {
2608
2736
  mCurrentLocation1 = location;
2609
- try {
2610
- System.out.println(mCurrentLocation1.getLatitude() + "," + mCurrentLocation1.getLongitude());
2611
- if (currentLocationInd.equals("true")) {
2612
- currentLocationInd = "";
2613
- callback.success(mCurrentLocation1.getLatitude() + "," + mCurrentLocation1.getLongitude());
2614
- }
2615
- } catch (Exception e) {
2616
- callback.error("onLocationChanged failed, exception is : " + e.getMessage());
2617
- }
2618
2737
  }
2619
2738
 
2620
2739
  private class DownloadTask extends AsyncTask<String, Integer, String> {
@@ -838,133 +838,164 @@ public class mInterfaceUtil {
838
838
  return imgString;
839
839
  }
840
840
 
841
- public void sendChatAttachment(String filePath, String fileName, String requestURL, Context context, CallbackContext callbackContext) {
842
- try {
843
- class SendFile extends AsyncTask<String, String, String> {
844
- @Override
845
- protected void onPreExecute() {
846
- }
841
+ private static final int CHAT_UPLOAD_CONNECT_TIMEOUT_MS = 30_000;
842
+ private static final int CHAT_UPLOAD_READ_TIMEOUT_MS = 60_000;
843
+ private static final long CHAT_UPLOAD_TIMEOUT_MS = 5 * 60_000L;
847
844
 
848
- @Override
849
- protected String doInBackground(String... args) {
850
- FileInputStream fileInputStream;
851
- HttpURLConnection urlConObj;
852
- URL requestPath;
853
- int bytesAvailable,
854
- bufferSize;
855
- int maxBufferSize = 1 * 1024 * 1024;
856
- DataOutputStream dos;
857
- String lineEnd = "\r\n";
858
- String twoHyphens = "--";
859
- String boundary = "*****";
860
- StringBuilder serverResponseObj;
861
- BufferedReader readerObj;
862
- String currentLine,
863
- receiveData = "", statusIndication = null;
864
- int progressCheck = 10;
865
- try {
866
- if (new File(filePath).exists()) {
867
- if (new File(filePath).exists()) {
868
- fileInputStream = new FileInputStream(new File(filePath));
869
-
870
- requestPath = new URL((requestURL).replaceAll(" ", "%20"));
871
- urlConObj = (HttpURLConnection) requestPath.openConnection();
872
- urlConObj.setDoInput(true); // Allow Inputs
873
- urlConObj.setDoOutput(true); // Allow Outputs
874
- urlConObj.setUseCaches(false); // Don't use a Cached Copy
875
- urlConObj.setRequestMethod("POST");
876
- urlConObj.setRequestProperty("Connection", "Keep-Alive");
877
- urlConObj.setRequestProperty("ENCTYPE", "multipart/form-data");
878
- urlConObj.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
879
- urlConObj.setRequestProperty("uploaded_file", fileName);
880
-
881
- dos = new DataOutputStream(urlConObj.getOutputStream());
882
- dos.writeBytes(twoHyphens + boundary + lineEnd);
883
- dos.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\";filename=\""
884
- + fileName + "\"" + lineEnd);
885
- dos.writeBytes(lineEnd);
886
- // create a buffer of maximum size
887
- bytesAvailable = fileInputStream.available();
888
-
889
- bufferSize = Math.min(bytesAvailable, maxBufferSize);
890
-
891
- int progress = 0;
892
- int progressToSend = 0;
893
- int fileLength = (int) new File(filePath).length();
894
- int bytesReadValue = 0;
895
- byte[] buf = new byte[1024];
896
- BufferedInputStream bufInput = new BufferedInputStream(new FileInputStream(new File(filePath)));
897
- while ((bytesReadValue = bufInput.read(buf)) != -1) {
898
- // write output
899
- dos.write(buf, 0, bytesReadValue);
900
- progress += bytesReadValue;
901
- progressToSend = (int) ((progress * 100) / fileLength);
902
-
903
- if (progressToSend >= progressCheck) {
904
- JSONObject progressObj = new JSONObject();
905
- progressObj.put("progress", progressCheck);
906
- progressObj.put("fileLength", fileLength);
907
- PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, progressObj);
908
- pluginResult.setKeepCallback(true);
909
- callbackContext.sendPluginResult(pluginResult);
910
- progressCheck = progressCheck + 10;
911
- }
912
- }
845
+ public void sendChatAttachment(String filePath, String fileName, String requestURL, Context context, CallbackContext callbackContext) {
846
+ new ChatAttachmentUpload(filePath, fileName, requestURL, callbackContext).start();
847
+ }
913
848
 
914
- // send multipart form data necesssary after file data...
915
- dos.writeBytes(lineEnd);
916
- dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
917
- urlConObj.getResponseCode();
918
- //close the streams //
919
- fileInputStream.close();
920
- dos.flush();
921
- dos.close();
922
- readerObj = new BufferedReader(new InputStreamReader(urlConObj.getInputStream()));
923
- currentLine = readerObj.readLine();
924
- readerObj.close();
925
- receiveData = currentLine;
926
- urlConObj.disconnect();
927
- JSONObject resultObj = new JSONObject(receiveData);
928
- if (resultObj.getString("status").equalsIgnoreCase("success")) {
929
- statusIndication = "true";
930
- } else {
931
- statusIndication = "false";
932
- }
933
- urlConObj.disconnect();
934
- }
935
- }
849
+ // Own the callback for the whole request, including progress, timeout and cleanup.
850
+ private static final class ChatAttachmentUpload implements Runnable {
851
+ private final String filePath;
852
+ private final String fileName;
853
+ private final String requestURL;
854
+ private final CallbackContext callback;
855
+ private volatile boolean finished;
856
+ private volatile HttpURLConnection connection;
857
+ private java.util.Timer timeout;
858
+ private long startedAt;
859
+
860
+ ChatAttachmentUpload(String filePath, String fileName, String requestURL, CallbackContext callback) {
861
+ this.filePath = filePath;
862
+ this.fileName = fileName;
863
+ this.requestURL = requestURL;
864
+ this.callback = callback;
865
+ }
936
866
 
937
- } catch (Exception e) {
938
- e.printStackTrace();
867
+ void start() {
868
+ try {
869
+ startedAt = android.os.SystemClock.elapsedRealtime();
870
+ timeout = new java.util.Timer("ChatAttachmentTimeout", true);
871
+ timeout.schedule(new java.util.TimerTask() {
872
+ @Override public void run() {
873
+ finish(false, "request timed out");
874
+ // Timer runs off the UI thread; disconnect can block on some devices.
875
+ disconnect();
939
876
  }
940
- return statusIndication;
877
+ }, CHAT_UPLOAD_TIMEOUT_MS);
878
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(this);
879
+ } catch (Exception e) {
880
+ finish(false, "could not start upload");
881
+ }
882
+ }
883
+
884
+ private synchronized void finish(boolean success, String reason) {
885
+ if (finished) return;
886
+ if (android.os.SystemClock.elapsedRealtime() - startedAt >= CHAT_UPLOAD_TIMEOUT_MS) {
887
+ success = false;
888
+ reason = "request timed out";
889
+ }
890
+ finished = true;
891
+ if (timeout != null) timeout.cancel();
892
+ // Do not log the URL, local path, file contents or server response.
893
+ if (!success) Log.w("mInterfaceChatUpload", reason);
894
+ if (success) callback.success("success");
895
+ else callback.error("failure");
896
+ }
897
+
898
+ private synchronized void progress(int percent, long length) throws JSONException {
899
+ if (finished) return;
900
+ JSONObject value = new JSONObject();
901
+ value.put("progress", percent);
902
+ value.put("fileLength", length);
903
+ PluginResult result = new PluginResult(PluginResult.Status.OK, value);
904
+ result.setKeepCallback(true);
905
+ callback.sendPluginResult(result);
906
+ }
941
907
 
908
+ private void disconnect() {
909
+ HttpURLConnection active = connection;
910
+ if (active != null) {
911
+ try {
912
+ active.disconnect();
913
+ } catch (Exception ignored) {
914
+ // Cleanup must never prevent delivery of the terminal callback.
942
915
  }
916
+ }
917
+ }
943
918
 
944
- public void onPostExecute(String statusIndication) {
945
- try {
946
- if (statusIndication == null) {
947
- callbackContext.error("failure");
948
- } else {
949
- if (statusIndication.equals("true")) {
950
- callbackContext.success("success");
951
- } else {
952
- callbackContext.error("failure");
953
- }
919
+ @Override public void run() {
920
+ try {
921
+ if (finished) return;
922
+ File file = new File(filePath);
923
+ if (!file.isFile() || !file.canRead()) {
924
+ finish(false, "attachment is not readable");
925
+ return;
926
+ }
927
+ String boundary = "*****";
928
+ byte[] header = ("--" + boundary + "\r\n"
929
+ + "Content-Disposition: form-data; name=\"uploaded_file\";filename=\""
930
+ + fileName.replace("\r", "").replace("\n", "").replace("\"", "_") + "\"\r\n\r\n")
931
+ .getBytes(StandardCharsets.UTF_8);
932
+ byte[] footer = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8);
933
+ long length = file.length();
934
+ HttpURLConnection active = (HttpURLConnection) new URL(requestURL.replace(" ", "%20")).openConnection();
935
+ connection = active;
936
+ if (finished) return;
937
+ active.setConnectTimeout(CHAT_UPLOAD_CONNECT_TIMEOUT_MS);
938
+ active.setReadTimeout(CHAT_UPLOAD_READ_TIMEOUT_MS);
939
+ active.setDoInput(true);
940
+ active.setDoOutput(true);
941
+ active.setUseCaches(false);
942
+ active.setRequestMethod("POST");
943
+ active.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
944
+ active.setRequestProperty("uploaded_file", fileName);
945
+
946
+ // Finish and close the request body BEFORE asking for the HTTP response.
947
+ try (InputStream input = new BufferedInputStream(new FileInputStream(file));
948
+ OutputStream output = active.getOutputStream()) {
949
+ output.write(header);
950
+ byte[] buffer = new byte[16 * 1024];
951
+ long sent = 0;
952
+ int count;
953
+ int nextProgress = 10;
954
+ while ((count = input.read(buffer)) != -1) {
955
+ if (finished) return;
956
+ output.write(buffer, 0, count);
957
+ sent += count;
958
+ int percent = length == 0 ? 100 : (int) (sent * 100.0 / length);
959
+ while (nextProgress <= 100 && percent >= nextProgress) {
960
+ progress(nextProgress, length);
961
+ nextProgress += 10;
954
962
  }
955
- } catch (Exception e) {
956
- e.printStackTrace();
957
963
  }
964
+ if (finished) return;
965
+ output.write(footer);
966
+ output.flush();
967
+ }
958
968
 
969
+ if (finished) return;
970
+ int status = active.getResponseCode();
971
+ if (status < 200 || status >= 300) {
972
+ finish(false, "HTTP " + status);
973
+ return;
974
+ }
975
+ StringBuilder response = new StringBuilder();
976
+ try (InputStreamReader reader = new InputStreamReader(active.getInputStream(), StandardCharsets.UTF_8)) {
977
+ char[] buffer = new char[4096];
978
+ int count;
979
+ while ((count = reader.read(buffer)) != -1) {
980
+ if (finished) return;
981
+ if (response.length() + count > 1024 * 1024) {
982
+ throw new IOException("Upload response too large");
983
+ }
984
+ response.append(buffer, 0, count);
985
+ }
959
986
  }
987
+ String body = response.toString().trim();
988
+ if (body.startsWith("\uFEFF")) body = body.substring(1).trim();
989
+ boolean success = "success".equalsIgnoreCase(new JSONObject(body).optString("status"));
990
+ finish(success, "server did not confirm success");
991
+ } catch (Exception e) {
992
+ finish(false, "upload failed: " + e.getClass().getSimpleName());
993
+ } finally {
994
+ disconnect();
960
995
  }
961
- new SendFile().execute();
962
- } catch (Exception e) {
963
- e.printStackTrace();
964
996
  }
965
997
  }
966
998
 
967
-
968
999
  public String sendAttachmentToServer(String filePath, String fileName, String requestURL) {
969
1000
  String statusIndication = "false";
970
1001
  try {
@@ -22,6 +22,16 @@
22
22
  app:layout_constraintTop_toTopOf="parent"
23
23
  app:layout_constraintStart_toStartOf="parent"/>
24
24
 
25
+ <!-- Only the controls receive system-bar/cutout padding; the preview fills the window. -->
26
+ <androidx.constraintlayout.widget.ConstraintLayout
27
+ android:id="@+id/cameraControls"
28
+ android:layout_width="0dp"
29
+ android:layout_height="0dp"
30
+ app:layout_constraintTop_toTopOf="parent"
31
+ app:layout_constraintBottom_toBottomOf="parent"
32
+ app:layout_constraintStart_toStartOf="parent"
33
+ app:layout_constraintEnd_toEndOf="parent">
34
+
25
35
  <ImageButton
26
36
  android:id="@+id/torchButton"
27
37
  android:layout_width="56dp"
@@ -61,4 +71,6 @@
61
71
  app:layout_constraintEnd_toEndOf="parent"
62
72
  app:layout_constraintStart_toEndOf="@+id/captureButton" />
63
73
 
74
+ </androidx.constraintlayout.widget.ConstraintLayout>
75
+
64
76
  </androidx.constraintlayout.widget.ConstraintLayout>