diff --git a/android/build.gradle b/android/build.gradle index e412eeaf948772fbb6d63f13c1750e154f78619e..7be952cc49be85b7e43ae734b9f90ba7f27e6c56 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,10 +16,6 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - - testOptions { - unitTests.returnDefaultValues = true - } } dependencies { @@ -30,4 +26,5 @@ dependencies { 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/RNNotificationsModule.java b/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java index 150196c47abcebef3edecf70fbf5bd7f5015c456..cb8935ba911fd544370eadc26b7a62113bf44edb 100644 --- a/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java +++ b/android/src/main/java/com/wix/reactnativenotifications/core/RNNotificationsModule.java @@ -70,22 +70,15 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule implements } @ReactMethod - public void postLocalNotification(ReadableMap notificationPropsMap, final Promise promise) { + public void postLocalNotification(ReadableMap notificationPropsMap, int notificationId) { Log.d(LOGTAG, "Native method invocation: postLocalNotification"); - Object result = null; - - try { - final Bundle notificationProps = Arguments.toBundle(notificationPropsMap); - final IPushNotification pushNotification = PushNotification.get(getReactApplicationContext().getApplicationContext(), notificationProps, ReactAppLifecycleFacade.get()); - int id = pushNotification.onPostRequest(); - result = id; - } finally { - promise.resolve(result); - } + final Bundle notificationProps = Arguments.toBundle(notificationPropsMap); + final IPushNotification pushNotification = PushNotification.get(getReactApplicationContext().getApplicationContext(), notificationProps, ReactAppLifecycleFacade.get()); + pushNotification.onPostRequest(notificationId); } @ReactMethod - public void removeLocalNotification(int notificationId) { + public void cancelLocalNotification(int notificationId) { IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext()); notificationsDrawer.onNotificationClear(notificationId); } 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 46f1015d59f84acd2feceee1f791451cec7a0098..0d70024acb10a42280c030892eea5b3df0e81624 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 @@ -20,9 +20,11 @@ public interface IPushNotification { /** * Handle a request to post this notification. - * @return ID to optionally use for notification deletion. + * + * @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(); + 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 4d1715ff6f7bb8192b4124d105baa9eee672b4cc..5bc7db29ae58aa91fef5afc28c2a66209d40e350 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,13 +7,7 @@ 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; @@ -66,7 +60,7 @@ public class PushNotification implements IPushNotification { @Override public void onReceived() throws InvalidNotificationException { - postNotification(); + postNotification(null); notifyReceivedToJS(); } @@ -76,8 +70,8 @@ public class PushNotification implements IPushNotification { } @Override - public int onPostRequest() { - return postNotification(); + public int onPostRequest(Integer notificationId) { + return postNotification(notificationId); } @Override @@ -85,10 +79,10 @@ public class PushNotification implements IPushNotification { return mNotificationProps.copy(); } - protected int postNotification() { + protected int postNotification(Integer notificationId) { final PendingIntent pendingIntent = getCTAPendingIntent(); final Notification notification = buildNotification(pendingIntent); - return postNotification(notification); + return postNotification(notification, notificationId); } protected void digestNotification() { @@ -153,8 +147,8 @@ public class PushNotification implements IPushNotification { .setAutoCancel(true); } - protected int postNotification(Notification notification) { - int id = createNotificationId(notification); + protected int postNotification(Notification notification, Integer notificationId) { + int id = notificationId != null ? notificationId : createNotificationId(notification); postNotification(id, notification); return id; } 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 index bde7d32d10c34971c86733f730abe9b88d0f6f32..99fde0dd81ac76b4b6a560df90cc6c9500f79256 100644 --- a/android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java +++ b/android/src/test/java/com/wix/reactnativenotifications/core/notification/PushNotificationTest.java @@ -15,7 +15,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; +import org.robolectric.RobolectricTestRunner; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -23,9 +25,12 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.Silent.class) +@RunWith(RobolectricTestRunner.class) public class PushNotificationTest { + private static final String NOTIFICATION_OPENED_EVENT_NAME = "notificationOpened"; + private static final String NOTIFICATION_RECEIVED_EVENT_NAME = "notificationReceived"; + @Mock private ReactContext mReactContext; @Mock private Context mContext; @@ -37,6 +42,8 @@ public class PushNotificationTest { @Before public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + when(mDefaultBundle.getString(eq("title"))).thenReturn("Notification-title"); when(mDefaultBundle.getString(eq("body"))).thenReturn("Notification-body"); when(mDefaultBundle.clone()).thenReturn(mDefaultBundle); @@ -88,7 +95,7 @@ public class PushNotificationTest { // Assert - verify(mReactContextAdapter).sendEventToJS(eq("notificationOpened"), eq(mDefaultBundle), eq(mContext)); + verify(mReactContextAdapter).sendEventToJS(eq(NOTIFICATION_OPENED_EVENT_NAME), eq(mDefaultBundle), eq(mContext)); } @Test @@ -100,7 +107,15 @@ public class PushNotificationTest { uut.onOpened(); verify(mContext, never()).startActivity(any(Intent.class)); - verify(mReactContextAdapter).sendEventToJS(eq("notificationOpened"), eq(mDefaultBundle), eq(mContext)); + verify(mReactContextAdapter).sendEventToJS(eq(NOTIFICATION_OPENED_EVENT_NAME), eq(mDefaultBundle), eq(mContext)); + } + + @Test + public void onPostRequest_withValidDataButNoId_postNotifAndNotifyJS() throws Exception { + final PushNotification uut = createUUT(); + uut.onPostRequest(null); + + verify(mReactContextAdapter).sendEventToJS(eq(NOTIFICATION_RECEIVED_EVENT_NAME), eq(mDefaultBundle), eq(mContext)); } protected PushNotification createUUT() { diff --git a/example/android/myapplication/src/main/AndroidManifest.xml b/example/android/myapplication/src/main/AndroidManifest.xml index 214751cac600ef8a87168b9eaf0496b3bf2c52e6..3df23d464dc64562b5bc9578f41bfe28c284a706 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 b0af4b48f3fc411cc4a36de8c1f478b3fe29cb83..24274b15a123906afc0ccca6e71e6a2fdfb9ae16 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 f78dc231e275281d463a106b5ece8aeadd428195..e98936d8c4c3e0b6266cd241e1fee9ff9b5060fd 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 4462924b113cb2ad14d65ae9743d8ac0e5330f47..d471737551ae1f0a62622fe67b6ba782e097e5cc 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); + }); + }); + });