Commit d5682e8b authored by Amit Davidi's avatar Amit Davidi

Extensibility refactor step 2

parent 00c8a581
...@@ -14,11 +14,9 @@ ...@@ -14,11 +14,9 @@
<application> <application>
<!-- <!--
A proxy-activity that either launches the main activity or resumes the currently running app. A proxy-service that gives the library an opportunity to do some work before launching/resuming the actual application task.
Required in order to keep the initial intent so it could be retrieved by the JS client.
Note: we declare it in its own private affinity so that a running app stack wouldn't shadow its creation.
--> -->
<activity android:name=".core.ProxyActivity" android:taskAffinity="com.wix.reactnativenotifications.core.ProxyActivity"/> <service android:name=".core.ProxyService"/>
<!-- <!--
Google's ready-to-use GcmReceiver. Google's ready-to-use GcmReceiver.
......
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());
return activity.getLocalClassName().equals(helperIntent.getComponent().getClassName());
}
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.content.Context;
import android.os.Bundle;
public interface INotificationsApplication {
IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade facade);
}
package com.wix.reactnativenotifications.core;
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();
}
...@@ -11,7 +11,7 @@ public class NotificationIntentAdapter { ...@@ -11,7 +11,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.getActivity(appContext, PENDING_INTENT_CODE, intent, 0); return PendingIntent.getService(appContext, PENDING_INTENT_CODE, intent, 0);
} }
public static Bundle extractPendingNotificationDataFromIntent(Intent intent) { public static Bundle extractPendingNotificationDataFromIntent(Intent intent) {
......
package com.wix.reactnativenotifications.core;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
/**
* The front-end (hidden) activity for handling notification <b>opening</b> actions triggered by the
* device user (i.e. upon tapping on them in the notifications drawer).
*/
public class ProxyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(LOGTAG, "ProxyActivity.onCreate, " + getIntent().getExtras());
final Bundle rawNotificationData = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(getIntent());
if (rawNotificationData != null) {
new PushNotification(this, rawNotificationData).onOpened(this);
}
finish();
}
}
package com.wix.reactnativenotifications.core;
import android.app.IntentService;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
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; package com.wix.reactnativenotifications.core;
import android.app.Activity;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactInstanceManager;
...@@ -17,36 +14,54 @@ import com.facebook.react.bridge.Arguments; ...@@ -17,36 +14,54 @@ 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.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.wix.reactnativenotifications.core.AppLifecycleFacade.AppVisibilityListener;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
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;
public class PushNotification { public class PushNotification implements IPushNotification {
public static class InvalidNotificationException extends Exception { final private Context mContext;
public InvalidNotificationException(String detailMessage) { final private AppLifecycleFacade mAppLifecycleFacade;
super(detailMessage); final private PushNotificationProps mNotificationProps;
final private AppVisibilityListener mAppVisibilityListener = new AppVisibilityListener() {
@Override
public void onAppVisible() {
mAppLifecycleFacade.removeVisibilityListener(this);
dispatchImmediately();
} }
}
private final Context mAppContext; @Override
private PushNotificationProps mNotificationProps; public void onAppNotVisible() {}
};
public PushNotification(Context context, Bundle bundle) { protected PushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade) {
mAppContext = context.getApplicationContext(); mContext = context;
mAppLifecycleFacade = appLifecycleFacade;
mNotificationProps = new PushNotificationProps(bundle); 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 { public void onReceived() throws InvalidNotificationException {
postNotification(); postNotification();
notifyReceivedToJS(); notifyReceivedToJS();
} }
public void onOpened(Activity hostActivity) { @Override
digestNotification(hostActivity); public void onOpened() {
digestNotification();
deleteAllPostedNotifications();
} }
@Override
public PushNotificationProps asProps() { public PushNotificationProps asProps() {
return mNotificationProps.copy(); return mNotificationProps.copy();
} }
...@@ -57,28 +72,50 @@ public class PushNotification { ...@@ -57,28 +72,50 @@ public class PushNotification {
postNotification((int) System.currentTimeMillis(), notification); postNotification((int) System.currentTimeMillis(), notification);
} }
protected void digestNotification(Activity activity) { protected void digestNotification() {
final ReactContext reactContext = getRunningReactContext(); if (!mAppLifecycleFacade.isReactInitialized()) {
if (reactContext != null) { setAsInitialNotification();
launchOrResumeApp(activity); launchOrResumeApp();
notifyOpenedToJS(reactContext); return;
}
if (mAppLifecycleFacade.isAppVisible()) {
dispatchImmediately();
} else { } else {
InitialNotification.set(mNotificationProps); dispatchUponVisibility();
launchOrResumeApp(activity);
} }
} }
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() { protected PendingIntent getCTAPendingIntent() {
// Note: we launch the proxy activity, assuming it'll take it from there. final Intent cta = new Intent(mContext, ProxyService.class);
final Intent cta = new Intent(mAppContext, ProxyActivity.class); return NotificationIntentAdapter.createPendingNotificationIntent(mContext, cta, mNotificationProps);
return NotificationIntentAdapter.createPendingNotificationIntent(mAppContext, cta, mNotificationProps);
} }
protected Notification buildNotification(PendingIntent intent) { protected Notification buildNotification(PendingIntent intent) {
final Notification.Builder notificationBuilder = new Notification.Builder(mAppContext) final Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setContentTitle(mNotificationProps.getTitle()) .setContentTitle(mNotificationProps.getTitle())
.setContentText(mNotificationProps.getBody()) .setContentText(mNotificationProps.getBody())
.setSmallIcon(mAppContext.getApplicationInfo().icon) .setSmallIcon(mContext.getApplicationInfo().icon)
.setContentIntent(intent) .setContentIntent(intent)
.setDefaults(Notification.DEFAULT_ALL) .setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true); .setAutoCancel(true);
...@@ -87,17 +124,17 @@ public class PushNotification { ...@@ -87,17 +124,17 @@ public class PushNotification {
} }
protected void postNotification(int id, Notification notification) { protected void postNotification(int id, Notification notification) {
final NotificationManager notificationManager = (NotificationManager) mAppContext.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 ReactContext getRunningReactContext() {
final ReactNativeHost rnHost = ((ReactApplication) mAppContext).getReactNativeHost(); final ReactNativeHost rnHost = ((ReactApplication) mContext.getApplicationContext()).getReactNativeHost();
if (!rnHost.hasInstance()) { if (!rnHost.hasInstance()) {
return null; return null;
} }
final ReactInstanceManager instanceManager = ((ReactApplication) mAppContext).getReactNativeHost().getReactInstanceManager(); final ReactInstanceManager instanceManager = rnHost.getReactInstanceManager();
final ReactContext reactContext = instanceManager.getCurrentReactContext(); final ReactContext reactContext = instanceManager.getCurrentReactContext();
if (reactContext == null || !reactContext.hasActiveCatalystInstance()) { if (reactContext == null || !reactContext.hasActiveCatalystInstance()) {
return null; return null;
...@@ -129,22 +166,13 @@ public class PushNotification { ...@@ -129,22 +166,13 @@ public class PushNotification {
} }
} }
protected void launchOrResumeApp(Activity activity) { protected void launchOrResumeApp() {
// The desired behavior upon notification opening is as follows: final Intent intent = AppLaunchHelper.getLaunchIntent(mContext);
// - If app is in foreground (and possibly has several activities in stack), simply keep it as-is in foreground. mContext.startActivity(intent);
// - If app is in background, bring it to foreground as-is (activity stack untampered).
// A distinction is made in this case such that if app went to back due to *back-button*, is should be recreated.
// - If app is dead, launch it through the main activity (as Android launchers do).
// 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 = mAppContext.getPackageManager().getLaunchIntentForPackage(mAppContext.getPackageName());
try {
final Intent intent = Intent.makeMainActivity(new ComponentName(mAppContext, Class.forName(helperIntent.getComponent().getClassName())));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
activity.startActivity(intent);
} catch (ClassNotFoundException e) {
Log.e(LOGTAG, "Failed to launch/resume app", e);
}
} }
protected void deleteAllPostedNotifications() {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}
} }
package com.wix.reactnativenotifications.core; package com.wix.reactnativenotifications.core;
import android.content.Context;
import android.content.Intent;
import android.util.Log; import android.util.Log;
import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Arguments;
...@@ -7,8 +9,7 @@ import com.facebook.react.bridge.Promise; ...@@ -7,8 +9,7 @@ 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.wix.reactnativenotifications.gcm.GcmToken; import com.wix.reactnativenotifications.gcm.GcmInstanceIdRefreshHandlerService;
import com.wix.reactnativenotifications.gcm.IGcmToken;
import static com.wix.reactnativenotifications.Defs.LOGTAG; import static com.wix.reactnativenotifications.Defs.LOGTAG;
...@@ -16,6 +17,8 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule { ...@@ -16,6 +17,8 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule {
public RNNotificationsModule(ReactApplicationContext reactContext) { public RNNotificationsModule(ReactApplicationContext reactContext) {
super(reactContext); super(reactContext);
ReactAppLifecycleFacade.get().onAppInit(reactContext);
} }
@Override @Override
...@@ -26,12 +29,18 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule { ...@@ -26,12 +29,18 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule {
@Override @Override
public void initialize() { public void initialize() {
Log.d(LOGTAG, "Native module init"); Log.d(LOGTAG, "Native module init");
IGcmToken gcmToken = GcmToken.get(getReactApplicationContext().getApplicationContext());
gcmToken.onAppReady(); final Context appContext = getReactApplicationContext().getApplicationContext();
final Intent tokenFetchIntent = new Intent(appContext, GcmInstanceIdRefreshHandlerService.class);
tokenFetchIntent.putExtra(GcmInstanceIdRefreshHandlerService.EXTRA_IS_APP_INIT, true);
appContext.startService(tokenFetchIntent);
} }
@ReactMethod @ReactMethod
public void getInitialNotification(final Promise promise) { public void getInitialNotification(final Promise promise) {
Log.d(LOGTAG, "Native method invocation: getInitialNotification");
Object result = null; Object result = null;
try { try {
...@@ -45,4 +54,14 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule { ...@@ -45,4 +54,14 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule {
promise.resolve(result); promise.resolve(result);
} }
} }
@ReactMethod
public void refreshToken() {
Log.d(LOGTAG, "Native method invocation: refreshToken()");
final Context appContext = getReactApplicationContext().getApplicationContext();
final Intent tokenFetchIntent = new Intent(appContext, GcmInstanceIdRefreshHandlerService.class);
tokenFetchIntent.putExtra(GcmInstanceIdRefreshHandlerService.EXTRA_MANUAL_REFRESH, 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 ReactAppLifecycleFacade sInstance = new ReactAppLifecycleFacade();
private ReactContext mReactContext;
private boolean mIsVisible;
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
public static ReactAppLifecycleFacade get() {
return sInstance;
}
public synchronized void onAppInit(ReactContext reactContext) {
mReactContext = reactContext;
mIsVisible = false;
reactContext.addLifecycleEventListener(new LifecycleEventListener() {
@Override
public void onHostResume() {
Log.d(LOGTAG, "onHostResume");
switchToVisible();
}
@Override
public void onHostPause() {
switchToInvisible();
}
@Override
public void onHostDestroy() {
switchToInvisible();
Log.d(LOGTAG, "onHostDestroy");
mReactContext.removeLifecycleEventListener(this);
mReactContext = null;
}
});
}
@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();
}
}
}
}
...@@ -5,6 +5,9 @@ import android.content.Intent; ...@@ -5,6 +5,9 @@ import android.content.Intent;
public class GcmInstanceIdRefreshHandlerService extends IntentService { public class GcmInstanceIdRefreshHandlerService extends IntentService {
public static String EXTRA_IS_APP_INIT = "isAppInit";
public static String EXTRA_MANUAL_REFRESH = "doManualRefresh";
public GcmInstanceIdRefreshHandlerService() { public GcmInstanceIdRefreshHandlerService() {
super(GcmInstanceIdRefreshHandlerService.class.getSimpleName()); super(GcmInstanceIdRefreshHandlerService.class.getSimpleName());
} }
...@@ -12,7 +15,15 @@ public class GcmInstanceIdRefreshHandlerService extends IntentService { ...@@ -12,7 +15,15 @@ public class GcmInstanceIdRefreshHandlerService extends IntentService {
@Override @Override
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
IGcmToken gcmToken = GcmToken.get(this); IGcmToken gcmToken = GcmToken.get(this);
if (gcmToken != null) { 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(); gcmToken.onNewTokenReady();
} }
} }
......
...@@ -4,7 +4,9 @@ import android.os.Bundle; ...@@ -4,7 +4,9 @@ import android.os.Bundle;
import android.util.Log; import android.util.Log;
import com.google.android.gms.gcm.GcmListenerService; import com.google.android.gms.gcm.GcmListenerService;
import com.wix.reactnativenotifications.core.IPushNotification;
import com.wix.reactnativenotifications.core.PushNotification; import com.wix.reactnativenotifications.core.PushNotification;
import com.wix.reactnativenotifications.core.ReactAppLifecycleFacade;
import static com.wix.reactnativenotifications.Defs.LOGTAG; import static com.wix.reactnativenotifications.Defs.LOGTAG;
...@@ -15,9 +17,9 @@ public class GcmMessageHandlerService extends GcmListenerService { ...@@ -15,9 +17,9 @@ public class GcmMessageHandlerService extends GcmListenerService {
Log.d(LOGTAG, "New message from GCM: " + bundle); Log.d(LOGTAG, "New message from GCM: " + bundle);
try { try {
final PushNotification notification = new PushNotification(getApplicationContext(), bundle); final IPushNotification notification = PushNotification.get(getApplicationContext(), bundle, ReactAppLifecycleFacade.get());
notification.onReceived(); notification.onReceived();
} catch (PushNotification.InvalidNotificationException e) { } catch (IPushNotification.InvalidNotificationException e) {
// A GCM message, yes - but not the kind we know how to work with. // A GCM message, yes - but not the kind we know how to work with.
Log.v(LOGTAG, "GCM message handling aborted", e); Log.v(LOGTAG, "GCM message handling aborted", e);
} }
......
...@@ -23,7 +23,7 @@ public class GcmToken implements IGcmToken { ...@@ -23,7 +23,7 @@ public class GcmToken implements IGcmToken {
protected static String sToken; protected static String sToken;
public GcmToken(Context appContext) { protected GcmToken(Context appContext) {
if (!(appContext instanceof ReactApplication)) { if (!(appContext instanceof ReactApplication)) {
throw new IllegalStateException("Application instance isn't a react-application"); throw new IllegalStateException("Application instance isn't a react-application");
} }
......
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