/* * Copyright 2018, Oath Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.flurry.android.reactnative; import android.content.Context; import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; 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.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.IllegalViewOperationException; import com.flurry.android.Constants; import com.flurry.android.FlurryAgent; import com.flurry.android.FlurryAgentListener; import com.flurry.android.FlurryConfig; import com.flurry.android.FlurryConfigListener; import com.flurry.android.marketing.FlurryMarketingModule; import com.flurry.android.marketing.FlurryMarketingOptions; import com.flurry.android.marketing.messaging.FlurryMessagingListener; import com.flurry.android.marketing.messaging.notification.FlurryMessage; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class FlurryModule extends ReactContextBaseJavaModule { private static final String TAG = "FlurryModule"; private static final String REACT_CLASS = "ReactNativeFlurry"; private static final String FLURRY_CONFIG_EVENT = "FlurryConfigEvent"; private static final String FLURRY_MESSAGING_EVENT = "FlurryMessagingEvent"; private static final String ORIGIN_NAME = "react-native-flurry-sdk"; private static final String ORIGIN_VERSION = "5.6.0"; private FlurryAgent.Builder mFlurryAgentBuilder; private static ReactApplicationContext sReactApplicationContext = null; private static boolean sEnableMessagingListener = false; private static FlurryMessage sFlurryMessage = null; private static RNFlurryConfigListener sRNFlurryConfigListener = null; private static int sRequestConfigListener = 0; @Override public String getName() { return REACT_CLASS; } public FlurryModule(ReactApplicationContext reactContext) { super(reactContext); sReactApplicationContext = reactContext; } @Override public void initialize() { super.initialize(); if ((sFlurryMessage != null) && (sReactApplicationContext != null)) { RNFlurryMessagingListener.sendEvent(RNFlurryMessagingListener.EventType.NotificationClicked, sFlurryMessage, false); sFlurryMessage = null; } } @ReactMethod public void initBuilder() { mFlurryAgentBuilder = new FlurryAgent.Builder(); } @ReactMethod public void build(String apiKey) { FlurryAgent.addOrigin(ORIGIN_NAME, ORIGIN_VERSION); Context context = getCurrentActivity(); if (context == null) { context = getReactApplicationContext(); } mFlurryAgentBuilder .withListener(new FlurryAgentListener() { @Override public void onSessionStarted() { } }) .withSessionForceStart(true) .build(context, apiKey); } @ReactMethod public void withCrashReporting(boolean crashReporting) { mFlurryAgentBuilder.withCaptureUncaughtExceptions(crashReporting); } @ReactMethod public void withContinueSessionMillis(int sessionMillis) { mFlurryAgentBuilder.withContinueSessionMillis(sessionMillis); } @ReactMethod public void withDataSaleOptOut(boolean isOptOut) { mFlurryAgentBuilder.withDataSaleOptOut(isOptOut); } @ReactMethod public void withIncludeBackgroundSessionsInMetrics(boolean includeBackgroundSessionsInMetrics) { mFlurryAgentBuilder.withIncludeBackgroundSessionsInMetrics(includeBackgroundSessionsInMetrics); } @ReactMethod public void withLogEnabled(boolean enableLog) { mFlurryAgentBuilder.withLogEnabled(enableLog); } @ReactMethod public void withLogLevel(int logLevel) { mFlurryAgentBuilder.withLogLevel(logLevel); } @ReactMethod public void withMessaging(boolean enableMessaging) { Log.i(TAG, "To enable Flurry Push for Android, please duplicate Builder setup in your MainApplication.java."); } @ReactMethod public void setAge(int age) { FlurryAgent.setAge(age); } @ReactMethod public void setGender(String gender) { byte _gender = Constants.UNKNOWN; if (gender.equalsIgnoreCase("m")) { _gender = Constants.MALE; } else if (gender.equalsIgnoreCase("f")) { _gender = Constants.FEMALE; } FlurryAgent.setGender(_gender); } @ReactMethod public void setReportLocation(boolean reportLocation) { FlurryAgent.setReportLocation(reportLocation); } @ReactMethod public void setSessionOrigin(String originName, String deepLink) { FlurryAgent.setSessionOrigin(originName, deepLink); } @ReactMethod public void setUserId(String userId) { FlurryAgent.setUserId(userId); } @ReactMethod public void setVersionName(String versionName) { FlurryAgent.setVersionName(versionName); } @ReactMethod public void setIAPReportingEnabled(boolean enableIAP) { Log.i(TAG, "setIAPReportingEnabled is not supported on Android. Please use logPayment instead."); } @ReactMethod public void setDataSaleOptOut(boolean isOptOut) { FlurryAgent.setDataSaleOptOut(isOptOut); } @ReactMethod public void deleteData() { FlurryAgent.deleteData(); } @ReactMethod public void addOrigin(String originName, String originVersion) { FlurryAgent.addOrigin(originName, originVersion); } @ReactMethod public void addOriginParams(String originName, String originVersion, final ReadableMap originParameters) { FlurryAgent.addOrigin(originName, originVersion, toMap(originParameters)); } @ReactMethod public void addSessionProperty(String name, String value) { FlurryAgent.addSessionProperty(name, value); } @ReactMethod public void getVersions(Callback errorCallback, Callback successCallback) { try { successCallback.invoke(FlurryAgent.getAgentVersion(), FlurryAgent.getReleaseVersion(), FlurryAgent.getSessionId()); } catch (IllegalViewOperationException e) { errorCallback.invoke(e.getMessage()); } } @ReactMethod public void getVersionsPromise(Promise promise) { try { WritableMap map = Arguments.createMap(); map.putInt("agentVersion", FlurryAgent.getAgentVersion()); map.putString("releaseVersion", FlurryAgent.getReleaseVersion()); map.putString("sessionId", FlurryAgent.getSessionId()); promise.resolve(map); } catch (IllegalViewOperationException e) { promise.reject("Flurry.getVersionsPromise", e); } } @ReactMethod public void logBreadcrumb(String crashBreadcrumb) { FlurryAgent.logBreadcrumb(crashBreadcrumb); } @ReactMethod public void logEvent(String eventId) { FlurryAgent.logEvent(eventId); } @ReactMethod public void logEventTimed(String eventId, boolean timed) { FlurryAgent.logEvent(eventId, timed); } @ReactMethod public void logEventParams(String eventId, ReadableMap parameters) { FlurryAgent.logEvent(eventId, toMap(parameters)); } @ReactMethod public void logEventParamsTimed(String eventId, ReadableMap parameters, boolean timed) { FlurryAgent.logEvent(eventId, toMap(parameters), timed); } @ReactMethod public void logPayment(String productName, String productId, int quantity, double price, String currency, String transactionId, ReadableMap parameters) { FlurryAgent.logPayment(productName, productId, quantity, price, currency, transactionId, toMap(parameters)); } @ReactMethod public void endTimedEvent(String eventId) { FlurryAgent.endTimedEvent(eventId); } @ReactMethod public void endTimedEventParams(String eventId, ReadableMap parameters) { FlurryAgent.endTimedEvent(eventId, toMap(parameters)); } @ReactMethod public void onError(String errorId, String message, String errorClass) { FlurryAgent.onError(errorId, message, errorClass); } @ReactMethod public void onErrorParams(String errorId, String message, String errorClass, ReadableMap errorParams) { FlurryAgent.onError(errorId, message, errorClass, toMap(errorParams)); } @ReactMethod public void onPageView() { // Deprecated API removed // FlurryAgent.onPageView(); } @ReactMethod public void UserPropertiesSet(String propertyName, String propertyValue) { FlurryAgent.UserProperties.set(propertyName, propertyValue); } @ReactMethod public void UserPropertiesSetList(String propertyName, ReadableArray propertyValues) { FlurryAgent.UserProperties.set(propertyName, toList(propertyValues)); } @ReactMethod public void UserPropertiesAdd(String propertyName, String propertyValue) { FlurryAgent.UserProperties.add(propertyName, propertyValue); } @ReactMethod public void UserPropertiesAddList(String propertyName, ReadableArray propertyValues) { FlurryAgent.UserProperties.add(propertyName, toList(propertyValues)); } @ReactMethod public void UserPropertiesRemove(String propertyName, String propertyValue) { FlurryAgent.UserProperties.remove(propertyName, propertyValue); } @ReactMethod public void UserPropertiesRemoveList(String propertyName, ReadableArray propertyValues) { FlurryAgent.UserProperties.remove(propertyName, toList(propertyValues)); } @ReactMethod public void UserPropertiesRemoveAll(String propertyName) { FlurryAgent.UserProperties.remove(propertyName); } @ReactMethod public void UserPropertiesFlag(String propertyName) { FlurryAgent.UserProperties.flag(propertyName); } @ReactMethod public void enableMessagingListener(boolean enable) { sEnableMessagingListener = enable; } @ReactMethod public void willHandleMessage(boolean handled) { RNFlurryMessagingListener.notifyCallbackReturn(handled); } @ReactMethod public void registerConfigListener() { sRequestConfigListener++; if (sRNFlurryConfigListener == null) { sRNFlurryConfigListener = new RNFlurryConfigListener(); FlurryConfig.getInstance().registerListener(sRNFlurryConfigListener); } } @ReactMethod public void unregisterConfigListener() { sRequestConfigListener--; } @ReactMethod public void fetchConfig() { FlurryConfig.getInstance().fetchConfig(); } @ReactMethod public void activateConfig() { FlurryConfig.getInstance().activateConfig(); } @ReactMethod public void getConfigString(String key, String defaultValue, Promise promise) { try { WritableMap map = Arguments.createMap(); map.putString(key, FlurryConfig.getInstance().getString(key, defaultValue)); promise.resolve(map); } catch (IllegalViewOperationException e) { promise.reject("Flurry.getConfigString", e); } } @ReactMethod public void getConfigStringMap(ReadableMap keyAndDefault, Promise promise) { try { WritableMap map = Arguments.createMap(); if (keyAndDefault != null) { ReadableMapKeySetIterator iterator = keyAndDefault.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); String defaultValue = keyAndDefault.getString(key); map.putString(key, FlurryConfig.getInstance().getString(key, defaultValue)); } } promise.resolve(map); } catch (IllegalViewOperationException e) { promise.reject("Flurry.getConfigString", e); } } private static Map<String, String> toMap(final ReadableMap readableMap) { if (readableMap == null) { return null; } ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); if (!iterator.hasNextKey()) { return null; } Map<String, String> result = new HashMap<>(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); result.put(key, readableMap.getString(key)); } return result; } private static List<String> toList(final ReadableArray readableArray) { if ((readableArray == null) || (readableArray.size() == 0)) { return null; } List<String> result = new ArrayList<>(); for (int i = 0; i < readableArray.size(); i++) { if (readableArray.getType(i) == ReadableType.String) { result.add(readableArray.getString(i)); } } return result; } /** * Builder Pattern class for Flurry. Used by MainApplication to initialize Flurry Push for messaging. */ public static class Builder { private FlurryAgent.Builder mFlurryAgentBuilder; public Builder() { mFlurryAgentBuilder = new FlurryAgent.Builder(); } /** * True to enable or false to disable the ability to catch all uncaught exceptions * and have them reported back to Flurry. * * @param captureExceptions true to enable, false to disable. * @return The Builder instance. */ public Builder withCrashReporting(final boolean captureExceptions) { mFlurryAgentBuilder.withCaptureUncaughtExceptions(captureExceptions); return this; } /** * Set the timeout for expiring a Flurry session. * * @param sessionMillis The time in milliseconds to set the session timeout to. Minimum value of 5000. * @return The Builder instance. */ public Builder withContinueSessionMillis(final long sessionMillis) { mFlurryAgentBuilder.withContinueSessionMillis(sessionMillis); return this; } /** * True if this session should be added to total sessions/DAUs when applicationstate is inactive or background. * Default is set to true. * * @param includeBackgroundSessionsInMetrics if background and inactive session should be counted toward dau */ public Builder withIncludeBackgroundSessionsInMetrics(final boolean includeBackgroundSessionsInMetrics) { mFlurryAgentBuilder.withIncludeBackgroundSessionsInMetrics(includeBackgroundSessionsInMetrics); return this; } /** * True to enable or false to disable the internal logging for the Flurry SDK. * * @param enableLog true to enable logging, false to disable it. * @return The Builder instance. */ public Builder withLogEnabled(final boolean enableLog) { mFlurryAgentBuilder.withLogEnabled(enableLog); return this; } /** * Set the log level of the internal Flurry SDK logging. * * @param logLevel The level to set it to. * @return The Builder instance. */ public Builder withLogLevel(final int logLevel) { mFlurryAgentBuilder.withLogLevel(logLevel); return this; } /** * Enable Flurry add-on Messaging. * * @param enableMessaging true to enable messaging, * currently support only auto integration. * @return The Builder instance. */ public Builder withMessaging(final boolean enableMessaging) { withMessaging(enableMessaging, (FlurryMessagingListener) null); return this; } /** * Enable Flurry add-on Messaging with listener. * * @param enableMessaging true to enable messaging, * currently support only auto integration. * @param messagingListener user's messaging listener. * @return The Builder instance. */ public Builder withMessaging(final boolean enableMessaging, FlurryMessagingListener messagingListener) { if (!enableMessaging) { return this; } if (messagingListener == null) { messagingListener = new RNFlurryMessagingListener(); } FlurryMarketingOptions messagingOptions = new FlurryMarketingOptions.Builder() .setupMessagingWithAutoIntegration() .withFlurryMessagingListener(messagingListener) // Define yours if needed // .withDefaultNotificationChannelId(NOTIFICATION_CHANNEL_ID) // .withDefaultNotificationIconResourceId(R.mipmap.ic_launcher_round) // .withDefaultNotificationIconAccentColor(getResources().getColor(R.color.colorPrimary)) .build(); FlurryMarketingModule marketingModule = new FlurryMarketingModule(messagingOptions); mFlurryAgentBuilder.withModule(marketingModule); return this; } /** * Enable Flurry add-on Messaging with options. * * @param enableMessaging true to enable messaging. * @param messagingOptions user's messaging options. * @return The Builder instance. */ public Builder withMessaging(final boolean enableMessaging, final FlurryMarketingOptions messagingOptions) { if (!enableMessaging) { return this; } FlurryMarketingModule marketingModule = new FlurryMarketingModule(messagingOptions); mFlurryAgentBuilder.withModule(marketingModule); return this; } public void build(final Context context, final String apiKey) { mFlurryAgentBuilder .withSessionForceStart(true) .build(context, apiKey); } } /** * Wrapper Flurry Config listenet. */ static class RNFlurryConfigListener implements FlurryConfigListener { enum EventType { FetchSuccess("FetchSuccess"), FetchNoChange("FetchNoChange"), FetchError("FetchError"), ActivateComplete("ActivateComplete"); private final String name; EventType(String name) { this.name = name; } public String getName() { return name; } } @Override public void onFetchSuccess() { if ((sRequestConfigListener > 0) && (sReactApplicationContext != null)) { sendEvent(EventType.FetchSuccess); } } @Override public void onFetchNoChange() { if ((sRequestConfigListener > 0) && (sReactApplicationContext != null)) { sendEvent(EventType.FetchNoChange); } } @Override public void onFetchError(boolean value) { if ((sRequestConfigListener > 0) && (sReactApplicationContext != null)) { sendEvent(EventType.FetchError, "isRetrying", value); } } @Override public void onActivateComplete(boolean value) { if ((sRequestConfigListener > 0) && (sReactApplicationContext != null)) { sendEvent(EventType.ActivateComplete, "isCache", value); } } private void sendEvent(EventType type) { sendEvent(type, null, false); } private void sendEvent(EventType type, String key, boolean value) { WritableMap params = Arguments.createMap(); params.putString("Type", type.getName()); if (key != null) { params.putBoolean(key, value); } sReactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(FLURRY_CONFIG_EVENT, params); } } /** * Wrapper Flurry Messaging listenet. */ static class RNFlurryMessagingListener implements FlurryMessagingListener { private volatile static boolean sCallbackReturnValue = false; private volatile static boolean sIsCallbackReturn = false; enum EventType { NotificationReceived("NotificationReceived"), NotificationClicked("NotificationClicked"), NotificationCancelled("NotificationCancelled"), TokenRefresh("TokenRefresh"), NonFlurryNotificationReceived("NonFlurryNotificationReceived"); private final String name; EventType(String name) { this.name = name; } public String getName() { return name; } } @Override public boolean onNotificationReceived(FlurryMessage flurryMessage) { if (sEnableMessagingListener && (sReactApplicationContext != null)) { return sendEvent(EventType.NotificationReceived, flurryMessage, true); } return false; } @Override public boolean onNotificationClicked(FlurryMessage flurryMessage) { if (sReactApplicationContext == null) { // React Native platform is inactive, cache for later. sFlurryMessage = flurryMessage; } else if (sEnableMessagingListener) { return sendEvent(EventType.NotificationClicked, flurryMessage, true); } return false; } @Override public void onNotificationCancelled(FlurryMessage flurryMessage) { if (sEnableMessagingListener && (sReactApplicationContext != null)) { sendEvent(EventType.NotificationCancelled, flurryMessage, false); } } @Override public void onTokenRefresh(String token) { if (sEnableMessagingListener && (sReactApplicationContext != null)) { sendEvent(EventType.TokenRefresh, token); } } @Override public void onNonFlurryNotificationReceived(Object message) { // no-op } private static boolean sendEvent(EventType type, FlurryMessage flurryMessage, boolean waitReturn) { WritableMap params = Arguments.createMap(); params.putString("Type", type.getName()); params.putString("Title", flurryMessage.getTitle()); params.putString("Body", flurryMessage.getBody()); params.putString("ClickAction", flurryMessage.getClickAction()); Map<String, String> appData = flurryMessage.getAppData(); WritableMap data = Arguments.createMap(); for (String key : appData.keySet()) { data.putString(key, appData.get(key)); } params.putMap("Data", data); sCallbackReturnValue = false; sIsCallbackReturn = !waitReturn; sReactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(FLURRY_MESSAGING_EVENT, params); waitCallbackReturn(); return sCallbackReturnValue; } private static void sendEvent(EventType type, String token) { WritableMap params = Arguments.createMap(); params.putString("Type", type.getName()); params.putString("Token", token); sReactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(FLURRY_MESSAGING_EVENT, params); } private static void waitCallbackReturn() { synchronized (sReactApplicationContext) { if (!sIsCallbackReturn) { try { sReactApplicationContext.wait(300); } catch (InterruptedException e) { Log.e(TAG, "Interrupted Exception!", e); } } } } private static void notifyCallbackReturn(boolean returnValue) { synchronized (sReactApplicationContext) { sCallbackReturnValue = returnValue; sIsCallbackReturn = true; sReactApplicationContext.notifyAll(); } } } }