diff --git a/android/build.gradle b/android/build.gradle
index ed1dac8fb..5266935a9 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -116,7 +116,12 @@ repositories {
}
dependencies {
+ // For NotificationCompact.Builder.addPerson(person)
+ def core_version = "1.5.0-rc01"
+ // Java language implementation
+ implementation "androidx.core:core:$core_version"
+ implementation "androidx.core:app:$core_version"
api 'androidx.annotation:annotation:1.1.0' // https://developer.android.com/jetpack/androidx/releases/annotation
api "com.squareup.okhttp3:okhttp:3.12.12" // okhttp must stay on 3.12.x to support minSdkVersion < 21
api 'androidx.concurrent:concurrent-futures:1.1.0' // https://developer.android.com/jetpack/androidx/releases/concurrent
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 5c247095b..30b1e1921 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
diff --git a/android/src/main/java/app/notifee/core/EventBus.java b/android/src/main/java/app/notifee/core/EventBus.java
index 6597cd98b..077443869 100644
--- a/android/src/main/java/app/notifee/core/EventBus.java
+++ b/android/src/main/java/app/notifee/core/EventBus.java
@@ -32,7 +32,7 @@ public static void post(Object event) {
getInstance().getDefault().post(event);
}
- static void postSticky(Object event) {
+ public static void postSticky(Object event) {
getInstance().getDefault().postSticky(event);
}
diff --git a/android/src/main/java/app/notifee/core/NotificationManager.java b/android/src/main/java/app/notifee/core/NotificationManager.java
index 78d526ea7..9e0a4c4f9 100644
--- a/android/src/main/java/app/notifee/core/NotificationManager.java
+++ b/android/src/main/java/app/notifee/core/NotificationManager.java
@@ -1,17 +1,18 @@
package app.notifee.core;
-import static app.notifee.core.ReceiverService.ACTION_PRESS_INTENT;
-
import android.app.Notification;
import android.app.PendingIntent;
+import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+
import androidx.annotation.NonNull;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
+import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import androidx.work.Data;
@@ -23,21 +24,11 @@
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.WorkQuery;
-import app.notifee.core.database.WorkDataEntity;
-import app.notifee.core.database.WorkDataRepository;
-import app.notifee.core.event.NotificationEvent;
-import app.notifee.core.model.IntervalTriggerModel;
-import app.notifee.core.model.NotificationAndroidActionModel;
-import app.notifee.core.model.NotificationAndroidModel;
-import app.notifee.core.model.NotificationAndroidStyleModel;
-import app.notifee.core.model.NotificationModel;
-import app.notifee.core.model.TimestampTriggerModel;
-import app.notifee.core.utility.ObjectUtils;
-import app.notifee.core.utility.ResourceUtils;
-import app.notifee.core.utility.TextUtils;
+
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -49,6 +40,27 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import app.notifee.core.database.WorkDataEntity;
+import app.notifee.core.database.WorkDataRepository;
+import app.notifee.core.event.MainComponentEvent;
+import app.notifee.core.event.NotificationEvent;
+import app.notifee.core.model.IntervalTriggerModel;
+import app.notifee.core.model.NotificationAndroidActionModel;
+import app.notifee.core.model.NotificationAndroidBubbleActionModel;
+import app.notifee.core.model.NotificationAndroidModel;
+import app.notifee.core.model.NotificationAndroidPersonModel;
+import app.notifee.core.model.NotificationAndroidPressActionModel;
+import app.notifee.core.model.NotificationAndroidStyleModel;
+import app.notifee.core.model.NotificationModel;
+import app.notifee.core.model.TimestampTriggerModel;
+import app.notifee.core.utility.IntentUtils;
+import app.notifee.core.utility.ObjectUtils;
+import app.notifee.core.utility.ResourceUtils;
+import app.notifee.core.utility.TextUtils;
+
+import static app.notifee.core.ContextHolder.getApplicationContext;
+import static app.notifee.core.ReceiverService.ACTION_PRESS_INTENT;
+
class NotificationManager {
private static final String TAG = "NotificationManager";
private static final ExecutorService CACHED_THREAD_POOL = Executors.newCachedThreadPool();
@@ -67,8 +79,7 @@ private static Task notificationBundleToBuilder(
() -> {
Boolean hasCustomSound = false;
NotificationCompat.Builder builder =
- new NotificationCompat.Builder(
- ContextHolder.getApplicationContext(), androidModel.getChannelId());
+ new NotificationCompat.Builder(getApplicationContext(), androidModel.getChannelId());
// must always keep at top
builder.setExtras(notificationModel.getData());
@@ -243,6 +254,42 @@ private static Task notificationBundleToBuilder(
return builder;
};
+ /*
+ * A task continuation for full-screen action, if specified.
+ */
+ Continuation
+ fullScreenActionContinuation =
+ task -> {
+ NotificationCompat.Builder builder = task.getResult();
+ if (androidModel.hasFullScreenAction()) {
+ NotificationAndroidPressActionModel fullScreenActionBundle =
+ androidModel.getFullScreenAction();
+
+ String launchActivity = fullScreenActionBundle.getLaunchActivity();
+ Class launchActivityClass = IntentUtils.getLaunchActivity(launchActivity);
+ Intent launchIntent = new Intent(getApplicationContext(), launchActivityClass);
+ if (fullScreenActionBundle.getLaunchActivityFlags() != -1) {
+ launchIntent.addFlags(fullScreenActionBundle.getLaunchActivityFlags());
+ }
+
+ if (fullScreenActionBundle.getMainComponent() != null) {
+ launchIntent.putExtra("mainComponent", fullScreenActionBundle.getMainComponent());
+ EventBus.postSticky(
+ new MainComponentEvent(fullScreenActionBundle.getMainComponent()));
+ }
+
+ PendingIntent fullScreenPendingIntent =
+ PendingIntent.getActivity(
+ getApplicationContext(),
+ notificationModel.getHashCode(),
+ launchIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ builder.setFullScreenIntent(fullScreenPendingIntent, true);
+ }
+
+ return builder;
+ };
+
/*
* A task continuation that builds all actions, if any. Additionally fetches
* icon bitmaps through Fresco.
@@ -303,6 +350,31 @@ private static Task notificationBundleToBuilder(
return builder;
};
+ /*
+ * A task continuation that builds the notification bubble action, if any.
+ */
+ Continuation bubbleActionContinuation =
+ task -> {
+ NotificationCompat.Builder builder = task.getResult();
+ NotificationAndroidBubbleActionModel androidBubbleActionBundle = androidModel.getBubbleAction();
+ if (androidBubbleActionBundle == null) {
+ return builder;
+ }
+
+ Task bubbleActionTask =
+ androidBubbleActionBundle.getBubbleActionTask(CACHED_THREAD_POOL, notificationModel);
+ if (bubbleActionTask == null) {
+ return builder;
+ }
+
+ NotificationCompat.BubbleMetadata bubbleMetadata = Tasks.await(bubbleActionTask);
+ if (bubbleMetadata != null) {
+ builder.setBubbleMetadata(bubbleMetadata);
+ }
+
+ return builder;
+ };
+
/*
* A task continuation that builds the notification style, if any. Additionally
* fetches any image bitmaps (e.g. Person image, or BigPicture image) through
@@ -330,9 +402,44 @@ private static Task notificationBundleToBuilder(
return builder;
};
+
+ /*
+ * A task continuation that builds the notification style, if any. Additionally
+ * fetches any image bitmaps (e.g. Person image, or BigPicture image) through
+ * Fresco.
+ */
+ Continuation personContinuation =
+ task -> {
+ NotificationCompat.Builder builder = task.getResult();
+ NotificationAndroidPersonModel androidPersonBundle = androidModel.getPerson();
+ if (androidPersonBundle == null) {
+ return builder;
+ }
+
+ Task personTask =
+ androidPersonBundle.buildPersonTask(CACHED_THREAD_POOL);
+ if (personTask == null) {
+ return builder;
+ }
+
+ Person person = Tasks.await(personTask);
+ if (person != null) {
+ // https://developer.android.com/reference/androidx/core/app/NotificationCompat.Builder?hl=zh-cn#addPerson(androidx.core.app.Person)
+ builder.addPerson(person);
+ }
+
+ return builder;
+ };
+
return Tasks.call(CACHED_THREAD_POOL, builderCallable)
// get a large image bitmap if largeIcon is set
.continueWith(CACHED_THREAD_POOL, largeIconContinuation)
+ // set a person, if person is set
+ .continueWith(CACHED_THREAD_POOL, personContinuation)
+ // set full screen action, if fullScreenAction is set
+ .continueWith(CACHED_THREAD_POOL, fullScreenActionContinuation)
+ // set bubble action, if bubbleAction is set
+ .continueWith(CACHED_THREAD_POOL, bubbleActionContinuation)
// build notification actions, tasks based to allow image fetching
.continueWith(CACHED_THREAD_POOL, actionsContinuation)
// build notification style, tasks based to allow image fetching
@@ -344,7 +451,7 @@ static Task cancelNotification(
return Tasks.call(
() -> {
NotificationManagerCompat notificationManagerCompat =
- NotificationManagerCompat.from(ContextHolder.getApplicationContext());
+ NotificationManagerCompat.from(getApplicationContext());
if (notificationType == NOTIFICATION_TYPE_DISPLAYED
|| notificationType == NOTIFICATION_TYPE_ALL) {
@@ -353,13 +460,12 @@ static Task cancelNotification(
if (notificationType == NOTIFICATION_TYPE_TRIGGER
|| notificationType == NOTIFICATION_TYPE_ALL) {
- WorkManager.getInstance(ContextHolder.getApplicationContext())
+ WorkManager.getInstance(getApplicationContext())
.cancelUniqueWork("trigger:" + notificationId);
}
// delete notification entry from database
- WorkDataRepository.getInstance(ContextHolder.getApplicationContext())
- .deleteById(notificationId);
+ WorkDataRepository.getInstance(getApplicationContext()).deleteById(notificationId);
return null;
});
}
@@ -368,7 +474,7 @@ static Task cancelAllNotifications(@NonNull int notificationType) {
return Tasks.call(
() -> {
NotificationManagerCompat notificationManagerCompat =
- NotificationManagerCompat.from(ContextHolder.getApplicationContext());
+ NotificationManagerCompat.from(getApplicationContext());
if (notificationType == NOTIFICATION_TYPE_DISPLAYED
|| notificationType == NOTIFICATION_TYPE_ALL) {
@@ -377,8 +483,7 @@ static Task cancelAllNotifications(@NonNull int notificationType) {
if (notificationType == NOTIFICATION_TYPE_TRIGGER
|| notificationType == NOTIFICATION_TYPE_ALL) {
- WorkManager workManager =
- WorkManager.getInstance(ContextHolder.getApplicationContext());
+ WorkManager workManager = WorkManager.getInstance(getApplicationContext());
workManager.cancelAllWorkByTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
// Remove all cancelled and finished work from its internal database
@@ -386,7 +491,7 @@ static Task cancelAllNotifications(@NonNull int notificationType) {
workManager.pruneWork();
// delete all from database
- WorkDataRepository.getInstance(ContextHolder.getApplicationContext()).deleteAll();
+ WorkDataRepository.getInstance(getApplicationContext()).deleteAll();
}
return null;
@@ -406,7 +511,7 @@ static Task displayNotification(NotificationModel notificationModel) {
if (androidBundle.getAsForegroundService()) {
ForegroundService.start(hashCode, notification, notificationModel.toBundle());
} else {
- NotificationManagerCompat.from(ContextHolder.getApplicationContext())
+ NotificationManagerCompat.from(getApplicationContext())
.notify(hashCode, notification);
}
@@ -444,7 +549,7 @@ static void createIntervalTriggerNotification(
NotificationModel notificationModel, Bundle triggerBundle) {
IntervalTriggerModel trigger = IntervalTriggerModel.fromBundle(triggerBundle);
String uniqueWorkName = "trigger:" + notificationModel.getId();
- WorkManager workManager = WorkManager.getInstance(ContextHolder.getApplicationContext());
+ WorkManager workManager = WorkManager.getInstance(getApplicationContext());
Data.Builder workDataBuilder =
new Data.Builder()
@@ -452,7 +557,7 @@ static void createIntervalTriggerNotification(
.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC)
.putString("id", notificationModel.getId());
- WorkDataRepository.getInstance(ContextHolder.getApplicationContext())
+ WorkDataRepository.getInstance(getApplicationContext())
.insertTriggerNotification(notificationModel, triggerBundle);
long interval = trigger.getInterval();
@@ -473,7 +578,7 @@ static void createTimestampTriggerNotification(
TimestampTriggerModel trigger = TimestampTriggerModel.fromBundle(triggerBundle);
String uniqueWorkName = "trigger:" + notificationModel.getId();
- WorkManager workManager = WorkManager.getInstance(ContextHolder.getApplicationContext());
+ WorkManager workManager = WorkManager.getInstance(getApplicationContext());
long delay = trigger.getDelay();
int interval = trigger.getInterval();
@@ -482,7 +587,7 @@ static void createTimestampTriggerNotification(
.putString(Worker.KEY_WORK_TYPE, Worker.WORK_TYPE_NOTIFICATION_TRIGGER)
.putString("id", notificationModel.getId());
- WorkDataRepository.getInstance(ContextHolder.getApplicationContext())
+ WorkDataRepository.getInstance(getApplicationContext())
.insertTriggerNotification(notificationModel, triggerBundle);
// One time trigger
@@ -520,9 +625,7 @@ static Task> getTriggerNotificationIds() {
query.addStates(Arrays.asList(WorkInfo.State.ENQUEUED));
List workInfos =
- WorkManager.getInstance(ContextHolder.getApplicationContext())
- .getWorkInfos(query.build())
- .get();
+ WorkManager.getInstance(getApplicationContext()).getWorkInfos(query.build()).get();
if (workInfos.size() == 0) {
return Collections.emptyList();
@@ -551,8 +654,7 @@ static void doScheduledWork(
String id = data.getString("id");
- WorkDataRepository workDataRepository =
- new WorkDataRepository(ContextHolder.getApplicationContext());
+ WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext());
Continuation> workContinuation =
task -> {
@@ -598,8 +700,7 @@ static void doScheduledWork(
if (workerRequestType != null
&& workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) {
// delete database entry if work is a one-time request
- WorkDataRepository.getInstance(ContextHolder.getApplicationContext())
- .deleteById(id);
+ WorkDataRepository.getInstance(getApplicationContext()).deleteById(id);
}
}
});
diff --git a/android/src/main/java/app/notifee/core/ReceiverService.java b/android/src/main/java/app/notifee/core/ReceiverService.java
index c69382421..dfdba8986 100644
--- a/android/src/main/java/app/notifee/core/ReceiverService.java
+++ b/android/src/main/java/app/notifee/core/ReceiverService.java
@@ -18,6 +18,7 @@
import app.notifee.core.event.NotificationEvent;
import app.notifee.core.model.NotificationAndroidPressActionModel;
import app.notifee.core.model.NotificationModel;
+import app.notifee.core.utility.IntentUtils;
public class ReceiverService extends Service {
public static final String REMOTE_INPUT_RECEIVER_KEY =
@@ -196,7 +197,7 @@ private void launchPendingIntentActivity(
@Nullable String launchActivity,
@Nullable String mainComponent,
int launchActivityFlags) {
- Class> launchActivityClass = getLaunchActivity(launchActivity);
+ Class> launchActivityClass = IntentUtils.getLaunchActivity(launchActivity);
Intent launchIntent = new Intent(getApplicationContext(), launchActivityClass);
@@ -231,50 +232,4 @@ private void launchPendingIntentActivity(
e);
}
}
-
- private Class> getLaunchActivity(@Nullable String launchActivity) {
- String activity;
-
- if (launchActivity != null && !launchActivity.equals("default")) {
- activity = launchActivity;
- } else {
- activity = getMainActivityClassName();
- }
-
- if (activity == null) {
- Logger.e("ReceiverService", "Launch Activity for notification could not be found.");
- return null;
- }
-
- Class> launchActivityClass = getClassForName(activity);
-
- if (launchActivityClass == null) {
- Logger.e(
- "ReceiverService",
- String.format("Launch Activity for notification does not exist ('%s').", launchActivity));
- return null;
- }
-
- return launchActivityClass;
- }
-
- private @Nullable Class> getClassForName(String className) {
- try {
- return Class.forName(className);
- } catch (ClassNotFoundException e) {
- return null;
- }
- }
-
- private @Nullable String getMainActivityClassName() {
- String packageName = getApplicationContext().getPackageName();
- Intent launchIntent =
- getApplicationContext().getPackageManager().getLaunchIntentForPackage(packageName);
-
- if (launchIntent == null || launchIntent.getComponent() == null) {
- return null;
- }
-
- return launchIntent.getComponent().getClassName();
- }
}
diff --git a/android/src/main/java/app/notifee/core/model/NotificationAndroidBubbleActionModel.java b/android/src/main/java/app/notifee/core/model/NotificationAndroidBubbleActionModel.java
new file mode 100644
index 000000000..f43f5c376
--- /dev/null
+++ b/android/src/main/java/app/notifee/core/model/NotificationAndroidBubbleActionModel.java
@@ -0,0 +1,192 @@
+package app.notifee.core.model;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+import app.notifee.core.EventBus;
+import app.notifee.core.event.MainComponentEvent;
+import app.notifee.core.utility.IntentUtils;
+
+import static app.notifee.core.ContextHolder.getApplicationContext;
+
+@Keep
+public class NotificationAndroidBubbleActionModel {
+ private Bundle mNotificationAndroidBubbleActionBundle;
+
+ private NotificationAndroidBubbleActionModel(Bundle bundle) {
+ mNotificationAndroidBubbleActionBundle = bundle;
+ }
+
+ public static NotificationAndroidBubbleActionModel fromBundle(Bundle bundle) {
+ return new NotificationAndroidBubbleActionModel(bundle);
+ }
+
+ public Bundle toBundle() {
+ return (Bundle) mNotificationAndroidBubbleActionBundle.clone();
+ }
+
+ public @NonNull String getId() {
+ return Objects.requireNonNull(mNotificationAndroidBubbleActionBundle.getString("id"));
+ }
+
+ public @Nullable String getLaunchActivity() {
+ return mNotificationAndroidBubbleActionBundle.getString("launchActivity");
+ }
+
+ public @Nullable String getMainComponent() {
+ return mNotificationAndroidBubbleActionBundle.getString("mainComponent");
+ }
+
+ public int getLaunchActivityFlags() {
+ if (!mNotificationAndroidBubbleActionBundle.containsKey("launchActivityFlags")) {
+ return -1;
+ }
+
+ int baseFlags = 0;
+ ArrayList launchActivityFlags =
+ Objects.requireNonNull(
+ mNotificationAndroidBubbleActionBundle.getIntegerArrayList("launchActivityFlags"));
+
+ for (int i = 0; i < launchActivityFlags.size(); i++) {
+ Integer flag = launchActivityFlags.get(i);
+ switch (flag) {
+ case 0:
+ baseFlags |= Intent.FLAG_ACTIVITY_NO_HISTORY;
+ break;
+ case 1:
+ baseFlags |= Intent.FLAG_ACTIVITY_SINGLE_TOP;
+ break;
+ case 2:
+ baseFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
+ break;
+ case 3:
+ baseFlags |= Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+ break;
+ case 4:
+ baseFlags |= Intent.FLAG_ACTIVITY_CLEAR_TOP;
+ break;
+ case 5:
+ baseFlags |= Intent.FLAG_ACTIVITY_FORWARD_RESULT;
+ break;
+ case 6:
+ baseFlags |= Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP;
+ break;
+ case 7:
+ baseFlags |= Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+ break;
+ case 8:
+ baseFlags |= Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT;
+ break;
+ case 9:
+ baseFlags |= Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
+ break;
+ case 10:
+ baseFlags |= Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY;
+ break;
+ case 11:
+ baseFlags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
+ break;
+ case 12:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ baseFlags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+ }
+ break;
+ case 13:
+ baseFlags |= Intent.FLAG_ACTIVITY_NO_USER_ACTION;
+ break;
+ case 14:
+ baseFlags |= Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
+ break;
+ case 15:
+ baseFlags |= Intent.FLAG_ACTIVITY_NO_ANIMATION;
+ break;
+ case 16:
+ baseFlags |= Intent.FLAG_ACTIVITY_CLEAR_TASK;
+ break;
+ case 17:
+ baseFlags |= Intent.FLAG_ACTIVITY_TASK_ON_HOME;
+ break;
+ case 18:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ baseFlags |= Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
+ }
+ break;
+ case 19:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ baseFlags |= Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
+ }
+ break;
+ case 20:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ baseFlags |= Intent.FLAG_ACTIVITY_MATCH_EXTERNAL;
+ }
+ break;
+ }
+ }
+ return baseFlags;
+ }
+
+
+ /*
+ * A task continuation for bubble, if specified.
+ */
+ public Task getBubbleActionTask(Executor executor, NotificationModel notificationModel) {
+ return Tasks.call(
+ executor,
+ () -> {
+ String launchActivity = getLaunchActivity();
+ Class launchActivityClass = IntentUtils.getLaunchActivity(launchActivity);
+ Intent bubbleIntent = new Intent(getApplicationContext(), launchActivityClass);
+ if (getLaunchActivityFlags() != -1) {
+ bubbleIntent.addFlags(getLaunchActivityFlags());
+ }
+
+ if (mNotificationAndroidBubbleActionBundle.containsKey("mainComponent")) {
+ bubbleIntent.putExtra("mainComponent", getMainComponent());
+ EventBus.postSticky(
+ new MainComponentEvent(getMainComponent()));
+ }
+
+ NotificationCompat.BubbleMetadata.Builder bubbleBuilder = new NotificationCompat.BubbleMetadata.Builder();
+ PendingIntent bubblePendingIntent =
+ PendingIntent.getActivity(
+ getApplicationContext(),
+ notificationModel.getHashCode(),
+ bubbleIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ bubbleBuilder.setIntent(bubblePendingIntent);
+
+ IconCompat bubbleIcon = IconCompat.createWithContentUri(Objects.requireNonNull(mNotificationAndroidBubbleActionBundle.getString("icon")));
+ bubbleBuilder.setIcon(bubbleIcon);
+
+ if (mNotificationAndroidBubbleActionBundle.containsKey("height")) {
+ bubbleBuilder.setDesiredHeight(mNotificationAndroidBubbleActionBundle.getInt("height"));
+ }
+
+ if (mNotificationAndroidBubbleActionBundle.containsKey("autoExpand")) {
+ bubbleBuilder.setAutoExpandBubble(mNotificationAndroidBubbleActionBundle.getBoolean("autoExpand"));
+ }
+
+ if (mNotificationAndroidBubbleActionBundle.containsKey("suppressNotification")) {
+ bubbleBuilder.setSuppressNotification(mNotificationAndroidBubbleActionBundle.getBoolean("suppressNotification"));
+ }
+
+ return bubbleBuilder.build();
+ });
+ }
+}
diff --git a/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java b/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java
index 01b96eab6..134fb5c9a 100644
--- a/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java
+++ b/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java
@@ -3,16 +3,19 @@
import android.app.Notification;
import android.graphics.Color;
import android.os.Bundle;
+
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
-import app.notifee.core.Logger;
-import app.notifee.core.utility.ResourceUtils;
+
import java.util.ArrayList;
import java.util.Objects;
+import app.notifee.core.Logger;
+import app.notifee.core.utility.ResourceUtils;
+
@Keep
public class NotificationAndroidModel {
private Bundle mNotificationAndroidBundle;
@@ -303,6 +306,25 @@ public Boolean getOnlyAlertOnce() {
return mNotificationAndroidBundle.getBoolean("onlyAlertOnce", false);
}
+ /**
+ * Returns true if the notification has a fullScreenAction
+ *
+ * @return Boolean
+ */
+ public Boolean hasFullScreenAction() {
+ return mNotificationAndroidBundle.containsKey("fullScreenAction");
+ }
+
+ /**
+ * Returns true if the notification has a bubbleAction
+ *
+ * @return Boolean
+ */
+ public Boolean hasBubbleAction() {
+ return mNotificationAndroidBundle.containsKey("bubbleAction");
+ }
+
+
/**
* Gets an pressAction bundle for the notification
*
@@ -312,6 +334,34 @@ public Boolean getOnlyAlertOnce() {
return mNotificationAndroidBundle.getBundle("pressAction");
}
+ /**
+ * Returns a notification full screen action
+ *
+ * @return NotificationAndroidFullScreenActionModel
+ */
+ public @Nullable NotificationAndroidPressActionModel getFullScreenAction() {
+ if (!hasFullScreenAction()) {
+ return null;
+ }
+
+ return NotificationAndroidPressActionModel.fromBundle(
+ mNotificationAndroidBundle.getBundle("fullScreenAction"));
+ }
+
+ /**
+ * Returns a notification bubble action
+ *
+ * @return NotificationAndroidBubbleActionModel
+ */
+ public @Nullable NotificationAndroidBubbleActionModel getBubbleAction() {
+ if (!hasBubbleAction()) {
+ return null;
+ }
+
+ return NotificationAndroidBubbleActionModel.fromBundle(
+ mNotificationAndroidBundle.getBundle("bubbleAction"));
+ }
+
/**
* JS uses the same API as importance for priority so we dont confuse users. This maps importance
* to a priority flag.
@@ -440,6 +490,19 @@ public Boolean hasStyle() {
return NotificationAndroidStyleModel.fromBundle(mNotificationAndroidBundle.getBundle("style"));
}
+ /**
+ * Returns a person
+ *
+ * @return AndroidPerson
+ */
+ public @Nullable NotificationAndroidPersonModel getPerson() {
+ if (mNotificationAndroidBundle.containsKey("person")) {
+ return null;
+ }
+
+ return NotificationAndroidPersonModel.fromBundle(mNotificationAndroidBundle.getBundle("person"));
+ }
+
/**
* Gets the ticker text
*
diff --git a/android/src/main/java/app/notifee/core/model/NotificationAndroidPersonModel.java b/android/src/main/java/app/notifee/core/model/NotificationAndroidPersonModel.java
new file mode 100644
index 000000000..5ee1ab7e5
--- /dev/null
+++ b/android/src/main/java/app/notifee/core/model/NotificationAndroidPersonModel.java
@@ -0,0 +1,154 @@
+package app.notifee.core.model;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.Person;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import app.notifee.core.Logger;
+import app.notifee.core.utility.ResourceUtils;
+
+@Keep
+public class NotificationAndroidPersonModel {
+ private static final String TAG = "NotificationAndroidPersonModel";
+
+ private Bundle mNotificationAndroidPersonBundle;
+
+ private NotificationAndroidPersonModel(Bundle actionBundle) {
+ mNotificationAndroidPersonBundle = actionBundle;
+ }
+
+ public static NotificationAndroidPersonModel fromBundle(Bundle actionBundle) {
+ return new NotificationAndroidPersonModel(actionBundle);
+ }
+
+ public Bundle toBundle() {
+ return (Bundle) mNotificationAndroidPersonBundle.clone();
+ }
+
+ /**
+ * Gets the id of the person
+ *
+ * @return String
+ */
+ public @NonNull
+ String getId() {
+ return mNotificationAndroidPersonBundle.getString("id");
+ }
+
+ /**
+ * Gets the name of the person
+ *
+ * @return String
+ */
+ public @Nullable
+ String getName() {
+ return mNotificationAndroidPersonBundle.getString("name");
+ }
+
+ /**
+ * Gets the bot of the person
+ *
+ * @return Boolean
+ */
+ public @NonNull
+ Boolean getBot() {
+ return mNotificationAndroidPersonBundle.getBoolean("bot", false);
+ }
+
+ /**
+ * Gets the bot of the person
+ *
+ * @return Boolean
+ */
+ public @NonNull
+ Boolean getImportant() {
+ return mNotificationAndroidPersonBundle.getBoolean("important", false);
+ }
+
+ /**
+ * Gets the icon of the person
+ *
+ * @return String
+ */
+ public @Nullable
+ String getIcon() {
+ return mNotificationAndroidPersonBundle.getString("icon");
+ }
+
+ /**
+ * Gets the icon of the person
+ *
+ * @return String
+ */
+ public @Nullable
+ String getUri() {
+ return mNotificationAndroidPersonBundle.getString("uri");
+ }
+
+ /**
+ * Converts a person bundle from JS into a Person
+ *
+ * @return Person
+ */
+ public
+ Task buildPersonTask(Executor executor) {
+ return Tasks.call(
+ executor,
+ () -> {
+ Person.Builder personBuilder = new Person.Builder();
+ personBuilder.setName(getName());
+
+ if (mNotificationAndroidPersonBundle.containsKey("id")) {
+ personBuilder.setKey(getId());
+ }
+
+ personBuilder.setBot(getBot());
+ personBuilder.setImportant(getImportant());
+
+
+ if (mNotificationAndroidPersonBundle.containsKey("icon")) {
+ String personIcon = Objects.requireNonNull(getIcon());
+ Bitmap personIconBitmap = null;
+
+ try {
+ personIconBitmap =
+ Tasks.await(
+ ResourceUtils.getImageBitmapFromUrl(personIcon), 10, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ Logger.e(
+ TAG,
+ "Timeout occurred whilst trying to retrieve a person icon: " + personIcon,
+ e);
+ } catch (Exception e) {
+ Logger.e(
+ TAG,
+ "An error occurred whilst trying to retrieve a person icon: " + personIcon,
+ e);
+ }
+
+ if (personIconBitmap != null) {
+ personBuilder.setIcon(IconCompat.createWithAdaptiveBitmap(personIconBitmap));
+ }
+ }
+
+ if (mNotificationAndroidPersonBundle.containsKey("uri")) {
+ personBuilder.setUri(getUri());
+ }
+
+ return personBuilder.build();
+ });
+ }
+}
diff --git a/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java b/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java
index a262f7f7b..46301e5ad 100644
--- a/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java
+++ b/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java
@@ -2,22 +2,26 @@
import android.graphics.Bitmap;
import android.os.Bundle;
+
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import androidx.core.graphics.drawable.IconCompat;
-import app.notifee.core.Logger;
-import app.notifee.core.utility.ResourceUtils;
-import app.notifee.core.utility.TextUtils;
+
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
+
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import app.notifee.core.Logger;
+import app.notifee.core.utility.ResourceUtils;
+import app.notifee.core.utility.TextUtils;
+
@Keep
public class NotificationAndroidStyleModel {
private static final String TAG = "NotificationAndroidStyle";
@@ -295,6 +299,7 @@ private Task getMessagingStyleTask(Executor executor)
long timestamp = (long) message.getDouble("timestamp");
if (message.containsKey("person")) {
+ // TODO: use AndroidPersonModel.buildPerson()
messagePerson =
Tasks.await(
getPerson(executor, Objects.requireNonNull(message.getBundle("person"))),
diff --git a/android/src/main/java/app/notifee/core/utility/IntentUtils.java b/android/src/main/java/app/notifee/core/utility/IntentUtils.java
index fdaed80d2..599bfcc5f 100644
--- a/android/src/main/java/app/notifee/core/utility/IntentUtils.java
+++ b/android/src/main/java/app/notifee/core/utility/IntentUtils.java
@@ -1,11 +1,13 @@
package app.notifee.core.utility;
+import static app.notifee.core.ContextHolder.getApplicationContext;
+
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import app.notifee.core.ContextHolder;
+import androidx.annotation.Nullable;
import app.notifee.core.Logger;
import java.util.List;
@@ -54,7 +56,7 @@ public static void startActivityOnUiThread(Activity activity, Intent intent) {
return;
}
- Context ctx = ContextHolder.getApplicationContext();
+ Context ctx = getApplicationContext();
if (ctx == null) {
Logger.w(TAG, "Unable to get application context when calling startActivityOnUiThread()");
}
@@ -68,4 +70,50 @@ public static void startActivityOnUiThread(Activity activity, Intent intent) {
}
});
}
+
+ public static Class> getLaunchActivity(@Nullable String launchActivity) {
+ String activity;
+
+ if (launchActivity != null && !launchActivity.equals("default")) {
+ activity = launchActivity;
+ } else {
+ activity = getMainActivityClassName();
+ }
+
+ if (activity == null) {
+ Logger.e("ReceiverService", "Launch Activity for notification could not be found.");
+ return null;
+ }
+
+ Class> launchActivityClass = getClassForName(activity);
+
+ if (launchActivityClass == null) {
+ Logger.e(
+ "ReceiverService",
+ String.format("Launch Activity for notification does not exist ('%s').", launchActivity));
+ return null;
+ }
+
+ return launchActivityClass;
+ }
+
+ private @Nullable static Class> getClassForName(String className) {
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ private @Nullable static String getMainActivityClassName() {
+ String packageName = getApplicationContext().getPackageName();
+ Intent launchIntent =
+ getApplicationContext().getPackageManager().getLaunchIntentForPackage(packageName);
+
+ if (launchIntent == null || launchIntent.getComponent() == null) {
+ return null;
+ }
+
+ return launchIntent.getComponent().getClassName();
+ }
}
diff --git a/packages/react-native b/packages/react-native
index 968aa867d..83236e36e 160000
--- a/packages/react-native
+++ b/packages/react-native
@@ -1 +1 @@
-Subproject commit 968aa867d09b623951cffc3c6ad3b95897c3e0d1
+Subproject commit 83236e36eb1164368e9fe605543d287a33e24369
diff --git a/tests_react_native/android/app/src/main/AndroidManifest.xml b/tests_react_native/android/app/src/main/AndroidManifest.xml
index bc23857e3..197bcdac7 100755
--- a/tests_react_native/android/app/src/main/AndroidManifest.xml
+++ b/tests_react_native/android/app/src/main/AndroidManifest.xml
@@ -20,12 +20,24 @@
android:name="com.notifee.testing.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
- android:windowSoftInputMode="adjustResize">
+ android:windowSoftInputMode="adjustResize"
+ android:showWhenLocked="true"
+ android:turnScreenOn="true">
-
+
+
diff --git a/tests_react_native/android/app/src/main/java/com/notifee/testing/BubbleActivity.java b/tests_react_native/android/app/src/main/java/com/notifee/testing/BubbleActivity.java
new file mode 100644
index 000000000..2d545ff34
--- /dev/null
+++ b/tests_react_native/android/app/src/main/java/com/notifee/testing/BubbleActivity.java
@@ -0,0 +1,17 @@
+package com.notifee.testing;
+
+import android.os.Bundle;
+import com.facebook.react.ReactActivity;
+
+public class BubbleActivity extends ReactActivity {
+
+ @Override
+ protected String getMainComponentName() {
+ return "bubble";
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/tests_react_native/android/app/src/main/java/com/notifee/testing/FullScreenActivity.java b/tests_react_native/android/app/src/main/java/com/notifee/testing/FullScreenActivity.java
new file mode 100644
index 000000000..c48a8acc4
--- /dev/null
+++ b/tests_react_native/android/app/src/main/java/com/notifee/testing/FullScreenActivity.java
@@ -0,0 +1,17 @@
+package com.notifee.testing;
+
+import android.os.Bundle;
+import com.facebook.react.ReactActivity;
+
+public class FullScreenActivity extends ReactActivity {
+
+ @Override
+ protected String getMainComponentName() {
+ return "full_screen";
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+}
diff --git a/tests_react_native/example/app.tsx b/tests_react_native/example/app.tsx
index 9a2153816..0a21d845f 100755
--- a/tests_react_native/example/app.tsx
+++ b/tests_react_native/example/app.tsx
@@ -41,9 +41,9 @@ const colors: { [key: string]: string } = {
const channels: AndroidChannel[] = [
{
name: 'High Importance',
- id: 'high',
+ id: 'highh',
importance: AndroidImportance.HIGH,
- // sound: 'hollow',
+ sound: 'hollow',
},
{
name: '🐴 Sound',
@@ -164,8 +164,8 @@ function Root(): any {
notification.android.channelId = channelId;
const date = new Date(Date.now());
- date.setSeconds(date.getSeconds() + 5);
- Notifee.displayNotification(notification)
+ date.setSeconds(date.getSeconds() + 2);
+ Notifee.createTriggerNotification(notification, { type: 0, timestamp: date.getTime() })
.then(notificationId => setId(notificationId))
.catch(console.error);
}
@@ -335,7 +335,7 @@ Notifee.registerForegroundService(notification => {
*/
async function stopService(id?: string): Promise {
console.warn('Stopping service, using notification id: ' + id);
- clearInterval(interval);
+ // clearInterval(interval);
if (id) {
await Notifee.cancelNotification(id);
}
@@ -361,20 +361,20 @@ Notifee.registerForegroundService(notification => {
Notifee.onBackgroundEvent(handleStopActionEvent);
// A fake progress updater.
- let current = 1;
- const interval = setInterval(async () => {
- notification.android = {
- progress: { current: current },
- };
- Notifee.displayNotification(notification);
- current++;
- }, 125);
-
- setTimeout(async () => {
- clearInterval(interval);
- console.warn('Background work has completed.');
- await stopService(notification.id);
- }, 15000);
+ // let current = 1;
+ // const interval = setInterval(async () => {
+ // notification.android = {
+ // progress: { current: current },
+ // };
+ // Notifee.displayNotification(notification);
+ // current++;
+ // }, 125);
+
+ // setTimeout(async () => {
+ // clearInterval(interval);
+ // console.warn('Background work has completed.');
+ // await stopService(notification.id);
+ // }, 15000);
});
});
@@ -415,4 +415,34 @@ function TestComponent(): any {
AppRegistry.registerComponent('test_component', () => TestComponent);
+function FullScreenComponent(): any {
+ return (
+ // eslint-disable-next-line react-native/no-inline-styles
+
+ FullScreen Component
+
+ );
+}
+
+function CustomComponent() {
+ return (
+
+ custom component
+
+ );
+}
+
+AppRegistry.registerComponent('full_screen', () => FullScreenComponent);
+AppRegistry.registerComponent('custom-component', () => CustomComponent);
+
+function BubbleTest() {
+ return (
+
+ Bubbling Component
+
+ );
+}
+
+AppRegistry.registerComponent('bubble', () => BubbleTest);
+
export default Root;
diff --git a/tests_react_native/example/notifications.ts b/tests_react_native/example/notifications.ts
index f3ef5558b..a0b5b06d9 100644
--- a/tests_react_native/example/notifications.ts
+++ b/tests_react_native/example/notifications.ts
@@ -1,4 +1,10 @@
-import { AndroidStyle, Notification, AndroidLaunchActivityFlag } from '@notifee/react-native';
+import {
+ AndroidStyle,
+ Notification,
+ AndroidLaunchActivityFlag,
+ AndroidCategory,
+ AndroidImportance,
+} from '@notifee/react-native';
export const notifications: { key: string; notification: Notification | Notification[] }[] = [
{
@@ -15,14 +21,52 @@ export const notifications: { key: string; notification: Notification | Notifica
},
},
},
+ {
+ key: 'FullScreenAction',
+ notification: {
+ title: 'Full-screen',
+ android: {
+ asForegroundService: false,
+ channelId: 'high',
+ autoCancel: false,
+ category: AndroidCategory.CALL,
+ importance: AndroidImportance.HIGH,
+ fullScreenAction: {
+ id: 'default',
+ launchActivity: 'default',
+ // launchActivity: 'com.notifee.testing.FullScreenActivity',
+ // launchActivityFlags: [AndroidLaunchActivityFlag.SINGLE_TOP],
+ mainComponent: 'full_screen',
+ },
+ },
+ },
+ },
+ {
+ key: 'BubbleAction',
+ notification: {
+ title: 'Bubble',
+ android: {
+ asForegroundService: false,
+ channelId: 'high',
+ autoCancel: false,
+ category: AndroidCategory.CALL,
+ importance: AndroidImportance.HIGH,
+ fullScreenAction: {
+ id: 'default',
+ launchActivity: 'default',
+ // launchActivity: 'com.notifee.testing.BubbleActivity',
+ // launchActivityFlags: [AndroidLaunchActivityFlag.SINGLE_TOP],
+ mainComponent: 'custom-component',
+ },
+ },
+ },
+ },
{
key: 'Basic',
notification: {
title: 'Title',
android: {
- showTimestamp: true,
- channelId: 'foo',
- largeIcon: 'https://storage.googleapis.com/static.invertase.io/assets/avatars/female.png',
+ channelId: 'high',
},
},
},