In the previous tutorial we introduced the concept of "custom controls" with a simple example. In this tutorial we go one step further and implement Flurry Analytics as custom control.
Flurry Analytics is a free analytics service to measure, track and analyze the app performance. The tutorial uses the "Flurry Android SDK vAndroid SDK 5.3.0" and the "Flurry iPhone SDK viPhone SDK 6.3.0", the latest versions which were available at the time of writing.
- Note
- In this tutorial we only use the analytics feature and omit the usage of Flurry Ads.
Quick links to the individual sections in this tutorial:
Backend
To be able to use Flurry as analytics service, we first have to setup the app on the Flurry server. We need to register on dev.flurry.com and create a separate project for Android and iOS. The unique ProjectApiKeys can then be used in the appropriate configuration files.
Flurry Interface
We start by defining the interface of our Flurry control, thus the C++ methods we will use to access the Flurry SDK.
We store all Flurry relevant files in the subfolder flurry
, to keep the project hierarchy clear.
source/flurry/flurry_control.h
// Copyright 2015 Spraylight #ifndef __FLURRY_CONTROL_H__ #define __FLURRY_CONTROL_H__ #include "murl_custom_controlable.h" #include "murl_map.h" namespace Murl { namespace App { class FlurryControl : public CustomControlable { public: static FlurryControl* Create(const String& name); enum FlurryEventRecordStatus { FlurryEventFailed = 0, FlurryEventRecorded, FlurryEventUniqueCountExceeded, FlurryEventParamsCountExceeded, FlurryEventLogCountExceeded, FlurryEventLoggingDelayed }; FlurryControl(const String& name) : CustomControlable(name) {} virtual ~FlurryControl() {} /** @brief Log a optionally timed event without parameters with the Flurry service. To end the timer, call endTimedEvent(String) with this eventId. @param eventId the name/id of the event @param timed true if the event should be timed, false otherwise @return the error code */ virtual SInt32 LogEvent(const String& eventId, const Bool timed=false) = 0; /** @brief Log a optionally timed event with one parameter with the Flurry service. To end the timer, call endTimedEvent(String) with this eventId. @param eventId the name/id of the event @param param the parameter key @param value the parameter value @param timed true if the event should be timed, false otherwise @return the error code */ virtual SInt32 LogEvent(const String& eventId, const String& param, const String& value, const Bool timed=false) = 0; /** @brief Log a optionally timed event with parameters with the Flurry service. The event is identified by the eventId, which is a String. A Map<String, String> of parameters can be passed in where the key is the parameter name, and the value is the value. Only up to 10 parameters can be passed in with the Map. If more than 10 parameters are passed, all parameters will be dropped and the event will be logged as if it had no parameters. @param eventId the name/id of the event @param parameters A Map<String, String> of the parameters which should be submitted with this event. @return the error code */ virtual SInt32 LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed=false) = 0; /** @brief End a timed event. @param eventId the name/id of the event to end the timer on */ virtual void EndTimedEvent(const String& eventId) = 0; /** End a timed event with one parameter. Only up to 10 unique parameters total can be passed for an event, including those passed when the event was initiated. If more than 10 unique parameters total are passed, all parameter updates passed on this endTimedEvent call will be ignored. @param eventId the name/id of the event to end the timer on @param param the parameter key @param value the parameter value */ virtual void EndTimedEvent(const String& eventId, const String& param, const String& value) = 0; /** End a timed event with parameters. Only up to 10 unique parameters total can be passed for an event, including those passed when the event was initiated. If more than 10 unique parameters total are passed, all parameter updates passed on this endTimedEvent call will be ignored. @param eventId the name/id of the event to end the timer on @param parameters A Map<String, String> of the parameters which should be submitted with this event. */ virtual void EndTimedEvent(const String& eventId, const Map<String, String>& parameters) = 0; /** Use OnError to report errors that your application catches. Flurry will report the first 10 errors to occur in each session. @param errorId The name/id of the error. @param message The error message. */ virtual void OnError(const String& errorId, const String& message) = 0; /** Use onPageView to report page view count. To increment the total count, you should call this method whenever a new page is shown to the user. */ virtual void OnPageView() = 0; }; } } #endif // __FLURRY_CONTROL_H__
As in the previous tutorial we declare a static Create
method and some pure virtual methods to log events, errors and page views. The function parameter timed
is a default parameter with the default value false
. Hence the specification of this parameter is optional.
Default Implementation
Next we implement a default version (FlurryControlDefault
) of our control, which merely prints simple debug messages.
source/flurry/flurry_control_default.h
source/flurry/flurry_control_default.cpp
#include "flurry_control.h" namespace Murl { namespace App { class FlurryControlDefault : public FlurryControl { public: static FlurryControl* Create(const String& name); FlurryControlDefault(const String& name) : FlurryControl(name) {} virtual ~FlurryControlDefault() {} virtual SInt32 LogEvent(const String& eventId, const Bool timed); virtual SInt32 LogEvent(const String& eventId, const String& param, const String& value, const Bool timed); virtual SInt32 LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed); virtual void EndTimedEvent(const String& eventId); virtual void EndTimedEvent(const String& eventId, const String& param, const String& value); virtual void EndTimedEvent(const String& eventId, const Map<String, String>& parameters); virtual void OnError(const String& errorId, const String& message); virtual void OnPageView(); }; } }
// Copyright 2015 Spraylight #include "flurry_control_default.h" using namespace Murl; App::FlurryControl* App::FlurryControl::Create(const String& name) { return new App::FlurryControlDefault(name); } SInt32 App::FlurryControlDefault::LogEvent(const String& eventId, const Bool timed) { MURL_TRACE(0, "%s, timed=%s", eventId.Begin(), timed ? "TRUE" : "FALSE"); return FlurryEventRecorded; } SInt32 App::FlurryControlDefault::LogEvent(const String& eventId, const String& param, const String& value, const Bool timed) { MURL_TRACE(0, "%s, %s=%s, timed=%s", eventId.Begin(), param.Begin(), value.Begin(), timed ? "TRUE" : "FALSE"); return FlurryEventRecorded; } SInt32 App::FlurryControlDefault::LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed) { MURL_TRACE(0, "%s, parametersCount=%d, timed=%s", eventId.Begin(), parameters.GetCount(), timed ? "TRUE" : "FALSE"); SInt32 size = parameters.GetCount(); for (SInt32 i = 0; i < size; i++) { const String& key = parameters.GetKey(i); const String& value = parameters[i]; MURL_TRACE(0, "parameter %d, %s=%s", i, key.Begin(), value.Begin()); } return FlurryEventRecorded; } void App::FlurryControlDefault::EndTimedEvent(const String& eventId) { MURL_TRACE(0, "%s", eventId.Begin()); } void App::FlurryControlDefault::EndTimedEvent(const String& eventId, const String& param, const String& value) { MURL_TRACE(0, "%s, %s=%s", eventId.Begin(), param.Begin(), value.Begin()); } void App::FlurryControlDefault::EndTimedEvent(const String& eventId, const Map<String, String>& parameters) { MURL_TRACE(0, "%s, parametersCount=%d", eventId.Begin(), parameters.GetCount()); SInt32 size = parameters.GetCount(); for (SInt32 i = 0; i < size; i++) { const String& key = parameters.GetKey(i); const String& value = parameters[i]; MURL_TRACE(0, "parameter %d, %s=%s", i, key.Begin(), value.Begin()); } } void App::FlurryControlDefault::OnError(const String& errorId, const String& message) { MURL_TRACE(0, "%s, %s", errorId.Begin(), message.Begin()); } void App::FlurryControlDefault::OnPageView() { MURL_TRACE(0, ""); }
Logic Code
By using the default implementation, we are now able to instance a FlurryControl
object and use it in our app.
#include "murl_app_types.h" #include "murl_logic_base_processor.h" #include "flurry/flurry_control.h" namespace Murl { namespace App { class FlurryLogic : public Logic::BaseProcessor { public: FlurryLogic(Logic::IFactory* factory); virtual ~FlurryLogic(); protected: virtual Bool OnInit(const Logic::IState* state); virtual Bool OnDeInit(const Logic::IState* state); virtual void OnProcessTick(const Logic::IState* state); void TestResult(SInt32 result); Logic::ButtonNode mBtn01; Logic::ButtonNode mBtn02; FlurryControl* mFlurryControl; }; } }
App::FlurryLogic::FlurryLogic(Logic::IFactory* factory) : BaseProcessor(factory) , mFlurryControl(0) { } App::FlurryLogic::~FlurryLogic() { } Bool App::FlurryLogic::OnInit(const Logic::IState* state) { state->GetLoader()->UnloadPackage("startup"); Graph::IRoot* root = state->GetGraphRoot(); AddGraphNode(mBtn01.GetReference(root, "b1/button")); AddGraphNode(mBtn02.GetReference(root, "b2/button")); if (!AreGraphNodesValid()) { return false; } Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler(); // create Flurry control instance mFlurryControl = FlurryControl::Create("FlurryControl"); // add Flurry control to outputDeviceHandler if (!outputDeviceHandler->AddCustomControl(mFlurryControl)) { return false; } state->SetUserDebugMessage("Flurry Init succeeded!"); return true; } Bool App::FlurryLogic::OnDeInit(const Logic::IState* state) { Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); Output::IDeviceHandler* outputDeviceHandler = deviceHandler->GetOutputDeviceHandler(); // remove Flurry control from outputDeviceHandler if (!outputDeviceHandler->RemoveCustomControl(mFlurryControl)) { return false; } return true; } void App::FlurryLogic::OnProcessTick(const Logic::IState* state) { Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler(); String prefix = "test log event "; if (deviceHandler->WasRawKeyPressed(RAWKEY_1) || mBtn01->WasReleasedInside()) { Debug::Trace("Start Test"); SInt32 result; result = mFlurryControl->LogEvent(prefix + "001"); TestResult(result); result = mFlurryControl->LogEvent(prefix + "002", "param 01", "value 01"); TestResult(result); Map<String, String> parameters; parameters.Put("paramA", "valueA"); parameters.Put("paramB", "valueB"); parameters.Put("paramC", "valueC"); result = mFlurryControl->LogEvent(prefix + "003", parameters); TestResult(result); result = mFlurryControl->LogEvent(prefix + "t01", true); TestResult(result); result = mFlurryControl->LogEvent(prefix + "t02", "param 01","value 01", true); TestResult(result); result = mFlurryControl->LogEvent(prefix + "t03", parameters, true); TestResult(result); mFlurryControl->OnError("error 01", "test error"); mFlurryControl->OnPageView(); } if (deviceHandler->WasRawKeyPressed(RAWKEY_2) || mBtn02->WasReleasedInside()) { Debug::Trace("End timed Events"); mFlurryControl->EndTimedEvent(prefix + "t01"); mFlurryControl->EndTimedEvent(prefix + "t02", "param 02", "value 02"); Map<String, String> parameters; parameters.Put("paramD", "valueD"); parameters.Put("paramE", "valueE"); mFlurryControl->EndTimedEvent(prefix + "t03", parameters); } // Exit if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) || deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK)) { deviceHandler->TerminateApp(); } } void App::FlurryLogic::TestResult(SInt32 result) { if (result != App::FlurryControl::FlurryEventRecorded) Debug::Trace("Failed %d", result); }
The default implementation produces the following output.
Start Test Murl::App::FlurryControlDefault::LogEvent(), line 14: test log event 001, timed=FALSE Murl::App::FlurryControlDefault::LogEvent(), line 20: test log event 002, param 01=value 01, timed=FALSE Murl::App::FlurryControlDefault::LogEvent(), line 26: test log event 003, parametersCount=3, timed=FALSE Murl::App::FlurryControlDefault::LogEvent(), line 33: parameter 0, paramA=valueA Murl::App::FlurryControlDefault::LogEvent(), line 33: parameter 1, paramB=valueB Murl::App::FlurryControlDefault::LogEvent(), line 33: parameter 2, paramC=valueC Murl::App::FlurryControlDefault::LogEvent(), line 14: test log event t01, timed=TRUE Murl::App::FlurryControlDefault::LogEvent(), line 20: test log event t02, param 01=value 01, timed=TRUE Murl::App::FlurryControlDefault::LogEvent(), line 26: test log event t03, parametersCount=3, timed=TRUE Murl::App::FlurryControlDefault::LogEvent(), line 33: parameter 0, paramA=valueA Murl::App::FlurryControlDefault::LogEvent(), line 33: parameter 1, paramB=valueB Murl::App::FlurryControlDefault::LogEvent(), line 33: parameter 2, paramC=valueC Murl::App::FlurryControlDefault::OnError(), line 62: error 01, test error Murl::App::FlurryControlDefault::OnPageView(), line 67: End timed Events Murl::App::FlurryControlDefault::EndTimedEvent(), line 40: test log event t01 Murl::App::FlurryControlDefault::EndTimedEvent(), line 45: test log event t02, param 02=value 02 Murl::App::FlurryControlDefault::EndTimedEvent(), line 50: test log event t03, parametersCount=2 Murl::App::FlurryControlDefault::EndTimedEvent(), line 56: parameter 0, paramD=valueD Murl::App::FlurryControlDefault::EndTimedEvent(), line 56: parameter 1, paramE=valueE
Android Version
Next we integrate the "Flurry Analytics Android SDK" for the Android version or our app.
Add JAR Archive
At first we have to add the JAR archive FlurryAnalytics_5.3.0.jar
to our project.
We store the JAR archive in a separate directory android/jar
.
source/android/jar/FlurryAnalytics-5.3.0.jar
- Note
- You can only assign one directory for all JAR files in the Makefile. It is a good practice to create a separate directory and store all JAR files there. The same applies for Java files, Proguard fragments, manifest fragments etc.
We add the file by specifying the parameters MURL_ANDROID_JAR_PATH
and MURL_ANDROID_JAR_FILES
in the common Makefile module_flurry.mk
. Additionally we set the required permissions for the Flurry SDK.
Initialisation
The Flurry SDK requires the addition of appropriate FlurryAgent
calls in the app methods onCreate()
, onStart()
and onStop()
. The Interface MurlCustomControl
can be used to get callbacks to these app methods.
Therefore we create a Java class FlurryControl
and implement the interface MurlCustomControl
. Additionally we create the class FlurryConfiguration
to configure the Flurry SDK.
source/android/java/at/spraylight/flurry/FlurryControl.java
source/android/java/at/spraylight/flurry/FlurryConfiguration.java
// Copyright 2015 Spraylight GmbH package at.spraylight.flurry; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import at.spraylight.murl.MurlCustomControl; import com.flurry.android.FlurryAgent; public class FlurryControl implements MurlCustomControl { private final Activity mActivity; public FlurryControl(Activity activity) { mActivity = activity; } @Override public void onCreate(Bundle savedInstanceState) { Log.d(FlurryConfiguration.LOG_TAG, "FlurryControl::onCreate(): Init Session"); // **** CONFIGURE FLURRY *** FlurryConfiguration.configure(mActivity); // **** INIT FLURRY *** FlurryAgent.init(mActivity, FlurryConfiguration.API_Key); } @Override public void onStart() { Log.d(FlurryConfiguration.LOG_TAG, "FlurryControl::onStart(): Start Session"); FlurryAgent.onStartSession(mActivity); } @Override public void onStop() { Log.d(FlurryConfiguration.LOG_TAG, "FlurryControl::onStop(): End Flurry session"); FlurryAgent.onEndSession(mActivity); } @Override public void onRestart() { } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { } @Override public void onResume() { } @Override public void onPause() { } @Override public void onSaveInstanceState(Bundle outState) { } @Override public void onDestroy() { } }
package at.spraylight.flurry; import android.app.Activity; import android.util.Log; import com.flurry.android.FlurryAgent; /** Limits to the number of Events and Parameters: 300 Events for each app. Each event can have up to 10 parameters, and each parameter can have any number of values. */ public class FlurryConfiguration { public static final String LOG_TAG = "Flurry"; // Enter your own key here: public static final String API_Key = "XXXXXXXXXXXXXXXXXXXX"; /** This method is called before init. */ public static final void configure(Activity activity) { // Use setLogEnabled to enable/disable internal Flurry SDK logging. This should be called before init. FlurryAgent.setLogEnabled(false); // Sets the log level of the internal Flurry SDK logging. Valid inputs are Log.VERBOSE, Log.WARN etc. Default log level is Log.WARN. This should be called before init. FlurryAgent.setLogLevel(Log.DEBUG); //Use setLogEvents to enable/disable the event logging. This should be called before init. FlurryAgent.setLogEvents(true); // To disable detailed location reporting even when your app has permission //FlurryAgent.setReportLocation(true); //Use this to log the user's assigned ID or username in your system. This should be called before init, if possible. //FlurryAgent.setUserID(String); //Use this to log the user's age. Valid inputs are between 1 and 109. This should be called before init, if possible. //FlurryAgent.setAge(int); //Use this to log the user's gender. Valid inputs are Constants.MALE or Constants.FEMALE. This should be called before init, if possible. //FlurryAgent.setGender(Constants.MALE); //Sets the version name of the app. Flurry automatically uses the versionName attribute from manifest if not set! This name will appear in the https://dev.flurry.com as a filtering option by version. This should be called before init. // FlurryAgent.setVersionName(versionName); // Get the version of the Flurry SDK. // FlurryAgent.getAgentVersion(); // Get the release version of the Flurry SDK. // FlurryAgent.getReleaseVersion(); } }
We again list the new files in the Makefile and register the new class as MurlCustomControl
class.
JNI Bridge
Next, we define our JNI bridge class containing the static Java methods which we will call from C++.
source/android/java/at/spraylight/flurry/FlurryJniBridge.java
// Copyright 2015 Spraylight GmbH package at.spraylight.flurry; import java.util.HashMap; import java.util.Map; import android.util.Log; import com.flurry.android.FlurryAgent; import com.flurry.android.FlurryEventRecordStatus; public class FlurryJniBridge { static Map<String, Map<String,String>> mapContainer = new HashMap<String, Map<String,String>>(); // JNI from C++ to Java /** Log a possibly timed event with the Flurry service. The method logEventPrepare can be used to add parameters to the event. Only up to 10 unique parameters total can be passed for an event. If more than 10 parameters are passed, all parameters will be dropped and the event will be logged as if it had no parameters. To end the timer, call endTimedEvent(String) with this eventId. @param eventId the name/id of the event @param timed true if the event should be timed, false otherwise @return the error code 0: FlurryEventFailed 1: FlurryEventRecorded 2: FlurryEventUniqueCountExceeded 3: FlurryEventParamsCountExceeded 4: FlurryEventLogCountExceeded 5: FlurryEventLoggingDelayed */ public static int logEvent(String eventId, boolean timed) { Log.d(FlurryConfiguration.LOG_TAG, "FlurryJniBridge::logEvent(): eventId=" + eventId + " timed=" + timed); Map<String, String> parameters = mapContainer.remove(eventId); FlurryEventRecordStatus status = (parameters == null) ? FlurryAgent.logEvent(eventId, timed) : FlurryAgent.logEvent(eventId, parameters, timed); return status.ordinal(); } /** Log a possibly timed event with one parameter with the Flurry service. To end the timer, call endTimedEvent(String) with this eventId. @param eventId the name/id of the event @param param the parameter key @param value the parameter value @param timed true if the event should be timed, false otherwise @return the error code 0: FlurryEventFailed 1: FlurryEventRecorded 2: FlurryEventUniqueCountExceeded 3: FlurryEventParamsCountExceeded 4: FlurryEventLogCountExceeded 5: FlurryEventLoggingDelayed */ public static int logEvent(String eventId, String param, String value, boolean timed) { Log.d(FlurryConfiguration.LOG_TAG, "FlurryJniBridge::logEvent(): eventId=" + eventId + " param=" + param + " value=" + value + " timed=" + timed); Map<String, String> parameters = new HashMap<String, String>(2, 1); parameters.put(param, value); FlurryEventRecordStatus status = FlurryAgent.logEvent(eventId, parameters, timed); return status.ordinal(); } /** Prepare parameters for a logEvent or endTimedEvent execution. The data is collected in a Map. */ public static void logEventPrepare(String eventId, String param, String value) { Map<String, String> parameters = mapContainer.get(eventId); if (parameters == null) { parameters = new HashMap<String, String>(); mapContainer.put(eventId, parameters); } parameters.put(param, value); } /** End a timed event. The method logEventPrepare can be used to add up to 10 parameters to the event. Only up to 10 unique parameters total can be passed for an event, including those passed when the event was initiated. If more than 10 unique parameters total are passed, all parameter updates passed on this endTimedEvent call will be ignored. @param eventId the name/id of the event to end the timer on */ public static void endTimedEvent(String eventId) { Log.d(FlurryConfiguration.LOG_TAG, "FlurryJniBridge::endTimedEvent(): eventId=" + eventId); Map<String, String> parameters = mapContainer.remove(eventId); if (parameters == null) { FlurryAgent.endTimedEvent(eventId); } else { FlurryAgent.endTimedEvent(eventId, parameters); } } /** End a timed event with one parameter. Only up to 10 unique parameters total can be passed for an event, including those passed when the event was initiated. If more than 10 unique parameters total are passed, all parameter updates passed on this endTimedEvent call will be ignored. @param eventId the name/id of the event to end the timer on @param param the parameter key @param value the parameter value */ public static void endTimedEvent(String eventId, String param, String value) { Log.d(FlurryConfiguration.LOG_TAG, "FlurryJniBridge::endTimedEvent(): eventId=" + eventId+", "+param+"="+value); Map<String, String> parameters = new HashMap<String, String>(2, 1); parameters.put(param, value); FlurryAgent.endTimedEvent(eventId, parameters); } /** Use onError to report errors that your application catches. Flurry will report the first 10 errors to occur in each session. @param errorId The name/id of the error. @param message The error message. */ public static void onError(String errorId, String message) { Log.d(FlurryConfiguration.LOG_TAG, "FlurryJniBridge::onError(): errorId=" + errorId+", "+message); Throwable th = new Exception(""); FlurryAgent.onError(message, message, th); } /** Use onPageView to report page view count. To increment the total count, you should call this method whenever a new page is shown to the user. */ public static void onPageView() { Log.d(FlurryConfiguration.LOG_TAG, "FlurryJniBridge::onPageView()"); FlurryAgent.onPageView(); } }
We list the new file in the Makefile and register the class as JNI class.
CPP Implementation
Now we are able to create the Android C++ implementation of our FlurryControl
and call the appropriate JNI bridge methods.
#include "flurry_control.h" #include "murl_platform_android_native_platform.h" #include "murl_platform_android_jni_bridge.h" namespace Murl { namespace App { class FlurryControlAndroid : public FlurryControl { public: static FlurryControl* Create(const String& name); FlurryControlAndroid(const String& name) : FlurryControl(name), mJniBridge(0) {} virtual ~FlurryControlAndroid() {} virtual SInt32 LogEvent(const String& eventId, const Bool timed); virtual SInt32 LogEvent(const String& eventId, const String& param, const String& value, const Bool timed); virtual SInt32 LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed); virtual void EndTimedEvent(const String& eventId); virtual void EndTimedEvent(const String& eventId, const String& param, const String& value); virtual void EndTimedEvent(const String& eventId, const Map<String, String>& parameters); virtual void OnError(const String& errorId, const String& message); virtual void OnPageView(); protected: virtual Bool Init(IPlatform* platform); virtual Bool DeInit(); Platform::Android::JniBridge* mJniBridge; }; } }
// Copyright 2015 Spraylight #include "flurry_control_android.h" using namespace Murl; App::FlurryControl* App::FlurryControl::Create(const String& name) { return new App::FlurryControlAndroid(name); } SInt32 App::FlurryControlAndroid::LogEvent(const String& eventId, const Bool timed) { MURL_TRACE(0, "%s, timed=%d", eventId.Begin(), timed); SInt32 ret; mJniBridge->CallStaticJavaIntFunc<jstring, jboolean>("FlurryJniBridge.logEvent", ret, eventId, timed); return ret; } SInt32 App::FlurryControlAndroid::LogEvent(const String& eventId, const String& param, const String& value, const Bool timed) { MURL_TRACE(0, "%s, %s=%s, timed=%d", eventId.Begin(), param.Begin(), value.Begin(), timed); SInt32 ret; mJniBridge->CallStaticJavaIntFunc<jstring, jstring, jstring, jboolean>("FlurryJniBridge.logEvent", ret, eventId, param, value, timed); return ret; } SInt32 App::FlurryControlAndroid::LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed) { MURL_TRACE(0, "%s, parametersCount=%d, timed=%s", eventId.Begin(), parameters.GetCount(), timed ? "TRUE" : "FALSE"); SInt32 size = parameters.GetCount(); for (SInt32 i = 0; i < size; i++) { const String& key = parameters.GetKey(i); const String& value = parameters[i]; mJniBridge->CallStaticJavaProc<jstring, jstring, jstring>("FlurryJniBridge.logEventPrepare", eventId, key, value); } SInt32 ret; mJniBridge->CallStaticJavaIntFunc<jstring, jboolean>("FlurryJniBridge.logEvent", ret, eventId, timed); return FlurryEventRecorded; } void App::FlurryControlAndroid::EndTimedEvent(const String& eventId) { MURL_TRACE(0, "%s", eventId.Begin()); mJniBridge->CallStaticJavaProc<jstring>("FlurryJniBridge.endTimedEvent", eventId); } void App::FlurryControlAndroid::EndTimedEvent(const String& eventId, const String& param, const String& value) { MURL_TRACE(0, "%s, %s=%s", eventId.Begin(), param.Begin(), value.Begin()); mJniBridge->CallStaticJavaProc<jstring, jstring, jstring>("FlurryJniBridge.endTimedEvent", eventId, param, value); } void App::FlurryControlAndroid::EndTimedEvent(const String& eventId, const Map<String, String>& parameters) { MURL_TRACE(0, "%s, parametersCount=%d", eventId.Begin(), parameters.GetCount()); SInt32 size = parameters.GetCount(); for (SInt32 i = 0; i < size; i++) { const String& key = parameters.GetKey(i); const String& value = parameters[i]; mJniBridge->CallStaticJavaProc<jstring, jstring, jstring>("FlurryJniBridge.logEventPrepare", eventId, key, value); } mJniBridge->CallStaticJavaProc<jstring>("FlurryJniBridge.endTimedEvent", eventId); } void App::FlurryControlAndroid::OnError(const String& errorId, const String& message) { MURL_TRACE(0, "%s, %s", errorId.Begin(), message.Begin()); mJniBridge->CallStaticJavaProc<jstring, jstring>("FlurryJniBridge.onError", errorId, message); } void App::FlurryControlAndroid::OnPageView() { mJniBridge->CallStaticJavaProc("FlurryJniBridge.onPageView"); MURL_TRACE(0, ""); } // ************************** INIT / DEINIT ***************************************************** Bool App::FlurryControlAndroid::Init(IPlatform* platform) { Platform::Android::NativePlatform* nativePlatform = dynamic_cast<Platform::Android::NativePlatform*>(platform); if (nativePlatform == 0) { MURL_TRACE(0, "Failed to get Android platform handle."); return false; } mJniBridge = nativePlatform->GetJniBridge(); if (mJniBridge == 0) { MURL_TRACE(0, "Failed to get JNI bridge."); return false; } return true; } Bool App::FlurryControlAndroid::DeInit() { mJniBridge = 0; return true; }
We list the new file in the Makefile as SRC file and depending on the platform, we either use the Android version or the default version.
We now can create an Android version of our app which utilizes the Flurry Analytics SDK. The Android implementation prints the following output in the Android Device Monitor:
D/Flurry(12945): FlurryControl::onCreate(): Init Session D/Flurry(12945): FlurryControl::onStart(): Start Session D/Flurry(12945): FlurryJniBridge::logEvent(): eventId=test log event 001 timed=false D/Flurry(12945): FlurryJniBridge::logEvent(): eventId=test log event 002 param=param 01 value=value 01 timed=false D/Flurry(12945): FlurryJniBridge::logEvent(): eventId=test log event 003 timed=false D/Flurry(12945): FlurryJniBridge::logEvent(): eventId=test log event t01 timed=true D/Flurry(12945): FlurryJniBridge::logEvent(): eventId=test log event t02 param=param 01 value=value 01 timed=true D/Flurry(12945): FlurryJniBridge::logEvent(): eventId=test log event t03 timed=true D/Flurry(12945): FlurryJniBridge::onError(): errorId=error 01, test error D/Flurry(12945): FlurryJniBridge::onPageView() D/Flurry(12945): FlurryJniBridge::endTimedEvent(): eventId=test log event t01 D/Flurry(12945): FlurryJniBridge::endTimedEvent(): eventId=test log event t02, param 02=value 02 D/Flurry(12945): FlurryJniBridge::endTimedEvent(): eventId=test log event t03 D/Flurry(12945): FlurryControl::onStop(): End Flurry session
The events also correctly appear in the Flurry Analytics Dashboard. Please note that it can take up to 24 hours for the events to show up.
Proguard
To be able to create a release build, we additionally have to configure Proguard via a text file.
source/android/proguard/proguard_flurry.txt
The text file is also added to the Makefile.
- Note
- The use of the "Google Play Service Library" and the Android "v4/v7 Support Library" is not necessary for analytics. Anyway, if desired both libraries may also be added to the project.
iOS Version
The following steps are necessary to add the Flurry Analytics SDK for iOS.
SDK
At first we have to add the Flurry iOS SDK files to our iOS project. We copy the files into the directory ios
.
source/ios/Flurry.h
source/ios/libFlurry_6.3.0.a
The easiest way to add the files to the project is by using the Dashboard and the command "Update Project". Additionally the library has to be added to the targets in Xcode.
- Add target membership for the file
libFlurry_6.3.0.a
System Libraries
The Flurry SDK requires the following iOS system libraries:
- Security.framework
- SystemConfiguration.framework
These can be added to the project with:
- Targets -> Build Phases -> Link Binary With Libraries -> Add
CPP Implementation
Now we can create the iOS implementation in C++ and Obj-C for our FlurryControl
.
source/flurry/flurry_control_ios.h
source/flurry/flurry_control_ios.mm
#include "flurry_control.h" namespace Murl { namespace App { class FlurryControliOS : public FlurryControl { public: static FlurryControl* Create(const String& name); FlurryControliOS(const String& name) : FlurryControl(name) {} virtual ~FlurryControliOS() {} virtual SInt32 LogEvent(const String& eventId, const Bool timed); virtual SInt32 LogEvent(const String& eventId, const String& param, const String& value, const Bool timed); virtual SInt32 LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed); virtual void EndTimedEvent(const String& eventId); virtual void EndTimedEvent(const String& eventId, const String& param, const String& value); virtual void EndTimedEvent(const String& eventId, const Map<String, String>& parameters); virtual void OnError(const String& errorId, const String& message); virtual void OnPageView(); protected: virtual Bool AppFinishLaunching(void* launchOptions); void Configure(); }; } }
// Copyright 2015 Spraylight #include "flurry_control_ios.h" #include "../ios/Flurry.h" #import <Foundation/Foundation.h> using namespace Murl; App::FlurryControl* App::FlurryControl::Create(const String& name) { return new App::FlurryControliOS(name); } Murl::SInt32 App::FlurryControliOS::LogEvent(const String& eventId, const Bool timed) { NSString* nsString = [NSString stringWithUTF8String : eventId.Begin()]; SInt32 ret = [Flurry logEvent : nsString timed:timed]; return ret; } Murl::SInt32 App::FlurryControliOS::LogEvent(const String& eventId, const String& param, const String& value, const Bool timed) { NSString* nsString = [NSString stringWithUTF8String : eventId.Begin()]; NSDictionary *nsDictionary = [NSDictionary dictionaryWithObjectsAndKeys : [NSString stringWithUTF8String :value.Begin()], [NSString stringWithUTF8String : param.Begin()], nil]; SInt32 ret = [Flurry logEvent : nsString withParameters: nsDictionary timed: timed]; return ret; } Murl::SInt32 App::FlurryControliOS::LogEvent(const String& eventId, const Map<String, String>& parameters, const Bool timed) { SInt32 size = parameters.GetCount(); if (size == 0) { return LogEvent(eventId, timed); } NSString* nsString = [NSString stringWithUTF8String : eventId.Begin()]; NSMutableDictionary *nsDictionary = [NSMutableDictionary dictionary]; for (SInt32 i = 0; i < size; i++) { const String& key = parameters.GetKey(i); const String& value = parameters[i]; [nsDictionary setObject : [NSString stringWithUTF8String : value.Begin()] forKey : [NSString stringWithUTF8String : key.Begin()]]; } SInt32 ret = [Flurry logEvent : nsString withParameters: nsDictionary timed: timed]; return ret; } void App::FlurryControliOS::EndTimedEvent(const String& eventId) { NSString* nsString = [NSString stringWithUTF8String : eventId.Begin()]; [Flurry endTimedEvent: nsString withParameters: nil]; } void App::FlurryControliOS::EndTimedEvent(const String& eventId, const String& param, const String& value) { NSString* nsString = [NSString stringWithUTF8String : eventId.Begin()]; NSDictionary *nsDictionary = [NSDictionary dictionaryWithObjectsAndKeys : [NSString stringWithUTF8String :value.Begin()], [NSString stringWithUTF8String : param.Begin()], nil]; [Flurry endTimedEvent: nsString withParameters: nsDictionary]; } void App::FlurryControliOS::EndTimedEvent(const String& eventId, const Map<String, String>& parameters) { SInt32 size = parameters.GetCount(); if (size == 0) { return EndTimedEvent(eventId); } NSString* nsString = [NSString stringWithUTF8String : eventId.Begin()]; NSMutableDictionary *nsDictionary = [NSMutableDictionary dictionary]; for (SInt32 i = 0; i < size; i++) { const String& key = parameters.GetKey(i); const String& value = parameters[i]; [nsDictionary setObject : [NSString stringWithUTF8String : value.Begin()] forKey : [NSString stringWithUTF8String : key.Begin()]]; } [Flurry endTimedEvent: nsString withParameters: nsDictionary]; } void App::FlurryControliOS::OnError(const String& errorId, const String& message) { NSString* nsStringErr = [NSString stringWithUTF8String : errorId.Begin()]; NSString* nsStringMsg = [NSString stringWithUTF8String : message.Begin()]; NSDictionary *nsDictionary = @{ NSLocalizedDescriptionKey : nsStringMsg }; NSError* nsError = [NSError errorWithDomain:@"com.MyCompany.MyApplication.ErrorDomain" code:100 userInfo:nsDictionary]; [Flurry logError:nsStringErr message:nsStringMsg error:nsError]; } void App::FlurryControliOS::OnPageView() { [Flurry logPageView]; } // ************************** INIT / DEINIT ***************************************************** Bool App::FlurryControliOS::AppFinishLaunching(void* launchOptions) { Configure(); // enter your own key here [Flurry startSession:@"XXXXXXXXXXXXXXXXXXXX"]; return true; } void App::FlurryControliOS::Configure() { //Retrieves the Flurry Agent Build Version. //(NSString *) + getFlurryAgentVersion NSString* nsString = [Flurry getFlurryAgentVersion]; String s([nsString UTF8String]); Debug::Trace("Flurry Agent Version %s", s.Begin()); //Generates debug logs to console. //This is an optional method that displays debug information related to the Flurry SDK. display information to the console. //The default setting for this method is NO which sets the log level to FlurryLogLevelCriticalOnly. //When set to YES the debug log level is set to FlurryLogLevelDebug // (void) setDebugLogEnabled: (BOOL) value [Flurry setDebugLogEnabled: true]; //Generates debug logs to console. //This is an optional method that displays debug information related to the Flurry SDK to the console. //The default setting for this method is FlurryLogLevelCritycalOnly. // (void) setLogLevel: (FlurryLogLevel) value [Flurry setLogLevel: FlurryLogLevelAll]; //Enable automatic collection of crash reports. //This is an optional method that collects crash reports when enabled. The default value is NO. //(void) setCrashReportingEnabled: (BOOL) value [Flurry setCrashReportingEnabled: true]; //Use this method to allow the capture of custom events. The default value is YES. //(void) setEventLoggingEnabled: (BOOL) value //Explicitly specifies the App Version that Flurry will use to group Analytics data. //(void) setAppVersion: (NSString *) version //Assign a unique id for a user in your app. //(void) setUserID: (NSString *) userID //Use this method to capture the gender of your user. //Only use this method if you collect this information explictly from your user (i.e. - there is no need to set a default value). //Allowable values are "m" or @c @"f" // (void) setGender: (NSString *) gender //Use this method to capture the age of your user. //Only use this method if you collect this information explictly from your user (i.e. - there is no need to set a default value). // (void) setAge: (int) age //Set the location of the session. //(void) setLatitude: (double) latitude longitude: (double) longitude horizontalAccuracy: (float) horizontalAccuracy verticalAccuracy: (float) verticalAccuracy }
We add the new files to the project and adapt the target membership:
- Add target membership for file
flurry_control_ios.mm
- Remove target membership for file
flurry_control_android.cpp
- Remove target membership for file
flurry_control_default.cpp
That finishes the iOS implementation and now also the iOS events are recorded with Flurry Analytics. Please note that it may take up to 24 hours until you can see the events in the Flurry dashboard.