diff --git a/android/build.gradle b/android/build.gradle index 4e2b7cfccda11c42db61b8fd5c68b6489b6dfab5..e412eeaf948772fbb6d63f13c1750e154f78619e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,13 +16,18 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + testOptions { + unitTests.returnDefaultValues = true + } } 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.+' } 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 b098e7c5d26b7fa1f679da3a10e3b93dbbb888dc..e2d243132ff6ae9c65b549744cf9786f60e24c9d 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/ProxyService.java b/android/src/main/java/com/wix/reactnativenotifications/core/ProxyService.java index bbc10c8b36bc7b101910d2c563959800a91f27b2..5f9e031b31fde6addd27a0714be35ea3bfb0bb63 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/ReactContextAdapter.java b/android/src/main/java/com/wix/reactnativenotifications/core/ReactContextAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..9dfea28fc92c6c90193d0ab83fb00e98cf5b2ebf --- /dev/null +++ b/android/src/main/java/com/wix/reactnativenotifications/core/ReactContextAdapter.java @@ -0,0 +1,51 @@ +package com.wix.reactnativenotifications.core; + +import android.content.Context; +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; + +public class ReactContextAdapter { + public ReactContext getRunningReactContext(Context context) { + final ReactNativeHost rnHost = ((ReactApplication) context.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; + } + + public void sendEventToJS(String eventName, Bundle data, Context context) { + final ReactContext reactContext = getRunningReactContext(context); + if (reactContext != null) { + sendEventToJS(eventName, data, reactContext); + } + } + + public void sendEventToJS(String eventName, WritableMap data, Context context) { + final ReactContext reactContext = getRunningReactContext(context); + if (reactContext != null) { + sendEventToJS(eventName, data, reactContext); + } + } + + public void sendEventToJS(String eventName, Bundle data, ReactContext reactContext) { + sendEventToJS(eventName, Arguments.fromBundle(data), reactContext); + } + + public void sendEventToJS(String eventName, WritableMap data, ReactContext reactContext) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, data); + } +} 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 6b46988ed3d2c3db15f22a7fc121ad261d8b55ba..ba88dbd57a8394b0534c2cf969b65417001d0e72 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/PushNotification.java b/android/src/main/java/com/wix/reactnativenotifications/core/notification/PushNotification.java index 5b8224e66d798acf6ef3de66c304747fd81c9c73..4d1715ff6f7bb8192b4124d105baa9eee672b4cc 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 @@ -20,7 +20,7 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade.AppVisibilityLis 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.ReactContextAdapter; import static com.wix.reactnativenotifications.Defs.NOTIFICATION_OPENED_EVENT_NAME; import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME; @@ -29,6 +29,8 @@ public class PushNotification implements IPushNotification { final protected Context mContext; final protected AppLifecycleFacade mAppLifecycleFacade; + final protected AppLaunchHelper mAppLaunchHelper; + final protected ReactContextAdapter mReactContextAdapter; final protected PushNotificationProps mNotificationProps; final protected AppVisibilityListener mAppVisibilityListener = new AppVisibilityListener() { @Override @@ -42,18 +44,24 @@ 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 ReactContextAdapter()); + } + + protected PushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, ReactContextAdapter reactContextAdapter) { + mContext = context; + mAppLifecycleFacade = appLifecycleFacade; + mAppLaunchHelper = appLaunchHelper; + mReactContextAdapter = reactContextAdapter; + mNotificationProps = createProps(bundle); } @Override @@ -65,7 +73,6 @@ public class PushNotification implements IPushNotification { @Override public void onOpened() { digestNotification(); - PushNotificationsDrawer.get(mContext).onNotificationOpened(); } @Override @@ -91,7 +98,7 @@ public class PushNotification implements IPushNotification { return; } - final ReactContext reactContext = getRunningReactContext(); + final ReactContext reactContext = mReactContextAdapter.getRunningReactContext(mContext); if (reactContext.getCurrentActivity() == null) { setAsInitialNotification(); } @@ -161,46 +168,16 @@ public class PushNotification implements IPushNotification { return (int) System.currentTimeMillis(); } - 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; - } - private void notifyReceivedToJS() { - notifyJS(NOTIFICATION_RECEIVED_EVENT_NAME, null); + mReactContextAdapter.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mContext); } 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); - } + mReactContextAdapter.sendEventToJS(NOTIFICATION_OPENED_EVENT_NAME, mNotificationProps.asBundle(), mContext); } 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/PushNotificationsDrawer.java b/android/src/main/java/com/wix/reactnativenotifications/core/notificationdrawer/PushNotificationsDrawer.java index 5c96426deaba63e022d5c6d3da40e29854e1a80e..e9f791f96c7e3539558d586a1acec9d90389af7b 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(); } } 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 0000000000000000000000000000000000000000..bde7d32d10c34971c86733f730abe9b88d0f6f32 --- /dev/null +++ b/android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java @@ -0,0 +1,109 @@ +package com.wix.reactnativenotifications.core.notification; + +import android.content.Context; +import android.content.Intent; +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.ReactContextAdapter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.mockito.ArgumentMatchers.any; +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(MockitoJUnitRunner.Silent.class) +public class PushNotificationTest { + + @Mock private ReactContext mReactContext; + @Mock private Context mContext; + + @Mock private Bundle mDefaultBundle; + @Mock private Intent mLaunchIntent; + @Mock private AppLifecycleFacade mAppLifecycleFacade; + @Mock private AppLaunchHelper mAppLaunchHelper; + @Mock private ReactContextAdapter mReactContextAdapter; + + @Before + public void setup() throws Exception { + when(mDefaultBundle.getString(eq("title"))).thenReturn("Notification-title"); + when(mDefaultBundle.getString(eq("body"))).thenReturn("Notification-body"); + when(mDefaultBundle.clone()).thenReturn(mDefaultBundle); + + when(mAppLaunchHelper.getLaunchIntent(eq(mContext))).thenReturn(mLaunchIntent); + when(mReactContextAdapter.getRunningReactContext(mContext)).thenReturn(mReactContext); + } + + @Test + public void onOpened_noReactContext_launchApp() throws Exception { + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(false); + + final PushNotification uut = createUUT(); + uut.onOpened(); + + verify(mContext).startActivity(eq(mLaunchIntent)); + } + + @Test + public void onOpened_appInvisible_resumeAppWaitForVisibility() throws Exception { + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(true); + when(mAppLifecycleFacade.isAppVisible()).thenReturn(false); + + 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 + + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(true); + when(mAppLifecycleFacade.isAppVisible()).thenReturn(false); + + // 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(mReactContextAdapter).sendEventToJS(eq("notificationOpened"), eq(mDefaultBundle), eq(mContext)); + } + + @Test + public void onOpened_appVisible_notifyJS() throws Exception { + when(mAppLifecycleFacade.isReactInitialized()).thenReturn(true); + when(mAppLifecycleFacade.isAppVisible()).thenReturn(true); + + final PushNotification uut = createUUT(); + uut.onOpened(); + + verify(mContext, never()).startActivity(any(Intent.class)); + verify(mReactContextAdapter).sendEventToJS(eq("notificationOpened"), eq(mDefaultBundle), eq(mContext)); + } + + protected PushNotification createUUT() { + return new PushNotification(mContext, mDefaultBundle, mAppLifecycleFacade, mAppLaunchHelper, mReactContextAdapter); + } +}