Commit d5682e8b authored by Amit Davidi's avatar Amit Davidi

Extensibility refactor step 2

parent 00c8a581
......@@ -14,11 +14,9 @@
<application>
<!--
A proxy-activity that either launches the main activity or resumes the currently running app.
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.
A proxy-service that gives the library an opportunity to do some work before launching/resuming the actual application task.
-->
<activity android:name=".core.ProxyActivity" android:taskAffinity="com.wix.reactnativenotifications.core.ProxyActivity"/>
<service android:name=".core.ProxyService"/>
<!--
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 {
public static PendingIntent createPendingNotificationIntent(Context appContext, Intent intent, PushNotificationProps notification) {
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) {
......
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;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
......@@ -17,36 +14,54 @@ 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.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_RECEIVED_EVENT_NAME;
public class PushNotification {
public class PushNotification implements IPushNotification {
public static class InvalidNotificationException extends Exception {
public InvalidNotificationException(String detailMessage) {
super(detailMessage);
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();
}
}
private final Context mAppContext;
private PushNotificationProps mNotificationProps;
@Override
public void onAppNotVisible() {}
};
public PushNotification(Context context, Bundle bundle) {
mAppContext = context.getApplicationContext();
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();
}
public void onOpened(Activity hostActivity) {
digestNotification(hostActivity);
@Override
public void onOpened() {
digestNotification();
deleteAllPostedNotifications();
}
@Override
public PushNotificationProps asProps() {
return mNotificationProps.copy();
}
......@@ -57,28 +72,50 @@ public class PushNotification {
postNotification((int) System.currentTimeMillis(), notification);
}
protected void digestNotification(Activity activity) {
final ReactContext reactContext = getRunningReactContext();
if (reactContext != null) {
launchOrResumeApp(activity);
notifyOpenedToJS(reactContext);
protected void digestNotification() {
if (!mAppLifecycleFacade.isReactInitialized()) {
setAsInitialNotification();
launchOrResumeApp();
return;
}
if (mAppLifecycleFacade.isAppVisible()) {
dispatchImmediately();
} else {
InitialNotification.set(mNotificationProps);
launchOrResumeApp(activity);
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() {
// Note: we launch the proxy activity, assuming it'll take it from there.
final Intent cta = new Intent(mAppContext, ProxyActivity.class);
return NotificationIntentAdapter.createPendingNotificationIntent(mAppContext, cta, mNotificationProps);
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(mAppContext)
final Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setContentTitle(mNotificationProps.getTitle())
.setContentText(mNotificationProps.getBody())
.setSmallIcon(mAppContext.getApplicationInfo().icon)
.setSmallIcon(mContext.getApplicationInfo().icon)
.setContentIntent(intent)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true);
......@@ -87,17 +124,17 @@ public class PushNotification {
}
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);
}
protected ReactContext getRunningReactContext() {
final ReactNativeHost rnHost = ((ReactApplication) mAppContext).getReactNativeHost();
final ReactNativeHost rnHost = ((ReactApplication) mContext.getApplicationContext()).getReactNativeHost();
if (!rnHost.hasInstance()) {
return null;
}
final ReactInstanceManager instanceManager = ((ReactApplication) mAppContext).getReactNativeHost().getReactInstanceManager();
final ReactInstanceManager instanceManager = rnHost.getReactInstanceManager();
final ReactContext reactContext = instanceManager.getCurrentReactContext();
if (reactContext == null || !reactContext.hasActiveCatalystInstance()) {
return null;
......@@ -129,22 +166,13 @@ public class PushNotification {
}
}
protected void launchOrResumeApp(Activity activity) {
// 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 (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 launchOrResumeApp() {
final Intent intent = AppLaunchHelper.getLaunchIntent(mContext);
mContext.startActivity(intent);
}
protected void deleteAllPostedNotifications() {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}
}
package com.wix.reactnativenotifications.core;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
......@@ -7,8 +9,7 @@ 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.gcm.GcmToken;
import com.wix.reactnativenotifications.gcm.IGcmToken;
import com.wix.reactnativenotifications.gcm.GcmInstanceIdRefreshHandlerService;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
......@@ -16,6 +17,8 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule {
public RNNotificationsModule(ReactApplicationContext reactContext) {
super(reactContext);
ReactAppLifecycleFacade.get().onAppInit(reactContext);
}
@Override
......@@ -26,12 +29,18 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule {
@Override
public void initialize() {
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
public void getInitialNotification(final Promise promise) {
Log.d(LOGTAG, "Native method invocation: getInitialNotification");
Object result = null;
try {
......@@ -45,4 +54,14 @@ public class RNNotificationsModule extends ReactContextBaseJavaModule {
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;
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());
}
......@@ -12,7 +15,15 @@ public class GcmInstanceIdRefreshHandlerService extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
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();
}
}
......
......@@ -4,7 +4,9 @@ import android.os.Bundle;
import android.util.Log;
import com.google.android.gms.gcm.GcmListenerService;
import com.wix.reactnativenotifications.core.IPushNotification;
import com.wix.reactnativenotifications.core.PushNotification;
import com.wix.reactnativenotifications.core.ReactAppLifecycleFacade;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
......@@ -15,9 +17,9 @@ public class GcmMessageHandlerService extends GcmListenerService {
Log.d(LOGTAG, "New message from GCM: " + bundle);
try {
final PushNotification notification = new PushNotification(getApplicationContext(), bundle);
final IPushNotification notification = PushNotification.get(getApplicationContext(), bundle, ReactAppLifecycleFacade.get());
notification.onReceived();
} catch (PushNotification.InvalidNotificationException e) {
} 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);
}
......
......@@ -23,7 +23,7 @@ public class GcmToken implements IGcmToken {
protected static String sToken;
public GcmToken(Context appContext) {
protected GcmToken(Context appContext) {
if (!(appContext instanceof ReactApplication)) {
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