cordova-plugin-salus-call 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Plugin proprietário do Salus para integrar a interface Cordova ao ciclo de vida nativo de chamadas VoIP no Android e no iOS.
4
4
 
5
- > Estado atual: a ponte Cordova e o contrato de eventos estão implementados. Android Telecom, WebRTC, PushKit e CallKit serão adicionados nas próximas versões.
5
+ > Estado atual: a ponte Cordova, o receptor FCM e a notificação telefônica Android estão implementados. Android Telecom/WebRTC e PushKit/CallKit serão adicionados nas próximas versões.
6
6
 
7
7
  ## Instalação
8
8
 
9
9
  ```xml
10
- <plugin name="cordova-plugin-salus-call" spec="0.1.0" />
10
+ <plugin name="cordova-plugin-salus-call" spec="0.2.1" />
11
11
  ```
12
12
 
13
13
  ## API
@@ -15,6 +15,7 @@ Plugin proprietário do Salus para integrar a interface Cordova ao ciclo de vida
15
15
  ```javascript
16
16
  SalusCall.initialize(options);
17
17
  SalusCall.getCapabilities();
18
+ SalusCall.showIncomingCall(call);
18
19
  SalusCall.startCall(call);
19
20
  SalusCall.answer(callId);
20
21
  SalusCall.reject(callId, reason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-salus-call",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Integração nativa de chamadas VoIP para o aplicativo Salus.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "engines": {
31
31
  "cordovaDependencies": {
32
- "0.1.0": {
32
+ "0.2.1": {
33
33
  "cordova": ">=12.0.0",
34
34
  "cordova-android": ">=14.0.0"
35
35
  }
package/plugin.xml CHANGED
@@ -2,7 +2,7 @@
2
2
  <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
3
3
  xmlns:android="http://schemas.android.com/apk/res/android"
4
4
  id="cordova-plugin-salus-call"
5
- version="0.1.0">
5
+ version="0.2.1">
6
6
  <name>Salus Call</name>
7
7
  <description>Ponte nativa para chamadas de interfone do Salus.</description>
8
8
  <license>UNLICENSED</license>
@@ -12,6 +12,20 @@
12
12
  </js-module>
13
13
 
14
14
  <platform name="android">
15
+ <config-file target="AndroidManifest.xml" parent="/manifest">
16
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
17
+ <uses-permission android:name="android.permission.VIBRATE" />
18
+ </config-file>
19
+ <config-file target="AndroidManifest.xml" parent="/manifest/application">
20
+ <provider
21
+ android:name="br.com.salus.call.SalusCallInitializerProvider"
22
+ android:authorities="${applicationId}.saluscall.initializer"
23
+ android:exported="false"
24
+ android:initOrder="100" />
25
+ <receiver
26
+ android:name="br.com.salus.call.SalusCallActionReceiver"
27
+ android:exported="false" />
28
+ </config-file>
15
29
  <config-file target="res/xml/config.xml" parent="/*">
16
30
  <feature name="SalusCall">
17
31
  <param name="android-package" value="br.com.salus.call.SalusCallPlugin" />
@@ -19,6 +33,18 @@
19
33
  </config-file>
20
34
  <source-file src="src/android/SalusCallPlugin.java"
21
35
  target-dir="src/br/com/salus/call" />
36
+ <source-file src="src/android/SalusCallInitializerProvider.java"
37
+ target-dir="src/br/com/salus/call" />
38
+ <source-file src="src/android/SalusFirebaseMessageReceiver.java"
39
+ target-dir="src/br/com/salus/call" />
40
+ <source-file src="src/android/SalusCallNotificationManager.java"
41
+ target-dir="src/br/com/salus/call" />
42
+ <source-file src="src/android/SalusCallActionReceiver.java"
43
+ target-dir="src/br/com/salus/call" />
44
+ <source-file src="src/android/SalusCallEventStore.java"
45
+ target-dir="src/br/com/salus/call" />
46
+ <framework src="androidx.core:core:1.15.0" />
47
+ <dependency id="cordova-plugin-firebasex-messaging" />
22
48
  </platform>
23
49
 
24
50
  <platform name="ios">
@@ -0,0 +1,32 @@
1
+ package br.com.salus.call;
2
+
3
+ import android.content.BroadcastReceiver;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+
7
+ import org.json.JSONObject;
8
+
9
+ public class SalusCallActionReceiver extends BroadcastReceiver {
10
+ @Override
11
+ public void onReceive(Context context, Intent intent) {
12
+ try {
13
+ JSONObject call = new JSONObject(intent.getStringExtra(SalusCallNotificationManager.EXTRA_CALL));
14
+ String callId = call.optString("callId", "");
15
+ boolean answered = SalusCallNotificationManager.ACTION_ANSWER.equals(intent.getAction());
16
+ SalusCallNotificationManager.cancel(context, callId);
17
+
18
+ JSONObject event = new JSONObject();
19
+ event.put("type", answered ? "answered" : "rejected");
20
+ event.put("callId", callId);
21
+ event.put("call", call);
22
+ event.put("reason", answered ? "system_answer" : "declined");
23
+ event.put("timestamp", System.currentTimeMillis());
24
+ SalusCallPlugin.dispatchEvent(context, event);
25
+
26
+ if (answered) {
27
+ Intent launch = SalusCallNotificationManager.launchIntent(context, call, "answer");
28
+ context.startActivity(launch);
29
+ }
30
+ } catch (Exception ignored) {}
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ package br.com.salus.call;
2
+
3
+ import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+
6
+ import org.json.JSONArray;
7
+ import org.json.JSONObject;
8
+
9
+ import java.util.ArrayList;
10
+ import java.util.List;
11
+
12
+ final class SalusCallEventStore {
13
+ private static final String PREFS = "salus_call_events";
14
+ private static final String KEY = "pending";
15
+
16
+ private SalusCallEventStore() {}
17
+
18
+ static synchronized void append(Context context, JSONObject event) {
19
+ SharedPreferences preferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
20
+ JSONArray events;
21
+ try { events = new JSONArray(preferences.getString(KEY, "[]")); }
22
+ catch (Exception ignored) { events = new JSONArray(); }
23
+ events.put(event);
24
+ while (events.length() > 20) events.remove(0);
25
+ preferences.edit().putString(KEY, events.toString()).apply();
26
+ }
27
+
28
+ static synchronized List<JSONObject> drain(Context context) {
29
+ SharedPreferences preferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
30
+ List<JSONObject> result = new ArrayList<>();
31
+ try {
32
+ JSONArray events = new JSONArray(preferences.getString(KEY, "[]"));
33
+ for (int i = 0; i < events.length(); i++) result.add(events.getJSONObject(i));
34
+ } catch (Exception ignored) {}
35
+ preferences.edit().remove(KEY).apply();
36
+ return result;
37
+ }
38
+ }
@@ -0,0 +1,25 @@
1
+ package br.com.salus.call;
2
+
3
+ import android.content.ContentProvider;
4
+ import android.content.ContentValues;
5
+ import android.database.Cursor;
6
+ import android.net.Uri;
7
+
8
+ public class SalusCallInitializerProvider extends ContentProvider {
9
+ private static boolean initialized;
10
+
11
+ @Override
12
+ public boolean onCreate() {
13
+ if (!initialized && getContext() != null) {
14
+ initialized = true;
15
+ new SalusFirebaseMessageReceiver(getContext().getApplicationContext());
16
+ }
17
+ return true;
18
+ }
19
+
20
+ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; }
21
+ @Override public String getType(Uri uri) { return null; }
22
+ @Override public Uri insert(Uri uri, ContentValues values) { return null; }
23
+ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; }
24
+ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; }
25
+ }
@@ -0,0 +1,126 @@
1
+ package br.com.salus.call;
2
+
3
+ import android.app.NotificationChannel;
4
+ import android.app.NotificationManager;
5
+ import android.app.PendingIntent;
6
+ import android.content.Context;
7
+ import android.content.Intent;
8
+ import android.media.AudioAttributes;
9
+ import android.media.RingtoneManager;
10
+ import android.net.Uri;
11
+ import android.os.Build;
12
+
13
+ import androidx.core.app.NotificationCompat;
14
+ import androidx.core.app.NotificationManagerCompat;
15
+ import androidx.core.app.Person;
16
+
17
+ import org.json.JSONObject;
18
+
19
+ public final class SalusCallNotificationManager {
20
+ static final String CHANNEL_INCOMING = "salus_incoming_calls";
21
+ static final String EXTRA_CALL = "salus_call";
22
+ static final String ACTION_ANSWER = "br.com.salus.call.ANSWER";
23
+ static final String ACTION_REJECT = "br.com.salus.call.REJECT";
24
+
25
+ private SalusCallNotificationManager() {}
26
+
27
+ public static void showIncomingCall(Context context, JSONObject call) {
28
+ createChannel(context);
29
+ String callId = call.optString("callId", String.valueOf(System.currentTimeMillis()));
30
+ String callerName = call.optString("callerName", "Chamada do interfone");
31
+ String callerAddress = call.optString("callerAddress", "");
32
+ int notificationId = notificationId(callId);
33
+
34
+ Person caller = new Person.Builder().setName(callerName).setImportant(true).build();
35
+ PendingIntent answer = answerIntent(context, call, notificationId + 1);
36
+ PendingIntent reject = actionIntent(context, ACTION_REJECT, call, notificationId + 2);
37
+ PendingIntent open = openAppIntent(context, call, notificationId + 3);
38
+
39
+ int icon = context.getResources().getIdentifier("notification_icon", "drawable", context.getPackageName());
40
+ if (icon == 0) icon = context.getApplicationInfo().icon;
41
+
42
+ boolean fullScreenAllowed = true;
43
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
44
+ if (Build.VERSION.SDK_INT >= 34 && manager != null) {
45
+ fullScreenAllowed = manager.canUseFullScreenIntent();
46
+ }
47
+
48
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_INCOMING)
49
+ .setSmallIcon(icon)
50
+ .setContentTitle(callerName)
51
+ .setContentText(callerAddress)
52
+ .setCategory(NotificationCompat.CATEGORY_CALL)
53
+ .setPriority(NotificationCompat.PRIORITY_MAX)
54
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
55
+ .setOngoing(true)
56
+ .setAutoCancel(false)
57
+ .setContentIntent(open)
58
+ .setFullScreenIntent(open, fullScreenAllowed)
59
+ .setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, reject, answer));
60
+
61
+ NotificationManagerCompat.from(context).notify(notificationId, builder.build());
62
+
63
+ JSONObject event = new JSONObject();
64
+ try {
65
+ event.put("type", "incomingCall");
66
+ event.put("callId", callId);
67
+ event.put("call", call);
68
+ event.put("timestamp", System.currentTimeMillis());
69
+ SalusCallPlugin.dispatchEvent(context, event);
70
+ } catch (Exception ignored) {}
71
+ }
72
+
73
+ public static void cancel(Context context, String callId) {
74
+ if (callId == null || callId.isEmpty()) return;
75
+ NotificationManagerCompat.from(context).cancel(notificationId(callId));
76
+ }
77
+
78
+ private static void createChannel(Context context) {
79
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
80
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
81
+ if (manager == null || manager.getNotificationChannel(CHANNEL_INCOMING) != null) return;
82
+
83
+ Uri ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
84
+ AudioAttributes audio = new AudioAttributes.Builder()
85
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
86
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
87
+ .build();
88
+ NotificationChannel channel = new NotificationChannel(CHANNEL_INCOMING, "Chamadas do interfone", NotificationManager.IMPORTANCE_HIGH);
89
+ channel.setDescription("Chamadas recebidas pelo interfone do condomínio");
90
+ channel.enableVibration(true);
91
+ channel.setLockscreenVisibility(android.app.Notification.VISIBILITY_PUBLIC);
92
+ channel.setSound(ringtone, audio);
93
+ manager.createNotificationChannel(channel);
94
+ }
95
+
96
+ private static PendingIntent actionIntent(Context context, String action, JSONObject call, int requestCode) {
97
+ Intent intent = new Intent(context, SalusCallActionReceiver.class);
98
+ intent.setAction(action);
99
+ intent.putExtra(EXTRA_CALL, call.toString());
100
+ return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
101
+ }
102
+
103
+ private static PendingIntent answerIntent(Context context, JSONObject call, int requestCode) {
104
+ Intent intent = launchIntent(context, call, "answer");
105
+ intent.setAction(ACTION_ANSWER);
106
+ return PendingIntent.getActivity(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
107
+ }
108
+
109
+ private static PendingIntent openAppIntent(Context context, JSONObject call, int requestCode) {
110
+ Intent intent = launchIntent(context, call, "open");
111
+ return PendingIntent.getActivity(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
112
+ }
113
+
114
+ static Intent launchIntent(Context context, JSONObject call, String action) {
115
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
116
+ if (intent == null) intent = new Intent();
117
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
118
+ intent.putExtra(EXTRA_CALL, call.toString());
119
+ intent.putExtra("salus_call_action", action);
120
+ return intent;
121
+ }
122
+
123
+ private static int notificationId(String callId) {
124
+ return 0x5A110000 | (callId.hashCode() & 0x0000FFFF);
125
+ }
126
+ }
@@ -1,15 +1,28 @@
1
1
  package br.com.salus.call;
2
2
 
3
+ import android.content.Context;
4
+ import android.content.Intent;
5
+
3
6
  import org.apache.cordova.CallbackContext;
7
+ import org.apache.cordova.CordovaInterface;
4
8
  import org.apache.cordova.CordovaPlugin;
9
+ import org.apache.cordova.CordovaWebView;
5
10
  import org.apache.cordova.PluginResult;
6
11
  import org.json.JSONArray;
7
12
  import org.json.JSONException;
8
13
  import org.json.JSONObject;
9
14
 
10
15
  public class SalusCallPlugin extends CordovaPlugin {
16
+ private static SalusCallPlugin activeInstance;
11
17
  private CallbackContext eventCallback;
12
18
 
19
+ @Override
20
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
21
+ super.initialize(cordova, webView);
22
+ activeInstance = this;
23
+ handleLaunchIntent(cordova.getActivity().getIntent());
24
+ }
25
+
13
26
  @Override
14
27
  public boolean execute(String action, JSONArray args, CallbackContext callback) throws JSONException {
15
28
  switch (action) {
@@ -26,11 +39,26 @@ public class SalusCallPlugin extends CordovaPlugin {
26
39
  PluginResult listening = new PluginResult(PluginResult.Status.NO_RESULT);
27
40
  listening.setKeepCallback(true);
28
41
  callback.sendPluginResult(listening);
42
+ emitPendingEvents();
43
+ return true;
44
+ case "showIncomingCall":
45
+ JSONObject call = args.getJSONObject(0);
46
+ SalusCallNotificationManager.showIncomingCall(cordova.getContext(), call);
47
+ callback.success(call);
29
48
  return true;
30
- case "startCall":
31
49
  case "answer":
50
+ handleLocalAction("answered", args.optString(0, ""), "answered_in_app");
51
+ callback.success();
52
+ return true;
32
53
  case "reject":
54
+ handleLocalAction("rejected", args.optString(0, ""), args.optString(1, "declined"));
55
+ callback.success();
56
+ return true;
33
57
  case "hangup":
58
+ handleLocalAction("ended", args.optString(0, ""), args.optString(1, "local_hangup"));
59
+ callback.success();
60
+ return true;
61
+ case "startCall":
34
62
  case "setMuted":
35
63
  case "setVideoEnabled":
36
64
  case "switchCamera":
@@ -47,7 +75,7 @@ public class SalusCallPlugin extends CordovaPlugin {
47
75
  result.put("nativeCalling", false);
48
76
  result.put("audio", false);
49
77
  result.put("video", false);
50
- result.put("incomingCallUi", false);
78
+ result.put("incomingCallUi", true);
51
79
  return result;
52
80
  }
53
81
 
@@ -65,4 +93,70 @@ public class SalusCallPlugin extends CordovaPlugin {
65
93
  result.setKeepCallback(true);
66
94
  eventCallback.sendPluginResult(result);
67
95
  }
96
+
97
+ static synchronized void dispatchEvent(Context context, JSONObject event) {
98
+ if (activeInstance != null && activeInstance.eventCallback != null) {
99
+ activeInstance.emitEvent(event);
100
+ } else {
101
+ SalusCallEventStore.append(context, event);
102
+ }
103
+ }
104
+
105
+ @Override
106
+ public void onNewIntent(Intent intent) {
107
+ handleLaunchIntent(intent);
108
+ emitPendingEvents();
109
+ }
110
+
111
+ @Override
112
+ public void onDestroy() {
113
+ if (activeInstance == this) activeInstance = null;
114
+ super.onDestroy();
115
+ }
116
+
117
+ private void emitPendingEvents() {
118
+ for (JSONObject event : SalusCallEventStore.drain(cordova.getContext())) {
119
+ emitEvent(event);
120
+ }
121
+ }
122
+
123
+ private void handleLocalAction(String type, String callId, String reason) throws JSONException {
124
+ SalusCallNotificationManager.cancel(cordova.getContext(), callId);
125
+ JSONObject event = new JSONObject();
126
+ event.put("type", type);
127
+ event.put("callId", callId);
128
+ event.put("reason", reason);
129
+ event.put("timestamp", System.currentTimeMillis());
130
+ emitEvent(event);
131
+ }
132
+
133
+ private void handleLaunchIntent(Intent intent) {
134
+ if (intent == null) return;
135
+
136
+ String action = intent.getStringExtra("salus_call_action");
137
+ String callJson = intent.getStringExtra(SalusCallNotificationManager.EXTRA_CALL);
138
+ if (action == null || callJson == null || callJson.isEmpty()) return;
139
+
140
+ intent.removeExtra("salus_call_action");
141
+ intent.removeExtra(SalusCallNotificationManager.EXTRA_CALL);
142
+
143
+ try {
144
+ JSONObject call = new JSONObject(callJson);
145
+ String callId = call.optString("callId", "");
146
+ SalusCallNotificationManager.cancel(cordova.getContext(), callId);
147
+
148
+ JSONObject event = new JSONObject();
149
+ if ("answer".equals(action)) {
150
+ event.put("type", "answered");
151
+ event.put("reason", "system_answer");
152
+ } else {
153
+ event.put("type", "opened");
154
+ event.put("reason", "notification_open");
155
+ }
156
+ event.put("callId", callId);
157
+ event.put("call", call);
158
+ event.put("timestamp", System.currentTimeMillis());
159
+ dispatchEvent(cordova.getContext(), event);
160
+ } catch (Exception ignored) {}
161
+ }
68
162
  }
@@ -0,0 +1,64 @@
1
+ package br.com.salus.call;
2
+
3
+ import android.content.Context;
4
+ import android.os.Bundle;
5
+
6
+ import com.google.firebase.messaging.RemoteMessage;
7
+
8
+ import org.apache.cordova.firebasex.FirebasePluginMessageReceiver;
9
+ import org.json.JSONObject;
10
+
11
+ import java.util.Map;
12
+
13
+ public class SalusFirebaseMessageReceiver extends FirebasePluginMessageReceiver {
14
+ private final Context context;
15
+
16
+ public SalusFirebaseMessageReceiver(Context context) {
17
+ this.context = context;
18
+ }
19
+
20
+ @Override
21
+ public boolean onMessageReceived(RemoteMessage remoteMessage) {
22
+ Map<String, String> data = remoteMessage.getData();
23
+ if (!isCall(data)) return false;
24
+
25
+ try {
26
+ long expiresAt = parseLong(data.get("expires_at"));
27
+ if (expiresAt > 0 && expiresAt * 1000L < System.currentTimeMillis()) {
28
+ return true;
29
+ }
30
+
31
+ JSONObject call = new JSONObject();
32
+ call.put("callId", value(data, "call_id", remoteMessage.getMessageId()));
33
+ call.put("remoteUserId", value(data, "id", "unknown"));
34
+ call.put("direction", "incoming");
35
+ call.put("media", value(data, "media", "audio"));
36
+ call.put("callerName", value(data, "namefrom", value(data, "title", "Chamada do interfone")));
37
+ call.put("callerAddress", value(data, "apto", value(data, "body", "")));
38
+ call.put("avatar", value(data, "urlprofile", value(data, "image", "")));
39
+ call.put("expiresAt", expiresAt);
40
+ SalusCallNotificationManager.showIncomingCall(context, call);
41
+ } catch (Exception error) {
42
+ return false;
43
+ }
44
+ return true;
45
+ }
46
+
47
+ @Override
48
+ public boolean sendMessage(Bundle bundle) {
49
+ return false;
50
+ }
51
+
52
+ private boolean isCall(Map<String, String> data) {
53
+ return data != null && ("chamadamorador".equals(data.get("info")) || "incoming".equals(data.get("call_type")));
54
+ }
55
+
56
+ private String value(Map<String, String> data, String key, String fallback) {
57
+ String value = data.get(key);
58
+ return value == null || value.trim().isEmpty() ? fallback : value;
59
+ }
60
+
61
+ private long parseLong(String value) {
62
+ try { return Long.parseLong(value); } catch (Exception ignored) { return 0L; }
63
+ }
64
+ }
package/www/SalusCall.js CHANGED
@@ -42,6 +42,11 @@ module.exports = {
42
42
  return callNative('getCapabilities');
43
43
  },
44
44
 
45
+ showIncomingCall: function(call) {
46
+ validateCall(call);
47
+ return callNative('showIncomingCall', [call]);
48
+ },
49
+
45
50
  startCall: function(call) {
46
51
  validateCall(call);
47
52
  return callNative('startCall', [call]);