Skip to content

Client Usage

The Client Usage classes allow you to enable your mobile applications to collect and upload client usage data. Administrators can use the SAP Cloud Platform Mobile Services cockpit to view reports or download client usage data and create custom reports using this data. See Usage Reporting in the SAP Cloud Platform Mobile Services documentation for more information on the server and its funtionality.

There are two options for managing sessions:

  • UsageBroker - A higher-level convenience helper that builds on AppUsage. It simplifies the initialization, collection, storage and uploading of Usage data.

    It can be enabled to automatically capture lifecycle events such as "Session/Event start/stop". The UsageBroker is configureable for a multi-user environment within an application.

  • AppUsage - Works with direct access to the lower level objects. This option provides the ability to directly control each component, but requires greater effort to set up and maintain.

    Note

    Only one usage provider should be initialized at a time.

Using the UsageBroker Convenience Helper

To collect, store, and upload client usage event data:

  1. Initialize the broker. We suggest adding this call to the application's onCreate().
  2. Start the broker. Add this call at the earliest opportunity (after collecting the server settings).
  3. Collect any events. Additional analytic data can be captured and reported to the server by using the AppUsage class.
  4. Call Upload (anywhere and often). It will only upload on the requested frequency.

Initialize UsageBroker

1
2
// In your Application class, init the UsageBroker
UsageBroker.initialize(this, true);
1
2
// In your Application class, init the UsageBroker
UsageBroker.initialize(this, true)

Start UsageBroker

1
2
3
4
5
try {
        UsageBroker.start(getApplicationContext(), AppState.getInstance().getSettingsParameters());
    } catch (Exception ex) {
        sLogger.error(ex.getMessage());
    }
1
2
3
4
5
try {
        UsageBroker.start(getApplicationContext(), AppState.getInstance().getSettingsParameters())
    } catch (Exception ex) {
        sLogger.error(ex.getMessage())
    }

Multi-user Support for UsageBroker

When developing a multi-user app, the UsageBroker can be reconfigured on a per-user basis. This introduces the concept of an "unattributed" and "attributed" store. The unattributed store is used to capture all usage events when a user has not been onboarded. The attributed store is used for all usage events once a user has been onboarded successfully.

By calling one of the provided configure() methods, you can reconfigure the UsageBroker to switch between existing and new users. When moving to an attributed user, call the configure() method passing in a UDID. This creates or opens the store associated with this UDID which should uniquely identify a user.

Likewise, to switch to an unattributed user, call one of the configure() overloads that does not specify a UDID. You can also optionally pass in an encryption key when calling configure for an attributed user. This key should remain constant after the initial call otherwise an OpenFailureException is thrown.

The retainLastUnattributedSession parameter defaults to true if not provided when calling configure(). This is useful for scenarios when a user is being onboarded. When configure() is called and you are switching from an unattributed store to an attributed store, this will take all the events from the beginning of the onboarding session and transfer these events to the newly onboarded user's usage store.

1
2
3
4
5
6
// Configure UsageBroker for an attributed user
try {
    UsageBroker.configure(context, uuid);
} catch (OpenFailureException | EncryptionError e) {
    // Handle Error
}
1
2
3
4
5
6
7
try {
    UsageBroker.configure(context, uuid)
} catch (e: OpenFailureException) {
    // Handle error
} catch (e: EncryptionError) {
    // Handle error 
}

Collect any Events

See Reporting Event Metrics for more information about custom events.

1
2
3
4
5
6
7
8
9
// Single event call.
AppUsage.event(
    "eventType1",          // Event type, optional.
    "testEvent1Key",       // Event key. When not specified, 'other' will be used.
    4L,                    // Duration.
    new AppUsageInfo()     // Event metrics using AppUsageInfo.
           .screen("firstScreen")
           .value("randomValue")
           .category("category1");
1
2
3
4
5
6
7
8
9
// Single event call.
AppUsage.event(
        "eventType1", // Event type, optional.
        "testEvent1Key", // Event key. When not specified, 'other' will be used.
        4L, // Duration.
        AppUsageInfo()     // Event metrics using AppUsageInfo.
                .screen("firstScreen")
                .value("randomValue")
                .category("category1"))

See AppUsage Behavior Events for more information about Behavior events.

1
2
3
if(AppUsage.isInitialized()){
    AppUsage.eventBehaviorViewDisplayed("MainActivity", "", "", "");
}
1
2
3
if (AppUsage.isInitialized()) {
    AppUsage.eventBehaviorViewDisplayed("MainActivity", "", "", "")
}
1
2
3
if(AppUsage.isInitialized()){
    AppUsage.eventBehaviorUserInteraction("MainActivity","FloatingActionButton","Button Pressed", "");
}
1
2
3
if (AppUsage.isInitialized()) {
    AppUsage.eventBehaviorUserInteraction("MainActivity", "FloatingActionButton", "Button Pressed", "")
}

Upload through UsageBroker

Uploading through the UsageBroker gives you convenience mechanisms that are not available when using the underying components. The UsageBroker can control upload frequency as well as the ability to upload an attributed and unattributed usage store together.

When uploading through the UsageBroker, the sendUnattributed flag is set to true by default unless specified in the appropriate method call. The sendUnattributed flag determines whether to upload the unattributed store. If currently on an attributed store, the flag uploads both the attributed store and the unattributed store. If currently configured for the unattributed store, this flag has no effect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
UsageBroker.upload(false, new AppUsageUploader.UploadListener() {
        @Override
        public void onSuccess() {
                sLogger.debug("Usage upload complete, time taken (in nanos): " + (System.nanoTime() - startTime));
        }

        @Override
        public void onError(Throwable error) {
                Toast.makeText(getActivity(), "Upload Err:\n"+error.getMessage(), Toast.LENGTH_LONG).show();
                if (error instanceof HttpException) {
                        sLogger.debug("Upload server error: {}, code = {}",
                                ((HttpException) error).message(), ((HttpException) error).code());
                } else {
                        sLogger.debug("Upload error: {}", error.getMessage());
                }
        }

        @Override
        public void onProgress(int i) {
                sLogger.debug("Usage upload progress: " + i);
        }
}, true);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
UsageBroker.upload(false, object : AppUsageUploader.UploadListener {
        override fun onSuccess() {
                sLogger.debug("Usage upload complete, time taken (in nanos): " + (System.nanoTime() - startTime))
        }

        override fun onError(error: Throwable) {
                Toast.makeText(activity, "Upload Err:\n" + error.message, Toast.LENGTH_LONG).show()
                if (error is HttpException) {
                    sLogger.debug("Upload server error: {}, code = {}",
                            error.message(), error.code())
                } else {
                    sLogger.debug("Upload error: {}", error.message)
                }
        }

        override fun onProgress(i: Int) {
                sLogger.debug("Usage upload progress: $i")
        }
}, true)

Using Lower Level Components

Sessions

For the purposes of reporting, define a user session as the duration the mobile application is in the foreground. The session acts as a container for event metrics data. In addition, it contains information such as:

  • Device Platform (Android/IOS)
  • Device Platform version
  • Device Id
  • Application Id
  • Application Version

Event Metrics Data

An application can report any event metrics data using AppUsageInfo. Mobile Services supports a standard schema to collect event metrics. The values for the metrics and how to interpret them for a custom report is up to your mobile application designers and product managers.

The AppUsageInfo class consists of the following fields and can collect metrics for the following events:

Field Description
Type The event type
Key The key to identify the event
Action The action associated with the event
Behaviour User-defined behavior properties of the event
Category User-defined event category type
Duration The duration of the event in milli-seconds
Element The SDK/UI element associated with the event
Measurement User-defined measurement of the event such as UI element size
Others Any other user-defined properties associated with the event
Result User-defined result associated with the event. This is usually used with 'Action'.
Screen The properties of the screen
Case User-defined case
Unit The unit associated with 'Measurement'
Value User-defined value properties
View Android View related properties

Reporting

During the reporting phase, client data is collected and persisted in an encrypted store on the device. The data can then be uploaded to Mobile Services server and viewed in client usage reports. See the uploading section in this topic for more information.

Reporting involves:

  1. Initializing the AppUsage class
  2. Recording session start
  3. Reporting event metrics
  4. Recording session end

Initializing AppUsage

A one-time initialization is required. An encryption key is required to encrypt the underlying persistence store.

SAP recommends that you use the encryption utility to obtain the encryption key. If it is the first time AppUsage is being configured and null is provided as the encryption key, a key is generated transparently and is used for subsequent configure calls. See Encryption utility for more information.

This must happen before any AppUsage related calls are done.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Application Id and version in SettingsParameter will be used during reporting and uploading to form Mobile Service URL.
// Encryption key is used to encrypt the underlying persistence store.
SettingsParameters settingsParameters = null;

try {
    settingsParameters = new SettingsParameters(
            "https://mobilepreview-yourAccount.hana.ondemand.com/",// CPMS server base URL
            "appId",   // CPMS app ID
            "deviceId",// This device's ID
            "1.0");    // CPMS app version
} catch (MalformedURLException ex) {
    // If serverBaseUrl is malformed.
}

byte[] encrytionKey = EncryptionUtil.getEncryptionKey("aliasForAppUsage", passcodeFromUser);

try {
    AppUsage.initialize(appContext, "myUsageStore", settingsParameters, encrytionKey);

    // Or passes null and the AppUsage will generate an encryption key to encrypt the persistence store
    // and subsequent 'initialize' calls.
    //      AppUsage.initialize(appContext, "myUsageStore", settingsParameters, null);
} catch (OpenFailureException ex) {
    logger.error("Failed to open Usage store.", ex);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Application Id and version in SettingsParameter will be used during reporting and uploading to form Mobile Service URL.
// Encryption key is used to encrypt the underlying persistence store.
var settingsParameters: SettingsParameters? = null

try {
    settingsParameters = SettingsParameters(
            "https://mobilepreview-yourAccount.hana.ondemand.com/", // CPMS server base URL
            "appId", // CPMS app ID
            "deviceId", // This device's ID
            "1.0")    // CPMS app version
} catch (ex: MalformedURLException) {
    // If serverBaseUrl is malformed.
}

val encrytionKey = EncryptionUtil.getEncryptionKey("aliasForAppUsage", passcodeFromUser)

try {
    AppUsage.initialize(appContext!!, "myUsageStore", settingsParameters!!, encrytionKey)

    // Or passes null and the AppUsage will generate an encryption key to encrypt the persistence store
    // and subsequent 'initialize' calls.
    //      AppUsage.initialize(appContext, "myUsageStore", settingsParameters, null);
} catch (ex: OpenFailureException) {
    logger.error("Failed to open Usage store.", ex)
}

Recording the Session Start

Reporting must begin with a sessionStart and end with a sessionEnd. Any calls to event reporting without a sessionStart are ignored. Event metrics reporting using eventStart, eventEnd, and event must happen after calling sessionStart and before sessionEnd is called.

A call to sessionStart marks the start of a session with the session-related attributes such as "Platform" and "Platform Version". Consecutive calls to sessionStart without calling sessionEnd ends the current session and creates a new session.

1
AppUsage.sessionStart();
1
AppUsage.sessionStart()

Reporting Event Metrics

After a sessionStart is called, one or more event records can be reported. Events can be reported using single call to event() or a pair of eventStart() and eventEnd() calls. In the single call, your application is responsible for providing duration information if applicable. In the paired call, the event duration is automatically calculated.

A string value is accepted for each of the event metrics field. It is up to you to decide what value should be recorded in each of the fields.

1
2
3
AppUsage.eventStart("filterEventType", "productFilter",
        new AppUsageInfo()
                .screen("Product Filter"));  // Uses one or more of the event metrics methods-- screen, element, view, category, etc.
1
2
3
AppUsage.eventStart("filterEventType", "productFilter",
        AppUsageInfo()
                .screen("Product Filter"))  // Uses one or more of the event metrics methods-- screen, element, view, category, etc.
1
2
3
AppUsage.eventEnd("filterEventType", "productFilter",
    new AppUsageInfo()
        .screen("Product Filter"));
1
2
3
AppUsage.eventEnd("filterEventType", "productFilter",
        AppUsageInfo()
                .screen("Product Filter"))
1
2
3
4
5
6
7
8
9
// Single event call.
AppUsage.event(
    "eventType1",          // Event type, optional.
    "testEvent1Key",       // Event key. When not specified, 'other' will be used.
    4L,                    // Duration.
    new AppUsageInfo()     // Event metrics using AppUsageInfo.
           .screen("firstScreen")
           .value("randomValue")
           .category("category1");
1
2
3
4
5
6
7
8
9
// Single event call.
AppUsage.event(
        "eventType1", // Event type, optional.
        "testEvent1Key", // Event key. When not specified, 'other' will be used.
        4L, // Duration.
        AppUsageInfo()     // Event metrics using AppUsageInfo.
                .screen("firstScreen")
                .value("randomValue")
                .category("category1"))

AppUsage Behavior Events

Analytic Behavior events have been introduced to simplify capturing two types of events in a consistant manner. They have been grouped into two categories: "View Displayed" and "User Interaction".

  • The "View Displayed" method is called by the developer to record that a particular UI control is displayed (MainActivity, for example). It only requires the view name/id.
    1
     AppUsage.eventBehaviorViewDisplayed(...)
    
1
2
3
if(AppUsage.isInitialized()){
    AppUsage.eventBehaviorViewDisplayed("MainActivity", "", "", "");
}
1
2
3
if (AppUsage.isInitialized()) {
    AppUsage.eventBehaviorViewDisplayed("MainActivity", "", "", "")
}
  • The "User Interaction" method is called to record some type of user interaction (for example, a button click or selecting a row).
    1
     AppUsage.eventBehaviorUserInteraction(...)
    
1
2
3
if(AppUsage.isInitialized()){
    AppUsage.eventBehaviorUserInteraction("MainActivity","FloatingActionButton","Button Pressed", "");
}
1
2
3
if (AppUsage.isInitialized()) {
    AppUsage.eventBehaviorUserInteraction("MainActivity", "FloatingActionButton", "Button Pressed", "")
}

Recording the Session End

A call to sessionEnd marks the end of a session.

1
AppUsage.sessionEnd();
1
AppUsage.sessionEnd()

Uploading

Before using AppUsageUploader, enable the upload of application-specific usage statistics and reports from mobile devices through the Mobile Services cockpit. See Defining Usage Report Policy.

AppUsageUploader retrieves the usage data reported prior to the last session end from the underlying persistence store, converts them to the format required by the mobile service, and then uploads them.

Any events that are in the current open session are uploaded on the next upload call.

Registering the Upload Listener

To be notified whether the upload succeeds or fails, or to get upload progress, implement AppUsageUploader.UploadListener. This is an optional but recommended step.

Note that all callback methods are invoked in UI(main) thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// All callback methods will be invoked in UI(main) thread.
class MyUploadListener implements AppUsageUploader.UploadListener {
    @Override
    public void onSuccess() {
        logger.debug("Usage Upload completed successfully");

        // For example, put a toast message.
    }

    @Override
    public void onError(Throwable error) {
        if (error instanceof HttpException) {
            logger.debug("Usage Upload server error: {}, code = {}", ((HttpException) error).getMessage(),
                    ((HttpException) error).getCause());
        } else {
            logger.debug("Usage Upload error: {}", error.getMessage());
        }
    }

    @Override
    public void onProgress(int percentage) {
        // For example, updates the progress dialog/bar.
    }
}

MyUploadListener myUploadListener = new MyUploadListener();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
internal var myUploadListener = MyUploadListener()

// All callback methods will be invoked in UI(main) thread.
internal inner class MyUploadListener : AppUsageUploader.UploadListener {
    override fun onSuccess() {
        logger.debug("Usage Upload completed successfully")

        // For example, put a toast message.
    }

    override fun onError(error: Throwable) {
        if (error is HttpException) {
            logger.debug("Usage Upload server error: {}, code = {}", error.message,
                    error.cause)
        } else {
            logger.debug("Usage Upload error: {}", error.message)
        }
    }

    override fun onProgress(percentage: Int) {
        // For example, updates the progress dialog/bar.
    }
}

Set the listener by calling:

1
AppUsageUploader.setListener(myUploadListener);
1
AppUsageUploader.setListener(myUploadListener)

Starting the Upload

To start the upload, call upload from the UI(main) thread. All usage data retrieval and uploading is performed in an AsyncTask.

Note

Make sure the mobile application has already authenticated with the server before the upload call. This call requires authentication.

1
AppUsageUploader.upload(okHttpClient); // okHttpClient was used for server authentication.
1
AppUsageUploader.upload(ClientProvider.get()) // okHttpClient was used for server authentication.

Viewing Usage Reports on the Server

After a successful client data upload, you can view the data in the Mobile Services cockpit. See Viewing Client Data Report.