From 198fd334bb17ad7ddfe3b0e61e2153a37db951c6 Mon Sep 17 00:00:00 2001 From: Meypod <106489260+meypod@users.noreply.github.com> Date: Sat, 5 Oct 2024 14:17:13 +0330 Subject: [PATCH] feat(android): Remove all gms usages and use guava instead (#510) * feat(android): Remove all gms usages and use guava instead * chore: add docs for added utility classes * chore: annotate Callbackable.call parameters * fix: remove gms from rn package build.gradle * fix: remove gms from tests as well * test(android): note that google-services plugin still needed for RNFB e2e test * style(lint, java): run code through updated google-java-format * refactor(android): move main executor service to Notifee / reformat --------- Co-authored-by: Mike Hardy --- android/build.gradle | 4 +- .../java/app/notifee/core/ChannelManager.java | 350 +++++---- .../main/java/app/notifee/core/Notifee.java | 364 +++++---- .../app/notifee/core/NotifeeAlarmManager.java | 197 ++--- .../app/notifee/core/NotificationManager.java | 709 +++++++++--------- .../core/database/NotifeeCoreDatabase.java | 4 + .../core/database/WorkDataRepository.java | 28 +- .../core/model/NotificationAndroidModel.java | 2 +- .../model/NotificationAndroidStyleModel.java | 60 +- .../core/model/TimestampTriggerModel.java | 2 +- .../notifee/core/utility/Callbackable.java | 12 + .../utility/ExtendedListenableFuture.java | 84 +++ .../notifee/core/utility/ResourceUtils.java | 19 +- package.json | 2 +- .../background/BackgroundExecutor.java | 1 + packages/react-native/android/build.gradle | 1 - tests_react_native/android/app/build.gradle | 4 +- tests_react_native/android/build.gradle | 10 +- yarn.lock | 53 +- 19 files changed, 1088 insertions(+), 818 deletions(-) create mode 100644 android/src/main/java/app/notifee/core/utility/Callbackable.java create mode 100644 android/src/main/java/app/notifee/core/utility/ExtendedListenableFuture.java diff --git a/android/build.gradle b/android/build.gradle index 7362c326f..11696bc00 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -105,10 +105,12 @@ dependencies { api 'androidx.annotation:annotation:1.3.0' // https://developer.android.com/jetpack/androidx/releases/annotation api 'androidx.concurrent:concurrent-futures:1.1.0' // https://developer.android.com/jetpack/androidx/releases/concurrent - api 'com.google.android.gms:play-services-tasks:18.0.1' // https://developers.google.com/android/guides/releases api 'androidx.work:work-runtime:2.8.0' // https://developer.android.com/jetpack/androidx/releases/work api 'com.facebook.fresco:fresco:2.6.0' // https://github.com/facebook/fresco/releases + implementation("com.google.guava:guava:31.1-android") // https://github.com/google/guava + implementation 'androidx.core:core:1.6.0' + def room_version = '2.5.0' // https://developer.android.com/jetpack/androidx/releases/room implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" diff --git a/android/src/main/java/app/notifee/core/ChannelManager.java b/android/src/main/java/app/notifee/core/ChannelManager.java index 959d1bc3d..776c75f2b 100644 --- a/android/src/main/java/app/notifee/core/ChannelManager.java +++ b/android/src/main/java/app/notifee/core/ChannelManager.java @@ -31,122 +31,120 @@ import app.notifee.core.model.ChannelModel; import app.notifee.core.utility.ColorUtils; import app.notifee.core.utility.ResourceUtils; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; +import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; public class ChannelManager { + private static String TAG = "ChannelManager"; - private static ExecutorService executorService = Executors.newCachedThreadPool(); - - static Task createChannel(ChannelModel channelModel) { - return Tasks.call( - executorService, - () -> { - if (Build.VERSION.SDK_INT < 26) { - return null; - } - - NotificationChannel channel = - new NotificationChannel( - channelModel.getId(), channelModel.getName(), channelModel.getImportance()); - - channel.setShowBadge(channelModel.getBadge()); - channel.setBypassDnd(channelModel.getBypassDnd()); - channel.setDescription(channelModel.getDescription()); - channel.setGroup(channelModel.getGroupId()); - channel.enableLights(channelModel.getLights()); - - if (channelModel.getLightColor() != null) { - channel.setLightColor(channelModel.getLightColor()); - } - - channel.setLockscreenVisibility(channelModel.getVisibility()); - channel.enableVibration(channelModel.getVibration()); - - long[] vibrationPattern = channelModel.getVibrationPattern(); - if (vibrationPattern.length > 0) { - channel.setVibrationPattern(vibrationPattern); - } - - if (channelModel.getSound() != null) { - Uri soundUri = ResourceUtils.getSoundUri(channelModel.getSound()); - if (soundUri != null) { - AudioAttributes audioAttributes = - new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .build(); - channel.setSound(soundUri, audioAttributes); - } else { - Logger.w( - TAG, - "Unable to retrieve sound for channel, sound was specified as: " - + channel.getSound()); - } - } else { - channel.setSound(null, null); - } - - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .createNotificationChannel(channel); - - return null; - }); + + static ListenableFuture createChannel(ChannelModel channelModel) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + if (Build.VERSION.SDK_INT < 26) { + return null; + } + + NotificationChannel channel = + new NotificationChannel( + channelModel.getId(), channelModel.getName(), channelModel.getImportance()); + + channel.setShowBadge(channelModel.getBadge()); + channel.setBypassDnd(channelModel.getBypassDnd()); + channel.setDescription(channelModel.getDescription()); + channel.setGroup(channelModel.getGroupId()); + channel.enableLights(channelModel.getLights()); + + if (channelModel.getLightColor() != null) { + channel.setLightColor(channelModel.getLightColor()); + } + + channel.setLockscreenVisibility(channelModel.getVisibility()); + channel.enableVibration(channelModel.getVibration()); + + long[] vibrationPattern = channelModel.getVibrationPattern(); + if (vibrationPattern.length > 0) { + channel.setVibrationPattern(vibrationPattern); + } + + if (channelModel.getSound() != null) { + Uri soundUri = ResourceUtils.getSoundUri(channelModel.getSound()); + if (soundUri != null) { + AudioAttributes audioAttributes = + new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(); + channel.setSound(soundUri, audioAttributes); + } else { + Logger.w( + TAG, + "Unable to retrieve sound for channel, sound was specified as: " + + channel.getSound()); + } + } else { + channel.setSound(null, null); + } + + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .createNotificationChannel(channel); + + return null; + }); } - static Task createChannels(List channelModels) { - return Tasks.call( - executorService, - () -> { - for (ChannelModel channelModel : channelModels) { - Tasks.await(createChannel(channelModel)); - } + static ListenableFuture createChannels(List channelModels) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + for (ChannelModel channelModel : channelModels) { + createChannel(channelModel).get(); + } - return null; - }); + return null; + }); } - static Task createChannelGroup(ChannelGroupModel channelGroupModel) { - return Tasks.call( - executorService, - () -> { - if (Build.VERSION.SDK_INT < 26) { - return null; - } + static ListenableFuture createChannelGroup(ChannelGroupModel channelGroupModel) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + if (Build.VERSION.SDK_INT < 26) { + return null; + } - NotificationChannelGroup notificationChannelGroup = - new NotificationChannelGroup(channelGroupModel.getId(), channelGroupModel.getName()); + NotificationChannelGroup notificationChannelGroup = + new NotificationChannelGroup( + channelGroupModel.getId(), channelGroupModel.getName()); - if (Build.VERSION.SDK_INT >= 28 && channelGroupModel.getDescription() != null) { - notificationChannelGroup.setDescription(channelGroupModel.getDescription()); - } + if (Build.VERSION.SDK_INT >= 28 && channelGroupModel.getDescription() != null) { + notificationChannelGroup.setDescription(channelGroupModel.getDescription()); + } - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .createNotificationChannelGroup(notificationChannelGroup); + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .createNotificationChannelGroup(notificationChannelGroup); - return null; - }); + return null; + }); } - static Task createChannelGroups(List channelGroupModels) { - return Tasks.call( - executorService, - () -> { - if (Build.VERSION.SDK_INT < 26) { - return null; - } + static ListenableFuture createChannelGroups(List channelGroupModels) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + if (Build.VERSION.SDK_INT < 26) { + return null; + } - for (ChannelGroupModel channelGroupModel : channelGroupModels) { - Tasks.await(createChannelGroup(channelGroupModel)); - } + for (ChannelGroupModel channelGroupModel : channelGroupModels) { + createChannelGroup(channelGroupModel).get(); + } - return null; - }); + return null; + }); } static void deleteChannel(@NonNull String channelId) { @@ -159,102 +157,102 @@ static void deleteChannelGroup(@NonNull String channelGroupId) { .deleteNotificationChannelGroup(channelGroupId); } - static Task> getChannels() { - return Tasks.call( - executorService, - () -> { - List channels = - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .getNotificationChannels(); - - if (channels.size() == 0 || Build.VERSION.SDK_INT < 26) { - return Collections.emptyList(); - } - - ArrayList channelBundles = new ArrayList<>(channels.size()); - for (NotificationChannel channel : channels) { - channelBundles.add(convertChannelToBundle(channel)); - } - - return channelBundles; - }); + static ListenableFuture> getChannels() { + return Notifee.getListeningExecutorService() + .submit( + () -> { + List channels = + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .getNotificationChannels(); + + if (channels.size() == 0 || Build.VERSION.SDK_INT < 26) { + return Collections.emptyList(); + } + + ArrayList channelBundles = new ArrayList<>(channels.size()); + for (NotificationChannel channel : channels) { + channelBundles.add(convertChannelToBundle(channel)); + } + + return channelBundles; + }); } - static Task getChannel(String channelId) { - return Tasks.call( - executorService, - () -> { - NotificationChannel channel = - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .getNotificationChannel(channelId); + static ListenableFuture getChannel(String channelId) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + NotificationChannel channel = + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .getNotificationChannel(channelId); - return convertChannelToBundle(channel); - }); + return convertChannelToBundle(channel); + }); } - static Task isChannelBlocked(String channelId) { - return Tasks.call( - executorService, - () -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; + static ListenableFuture isChannelBlocked(String channelId) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; - NotificationChannel channel = - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .getNotificationChannel(channelId); + NotificationChannel channel = + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .getNotificationChannel(channelId); - if (channel == null) { - return false; - } + if (channel == null) { + return false; + } - return IMPORTANCE_NONE == channel.getImportance(); - }); + return IMPORTANCE_NONE == channel.getImportance(); + }); } - static Task isChannelCreated(String channelId) { - return Tasks.call( - executorService, - () -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; + static ListenableFuture isChannelCreated(String channelId) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; - NotificationChannel channel = - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .getNotificationChannel(channelId); + NotificationChannel channel = + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .getNotificationChannel(channelId); - return channel != null; - }); + return channel != null; + }); } - static Task> getChannelGroups() { - return Tasks.call( - executorService, - () -> { - List channelGroups = - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .getNotificationChannelGroups(); - - if (channelGroups.size() == 0 || Build.VERSION.SDK_INT < 26) { - return Collections.emptyList(); - } - - ArrayList channelGroupBundles = new ArrayList<>(channelGroups.size()); - for (NotificationChannelGroup channelGroup : channelGroups) { - channelGroupBundles.add(convertChannelGroupToBundle(channelGroup)); - } - - return channelGroupBundles; - }); + static ListenableFuture> getChannelGroups() { + return Notifee.getListeningExecutorService() + .submit( + () -> { + List channelGroups = + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .getNotificationChannelGroups(); + + if (channelGroups.size() == 0 || Build.VERSION.SDK_INT < 26) { + return Collections.emptyList(); + } + + ArrayList channelGroupBundles = new ArrayList<>(channelGroups.size()); + for (NotificationChannelGroup channelGroup : channelGroups) { + channelGroupBundles.add(convertChannelGroupToBundle(channelGroup)); + } + + return channelGroupBundles; + }); } - static Task getChannelGroup(String channelGroupId) { - return Tasks.call( - executorService, - () -> { - NotificationChannelGroup channelGroup = - NotificationManagerCompat.from(ContextHolder.getApplicationContext()) - .getNotificationChannelGroup(channelGroupId); + static ListenableFuture getChannelGroup(String channelGroupId) { + return Notifee.getListeningExecutorService() + .submit( + () -> { + NotificationChannelGroup channelGroup = + NotificationManagerCompat.from(ContextHolder.getApplicationContext()) + .getNotificationChannelGroup(channelGroupId); - return convertChannelGroupToBundle(channelGroup); - }); + return convertChannelGroupToBundle(channelGroup); + }); } private static Bundle convertChannelToBundle(NotificationChannel channel) { diff --git a/android/src/main/java/app/notifee/core/Notifee.java b/android/src/main/java/app/notifee/core/Notifee.java index 446cd1df7..138c27aca 100644 --- a/android/src/main/java/app/notifee/core/Notifee.java +++ b/android/src/main/java/app/notifee/core/Notifee.java @@ -37,8 +37,14 @@ import app.notifee.core.model.NotificationModel; import app.notifee.core.utility.AlarmUtils; import app.notifee.core.utility.PowerManagerUtils; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; @KeepForSdk public class Notifee { @@ -46,8 +52,17 @@ public class Notifee { private static Notifee mNotifee = null; private static boolean mIsnitialized = false; + private static final ExecutorService executorService = Executors.newCachedThreadPool(); + private static final ListeningExecutorService lExecutorService = + MoreExecutors.listeningDecorator(executorService); + @KeepForSdk public static final int REQUEST_CODE_NOTIFICATION_PERMISSION = 11111; + @KeepForSdk + public static ListeningExecutorService getListeningExecutorService() { + return lExecutorService; + } + @KeepForSdk public static Notifee getInstance() { if (!mIsnitialized) { @@ -100,29 +115,39 @@ public static void initialize(@Nullable EventListener eventListener) { */ @KeepForSdk public void cancelAllNotifications(int notificationType, MethodCallResult result) { - NotificationManager.cancelAllNotifications(notificationType) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + NotificationManager.cancelAllNotifications(notificationType), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void cancelAllNotificationsWithIds( int type, List ids, String tag, MethodCallResult result) { - NotificationManager.cancelAllNotificationsWithIds(type, ids, tag) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + NotificationManager.cancelAllNotificationsWithIds(type, ids, tag), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk @@ -134,15 +159,20 @@ public void openAlarmPermissionSettings(Activity activity, MethodCallResult result) { ChannelModel channelModel = ChannelModel.fromBundle(channelMap); - ChannelManager.createChannel(channelModel) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.createChannel(channelModel), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk @@ -151,30 +181,39 @@ public void createChannels(List channelsList, MethodCallResult res for (Bundle bundle : channelsList) { channelModels.add(ChannelModel.fromBundle(bundle)); } + Futures.addCallback( + ChannelManager.createChannels(channelModels), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } - ChannelManager.createChannels(channelModels) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void createChannelGroup(Bundle channelGroupMap, MethodCallResult result) { ChannelGroupModel channelGroupModel = ChannelGroupModel.fromBundle(channelGroupMap); - ChannelManager.createChannelGroup(channelGroupModel) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.createChannelGroup(channelGroupModel), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk @@ -183,16 +222,20 @@ public void createChannelGroups(List channelGroupsList, MethodCallResult for (Bundle bundle : channelGroupsList) { channelGroupModels.add(ChannelGroupModel.fromBundle(bundle)); } + Futures.addCallback( + ChannelManager.createChannelGroups(channelGroupModels), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } - ChannelManager.createChannelGroups(channelGroupModels) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk @@ -210,32 +253,44 @@ public void deleteChannelGroup(String channelGroupId, MethodCallResult res @KeepForSdk public void displayNotification(Bundle notificationMap, MethodCallResult result) { NotificationModel notificationModel = NotificationModel.fromBundle(notificationMap); - NotificationManager.displayNotification(notificationModel, null) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, null); - } else { - Logger.e(TAG, "displayNotification", task.getException()); - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + NotificationManager.displayNotification(notificationModel, null), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + Exception e = new Exception(t); + Logger.e(TAG, "displayNotification", e); + result.onComplete(e, null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void createTriggerNotification( Bundle notificationMap, Bundle triggerMap, MethodCallResult result) { NotificationModel notificationModel = NotificationModel.fromBundle(notificationMap); - NotificationManager.createTriggerNotification(notificationModel, triggerMap) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, null); - } else { - Logger.e(TAG, "createTriggerNotification", task.getException()); - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + NotificationManager.createTriggerNotification(notificationModel, triggerMap), + new FutureCallback() { + @Override + public void onSuccess(Void taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + Exception e = new Exception(t); + Logger.e(TAG, "createTriggerNotification", e); + result.onComplete(e, null); + } + }, + getListeningExecutorService()); } @KeepForSdk @@ -245,15 +300,20 @@ public void getTriggerNotificationIds(MethodCallResult> result) { @KeepForSdk public void getDisplayedNotifications(MethodCallResult> result) { - NotificationManager.getDisplayedNotifications() - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + NotificationManager.getDisplayedNotifications(), + new FutureCallback>() { + @Override + public void onSuccess(List taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk @@ -263,80 +323,110 @@ public void getTriggerNotifications(MethodCallResult> result) { @KeepForSdk public void getChannels(MethodCallResult> result) { - ChannelManager.getChannels() - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.getChannels(), + new FutureCallback>() { + @Override + public void onSuccess(List taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void getChannel(String channelId, MethodCallResult result) { - ChannelManager.getChannel(channelId) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.getChannel(channelId), + new FutureCallback() { + @Override + public void onSuccess(Bundle taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void getChannelGroups(MethodCallResult> result) { - ChannelManager.getChannelGroups() - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.getChannelGroups(), + new FutureCallback>() { + @Override + public void onSuccess(List taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void getChannelGroup(String channelGroupId, MethodCallResult result) { - ChannelManager.getChannelGroup(channelGroupId) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.getChannelGroup(channelGroupId), + new FutureCallback() { + @Override + public void onSuccess(Bundle taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void isChannelCreated(String channelId, MethodCallResult result) { - ChannelManager.isChannelCreated(channelId) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.isChannelCreated(channelId), + new FutureCallback() { + @Override + public void onSuccess(Boolean taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk public void isChannelBlocked(String channelId, MethodCallResult result) { - ChannelManager.isChannelBlocked(channelId) - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - result.onComplete(null, task.getResult()); - } else { - result.onComplete(task.getException(), null); - } - }); + Futures.addCallback( + ChannelManager.isChannelBlocked(channelId), + new FutureCallback() { + @Override + public void onSuccess(Boolean taskResult) { + result.onComplete(null, taskResult); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + getListeningExecutorService()); } @KeepForSdk diff --git a/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java b/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java index 2e246241f..7b767b055 100644 --- a/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +++ b/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java @@ -31,9 +31,13 @@ import app.notifee.core.model.NotificationModel; import app.notifee.core.model.TimestampTriggerModel; import app.notifee.core.utility.AlarmUtils; +import app.notifee.core.utility.ExtendedListenableFuture; import app.notifee.core.utility.ObjectUtils; -import com.google.android.gms.tasks.Continuation; -import com.google.android.gms.tasks.Task; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutorService; @@ -43,6 +47,8 @@ class NotifeeAlarmManager { private static final String TAG = "NotifeeAlarmManager"; private static final String NOTIFICATION_ID_INTENT_KEY = "notificationId"; private static final ExecutorService alarmManagerExecutor = Executors.newCachedThreadPool(); + private static final ListeningExecutorService alarmManagerListeningExecutor = + MoreExecutors.listeningDecorator(alarmManagerExecutor); static void displayScheduledNotification(Bundle alarmManagerNotification) { if (alarmManagerNotification == null) { @@ -56,70 +62,68 @@ static void displayScheduledNotification(Bundle alarmManagerNotification) { WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext()); - Continuation> workContinuation = - task -> { - WorkDataEntity workDataEntity = task.getResult(); - - Bundle notificationBundle; - - Bundle triggerBundle; - - if (workDataEntity == null - || workDataEntity.getNotification() == null - || workDataEntity.getTrigger() == null) { - // check if notification bundle is stored with Work Manager - Logger.w( - TAG, "Attempted to handle doScheduledWork but no notification data was found."); - return null; - } else { - triggerBundle = ObjectUtils.bytesToBundle(workDataEntity.getTrigger()); - notificationBundle = ObjectUtils.bytesToBundle(workDataEntity.getNotification()); - } - - NotificationModel notificationModel = NotificationModel.fromBundle(notificationBundle); - - return NotificationManager.displayNotification(notificationModel, triggerBundle) - .addOnCompleteListener( - displayNotificationTask -> { - if (!displayNotificationTask.isSuccessful()) { - Logger.e( - TAG, - "Failed to display notification", - displayNotificationTask.getException()); - } else { - if (triggerBundle.containsKey("repeatFrequency") - && ObjectUtils.getInt(triggerBundle.get("repeatFrequency")) != -1) { - TimestampTriggerModel trigger = - TimestampTriggerModel.fromBundle(triggerBundle); - // Ensure trigger is in the future and the latest timestamp is updated in - // the database - trigger.setNextTimestamp(); - scheduleTimestampTriggerNotification(notificationModel, trigger); - WorkDataRepository.getInstance(getApplicationContext()) - .update( - new WorkDataEntity( - id, - workDataEntity.getNotification(), - ObjectUtils.bundleToBytes(triggerBundle), - true)); - } else { - // not repeating, delete database entry if work is a one-time request - WorkDataRepository.getInstance(getApplicationContext()).deleteById(id); - } - } - }); - }; - - workDataRepository - .getWorkDataById(id) - .continueWithTask(alarmManagerExecutor, workContinuation) - .addOnCompleteListener( - task -> { - if (!task.isSuccessful()) { - Logger.e(TAG, "Failed to display notification", task.getException()); - return; - } - }); + ListenableFuture displayFuture = + new ExtendedListenableFuture<>(workDataRepository.getWorkDataById(id)) + .continueWith( + workDataEntity -> { + Bundle notificationBundle; + + Bundle triggerBundle; + + if (workDataEntity == null + || workDataEntity.getNotification() == null + || workDataEntity.getTrigger() == null) { + // check if notification bundle is stored with Work Manager + Logger.w( + TAG, + "Attempted to handle doScheduledWork but no notification data was found."); + return Futures.immediateFuture(null); + } else { + triggerBundle = ObjectUtils.bytesToBundle(workDataEntity.getTrigger()); + notificationBundle = + ObjectUtils.bytesToBundle(workDataEntity.getNotification()); + } + + NotificationModel notificationModel = + NotificationModel.fromBundle(notificationBundle); + + return new ExtendedListenableFuture<>( + NotificationManager.displayNotification(notificationModel, triggerBundle)) + .continueWith( + voidDisplayedNotification -> { + if (triggerBundle.containsKey("repeatFrequency") + && ObjectUtils.getInt(triggerBundle.get("repeatFrequency")) != -1) { + TimestampTriggerModel trigger = + TimestampTriggerModel.fromBundle(triggerBundle); + // Ensure trigger is in the future and the latest timestamp is updated + // in + // the database + trigger.setNextTimestamp(); + scheduleTimestampTriggerNotification(notificationModel, trigger); + WorkDataRepository.getInstance(getApplicationContext()) + .update( + new WorkDataEntity( + id, + workDataEntity.getNotification(), + ObjectUtils.bundleToBytes(triggerBundle), + true)); + } else { + // not repeating, delete database entry if work is a one-time request + WorkDataRepository.getInstance(getApplicationContext()) + .deleteById(id); + } + return Futures.immediateFuture(null); + }, + alarmManagerExecutor); + }, + alarmManagerExecutor) + .addOnCompleteListener( + (e, result) -> { + if (e != null) { + Logger.e(TAG, "Failed to display notification", e); + } + }, + alarmManagerExecutor); } public static PendingIntent getAlarmManagerIntentForNotification(String notificationId) { @@ -210,7 +214,7 @@ static void scheduleTimestampTriggerNotification( } } - Task> getScheduledNotifications() { + ListenableFuture> getScheduledNotifications() { WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext()); return workDataRepository.getAllWithAlarmManager(true); } @@ -223,28 +227,20 @@ public static void cancelNotification(String notificationId) { } } - public static Continuation cancelAllNotifications() { - - Continuation continuation = - task -> { - WorkDataRepository workDataRepository = - WorkDataRepository.getInstance(getApplicationContext()); - - return workDataRepository - .getAllWithAlarmManager(true) - .continueWith( - resultTask -> { - if (resultTask.isSuccessful()) { - List workDataEntities = resultTask.getResult(); - for (WorkDataEntity workDataEntity : workDataEntities) { - NotifeeAlarmManager.cancelNotification(workDataEntity.getId()); - } - } - return null; - }); - }; - - return continuation; + public static ListenableFuture cancelAllNotifications() { + WorkDataRepository workDataRepository = WorkDataRepository.getInstance(getApplicationContext()); + + return new ExtendedListenableFuture<>(workDataRepository.getAllWithAlarmManager(true)) + .continueWith( + workDataEntities -> { + if (workDataEntities != null) { + for (WorkDataEntity workDataEntity : workDataEntities) { + NotifeeAlarmManager.cancelNotification(workDataEntity.getId()); + } + } + return Futures.immediateFuture(null); + }, + alarmManagerListeningExecutor); } /* On reboot, reschedule trigger notifications created via alarm manager */ @@ -279,14 +275,21 @@ void rescheduleNotification(WorkDataEntity workDataEntity) { void rescheduleNotifications() { Logger.d(TAG, "Reschedule Notifications on reboot"); - getScheduledNotifications() - .addOnCompleteListener( - task -> { - List workDataEntities = task.getResult(); + Futures.addCallback( + getScheduledNotifications(), + new FutureCallback>() { + @Override + public void onSuccess(List workDataEntities) { + for (WorkDataEntity workDataEntity : workDataEntities) { + rescheduleNotification(workDataEntity); + } + } - for (WorkDataEntity workDataEntity : workDataEntities) { - rescheduleNotification(workDataEntity); - } - }); + @Override + public void onFailure(Throwable t) { + // silently fail + } + }, + alarmManagerListeningExecutor); } } diff --git a/android/src/main/java/app/notifee/core/NotificationManager.java b/android/src/main/java/app/notifee/core/NotificationManager.java index 5c1a98df4..cfa1d49cf 100644 --- a/android/src/main/java/app/notifee/core/NotificationManager.java +++ b/android/src/main/java/app/notifee/core/NotificationManager.java @@ -42,6 +42,7 @@ import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.ExistingWorkPolicy; import androidx.work.ListenableWorker; +import androidx.work.ListenableWorker.Result; import androidx.work.OneTimeWorkRequest; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; @@ -57,14 +58,18 @@ import app.notifee.core.model.NotificationAndroidStyleModel; import app.notifee.core.model.NotificationModel; import app.notifee.core.model.TimestampTriggerModel; +import app.notifee.core.utility.ExtendedListenableFuture; import app.notifee.core.utility.IntentUtils; import app.notifee.core.utility.ObjectUtils; import app.notifee.core.utility.PowerManagerUtils; 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 com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -79,11 +84,13 @@ class NotificationManager { private static final String EXTRA_NOTIFEE_NOTIFICATION = "notifee.notification"; private static final String EXTRA_NOTIFEE_TRIGGER = "notifee.trigger"; private static final ExecutorService CACHED_THREAD_POOL = Executors.newCachedThreadPool(); + private static final ListeningExecutorService LISTENING_CACHED_THREAD_POOL = + MoreExecutors.listeningDecorator(CACHED_THREAD_POOL); private static final int NOTIFICATION_TYPE_ALL = 0; private static final int NOTIFICATION_TYPE_DISPLAYED = 1; private static final int NOTIFICATION_TYPE_TRIGGER = 2; - private static Task notificationBundleToBuilder( + private static ListenableFuture notificationBundleToBuilder( NotificationModel notificationModel) { final NotificationAndroidModel androidModel = notificationModel.getAndroid(); @@ -247,308 +254,325 @@ private static Task notificationBundleToBuilder( /* * A task continuation that fetches the largeIcon through Fresco, if specified. */ - Continuation largeIconContinuation = - task -> { - NotificationCompat.Builder builder = task.getResult(); - - if (androidModel.hasLargeIcon()) { - String largeIcon = androidModel.getLargeIcon(); - Bitmap largeIconBitmap = null; - - try { - largeIconBitmap = - Tasks.await(ResourceUtils.getImageBitmapFromUrl(largeIcon), 10, TimeUnit.SECONDS); - } catch (TimeoutException e) { - Logger.e( - TAG, - "Timeout occurred whilst trying to retrieve a largeIcon image: " + largeIcon, - e); - } catch (Exception e) { - Logger.e( - TAG, - "An error occurred whilst trying to retrieve a largeIcon image: " + largeIcon, - e); - } + AsyncFunction largeIconContinuation = + taskResult -> + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + NotificationCompat.Builder builder = taskResult; - if (largeIconBitmap != null) { - if (androidModel.getCircularLargeIcon()) { - largeIconBitmap = ResourceUtils.getCircularBitmap(largeIconBitmap); - } + if (androidModel.hasLargeIcon()) { + String largeIcon = androidModel.getLargeIcon(); + Bitmap largeIconBitmap = null; - builder.setLargeIcon(largeIconBitmap); - } - } + try { + largeIconBitmap = + ResourceUtils.getImageBitmapFromUrl(largeIcon).get(10, TimeUnit.SECONDS); + } catch (TimeoutException e) { + Logger.e( + TAG, + "Timeout occurred whilst trying to retrieve a largeIcon image: " + + largeIcon, + e); + } catch (Exception e) { + Logger.e( + TAG, + "An error occurred whilst trying to retrieve a largeIcon image: " + + largeIcon, + e); + } - return builder; - }; + if (largeIconBitmap != null) { + if (androidModel.getCircularLargeIcon()) { + largeIconBitmap = ResourceUtils.getCircularBitmap(largeIconBitmap); + } + + builder.setLargeIcon(largeIconBitmap); + } + } + + return builder; + }); /* * A task continuation for full-screen action, if specified. */ - Continuation + AsyncFunction fullScreenActionContinuation = - task -> { - NotificationCompat.Builder builder = task.getResult(); - if (androidModel.hasFullScreenAction()) { - NotificationAndroidPressActionModel fullScreenActionBundle = - androidModel.getFullScreenAction(); - - String launchActivity = fullScreenActionBundle.getLaunchActivity(); - Class launchActivityClass = IntentUtils.getLaunchActivity(launchActivity); - if (launchActivityClass == null) { - Logger.e( - TAG, - String.format( - "Launch Activity for full-screen action does not exist ('%s').", - launchActivity)); - return builder; - } - - Intent launchIntent = new Intent(getApplicationContext(), launchActivityClass); - if (fullScreenActionBundle.getLaunchActivityFlags() != -1) { - launchIntent.addFlags(fullScreenActionBundle.getLaunchActivityFlags()); - } - - if (fullScreenActionBundle.getMainComponent() != null) { - launchIntent.putExtra("mainComponent", fullScreenActionBundle.getMainComponent()); - launchIntent.putExtra("notification", notificationModel.toBundle()); - EventBus.postSticky( - new MainComponentEvent(fullScreenActionBundle.getMainComponent())); - } - - PendingIntent fullScreenPendingIntent = - PendingIntent.getActivity( - getApplicationContext(), - notificationModel.getHashCode(), - launchIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); - builder.setFullScreenIntent(fullScreenPendingIntent, true); - } - - return builder; - }; + taskResult -> + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + NotificationCompat.Builder builder = taskResult; + if (androidModel.hasFullScreenAction()) { + NotificationAndroidPressActionModel fullScreenActionBundle = + androidModel.getFullScreenAction(); + + String launchActivity = fullScreenActionBundle.getLaunchActivity(); + Class launchActivityClass = IntentUtils.getLaunchActivity(launchActivity); + if (launchActivityClass == null) { + Logger.e( + TAG, + String.format( + "Launch Activity for full-screen action does not exist ('%s').", + launchActivity)); + return builder; + } + + Intent launchIntent = + new Intent(getApplicationContext(), launchActivityClass); + if (fullScreenActionBundle.getLaunchActivityFlags() != -1) { + launchIntent.addFlags(fullScreenActionBundle.getLaunchActivityFlags()); + } + + if (fullScreenActionBundle.getMainComponent() != null) { + launchIntent.putExtra( + "mainComponent", fullScreenActionBundle.getMainComponent()); + launchIntent.putExtra("notification", notificationModel.toBundle()); + EventBus.postSticky( + new MainComponentEvent(fullScreenActionBundle.getMainComponent())); + } + + PendingIntent fullScreenPendingIntent = + PendingIntent.getActivity( + getApplicationContext(), + notificationModel.getHashCode(), + launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + builder.setFullScreenIntent(fullScreenPendingIntent, true); + } + + return builder; + }); /* * A task continuation that builds all actions, if any. Additionally fetches * icon bitmaps through Fresco. */ - Continuation actionsContinuation = - task -> { - NotificationCompat.Builder builder = task.getResult(); - ArrayList actionBundles = androidModel.getActions(); - - if (actionBundles == null) { - return builder; - } + AsyncFunction actionsContinuation = + taskResult -> + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + NotificationCompat.Builder builder = taskResult; + ArrayList actionBundles = + androidModel.getActions(); + + if (actionBundles == null) { + return builder; + } - for (NotificationAndroidActionModel actionBundle : actionBundles) { - PendingIntent pendingIntent = null; - int targetSdkVersion = - ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion; - if (targetSdkVersion >= Build.VERSION_CODES.S - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingIntent = - NotificationPendingIntent.createIntent( - notificationModel.getHashCode(), - actionBundle.getPressAction().toBundle(), - TYPE_ACTION_PRESS, - new String[] {"notification", "pressAction"}, - notificationModel.toBundle(), - actionBundle.getPressAction().toBundle()); - } else { - pendingIntent = - ReceiverService.createIntent( - ACTION_PRESS_INTENT, - new String[] {"notification", "pressAction"}, - notificationModel.toBundle(), - actionBundle.getPressAction().toBundle()); - } + for (NotificationAndroidActionModel actionBundle : actionBundles) { + PendingIntent pendingIntent = null; + int targetSdkVersion = + ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion; + if (targetSdkVersion >= Build.VERSION_CODES.S + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + pendingIntent = + NotificationPendingIntent.createIntent( + notificationModel.getHashCode(), + actionBundle.getPressAction().toBundle(), + TYPE_ACTION_PRESS, + new String[] {"notification", "pressAction"}, + notificationModel.toBundle(), + actionBundle.getPressAction().toBundle()); + } else { + pendingIntent = + ReceiverService.createIntent( + ACTION_PRESS_INTENT, + new String[] {"notification", "pressAction"}, + notificationModel.toBundle(), + actionBundle.getPressAction().toBundle()); + } - String icon = actionBundle.getIcon(); - Bitmap iconBitmap = null; - - if (icon != null) { - try { - iconBitmap = - Tasks.await( - ResourceUtils.getImageBitmapFromUrl(actionBundle.getIcon()), - 10, - TimeUnit.SECONDS); - } catch (TimeoutException e) { - Logger.e( - TAG, "Timeout occurred whilst trying to retrieve an action icon: " + icon, e); - } catch (Exception e) { - Logger.e( - TAG, "An error occurred whilst trying to retrieve an action icon: " + icon, e); - } - } + String icon = actionBundle.getIcon(); + Bitmap iconBitmap = null; + + if (icon != null) { + try { + iconBitmap = + ResourceUtils.getImageBitmapFromUrl(actionBundle.getIcon()) + .get(10, TimeUnit.SECONDS); + } catch (TimeoutException e) { + Logger.e( + TAG, + "Timeout occurred whilst trying to retrieve an action icon: " + icon, + e); + } catch (Exception e) { + Logger.e( + TAG, + "An error occurred whilst trying to retrieve an action icon: " + icon, + e); + } + } - IconCompat iconCompat = null; - if (iconBitmap != null) { - iconCompat = IconCompat.createWithAdaptiveBitmap(iconBitmap); - } + IconCompat iconCompat = null; + if (iconBitmap != null) { + iconCompat = IconCompat.createWithAdaptiveBitmap(iconBitmap); + } - NotificationCompat.Action.Builder actionBuilder = - new NotificationCompat.Action.Builder( - iconCompat, TextUtils.fromHtml(actionBundle.getTitle()), pendingIntent); + NotificationCompat.Action.Builder actionBuilder = + new NotificationCompat.Action.Builder( + iconCompat, TextUtils.fromHtml(actionBundle.getTitle()), pendingIntent); - RemoteInput remoteInput = actionBundle.getRemoteInput(actionBuilder); - if (remoteInput != null) { - actionBuilder.addRemoteInput(remoteInput); - } + RemoteInput remoteInput = actionBundle.getRemoteInput(actionBuilder); + if (remoteInput != null) { + actionBuilder.addRemoteInput(remoteInput); + } - builder.addAction(actionBuilder.build()); - } + builder.addAction(actionBuilder.build()); + } - return builder; - }; + 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 styleContinuation = - task -> { - NotificationCompat.Builder builder = task.getResult(); - NotificationAndroidStyleModel androidStyleBundle = androidModel.getStyle(); - if (androidStyleBundle == null) { - return builder; - } + AsyncFunction styleContinuation = + builder -> + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + NotificationAndroidStyleModel androidStyleBundle = androidModel.getStyle(); + if (androidStyleBundle == null) { + return builder; + } - Task styleTask = - androidStyleBundle.getStyleTask(CACHED_THREAD_POOL); - if (styleTask == null) { - return builder; - } + ListenableFuture styleTask = + androidStyleBundle.getStyleTask(LISTENING_CACHED_THREAD_POOL); + if (styleTask == null) { + return builder; + } - NotificationCompat.Style style = Tasks.await(styleTask); - if (style != null) { - builder.setStyle(style); - } + NotificationCompat.Style style = styleTask.get(); + if (style != null) { + builder.setStyle(style); + } - return builder; - }; + return builder; + }); - return Tasks.call(CACHED_THREAD_POOL, builderCallable) + return new ExtendedListenableFuture<>(LISTENING_CACHED_THREAD_POOL.submit(builderCallable)) // get a large image bitmap if largeIcon is set - .continueWith(CACHED_THREAD_POOL, largeIconContinuation) + .continueWith(largeIconContinuation, LISTENING_CACHED_THREAD_POOL) // build notification actions, tasks based to allow image fetching - .continueWith(CACHED_THREAD_POOL, actionsContinuation) + .continueWith(actionsContinuation, LISTENING_CACHED_THREAD_POOL) // build notification style, tasks based to allow image fetching - .continueWith(CACHED_THREAD_POOL, styleContinuation) + .continueWith(styleContinuation, LISTENING_CACHED_THREAD_POOL) // set full screen action, if fullScreenAction is set - .continueWith(CACHED_THREAD_POOL, fullScreenActionContinuation); + .continueWith(fullScreenActionContinuation, LISTENING_CACHED_THREAD_POOL); } - static Task cancelAllNotifications(@NonNull int notificationType) { - return Tasks.call( - () -> { - NotificationManagerCompat notificationManagerCompat = - NotificationManagerCompat.from(getApplicationContext()); + static ListenableFuture cancelAllNotifications(@NonNull int notificationType) { + return new ExtendedListenableFuture<>( + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + NotificationManagerCompat notificationManagerCompat = + NotificationManagerCompat.from(getApplicationContext()); - if (notificationType == NOTIFICATION_TYPE_DISPLAYED - || notificationType == NOTIFICATION_TYPE_ALL) { - notificationManagerCompat.cancelAll(); - } + if (notificationType == NOTIFICATION_TYPE_DISPLAYED + || notificationType == NOTIFICATION_TYPE_ALL) { + notificationManagerCompat.cancelAll(); + } - if (notificationType == NOTIFICATION_TYPE_TRIGGER - || notificationType == NOTIFICATION_TYPE_ALL) { - WorkManager workManager = WorkManager.getInstance(getApplicationContext()); - workManager.cancelAllWorkByTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER); + if (notificationType == NOTIFICATION_TYPE_TRIGGER + || notificationType == NOTIFICATION_TYPE_ALL) { + WorkManager workManager = WorkManager.getInstance(getApplicationContext()); + workManager.cancelAllWorkByTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER); - // Remove all cancelled and finished work from its internal database - // states include SUCCEEDED, FAILED and CANCELLED - workManager.pruneWork(); - } - return null; - }) + // Remove all cancelled and finished work from its internal database + // states include SUCCEEDED, FAILED and CANCELLED + workManager.pruneWork(); + } + return null; + })) .continueWith( - CACHED_THREAD_POOL, task -> { if (notificationType == NOTIFICATION_TYPE_TRIGGER || notificationType == NOTIFICATION_TYPE_ALL) { - task.continueWith(NotifeeAlarmManager.cancelAllNotifications()) - .addOnSuccessListener( - t -> { - t.continueWith( - a -> { - // delete all from database after canceling the alarms - WorkDataRepository.getInstance(getApplicationContext()).deleteAll(); - return null; - }); - }); + return new ExtendedListenableFuture( + NotifeeAlarmManager.cancelAllNotifications()) + .addOnCompleteListener( + (e, result) -> { + if (e == null) { + WorkDataRepository.getInstance(getApplicationContext()).deleteAll(); + } + }, + LISTENING_CACHED_THREAD_POOL); } - return null; - }); + return Futures.immediateFuture(null); + }, + LISTENING_CACHED_THREAD_POOL); } - static Task cancelAllNotificationsWithIds( + static ListenableFuture cancelAllNotificationsWithIds( @NonNull int notificationType, @NonNull List ids, String tag) { - return Tasks.call( - () -> { - WorkManager workManager = WorkManager.getInstance(getApplicationContext()); - NotificationManagerCompat notificationManagerCompat = - NotificationManagerCompat.from(getApplicationContext()); - - for (String id : ids) { - Logger.i(TAG, "Removing notification with id " + id); - - if (notificationType != NOTIFICATION_TYPE_TRIGGER) { - // Cancel notifications displayed by FCM which will always have - // an id of 0 and a tag, see https://github.com/invertase/notifee/pull/175 - if (tag != null && id.equals("0")) { - // Attempt to parse id as integer - Integer integerId = null; - - try { - integerId = parseInt(id); - } catch (Exception e) { - Logger.e( - TAG, - "cancelAllNotificationsWithIds -> Failed to parse id as integer " + id); + return new ExtendedListenableFuture<>( + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + WorkManager workManager = WorkManager.getInstance(getApplicationContext()); + NotificationManagerCompat notificationManagerCompat = + NotificationManagerCompat.from(getApplicationContext()); + + for (String id : ids) { + Logger.i(TAG, "Removing notification with id " + id); + + if (notificationType != NOTIFICATION_TYPE_TRIGGER) { + // Cancel notifications displayed by FCM which will always have + // an id of 0 and a tag, see https://github.com/invertase/notifee/pull/175 + if (tag != null && id.equals("0")) { + // Attempt to parse id as integer + Integer integerId = null; + + try { + integerId = parseInt(id); + } catch (Exception e) { + Logger.e( + TAG, + "cancelAllNotificationsWithIds -> Failed to parse id as integer " + + id); + } + + if (integerId != null) { + notificationManagerCompat.cancel(tag, integerId); + } + } + + // Cancel a notification created with notifee + notificationManagerCompat.cancel(tag, id.hashCode()); } - if (integerId != null) { - notificationManagerCompat.cancel(tag, integerId); - } - } - - // Cancel a notification created with notifee - notificationManagerCompat.cancel(tag, id.hashCode()); - } + if (notificationType != NOTIFICATION_TYPE_DISPLAYED) { + Logger.i(TAG, "Removing notification with id " + id); - if (notificationType != NOTIFICATION_TYPE_DISPLAYED) { - Logger.i(TAG, "Removing notification with id " + id); + workManager.cancelUniqueWork("trigger:" + id); + // Remove all cancelled and finished work from its internal database + // states include SUCCEEDED, FAILED and CANCELLED + workManager.pruneWork(); - workManager.cancelUniqueWork("trigger:" + id); - // Remove all cancelled and finished work from its internal database - // states include SUCCEEDED, FAILED and CANCELLED - workManager.pruneWork(); - - // And with alarm manager - NotifeeAlarmManager.cancelNotification(id); - } - } + // And with alarm manager + NotifeeAlarmManager.cancelNotification(id); + } + } - return null; - }) + return null; + })) .continueWith( task -> { // delete all from database if (notificationType != NOTIFICATION_TYPE_DISPLAYED) { WorkDataRepository.getInstance(getApplicationContext()).deleteByIds(ids); } - return null; - }); + return Futures.immediateFuture(null); + }, + LISTENING_CACHED_THREAD_POOL); } - static Task displayNotification(NotificationModel notificationModel, Bundle triggerBundle) { - return notificationBundleToBuilder(notificationModel) + static ListenableFuture displayNotification( + NotificationModel notificationModel, Bundle triggerBundle) { + return new ExtendedListenableFuture<>(notificationBundleToBuilder(notificationModel)) .continueWith( - CACHED_THREAD_POOL, - (task) -> { - NotificationCompat.Builder builder = task.getResult(); + (taskResult) -> { + NotificationCompat.Builder builder = taskResult; // Add the following extras for `getDisplayedNotifications()` Bundle extrasBundle = new Bundle(); @@ -588,14 +612,14 @@ static Task displayNotification(NotificationModel notificationModel, Bundl EventBus.post( new NotificationEvent(NotificationEvent.TYPE_DELIVERED, notificationModel)); - return null; - }); + return Futures.immediateFuture(null); + }, + CACHED_THREAD_POOL); } - static Task createTriggerNotification( + static ListenableFuture createTriggerNotification( NotificationModel notificationModel, Bundle triggerBundle) { - return Tasks.call( - CACHED_THREAD_POOL, + return LISTENING_CACHED_THREAD_POOL.submit( () -> { int triggerType = ObjectUtils.getInt(triggerBundle.get("type")); switch (triggerType) { @@ -701,8 +725,8 @@ static void createTimestampTriggerNotification( } } - static Task> getDisplayedNotifications() { - return Tasks.call( + static ListenableFuture> getDisplayedNotifications() { + return LISTENING_CACHED_THREAD_POOL.submit( () -> { List notifications = new ArrayList(); @@ -778,52 +802,57 @@ static Task> getDisplayedNotifications() { static void getTriggerNotifications(MethodCallResult> result) { WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext()); - workDataRepository - .getAll() - .addOnCompleteListener( - task -> { - List triggerNotifications = new ArrayList(); + List triggerNotifications = new ArrayList(); - if (task.isSuccessful()) { - List workDataEntities = task.getResult(); - for (WorkDataEntity workDataEntity : workDataEntities) { - Bundle triggerNotificationBundle = new Bundle(); + Futures.addCallback( + workDataRepository.getAll(), + new FutureCallback>() { + @Override + public void onSuccess(List workDataEntities) { + for (WorkDataEntity workDataEntity : workDataEntities) { + Bundle triggerNotificationBundle = new Bundle(); - triggerNotificationBundle.putBundle( - "notification", ObjectUtils.bytesToBundle(workDataEntity.getNotification())); + triggerNotificationBundle.putBundle( + "notification", ObjectUtils.bytesToBundle(workDataEntity.getNotification())); - triggerNotificationBundle.putBundle( - "trigger", ObjectUtils.bytesToBundle(workDataEntity.getTrigger())); - triggerNotifications.add(triggerNotificationBundle); - } + triggerNotificationBundle.putBundle( + "trigger", ObjectUtils.bytesToBundle(workDataEntity.getTrigger())); + triggerNotifications.add(triggerNotificationBundle); + } - result.onComplete(null, triggerNotifications); - } else { - result.onComplete(task.getException(), triggerNotifications); - } - }); + result.onComplete(null, triggerNotifications); + } + + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), triggerNotifications); + } + }, + LISTENING_CACHED_THREAD_POOL); } static void getTriggerNotificationIds(MethodCallResult> result) { WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext()); - workDataRepository - .getAll() - .addOnCompleteListener( - task -> { - List triggerNotificationIds = new ArrayList(); + Futures.addCallback( + workDataRepository.getAll(), + new FutureCallback>() { + @Override + public void onSuccess(List workDataEntities) { + List triggerNotificationIds = new ArrayList(); + for (WorkDataEntity workDataEntity : workDataEntities) { + triggerNotificationIds.add(workDataEntity.getId()); + } - if (task.isSuccessful()) { - List workDataEntities = task.getResult(); - for (WorkDataEntity workDataEntity : workDataEntities) { - triggerNotificationIds.add(workDataEntity.getId()); - } + result.onComplete(null, triggerNotificationIds); + } - result.onComplete(null, triggerNotificationIds); - } else { - result.onComplete(task.getException(), null); - } - }); + @Override + public void onFailure(Throwable t) { + result.onComplete(new Exception(t), null); + } + }, + LISTENING_CACHED_THREAD_POOL); } /* Execute work from trigger notifications via WorkManager*/ @@ -834,60 +863,72 @@ static void doScheduledWork( WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext()); - Continuation> workContinuation = - task -> { - WorkDataEntity workDataEntity = task.getResult(); - - byte[] notificationBytes; - - if (workDataEntity == null || workDataEntity.getNotification() == null) { - // check if notification bundle is stored with Work Manager - notificationBytes = data.getByteArray("notification"); - if (notificationBytes != null) { - Logger.w( - TAG, - "The trigger notification was created using an older version, please consider" - + " recreating the notification."); - } else { - Logger.w( - TAG, "Attempted to handle doScheduledWork but no notification data was found."); - completer.set(ListenableWorker.Result.success()); - return null; - } - } else { - notificationBytes = workDataEntity.getNotification(); - } + AsyncFunction> workContinuation = + workDataEntity -> + LISTENING_CACHED_THREAD_POOL.submit( + () -> { + byte[] notificationBytes; + + if (workDataEntity == null || workDataEntity.getNotification() == null) { + // check if notification bundle is stored with Work Manager + notificationBytes = data.getByteArray("notification"); + if (notificationBytes != null) { + Logger.w( + TAG, + "The trigger notification was created using an older version, please" + + " consider recreating the notification."); + } else { + Logger.w( + TAG, + "Attempted to handle doScheduledWork but no notification data was" + + " found."); + completer.set(ListenableWorker.Result.success()); + return Futures.immediateFuture(null); + } + } else { + notificationBytes = workDataEntity.getNotification(); + } - NotificationModel notificationModel = - NotificationModel.fromBundle(ObjectUtils.bytesToBundle(notificationBytes)); + NotificationModel notificationModel = + NotificationModel.fromBundle(ObjectUtils.bytesToBundle(notificationBytes)); - byte[] triggerBytes = workDataEntity.getTrigger(); - Bundle triggerBundle = null; + byte[] triggerBytes = workDataEntity.getTrigger(); + Bundle triggerBundle = null; - if (workDataEntity.getTrigger() != null) { - triggerBundle = ObjectUtils.bytesToBundle(triggerBytes); - } + if (workDataEntity.getTrigger() != null) { + triggerBundle = ObjectUtils.bytesToBundle(triggerBytes); + } - return NotificationManager.displayNotification(notificationModel, triggerBundle); - }; + return NotificationManager.displayNotification(notificationModel, triggerBundle); + }); - workDataRepository - .getWorkDataById(id) - .continueWithTask(CACHED_THREAD_POOL, workContinuation) + new ExtendedListenableFuture<>(workDataRepository.getWorkDataById(id)) + .continueWith(workContinuation, LISTENING_CACHED_THREAD_POOL) .addOnCompleteListener( - task -> { - completer.set(ListenableWorker.Result.success()); - - if (!task.isSuccessful()) { - Logger.e(TAG, "Failed to display notification", task.getException()); + (e, result) -> { + if (e == null) { + new ExtendedListenableFuture<>(result) + .addOnCompleteListener( + (e2, _unused) -> { + completer.set(Result.success()); + if (e2 != null) { + Logger.e(TAG, "Failed to display notification", e2); + } else { + String workerRequestType = data.getString(Worker.KEY_WORK_REQUEST); + if (workerRequestType != null + && workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) { + // delete database entry if work is a one-time request + WorkDataRepository.getInstance(getApplicationContext()) + .deleteById(id); + } + } + }, + LISTENING_CACHED_THREAD_POOL); } else { - String workerRequestType = data.getString(Worker.KEY_WORK_REQUEST); - if (workerRequestType != null - && workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) { - // delete database entry if work is a one-time request - WorkDataRepository.getInstance(getApplicationContext()).deleteById(id); - } + completer.set(Result.success()); + Logger.e(TAG, "Failed to display notification", e); } - }); + }, + LISTENING_CACHED_THREAD_POOL); } } diff --git a/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java b/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java index 97f6a4a34..977d0d5f3 100644 --- a/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java +++ b/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java @@ -24,6 +24,8 @@ import androidx.room.RoomDatabase; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -38,6 +40,8 @@ public abstract class NotifeeCoreDatabase extends RoomDatabase { private static volatile NotifeeCoreDatabase INSTANCE; static final ExecutorService databaseWriteExecutor = Executors.newCachedThreadPool(); + static final ListeningExecutorService databaseWriteListeningExecutor = + MoreExecutors.listeningDecorator(databaseWriteExecutor); /** * Migrate from: version 1 to version 2 - where the {@link WorkDataEntity} has an extra field: diff --git a/android/src/main/java/app/notifee/core/database/WorkDataRepository.java b/android/src/main/java/app/notifee/core/database/WorkDataRepository.java index a159914cc..a70d68b49 100644 --- a/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +++ b/android/src/main/java/app/notifee/core/database/WorkDataRepository.java @@ -22,8 +22,7 @@ import androidx.annotation.NonNull; import app.notifee.core.model.NotificationModel; import app.notifee.core.utility.ObjectUtils; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; +import com.google.common.util.concurrent.ListenableFuture; import java.util.List; public class WorkDataRepository { @@ -46,43 +45,42 @@ public WorkDataRepository(Context context) { } public void insert(WorkDataEntity workData) { - NotifeeCoreDatabase.databaseWriteExecutor.execute( + NotifeeCoreDatabase.databaseWriteListeningExecutor.execute( () -> { mWorkDataDao.insert(workData); }); } - public Task getWorkDataById(String id) { - return Tasks.call( - NotifeeCoreDatabase.databaseWriteExecutor, () -> mWorkDataDao.getWorkDataById(id)); + public ListenableFuture getWorkDataById(String id) { + return NotifeeCoreDatabase.databaseWriteListeningExecutor.submit( + () -> mWorkDataDao.getWorkDataById(id)); } - public Task> getAllWithAlarmManager(Boolean withAlarmManager) { - return Tasks.call( - NotifeeCoreDatabase.databaseWriteExecutor, + public ListenableFuture> getAllWithAlarmManager(Boolean withAlarmManager) { + return NotifeeCoreDatabase.databaseWriteListeningExecutor.submit( () -> mWorkDataDao.getAllWithAlarmManager(withAlarmManager)); } - public Task> getAll() { - return Tasks.call(NotifeeCoreDatabase.databaseWriteExecutor, () -> mWorkDataDao.getAll()); + public ListenableFuture> getAll() { + return NotifeeCoreDatabase.databaseWriteListeningExecutor.submit(() -> mWorkDataDao.getAll()); } public void deleteById(String id) { - NotifeeCoreDatabase.databaseWriteExecutor.execute( + NotifeeCoreDatabase.databaseWriteListeningExecutor.execute( () -> { mWorkDataDao.deleteById(id); }); } public void deleteByIds(List ids) { - NotifeeCoreDatabase.databaseWriteExecutor.execute( + NotifeeCoreDatabase.databaseWriteListeningExecutor.execute( () -> { mWorkDataDao.deleteByIds(ids); }); } public void deleteAll() { - NotifeeCoreDatabase.databaseWriteExecutor.execute( + NotifeeCoreDatabase.databaseWriteListeningExecutor.execute( () -> { mWorkDataDao.deleteAll(); }); @@ -101,7 +99,7 @@ public static void insertTriggerNotification( } public void update(WorkDataEntity workData) { - NotifeeCoreDatabase.databaseWriteExecutor.execute( + NotifeeCoreDatabase.databaseWriteListeningExecutor.execute( () -> { mWorkDataDao.update(workData); }); 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 54cec33b1..23103f1d2 100644 --- a/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +++ b/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java @@ -555,7 +555,7 @@ public Boolean hasStyle() { /** * Returns a task containing a notification style * - * @return Task + * @return ListenableFuture */ public @Nullable NotificationAndroidStyleModel getStyle() { if (!hasStyle()) { 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 d9cb42966..6a5d89331 100644 --- a/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java +++ b/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java @@ -28,11 +28,11 @@ import app.notifee.core.utility.ObjectUtils; 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 com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.Objects; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -55,9 +55,9 @@ public static NotificationAndroidStyleModel fromBundle(Bundle styleBundle) { * @param personBundle * @return */ - private static Task getPerson(Executor executor, Bundle personBundle) { - return Tasks.call( - executor, + private static ListenableFuture getPerson( + ListeningExecutorService lExecutor, Bundle personBundle) { + return lExecutor.submit( () -> { Person.Builder personBuilder = new Person.Builder(); @@ -81,8 +81,7 @@ private static Task getPerson(Executor executor, Bundle personBundle) { try { personIconBitmap = - Tasks.await( - ResourceUtils.getImageBitmapFromUrl(personIcon), 10, TimeUnit.SECONDS); + ResourceUtils.getImageBitmapFromUrl(personIcon).get(10, TimeUnit.SECONDS); } catch (TimeoutException e) { Logger.e( TAG, @@ -113,22 +112,23 @@ public Bundle toBundle() { } @Nullable - public Task getStyleTask(Executor executor) { + public ListenableFuture getStyleTask( + ListeningExecutorService lExecutor) { int type = ObjectUtils.getInt(mNotificationAndroidStyleBundle.get("type")); - Task styleTask = null; + ListenableFuture styleTask = null; switch (type) { case 0: - styleTask = getBigPictureStyleTask(executor); + styleTask = getBigPictureStyleTask(lExecutor); break; case 1: - styleTask = Tasks.forResult(getBigTextStyle()); + styleTask = Futures.immediateFuture(getBigTextStyle()); break; case 2: - styleTask = Tasks.forResult(getInboxStyle()); + styleTask = Futures.immediateFuture(getInboxStyle()); break; case 3: - styleTask = getMessagingStyleTask(executor); + styleTask = getMessagingStyleTask(lExecutor); break; } @@ -140,9 +140,9 @@ public Task getStyleTask(Executor executor) { * * @return */ - private Task getBigPictureStyleTask(Executor executor) { - return Tasks.call( - executor, + private ListenableFuture getBigPictureStyleTask( + ListeningExecutorService lExecutor) { + return lExecutor.submit( () -> { NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); @@ -154,7 +154,7 @@ private Task getBigPictureStyleTask(Executor executor) try { pictureBitmap = - Tasks.await(ResourceUtils.getImageBitmapFromUrl(picture), 10, TimeUnit.SECONDS); + ResourceUtils.getImageBitmapFromUrl(picture).get(10, TimeUnit.SECONDS); } catch (TimeoutException e) { Logger.e( TAG, @@ -190,7 +190,7 @@ private Task getBigPictureStyleTask(Executor executor) try { largeIconBitmap = - Tasks.await(ResourceUtils.getImageBitmapFromUrl(largeIcon), 10, TimeUnit.SECONDS); + ResourceUtils.getImageBitmapFromUrl(largeIcon).get(10, TimeUnit.SECONDS); } catch (TimeoutException e) { Logger.e( TAG, @@ -286,17 +286,15 @@ private NotificationCompat.InboxStyle getInboxStyle() { } /** Gets a MessagingStyle for a notification */ - private Task getMessagingStyleTask(Executor executor) { - return Tasks.call( - executor, + private ListenableFuture getMessagingStyleTask( + ListeningExecutorService lExecutor) { + return lExecutor.submit( () -> { Person person = - Tasks.await( - getPerson( - executor, - Objects.requireNonNull(mNotificationAndroidStyleBundle.getBundle("person"))), - 20, - TimeUnit.SECONDS); + getPerson( + lExecutor, + Objects.requireNonNull(mNotificationAndroidStyleBundle.getBundle("person"))) + .get(20, TimeUnit.SECONDS); NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(person); @@ -323,10 +321,8 @@ private Task getMessagingStyleTask(Executor executor) if (message.containsKey("person")) { messagePerson = - Tasks.await( - getPerson(executor, Objects.requireNonNull(message.getBundle("person"))), - 20, - TimeUnit.SECONDS); + getPerson(lExecutor, Objects.requireNonNull(message.getBundle("person"))) + .get(20, TimeUnit.SECONDS); } messagingStyle = diff --git a/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java b/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java index 57c9b36a8..ff5cb3f07 100644 --- a/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java +++ b/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java @@ -100,7 +100,7 @@ private TimestampTriggerModel(Bundle bundle) { case 1: mAlarmType = AlarmType.SET_AND_ALLOW_WHILE_IDLE; break; - // default behavior when alarmManager is true: + // default behavior when alarmManager is true: default: case 2: mAlarmType = AlarmType.SET_EXACT; diff --git a/android/src/main/java/app/notifee/core/utility/Callbackable.java b/android/src/main/java/app/notifee/core/utility/Callbackable.java new file mode 100644 index 000000000..1c2e047dc --- /dev/null +++ b/android/src/main/java/app/notifee/core/utility/Callbackable.java @@ -0,0 +1,12 @@ +package app.notifee.core.utility; + +import androidx.annotation.Nullable; + +/** + * Since lambda functions are not supported prior to SDK 24, and this project's SDK min. is 20, This + * interface was created to pass a function to another object. + */ +public interface Callbackable { + + void call(@Nullable Exception e, @Nullable T result); +} diff --git a/android/src/main/java/app/notifee/core/utility/ExtendedListenableFuture.java b/android/src/main/java/app/notifee/core/utility/ExtendedListenableFuture.java new file mode 100644 index 000000000..ff66eb62e --- /dev/null +++ b/android/src/main/java/app/notifee/core/utility/ExtendedListenableFuture.java @@ -0,0 +1,84 @@ +package app.notifee.core.utility; + +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A simple wrapper around {@link ListenableFuture} that provides additional methods that makes + * chaining {@link ListenableFuture}s easier and more readable. + */ +public class ExtendedListenableFuture implements ListenableFuture { + + private final ListenableFuture future; + + public ExtendedListenableFuture(ListenableFuture future) { + this.future = future; + } + + /** + * A wrapper around {@link Futures#transformAsync} that uses the Future It's called on as first + * parameter by default. This allows easier chaining. + */ + public ExtendedListenableFuture continueWith( + AsyncFunction asyncFunction, Executor lExecutor) { + ListenableFuture future = Futures.transformAsync(this, asyncFunction, lExecutor); + return new ExtendedListenableFuture<>(future); + } + + public ExtendedListenableFuture addOnCompleteListener( + Callbackable callbackable, Executor executor) { + Futures.addCallback( + this, + new FutureCallback() { + @Override + public void onSuccess(T result) { + callbackable.call(null, result); + } + + @Override + public void onFailure(Throwable t) { + callbackable.call(new Exception(t), null); + } + }, + executor); + return this; + } + + @Override + public void addListener(Runnable listener, Executor executor) { + future.addListener(listener, executor); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public T get() throws ExecutionException, InterruptedException { + return future.get(); + } + + @Override + public T get(long timeout, TimeUnit unit) + throws ExecutionException, InterruptedException, TimeoutException { + return future.get(timeout, unit); + } +} diff --git a/android/src/main/java/app/notifee/core/utility/ResourceUtils.java b/android/src/main/java/app/notifee/core/utility/ResourceUtils.java index 0d6137daf..6b13b9049 100644 --- a/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +++ b/android/src/main/java/app/notifee/core/utility/ResourceUtils.java @@ -40,8 +40,8 @@ import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; import java.util.HashMap; import java.util.Map; @@ -133,16 +133,15 @@ public static Bitmap getCircularBitmap(Bitmap bitmap) { * @param imageUrl * @return Bitmap or null if the image failed to load */ - public static Task getImageBitmapFromUrl(String imageUrl) { + public static ListenableFuture getImageBitmapFromUrl(String imageUrl) { Uri imageUri; - final TaskCompletionSource bitmapTCS = new TaskCompletionSource<>(); - Task bitmapTask = bitmapTCS.getTask(); + final SettableFuture bitmapTCS = SettableFuture.create(); if (!imageUrl.contains("/")) { String imageResourceUrl = getImageResourceUrl(imageUrl); if (imageResourceUrl == null) { - bitmapTCS.setResult(null); - return bitmapTask; + bitmapTCS.set(null); + return bitmapTCS; } imageUri = getImageSourceUri(imageResourceUrl); } else { @@ -168,19 +167,19 @@ public static Task getImageBitmapFromUrl(String imageUrl) { new BaseBitmapDataSubscriber() { @Override protected void onNewResultImpl(@Nullable Bitmap bitmap) { - bitmapTCS.setResult(bitmap); + bitmapTCS.set(bitmap); } @Override protected void onFailureImpl( @NonNull DataSource> dataSource) { Logger.e(TAG, "Failed to load an image: " + imageUrl, dataSource.getFailureCause()); - bitmapTCS.setResult(null); + bitmapTCS.set(null); } }, CallerThreadExecutor.getInstance()); - return bitmapTask; + return bitmapTCS; } /** diff --git a/package.json b/package.json index 1b8cbc119..d6ca6b0a4 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "eslint-plugin-react": "^7.29.3", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-native": "^4.1.0", - "google-java-format": "^1.0.1", + "google-java-format": "^1.4.0", "lerna": "^4.0.0", "prettier": "^2.5.1", "rimraf": "^3.0.2", diff --git a/packages/flutter/packages/notifee/android/src/main/java/io/flutter/plugins/notifee/background/BackgroundExecutor.java b/packages/flutter/packages/notifee/android/src/main/java/io/flutter/plugins/notifee/background/BackgroundExecutor.java index b07ad1553..b4737967c 100644 --- a/packages/flutter/packages/notifee/android/src/main/java/io/flutter/plugins/notifee/background/BackgroundExecutor.java +++ b/packages/flutter/packages/notifee/android/src/main/java/io/flutter/plugins/notifee/background/BackgroundExecutor.java @@ -55,6 +55,7 @@ public class BackgroundExecutor implements MethodCallHandler { private static final String USER_CALLBACK_HANDLE_KEY = "user_callback_handle"; private final AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false); + /** * The {@link MethodChannel} that connects the Android side of this plugin with the background * Dart isolate that was created by this plugin. diff --git a/packages/react-native/android/build.gradle b/packages/react-native/android/build.gradle index ec7a40f2b..02a21ae80 100644 --- a/packages/react-native/android/build.gradle +++ b/packages/react-native/android/build.gradle @@ -99,7 +99,6 @@ dependencies { implementation(group: 'app.notifee', name:'core', version: '+') } implementation 'androidx.concurrent:concurrent-futures:1.1.0' // https://developer.android.com/jetpack/androidx/releases/concurrent - implementation 'com.google.android.gms:play-services-tasks:18.0.1' // https://developers.google.com/android/guides/releases implementation 'androidx.work:work-runtime:2.8.0' // https://developer.android.com/jetpack/androidx/releases/work implementation 'org.greenrobot:eventbus:3.3.1' // https://github.com/greenrobot/EventBus/releases implementation 'androidx.lifecycle:lifecycle-process:2.3.1' diff --git a/tests_react_native/android/app/build.gradle b/tests_react_native/android/app/build.gradle index 14a84ffb2..1cbf4c7f9 100755 --- a/tests_react_native/android/app/build.gradle +++ b/tests_react_native/android/app/build.gradle @@ -149,4 +149,6 @@ task copyDownloadableDepsToLibs(type: Copy) { } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) -apply plugin: 'com.google.gms.google-services' + +// Still needed for react-native-firebase integration part of tests +apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/tests_react_native/android/build.gradle b/tests_react_native/android/build.gradle index d0bf76673..34088b76c 100755 --- a/tests_react_native/android/build.gradle +++ b/tests_react_native/android/build.gradle @@ -23,10 +23,12 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.1.1") - classpath("com.facebook.react:react-native-gradle-plugin") - classpath("de.undercouch:gradle-download-task:5.0.1") - classpath 'com.google.gms:google-services:4.3.10' + classpath("com.android.tools.build:gradle:7.1.1") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("de.undercouch:gradle-download-task:5.0.1") + + // Still needed for react-native-firebase integration part of tests + classpath 'com.google.gms:google-services:4.3.10' } } diff --git a/yarn.lock b/yarn.lock index bce32c8ed..e02eb9ae1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6078,6 +6078,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -6311,6 +6316,17 @@ glob@^8.0.3: minimatch "^5.0.1" once "^1.3.0" +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -6369,14 +6385,14 @@ google-gax@^2.24.1: protobufjs "6.11.3" retry-request "^4.0.0" -google-java-format@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/google-java-format/-/google-java-format-1.1.0.tgz#0af97cf6a58e375ff215039acb8831724e488a66" - integrity sha512-sW5oBUJXs3J7mJgdv7+eXeY7bwv5VNHZvBrSKIClrV2Tz+UCeTmuYNEQW+/yv9RSfhQ9rjqerO43J7Jui6uuyg== +google-java-format@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/google-java-format/-/google-java-format-1.4.0.tgz#d944b7a20f0a3729318b5c120931178843ed9a82" + integrity sha512-TlO1nUogUW6PonVL4xZCciVoJcjB2Nxo/dEY5M8GC5TJnQi6A4XeqJoiOot/To20350wPup9aQfP3Ia6GR+vEg== dependencies: async "^3.2.4" - glob "^8.0.3" - resolve "^1.22.1" + glob "^8.1.0" + resolve "^1.22.8" google-p12-pem@^3.1.3: version "3.1.4" @@ -6511,6 +6527,13 @@ hash-stream-validation@^0.2.2: resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hermes-engine@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/hermes-engine/-/hermes-engine-0.11.0.tgz#bb224730d230a02a5af02c4e090d1f52d57dd3db" @@ -6863,6 +6886,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + is-core-module@^2.5.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -10697,7 +10727,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -10706,6 +10736,15 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.8: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.3: version "2.0.0-next.4" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"