Skip to content

Commit 49aeff2

Browse files
authored
Merge pull request bkdev98#10 from bkdev98/v2
Version 2 is here 🥳
2 parents 72bcb06 + c1923b1 commit 49aeff2

File tree

18 files changed

+304
-121
lines changed

18 files changed

+304
-121
lines changed

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
example/
2+
.github/

README.md

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1-
# react-native-incoming-call
1+
# RN Incoming Call Version 2
22

33
> React Native module to display custom incoming call activity, best result when using with firebase background messaging. Only for Android since iOS we have VoIP.
44
55
Yes I heard you could use **self managed ConnectionService** thing. But since I'm not an Android expert, this is a solution I found acceptable.
66

77
You could also wait for [this feature request](https://github.com/react-native-webrtc/react-native-callkeep/issues/43) from `react-native-callkeep` to be resolved and have an easier life.
88

9+
## Version 2 Breaking Changes
10+
11+
Hello there! It's been a while since I first public version 1 of this library, which contains some bugs that I don't have much time to fix.
12+
13+
Luckily I got a client project which needs this feature again and now I have time to improve it and make sure all major features work. So here is most of it I guess:
14+
15+
[x] More generic incoming call UI.
16+
17+
[x] Work nicely with all application state (foreground, background, killed, locked).
18+
19+
[x] More flexible APIs.
20+
21+
*Thanks to [jpudysz](https://github.com/jpudysz/react-native-callkeep)'s folk of react-native-callkeep, version 2 is heavily depended on it.*
22+
23+
### Migrate from v1
24+
25+
- `getLaunchParameters()` & `clearLaunchParameters()` is now replaced by `openAppFromHeadlessMode()` & `getExtrasFromHeadlessMode()`.
26+
27+
- Answer calls from background / killed state no longer open app and send launchParams, you need to listen to `answerCall` event from headless job and trigger `backToForeground` or `openAppFromHeadlessMode` manually.
28+
929
## Getting started
1030

1131
`$ npm install react-native-incoming-call --save`
@@ -16,48 +36,103 @@ or
1636

1737
### Addition installation step
1838

19-
In `AndroidManifest.xml`, add `<activity android:name="com.incomingcall.UnlockScreenActivity" />` line between `<application>` tag.
39+
In `AndroidManifest.xml`:
40+
41+
- Add `<activity android:name="com.incomingcall.UnlockScreenActivity" />` line between `<application>` tag.
42+
43+
- Add `<uses-permission android:name="android.permission.VIBRATE" />` permission.
44+
45+
- Also, it's recommend to put `android:launchMode="singleInstance"` in `<activity android:name=".MainActivity"...` tag to prevent duplicate activities.
2046

2147
For RN >= 0.60, it's done. Otherwise:
2248

2349
`$ react-native link react-native-incoming-call`
2450

2551
## Usage
52+
53+
In `App.js`:
54+
2655
```javascript
2756
import {useEffect} from 'react';
57+
import {DeviceEventEmitter, Platform} from 'react-native';
2858
import IncomingCall from 'react-native-incoming-call';
2959

30-
// Display incoming call activity. Should be called in backgroundHandler function of react-native-firebase.
31-
IncomingCall.display(uuid, name, avatar);
32-
33-
// Dismiss current activity. Should be called when call ended.
34-
IncomingCall.dismiss();
35-
3660
// Listen to cancel and answer call events
3761
useEffect(() => {
3862
if (Platform.OS === "android") {
3963
/**
40-
* App in background or killed state, if user press answer button
41-
* IncomingCall open app and put payload to getLaunchParameters
42-
* You could start the call action here.
43-
* End call action in this case not supported yet.
64+
* App open from killed state (headless mode)
4465
*/
45-
const payload = await IncomingCall.getLaunchParameters();
66+
const payload = await IncomingCall.getExtrasFromHeadlessMode();
4667
console.log('launchParameters', payload);
47-
IncomingCall.clearLaunchParameters();
4868
if (payload) {
49-
// Start call here
69+
// Start call action here. You probably want to navigate to some CallRoom screen with the payload.uuid.
5070
}
5171

5272
/**
53-
* App in foreground: listen to call events and determine what to do next
73+
* App in foreground / background: listen to call events and determine what to do next
5474
*/
5575
DeviceEventEmitter.addListener("endCall", payload => {
5676
// End call action here
5777
});
5878
DeviceEventEmitter.addListener("answerCall", payload => {
59-
// Start call action here
79+
// Start call action here. You probably want to navigate to some CallRoom screen with the payload.uuid.
6080
});
6181
}
6282
}, []);
6383
```
84+
85+
In `index.js` or anywhere firebase background handler lies:
86+
87+
```javascript
88+
import messaging from '@react-native-firebase/messaging';
89+
import {DeviceEventEmitter} from 'react-native';
90+
import IncomingCall from 'react-native-incoming-call';
91+
92+
messaging().setBackgroundMessageHandler(async remoteMessage => {
93+
// Receive remote message
94+
if (remoteMessage?.notification?.title === 'Incoming Call') {
95+
// Display incoming call activity.
96+
IncomingCall.display(
97+
'callUUIDv4', // Call UUID v4
98+
'Quocs', // Username
99+
'https://avatars3.githubusercontent.com/u/16166195', // Avatar URL
100+
'Incomming Call' // Info text
101+
);
102+
} else if (remoteMessage?.notification?.title === 'Missed Call') {
103+
// Terminate incoming activity. Should be called when call expired.
104+
IncomingCall.dismiss();
105+
}
106+
107+
// Listen to headless action events
108+
DeviceEventEmitter.addListener("endCall", payload => {
109+
// End call action here
110+
});
111+
DeviceEventEmitter.addListener("answerCall", (payload) => {
112+
console.log('answerCall', payload);
113+
if (payload.isHeadless) {
114+
// Called from killed state
115+
IncomingCall.openAppFromHeadlessMode(payload.uuid);
116+
} else {
117+
// Called from background state
118+
IncomingCall.backToForeground();
119+
}
120+
});
121+
});
122+
```
123+
124+
## Well-known issues
125+
126+
### Incoming screen not show on android > 9:
127+
128+
You need to turn on autostart and display pop-up windows permissions manually. I'm searching for a better solution.
129+
130+
### No vibration when screen locked:
131+
132+
PR is welcomed! 😂
133+
134+
## License
135+
136+
This project is licensed under the MIT License.
137+
138+

android/src/main/java/com/incomingcall/IncomingCallModule.java

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@
44
import android.os.Bundle;
55
import android.app.Activity;
66
import android.view.WindowManager;
7+
import android.content.Context;
8+
import android.util.Log;
79

810
import com.facebook.react.bridge.ReactApplicationContext;
911
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1012
import com.facebook.react.bridge.ReactMethod;
1113
import com.facebook.react.bridge.Callback;
1214
import com.facebook.react.bridge.Promise;
15+
import com.facebook.react.bridge.WritableMap;
16+
import com.facebook.react.bridge.WritableNativeMap;
1317

1418
public class IncomingCallModule extends ReactContextBaseJavaModule {
1519

1620
public static ReactApplicationContext reactContext;
1721
public static Activity mainActivity;
1822

23+
private static final String TAG = "RNIC:IncomingCallModule";
24+
private WritableMap headlessExtras;
25+
1926
public IncomingCallModule(ReactApplicationContext context) {
2027
super(context);
2128
reactContext = context;
@@ -28,7 +35,7 @@ public String getName() {
2835
}
2936

3037
@ReactMethod
31-
public void display(String uuid, String name, String avatar) {
38+
public void display(String uuid, String name, String avatar, String info) {
3239
if (UnlockScreenActivity.active) {
3340
return;
3441
}
@@ -37,6 +44,7 @@ public void display(String uuid, String name, String avatar) {
3744
bundle.putString("uuid", uuid);
3845
bundle.putString("name", name);
3946
bundle.putString("avatar", avatar);
47+
bundle.putString("info", info);
4048
Intent i = new Intent(reactContext, UnlockScreenActivity.class);
4149
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
4250
i.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED +
@@ -52,31 +60,63 @@ public void display(String uuid, String name, String avatar) {
5260
public void dismiss() {
5361
final Activity activity = reactContext.getCurrentActivity();
5462

55-
// if (MainActivity.active) {
56-
// Intent i = new Intent(reactContext, MainActivity.class);
57-
// i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
58-
// reactContext.getApplicationContext().startActivity(i);
59-
// }
6063
assert activity != null;
6164
}
6265

66+
private Context getAppContext() {
67+
return this.reactContext.getApplicationContext();
68+
}
69+
6370
@ReactMethod
64-
public void getLaunchParameters(final Promise promise) {
65-
final Activity activity = getCurrentActivity();
66-
final Intent intent = activity.getIntent();
67-
Bundle b = intent.getExtras();
68-
String value = "";
69-
if (b != null) {
70-
value = b.getString("uuid", "");
71+
public void backToForeground() {
72+
Context context = getAppContext();
73+
String packageName = context.getApplicationContext().getPackageName();
74+
Intent focusIntent = context.getPackageManager().getLaunchIntentForPackage(packageName).cloneFilter();
75+
Activity activity = getCurrentActivity();
76+
boolean isOpened = activity != null;
77+
Log.d(TAG, "backToForeground, app isOpened ?" + (isOpened ? "true" : "false"));
78+
79+
if (isOpened) {
80+
focusIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
81+
activity.startActivity(focusIntent);
7182
}
72-
promise.resolve(value);
7383
}
7484

7585
@ReactMethod
76-
public void clearLaunchParameters() {
77-
final Activity activity = getCurrentActivity();
78-
final Intent intent = activity.getIntent();
79-
Bundle b = new Bundle();
80-
intent.putExtras(b);
86+
public void openAppFromHeadlessMode(String uuid) {
87+
Context context = getAppContext();
88+
String packageName = context.getApplicationContext().getPackageName();
89+
Intent focusIntent = context.getPackageManager().getLaunchIntentForPackage(packageName).cloneFilter();
90+
Activity activity = getCurrentActivity();
91+
boolean isOpened = activity != null;
92+
93+
if (!isOpened) {
94+
focusIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
95+
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
96+
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
97+
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
98+
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
99+
100+
final WritableMap response = new WritableNativeMap();
101+
response.putBoolean("isHeadless", true);
102+
response.putString("uuid", uuid);
103+
104+
this.headlessExtras = response;
105+
106+
getReactApplicationContext().startActivity(focusIntent);
107+
}
108+
}
109+
110+
@ReactMethod
111+
public void getExtrasFromHeadlessMode(Promise promise) {
112+
if (this.headlessExtras != null) {
113+
promise.resolve(this.headlessExtras);
114+
115+
this.headlessExtras = null;
116+
117+
return;
118+
}
119+
120+
promise.resolve(null);
81121
}
82122
}

android/src/main/java/com/incomingcall/UnlockScreenActivity.java

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import androidx.appcompat.app.AppCompatActivity;
1818
import android.app.ActivityManager;
19+
import android.app.ActivityManager.RunningAppProcessInfo;
1920

2021
import com.facebook.react.bridge.Arguments;
2122
import com.facebook.react.bridge.ReactContext;
@@ -30,6 +31,7 @@ public class UnlockScreenActivity extends AppCompatActivity implements UnlockScr
3031

3132
private static final String TAG = "MessagingService";
3233
private TextView tvName;
34+
private TextView tvInfo;
3335
private ImageView ivAvatar;
3436
private String uuid = "";
3537
static boolean active = false;
@@ -56,6 +58,7 @@ protected void onCreate(Bundle savedInstanceState) {
5658
setContentView(R.layout.activity_call_incoming);
5759

5860
tvName = findViewById(R.id.tvName);
61+
tvInfo = findViewById(R.id.tvInfo);
5962
ivAvatar = findViewById(R.id.ivAvatar);
6063

6164
Bundle bundle = getIntent().getExtras();
@@ -67,6 +70,10 @@ protected void onCreate(Bundle savedInstanceState) {
6770
String name = bundle.getString("name");
6871
tvName.setText(name);
6972
}
73+
if (bundle.containsKey("info")) {
74+
String info = bundle.getString("info");
75+
tvInfo.setText(info);
76+
}
7077
if (bundle.containsKey("avatar")) {
7178
String avatar = bundle.getString("avatar");
7279
if (avatar != null) {
@@ -117,46 +124,27 @@ public void onBackPressed() {
117124

118125
private void acceptDialing() {
119126
WritableMap params = Arguments.createMap();
120-
params.putBoolean("done", true);
127+
params.putBoolean("accept", true);
121128
params.putString("uuid", uuid);
122-
123-
if (IncomingCallModule.reactContext.hasCurrentActivity() && isAppOnForeground(IncomingCallModule.reactContext)) {
124-
// App in foreground, send event for app to listen
125-
sendEvent("answerCall", params);
126-
} else {
127-
// App in background or killed, start app and add launch params
128-
String packageNames = IncomingCallModule.reactContext.getPackageName();
129-
Intent launchIntent = IncomingCallModule.reactContext.getPackageManager().getLaunchIntentForPackage(packageNames);
130-
String className = launchIntent.getComponent().getClassName();
131-
try {
132-
Class<?> activityClass = Class.forName(className);
133-
Intent i = new Intent(IncomingCallModule.reactContext, activityClass);
134-
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
135-
Bundle b = new Bundle();
136-
b.putString("uuid", uuid);
137-
i.putExtras(b);
138-
IncomingCallModule.reactContext.startActivity(i);
139-
} catch (Exception e) {
140-
Log.e("RNIncomingCall", "Class not found", e);
141-
return;
142-
}
129+
if (!IncomingCallModule.reactContext.hasCurrentActivity()) {
130+
params.putBoolean("isHeadless", true);
143131
}
144132

133+
sendEvent("answerCall", params);
134+
145135
finish();
146136
}
147137

148138
private void dismissDialing() {
149139
WritableMap params = Arguments.createMap();
150-
params.putBoolean("done", false);
140+
params.putBoolean("accept", false);
151141
params.putString("uuid", uuid);
152-
153-
if (IncomingCallModule.reactContext.hasCurrentActivity()) {
154-
// App in foreground or background, send event for app to listen
155-
sendEvent("endCall", params);
156-
} else {
157-
// App killed, need to do something after
142+
if (!IncomingCallModule.reactContext.hasCurrentActivity()) {
143+
params.putBoolean("isHeadless", true);
158144
}
159145

146+
sendEvent("endCall", params);
147+
160148
finish();
161149
}
162150

@@ -193,24 +181,4 @@ private void sendEvent(String eventName, WritableMap params) {
193181
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
194182
.emit(eventName, params);
195183
}
196-
197-
private boolean isAppOnForeground(ReactApplicationContext context) {
198-
/**
199-
* We need to check if app is in foreground otherwise the app will crash.
200-
* http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
201-
**/
202-
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
203-
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
204-
if (appProcesses == null) {
205-
return false;
206-
}
207-
final String packageName = context.getPackageName();
208-
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
209-
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
210-
&& appProcess.processName.equals(packageName)) {
211-
return true;
212-
}
213-
}
214-
return false;
215-
}
216184
}
-644 KB
Binary file not shown.
10.4 KB
Loading
55.9 KB
Loading
12 KB
Loading
29.5 KB
Loading

0 commit comments

Comments
 (0)