Skip to content

Setting Up an Application

Set up a server-side application and initialize the offline store.

Setting Up Server-Side Application

You must set up a server-side application on SAP Business Technology Platform before an offline application that uses the framework on a client device can access the SAP Business Technology Platform and the back end.

Note

A supported version of an OData service must be available and accessible by SAP Business Technology Platform (such as Netweaver Gateway, Integration Gateway, SAP HANA, or a third-party service such as Apache Olingo). See Version Support and Limitations for details on Version 2.0 and Version 4.0 support.

  1. Determine your data requirements:
    1. Identify the portions of your application that are to operate offline, and the OData entity sets to be referenced.
    2. Compile the list of entity sets into an initial list of defining queries. Defining queries identify the entities and relationships that are required to operate offline.
    3. Reduce the overall data transmitted to the client. Often an application requires only a subset of the data exposed through the OData service. Review each defining query to determine if you can reduce the amount of data by using the $select or $filter OData query options, or other options. Add those OData URI primitives to the defining query.
    4. Identify shareable defining queries. For security or performance reasons an OData service may filter the results that are visible to the user that is logged in. In other cases, the result set may be the same for all users of the application. Such defining queries can be marked as shared on SAP Business Technology Platform, which optimizes back-end requests. Review your defining queries and identify any that meet this "shared" definition.
  2. Create the application configuration (.ini) file (only if non-default behavior is needed) for the server-side application.

    An application configuration file consists of one or more endpoints, each of which can be optionally followed by one or more defining queries:

    1. Endpoint – identifies and determines how OData services from which the offline application retrieves data and stores offline data is processed by SAP Business Technology Platform: how the offline store is prepopulated before it is downloaded to the client, indexed, and so on. See Application Configuration File for further details.
    2. Defining Query – defines various characteristics of how the retrieved data for a given endpoint is managed by SAP Business Technology Platform: whether or not the data is shared, refresh interval, delta tracking, and so on. See Defining Queries for further details.
    3. On SAP Business Technology Platform:
    4. Create the application. See: Defining Applications.
    5. Configure the connectivity. See: Defining Connectivity.
    6. Configure offline-specific settings. See: Defining Offline Settings for Applications.
    7. Import offline settings from the application configuration file. All settings have default values, so you'll need to create an application configuration file only if you require non-default behaviors. See: Defining Offline Settings for Applications.

Both application code and configurations on the server side work together to meet your needs.

Working with an Offline Store in Client Application

Working with an offline store in a client application involves the following steps.

  1. Initialize and open an offline store. An offline application can manage multiple stores if required.

    When an offline store is opened for the first time, it will be populated by performing an initial download (which is built into the open operation). For this step, the application must have network connectivity. The application communicates with SAP Business Technology Platform, which collects data from an OData service based on the defining queries, creates the database that stores the data, and pushes that database down to the offline store on the client device.

  2. Perform any necessary operations (create, read, update, and delete) on the data in the offline store.

  3. Send pending modification requests to the mobile service (using the upload operation). For this step, the application must have network connectivity.
  4. Download data from the back end based on defining queries and merge it into the offline store. The application must have network connectivity. See Synchronizing Data for more information.
  5. (Optional) Test the offline store. See ILOData.

Initialization of the Offline Store

An offline store is created for a given service URL and a set of defining queries. The service URL refers to an endpoint in the server-side application that you set up earlier. There should only be one offline store per endpoint. You can have multiple offline stores per application.

The offline store is represented by an OfflineODataProvider class instance constructed using the service URL and a set of defining queries. The provider instance is passed to a service object that is either a DataService class instance, or in the case of proxy classes, an instance of a generated subclass extending DataService. It is used by the offline application to interact with the local OData service and is constructed using an OfflineODataProvider instance which it delegates to for executing requests against offline store.

Note

When you see ... in the code samples in the guide, you need to replace it with your own code to make it work.

In swift source code: First, you need to have an SAPURLSession object in place. During onboarding, you will have access to a valid object via OnboardingContext.sapURLSession. You may also construct a valid object in your own approach.

Second, you need to implement the OfflineODataDelegate in order to get progress updates, offline store state changes, etc. Then, initialize OfflineODataProvider (you need to have the offline store name, mobile service application ID, and service root in place):

// Retrieve the OkHttpClient initialized in the Foundation module for
// authentication, which has the necessary information to communicate
// with the mobile services
OkHttpClient okHttpClient = ClientProvider.get();

// Initialize application context for use by OfflineODataProvider
AndroidSystem.setContext(applicationContext);

// Specify parameters for OfflineODataProvider construction
OfflineODataParameters parameters = new OfflineODataParameters();

// Use server-driven paging with a page size of 100.
// Note that the "server" here refers to the offline store as a local service, not the back-end OData service
parameters.setPageSize(100);

// If you know that the back-end OData service or mobile services supports repeatable requests, enable repeatable requests on the client
// so that Offline OData will generate repeatable request header for each request.
// If none of them support repeatable requests, you should disable this on the client.
// Be aware that disabling repeatable requests can lead to multiple executions of the same requests in some cases.
parameters.setEnableRepeatableRequests(true);

// Enable encryption. See next section for more details
parameters.setStoreEncryptionKey("...");

// Create a new instance of OfflineODataProvider
// with the service URL from the mobile services
OfflineODataProvider offlineODataProvider =
    new OfflineODataProvider(new URL(SERVICE_URL), parameters, okHttpClient, null, null);

// Add defining queries
offlineODataProvider.addDefiningQuery(new OfflineODataDefiningQuery("req1", "/Events", false));
...

// Construct a DataService instance with the provider instance.
// This code uses the generated proxy class EventService, which
// extends DataService.
EventService eventService = new EventService(offlineODataProvider);
class OfflineODataDelegateSample: OfflineODataDelegate {
    public func offlineODataProvider(_: OfflineODataProvider, didUpdateDownloadProgress progress: OfflineODataProgress) {
        /// Handle the progress
        ...
    }

    ...
}

/// Set up parameters
var params = OfflineODataParameters()

/// Assign a store name
params.storeName = ...

/// If you know that the back-end OData service or mobile services support repeatable requests, enable repeatable requests on the client
/// so that Offline OData will generate a repeatable request header for each request.
/// If none of them support repeatable requests, you should disable this on the client.
/// Be aware that disabling repeatable requests can lead to multiple executions of the same requests in some cases.
params.enableRepeatableRequests = true

/// Use server-driven paging with a page size of 100.
/// Note that the "server" here refers to the offline store as a local service, not the back-end OData service
params.pageSize = 100

/// The SAPURLSession object can be fetched from the onboarding context.
/// During onboarding, the SAPURLSession object should have
/// successfully registered to mobile services with an application ID
let sapURLSession = ...

/// Create delegate object
let delegate = OfflineODataDelegateSample()

/// Create OfflineODataProvider object
let offlineODataProvider = OfflineODataProvider(serviceRoot: ..., parameters: params, sapURLSession: sapURLSession, delegate: delegate)

/// Add defining queries
try offlineODataProvider.add(definingQuery: OfflineODataDefiningQuery(name: "req1", query: "/Events", automaticallyRetrievesStreams: false))
...

/// Open the provider with a completion handler. The open() function is asynchronous
offlineODataProvider.open(completionHandler: {(_ error: OfflineODataError?) -> Void in
    if let error = error {
        /// Handle the error
        ...
    } else {
        /// Proceed to use the offline store after it is successfully opened
        ...
    }
})

For android applications, see Authentication for more information about retrieving the OkHttpClient.

The provider needs to be closed if it is no longer needed:

// Close the provider
try {
    offlineODataProvider.close();
} catch (OfflineODataException e) {
    // Handle the error
    ...
}
/// Close the provider
try offlineODataProvider.close()

Offline-specific operations, for example upload and download, are not available from the DataService class. They can be accessed using OfflineODataProvider. Due to potential long execution time, upload, download and open calls can only be invoked asynchronously. Should the operation fail, the error parameter for the completion handler will contain detailed information.

Data Encryption with an Offline Store

Before deploying your app to production, you must enable offline store encryption to protect sensitive business or personal data (although you can omit data encryption for development and testing purposes).

By supplying an encryption key, the offline store is able to provide strong encryption, using the AES 256-bit algorithm, to provide security against skilled and determined attempts to gain access to the data. Encryption does come with a small impact on performance. The scope of the impact depends, in part, on the size of the cache.

When the offline store is opened for the first time, an encryption key must be supplied to enable encryption using the OfflineODataParameters class. Once the offline store is encrypted, the encryption key cannot be recovered from the offline store itself and must be presented on subsequent opens. It is critical to securely store the key.

The encryption key for the offline store can be stored securely in the SecureKeyValueStore. Each time the offline store is opened, the SecureKeyValueStore must also be opened. You should enforce a mobile passcode to protect a mobile app from unauthorized access. The mobile passcode derives an encryption key that secures a SecureKeyValueStore. The passcode is typically required during onboarding, subsequent application launches, and returning to foreground. Neither the passcode nor the derived encryption key for the SecureKeyValueStore is ever stored.

For Android applications, see Foundation Module on Secure Store for more information.

The offline store is encrypted using an AES 256-bit cipher. The encryption key is used as an input to a password-based key derivation function. The length and complexity of the encryption key is critical to ensure that brute force guessing attacks will not succeed.

Consider the following when creating a key:

  • Do not include semicolons in your key.
  • Do not put the key itself in quotes, or the quotes are considered part of the key.
  • Lost or forgotten keys result in completely inaccessible stores.
// Set a strong random password of at least 16 characters
String strongEncryptionKey = ...;

// Assume that we have created and opened a secure key-value store
// when user enters passcode
appSecureKeyValueStore.put("OfflineStoreEncryptionKey", strongEncryptionKey);
var params = OfflineODataParameters()
/// Set a strong random key of at least 16 characters
params.storeEncryptionKey = ...
/// Proceed to open OfflineODataProvider with the parameters
...

On application relaunch, the offline store encryption key can be retrieved as follows:

// Assume that we have opened the secure key-value store as user enters passcode
String strongEncryptionKey = appSecureKeyValueStore.getString("OfflineStoreEncryptionKey");
offlineODataParameters.setStoreEncryptionKey(strongEncyrptionKey);

// Open offline store
...

Default Encryption Support for an Offline Store

When you open an offline store, you can set an encryption key to encrypt it. Now, if you do not explicitly set the encryption key, the offline SDK will generate a random encryption key to encrypt the offline store in order to protect sensitive business or personal data.

The generated encryption key is saved into the Security Key Store of the device and is transparent to your application. If you want to open the offline store outside of the application using other tools, you need to know the generated encryption key. In order to get the encryption key, you need to write code in your application to do so:

...
import com.sap.cloud.mobile.foundation.common.EncryptionUtil;
import android.util.Base64;
...
// Set application context
EncryptionUtil.initialize(AndroidSystem.getRequiredContext());
// Get the default encryption key
String defaultEncryptionKey = Base64.encodeToString(EncryptionUtil.getEncryptionKey("offline.encryption.keystore"), Base64.DEFAULT).trim();
...
...
// Get the default encryption key
let keychainStoragestore = try KeychainStorage.openStore(name: "OfflineOData.EncryptionKeyStore")
let keydata = (try keychainStoragestore.data(for: "key"))!
let defaultEncryptionKey = String(data:keydata, encoding: .utf8)
...

If you don't want to get the default encryption key, you also can upload the offline store to 'mobile services cockpit' using the uploading offline store API. The API supports recreating the offline store using your newly-set encryption key rather than the default (randomly generated) encryption key. You can then download the uploaded offline store and open it using your encryption key. See Uploading Offline Stores on Administration for more information.

If you don't want to encrypt the offline store, you can disable the default encryption key using the __disable_default_encryption_key parameter:

OfflineODataParameters parameters = ...;
// Set other parameters
parameters.setExtraStreamParameters(...);
// Disable default encryption
parameters.setExtraStreamParameters(parameters.getExtraStreamParameters() + ";__disable_default_encryption_key");
// Proceed to open OfflineODataProvider using the parameters
...
var params = OfflineODataParameters()
// Set other parameters
params.extraStreamParameters = ...
// Disable default encryption
params.extraStreamParameters = params.extraStreamParameters! + ";__disable_default_encryption_key"
// Proceed to open OfflineODataProvider using the parameters
...

Multiuser Considerations

If the application is to support multiple users on the same device, you can specify a unique store name and store path for the offline store for the given user. For Android applications, the information for the user providing the passcode can be obtained using the Foundation user API. See User documentation.

To avoid offline store files belonging to different users clashing with each other, you can use OfflineODataParameters to instruct OfflineODataProvider to store the database files under separate directories with user-specific names by leveraging information from User.

// Get userId from User
String userId = ...;

// Get application directory
File filesFolder = appContext.getFilesDir();

// Create a user-specific directory, applicable for initial open
File offlineStoreDir = new File(filesFolder, userId);
if (!offlineStoreDir.exists()) {
    offlineStoreDir.mkdir();
}

OfflineODataParameters offlineODataParameters = new OfflineODataParameters();
// Set user specific offline store name
offlineODataParameters.setStoreName("offlinestore-" + userId);
// Set user specific directory to store offline store files
offlineODataParameters.setStorePath(new URL("file:" + offlineStoreDir.toString()));
offlineODataParameters.setStoreEncryptionKey("...");

offlineODataProvider = new OfflineODataProvider(new URL(SERVICE_URL), offlineODataParameters, okHttpClient, null, null);
/// Set user id
let userId = ...

/// Create directory for offline store files if needed
let fm = FileManager.default
let storeFilePath = getDocumentsDirectory() + "/" + userId
if !fm.fileExists(atPath: storeFilePath) {
    try fm.createDirectory(atPath: storeFilePath, withIntermediateDirectories: false)
}

var params = OfflineODataParameters()

/// Set user-specific store name and path
params.storeName = "offlinestore_" + userId
params.storePath = URL(fileURLWithPath: getDocumentsDirectory() + "/" + userId)
params.storeEncryptionKey = ...

/// Create OfflineODataProvider object with given parameters
let offlineODataProvider = ...

Another common approach is to remove the offline store before opening a new one for a different user.

Regardless of the approach, users should be mindful about uploading pending modification requests before releasing that device to a different user.

The third approach is to use same offline store for every user. In this approach, we need to consider two things:

  1. Server data. If some data is shared by users, including user-specific data, we recommend separating the two kinds of data into different defining queries. During a user switch, the application can remove the user-specific defining queries of the previous user and add new defining queries for the current user. The app then calls download, and after this download, the previous user’s data will be removed and the current user’s data will be downloaded.

  2. Local update. Usually, the previous user’s local change should be uploaded when they hand over the device to the next user. But if the previous user forgets to do so, the application can use the 'UploadOnUserSwitch' functionality to upload the previous user’s work.

    To enable 'UploadOnUserSwitch', enable Allow Upload of Pending Changes from Previous User in the SAP mobile service cockpit first, and then, in Java, call setForceUploadOnUserSwitch(true) and setCurrentUser() of an OfflineODataParameters instance, or, in Swift, set the forceUploadOnUserSwitch property to true and set the 'currentUser' property of the OfflineODataParameters instance. Finally, call the open function as normal.

// Get userId from User
String userId = ...;

OfflineODataParameters offlineODataParameters = new OfflineODataParameters();

// Enable forceUploadOnUserSwitch and set currentUser
offlineODataParameters.setForceUploadOnUserSwitch(true);
offlineODataParameters.setCurrentUser(userId);
...

// Create OfflineODataProvider object with given parameters
offlineODataProvider = new OfflineODataProvider(new URL(SERVICE_URL), offlineODataParameters, okHttpClient, null, null);

// Call open function and process error
offlineODataProvider.open(
    () -> {
        // Open successfully. Notify application for appropriate action
        ...
    },
    (error) -> {
        if (error.code == -10425) {
            String previousUserId = offlineODataProvider.getPreviousUser();
            // Inform previous user to logon and fix issue.
            ...
        }
        ...
    }
);
/// Set user id
let userId = ...

var params = OfflineODataParameters()
params.forceUploadOnUserSwitch = true
params.currentUser = userId
...

/// Create OfflineODataProvider object with given parameters
let offlineODataProvider = ...

offlineODataProvider.open(completionHandler: {(_ error: OfflineODataError?) -> void in
    if let error = error {
        if (error.code == -10425) {
            let previousUserId = offlineODataProvider.getPreviousUser()
            /// Inform previous user to logon and fix issue.
            ...
         }
        ...
    } else {
        /// Proceed to use the offline store after it is successfully opened
        ...
    }
})

The upload function will be called automatically inside the open function when the second user opens the local store. After the pending changes from the previous user are uploaded to SAP Mobile Services, SAP Mobile Services will send them to the OData back end in the previous user's context.

If all changes are successfully executed by the back end, the pending state will be cleared in the local store. If some changes fail at the back end, open will be terminated. The application should use OfflineProvider.getPreviousUser() to notify the previous user and ask them to take over the device and fix the issue. This typically requires the previous user's manual involvement. Or, the application can choose to open the store without forceUploadOnUserSwitch and rollback the changes from the previous user. This should be done with care and is generally not recommended, because the previous user's un-uploaded work will be lost. Instead of rollback, the application can also upload the previous user's work in the current user context. Careful consideration should be used before implementing this process, because it means the back end will receive the previous user's request in the current user's context, which may be a security concern.

Note

UploadOnUserSwitch is not supported if the mobile services server connects to the back end using Basic Authentication single sign-on.

Automatic Client Registration to Mobile Back-End Tools

Client registration to the mobile back-end tools can be made automatic.

Note

This feature is only for mobile back-end tools. If your back end is not mobile back-end tools, please do not enable this feature, or it will fail to initialize the store.

OfflineODataParameters parameters = new OfflineODataParameters();
parameters.setAutoRegisterClient(true);
...
var params = OfflineODataParameters()
params.autoRegisterClient = true
...

Certificate-Based Authentication

If the offline application (for Android applications) is configured to use certificate-based authentication, OfflineODataProvider requires access to the certificate to establish an SSL session with the server. This is accomplished by providing an instance of the SslClientAuth class as the fourth parameter to the constructor. See SSL Client Certificate documentation.


Last update: January 30, 2023