From f3e4de9034df7b603675595495c479d26947f3f2 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Tue, 29 Nov 2016 15:23:49 +0200 Subject: [PATCH] Local notifications set/clear with tests and refactoring --- android/build.gradle | 4 +- .../core/AppLaunchHelper.java | 6 +- .../core/AppLifecycleFacade.java | 3 + .../core/JsIOHelper.java | 26 ++ .../core/NotificationIntentAdapter.java | 2 +- .../core/ProxyService.java | 5 + .../core/RNNotificationsModule.java | 17 ++ .../core/ReactAppLifecycleFacade.java | 10 + .../INotificationsApplication.java | 3 +- .../core/notification/IPushNotification.java | 8 + .../core/notification/PushNotification.java | 86 +++--- .../IPushNotificationsDrawer.java | 1 + .../PushNotificationsDrawer.java | 24 +- .../notification/PushNotificationTest.java | 259 ++++++++++++++++++ .../PushNotificationsDrawerTest.java | 64 +++++ .../src/main/AndroidManifest.xml | 5 - example/index.android.js | 39 ++- index.android.js | 10 + test/index.android.spec.js | 53 +++- 19 files changed, 550 insertions(+), 75 deletions(-) create mode 100644 android/src/main/java/com/wix/reactnativenotifications/core/JsIOHelper.java create mode 100644 android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java create mode 100644 android/src/test/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawerTest.java diff --git a/android/build.gradle b/android/build.gradle index c88f0fb..4d0a65f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,9 +20,11 @@ android { dependencies { // Google's GCM. - compile "com.google.android.gms:play-services-gcm:9.4.0" + compile 'com.google.android.gms:play-services-gcm:9.4.0' compile 'com.facebook.react:react-native:+' testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:2.+' + testCompile "org.robolectric:robolectric:3.1.4" } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/AppLaunchHelper.java b/android/src/main/java/com/wix/reactnativenotifications/core/AppLaunchHelper.java index b098e7c..e2d2431 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/AppLaunchHelper.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/AppLaunchHelper.java @@ -10,7 +10,7 @@ public class AppLaunchHelper { private static final String LAUNCH_FLAG_KEY_NAME = "launchedFromNotification"; - public static Intent getLaunchIntent(Context appContext) { + public Intent getLaunchIntent(Context appContext) { try { // The desired behavior upon notification opening is as follows: // - If app is in foreground (and possibly has several activities in stack), simply keep it as-is in foreground. @@ -32,14 +32,14 @@ public class AppLaunchHelper { } } - public static boolean isLaunchIntentsActivity(Activity activity) { + public boolean isLaunchIntentsActivity(Activity activity) { final Intent helperIntent = activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName()); final String activityName = activity.getComponentName().getClassName(); final String launchIntentActivityName = helperIntent.getComponent().getClassName(); return activityName.equals(launchIntentActivityName); } - public static boolean isLaunchIntent(Intent intent) { + public boolean isLaunchIntent(Intent intent) { return intent.getBooleanExtra(LAUNCH_FLAG_KEY_NAME, false); } } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/AppLifecycleFacade.java b/android/src/main/java/com/wix/reactnativenotifications/core/AppLifecycleFacade.java index 04eae26..bba1e91 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/AppLifecycleFacade.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/AppLifecycleFacade.java @@ -1,5 +1,7 @@ package com.wix.reactnativenotifications.core; +import com.facebook.react.bridge.ReactContext; + public interface AppLifecycleFacade { interface AppVisibilityListener { @@ -8,6 +10,7 @@ public interface AppLifecycleFacade { } boolean isReactInitialized(); + ReactContext getRunningReactContext(); boolean isAppVisible(); void addVisibilityListener(AppVisibilityListener listener); void removeVisibilityListener(AppVisibilityListener listener); diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/JsIOHelper.java b/android/src/main/java/com/wix/reactnativenotifications/core/JsIOHelper.java new file mode 100644 index 0000000..4d8f4d1 --- /dev/null +++ b/android/src/main/java/com/wix/reactnativenotifications/core/JsIOHelper.java @@ -0,0 +1,26 @@ +package com.wix.reactnativenotifications.core; + +import android.os.Bundle; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +public class JsIOHelper { + public boolean sendEventToJS(String eventName, Bundle data, ReactContext reactContext) { + if (reactContext != null) { + sendEventToJS(eventName, Arguments.fromBundle(data), reactContext); + return true; + } + return false; + } + + public boolean sendEventToJS(String eventName, WritableMap data, ReactContext reactContext) { + if (reactContext != null) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, data); + return true; + } + return false; + } +} diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java b/android/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java index de89b84..0413e2f 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/NotificationIntentAdapter.java @@ -13,7 +13,7 @@ public class NotificationIntentAdapter { public static PendingIntent createPendingNotificationIntent(Context appContext, Intent intent, PushNotificationProps notification) { intent.putExtra(PUSH_NOTIFICATION_EXTRA_NAME, notification.asBundle()); - return PendingIntent.getService(appContext, PENDING_INTENT_CODE, intent, 0); + return PendingIntent.getService(appContext, PENDING_INTENT_CODE, intent, PendingIntent.FLAG_ONE_SHOT); } public static Bundle extractPendingNotificationDataFromIntent(Intent intent) { diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/ProxyService.java b/android/src/main/java/com/wix/reactnativenotifications/core/ProxyService.java index bbc10c8..5f9e031 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/ProxyService.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/ProxyService.java @@ -7,6 +7,8 @@ import android.util.Log; import com.wix.reactnativenotifications.core.notification.IPushNotification; import com.wix.reactnativenotifications.core.notification.PushNotification; +import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer; +import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer; public class ProxyService extends IntentService { @@ -24,5 +26,8 @@ public class ProxyService extends IntentService { if (pushNotification != null) { pushNotification.onOpened(); } + + final IPushNotificationsDrawer pushNotificationDrawer = PushNotificationsDrawer.get(this); + pushNotificationDrawer.onNotificationOpened(); } } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java b/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java index 5db1c32..7a55b52 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java @@ -12,6 +12,9 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.wix.reactnativenotifications.core.notification.IPushNotification; +import com.wix.reactnativenotifications.core.notification.PushNotification; import com.wix.reactnativenotifications.core.notification.PushNotificationProps; import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer; import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer; @@ -66,6 +69,20 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements } } + @ReactMethod + public void postLocalNotification(ReadableMap notificationPropsMap, int notificationId) { + Log.d(LOGTAG, "Native method invocation: postLocalNotification"); + final Bundle notificationProps = Arguments.toBundle(notificationPropsMap); + final IPushNotification pushNotification = PushNotification.get(getReactApplicationContext().getApplicationContext(), notificationProps, ReactAppLifecycleFacade.get()); + pushNotification.onPostRequest(notificationId); + } + + @ReactMethod + public void cancelLocalNotification(int notificationId) { + IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext()); + notificationsDrawer.onNotificationClearRequest(notificationId); + } + @Override public void onAppVisible() { final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext()); diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/ReactAppLifecycleFacade.java b/android/src/main/java/com/wix/reactnativenotifications/core/ReactAppLifecycleFacade.java index 26ef28f..13934f0 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/ReactAppLifecycleFacade.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/ReactAppLifecycleFacade.java @@ -54,6 +54,16 @@ public class ReactAppLifecycleFacade implements AppLifecycleFacade { return mReactContext.hasActiveCatalystInstance(); } + @Override + public ReactContext getRunningReactContext() { + ReactContext reactContext = mReactContext; + if (reactContext == null) { + return null; + } + + return mReactContext; + } + @Override public boolean isAppVisible() { return mIsVisible; diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/notification/INotificationsApplication.java b/android/src/main/java/com/wix/reactnativenotifications/core/notification/INotificationsApplication.java index 6b46988..ba88dbd 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/notification/INotificationsApplication.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/notification/INotificationsApplication.java @@ -3,8 +3,9 @@ package com.wix.reactnativenotifications.core.notification; import android.content.Context; import android.os.Bundle; +import com.wix.reactnativenotifications.core.AppLaunchHelper; import com.wix.reactnativenotifications.core.AppLifecycleFacade; public interface INotificationsApplication { - IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade facade); + IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper); } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/notification/IPushNotification.java b/android/src/main/java/com/wix/reactnativenotifications/core/notification/IPushNotification.java index a142f49..0d70024 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/notification/IPushNotification.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/notification/IPushNotification.java @@ -18,5 +18,13 @@ public interface IPushNotification { */ void onOpened(); + /** + * Handle a request to post this notification. + * + * @param notificationId (optional) The specific ID to associated with the notification. + * @return The ID effectively assigned to the notification (Auto-assigned if not specified as a parameter). + */ + int onPostRequest(Integer notificationId); + PushNotificationProps asProps(); } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java b/android/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java index 03706c5..8f0c34d 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java @@ -7,20 +7,14 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import com.facebook.react.ReactApplication; -import com.facebook.react.ReactInstanceManager; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; import com.wix.reactnativenotifications.core.AppLaunchHelper; import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.AppLifecycleFacade.AppVisibilityListener; import com.wix.reactnativenotifications.core.InitialNotification; import com.wix.reactnativenotifications.core.NotificationIntentAdapter; import com.wix.reactnativenotifications.core.ProxyService; -import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer; +import com.wix.reactnativenotifications.core.JsIOHelper; import static com.wix.reactnativenotifications.Defs.NOTIFICATION_OPENED_EVENT_NAME; import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME; @@ -29,6 +23,8 @@ public class PushNotification implements IPushNotification { final protected Context mContext; final protected AppLifecycleFacade mAppLifecycleFacade; + final protected AppLaunchHelper mAppLaunchHelper; + final protected JsIOHelper mJsIOHelper; final protected PushNotificationProps mNotificationProps; final protected AppVisibilityListener mAppVisibilityListener = new AppVisibilityListener() { @Override @@ -42,30 +38,40 @@ public class PushNotification implements IPushNotification { } }; - protected PushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade) { - mContext = context; - mAppLifecycleFacade = appLifecycleFacade; - mNotificationProps = createProps(bundle); + public static IPushNotification get(Context context, Bundle bundle, AppLifecycleFacade facade) { + return PushNotification.get(context, bundle, facade, new AppLaunchHelper()); } - public static IPushNotification get(Context context, Bundle bundle, AppLifecycleFacade facade) { + public static IPushNotification get(Context context, Bundle bundle, AppLifecycleFacade facade, AppLaunchHelper appLaunchHelper) { Context appContext = context.getApplicationContext(); if (appContext instanceof INotificationsApplication) { - return ((INotificationsApplication) appContext).getPushNotification(context, bundle, facade); + return ((INotificationsApplication) appContext).getPushNotification(context, bundle, facade, appLaunchHelper); } - return new PushNotification(context, bundle, facade); + return new PushNotification(context, bundle, facade, appLaunchHelper, new JsIOHelper()); + } + + protected PushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper JsIOHelper) { + mContext = context; + mAppLifecycleFacade = appLifecycleFacade; + mAppLaunchHelper = appLaunchHelper; + mJsIOHelper = JsIOHelper; + mNotificationProps = createProps(bundle); } @Override public void onReceived() throws InvalidNotificationException { - postNotification(); + postNotification(null); notifyReceivedToJS(); } @Override public void onOpened() { digestNotification(); - PushNotificationsDrawer.get(mContext).onNotificationOpened(); + } + + @Override + public int onPostRequest(Integer notificationId) { + return postNotification(notificationId); } @Override @@ -73,10 +79,10 @@ public class PushNotification implements IPushNotification { return mNotificationProps.copy(); } - protected void postNotification() { + protected int postNotification(Integer notificationId) { final PendingIntent pendingIntent = getCTAPendingIntent(); final Notification notification = buildNotification(pendingIntent); - postNotification((int) System.currentTimeMillis(), notification); + return postNotification(notification, notificationId); } protected void digestNotification() { @@ -86,7 +92,7 @@ public class PushNotification implements IPushNotification { return; } - final ReactContext reactContext = getRunningReactContext(); + final ReactContext reactContext = mAppLifecycleFacade.getRunningReactContext(); if (reactContext.getCurrentActivity() == null) { setAsInitialNotification(); } @@ -141,51 +147,31 @@ public class PushNotification implements IPushNotification { .setAutoCancel(true); } + protected int postNotification(Notification notification, Integer notificationId) { + int id = notificationId != null ? notificationId : createNotificationId(notification); + postNotification(id, notification); + return id; + } + protected void postNotification(int id, Notification notification) { final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(id, notification); } - protected ReactContext getRunningReactContext() { - final ReactNativeHost rnHost = ((ReactApplication) mContext.getApplicationContext()).getReactNativeHost(); - if (!rnHost.hasInstance()) { - return null; - } - - final ReactInstanceManager instanceManager = rnHost.getReactInstanceManager(); - final ReactContext reactContext = instanceManager.getCurrentReactContext(); - if (reactContext == null || !reactContext.hasActiveCatalystInstance()) { - return null; - } - - return reactContext; + protected int createNotificationId(Notification notification) { + return (int) System.nanoTime(); } private void notifyReceivedToJS() { - notifyJS(NOTIFICATION_RECEIVED_EVENT_NAME, null); + mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); } private void notifyOpenedToJS() { - notifyOpenedToJS(null); - } - - private void notifyOpenedToJS(ReactContext reactContext) { - notifyJS(NOTIFICATION_OPENED_EVENT_NAME, reactContext); - } - - private void notifyJS(String eventName, ReactContext reactContext) { - if (reactContext == null) { - reactContext = getRunningReactContext(); - } - - if (reactContext != null) { - final WritableMap notificationAsMap = Arguments.fromBundle(mNotificationProps.asBundle()); - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, notificationAsMap); - } + mJsIOHelper.sendEventToJS(NOTIFICATION_OPENED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); } protected void launchOrResumeApp() { - final Intent intent = AppLaunchHelper.getLaunchIntent(mContext); + final Intent intent = mAppLaunchHelper.getLaunchIntent(mContext); mContext.startActivity(intent); } } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java b/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java index 6711baa..3be3dc1 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/IPushNotificationsDrawer.java @@ -8,4 +8,5 @@ public interface IPushNotificationsDrawer { void onNewActivity(Activity activity); void onNotificationOpened(); + void onNotificationClearRequest(int id); } diff --git a/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java b/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java index 9b3383b..cd59c83 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java @@ -10,18 +10,24 @@ import com.wix.reactnativenotifications.core.InitialNotification; public class PushNotificationsDrawer implements IPushNotificationsDrawer { final protected Context mContext; + final protected AppLaunchHelper mAppLaunchHelper; - public PushNotificationsDrawer(Context context) { - mContext = context; + public static IPushNotificationsDrawer get(Context context) { + return PushNotificationsDrawer.get(context, new AppLaunchHelper()); } - public static IPushNotificationsDrawer get(Context context) { + public static IPushNotificationsDrawer get(Context context, AppLaunchHelper appLaunchHelper) { final Context appContext = context.getApplicationContext(); if (appContext instanceof INotificationsDrawerApplication) { return ((INotificationsDrawerApplication) appContext).getPushNotificationsDrawer(context); } - return new PushNotificationsDrawer(context); + return new PushNotificationsDrawer(context, appLaunchHelper); + } + + protected PushNotificationsDrawer(Context context, AppLaunchHelper appLaunchHelper) { + mContext = context; + mAppLaunchHelper = appLaunchHelper; } @Override @@ -36,8 +42,8 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer { @Override public void onNewActivity(Activity activity) { - if (AppLaunchHelper.isLaunchIntentsActivity(activity) && - !AppLaunchHelper.isLaunchIntent(activity.getIntent())) { + if (mAppLaunchHelper.isLaunchIntentsActivity(activity) && + !mAppLaunchHelper.isLaunchIntent(activity.getIntent())) { InitialNotification.clear(); } } @@ -47,6 +53,12 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer { clearAll(); } + @Override + public void onNotificationClearRequest(int id) { + final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(id); + } + protected void clearAll() { final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancelAll(); diff --git a/android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java b/android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java new file mode 100644 index 0000000..1d711ad --- /dev/null +++ b/android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java @@ -0,0 +1,259 @@ +package com.wix.reactnativenotifications.core.notification; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.os.Bundle; + +import com.facebook.react.bridge.ReactContext; +import com.wix.reactnativenotifications.core.AppLaunchHelper; +import com.wix.reactnativenotifications.core.AppLifecycleFacade; +import com.wix.reactnativenotifications.core.AppLifecycleFacade.AppVisibilityListener; +import com.wix.reactnativenotifications.core.JsIOHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowNotification; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class PushNotificationTest { + + private static final String NOTIFICATION_OPENED_EVENT_NAME = "notificationOpened"; + private static final String NOTIFICATION_RECEIVED_EVENT_NAME = "notificationReceived"; + + private static final String DEFAULT_NOTIFICATION_TITLE = "Notification-title"; + private static final String DEFAULT_NOTIFICATION_BODY = "Notification-body"; + + @Mock private ReactContext mReactContext; + @Mock private Context mContext; + @Mock private NotificationManager mNotificationManager; + + @Mock private Bundle mDefaultBundle; + @Mock private Intent mLaunchIntent; + @Mock private AppLifecycleFacade mAppLifecycleFacade; + @Mock private AppLaunchHelper mAppLaunchHelper; + @Mock private JsIOHelper mJsIOHelper; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mDefaultBundle.getString(eq("title"))).thenReturn(DEFAULT_NOTIFICATION_TITLE); + when(mDefaultBundle.getString(eq("body"))).thenReturn(DEFAULT_NOTIFICATION_BODY); + when(mDefaultBundle.clone()).thenReturn(mDefaultBundle); + + when(mAppLaunchHelper.getLaunchIntent(eq(mContext))).thenReturn(mLaunchIntent); + + ApplicationInfo ai = mock(ApplicationInfo.class); + when(mContext.getApplicationInfo()).thenReturn(ai); + + when(mContext.getSystemService(Context.NOTIFICATION_SERVICE)).thenReturn(mNotificationManager); + } + + @Test + public void onOpened_noReactContext_launchApp() throws Exception { + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(false); + + final PushNotification uut = createUUT(); + uut.onOpened(); + + verify(mContext).startActivity(eq(mLaunchIntent)); + + // The unit shouldn't wait for visibility in this case cause we dont make the extra effort of + // notifying the notification upon app launch completion (simply cause we dont know when in completes). + // Instead, the user is expected to use getInitialNotification(). + verify(mAppLifecycleFacade, never()).addVisibilityListener(any(AppVisibilityListener.class)); + } + + @Test + public void onOpened_appInvisible_resumeAppWaitForVisibility() throws Exception { + setUpBackgroundApp(); + + final PushNotification uut = createUUT(); + uut.onOpened(); + + verify(mContext).startActivity(any(Intent.class)); + verify(mAppLifecycleFacade).addVisibilityListener(any(AppVisibilityListener.class)); + } + + @Test + public void onOpened_appGoesVisible_resumeAppAndNotifyJs() throws Exception { + + // Arrange + + setUpBackgroundApp(); + + // Act + + final PushNotification uut = createUUT(); + uut.onOpened(); + + // Hijack and invoke visibility listener + ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(AppVisibilityListener.class); + verify(mAppLifecycleFacade).addVisibilityListener(listenerCaptor.capture()); + AppVisibilityListener listener = listenerCaptor.getValue(); + listener.onAppVisible(); + + // Assert + + verify(mJsIOHelper).sendEventToJS(eq(NOTIFICATION_OPENED_EVENT_NAME), eq(mDefaultBundle), eq(mReactContext)); + } + + @Test + public void onOpened_appVisible_notifyJS() throws Exception { + setUpForegroundApp(); + + final PushNotification uut = createUUT(); + uut.onOpened(); + + verify(mContext, never()).startActivity(any(Intent.class)); + verify(mJsIOHelper).sendEventToJS(eq(NOTIFICATION_OPENED_EVENT_NAME), eq(mDefaultBundle), eq(mReactContext)); + } + + @Test + public void onReceived_validData_postNotificationAndNotifyJS() throws Exception { + // Arrange + + setUpForegroundApp(); + + // Act + + final PushNotification uut = createUUT(); + uut.onReceived(); + + // Assert + + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager).notify(anyInt(), notificationCaptor.capture()); + verifyNotification(notificationCaptor.getValue()); + + verify(mJsIOHelper).sendEventToJS(eq(NOTIFICATION_RECEIVED_EVENT_NAME), eq(mDefaultBundle), eq(mReactContext)); + } + + @Test + public void onReceived_validDataForBackgroundApp_postNotificationAndNotifyJs() throws Exception { + // Arrange + + setUpForegroundApp(); + + // Act + + final PushNotification uut = createUUT(); + uut.onReceived(); + + // Assert + + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager).notify(anyInt(), notificationCaptor.capture()); + verifyNotification(notificationCaptor.getValue()); + + verify(mJsIOHelper).sendEventToJS(eq(NOTIFICATION_RECEIVED_EVENT_NAME), eq(mDefaultBundle), eq(mReactContext)); + } + + @Test + public void onReceived_validDataForDeadApp_postNotificationDontNotifyJS() throws Exception { + final PushNotification uut = createUUT(); + uut.onReceived(); + + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager).notify(anyInt(), notificationCaptor.capture()); + verifyNotification(notificationCaptor.getValue()); + + verify(mJsIOHelper, never()).sendEventToJS(eq(NOTIFICATION_RECEIVED_EVENT_NAME), any(Bundle.class), any(ReactContext.class)); + } + + @Test + public void onPostRequest_withValidDataButNoId_postNotifications() throws Exception { + + // Arrange + + setUpForegroundApp(); + + // Act + + final PushNotification uut = createUUT(); + uut.onPostRequest(null); + + // Assert + + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager).notify(anyInt(), notificationCaptor.capture()); + verifyNotification(notificationCaptor.getValue()); + + // Shouldn't notify an event on an explicit call to notification posting + verify(mJsIOHelper, never()).sendEventToJS(eq(NOTIFICATION_RECEIVED_EVENT_NAME), any(Bundle.class), any(ReactContext.class)); + } + + @Test + public void onPostRequest_withValidDataButNoId_idsShouldBeUnique() throws Exception { + createUUT().onPostRequest(null); + createUUT().onPostRequest(null); + + ArgumentCaptor idsCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mNotificationManager, times(2)).notify(idsCaptor.capture(), any(Notification.class)); + assertNotEquals(idsCaptor.getAllValues().get(0), idsCaptor.getAllValues().get(1)); + } + + @Test + public void onPostRequest_withValidDataAndExplicitId_postNotification() throws Exception { + final int id = 666; + + final PushNotification uut = createUUT(); + uut.onPostRequest(id); + + verify(mNotificationManager).notify(eq(id), any(Notification.class)); + } + + @Test + public void onPostRequest_emptyData_postNotification() throws Exception { + PushNotification uut = createUUT(new Bundle()); + uut.onPostRequest(null); + + verify(mNotificationManager).notify(anyInt(), any(Notification.class)); + } + + protected PushNotification createUUT() { + return createUUT(mDefaultBundle); + } + + protected PushNotification createUUT(Bundle bundle) { + return new PushNotification(mContext, bundle, mAppLifecycleFacade, mAppLaunchHelper, mJsIOHelper); + } + + protected void setUpBackgroundApp() { + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(true); + when(mAppLifecycleFacade.getRunningReactContext()).thenReturn(mReactContext); + when(mAppLifecycleFacade.isAppVisible()).thenReturn(false); + } + + protected void setUpForegroundApp() { + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(true); + when(mAppLifecycleFacade.getRunningReactContext()).thenReturn(mReactContext); + when(mAppLifecycleFacade.isAppVisible()).thenReturn(true); + } + + protected void verifyNotification(Notification notification) { + ShadowNotification shadowNotification = Shadows.shadowOf(notification); + assertEquals(shadowNotification.getContentText(), DEFAULT_NOTIFICATION_BODY); + assertEquals(shadowNotification.getContentTitle(), DEFAULT_NOTIFICATION_TITLE); + } +} diff --git a/android/src/test/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawerTest.java b/android/src/test/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawerTest.java new file mode 100644 index 0000000..1caf9bb --- /dev/null +++ b/android/src/test/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawerTest.java @@ -0,0 +1,64 @@ +package com.wix.reactnativenotifications.core.notificationdrawer; + +import android.app.NotificationManager; +import android.content.Context; + +import com.facebook.react.bridge.ReactContext; +import com.wix.reactnativenotifications.core.AppLaunchHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class PushNotificationsDrawerTest { + + @Mock private ReactContext mReactContext; + @Mock private Context mContext; + @Mock private NotificationManager mNotificationManager; + @Mock private AppLaunchHelper mAppLaunchHelper; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mContext.getSystemService(Context.NOTIFICATION_SERVICE)).thenReturn(mNotificationManager); + } + + @Test + public void onAppInit_clearAllNotifications() throws Exception { + createUUT().onAppInit(); + verify(mNotificationManager).cancelAll(); + } + + @Test + public void onAppVisible_clearAllNotifications() throws Exception { + createUUT().onAppVisible(); + verify(mNotificationManager).cancelAll(); + } + + @Test + public void onNotificationOpened_clearAllNotifications() throws Exception { + createUUT().onNotificationOpened(); + verify(mNotificationManager).cancelAll(); + } + + @Test + public void onNotificationClearRequest_clearSpecificNotification() throws Exception { + createUUT().onNotificationClearRequest(666); + verify(mNotificationManager).cancel(eq(666)); + verify(mNotificationManager, never()).cancelAll(); + } + + protected PushNotificationsDrawer createUUT() { + return new PushNotificationsDrawer(mContext, mAppLaunchHelper); + } +} diff --git a/example/android/myapplication/src/main/AndroidManifest.xml b/example/android/myapplication/src/main/AndroidManifest.xml index 214751c..3df23d4 100644 --- a/example/android/myapplication/src/main/AndroidManifest.xml +++ b/example/android/myapplication/src/main/AndroidManifest.xml @@ -24,11 +24,6 @@ - - - diff --git a/example/index.android.js b/example/index.android.js index b0af4b4..24274b1 100644 --- a/example/index.android.js +++ b/example/index.android.js @@ -5,7 +5,8 @@ import { AppRegistry, StyleSheet, Text, - View + View, + TouchableHighlight } from 'react-native'; import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications'; @@ -42,7 +43,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, titleText: { - fontSize: 22, + fontSize: 24, textAlign: 'center', margin: 10, }, @@ -51,6 +52,19 @@ const styles = StyleSheet.create({ textAlign: 'center', margin: 10, }, + mainButtonText: { + fontSize: 25, + fontStyle: 'italic', + fontWeight: 'bold', + textAlign: 'center', + margin: 10, + }, + plainButtonText: { + fontSize: 18, + fontStyle: 'italic', + textAlign: 'center', + margin: 10, + }, }); class MainComponent extends Component { @@ -58,6 +72,9 @@ class MainComponent extends Component { constructor(props) { super(props); + this.onPostNotification = this.onPostNotification.bind(this); + this.onCancelNotification = this.onCancelNotification.bind(this); + this.state = { elapsed: 0, lastNotification: undefined @@ -84,6 +101,17 @@ class MainComponent extends Component { this.setState({elapsed: this.state.elapsed + 1}); } + onPostNotification() { + this.lastNotificationId = NotificationsAndroid.localNotification({title: "Local notification", body: "This notification was generated by the app!"}); + } + + onCancelNotification() { + if (this.lastNotificationId) { + NotificationsAndroid.cancelLocalNotification(this.lastNotificationId); + this.lastNotificationId = undefined; + } + } + render() { return ( @@ -91,6 +119,13 @@ class MainComponent extends Component { {this.state.initialNotification ? 'Opened from notification' : ''} Last notification: {this.state.lastNotification ? '\n'+this.state.lastNotification.body + ` (opened at ''${this.state.notificationRxTime})` : "N/A"} Time elapsed: {this.state.elapsed} + {"\n\n"} + this.onPostNotification()}> + Try Me! + + this.onCancelNotification()}> + Undo last + ) } diff --git a/index.android.js b/index.android.js index f78dc23..e98936d 100644 --- a/index.android.js +++ b/index.android.js @@ -44,6 +44,16 @@ export class NotificationsAndroid { static refreshToken() { RNNotifications.refreshToken(); } + + static localNotification(notification: Object) { + const id = Date.now() | 0; // Bitwise-OR forces value onto a 32bit limit + RNNotifications.postLocalNotification(notification, id); + return id; + } + + static cancelLocalNotification(id) { + RNNotifications.cancelLocalNotification(id); + } } export class PendingNotifications { diff --git a/test/index.android.spec.js b/test/index.android.spec.js index 4462924..d471737 100644 --- a/test/index.android.spec.js +++ b/test/index.android.spec.js @@ -3,16 +3,20 @@ let expect = require("chai").use(require("sinon-chai")).expect; import proxyquire from "proxyquire"; import sinon from "sinon"; -describe("Notifications-Android", () => { +describe("Notifications-Android > ", () => { proxyquire.noCallThru(); let refreshTokenStub; let getInitialNotificationStub; + let postLocalNotificationStub; + let cancelLocalNotificationStub; let deviceEventEmitterListenerStub; let libUnderTest; beforeEach(() => { refreshTokenStub = sinon.stub(); getInitialNotificationStub = sinon.stub(); + postLocalNotificationStub = sinon.stub(); + cancelLocalNotificationStub = sinon.stub(); deviceEventEmitterListenerStub = sinon.stub(); libUnderTest = proxyquire("../index.android", { @@ -20,7 +24,9 @@ describe("Notifications-Android", () => { NativeModules: { WixRNNotifications: { refreshToken: refreshTokenStub, - getInitialNotification: getInitialNotificationStub + getInitialNotification: getInitialNotificationStub, + postLocalNotification: postLocalNotificationStub, + cancelLocalNotification: cancelLocalNotificationStub } }, DeviceEventEmitter: { @@ -153,10 +159,12 @@ describe("Notifications-Android", () => { }); }); - it("should refresh notification token upon refreshing request by the user", () => { - expect(refreshTokenStub).to.not.have.been.called; - libUnderTest.NotificationsAndroid.refreshToken(); - expect(refreshTokenStub).to.have.been.calledOnce; + describe("Notification token", () => { + it("should refresh notification token upon refreshing request by the user", () => { + expect(refreshTokenStub).to.not.have.been.called; + libUnderTest.NotificationsAndroid.refreshToken(); + expect(refreshTokenStub).to.have.been.calledOnce; + }); }); describe("Initial notification API", () => { @@ -187,4 +195,37 @@ describe("Notifications-Android", () => { }); + describe("Local notification", () => { + const notification = { + title: "notification-title", + body: "notification-body" + }; + + it("should get published when posted manually", () => { + expect(postLocalNotificationStub).to.not.have.been.called; + + const id = libUnderTest.NotificationsAndroid.localNotification(notification); + expect(id).to.not.be.undefined; + expect(postLocalNotificationStub).to.have.been.calledWith(notification, id); + }); + + it("should be called with a unique ID", () => { + expect(postLocalNotificationStub).to.not.have.been.called; + + const id = libUnderTest.NotificationsAndroid.localNotification(notification); + const id2 = libUnderTest.NotificationsAndroid.localNotification(notification); + expect(id).to.not.be.undefined; + expect(id2).to.not.be.undefined; + expect(id).to.not.equal(id2); + }); + + it("should be cancellable with an ID", () => { + expect(cancelLocalNotificationStub).to.not.have.been.called; + + libUnderTest.NotificationsAndroid.cancelLocalNotification(666); + + expect(cancelLocalNotificationStub).to.have.been.calledWith(666); + }); + }); + }); -- 2.26.2