Commit f3e4de90 authored by d4vidi's avatar d4vidi

Local notifications set/clear with tests and refactoring

parent 4353cc6f
...@@ -20,9 +20,11 @@ android { ...@@ -20,9 +20,11 @@ android {
dependencies { dependencies {
// Google's GCM. // 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:+' compile 'com.facebook.react:react-native:+'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.+'
testCompile "org.robolectric:robolectric:3.1.4"
} }
...@@ -10,7 +10,7 @@ public class AppLaunchHelper { ...@@ -10,7 +10,7 @@ public class AppLaunchHelper {
private static final String LAUNCH_FLAG_KEY_NAME = "launchedFromNotification"; private static final String LAUNCH_FLAG_KEY_NAME = "launchedFromNotification";
public static Intent getLaunchIntent(Context appContext) { public Intent getLaunchIntent(Context appContext) {
try { try {
// The desired behavior upon notification opening is as follows: // 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. // - 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 { ...@@ -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 Intent helperIntent = activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName());
final String activityName = activity.getComponentName().getClassName(); final String activityName = activity.getComponentName().getClassName();
final String launchIntentActivityName = helperIntent.getComponent().getClassName(); final String launchIntentActivityName = helperIntent.getComponent().getClassName();
return activityName.equals(launchIntentActivityName); return activityName.equals(launchIntentActivityName);
} }
public static boolean isLaunchIntent(Intent intent) { public boolean isLaunchIntent(Intent intent) {
return intent.getBooleanExtra(LAUNCH_FLAG_KEY_NAME, false); return intent.getBooleanExtra(LAUNCH_FLAG_KEY_NAME, false);
} }
} }
package com.wix.reactnativenotifications.core; package com.wix.reactnativenotifications.core;
import com.facebook.react.bridge.ReactContext;
public interface AppLifecycleFacade { public interface AppLifecycleFacade {
interface AppVisibilityListener { interface AppVisibilityListener {
...@@ -8,6 +10,7 @@ public interface AppLifecycleFacade { ...@@ -8,6 +10,7 @@ public interface AppLifecycleFacade {
} }
boolean isReactInitialized(); boolean isReactInitialized();
ReactContext getRunningReactContext();
boolean isAppVisible(); boolean isAppVisible();
void addVisibilityListener(AppVisibilityListener listener); void addVisibilityListener(AppVisibilityListener listener);
void removeVisibilityListener(AppVisibilityListener listener); void removeVisibilityListener(AppVisibilityListener listener);
......
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;
}
}
...@@ -13,7 +13,7 @@ public class NotificationIntentAdapter { ...@@ -13,7 +13,7 @@ public class NotificationIntentAdapter {
public static PendingIntent createPendingNotificationIntent(Context appContext, Intent intent, PushNotificationProps notification) { public static PendingIntent createPendingNotificationIntent(Context appContext, Intent intent, PushNotificationProps notification) {
intent.putExtra(PUSH_NOTIFICATION_EXTRA_NAME, notification.asBundle()); 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) { public static Bundle extractPendingNotificationDataFromIntent(Intent intent) {
......
...@@ -7,6 +7,8 @@ import android.util.Log; ...@@ -7,6 +7,8 @@ import android.util.Log;
import com.wix.reactnativenotifications.core.notification.IPushNotification; import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notification.PushNotification; 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 { public class ProxyService extends IntentService {
...@@ -24,5 +26,8 @@ public class ProxyService extends IntentService { ...@@ -24,5 +26,8 @@ public class ProxyService extends IntentService {
if (pushNotification != null) { if (pushNotification != null) {
pushNotification.onOpened(); pushNotification.onOpened();
} }
final IPushNotificationsDrawer pushNotificationDrawer = PushNotificationsDrawer.get(this);
pushNotificationDrawer.onNotificationOpened();
} }
} }
...@@ -12,6 +12,9 @@ import com.facebook.react.bridge.Promise; ...@@ -12,6 +12,9 @@ import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; 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.notification.PushNotificationProps;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer; import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer; import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
...@@ -66,6 +69,20 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements ...@@ -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 @Override
public void onAppVisible() { public void onAppVisible() {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext()); final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
......
...@@ -54,6 +54,16 @@ public class ReactAppLifecycleFacade implements AppLifecycleFacade { ...@@ -54,6 +54,16 @@ public class ReactAppLifecycleFacade implements AppLifecycleFacade {
return mReactContext.hasActiveCatalystInstance(); return mReactContext.hasActiveCatalystInstance();
} }
@Override
public ReactContext getRunningReactContext() {
ReactContext reactContext = mReactContext;
if (reactContext == null) {
return null;
}
return mReactContext;
}
@Override @Override
public boolean isAppVisible() { public boolean isAppVisible() {
return mIsVisible; return mIsVisible;
......
...@@ -3,8 +3,9 @@ package com.wix.reactnativenotifications.core.notification; ...@@ -3,8 +3,9 @@ package com.wix.reactnativenotifications.core.notification;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.AppLifecycleFacade;
public interface INotificationsApplication { public interface INotificationsApplication {
IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade facade); IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper);
} }
...@@ -18,5 +18,13 @@ public interface IPushNotification { ...@@ -18,5 +18,13 @@ public interface IPushNotification {
*/ */
void onOpened(); 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(); PushNotificationProps asProps();
} }
...@@ -7,20 +7,14 @@ import android.content.Context; ...@@ -7,20 +7,14 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; 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.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.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.AppLifecycleFacade.AppVisibilityListener; import com.wix.reactnativenotifications.core.AppLifecycleFacade.AppVisibilityListener;
import com.wix.reactnativenotifications.core.InitialNotification; import com.wix.reactnativenotifications.core.InitialNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter; import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.ProxyService; 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_OPENED_EVENT_NAME;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME; import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
...@@ -29,6 +23,8 @@ public class PushNotification implements IPushNotification { ...@@ -29,6 +23,8 @@ public class PushNotification implements IPushNotification {
final protected Context mContext; final protected Context mContext;
final protected AppLifecycleFacade mAppLifecycleFacade; final protected AppLifecycleFacade mAppLifecycleFacade;
final protected AppLaunchHelper mAppLaunchHelper;
final protected JsIOHelper mJsIOHelper;
final protected PushNotificationProps mNotificationProps; final protected PushNotificationProps mNotificationProps;
final protected AppVisibilityListener mAppVisibilityListener = new AppVisibilityListener() { final protected AppVisibilityListener mAppVisibilityListener = new AppVisibilityListener() {
@Override @Override
...@@ -42,30 +38,40 @@ public class PushNotification implements IPushNotification { ...@@ -42,30 +38,40 @@ public class PushNotification implements IPushNotification {
} }
}; };
protected PushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade) { public static IPushNotification get(Context context, Bundle bundle, AppLifecycleFacade facade) {
mContext = context; return PushNotification.get(context, bundle, facade, new AppLaunchHelper());
mAppLifecycleFacade = appLifecycleFacade;
mNotificationProps = createProps(bundle);
} }
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(); Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsApplication) { 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 @Override
public void onReceived() throws InvalidNotificationException { public void onReceived() throws InvalidNotificationException {
postNotification(); postNotification(null);
notifyReceivedToJS(); notifyReceivedToJS();
} }
@Override @Override
public void onOpened() { public void onOpened() {
digestNotification(); digestNotification();
PushNotificationsDrawer.get(mContext).onNotificationOpened(); }
@Override
public int onPostRequest(Integer notificationId) {
return postNotification(notificationId);
} }
@Override @Override
...@@ -73,10 +79,10 @@ public class PushNotification implements IPushNotification { ...@@ -73,10 +79,10 @@ public class PushNotification implements IPushNotification {
return mNotificationProps.copy(); return mNotificationProps.copy();
} }
protected void postNotification() { protected int postNotification(Integer notificationId) {
final PendingIntent pendingIntent = getCTAPendingIntent(); final PendingIntent pendingIntent = getCTAPendingIntent();
final Notification notification = buildNotification(pendingIntent); final Notification notification = buildNotification(pendingIntent);
postNotification((int) System.currentTimeMillis(), notification); return postNotification(notification, notificationId);
} }
protected void digestNotification() { protected void digestNotification() {
...@@ -86,7 +92,7 @@ public class PushNotification implements IPushNotification { ...@@ -86,7 +92,7 @@ public class PushNotification implements IPushNotification {
return; return;
} }
final ReactContext reactContext = getRunningReactContext(); final ReactContext reactContext = mAppLifecycleFacade.getRunningReactContext();
if (reactContext.getCurrentActivity() == null) { if (reactContext.getCurrentActivity() == null) {
setAsInitialNotification(); setAsInitialNotification();
} }
...@@ -141,51 +147,31 @@ public class PushNotification implements IPushNotification { ...@@ -141,51 +147,31 @@ public class PushNotification implements IPushNotification {
.setAutoCancel(true); .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) { protected void postNotification(int id, Notification notification) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(id, notification); notificationManager.notify(id, notification);
} }
protected ReactContext getRunningReactContext() { protected int createNotificationId(Notification notification) {
final ReactNativeHost rnHost = ((ReactApplication) mContext.getApplicationContext()).getReactNativeHost(); return (int) System.nanoTime();
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() { private void notifyReceivedToJS() {
notifyJS(NOTIFICATION_RECEIVED_EVENT_NAME, null); mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
} }
private void notifyOpenedToJS() { private void notifyOpenedToJS() {
notifyOpenedToJS(null); mJsIOHelper.sendEventToJS(NOTIFICATION_OPENED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
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);
}
} }
protected void launchOrResumeApp() { protected void launchOrResumeApp() {
final Intent intent = AppLaunchHelper.getLaunchIntent(mContext); final Intent intent = mAppLaunchHelper.getLaunchIntent(mContext);
mContext.startActivity(intent); mContext.startActivity(intent);
} }
} }
...@@ -8,4 +8,5 @@ public interface IPushNotificationsDrawer { ...@@ -8,4 +8,5 @@ public interface IPushNotificationsDrawer {
void onNewActivity(Activity activity); void onNewActivity(Activity activity);
void onNotificationOpened(); void onNotificationOpened();
void onNotificationClearRequest(int id);
} }
...@@ -10,18 +10,24 @@ import com.wix.reactnativenotifications.core.InitialNotification; ...@@ -10,18 +10,24 @@ import com.wix.reactnativenotifications.core.InitialNotification;
public class PushNotificationsDrawer implements IPushNotificationsDrawer { public class PushNotificationsDrawer implements IPushNotificationsDrawer {
final protected Context mContext; final protected Context mContext;
final protected AppLaunchHelper mAppLaunchHelper;
public PushNotificationsDrawer(Context context) { public static IPushNotificationsDrawer get(Context context) {
mContext = 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(); final Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsDrawerApplication) { if (appContext instanceof INotificationsDrawerApplication) {
return ((INotificationsDrawerApplication) appContext).getPushNotificationsDrawer(context); 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 @Override
...@@ -36,8 +42,8 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer { ...@@ -36,8 +42,8 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer {
@Override @Override
public void onNewActivity(Activity activity) { public void onNewActivity(Activity activity) {
if (AppLaunchHelper.isLaunchIntentsActivity(activity) && if (mAppLaunchHelper.isLaunchIntentsActivity(activity) &&
!AppLaunchHelper.isLaunchIntent(activity.getIntent())) { !mAppLaunchHelper.isLaunchIntent(activity.getIntent())) {
InitialNotification.clear(); InitialNotification.clear();
} }
} }
...@@ -47,6 +53,12 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer { ...@@ -47,6 +53,12 @@ public class PushNotificationsDrawer implements IPushNotificationsDrawer {
clearAll(); clearAll();
} }
@Override
public void onNotificationClearRequest(int id) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(id);
}
protected void clearAll() { protected void clearAll() {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll(); notificationManager.cancelAll();
......
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<AppVisibilityListener> 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<Notification> 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<Notification> 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<Notification> 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<Notification> 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<Integer> 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);
}
}
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);
}
}
...@@ -24,11 +24,6 @@ ...@@ -24,11 +24,6 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ChildActivity"
android:label="Child Activity">
<meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.wix.reactnativenotifications.app.MainActivity"/>
</activity>
</application> </application>
......
...@@ -5,7 +5,8 @@ import { ...@@ -5,7 +5,8 @@ import {
AppRegistry, AppRegistry,
StyleSheet, StyleSheet,
Text, Text,
View View,
TouchableHighlight
} from 'react-native'; } from 'react-native';
import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications'; import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications';
...@@ -42,7 +43,7 @@ const styles = StyleSheet.create({ ...@@ -42,7 +43,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
titleText: { titleText: {
fontSize: 22, fontSize: 24,
textAlign: 'center', textAlign: 'center',
margin: 10, margin: 10,
}, },
...@@ -51,6 +52,19 @@ const styles = StyleSheet.create({ ...@@ -51,6 +52,19 @@ const styles = StyleSheet.create({
textAlign: 'center', textAlign: 'center',
margin: 10, 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 { class MainComponent extends Component {
...@@ -58,6 +72,9 @@ class MainComponent extends Component { ...@@ -58,6 +72,9 @@ class MainComponent extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onPostNotification = this.onPostNotification.bind(this);
this.onCancelNotification = this.onCancelNotification.bind(this);
this.state = { this.state = {
elapsed: 0, elapsed: 0,
lastNotification: undefined lastNotification: undefined
...@@ -84,6 +101,17 @@ class MainComponent extends Component { ...@@ -84,6 +101,17 @@ class MainComponent extends Component {
this.setState({elapsed: this.state.elapsed + 1}); 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() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
...@@ -91,6 +119,13 @@ class MainComponent extends Component { ...@@ -91,6 +119,13 @@ class MainComponent extends Component {
<Text style={styles.bodyText}>{this.state.initialNotification ? 'Opened from notification' : ''}</Text> <Text style={styles.bodyText}>{this.state.initialNotification ? 'Opened from notification' : ''}</Text>
<Text style={styles.bodyText}>Last notification: {this.state.lastNotification ? '\n'+this.state.lastNotification.body + ` (opened at ''${this.state.notificationRxTime})` : "N/A"}</Text> <Text style={styles.bodyText}>Last notification: {this.state.lastNotification ? '\n'+this.state.lastNotification.body + ` (opened at ''${this.state.notificationRxTime})` : "N/A"}</Text>
<Text style={styles.bodyText}>Time elapsed: {this.state.elapsed}</Text> <Text style={styles.bodyText}>Time elapsed: {this.state.elapsed}</Text>
<Text>{"\n\n"}</Text>
<TouchableHighlight onPress={() => this.onPostNotification()}>
<Text style={styles.mainButtonText}>Try Me!</Text>
</TouchableHighlight>
<TouchableHighlight onPress={() => this.onCancelNotification()}>
<Text style={styles.plainButtonText}>Undo last</Text>
</TouchableHighlight>
</View> </View>
) )
} }
......
...@@ -44,6 +44,16 @@ export class NotificationsAndroid { ...@@ -44,6 +44,16 @@ export class NotificationsAndroid {
static refreshToken() { static refreshToken() {
RNNotifications.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 { export class PendingNotifications {
......
...@@ -3,16 +3,20 @@ let expect = require("chai").use(require("sinon-chai")).expect; ...@@ -3,16 +3,20 @@ let expect = require("chai").use(require("sinon-chai")).expect;
import proxyquire from "proxyquire"; import proxyquire from "proxyquire";
import sinon from "sinon"; import sinon from "sinon";
describe("Notifications-Android", () => { describe("Notifications-Android > ", () => {
proxyquire.noCallThru(); proxyquire.noCallThru();
let refreshTokenStub; let refreshTokenStub;
let getInitialNotificationStub; let getInitialNotificationStub;
let postLocalNotificationStub;
let cancelLocalNotificationStub;
let deviceEventEmitterListenerStub; let deviceEventEmitterListenerStub;
let libUnderTest; let libUnderTest;
beforeEach(() => { beforeEach(() => {
refreshTokenStub = sinon.stub(); refreshTokenStub = sinon.stub();
getInitialNotificationStub = sinon.stub(); getInitialNotificationStub = sinon.stub();
postLocalNotificationStub = sinon.stub();
cancelLocalNotificationStub = sinon.stub();
deviceEventEmitterListenerStub = sinon.stub(); deviceEventEmitterListenerStub = sinon.stub();
libUnderTest = proxyquire("../index.android", { libUnderTest = proxyquire("../index.android", {
...@@ -20,7 +24,9 @@ describe("Notifications-Android", () => { ...@@ -20,7 +24,9 @@ describe("Notifications-Android", () => {
NativeModules: { NativeModules: {
WixRNNotifications: { WixRNNotifications: {
refreshToken: refreshTokenStub, refreshToken: refreshTokenStub,
getInitialNotification: getInitialNotificationStub getInitialNotification: getInitialNotificationStub,
postLocalNotification: postLocalNotificationStub,
cancelLocalNotification: cancelLocalNotificationStub
} }
}, },
DeviceEventEmitter: { DeviceEventEmitter: {
...@@ -153,11 +159,13 @@ describe("Notifications-Android", () => { ...@@ -153,11 +159,13 @@ describe("Notifications-Android", () => {
}); });
}); });
describe("Notification token", () => {
it("should refresh notification token upon refreshing request by the user", () => { it("should refresh notification token upon refreshing request by the user", () => {
expect(refreshTokenStub).to.not.have.been.called; expect(refreshTokenStub).to.not.have.been.called;
libUnderTest.NotificationsAndroid.refreshToken(); libUnderTest.NotificationsAndroid.refreshToken();
expect(refreshTokenStub).to.have.been.calledOnce; expect(refreshTokenStub).to.have.been.calledOnce;
}); });
});
describe("Initial notification API", () => { describe("Initial notification API", () => {
it("should return initial notification data if available", (done) => { it("should return initial notification data if available", (done) => {
...@@ -187,4 +195,37 @@ describe("Notifications-Android", () => { ...@@ -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);
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment