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', }, }, },