Tutorial #02: Flurry Control

Im vorherigen Tutorial wurden "Custom Controls" mit einem einfachen Beispiel vorgestellt. In diesem Tutorial gehen wir einen Schritt weiter und implementieren Flurry Analytics als Custom Control.

Flurry Analytics ist ein kostenloses Analyse Service, mit dem die App-Performance gemessen, überwacht und analysiert werden kann. Für das Tutorial wurden die zum Erstellzeitpunkt letztgültigen SDKs "Flurry Android SDK vAndroid SDK 5.3.0" und "Flurry iPhone SDK viPhone SDK 6.3.0" verwendet.

Zu beachten
Hinweis: Wir verwenden in diesem Tutorial lediglich die Analytics Funktion und verzichten auf die Verwendung von Flurry Ads.

Quicklinks zu den einzelnen Abschnitten in diesem Tutorial:

Back-End

Damit Flurry als Analyseservice verwendet werden kann, muss die App zunächst am Flurry Server eingerichtet werden. Dafür muss auf dev.flurry.com ein Konto erstellt und für Android und iOS je ein getrenntes Projekt angelegt werden. Die eindeutigen ProjectApiKeys werden dann in den entsprechenden Konfigurationsdateien eingetragen.

tut0401_flurry_example.png
Beispiel Screenshot (Flurry Website)

Flurry Interface

Zunächst definieren wir mit einer abstrakten Klasse das Interface für unser Flurry-Control, also die C++ Methoden, mit denen wir auf das Flurry SDK zugreifen wollen.

Um die Projektstruktur übersichtlich zu halten, speichern wir alle Flurry relevanten Dateien im Unterordner flurry ab.

  • 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__

Wie zuvor deklarieren wir eine statische Create Methode und einige pure virtual Methoden um Events, Fehler und Page-Views zu loggen. Der Funktionsparameter timed wird als Default-Parameter mit dem Standardwert false definiert. Die Angabe dieses Parameters ist beim Aufruf der Methode daher optional.

Default Implementierung

Als nächstes implementieren wir eine Default-Implementierung (FlurryControlDefault), die lediglich eine passende Debug Meldung ausgibt.

  • 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, "");
}

Logik-Code

Mit der Default-Implementierung können wir ein FlurryControl Objekt instanzieren und in unserer App verwenden.

#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);
}

Die Default-Implementierung liefert folgende Ausgabe.

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

Im nächsten Schritt integrieren wir das "Flurry Analytics Android SDK" für die Android Version unserer App. Dafür sind folgende Schritte notwendig.

JAR-Archiv hinzufügen

Zunächst müssen wir das JAR-Archiv FlurryAnalytics_5.3.0.jar zu unserem Projekt hinzufügen.

Wir speichern das JAR-Archiv in einem eigenen Verzeichnis android/jar.

  • source/android/jar/FlurryAnalytics-5.3.0.jar
Zu beachten
Im Makefile kann nur eine Pfadangabe für JAR-Dateien gemacht werden. Es ist daher sinnvoll ein eigenes Verzeichnis dafür anzulegen und alle JAR-Archive dort zu speichern. Dasselbe gilt für Java Files, Proguard Fragmente, Manifest Fragmente etc.

Die Datei listen wir mit MURL_ANDROID_JAR_PATH und MURL_ANDROID_JAR_FILES im Common Makefile module_flurry.mk auf. Zusätzlich definieren wir die notwendigen Berechtigungen für das 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

Initialisieren

Das Flurry SDK fordert, dass der FlurryAgent in den App-Methoden onCreate(), onStart() und onStop() passend aufgerufen werden muss. Das Interface MurlCustomControl kann verwendet werden, um Callbacks auf diese App-Methoden zu erhalten.

Wir erstellen also die Java-Klasse FlurryControl und implementieren das Interface MurlCustomControl. Für die Konfiguration des Flurry SDKs erstellen wir auch noch die Klasse FlurryConfiguration.

  • 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();
    }
}

Wir listen die neuen Dateien wieder im Makefile auf und registrieren die neue Klasse als MurlCustomControl Klasse.

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

Als nächstes definieren wir unsere JNI-Bridge Klasse mit den statischen Java-Methoden, die von C++ aus aufgerufen werden können.

  • 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();
    }
}

Wir listen die neue Datei im Common Makefile und registrieren die Klasse als JNI-Klasse.

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

CPP Implementierung

Nun können wir die Android Implementierung in C++ für unser FlurryControl erstellen und die entsprechenden JNI-Bridge Methoden aufrufen.

#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;
}

Wir listen die neue Datei im Makefile als SRC-File, wobei abhängig von der Plattform die Android- oder die Default-Version verwendet wird.

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

Damit können wir eine Android-Version unserer App erstellen, die das Flurry Analytics SDK verwendet. Die Android Implementierung liefert folgende Ausgabe im 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

Die Events sollten auch im Flurry Analytics Dashboard aufscheinen. Das kann allerdings bis zu 24h dauern.

Proguard

Für einen Release-Build müssen wir noch Proguard über eine Textdatei konfigurieren.

  • 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);
}

Die Textdatei wird ebenfalls im Makefile gelistet.

MURL_ANDROID_PROGUARD_PATH := source/android/proguard
MURL_ANDROID_PROGUARD_FRAGMENTS += proguard_flurry.txt
Zu beachten
Die Verwendung der "Google Play Service Library" und der Android "v4/v7 Support Library" ist für Analytics nicht notwendig. Wenn gewünscht, können auch diese beiden Bibliotheken dem Projekt hinzugefügt werden.

iOS Version

Um das Flurry Analytics SDK für die iOS Version zu implementieren, sind folgende Schritte notwendig.

SDK

Zunächst müssen wir die Flurry iOS SDK Dateien dem iOS Projekt hinzufügen. Dafür kopieren wir diese in das Verzeichnis ios.

  • source/ios/Flurry.h
  • source/ios/libFlurry_6.3.0.a

Die Dateien können am einfachsten mit dem Dashboard und dem Befehl "Update Project" hinzugefügt werden. In Xcode muss dann noch die Bibliothek zu den Targets hinzugefügt werden:

  • Target Membership für die Datei libFlurry_6.3.0.a hinzufügen

Bibliotheken

Das Flurry SDK benötigt folgende iOS System Bibliotheken:

  • Security.framework
  • SystemConfiguration.framework

Diese können mit

  • Targets -> Build Phases -> Link Binary With Libraries -> Add

dem Projekt hinzugefügt werden können.

CPP Implementierung

Nun können wir die iOS Implementierung in C++ bzw. Obj-C für unser FlurryControl erstellen.

  • 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
}

Wir fügen die neuen Dateien dem Projekt hinzu und passen die "Target Membership" an:

  • Target Membership für die Datei flurry_control_ios.mm hinzufügen
  • Target Membership für die Dateien flurry_control_android.cpp entfernen
  • Target Membership für die Dateien flurry_control_default.cpp entfernen

Damit ist die iOS Implementierung abgeschlossen und die Events werden mit Flurry Analytics aufgezeichnet. Es kann bis zu 24h dauern, bis die Events im Dashboard aufscheinen.

tut0401_flurry_control.png
Flurry Control Test-App


Copyright © 2011-2018 Spraylight GmbH.