Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): Support android call notification style #1134

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b9b7f8a
non working upgrade to androidx.core 1.10
dprevost-LMI Oct 30, 2024
f86efb6
Need to update room too for duplicate class error
dprevost-LMI Oct 30, 2024
9529fa4
Draft work support Call Android notification style
dprevost-LMI Oct 29, 2024
12fe620
Add CallStyle support in native
dprevost-LMI Oct 29, 2024
9fefb00
Review AndroidCallType + review validation
dprevost-LMI Oct 30, 2024
7f790f3
weird tentative to add intent in callStyle
dprevost-LMI Oct 30, 2024
0a1e89e
fix ts error
dprevost-LMI Oct 30, 2024
b848e68
Use actions in the style to create intent for the different buttons
dprevost-LMI Oct 30, 2024
d3a9f64
remove title & summary non existing on the call style
dprevost-LMI Oct 30, 2024
909d74f
Add more validation to the callStyle
dprevost-LMI Oct 30, 2024
e34230e
Code review
dprevost-LMI Oct 30, 2024
c3512f7
Add `Android Call Style` example
dprevost-LMI Oct 31, 2024
9aa2193
Better working example!
dprevost-LMI Oct 31, 2024
cf4df88
answer & decline trigger foreground events
dprevost-LMI Oct 31, 2024
ce7a523
fix error on triggering call style notif twice
dprevost-LMI Oct 31, 2024
190283a
Simplify config to only pass the callType without pressActions
dprevost-LMI Oct 31, 2024
2e3f732
Use default answerAction
dprevost-LMI Oct 31, 2024
7b64f71
Add `AndroidStyle.CALL` in error validation
dprevost-LMI Oct 31, 2024
5965a11
Use java enum for call & style type + finish validation unit test
dprevost-LMI Nov 1, 2024
58b1659
Add style md doc
dprevost-LMI Nov 1, 2024
695444f
Code review
dprevost-LMI Nov 1, 2024
2cea6f5
Better separation of concerns with `CallStyleNotificationPendingIntent`
dprevost-LMI Nov 1, 2024
7b22e3a
Fix linting problems
dprevost-LMI Nov 1, 2024
35d7f24
add it for incoming call style + fix command type in contribution
dprevost-LMI Nov 1, 2024
755e9f3
fix const enum causing problem importing `AndroidCallType`
dprevost-LMI Nov 1, 2024
367a442
remove forgotten useless code + final code review
dprevost-LMI Nov 1, 2024
edfe3e6
add missing coverage
dprevost-LMI Nov 1, 2024
3fe8d07
Fixes on `yarn format:all:check`
dprevost-LMI Nov 3, 2024
f3e3155
Add `yarn format:all` to CONTRIBUTING.md
dprevost-LMI Nov 3, 2024
91f59a4
Remove previous not needed changes anymore
dprevost-LMI Feb 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,26 @@ yarn build:rn:watch

## Testing Code

### Manual testing

Run on iOS. Change `--simulator 'iPhone 16'` for you simulator if needed.
```bash
yarn run:ios
```

or Android
```bash
yarn run:android
```


### Unit Testing

The following package scripts are exported to help you run tests;

- `yarn tests_rn:test` - run Jest tests once and exit.
- `yarn tests_rn:jest-watch` - run Jest tests in interactive mode and watch for changes.
- `yarn tests_rn:jest-coverage` - run Jest tests with coverage. Coverage is output to `./coverage`.
- `yarn tests_rn:test-watch` - run Jest tests in interactive mode and watch for changes.
- `yarn tests_rn:test-coverage` - run Jest tests with coverage. Coverage is output to `./coverage`.

### End-to-end Testing

Expand All @@ -76,6 +89,13 @@ yarn validate:all:js
yarn validate:all:ts
```

Runs auto-formatting

```bash
yarn format:all
```


## Publishing

Maintainers with write access to the repo and the npm organization can publish new versions by following the release checklist below.
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ dependencies {
api 'com.facebook.fresco:fresco:2.6.0' // https://github.com/facebook/fresco/releases

implementation("com.google.guava:guava:33.3.1-android") // https://github.com/google/guava
implementation 'androidx.core:core:1.6.0'
implementation 'androidx.core:core:1.10.0'

def room_version = '2.5.0' // https://developer.android.com/jetpack/androidx/releases/room
def room_version = '2.6.1' // https://developer.android.com/jetpack/androidx/releases/room
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package app.notifee.core;

import static app.notifee.core.event.NotificationEvent.TYPE_ACTION_PRESS;

import android.app.PendingIntent;
import android.os.Bundle;
import app.notifee.core.model.NotificationModel;
import java.util.Objects;

public class CallStyleNotificationPendingIntent {

public static PendingIntent getAnswerIntent(
Bundle callTypeActionsBundle, NotificationModel notificationModel) {
Bundle answerActionBundle =
Objects.requireNonNull(callTypeActionsBundle.getBundle("answerAction"));
return getPendingIntent(answerActionBundle, notificationModel.getHashCode(), notificationModel);
}

public static PendingIntent getDeclineIntent(
Bundle callTypeActionsBundle, NotificationModel notificationModel) {
Bundle declineActionBundle =
Objects.requireNonNull(callTypeActionsBundle.getBundle("declineAction"));
return getPendingIntent(
declineActionBundle, notificationModel.getHashCode() + 1, notificationModel);
}

public static PendingIntent getHangupIntent(
Bundle callTypeActionsBundle, NotificationModel notificationModel) {
Bundle hangUpActionBundle =
Objects.requireNonNull(callTypeActionsBundle.getBundle("hangUpAction"));
return getPendingIntent(
hangUpActionBundle, notificationModel.getHashCode() + 1, notificationModel);
}

private static PendingIntent getPendingIntent(
Bundle hangUpActionBundle, int notificationModel, NotificationModel notificationModel1) {
Bundle pressActionBundle = hangUpActionBundle.getBundle("pressAction");
return NotificationPendingIntent.createIntent(
notificationModel,
pressActionBundle,
TYPE_ACTION_PRESS,
new String[] {"notification", "pressAction"},
notificationModel1.toBundle(),
pressActionBundle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@ private static ListenableFuture<NotificationCompat.Builder> notificationBundleTo
}

ListenableFuture<NotificationCompat.Style> styleTask =
androidStyleBundle.getStyleTask(LISTENING_CACHED_THREAD_POOL);
androidStyleBundle.getStyleTask(
LISTENING_CACHED_THREAD_POOL, notificationModel);
if (styleTask == null) {
return builder;
}
Expand Down
8 changes: 8 additions & 0 deletions android/src/main/java/app/notifee/core/model/CallType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.notifee.core.model;

public enum CallType {
UNKNOWN,
INCOMING,
ONGOING,
SCREENING
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
*
*/

import android.app.PendingIntent;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import androidx.core.graphics.drawable.IconCompat;
import app.notifee.core.CallStyleNotificationPendingIntent;
import app.notifee.core.Logger;
import app.notifee.core.utility.ObjectUtils;
import app.notifee.core.utility.ResourceUtils;
Expand All @@ -39,7 +44,7 @@
@Keep
public class NotificationAndroidStyleModel {
private static final String TAG = "NotificationAndroidStyle";
private Bundle mNotificationAndroidStyleBundle;
private final Bundle mNotificationAndroidStyleBundle;

private NotificationAndroidStyleModel(Bundle styleBundle) {
mNotificationAndroidStyleBundle = styleBundle;
Expand Down Expand Up @@ -113,23 +118,30 @@ public Bundle toBundle() {

@Nullable
public ListenableFuture<NotificationCompat.Style> getStyleTask(
ListeningExecutorService lExecutor) {
int type = ObjectUtils.getInt(mNotificationAndroidStyleBundle.get("type"));
ListeningExecutorService lExecutor, NotificationModel notificationModel) {
NotificationAndroidStyleType type =
NotificationAndroidStyleType.values()[
ObjectUtils.getInt(mNotificationAndroidStyleBundle.get("type"))];
ListenableFuture<NotificationCompat.Style> styleTask = null;

switch (type) {
case 0:
case BIG_PICTURE:
styleTask = getBigPictureStyleTask(lExecutor);
break;
case 1:
case BIG_TEXT:
styleTask = Futures.immediateFuture(getBigTextStyle());
break;
case 2:
case INBOX:
styleTask = Futures.immediateFuture(getInboxStyle());
break;
case 3:
case MESSAGING:
styleTask = getMessagingStyleTask(lExecutor);
break;
case CALL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
styleTask = getCallStyleTask(lExecutor, notificationModel);
}
break;
}

return styleTask;
Expand Down Expand Up @@ -181,7 +193,9 @@ private ListenableFuture<NotificationCompat.Style> getBigPictureStyleTask(

// largeIcon has been specified to be null for BigPicture
if (largeIcon == null) {
bigPictureStyle.bigLargeIcon(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
bigPictureStyle.bigLargeIcon((Icon) null);
}
}
}

Expand Down Expand Up @@ -333,4 +347,65 @@ private ListenableFuture<NotificationCompat.Style> getMessagingStyleTask(
return messagingStyle;
});
}

/**
* Gets a CallStyle for a notification
*
* @return NotificationCompat.CallStyle
*/
@RequiresApi(31)
private ListenableFuture<NotificationCompat.Style> getCallStyleTask(
ListeningExecutorService lExecutor, NotificationModel notificationModel) {
return lExecutor.submit(
() -> {
Person caller =
getPerson(
lExecutor,
Objects.requireNonNull(mNotificationAndroidStyleBundle.getBundle("person")))
.get(20, TimeUnit.SECONDS);

Bundle callTypeActionsBundle =
Objects.requireNonNull(mNotificationAndroidStyleBundle.getBundle("callTypeActions"));

CallType callType =
CallType.values()[ObjectUtils.getInt(callTypeActionsBundle.get("callType"))];

switch (callType) {
case INCOMING:
{
PendingIntent answerIntent =
CallStyleNotificationPendingIntent.getAnswerIntent(
callTypeActionsBundle, notificationModel);
PendingIntent declineIntent =
CallStyleNotificationPendingIntent.getDeclineIntent(
callTypeActionsBundle, notificationModel);

return NotificationCompat.CallStyle.forIncomingCall(
caller, declineIntent, answerIntent);
}
case ONGOING:
{
PendingIntent hangupIntent =
CallStyleNotificationPendingIntent.getHangupIntent(
callTypeActionsBundle, notificationModel);

return NotificationCompat.CallStyle.forOngoingCall(caller, hangupIntent);
}
case SCREENING:
{
PendingIntent answerIntent =
CallStyleNotificationPendingIntent.getAnswerIntent(
callTypeActionsBundle, notificationModel);
PendingIntent hangupIntent =
CallStyleNotificationPendingIntent.getHangupIntent(
callTypeActionsBundle, notificationModel);

return NotificationCompat.CallStyle.forScreeningCall(
caller, hangupIntent, answerIntent);
}
default:
throw new RuntimeException("CallStyleTask: Invalid callType " + callType);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app.notifee.core.model;

public enum NotificationAndroidStyleType {
BIG_PICTURE,
BIG_TEXT,
INBOX,
MESSAGING,
CALL,
}
38 changes: 38 additions & 0 deletions docs/react-native/android/styles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,41 @@ Media style notifications are a way of showing a notification with intractable &
song/video playing with album art.

> Currently, Media Style notifications are not supported.

# Call

Call-style notifications starting from Android 12 display phone call-related notifications with a top ranking in the shade.

At the root, the style accepts a `person` property, which contains information of the current device user. To see more about
the options available to you, view the [`AndroidPerson`](/react-native/reference/androidperson) documentation.

The property `callTypeActions` accepts an object with either the desired call type number (incoming, ongoing, screening) or a whole action object with only the `pressAction` object for better customization. When using the call type only, predefined pressAction IDs are provided matching the predefined button name (e.g., `answer`, `decline`, `hangUp`). If desired, we can change those IDs and even provide launch activity flags when defining the `pressAction`.

It is important to known that the call style must be used with the Foreground service or a `fullScreenIntent`.

To implement this style, we provide the `AndroidStyle.CALL` to the `style` object:

```js
import notifee, { AndroidStyle, AndroidImportance, AndroidCallType } from '@notifee/react-native';

notifee.displayNotification({
title: 'Incoming call',
body: 'Sarah is calling',
android: {
channelId,
asForegroundService: true, // needs foreground Service or a fullScreenIntent.
category: AndroidCategory.CALL,
importance: AndroidImportance.HIGH,
style: {
type: AndroidStyle.CALL,
person: {
name: 'Sarah Lane',
icon: 'https://my-cdn.com/avatars/123.png',
},
callTypeActions: {
callType: AndroidCallType.INCOMING,
},
},
},
});
```
Loading