cordova-plugin-mserviceinterface-2.0 1.0.101 → 1.0.103

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.101",
3
+ "version": "1.0.103",
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.101">
3
+ xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-mserviceinterface-2.0" version="1.0.103">
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>
@@ -0,0 +1,59 @@
1
+ package com.selfservit.util;
2
+
3
+ import java.io.File;
4
+ import java.io.FileOutputStream;
5
+ import java.io.IOException;
6
+ import java.io.InputStream;
7
+ import java.io.OutputStream;
8
+
9
+ /** Copies a provider stream into an app-owned file without assuming a filesystem URI. */
10
+ final class AttachmentFileIO {
11
+ private AttachmentFileIO() { }
12
+
13
+ static File copy(InputStream input, File directory, String displayName) throws IOException {
14
+ File outputFile = null;
15
+ boolean complete = false;
16
+ try {
17
+ try (InputStream source = input) {
18
+ if (source == null) {
19
+ throw new IOException("The selected file is no longer available");
20
+ }
21
+ if (!directory.isDirectory() && !directory.mkdirs()) {
22
+ throw new IOException("Cannot create attachment directory");
23
+ }
24
+ String name = displayName == null ? "attachment" : displayName;
25
+ // Provider display names are labels, never paths.
26
+ name = name.replaceAll("[\\\\/\\p{Cntrl}:*?\"<>|]", "_").trim();
27
+ if (name.isEmpty() || name.equals(".") || name.equals("..")) {
28
+ name = "attachment";
29
+ }
30
+ int dot = name.lastIndexOf('.');
31
+ String stem = dot > 0 ? name.substring(0, dot) : name;
32
+ String suffix = dot > 0 ? name.substring(dot) : "";
33
+ // Bound provider-supplied names, retaining normal filename extensions.
34
+ stem = stem.substring(0, Math.min(stem.length(), 80));
35
+ suffix = suffix.substring(0, Math.min(suffix.length(), 20));
36
+ outputFile = File.createTempFile(stem + "_attachment_", suffix, directory);
37
+ try (OutputStream output = new FileOutputStream(outputFile)) {
38
+ byte[] buffer = new byte[8192];
39
+ int count;
40
+ while ((count = source.read(buffer)) != -1) {
41
+ if (count == 0) {
42
+ int next = source.read();
43
+ if (next == -1) break;
44
+ output.write(next);
45
+ } else {
46
+ output.write(buffer, 0, count);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ complete = true;
52
+ } finally {
53
+ if (!complete && outputFile != null && outputFile.exists()) {
54
+ outputFile.delete();
55
+ }
56
+ }
57
+ return outputFile;
58
+ }
59
+ }
@@ -3,14 +3,20 @@ package com.selfservit.util;
3
3
  import android.Manifest;
4
4
  import android.annotation.SuppressLint;
5
5
  import android.app.Activity;
6
+ import android.content.Context;
6
7
  import android.content.Intent;
7
8
  import android.content.pm.PackageManager;
9
+ import android.content.res.Configuration;
10
+ import android.hardware.display.DisplayManager;
8
11
  import android.os.Bundle;
12
+ import android.os.Handler;
13
+ import android.os.Looper;
9
14
  import android.util.Log;
15
+ import android.view.Display;
10
16
  import android.view.MotionEvent;
11
17
  import android.view.ScaleGestureDetector;
18
+ import android.view.Surface;
12
19
  import android.view.View;
13
- import android.view.ViewGroup;
14
20
  import android.widget.ImageButton;
15
21
  import android.widget.Toast;
16
22
 
@@ -27,6 +33,10 @@ import androidx.camera.lifecycle.ProcessCameraProvider;
27
33
  import androidx.camera.view.PreviewView;
28
34
  import androidx.core.app.ActivityCompat;
29
35
  import androidx.core.content.ContextCompat;
36
+ import androidx.core.graphics.Insets;
37
+ import androidx.core.view.ViewCompat;
38
+ import androidx.core.view.WindowCompat;
39
+ import androidx.core.view.WindowInsetsCompat;
30
40
 
31
41
  import com.google.common.util.concurrent.ListenableFuture;
32
42
 
@@ -46,6 +56,26 @@ public class CameraActivity extends AppCompatActivity {
46
56
  private ImageButton torchButton;
47
57
  private ProcessCameraProvider cameraProvider;
48
58
  private Camera camera;
59
+ private View cameraControls;
60
+ private DisplayManager displayManager;
61
+ private int torchOnDrawable;
62
+ private int torchOffDrawable;
63
+
64
+ private final DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener() {
65
+ @Override
66
+ public void onDisplayAdded(int displayId) {}
67
+
68
+ @Override
69
+ public void onDisplayRemoved(int displayId) {}
70
+
71
+ @Override
72
+ public void onDisplayChanged(int displayId) {
73
+ Display display = previewView == null ? null : previewView.getDisplay();
74
+ if (display != null && display.getDisplayId() == displayId) {
75
+ updateCaptureRotation();
76
+ }
77
+ }
78
+ };
49
79
 
50
80
  private int lensFacing = CameraSelector.LENS_FACING_BACK;
51
81
  private boolean isTorchOn = false;
@@ -57,6 +87,17 @@ public class CameraActivity extends AppCompatActivity {
57
87
  protected void onCreate(Bundle savedInstanceState) {
58
88
  super.onCreate(savedInstanceState);
59
89
 
90
+ // Let the system handle predictive Back while preserving the plugin's cancellation result.
91
+ Intent cancelled = new Intent();
92
+ cancelled.putExtra("result", "cancelled");
93
+ setResult(Activity.RESULT_CANCELED, cancelled);
94
+ if (savedInstanceState != null) {
95
+ lensFacing = savedInstanceState.getInt("lensFacing", CameraSelector.LENS_FACING_BACK);
96
+ isTorchOn = savedInstanceState.getBoolean("isTorchOn", false);
97
+ }
98
+ displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
99
+ WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
100
+
60
101
  // Dynamically inflate layout
61
102
  String pkg = getPackageName();
62
103
  int layoutId = getResources().getIdentifier("activity_camera", "layout", pkg);
@@ -68,10 +109,11 @@ public class CameraActivity extends AppCompatActivity {
68
109
  int switchId = getResources().getIdentifier("switchCamera", "id", pkg);
69
110
  int torchId = getResources().getIdentifier("torchButton", "id", pkg);
70
111
  int focusViewId = getResources().getIdentifier("focusView", "id", pkg);
112
+ int controlsId = getResources().getIdentifier("cameraControls", "id", pkg);
71
113
 
72
114
  // Resolve drawable IDs
73
- final int torchOnDrawable = getResources().getIdentifier("ic_flashlight_on", "drawable", pkg);
74
- final int torchOffDrawable = getResources().getIdentifier("ic_flashlight_off", "drawable", pkg);
115
+ torchOnDrawable = getResources().getIdentifier("ic_flashlight_on", "drawable", pkg);
116
+ torchOffDrawable = getResources().getIdentifier("ic_flashlight_off", "drawable", pkg);
75
117
 
76
118
  // Bind views to fields
77
119
  previewView = findViewById(previewId);
@@ -79,6 +121,9 @@ public class CameraActivity extends AppCompatActivity {
79
121
  ImageButton switchButton = findViewById(switchId);
80
122
  torchButton = findViewById(torchId);
81
123
  focusView = findViewById(focusViewId);
124
+ cameraControls = findViewById(controlsId);
125
+ applyControlInsets();
126
+ previewView.post(this::updateCaptureRotation);
82
127
 
83
128
  // Utility to add press animation to any view
84
129
  View.OnTouchListener pressAnim = (v, e) -> {
@@ -185,6 +230,7 @@ public class CameraActivity extends AppCompatActivity {
185
230
  lensFacing = (lensFacing == CameraSelector.LENS_FACING_BACK)
186
231
  ? CameraSelector.LENS_FACING_FRONT
187
232
  : CameraSelector.LENS_FACING_BACK;
233
+ isTorchOn = false;
188
234
  bindUseCases();
189
235
  });
190
236
 
@@ -198,11 +244,69 @@ public class CameraActivity extends AppCompatActivity {
198
244
  });
199
245
  }
200
246
 
247
+ private void applyControlInsets() {
248
+ final int left = cameraControls.getPaddingLeft();
249
+ final int top = cameraControls.getPaddingTop();
250
+ final int right = cameraControls.getPaddingRight();
251
+ final int bottom = cameraControls.getPaddingBottom();
252
+ ViewCompat.setOnApplyWindowInsetsListener(cameraControls, (view, windowInsets) -> {
253
+ Insets safeInsets = windowInsets.getInsets(
254
+ WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
255
+ view.setPadding(left + safeInsets.left, top + safeInsets.top,
256
+ right + safeInsets.right, bottom + safeInsets.bottom);
257
+ return windowInsets;
258
+ });
259
+ cameraControls.post(() -> ViewCompat.requestApplyInsets(cameraControls));
260
+ }
261
+
262
+ @Override
263
+ protected void onStart() {
264
+ super.onStart();
265
+ if (displayManager != null) {
266
+ displayManager.registerDisplayListener(displayListener, new Handler(Looper.getMainLooper()));
267
+ }
268
+ previewView.post(this::updateCaptureRotation);
269
+ }
270
+
271
+ @Override
272
+ protected void onStop() {
273
+ if (displayManager != null) {
274
+ displayManager.unregisterDisplayListener(displayListener);
275
+ }
276
+ super.onStop();
277
+ }
278
+
279
+ @Override
280
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
281
+ super.onConfigurationChanged(newConfig);
282
+ previewView.post(this::updateCaptureRotation);
283
+ ViewCompat.requestApplyInsets(cameraControls);
284
+ }
285
+
286
+ @Override
287
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
288
+ outState.putInt("lensFacing", lensFacing);
289
+ outState.putBoolean("isTorchOn", isTorchOn);
290
+ super.onSaveInstanceState(outState);
291
+ }
292
+
293
+ private int getCaptureRotation() {
294
+ Display display = previewView == null ? null : previewView.getDisplay();
295
+ return display == null ? Surface.ROTATION_0 : display.getRotation();
296
+ }
297
+
298
+ private void updateCaptureRotation() {
299
+ if (imageCapture != null && previewView.getDisplay() != null) {
300
+ imageCapture.setTargetRotation(getCaptureRotation());
301
+ }
302
+ }
303
+
201
304
  private void startCamera() {
202
305
  ListenableFuture<ProcessCameraProvider> future =
203
306
  ProcessCameraProvider.getInstance(this);
204
307
  future.addListener(() -> {
205
308
  try {
309
+ if (isFinishing() || isDestroyed()) return;
206
310
  cameraProvider = future.get();
207
311
  bindUseCases();
208
312
  } catch (ExecutionException | InterruptedException e) {
@@ -224,6 +328,7 @@ public class CameraActivity extends AppCompatActivity {
224
328
  // Image capture use-case
225
329
  imageCapture = new ImageCapture.Builder()
226
330
  .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
331
+ .setTargetRotation(getCaptureRotation())
227
332
  .build();
228
333
 
229
334
  // Select camera
@@ -238,14 +343,17 @@ public class CameraActivity extends AppCompatActivity {
238
343
  boolean hasFlash = camera.getCameraInfo().hasFlashUnit();
239
344
  if (hasFlash) {
240
345
  torchButton.setVisibility(View.VISIBLE);
346
+ camera.getCameraControl().enableTorch(isTorchOn);
241
347
  } else {
242
348
  torchButton.setVisibility(View.GONE);
243
349
  // reset state in case you were mid-toggle
244
350
  isTorchOn = false;
245
351
  }
352
+ torchButton.setImageResource(isTorchOn ? torchOnDrawable : torchOffDrawable);
246
353
  }
247
354
  private void takePhoto() {
248
355
  if (imageCapture == null) return;
356
+ updateCaptureRotation();
249
357
 
250
358
  // Prepare output file
251
359
  File photoFile = new File(getCacheDir(),
@@ -316,13 +424,6 @@ public class CameraActivity extends AppCompatActivity {
316
424
  return true;
317
425
  }
318
426
  @Override
319
- public void onBackPressed() {
320
- Intent intent = new Intent();
321
- intent.putExtra("result", "cancelled");
322
- setResult(Activity.RESULT_CANCELED, intent);
323
- finish();
324
- }
325
- @Override
326
427
  public void onRequestPermissionsResult(int rc, @NonNull String[] perms,
327
428
  @NonNull int[] results) {
328
429
  super.onRequestPermissionsResult(rc, perms, results);
@@ -1,16 +1,19 @@
1
1
  package com.selfservit.util;
2
2
 
3
+ import android.Manifest;
3
4
  import android.app.NotificationChannel;
4
5
  import android.app.NotificationManager;
5
6
  import android.app.PendingIntent;
6
7
  import android.content.BroadcastReceiver;
7
8
  import android.content.Context;
8
9
  import android.content.Intent;
10
+ import android.content.pm.PackageManager;
9
11
  import android.os.Build;
10
12
  import android.util.Log;
11
13
  import android.widget.Toast;
12
14
  import androidx.core.app.NotificationCompat;
13
15
  import androidx.core.app.NotificationManagerCompat;
16
+ import androidx.core.content.ContextCompat;
14
17
 
15
18
  import com.google.android.gms.location.Geofence;
16
19
  import com.google.android.gms.location.GeofencingEvent;
@@ -22,6 +25,7 @@ import java.io.FileOutputStream;
22
25
  import java.io.FileWriter;
23
26
  import java.io.IOException;
24
27
  import java.io.OutputStreamWriter;
28
+ import java.util.List;
25
29
  import java.util.UUID;
26
30
 
27
31
  public class GeofenceBroadcastReceiver extends BroadcastReceiver {
@@ -31,29 +35,76 @@ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
31
35
 
32
36
  @Override
33
37
  public void onReceive(Context context, Intent intent) {
34
- GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
38
+ if (intent == null) {
39
+ Log.w(TAG, "Ignoring null geofence intent");
40
+ return;
41
+ }
42
+ GeofencingEvent geofencingEvent;
43
+ String enterMessage;
44
+ String exitMessage;
45
+ try {
46
+ geofencingEvent = GeofencingEvent.fromIntent(intent);
47
+ enterMessage = intent.getStringExtra("enterMessage");
48
+ exitMessage = intent.getStringExtra("exitMessage");
49
+ } catch (RuntimeException e) {
50
+ Log.e(TAG, "Ignoring malformed geofence intent", e);
51
+ return;
52
+ }
35
53
 
54
+ if (geofencingEvent == null) {
55
+ Log.w(TAG, "Ignoring intent without a geofencing event");
56
+ return;
57
+ }
36
58
  if (geofencingEvent.hasError()) {
37
59
  Log.e(TAG, "GeofencingEvent error: " + geofencingEvent.getErrorCode());
38
60
  return;
39
61
  }
40
- String enterMessage = intent.getStringExtra("enterMessage");
41
- String exitMessage = intent.getStringExtra("exitMessage");
42
62
  int geofenceTransition = geofencingEvent.getGeofenceTransition();
43
63
  if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
44
64
  geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
45
65
  String transitionType = geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ? "enter" : "exit";
66
+ List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
67
+ if (triggeringGeofences == null || triggeringGeofences.isEmpty()) {
68
+ Log.w(TAG, "Ignoring event without triggering geofences");
69
+ return;
70
+ }
46
71
 
47
- for (Geofence geofence : geofencingEvent.getTriggeringGeofences()) {
72
+ for (Geofence geofence : triggeringGeofences) {
73
+ if (geofence == null) {
74
+ Log.w(TAG, "Ignoring null triggering geofence");
75
+ continue;
76
+ }
48
77
  String geofenceId = geofence.getRequestId();
78
+ if (!isValidGeofenceId(geofenceId)) {
79
+ Log.w(TAG, "Ignoring geofence with an invalid request ID");
80
+ continue;
81
+ }
49
82
  writeEventToJsonFile(context, geofenceId, transitionType);
50
- createNotificationChannel(context);
51
83
  String message = (transitionType.equals("enter") ? enterMessage : exitMessage);
52
84
  sendNotification(context, "GeoFence", message);
53
85
  // showToast(context, "Geofence " + transitionType + ": " + geofenceId);
54
86
  }
55
87
  }
56
88
  }
89
+
90
+ private static boolean isValidGeofenceId(String geofenceId) {
91
+ if (geofenceId == null) return false;
92
+ String[] parameters = geofenceId.split("--", -1);
93
+ if (parameters.length < 4) return false;
94
+ for (String parameter : parameters) {
95
+ if (parameter.isEmpty() || parameter.equals(".") || parameter.equals("..")) {
96
+ return false;
97
+ }
98
+ for (int i = 0; i < parameter.length(); i++) {
99
+ char character = parameter.charAt(i);
100
+ if (character == '/' || character == '\\' || Character.isISOControl(character)) {
101
+ return false;
102
+ }
103
+ }
104
+ }
105
+ return true;
106
+ }
107
+
57
108
  private void showToast(Context context, String message) {
58
109
  //Toast.makeText(context.getApplicationContext(), message, Toast.LENGTH_LONG).show();
59
110
  }
@@ -63,18 +114,23 @@ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
63
114
  JSONObject jsonObject = new JSONObject();
64
115
  jsonObject.put("eventName", eventType);
65
116
  jsonObject.put("geofenceID", geofenceId);
66
- String[] parameters = geofenceId.split("--");
67
- fileDirectory = new File(new File(context.getExternalFilesDir(""), "") + "/" + parameters[0] + "/" + parameters[1] + "/" + parameters[2] + "/" + parameters[3] + "/" + "geofence");
68
- if (!fileDirectory.exists()) {
69
- boolean success = fileDirectory.mkdirs();
70
- if (!success) {
71
- System.out.println("Directory not created");
72
- }
73
- };
117
+ String[] parameters = geofenceId.split("--", -1);
118
+ fileDirectory = context.getExternalFilesDir("");
119
+ if (fileDirectory == null) {
120
+ Log.w(TAG, "External app storage is unavailable for geofence event");
121
+ return;
122
+ }
123
+ for (int i = 0; i < 4; i++) {
124
+ fileDirectory = new File(fileDirectory, parameters[i]);
125
+ }
126
+ fileDirectory = new File(fileDirectory, "geofence");
127
+ if (!fileDirectory.isDirectory() && !fileDirectory.mkdirs()) {
128
+ throw new IOException("Unable to create geofence event directory");
129
+ }
74
130
  notificationsFile = new File(fileDirectory, geofenceId + "--" + eventType + ".json");
75
- FileWriter fw = new FileWriter(notificationsFile);
76
- fw.write(mInterfaceUtil.encrypt(jsonObject.toString()));
77
- fw.close();
131
+ try (FileWriter fw = new FileWriter(notificationsFile)) {
132
+ fw.write(mInterfaceUtil.encrypt(jsonObject.toString()));
133
+ }
78
134
  Log.i(TAG, "Event written to file: " + geofenceId + ".json");
79
135
  } catch (Exception e) {
80
136
  Log.e(TAG, "Error writing event to JSON file", e);
@@ -83,6 +139,26 @@ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
83
139
 
84
140
  private void sendNotification(Context context, String title, String message) {
85
141
  try {
142
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
143
+ && ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
144
+ != PackageManager.PERMISSION_GRANTED) {
145
+ Log.i(TAG, "Skipping geofence notification; notification permission is not granted");
146
+ return;
147
+ }
148
+ if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) {
149
+ Log.i(TAG, "Skipping geofence notification; notifications are disabled");
150
+ return;
151
+ }
152
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
153
+ if (notificationManager == null) return;
154
+ createNotificationChannel(context);
155
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
156
+ NotificationChannel channel = notificationManager.getNotificationChannel(CHANNEL_ID);
157
+ if (channel != null && channel.getImportance() == NotificationManager.IMPORTANCE_NONE) {
158
+ Log.i(TAG, "Skipping geofence notification; notification channel is disabled");
159
+ return;
160
+ }
161
+ }
86
162
  String notification_icon, notification_color, app_name;
87
163
  int imageResource,notification_color_id;
88
164
  String packageName = context.getPackageName();
@@ -104,15 +180,17 @@ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
104
180
  .setAutoCancel(true)
105
181
  .setColor(context.getResources().getColor(notification_color_id))
106
182
  .setContentIntent(pendingIntent);
107
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
108
183
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
109
184
  NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT);
110
185
  notificationManager.createNotificationChannel(channel);
111
- };
186
+ }
112
187
  int notificationId = UUID.randomUUID().hashCode();
113
188
  notificationManager.notify(notificationId, notificationBuilder.build());
114
189
  } catch (ClassNotFoundException e) {
115
- throw new RuntimeException(e);
190
+ Log.e(TAG, "Unable to open the app from geofence notification", e);
191
+ } catch (RuntimeException e) {
192
+ // Permission or notification settings can change after the checks above.
193
+ Log.e(TAG, "Unable to show geofence notification", e);
116
194
  }
117
195
  }
118
196
 
@@ -124,7 +202,9 @@ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
124
202
  NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
125
203
  channel.setDescription(description);
126
204
  NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
127
- notificationManager.createNotificationChannel(channel);
205
+ if (notificationManager != null) {
206
+ notificationManager.createNotificationChannel(channel);
207
+ }
128
208
  }
129
209
  }
130
210
  }