Tutorial #01: Flurry Control

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.

tut0401_flurry_example.png
Example screenshot (Flurry website)

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.

# --- Flurry ---
# requires minimum Android API level 10
# requires MURL_ANDROID_PERMISSIONS += android.permission.INTERNET
# optional MURL_ANDROID_PERMISSIONS += android.permission.ACCESS_NETWORK_STATE
# optional MURL_ANDROID_PERMISSIONS += android.permission.ACCESS_FINE_LOCATION
MURL_ANDROID_TARGET_API_LEVEL := 19
MURL_ANDROID_MINIMUM_API_LEVEL := 10
MURL_ANDROID_PERMISSIONS += android.permission.INTERNET
MURL_ANDROID_PERMISSIONS += android.permission.ACCESS_NETWORK_STATE
MURL_ANDROID_PERMISSIONS += android.permission.ACCESS_FINE_LOCATION
MURL_ANDROID_JAR_PATH := source/android/jar
MURL_ANDROID_JAR_FILES += FlurryAnalytics-5.3.0.jar

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 http://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.

MURL_ANDROID_JAVA_PATH := source/android/java
MURL_ANDROID_JAVA_FILES += at/spraylight/flurry/FlurryControl.java
MURL_ANDROID_JAVA_FILES += at/spraylight/flurry/FlurryConfiguration.java
MURL_ANDROID_CUSTOM_CONTROL_CLASSES += at.spraylight.flurry.FlurryControl

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.

MURL_ANDROID_JAVA_FILES += at/spraylight/flurry/FlurryJniBridge.java
MURL_ANDROID_JNI_CLASSES += at.spraylight.flurry.FlurryJniBridge

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.

ifeq ($(MURL_OS), android)
MURL_MODULE_SRC_FILES += flurry/flurry_control_android.cpp
else
MURL_MODULE_SRC_FILES += flurry/flurry_control_default.cpp
endif

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
#App
-keep class at.spraylight.flurry.FlurryJniBridge { *;}
-keep class at.spraylight.flurry.FlurryControl { *;}
#Flurry
-keep class com.flurry.** { *; }
-dontwarn com.flurry.**
-keepattributes *Annotation*,EnclosingMethod,Signature
-keepclasseswithmembers class * {
public &lt;init&gt;(android.content.Context, android.util.AttributeSet, int);
}

The text file is also added to the Makefile.

MURL_ANDROID_PROGUARD_PATH := source/android/proguard
MURL_ANDROID_PROGUARD_FRAGMENTS += proguard_flurry.txt
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.

tut0401_flurry_control.png
FlurryControl


Copyright © 2011-2017 Spraylight GmbH.