Commit c8594ba1 authored by d4vidi's avatar d4vidi Committed by GitHub

Merge pull request #10 from wix/android

Android alpha
parents 8762d5e5 900f871a
The MIT License (MIT)
Copyright (c) 2013 Steve
Copyright (c) 2016 Wix.com LTD
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
......
......@@ -2,9 +2,11 @@
Handle all the aspects of push notifications for your app, including remote and local notifications, interactive notifications, silent notifications, and more.
**All the native iOS notifications features are supported!** Android push support is in progress.
**All the native iOS notifications features are supported!**
### Supported Features (iOS)
## Supported Features
### iOS
- [Remote notifications](#handling-received-notifications).
- [Local notifications](#triggering-local-notifications).
......@@ -15,6 +17,16 @@ Handle all the aspects of push notifications for your app, including remote and
![Interactive notifications example](https://s3.amazonaws.com/nrjio/interactive.gif)
### Android
>**Please advise that Android support is a work in progress and is subject to breaking changes in the near future**
- Receiving notifications in any App state (foreground, background, "dead")
- Built-in notification drawer management
- High degree of code extensibility to allow for advanced custom layouts and any specific notifications behavior as available by [Android's API](https://developer.android.com/training/notify-user/build-notification.html)
- Android equivalent of React-Native's implementation of [`PushNotificationsIOS.getInitialNotification()`](https://facebook.github.io/react-native/docs/pushnotificationios.html#getinitialnotification).
_Upcoming: local notifications, background-state Rx queue (iOS equivalent)_
## Installation
......@@ -59,12 +71,60 @@ And the following methods to support registration and receiving notifications:
```
### Android
WIP.
Add a reference to the library's native code in your global `settings.gradle`:
```
include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
```
Declare the library as a dependency in your **app-project's** `build.gradle`:
```
dependencies {
// ...
compile project(':reactnativenotifications')
}
```
### Receiving push notifications
> This section is only necessary in case you wish to **receive** push notifications in your React-Native app.
Push notifications on Android are managed and dispatched using [Google's GCM service](https://developers.google.com/cloud-messaging/gcm) (now integrated into Firebase). The following installation steps are a TL;DR of [Google's GCM setup guide](https://developers.google.com/cloud-messaging/android/client). You can follow them to get GCM integrated quickly, but we recommend that you will in the very least have a peek at the guide's overview.
##### Step #1: Subscribe to Google's GCM
To set GCM in your app, you must first create a Google API-project and obtain a **Sender ID** and a **Server API Key**. If you have no existing API project yet, the easiest way to go about in creating one is using [this step-by-step installation process](https://developers.google.com/mobile/add); Use [this tutorial](https://code.tutsplus.com/tutorials/how-to-get-started-with-push-notifications-on-android--cms-25870) for insturctions.
Alternatively, follow [Google's complete guide](https://developers.google.com/cloud-messaging/android/client#create-an-api-project).
##### Step #2: Add Sender ID to Manifest File
Once obtained, bundle the Sender ID onto your main `manifest.xml` file:
```
<manifest>
...
<application>
...
// Replace '1234567890' with your sender ID.
// IMPORTANT: Leave the trailing \0 intact!!!
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="1234567890\0"/>
</application>
</manifest>
```
---
## Register to Push Notifications
### iOS
In order to handle notifications, you must register before- handle `remoteNotificationsRegistered` event.
In your React Native app:
......@@ -79,7 +139,7 @@ class App extends Component {
}
onPushRegistered(deviceToken) {
console.log("Device Token Received: " + deviceToken);
console.log("Device Token Received", deviceToken);
}
componentWillUnmount() {
......@@ -92,10 +152,29 @@ class App extends Component {
When you have the device token, POST it to your server and register the device in your notifications provider (Amazon SNS, Azure, etc.).
### Android
The React-Native code equivalent on Android is:
```javascript
import {NotificationsAndroid} from 'react-native-notifications';
// On Android, we allow for only one (global) listener per each event type.
NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken) => {
console.log('Push-notifications regsitered!', deviceToken)
});
```
`deviceToken` being the token used to identify the device on the GCM.
---
## Handling Received Notifications
### iOS
When you receive a notification, the application can be in one of the following states:
1. **Forground**- When the app in running and is used by the user right now. in this case, `notificationReceivedForeground` event will be fired.
......@@ -112,15 +191,15 @@ constructor() {
}
onNotificationReceivedForeground(notification) {
console.log("Notification Received Foreground: " + JSON.stringify(notification));
console.log("Notification Received - Foreground", notification);
}
onNotificationReceivedBackground(notification) {
console.log("Notification Received Background: " + JSON.stringify(notification));
console.log("Notification Received - Background", notification);
}
onNotificationOpened(notification) {
console.log("Notification Opened: " + JSON.stringify(notification));
console.log("Notification opened by device user", notification);
}
componentWillUnmount() {
......@@ -131,7 +210,7 @@ componentWillUnmount() {
}
```
### Notification Object
#### Notification Object
When you receive a push notification, you'll get an instance of `IOSNotification` object, contains the following methods:
- **`getMessage()`**- returns the notification's main message string.
......@@ -141,17 +220,60 @@ When you receive a push notification, you'll get an instance of `IOSNotification
- **`getData()`**- returns the data payload (additional info) of the notification.
- **`getType()`**- returns `managed` for managed notifications, otherwise returns `regular`.
### Background Queue (Important!)
#### Background Queue (Important!)
When a push notification is opened but the app is not running, the application will be in a **cold launch** state, until the JS engine is up and ready to handle the notification.
The application will collect the events (notifications, actions, etc.) that happend during the cold launch for you.
When your app is ready (most of the time it's after the call to `requestPermissions()`), just call to `NotificationsIOS.consumeBackgroundQueue();` in order to consume the background queue. For more info see `index.ios.js` in the example app.
### Android
```javascript
import {NotificationsAndroid} from 'react-native-notifications';
// On Android, we allow for only one (global) listener per each event type.
NotificationsAndroid.setNotificationReceivedListener((notification) => {
console.log("Notification received on device", notification.getData());
});
NotificationsAndroid.setNotificationOpenedListener((notification) => {
console.log("Notification opened by device user", notification.getData());
});
```
#### Notification Object
- **`getData()`**- content of the `data` section of the original message (sent to GCM).
- **`getTitle()`**- Convenience for returning `data.title`.
- **`getMessage()`**- Convenience for returning `data.body`.
---
## Querying initial notification
React-Native's [`PushNotificationsIOS.getInitialNotification()`](https://facebook.github.io/react-native/docs/pushnotificationios.html#getinitialnotification) allows for the async retrieval of the original notification used to open the App on iOS, but it has no equivalent implementation for Android.
We provide a similar implementation on Android using `PendingNotifications.getInitialNotification()` which returns a promise:
```javascript
import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications';
PendingNotifications.getInitialNotification()
.then((notification) => {
console.log("Initial notification was:", (notification ? notification.getData() : 'N/A');
})
.catch((err) => console.error("getInitialNotifiation() failed", err));
```
> Notifications are considered 'initial' under the following terms:
> - User tapped on a notification, _AND_ -
> - App was either not running at all ("dead" state), _OR_ it existed in the background with **no running activities** associated with it.
## Triggering Local Notifications
> Currently, an iOS-only feture
You can schedule a local notification for future presentation.
Triggering local notifications is fully compatible with React Native `PushNotificationsIOS` library.
......
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "24.0.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
// Google's GCM related framework components.
compile "com.google.android.gms:play-services-gcm:9+"
compile 'com.facebook.react:react-native:+'
testCompile 'junit:junit:4.12'
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/amitd/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
This diff is collapsed.
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wix.reactnativenotifications">
<!--
Permissions required for enabling GCM.
-->
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<application>
<!--
A proxy-service that gives the library an opportunity to do some work before launching/resuming the actual application task.
-->
<service android:name=".core.ProxyService"/>
<!--
Google's ready-to-use GcmReceiver.
1. Awaits actual GCM messages (e.g. push notifications) and invokes the GCM service with the concrete content.
2. Awaits instance-ID/token refresh requests from the GCM and invokes the Instance-ID listener service.
-->
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<!-- Dispatched by the GcmReceiver when messages are received. -->
<service
android:name="com.wix.reactnativenotifications.gcm.GcmMessageHandlerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<!-- Dispatched by the GcmReceiver. Starts the designated refresh-handling service. -->
<service
android:name=".gcm.GcmInstanceIdListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.iid.InstanceID" />
</intent-filter>
</service>
<service
android:name=".gcm.GcmInstanceIdRefreshHandlerService"
android:exported="false" />
</application>
</manifest>
package com.wix.reactnativenotifications;
public interface Defs {
String LOGTAG = "ReactNativeNotifs";
String GCM_SENDER_ID_ATTR_NAME = "com.wix.reactnativenotifications.gcmSenderId";
String TOKEN_RECEIVED_EVENT_NAME = "remoteNotificationsRegistered";
String NOTIFICATION_RECEIVED_EVENT_NAME = "notificationReceived";
String NOTIFICATION_OPENED_EVENT_NAME = "notificationOpened";
}
package com.wix.reactnativenotifications;
import android.app.Application;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.wix.reactnativenotifications.core.RNNotificationsModule;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class RNNotificationsPackage implements ReactPackage {
final Application mApplication;
public RNNotificationsPackage(Application application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new RNNotificationsModule(mApplication, reactContext));
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
package com.wix.reactnativenotifications.core;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class AppLaunchHelper {
private static final String TAG = AppLaunchHelper.class.getSimpleName();
private static final String LAUNCH_FLAG_KEY_NAME = "launchedFromNotification";
public static 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.
// - If app is in background, bring it to foreground as-is (context stack untampered).
// A distinction is made in this case such that if app went to back due to *back-button*, is should be recreated (this
// is Android's native behavior).
// - If app is dead, launch it through the main context (as Android launchers do).
// Overall, THIS IS EXACTLY THE SAME AS ANDROID LAUNCHERS WORK. So, we use the same configuration (action, categories and
// flags) as they do.
final Intent helperIntent = appContext.getPackageManager().getLaunchIntentForPackage(appContext.getPackageName());
final Intent intent = new Intent(appContext, Class.forName(helperIntent.getComponent().getClassName()));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
intent.putExtra(LAUNCH_FLAG_KEY_NAME, true);
return intent;
} catch (ClassNotFoundException e) {
// Note: this is an imaginary scenario cause we're asking for a class of our very own package.
Log.e(TAG, "Failed to launch/resume app", e);
return null;
}
}
public static 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) {
return intent.getBooleanExtra(LAUNCH_FLAG_KEY_NAME, false);
}
}
package com.wix.reactnativenotifications.core;
public interface AppLifecycleFacade {
interface AppVisibilityListener {
void onAppVisible();
void onAppNotVisible();
}
boolean isReactInitialized();
boolean isAppVisible();
void addVisibilityListener(AppVisibilityListener listener);
void removeVisibilityListener(AppVisibilityListener listener);
}
package com.wix.reactnativenotifications.core;
import android.support.annotation.Nullable;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
public class InitialNotification {
private static PushNotificationProps sNotification;
public static void set(PushNotificationProps pushNotificationProps) {
sNotification = pushNotificationProps;
}
public static void clear() {
sNotification = null;
}
@Nullable
public static PushNotificationProps get() {
return sNotification;
}
}
package com.wix.reactnativenotifications.core;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
public class NotificationIntentAdapter {
private static final int PENDING_INTENT_CODE = 0;
private static final String PUSH_NOTIFICATION_EXTRA_NAME = "pushNotification";
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);
}
public static Bundle extractPendingNotificationDataFromIntent(Intent intent) {
return intent.getBundleExtra(PUSH_NOTIFICATION_EXTRA_NAME);
}
}
package com.wix.reactnativenotifications.core;
import android.app.IntentService;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notification.PushNotification;
public class ProxyService extends IntentService {
private static final String TAG = ProxyService.class.getSimpleName();
public ProxyService() {
super("notificationsProxyService");
}
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "New intent: "+intent);
final Bundle notificationData = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
final IPushNotification pushNotification = PushNotification.get(this, notificationData, ReactAppLifecycleFacade.get());
if (pushNotification != null) {
pushNotification.onOpened();
}
}
}
package com.wix.reactnativenotifications.core;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
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.wix.reactnativenotifications.core.notification.PushNotificationProps;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
import com.wix.reactnativenotifications.gcm.GcmInstanceIdRefreshHandlerService;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class RNNotificationsModule extends ReactContextBaseJavaModule implements AppLifecycleFacade.AppVisibilityListener, Application.ActivityLifecycleCallbacks {
public RNNotificationsModule(Application application, ReactApplicationContext reactContext) {
super(reactContext);
ReactAppLifecycleFacade.get().init(reactContext);
ReactAppLifecycleFacade.get().addVisibilityListener(this);
application.registerActivityLifecycleCallbacks(this);
}
@Override
public String getName() {
return "WixRNNotifications";
}
@Override
public void initialize() {
Log.d(LOGTAG, "Native module init");
startGcmIntentService(GcmInstanceIdRefreshHandlerService.EXTRA_IS_APP_INIT);
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
notificationsDrawer.onAppInit();
}
@ReactMethod
public void refreshToken() {
Log.d(LOGTAG, "Native method invocation: refreshToken()");
startGcmIntentService(GcmInstanceIdRefreshHandlerService.EXTRA_MANUAL_REFRESH);
}
@ReactMethod
public void getInitialNotification(final Promise promise) {
Log.d(LOGTAG, "Native method invocation: getInitialNotification");
Object result = null;
try {
final PushNotificationProps notification = InitialNotification.get();
if (notification == null) {
return;
}
result = Arguments.fromBundle(notification.asBundle());
} finally {
promise.resolve(result);
}
}
@Override
public void onAppVisible() {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
notificationsDrawer.onAppVisible();
}
@Override
public void onAppNotVisible() {
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
final IPushNotificationsDrawer notificationsDrawer = PushNotificationsDrawer.get(getReactApplicationContext().getApplicationContext());
notificationsDrawer.onNewActivity(activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
protected void startGcmIntentService(String extraFlag) {
final Context appContext = getReactApplicationContext().getApplicationContext();
final Intent tokenFetchIntent = new Intent(appContext, GcmInstanceIdRefreshHandlerService.class);
tokenFetchIntent.putExtra(extraFlag, true);
appContext.startService(tokenFetchIntent);
}
}
package com.wix.reactnativenotifications.core;
import android.util.Log;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactContext;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class ReactAppLifecycleFacade implements AppLifecycleFacade {
private static final ReactAppLifecycleFacade sInstance = new ReactAppLifecycleFacade();
private ReactContext mReactContext;
private boolean mIsVisible;
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
public static ReactAppLifecycleFacade get() {
return sInstance;
}
public void init(ReactContext reactContext) {
mReactContext = reactContext;
reactContext.addLifecycleEventListener(new LifecycleEventListener() {
@Override
public void onHostResume() {
Log.d(LOGTAG, "onHostResume");
switchToVisible();
}
@Override
public void onHostPause() {
Log.d(LOGTAG, "onHostPause");
switchToInvisible();
}
@Override
public void onHostDestroy() {
Log.d(LOGTAG, "onHostDestroy");
switchToInvisible();
}
});
}
@Override
public synchronized boolean isReactInitialized() {
if (mReactContext == null) {
return false;
}
return mReactContext.hasActiveCatalystInstance();
}
@Override
public boolean isAppVisible() {
return mIsVisible;
}
@Override
public void addVisibilityListener(AppVisibilityListener listener) {
mListeners.add(listener);
}
@Override
public void removeVisibilityListener(AppVisibilityListener listener) {
mListeners.remove(listener);
}
private synchronized void switchToVisible() {
if (!mIsVisible) {
Log.d(LOGTAG, "App is now visible");
mIsVisible = true;
for (AppVisibilityListener listener : mListeners) {
listener.onAppVisible();
}
}
}
private synchronized void switchToInvisible() {
if (mIsVisible) {
Log.d(LOGTAG, "App is now not visible");
mIsVisible = false;
for (AppVisibilityListener listener : mListeners) {
listener.onAppNotVisible();
}
}
}
}
package com.wix.reactnativenotifications.core.notification;
import android.content.Context;
import android.os.Bundle;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
public interface INotificationsApplication {
IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade facade);
}
package com.wix.reactnativenotifications.core.notification;
public interface IPushNotification {
class InvalidNotificationException extends Exception {
public InvalidNotificationException(String detailMessage) {
super(detailMessage);
}
}
/**
* Handle an event where notification has just been received.
* @throws InvalidNotificationException
*/
void onReceived() throws InvalidNotificationException;
/**
* Handle an event where notification has already been dispatched and is not being opened by the device user.
*/
void onOpened();
PushNotificationProps asProps();
}
package com.wix.reactnativenotifications.core.notification;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
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 static com.wix.reactnativenotifications.Defs.NOTIFICATION_OPENED_EVENT_NAME;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
// TODO:
// 1. Opening the from notif, then tapping 'home', the opening from launcher icon results in main activity over main activity
// 2. Double check initial notification set up and clearing
public class PushNotification implements IPushNotification {
final private Context mContext;
final private AppLifecycleFacade mAppLifecycleFacade;
final private PushNotificationProps mNotificationProps;
final private AppVisibilityListener mAppVisibilityListener = new AppVisibilityListener() {
@Override
public void onAppVisible() {
mAppLifecycleFacade.removeVisibilityListener(this);
dispatchImmediately();
}
@Override
public void onAppNotVisible() {}
};
protected PushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade) {
mContext = context;
mAppLifecycleFacade = appLifecycleFacade;
mNotificationProps = new PushNotificationProps(bundle);
}
public static IPushNotification get(Context context, Bundle bundle, AppLifecycleFacade facade) {
Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsApplication) {
return ((INotificationsApplication) appContext).getPushNotification(context, bundle, facade);
}
return new PushNotification(context, bundle, facade);
}
@Override
public void onReceived() throws InvalidNotificationException {
postNotification();
notifyReceivedToJS();
}
@Override
public void onOpened() {
digestNotification();
PushNotificationsDrawer.get(mContext).onNotificationOpened();
}
@Override
public PushNotificationProps asProps() {
return mNotificationProps.copy();
}
protected void postNotification() {
final PendingIntent pendingIntent = getCTAPendingIntent();
final Notification notification = buildNotification(pendingIntent);
postNotification((int) System.currentTimeMillis(), notification);
}
protected void digestNotification() {
if (!mAppLifecycleFacade.isReactInitialized()) {
setAsInitialNotification();
launchOrResumeApp();
return;
}
final ReactContext reactContext = getRunningReactContext();
if (reactContext.getCurrentActivity() == null) {
setAsInitialNotification();
}
if (mAppLifecycleFacade.isAppVisible()) {
dispatchImmediately();
} else {
dispatchUponVisibility();
}
}
protected void setAsInitialNotification() {
InitialNotification.set(mNotificationProps);
}
protected void dispatchImmediately() {
notifyOpenedToJS();
}
protected void dispatchUponVisibility() {
mAppLifecycleFacade.addVisibilityListener(getIntermediateAppVisibilityListener());
// Make the app visible so that we'll dispatch the notification opening when visibility changes to 'true' (see
// above listener registration).
launchOrResumeApp();
}
protected AppVisibilityListener getIntermediateAppVisibilityListener() {
return mAppVisibilityListener;
}
protected PendingIntent getCTAPendingIntent() {
final Intent cta = new Intent(mContext, ProxyService.class);
return NotificationIntentAdapter.createPendingNotificationIntent(mContext, cta, mNotificationProps);
}
protected Notification buildNotification(PendingIntent intent) {
final Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setContentTitle(mNotificationProps.getTitle())
.setContentText(mNotificationProps.getBody())
.setSmallIcon(mContext.getApplicationInfo().icon)
.setContentIntent(intent)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
return notificationBuilder.build();
}
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;
}
private void notifyReceivedToJS() {
notifyJS(NOTIFICATION_RECEIVED_EVENT_NAME, null);
}
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);
}
}
protected void launchOrResumeApp() {
final Intent intent = AppLaunchHelper.getLaunchIntent(mContext);
mContext.startActivity(intent);
}
}
package com.wix.reactnativenotifications.core.notification;
import android.os.Bundle;
/**
* @author amitd
*/
public class PushNotificationProps {
private Bundle mBundle;
public PushNotificationProps(Bundle bundle) {
final String title = bundle.getString("title");
final String body = bundle.getString("body");
if (title == null || title.trim().isEmpty() || body == null || body.trim().isEmpty()) {
throw new IllegalArgumentException("Invalid notification");
}
mBundle = bundle;
}
public String getTitle() {
return mBundle.getString("title");
}
public String getBody() {
return mBundle.getString("body");
}
public Bundle asBundle() {
return (Bundle) mBundle.clone();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(1024);
for (String key : mBundle.keySet()) {
sb.append(key).append("=").append(mBundle.get(key)).append(", ");
}
return sb.toString();
}
protected PushNotificationProps copy() {
return new PushNotificationProps((Bundle) mBundle.clone());
}
}
package com.wix.reactnativenotifications.core.notificationdrawer;
public interface INotificationsDrawerApplication {
IPushNotificationsDrawer getPushNotificationsDrawer();
}
package com.wix.reactnativenotifications.core.notificationdrawer;
import android.app.Activity;
public interface IPushNotificationsDrawer {
void onAppInit();
void onAppVisible();
void onNewActivity(Activity activity);
void onNotificationOpened();
}
package com.wix.reactnativenotifications.core.notificationdrawer;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.InitialNotification;
public class PushNotificationsDrawer implements IPushNotificationsDrawer {
protected final Context mContext;
public PushNotificationsDrawer(Context context) {
mContext = context;
}
public static IPushNotificationsDrawer get(Context context) {
final Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsDrawerApplication) {
return ((INotificationsDrawerApplication) appContext).getPushNotificationsDrawer();
}
return new PushNotificationsDrawer(context);
}
@Override
public void onAppInit() {
clearAll();
}
@Override
public void onAppVisible() {
clearAll();
}
@Override
public void onNewActivity(Activity activity) {
if (AppLaunchHelper.isLaunchIntentsActivity(activity) &&
!AppLaunchHelper.isLaunchIntent(activity.getIntent())) {
InitialNotification.clear();
}
}
@Override
public void onNotificationOpened() {
clearAll();
}
protected void clearAll() {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}
}
package com.wix.reactnativenotifications.gcm;
import android.content.Intent;
import com.google.android.gms.iid.InstanceIDListenerService;
/**
* Instance-ID + token refreshing handling service. Contacts the GCM to fetch the updated token.
*
* @author amitd
*/
public class GcmInstanceIdListenerService extends InstanceIDListenerService {
@Override
public void onTokenRefresh() {
// Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
// Google recommends running this from an intent service.
Intent intent = new Intent(this, GcmInstanceIdRefreshHandlerService.class);
startService(intent);
}
}
package com.wix.reactnativenotifications.gcm;
import android.app.IntentService;
import android.content.Intent;
public class GcmInstanceIdRefreshHandlerService extends IntentService {
public static String EXTRA_IS_APP_INIT = "isAppInit";
public static String EXTRA_MANUAL_REFRESH = "doManualRefresh";
public GcmInstanceIdRefreshHandlerService() {
super(GcmInstanceIdRefreshHandlerService.class.getSimpleName());
}
@Override
protected void onHandleIntent(Intent intent) {
IGcmToken gcmToken = GcmToken.get(this);
if (gcmToken == null) {
return;
}
if (intent.getBooleanExtra(EXTRA_IS_APP_INIT, false)) {
gcmToken.onAppReady();
} else if (intent.getBooleanExtra(EXTRA_MANUAL_REFRESH, false)) {
gcmToken.onManualRefresh();
} else {
gcmToken.onNewTokenReady();
}
}
}
package com.wix.reactnativenotifications.gcm;
import android.os.Bundle;
import android.util.Log;
import com.google.android.gms.gcm.GcmListenerService;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.ReactAppLifecycleFacade;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class GcmMessageHandlerService extends GcmListenerService {
@Override
public void onMessageReceived(String s, Bundle bundle) {
Log.d(LOGTAG, "New message from GCM: " + bundle);
try {
final IPushNotification notification = PushNotification.get(getApplicationContext(), bundle, ReactAppLifecycleFacade.get());
notification.onReceived();
} catch (IPushNotification.InvalidNotificationException e) {
// A GCM message, yes - but not the kind we know how to work with.
Log.v(LOGTAG, "GCM message handling aborted", e);
}
}
}
package com.wix.reactnativenotifications.gcm;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.util.Log;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.android.gms.iid.InstanceID;
import static com.wix.reactnativenotifications.Defs.GCM_SENDER_ID_ATTR_NAME;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
import static com.wix.reactnativenotifications.Defs.TOKEN_RECEIVED_EVENT_NAME;
public class GcmToken implements IGcmToken {
final protected Context mAppContext;
protected static String sToken;
protected GcmToken(Context appContext) {
if (!(appContext instanceof ReactApplication)) {
throw new IllegalStateException("Application instance isn't a react-application");
}
mAppContext = appContext;
}
public static IGcmToken get(Context context) {
Context appContext = context.getApplicationContext();
if (appContext instanceof INotificationsGcmApplication) {
return ((INotificationsGcmApplication) appContext).getGcmToken(context);
}
return new GcmToken(appContext);
}
@Override
public void onNewTokenReady() {
synchronized (mAppContext) {
refreshToken();
}
}
@Override
public void onManualRefresh() {
synchronized (mAppContext) {
if (sToken == null) {
Log.i(LOGTAG, "Manual token refresh => asking for new token");
refreshToken();
} else {
Log.i(LOGTAG, "Manual token refresh => publishing existing token ("+sToken+")");
sendTokenToJS();
}
}
}
@Override
public void onAppReady() {
synchronized (mAppContext) {
if (sToken == null) {
Log.i(LOGTAG, "App initialized => asking for new token");
refreshToken();
} else {
// Except for first run, this should be the case.
Log.i(LOGTAG, "App initialized => publishing existing token ("+sToken+")");
sendTokenToJS();
}
}
}
protected void refreshToken() {
try {
sToken = getNewToken();
} catch (Exception e) {
Log.e(LOGTAG, "Failed to retrieve new token", e);
return;
}
sendTokenToJS();
}
@NonNull
protected String getNewToken() throws Exception {
final InstanceID instanceId = InstanceID.getInstance(mAppContext);
Log.d(LOGTAG, "GCM is refreshing token... instanceId=" + instanceId.getId());
// TODO why is this needed?
GoogleCloudMessaging.getInstance(mAppContext).close();
try {
final String registrationToken = instanceId.getToken(getSenderId(), GoogleCloudMessaging.INSTANCE_ID_SCOPE);
Log.i(LOGTAG, "GCM has a new token: instanceId=" + instanceId.getId() + ", token=" + registrationToken);
return registrationToken;
} catch (Exception e) {
throw new Exception("FATAL: Failed to fetch a fresh new token, instanceId=" + instanceId.getId(), e);
}
}
protected String getSenderId() {
final String senderId = getSenderIdFromManifest();
if (senderId == null) {
throw new IllegalStateException("Sender ID not found in manifest. Did you forget to add it as the value of a '"+GCM_SENDER_ID_ATTR_NAME+"' meta-data field?");
}
return senderId;
}
protected String getSenderIdFromManifest() {
final ApplicationInfo appInfo;
try {
appInfo = mAppContext.getPackageManager().getApplicationInfo(mAppContext.getPackageName(), PackageManager.GET_META_DATA);
return appInfo.metaData.getString(GCM_SENDER_ID_ATTR_NAME);
} catch (PackageManager.NameNotFoundException e) {
// Should REALLY never happen cause we're querying for our own package.
Log.e(LOGTAG, "Failed to resolve sender ID from manifest", e);
return null;
}
}
protected void sendTokenToJS() {
final ReactInstanceManager instanceManager = ((ReactApplication) mAppContext).getReactNativeHost().getReactInstanceManager();
final ReactContext reactContext = instanceManager.getCurrentReactContext();
// Note: Cannot assume react-context exists cause this is an async dispatched service.
if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(TOKEN_RECEIVED_EVENT_NAME, sToken);
}
}
}
package com.wix.reactnativenotifications.gcm;
public interface IGcmToken {
/**
* Handle an event where we've been notified of a that a fresh token is now available from Google.
*/
void onNewTokenReady();
/**
* Handle an event where application is ready; typically used for sending token to JS.
*/
void onAppReady();
/**
* Handle a request to actively refresh the token on demand.
* This is in essence a workaround so as to allow apps to handle end-cases of token refreshing. It
* shouldn't be used by standard apps, as the token management is self sufficient.
*/
void onManualRefresh();
}
package com.wix.reactnativenotifications.gcm;
import android.content.Context;
public interface INotificationsGcmApplication {
IGcmToken getGcmToken(Context context);
}
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
import re
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
android_library(
name = 'all-libs',
exported_deps = lib_deps
)
android_library(
name = 'app-code',
srcs = glob([
'src/main/java/**/*.java',
]),
deps = [
':all-libs',
':build_config',
':res',
],
)
android_build_config(
name = 'build_config',
package = 'com.notificationsexampleapp',
)
android_resource(
name = 'res',
res = 'src/main/res',
package = 'com.notificationsexampleapp',
)
android_binary(
name = 'app',
package_type = 'debug',
manifest = 'src/main/AndroidManifest.xml',
keystore = '//android/keystores:debug',
deps = [
':app-code',
],
)
apply plugin: "com.android.application"
import com.android.build.OutputFile
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation
* entryFile: "index.android.js",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"]
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
apply from: "../../node_modules/react-native/react.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "com.notificationsexampleapp"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
}
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
}
dependencies {
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
@com.facebook.common.internal.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
# okhttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
import org.apache.tools.ant.taskdefs.condition.Os
def config = project.hasProperty("react") ? project.react : [];
def bundleAssetName = config.bundleAssetName ?: "index.android.bundle"
def entryFile = config.entryFile ?: "index.android.js"
// because elvis operator
def elvisFile(thing) {
return thing ? file(thing) : null;
}
def reactRoot = elvisFile(config.root) ?: file("../../")
def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"]
void runBefore(String dependentTaskName, Task task) {
Task dependentTask = tasks.findByPath(dependentTaskName);
if (dependentTask != null) {
dependentTask.dependsOn task
}
}
gradle.projectsEvaluated {
// Grab all build types and product flavors
def buildTypes = android.buildTypes.collect { type -> type.name }
def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
// When no product flavors defined, use empty
if (!productFlavors) productFlavors.add('')
productFlavors.each { productFlavorName ->
buildTypes.each { buildTypeName ->
// Create variant and target names
def targetName = "${productFlavorName.capitalize()}${buildTypeName.capitalize()}"
def targetPath = productFlavorName ?
"${productFlavorName}/${buildTypeName}" :
"${buildTypeName}"
// React js bundle directories
def jsBundleDirConfigName = "jsBundleDir${targetName}"
def jsBundleDir = elvisFile(config."$jsBundleDirConfigName") ?:
file("$buildDir/intermediates/assets/${targetPath}")
def resourcesDirConfigName = "resourcesDir${targetName}"
def resourcesDir = elvisFile(config."${resourcesDirConfigName}") ?:
file("$buildDir/intermediates/res/merged/${targetPath}")
def jsBundleFile = file("$jsBundleDir/$bundleAssetName")
// Bundle task name for variant
def bundleJsAndAssetsTaskName = "bundle${targetName}JsAndAssets"
def currentBundleTask = tasks.create(
name: bundleJsAndAssetsTaskName,
type: Exec) {
group = "react"
description = "bundle JS and assets for ${targetName}."
// Create dirs if they are not there (e.g. the "clean" task just ran)
doFirst {
jsBundleDir.mkdirs()
resourcesDir.mkdirs()
}
// Set up inputs and outputs so gradle can cache the result
inputs.files fileTree(dir: reactRoot, excludes: inputExcludes)
outputs.dir jsBundleDir
outputs.dir resourcesDir
// Set up the call to the react-native cli
workingDir reactRoot
// Set up dev mode
def devEnabled = !targetName.toLowerCase().contains("release")
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine "cmd", "/c", "react-native", "bundle", "--platform", "android", "--dev", "${devEnabled}",
"--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir
} else {
commandLine "react-native", "bundle", "--platform", "android", "--dev", "${devEnabled}",
"--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir
}
enabled config."bundleIn${targetName}" ||
config."bundleIn${buildTypeName.capitalize()}" ?:
targetName.toLowerCase().contains("release")
}
// Hook bundle${productFlavor}${buildType}JsAndAssets into the android build process
currentBundleTask.dependsOn("merge${targetName}Resources")
currentBundleTask.dependsOn("merge${targetName}Assets")
runBefore("processArmeabi-v7a${targetName}Resources", currentBundleTask)
runBefore("processX86${targetName}Resources", currentBundleTask)
runBefore("processUniversal${targetName}Resources", currentBundleTask)
runBefore("process${targetName}Resources", currentBundleTask)
}
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smartnotificationsapp">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
package com.notificationsexampleapp;
import com.facebook.react.ReactActivity;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "NotificationsExampleApp";
}
}
package com.smartnotificationsapp;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "NotificationsExampleApp";
}
/**
* Returns whether dev mode should be enabled.
* This enables e.g. the dev menu.
*/
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
/**
* A list of packages used by the app. If the app uses additional views
* or modules besides the default ones, add more packages here.
*/
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
}
<resources>
<string name="app_name">NotificationsExampleApp</string>
</resources>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
......@@ -5,20 +5,21 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.android.tools.build:gradle:2.2.0'
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
......@@ -16,5 +16,3 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
\ No newline at end of file
android.useDeprecatedNdk=true
#Sun Oct 09 14:30:04 IDT 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
......@@ -42,11 +42,6 @@ case "`uname`" in
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
......@@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
......@@ -114,6 +109,7 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
......
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "24.0.1"
defaultConfig {
applicationId "com.wix.reactnativenotifications.app"
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
// packagingOptions {
// exclude "lib/arm64-v8a/librealm-jni.so"
// }
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.facebook.react:react-native:+'
compile project(':reactnativenotifications')
testCompile 'junit:junit:4.12'
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/amitd/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wix.reactnativenotifications.app">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<meta-data
android:name="com.wix.reactnativenotifications.gcmSenderId"
android:value="434691868895\0"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</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>
</manifest>
\ No newline at end of file
package com.wix.reactnativenotifications.app;
import android.annotation.TargetApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.ViewGroup;
import android.widget.Toolbar;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactRootView;
import static android.os.Build.VERSION.SDK_INT;
public class MainActivity extends ReactActivity {
private static final int OVERLAY_PERMISSION_REQ_CODE = 1234;
private ReactRootView mReactRootView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ViewGroup layout = (ViewGroup) getLayoutInflater().inflate(R.layout.activity_main, null);
if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Toolbar toolbar = (Toolbar) layout.findViewById(R.id.toolbar);
setActionBar(toolbar);
}
mReactRootView = new ReactRootView(this);
layout.addView(mReactRootView);
setContentView(layout);
if (SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
} else {
startReactApplication();
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
if (Settings.canDrawOverlays(this)) {
startReactApplication();
} else {
finish();
}
}
}
private void startReactApplication() {
mReactRootView.startReactApplication(getReactInstanceManager(), "WixRNNotifications", null);
}
}
package com.notificationsexampleapp;
package com.wix.reactnativenotifications.app;
import android.app.Application;
import android.util.Log;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import java.util.Arrays;
import java.util.List;
......@@ -22,8 +21,9 @@ public class MainApplication extends Application implements ReactApplication {
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
return Arrays.asList(
new MainReactPackage(),
new RNNotificationsPackage(MainApplication.this)
);
}
};
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.wix.reactnativenotifications.app.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
<resources>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
<resources>
<string name="app_name">Wix RN Notifications</string>
</resources>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
</resources>
from urllib2 import *
import json
import sys
if len(sys.argv) < 2:
print 'Error: missing token argument'
sys.exit(1)
API_KEY = 'AIzaSyBVtqdO_SgPVhhXnyNGC_VXSbIX-fxk1YY'
TOKEN = sys.argv[1]
data = {
"to": TOKEN,
"data" : {
"body": "SUCCESS! Sent from script :)",
"title": "Wix Example Project"
}
}
dataJson = json.dumps(data)
request = Request(
'https://gcm-http.googleapis.com/gcm/send',
dataJson,
{
"Authorization" : "key="+API_KEY,
"Content-type" : "application/json"
}
)
print "Sending notification..."
print urlopen(request).read()
rootProject.name = 'NotificationsExampleApp'
include ':myapplication'
include ':app'
include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
/**
* Sample React Native App
* https://github.com/facebook/react-native
*/
'use strict';
import React, {Component} from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import React, {Component} from 'react';
class NotificationsExampleApp extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native Notifications Demo App!
</Text>
<Text style={styles.instructions}>
To get started, edit index.android.js
</Text>
<Text style={styles.instructions}>
Shake or press menu button for dev menu
</Text>
</View>
);
import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications';
let mainScreen;
function onPushRegistered() {
if (mainScreen) {
mainScreen.onPushRegistered();
}
}
function onNotificationOpened(notification) {
if (mainScreen) {
mainScreen.onNotificationOpened(notification)
}
}
function onNotificationReceived(notification) {
if (mainScreen) {
mainScreen.onNotificationReceived(notification)
}
}
// It's highly recommended to keep listeners registration at global scope rather than at screen-scope seeing that
// component mount and unmount lifecycle tend to be asymmetric!
NotificationsAndroid.setRegistrationTokenUpdateListener(onPushRegistered);
NotificationsAndroid.setNotificationOpenedListener(onNotificationOpened);
NotificationsAndroid.setNotificationReceivedListener(onNotificationReceived);
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
titleText: {
fontSize: 22,
textAlign: 'center',
margin: 10,
},
instructions: {
bodyText: {
fontSize: 18,
textAlign: 'center',
color: '#333333',
marginBottom: 5,
margin: 10,
},
});
AppRegistry.registerComponent('NotificationsExampleApp', () => NotificationsExampleApp);
class MainComponent extends Component {
constructor(props) {
super(props);
this.state = {
elapsed: 0,
lastNotification: undefined
};
console.log('ReactScreen', 'ReactScreen');
mainScreen = this;
setInterval(this.onTick.bind(this), 1000);
}
componentDidMount() {
console.log('ReactScreen', 'componentDidMount');
PendingNotifications.getInitialNotification()
.then((notification) => {console.log("getInitialNotification:", notification); this.setState({initialNotification: notification.getData()});})
.catch((err) => console.error("getInitialNotifiation failed", err));
}
componentWillUnmount() {
console.log('ReactScreen', 'componentWillUnmount');
}
onTick() {
this.setState({elapsed: this.state.elapsed + 1});
}
render() {
return (
<View style={styles.container}>
<Text style={styles.titleText}>Wix React Native Notifications</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}>Time elapsed: {this.state.elapsed}</Text>
</View>
)
}
onPushRegistered() {
}
onNotificationOpened(notification) {
console.log("onNotificationOpened: ", notification);
this.setState({lastNotification: notification.getData(), notificationRxTime: this.state.elapsed});
}
onNotificationReceived(notification) {
console.log("onNotificationReceived: ", notification);
}
}
AppRegistry.registerComponent('WixRNNotifications', () => MainComponent);
......@@ -7,7 +7,7 @@
},
"dependencies": {
"babel-preset-react-native-stage-0": "^1.0.1",
"react": "^15.3.1",
"react": "15.3.1",
"react-native": "0.34.0",
"react-native-notifications": "../"
},
......
import {NativeModules, DeviceEventEmitter} from 'react-native';
import NotificationAndroid from './notification';
const RNNotifications = NativeModules.WixRNNotifications;
let notificationReceivedListener;
let notificationOpenedListener;
let registrationTokenUpdateListener;
export class NotificationsAndroid {
static setRegistrationTokenUpdateListener(listener) {
registrationTokenUpdateListener = DeviceEventEmitter.addListener('remoteNotificationsRegistered', listener);
}
static clearRegistrationTokenUpdateListener() {
if (registrationTokenUpdateListener) {
registrationTokenUpdateListener.remove();
registrationTokenUpdateListener = null;
}
}
static setNotificationOpenedListener(listener) {
notificationOpenedListener = DeviceEventEmitter.addListener('notificationOpened', (notification) => listener(new NotificationAndroid(notification)));
}
static setNotificationReceivedListener(listener) {
notificationReceivedListener = DeviceEventEmitter.addListener('notificationReceived', (notification) => listener(new NotificationAndroid(notification)));
}
static clearNotificationOpenedListener() {
if (notificationOpenedListener) {
notificationOpenedListener.remove();
notificationOpenedListener = null;
}
}
static clearNotificationReceivedListener() {
if (notificationReceivedListener) {
notificationReceivedListener.remove();
notificationReceivedListener = null;
}
}
}
export class PendingNotifications {
static async getInitialNotification() {
return new NotificationAndroid(await RNNotifications.getInitialNotification());
}
}
/** A wrapper to align Android with iOS in terms on notification structure. */
export default class NotificationAndroid {
constructor(notification) {
this.data = notification;
}
getData() {
return this.data;
}
getTitle() {
return this.data.title;
}
getMessage() {
return this.data.body;
}
}
......@@ -25,7 +25,8 @@
"uuid": "^2.0.3"
},
"peerDependencies": {
"react-native": ">=0.25.1"
"react-native": ">=0.25.1",
"react": "^0.14.5"
},
"devDependencies": {
"babel-eslint": "^6.0.2",
......@@ -55,7 +56,6 @@
"bugs": {
"url": "https://github.com/wix/react-native-notifications/issues"
},
"main": "index.ios.js",
"babel": {
"presets": [
"react-native"
......
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