Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Dan_vL
Product and Topic Expert
Product and Topic Expert
Previous   Home   Next


The Kapsel Offline OData plugin enables an OData version 2.0 based application that has its data proxied through an SMP or SAP Cloud Platform Mobile Services server to be used when a device or emulator is offline by creating a store on the device using the code below.
var properties = {
"name": "ProductsOfflineStore",
"host": applicationContext.registrationContext.serverHost,
"port": getServerPort(),
"https": applicationContext.registrationContext.https,
"serviceRoot": appId,
"definingRequests": {
"ProductsDR": "/Products",
"SuppliersDR": "/Suppliers"
}
};
store = sap.OData.createOfflineStore(properties);
store.open(openStoreSuccessCallback, errorCallback);

A set of defining requests specify the data that will populate the offline store.
Once the store is open and the custom OData client is applied with the following call, all calls made using the datajs library that match the service root (connection name for the endpoint such as com.kapsel.gs) are routed to the offline store.
sap.OData.applyHttpClient();
//sap.OData.removeHttpClient();

The data that will be updated when a device is offline should ideally be data that multiple offline users are not simultaneously updating. If possible, partition the data to the subset required by each user. Adding OData filters to the defining requests to limit the data on the device to the subset of data required is one technique to do this.

The offline store has a 16 GB limit. The initial download time, database size and refresh times can be affected by the amount of data being stored on the device.

An offline store can only be used with one OData service. For example, one offline store could be associated with one service such as
http://services.odata.org/V2/OData/OData.svc/
or
http://services.odata.org/V2/Northwind/Northwind.svc/
but not both.
Multiple stores can however exist on a device.

Once the offline store has been created, local changes made to it can be sent out via a call to store.flush().
The offline store can update its copy of the data by calling store.refresh().

OData function imports are not supported as the service metadata document does not describe what changes occur as a result of calling them. See also Offline OData Version Support for further details on supported features.

For additional details on the Offline OData plugin see Week 5 Unit 1 of Developing Mobile Apps with SAP HANA Cloud Platform, Offline Enablement for SAP Fiori, C:\SAP\MobileSDK3\KapselSDK\docs\api\sap.OData.html or Using the Offline OData Plugin.

The following samples will help demonstrate the Kapsel Offline OData plugin.

Accessing Data When Offline
The Offline OData plugin is added to the project to enable the accessing of OData when device is offline.

Using ILOData to Query an Offline Database
The offline database is copied from a device and then OData queries can be made directly against the database. This can be useful when troubleshooting or for technical support.

Encrypting the Offline Database
A store option named storeEncryptionKey can be provided when creating the offline store to encrypt the data within it.

Offline enabled App with Create, Update, Delete, Access to the Error Archive
Adds CUD operations (create, update, delete) and how to view failed updates in the ErrorArchive.

Error Conditions
Explores a few of the common error conditions that can occur.

Auto Increment Primary Keys
Mentions where to get the temporary primary key value from.

Offline and Online in One App
Demonstrates an app where a portion of the data is made available when offline.

Questions and Answers

Accessing Data When Offline


The following steps will enable the app to be used when the device or simulator is online or offline.

Note, the techniques used in this sample are for learning purposes only. In a non-demo app, it is recommended that the offline store be used all the time to maintain consistency of the data.

  • Add the Cordova network information plugin, and the Kapsel Logger and Offline OData plugins.
    cordova plugin add cordova-plugin-network-information

    cordova plugin add kapsel-plugin-logger --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    or
    cordova plugin add kapsel-plugin-logger --searchpath $KAPSEL_HOME/plugins
    cordova plugin add kapsel-plugin-odata --searchpath $KAPSEL_HOME/plugins


  • The Offline OData plugin requires that the OData source is proxied through a SMP or SAP Cloud Platform Mobile Services server.
    Ensure the rewrite mode is Rewrite URL as shown below. For offline apps it is important that the URI's are routed through an SMP or SAP Cloud Platform Mobile Services server.

  • Create a file named offline.ini.
    Copy the following text into the file.
    [endpoint]
    name=com.kapsel.gs
    allow_omitting_max_length_facet=Y

    Import this file into the management cockpit under the tab OFFLINE CONFIGURATION.
    This setting provides a default value for Edm.String and Edm.Binary key values as this OData service did not provide these values.
    For additional details, see Application Configuration File.

  • Replace www/index.html with the following contents.
    <!DOCTYPE html>
    <html>
    <head>
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
    <script type="text/javascript" charset="utf-8" src="datajs-1.1.2.min.js"></script>
    <script type="text/javascript" charset="utf-8" src="serverContext.js"></script>
    <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
    <script>
    var applicationContext = null;
    var online = false;
    var store = null; //Offline OData store
    var startTime = new Date();
    var initTime = null;
    var unlockTime = null;
    var resumeTime = null;

    window.onerror = onError;

    function onError(msg, url, line) {
    var idx = url.lastIndexOf("/");
    var file = "unknown";
    if (idx > -1) {
    file = url.substring(idx + 1);
    }
    alert("An error occurred in " + file + " (at line # " + line + "): " + msg);
    return false; //suppressErrorAlert;
    }

    function init() {
    updateStatus2("Calling Logon.init");
    initTime = new Date();
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    console.log("EventLogging: Time from onload to deviceready " + duration + " seconds");

    if (sap.Logger) {
    sap.Logger.setLogLevel(sap.Logger.DEBUG); //enables the display of debug log messages from the Kapsel plugins.
    sap.Logger.debug("EventLogging: Log level set to DEBUG");
    }

    if (navigator.notification) { // Override default HTML alert with native dialog. alert is not supported on Windows
    window.alert = navigator.notification.alert;
    }

    register();
    console.log("EventLogging: init completed");
    }

    function logonSuccessCallback(result) {
    updateStatus2("logonSuccessCallback called");
    var endTime = new Date();
    if (unlockTime) {
    var duration = (endTime - unlockTime)/1000;
    console.log("EventLogging: Unlock Time " + duration + " seconds");
    unlockTime = null;
    }

    console.log("EventLogging: logonSuccessCallback " + JSON.stringify(result));
    applicationContext = result;
    showScreen("MainDiv");
    }

    function logonErrorCallback(error) { //this method is called if the user cancels the registration.
    alert("An error occurred: " + JSON.stringify(error));
    if (device.platform == "Android") { //Not supported on iOS
    navigator.app.exitApp();
    }
    }

    function read() {
    updateStatus2("Read request started");
    startTime = new Date();
    if (!applicationContext) {
    alert("Register or unlock before proceeding");
    return;
    }
    clearTable();
    sUrl = applicationContext.applicationEndpointURL + "/CarrierCollection?$format=json&$orderby=carrid"; //JSON format is less verbose than atom/xml
    var oHeaders = {};
    //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId; //not needed as this will be sent by the logon plugin

    var request = {
    headers : oHeaders,
    requestUri : sUrl,
    method : "GET"
    };

    if (device.platform == "windows") { //provided by the authproxy and logon plugins on Android and iOS but not on Windows https://support.wdf.sap.corp/sap/support/message/1680272744
    request.user = applicationContext.registrationContext.user;
    request.password = applicationContext.registrationContext.password;
    }
    OData.read(request, readSuccessCallback, errorCallback);
    }

    function readSuccessCallback(data, response) {
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Read " + data.results.length + " records in " + duration + " seconds");
    var carrierTable = document.getElementById("carrierTable");

    for (var i = data.results.length -1; i >= 0; i--) {
    var row = carrierTable.insertRow(1);
    var cell1 = row.insertCell(0);
    var cell2 = row.insertCell(1);
    cell1.innerHTML = data.results[i].carrid;
    cell2.innerHTML = data.results[i].CARRNAME;
    }
    }

    function clearTable() {
    var carrierTable = document.getElementById("carrierTable");
    while(carrierTable.rows.length > 1) {
    carrierTable.deleteRow(1);
    }
    }

    function errorCallback(e) {
    alert("An error occurred: " + JSON.stringify(e));
    showScreen("MainDiv");
    }

    function register() {
    updateStatus2("Calling Logon.init");
    sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
    }

    function unRegister() {
    showScreen("LoadingDiv");
    updateStatus2("Calling deleteRegistration");
    sap.Logon.core.deleteRegistration(logonUnregisterSuccessCallback, errorCallback);
    clearTable();
    }

    function logonUnregisterSuccessCallback(result) {
    updateStatus2("Successfully Unregistered");
    console.log("EventLogging: logonUnregisterSuccessCallback " + JSON.stringify(result));
    applicationContext = null;
    register();
    }

    function lock() {
    sap.Logon.lock(logonLockSuccessCallback, errorCallback);
    clearTable();
    }

    function logonLockSuccessCallback(result) {
    console.log("EventLogging: logonLockSuccessCallback " + JSON.stringify(result));
    applicationContext = null;
    showScreen("LockedDiv"); //sap.Logon.unlock(function () {},function (error) {}); //alternatively show the unlock screen
    }

    function unlock() {
    unlockTime = new Date();
    sap.Logon.unlock(logonSuccessCallback, errorCallback);
    }

    function managePasscode() {
    sap.Logon.managePasscode(managePasscodeSuccessCallback, errorCallback);
    }

    function managePasscodeSuccessCallback() {
    console.log("EventLogging: managePasscodeSuccess");
    }

    function showScreen(screenIDToShow) {
    var screenToShow = document.getElementById(screenIDToShow);
    screenToShow.style.display = "block";
    var screens = document.getElementsByClassName('screenDiv');
    for (var i = 0; i < screens.length; i++) {
    if (screens[i].id != screenToShow.id) {
    screens[i].style.display = "none";
    }
    }
    }

    function openStore() {
    console.log("EventLogging: openStore");
    startTime = new Date();
    updateStatus2("store.open called");
    var properties = {
    "name": "CarrierOfflineStore",
    "host": applicationContext.registrationContext.serverHost,
    "port": applicationContext.registrationContext.serverPort,
    "https": applicationContext.registrationContext.https,
    "serviceRoot" : appId,
    //"storePath" : cordova.file.externalRootDirectory,

    "definingRequests" : {
    "CarriersDR" : "/CarrierCollection"
    }
    };

    if (device.platform == "windows") {
    var authStr = "Basic " + btoa(applicationContext.registrationContext.user + ":" + applicationContext.registrationContext.password);
    properties.streamParams = "custom_header=Authorization:" + authStr;
    }

    store = sap.OData.createOfflineStore(properties);
    var options = {};
    store.open(openStoreSuccessCallback, errorCallback, options, progressCallback);
    }

    function openStoreSuccessCallback() {
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Store opened in " + duration + " seconds");
    updateStatus1("Store is OPEN.");
    console.log("EventLogging: openStoreSuccessCallback. Stored opened in " + duration);
    sap.OData.applyHttpClient(); //Offline OData calls can now be made against datajs.
    }

    function closeStore() {
    if (!store) {
    updateStatus2("The store must be opened before it can be closed");
    return;
    }
    console.log("EventLogging: closeStore");
    updateStatus2("store.close called");
    store.close(closeStoreSuccessCallback, errorCallback);
    }

    function closeStoreSuccessCallback() {
    console.log("EventLogging: closeStoreSuccessCallback");
    sap.OData.removeHttpClient();
    updateStatus1("Store is CLOSED.");
    updateStatus2("Store closed");
    }

    //Removes the physical store from the filesystem
    function clearStore() {
    console.log("EventLogging: clearStore");
    if (!store) {
    updateStatus2("The store must be closed before it can be cleared");
    return;
    }
    store.clear(clearStoreSuccessCallback, errorCallback);
    }

    function clearStoreSuccessCallback() {
    console.log("EventLogging: clearStoreSuccessCallback");
    updateStatus1("");
    updateStatus2("Store is CLEARED");
    store = null;
    }

    function refreshStore() {
    console.log("EventLogging: refreshStore");
    if (!store) {
    updateStatus2("The store must be open before it can be refreshed");
    return;
    }
    startTime = new Date();
    updateStatus2("Store refresh called");
    store.refresh(refreshStoreCallback, errorCallback, null, progressCallback);
    }

    function refreshStoreCallback() {
    console.log("EventLogging: refreshStoreCallback");
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Store refreshed in " + duration + " seconds");
    }

    function flushStore() {
    console.log("EventLogging: flushStore");
    if (!store) {
    updateStatus2("The store must be open before it can be flushed");
    return;
    }
    startTime = new Date();
    updateStatus2("Store flush called");
    store.flush(flushStoreSuccessCallback, errorCallback, null, progressCallback);
    }

    function flushStoreSuccessCallback() {
    console.log("EventLogging: flushStoreSuccessCallback");
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Store flushed in " + duration + " seconds");
    refreshStore();
    }

    function viewLog() {
    if (sap.Logger) {
    sap.Logger.getLogEntries(getLogEntriesSuccess, errorCallback)
    }
    else {
    alert("ensure the kapsel-logger plugin has been added to the project");
    }
    }

    function getLogEntriesSuccess(logEntries) {
    var stringToShow = "";
    var logArray;

    if (device.platform == "windows") {
    logArray = logEntries.split("\n");
    if (logArray.length > 0) {
    for (var i = 0; i < logArray.length; i++) {
    stringToShow += logArray[i] + "\n";
    }
    }
    }
    else if (device.platform == "iOS") {
    logArray = logEntries.split("\n");
    if (logArray.length > 0) {
    for (var i = 0; i < logArray.length; i++) {
    logLineEntries = logArray[i].split(" ");
    for (var j = 7; j < logLineEntries.length; j++) {
    stringToShow += logLineEntries[j] + " ";
    }
    stringToShow = stringToShow + "\n";
    }
    }
    }
    else { //Android
    logArray = logEntries.split('#');
    if (logArray.length > 0) {
    var numOfMessages = parseInt(logArray.length / 15);
    for (var i = 0; i < numOfMessages; i++) {
    stringToShow += logArray[i * 15 + 1] + ": " + logArray[i * 15 + 3] + ": " + logArray[i * 15 + 14] + "\n";
    }
    }
    }
    alert(stringToShow);
    console.log("EventLogging: Device Log follows " + stringToShow);
    }


    function updateStatus1(msg) {
    document.getElementById('statusID').innerHTML = msg + " " + getDeviceStatusString();
    console.log("EventLogging: " + msg + " " + getDeviceStatusString());
    }

    function updateStatus2(msg) {
    var d = new Date();
    document.getElementById('statusID2').innerHTML = msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds());
    console.log("EventLogging: " + msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds()));
    }

    function addZero(input) {
    if (input < 10) {
    return "0" + input;
    }
    return input;
    }

    function getDeviceStatusString() {
    if (online) {
    return "Device is ONLINE";
    }
    else {
    return "Device is OFFLINE";
    }
    }

    function progressCallback(progressStatus) {
    var status = progressStatus.progressState;
    var lead = "unknown";
    if (status === sap.OfflineStore.ProgressState.STORE_DOWNLOADING) {
    lead = "Downloading ";
    }
    else if (status === sap.OfflineStore.ProgressState.REFRESH) {
    lead = "Refreshing ";
    }
    else if (status === sap.OfflineStore.ProgressState.FLUSH_REQUEST_QUEUE) {
    lead = "Flushing ";
    }
    else if (status === sap.OfflineStore.ProgressState.DONE) {
    lead = "Complete ";
    }
    else {
    alert("Unknown status in progressCallback");
    }
    updateStatus2(lead + "Sent: " + progressStatus.bytesSent + " Received: " + progressStatus.bytesRecv + " File Size: " + progressStatus.fileSize);
    }

    function deviceOnline() {
    online = true;
    updateStatus1("");
    }

    function deviceOffline() {
    online = false;
    updateStatus1("");
    }


    function onLoad() {
    console.log("EventLogging: onLoad");
    }

    function onBeforeUnload() {
    console.log("EventLogging: onBeforeUnLoad");
    }

    function onUnload() {
    console.log("EventLogging: onUnload");
    }

    function onPause() {
    console.log("EventLogging: onPause");
    }

    function onResume() {
    resumeTime = new Date();
    console.log("EventLogging: onResume");
    }

    function onSapResumeSuccess() {
    console.log("EventLogging: onSapResumeSuccess");
    var endTime = new Date();
    var duration = (endTime - resumeTime)/1000;
    console.log("EventLogging: Time from onresume to onSapResumeSuccess " + duration + " seconds");
    }

    function onSapLogonSuccess() {
    console.log("EventLogging: onSapLogonSuccess");
    }

    function onSapResumeError(error) {
    console.log("EventLogging: onSapResumeError " + JSON.stringify(error));
    }

    document.addEventListener("deviceready", init, false);
    document.addEventListener("pause", onPause, false);
    document.addEventListener("resume", onResume, false);
    document.addEventListener("online", deviceOnline, false);
    document.addEventListener("offline", deviceOffline, false);
    document.addEventListener("onSapResumeSuccess", onSapResumeSuccess, false);
    document.addEventListener("onSapLogonSuccess", onSapLogonSuccess, false);
    document.addEventListener("onSapResumeError", onSapResumeError, false);

    </script>

    </head>
    <body onload="onLoad()" onunload="onUnload()" onbeforeunload="onBeforeUnload()">
    <div class="screenDiv" id="LoadingDiv">
    <h1>Loading ...</h1>
    </div>

    <div class="screenDiv" id="LockedDiv" style="display: none">
    <h1>Locked</h1>
    <button id="unlock2" onclick="unlock()">Unlock</button>
    </div>

    <div class="screenDiv" id="MainDiv" style="display: none">
    <h1>Offline Sample</h1>
    <button id="read" onclick="read()">Read</button>
    <button id="unregister" onclick="unRegister()">Unregister</button>
    <button id="openStore" onclick="setTimeout(openStore, 10);">Open Offline Store</button>
    <button id="closeStore" onclick="closeStore()">Close Offline Store</button>
    <button id="clearStore" onclick="clearStore()">Clear Offline Store</button>
    <button id="sync" onclick="flushStore()">Flush and Refresh</button>
    <button id="clearLog" onclick="sap.Logger.clearLog();updateStatus2('Cleared the Log')">Clear Log</button><br>
    <button id="viewLog" onclick="viewLog()">View Log</button><br>
    <span id="statusID"></span><br>
    <span id="statusID2"></span>
    <table id="carrierTable"><tr><th>Carrier ID</th><th>Carrier Name</th></tr></table>
    </div>
    </body>
    </html>​


  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run windows --archs=x86
    or
    cordova run ios

    Click on Open Offline Store.

    Place the device into airplane mode.

    Airplane mode can be turned on in the following manner.

    On Android choose Settings > More > Airplane mode

    On an iOS device choose Settings > Airplane Mode.

    On an iOS simulator, unplug the Ethernet cable or turn off the WI-FI for the Mac hosting the simulator.

    Click on Read.


    Notice that the app now returns the results using the data retrieved from the local store and does so much quicker since a network request is not needed to retrieve the results.


Using ILOData to Query an Offline Database


The steps below will demonstrate how to take an already created database from an application and query it with ILOData.

Note, it is possible to create an offline database directly with ILOData tool.

Browse to the <Install Media>\modules\OfflineODataTools folder. There is a zip file named OfflineODataTools.zip. Unzip the file to extract iLoData.exe.

Note there are two database files, one named .udb and one name .rq.udb.
udb stands for UltraLite database and rq stands for request queue.

iOS


In Xcode > Windows > Devices, select your connected device, select your deployed Kapsel app, and in the gear at the bottom choose Download Container.

Once downloaded, select it with finder and choose Show Package Contents. The offline database files are shown below in their default location if a path is not provided via the storepath option.

Android


To retrieve the database files from an Android device, specify the store path when creating the offline database. Modify the index.html file and in the openStore method, uncomment the following entry.
"storePath" : cordova.file.externalRootDirectory,

Add the file plugin.
cordova plugin add cordova-plugin-file

Deploy and run the app. Once the store has been recreated, the database files can be retrieved using following commands.
adb shell
cd /sdcard
ls CarrierOfflineStore.*
exit


adb pull /sdcard/CarrierOfflineStore.udb
adb pull /sdcard/CarrierOfflineStore.rq.udb

If you are using an Android emulator, you can execute the below commands directly without using the storePath option.
adb root
adb shell
cd /data/data/com.kapsel.gs/files
ls -l
exit
adb pull /data/data/com.kapsel.gs/files/CarrierOfflineStore.rq.udb
adb pull /data/data/com.kapsel.gs/files/CarrierOfflineStore.udb

Windows


For a Kapsel app deployed to a Windows laptop, the database files can be found under the following folder.
C:\Users\i8xxxx\AppData\Local\Packages\com.kapsel.gs_h35559jr9hy9m\LocalState

Using ILOData


C:\SAP\MobileSDK3\ilodata.exe store_name=./CarrierOfflineStore
get /CarrierCollection?$top=1




Encrypting the Offline Database


To encrypt the data in the offline stores, specify an encryption key. Without this, it is possible on an Android or iOS simulator to access the UltraLite database files and view the data as shown below.


The following option can be provided when opening the store to encrypt the stores using AES-256.
var options = {
"storeEncryptionKey" : "myVerySecretKey123!"
//"storeEncryptionKey" : applicationContext.registrationContext.password //if using the Logon plugin, the user entered password for the registration can be used if it never changes
};
store.open(openStoreSuccessCallback, errorCallback, options);

A better solution than hardcoding the password would be to generate a random password and then to store it in the datavault provided by the Logon plugin. Here is an example of how that might look.
var otp = null;  //one time password.  Must be set once the first time the app starts

function logonSuccessCallback() {
getOneTimePassword();
...
}

function getOneTimePassword() {
sap.Logon.get(getSuccess, errorCallback, "key");
}

function getSuccess(val) {
if (val == null) {
val = Math.random().toString(36).substring(16);
sap.Logon.set(val);
return;
}
otp = val;
}

function openStore() {
...
var options = {
"storeEncryptionKey" : otp
};
store.open(openStoreSuccessCallback, errorCallback, options);
}

Offline Enabled App with Create, Update, Delete and Access to the Error Archive


The following steps will create an offline enabled app that can create, update and delete records and will also display the contents of the ErrorArchive which contains any error that occurred during a flush. A flush attempts to send locally made changes on the device to the OData producer.

This sample uses the SAP Cloud Platform Mobile Services server as it provides the sample OData service that will be used.

Note, the techniques used in this sample are for learning purposes only. In a non-demo app, it is recommended that the offline store be used all the time to maintain consistency of the data.

  • In the management cockpit create a new application with an ID of
    com.kapsel.gs2

    Set the Type to be Hybrid.

    Set the Security Configuration to be Basic.

    On the Back End tab, set an endpoint with the following value.
    https://hcpms-i82XXXXtrial.hanatrial.ondemand.com/SampleServices/ESPM.svc

    Set the Proxy Type to be Internet.

    Ensure the rewrite mode is Rewrite URL. For offline apps it is important that the URI's are routed through the SMP or SAP Cloud Platform Mobile Services server.

    Add a Basic Authentication or Application to Application SSO mechanism.

  • Create the project.
    cordova create C:\Kapsel_Projects\KapselGSDemo2 com.kapsel.gs2 KapselGSDemo2
    cd C:\Kapsel_Projects\KapselGSDemo2
    cordova platform add android
    cordova platform add windows

    or on iOS
    cordova create ~/Documents/Kapsel_Projects/KapselGSDemo2 com.kapsel.gs2 KapselGSDemo2
    cd ~/Documents/Kapsel_Projects/KapselGSDemo2
    cordova platform add ios


  • Add the Cordova network information plugin and the Kapsel Offline OData plugin.
    cordova plugin add cordova-plugin-network-information
    cordova plugin add cordova-plugin-dialogs

    cordova plugin add kapsel-plugin-odata --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logon --searchpath %KAPSEL_HOME%/plugins
    cordova plugin add kapsel-plugin-logger --searchpath %KAPSEL_HOME%/plugins
    or
    cordova plugin add kapsel-plugin-odata --searchpath $KAPSEL_HOME/plugins
    cordova plugin add kapsel-plugin-logon --searchpath $KAPSEL_HOME/plugins
    cordova plugin add kapsel-plugin-logger --searchpath $KAPSEL_HOME/plugins


  • Replace C:\Kapsel_Projects\KapselGSDemo2\www\index.html with the contents below.
    <!DOCTYPE html>
    <html>
    <head>
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
    <!--<meta http-equiv="Content-Security-Policy" content="default-src http://10.7.171.234:8080 'self' 'unsafe-inline' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *">-->
    <script type="text/javascript" charset="utf-8" src="datajs-1.1.2.min.js"></script>
    <script type="text/javascript" charset="utf-8" src="serverContext.js"></script>
    <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
    <script>
    var applicationContext = null;
    var store = null; //Offline OData store
    var startTime = new Date();
    var initTime = null;
    var unlockTime = null;
    var resumeTime = null;

    window.onerror = onError;

    function onError(msg, url, line) {
    var idx = url.lastIndexOf("/");
    var file = "unknown";
    if (idx > -1) {
    file = url.substring(idx + 1);
    }
    alert("An error occurred in " + file + " (at line # " + line + "): " + msg);
    return false; //suppressErrorAlert;
    }

    function init() {
    updateStatus2("Calling Logon.init");
    initTime = new Date();
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    console.log("EventLogging: Time from onload to deviceready " + duration + " seconds");

    if (sap.Logger) {
    sap.Logger.setLogLevel(sap.Logger.DEBUG); //enables the display of debug log messages from the Kapsel plugins.
    sap.Logger.debug("EventLogging: Log level set to DEBUG");
    }

    if (navigator.notification) { // Override default HTML alert with native dialog. alert is not supported on Windows
    window.alert = navigator.notification.alert;
    }

    sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
    console.log("EventLogging: init completed");
    }

    function logonSuccessCallback(result) {
    updateStatus2("logonSuccessCallback called");
    var endTime = new Date();
    if (unlockTime) {
    var duration = (endTime - unlockTime)/1000;
    console.log("EventLogging: Unlock Time " + duration + " seconds");
    unlockTime = null;
    }

    applicationContext = result;
    showScreen("MainDiv");
    }

    function logonErrorCallback(error) { //this method is called if the user cancels the registration.
    alert("An error occurred: " + JSON.stringify(error));
    if (device.platform == "Android") { //Not supported on iOS
    navigator.app.exitApp();
    }
    }

    function readProducts(isLocal, inErrorState) {
    showScreen("MainDiv");
    updateStatus2("Read request started");
    startTime = new Date();
    if (!applicationContext) {
    alert("Register or unlock before proceeding");
    return;
    }
    clearTable();
    var sUrl = applicationContext.applicationEndpointURL + "/Products?$format=json&$orderby=Category,ProductId"; //JSON format is less verbose than atom/xml
    if (isLocal === true) {
    if (!store) {
    updateStatus2("The store must be opened before reading local entities.");
    return;
    }
    sUrl = sUrl + "&$filter=sap.islocal()";
    }
    if (inErrorState === true) {
    if (!store) {
    updateStatus2("The store must be opened before reading error entities.");
    return;
    }
    sUrl = sUrl + "&$filter=sap.inerrorstate()";
    }

    var oHeaders = {};
    //oHeaders['X-SMP-APPCID'] = applicationContext.applicationConnectionId; //not needed as this will be sent by the logon plugin

    var request = {
    headers : oHeaders,
    requestUri : sUrl,
    method : "GET"
    };

    if (device.platform == "windows") { //provided by the authproxy and logon plugins on Android and iOS but not on Windows https://support.wdf.sap.corp/sap/support/message/1680272744
    request.user = applicationContext.registrationContext.user;
    request.password = applicationContext.registrationContext.password;
    }
    OData.read(request, readSuccessCallback, errorCallback);
    }

    function createProducts() {
    var updateForm = document.forms["CreateForm"];
    updateForm.ID.value = "";
    updateForm.prodName.value = "";
    updateForm.currency.value = "";
    updateForm.description.value = "";
    updateForm.price.value = "";

    showScreen("CreateDiv");
    }

    function readSuccessCallback(data, response) {
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Read " + data.results.length + " records in " + duration + " seconds");
    var productsTable = document.getElementById("productsTable");

    for (var i = data.results.length -1; i >= 0; i--) {
    var row = productsTable.insertRow(1);
    var dresult = data.results[i];
    applyColor(data.results[i], row);
    var cell1 = row.insertCell(0);
    var cell2 = row.insertCell(1);
    var cell3 = row.insertCell(2);
    var cell4 = row.insertCell(3);
    var cell5 = row.insertCell(4);
    var productID = data.results[i].ProductId
    cell1.innerHTML = data.results[i].ProductId;
    cell2.innerHTML = '<a href="javascript:void(0)" onclick="showProductUpdateScreen(\'' + productID + '\')">' + data.results[i].Name + '</a>';
    cell3.innerHTML = data.results[i].ShortDescription;
    cell4.innerHTML = Math.round(data.results[i].Price * 100) / 100;
    cell5.innerHTML = data.results[i].CategoryName;
    }
    }

    function applyColor(data, row) {
    if (data["@com.sap.vocabularies.Offline.v1.isLocal"]) {
    row.style.color = "green";
    }
    if (data["@com.sap.vocabularies.Offline.v1.inErrorState"]) {
    row.style.color = "red";
    }
    if (data["@com.sap.vocabularies.Offline.v1.isDeleteError"]) {
    row.style.color = "orange";
    }
    }


    function showProductUpdateScreen(productID) {
    updateStatus2("Read product request started");
    startTime = new Date();
    showScreen("UpdateDiv");

    var sUrl = applicationContext.applicationEndpointURL + "/Products('" + productID + "')";
    //HTTP headers
    var oHeaders = {};

    //HTTP Request
    var request = {
    headers : oHeaders,
    requestUri : sUrl,
    method : "GET"
    };
    OData.read(request, readProductItemSuccessCallback, errorCallback);
    }

    function readProductItemSuccessCallback(data, response) {
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Read item in " + duration + " seconds");

    var updateForm = document.forms["UpdateForm"];
    //Add Properties that should be updated
    updateForm.ID.value = data.ProductId;
    updateForm.prodName.value = data.Name;
    updateForm.description.value = data.ShortDescription;
    updateForm.currency.value = data.CurrencyCode;
    updateForm.price.value = Math.round(data.Price * 100) / 100;
    }

    function createRecord() {
    var updateForm = document.forms["CreateForm"];
    var updateProductsURL = applicationContext.applicationEndpointURL + "/Products";
    var oHeaders = {};
    var params = {};

    //The below values will be set when we issue a patch
    params.ProductId = updateForm.ID.value;
    /*params.Name = updateForm.prodName.value;
    params.CurrencyCode = updateForm.currency.value;
    params.ShortDescription = updateForm.description.value;
    params.Price = updateForm.price.value;*/

    //HTTP Headers
    /*if (csrf) {
    oHeaders['X-CSRF-TOKEN'] = csrf;
    }*/

    oHeaders['Content-Type'] = "application/json";
    oHeaders['accept'] = "application/json";
    oHeaders['If-Match'] = "*"; //Some services needs If-Match Header for Update/delete

    var request = {
    headers : oHeaders,
    requestUri : updateProductsURL,
    method : "POST",
    data : params
    };
    //Since Products has a hasStream = "true", the initial create or Post succeeds but the values are not null, need to do a Patch to set the values.
    OData.request(request, patchEntity, errorCallback);
    }

    function patchEntity(data, response) {
    var localOfflinePrimaryKey = data.__metadata.etag;

    var updateForm = document.forms["CreateForm"];
    var updateProductsURL = data.__metadata.uri; //applicationContext.applicationEndpointURL + "/Products";
    var oHeaders = {};
    var params = {};

    //params.ProductId = updateForm.ID.value;
    params.Name = updateForm.prodName.value;
    params.CurrencyCode = updateForm.currency.value;
    params.ShortDescription = updateForm.description.value;
    params.Price = updateForm.price.value;

    //HTTP Headers
    /*if (csrf) {
    oHeaders['X-CSRF-TOKEN'] = csrf;
    }*/

    oHeaders['Content-Type'] = "application/json";
    oHeaders['accept'] = "application/json";
    oHeaders['If-Match'] = "*"; //Some services needs If-Match Header for Update/delete

    var request = {
    headers : oHeaders,
    requestUri : updateProductsURL,
    method : "PATCH",
    data : params
    };
    OData.request(request, readProducts, errorCallback);
    }


    function updateRecord() {
    var updateForm = document.forms["UpdateForm"];
    var updateProductsURL = applicationContext.applicationEndpointURL + "/Products('" + updateForm.ID.value + "')";
    var oHeaders = {};
    var params = {};

    params.Name = updateForm.prodName.value;
    params.CurrencyCode = updateForm.currency.value;
    params.ShortDescription = updateForm.description.value;
    params.Price = updateForm.price.value;

    //HTTP Headers
    /*if (csrf) {
    oHeaders['X-CSRF-TOKEN'] = csrf;
    }*/

    oHeaders['Content-Type'] = "application/json";
    oHeaders['accept'] = "application/json";
    oHeaders['If-Match'] = "*"; //Some services needs If-Match Header for Update/delete

    var request = {
    headers : oHeaders,
    requestUri : updateProductsURL,
    method : "PUT", //MERGE/PATCH/PUT
    data : params
    };
    OData.request(request, readProducts, errorCallback);
    }

    function deleteRecord() {
    var updateForm = document.forms["UpdateForm"];
    var deleteStockURL = applicationContext.applicationEndpointURL + "/Stock('" + updateForm.ID.value + "')";
    var deleteProductsURL = applicationContext.applicationEndpointURL + "/Products('" + updateForm.ID.value + "')";
    var oHeaders = {};
    var params = {};

    oHeaders['Content-Type'] = "application/json";
    oHeaders['accept'] = "application/json";
    oHeaders['If-Match'] = "*"; //Some services needs If-Match Header for Update/delete

    var request = {
    headers : oHeaders,
    requestUri : deleteStockURL,
    method : "DELETE",
    //data : params
    };
    OData.request(request, function() {console.log("Delete stock " + updateForm.ID.value + " succeeded.")}, function(e) {console.log("Delete Request failed: " + JSON.stringify(e))}); //might not be any stock entries
    request.requestUri = deleteProductsURL;
    OData.request(request, readProducts, errorCallback);
    }

    function clearTable2(tableId) {
    var productsTable = document.getElementById(tableId);
    while(productsTable.rows.length > 1) {
    productsTable.deleteRow(1);
    }
    }

    function clearTable() {
    var productsTable = document.getElementById("productsTable");
    while(productsTable.rows.length > 1) {
    productsTable.deleteRow(1);
    }
    }

    function errorCallback(e) {
    alert("An error occurred: " + JSON.stringify(e));
    console.log("EventLogging: errorCallback " + JSON.stringify(e));
    showScreen("MainDiv");
    }

    function register() {
    updateStatus2("Calling Logon.init");
    sap.Logon.init(logonSuccessCallback, logonErrorCallback, appId, context);
    }

    function unRegister() {
    updateStatus2("Calling deleteRegistration");
    sap.Logon.core.deleteRegistration(logonUnregisterSuccessCallback, errorCallback);
    clearTable();
    }

    function logonUnregisterSuccessCallback(result) {
    updateStatus2("Successfully Unregistered");
    console.log("EventLogging: logonUnregisterSuccessCallback " + JSON.stringify(result));
    applicationContext = null;
    register();
    }

    function lock() {
    sap.Logon.lock(logonLockSuccessCallback, errorCallback);
    clearTable();
    }

    function logonLockSuccessCallback(result) {
    console.log("EventLogging: logonLockSuccessCallback " + JSON.stringify(result));
    applicationContext = null;
    showScreen("LockedDiv"); //sap.Logon.unlock(function () {},function (error) {}); //alternatively show the unlock screen
    }

    function unlock() {
    unlockTime = new Date();
    sap.Logon.unlock(logonSuccessCallback, errorCallback);
    }

    function managePasscode() {
    sap.Logon.managePasscode(managePasscodeSuccessCallback, errorCallback);
    }

    function managePasscodeSuccessCallback() {
    console.log("EventLogging: managePasscodeSuccess");
    }

    function showScreen(screenIDToShow) {
    var screenToShow = document.getElementById(screenIDToShow);
    screenToShow.style.display = "block";
    var screens = document.getElementsByClassName('screenDiv');
    for (var i = 0; i < screens.length; i++) {
    if (screens[i].id != screenToShow.id) {
    screens[i].style.display = "none";
    }
    }
    }

    function openStore() {
    console.log("EventLogging: openStore");
    startTime = new Date();
    updateStatus2("store.open called");
    var properties = {
    "name": "CarrierOfflineStore",
    "host": applicationContext.registrationContext.serverHost,
    "port": applicationContext.registrationContext.serverPort,
    "https": applicationContext.registrationContext.https,
    "serviceRoot" : appId,
    //"storePath" : cordova.file.externalRootDirectory,

    "definingRequests" : {
    "ProductsDR" : "/Products",
    "StockDR" : "/Stock"
    }
    };

    if (device.platform == "windows") {
    var authStr = "Basic " + btoa(applicationContext.registrationContext.user + ":" + applicationContext.registrationContext.password);
    properties.streamParams = "custom_header=Authorization:" + authStr;
    }

    store = sap.OData.createOfflineStore(properties);
    store.onrequesterror = onRequestError; //called for each modification error during flush
    var options = {};
    store.open(openStoreSuccessCallback, errorCallback, options, progressCallback);
    }

    function onRequestError(error) {
    navigator.notification.alert("An error occurred while FLUSHING " + JSON.stringify(error));
    console.log("An error occurred while FLUSHING " + JSON.stringify(error));
    }

    function openStoreSuccessCallback() {
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Store opened in " + duration + " seconds");
    updateStatus1("Store is OPEN.");
    console.log("EventLogging: openStoreSuccessCallback. Stored opened in " + duration);
    sap.OData.applyHttpClient(); //Offline OData calls can now be made against datajs.
    }

    function closeStore() {
    if (!store) {
    updateStatus2("The store must be opened before it can be closed");
    return;
    }
    console.log("EventLogging: closeStore");
    updateStatus2("store.close called");
    store.close(closeStoreSuccessCallback, errorCallback);
    }

    function closeStoreSuccessCallback() {
    console.log("EventLogging: closeStoreSuccessCallback");
    sap.OData.removeHttpClient();
    updateStatus1("Store is CLOSED.");
    updateStatus2("Store closed");
    }

    //Removes the physical store from the filesystem
    function clearStore() {
    console.log("EventLogging: clearStore");
    if (!store) {
    updateStatus2("The store must be closed before it can be cleared");
    return;
    }
    store.clear(clearStoreSuccessCallback, errorCallback);
    }

    function clearStoreSuccessCallback() {
    console.log("EventLogging: clearStoreSuccessCallback");
    updateStatus1("");
    updateStatus2("Store is CLEARED");
    store = null;
    }

    function refreshStore() {
    console.log("EventLogging: refreshStore");
    if (!store) {
    updateStatus2("The store must be open before it can be refreshed");
    return;
    }
    startTime = new Date();
    updateStatus2("Store refresh called");
    store.refresh(refreshStoreCallback, errorCallback, null, progressCallback);
    }

    function refreshStoreCallback() {
    console.log("EventLogging: refreshStoreCallback");
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Store refreshed in " + duration + " seconds");
    }

    function flushStore() {
    console.log("EventLogging: flushStore");
    if (!store) {
    updateStatus2("The store must be open before it can be flushed");
    return;
    }
    startTime = new Date();
    updateStatus2("Store flush called");
    store.flush(flushStoreSuccessCallback, errorCallback, null, progressCallback);
    }

    function flushStoreSuccessCallback() {
    console.log("EventLogging: flushStoreSuccessCallback");
    var endTime = new Date();
    var duration = (endTime - startTime)/1000;
    updateStatus2("Store flushed in " + duration + " seconds");
    refreshStore();
    }

    function viewLog() {
    if (sap.Logger) {
    sap.Logger.getLogEntries(getLogEntriesSuccess, errorCallback)
    }
    else {
    alert("ensure the kapsel-logger plugin has been added to the project");
    }
    }

    function getLogEntriesSuccess(logEntries) {
    var stringToShow = "";
    var logArray;

    if (device.platform == "windows") {
    logArray = logEntries.split("\n");
    if (logArray.length > 0) {
    for (var i = 0; i < logArray.length; i++) {
    stringToShow += logArray[i] + "\n";
    }
    }
    }
    else if (device.platform == "iOS") {
    logArray = logEntries.split("\n");
    if (logArray.length > 0) {
    for (var i = 0; i < logArray.length; i++) {
    logLineEntries = logArray[i].split(" ");
    for (var j = 7; j < logLineEntries.length; j++) {
    stringToShow += logLineEntries[j] + " ";
    }
    stringToShow = stringToShow + "\n";
    }
    }
    }
    else { //Android
    logArray = logEntries.split('#');
    if (logArray.length > 0) {
    var numOfMessages = parseInt(logArray.length / 15);
    for (var i = 0; i < numOfMessages; i++) {
    stringToShow += logArray[i * 15 + 1] + ": " + logArray[i * 15 + 3] + ": " + logArray[i * 15 + 14] + "\n";
    }
    }
    }
    alert(stringToShow);
    console.log("EventLogging: Device Log follows " + stringToShow);
    }


    function updateStatus1(msg) {
    document.getElementById('statusID').innerHTML = msg + " " + getDeviceStatusString();
    console.log("EventLogging: " + msg + " " + getDeviceStatusString());
    }

    function updateStatus2(msg) {
    var d = new Date();
    document.getElementById('statusID2').innerHTML = msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds());
    if (msg.indexOf("Refreshing Sent:") != 0) { //don't bother logging the messages regarding the progress callback for flush and refresh
    console.log("EventLogging: " + msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds()));
    }
    }

    function addZero(input) {
    if (input < 10) {
    return "0" + input;
    }
    return input;
    }

    function getDeviceStatusString() {
    if (online) {
    return "Device is ONLINE";
    }
    else {
    return "Device is OFFLINE";
    }
    }

    function progressCallback(progressStatus) {
    var status = progressStatus.progressState;
    var lead = "unknown";
    if (status === sap.OfflineStore.ProgressState.STORE_DOWNLOADING) {
    lead = "Downloading ";
    }
    else if (status === sap.OfflineStore.ProgressState.REFRESH) {
    lead = "Refreshing ";
    }
    else if (status === sap.OfflineStore.ProgressState.FLUSH_REQUEST_QUEUE) {
    lead = "Flushing ";
    }
    else if (status === sap.OfflineStore.ProgressState.DONE) {
    lead = "Complete ";
    }
    else {
    alert("Unknown status in progressCallback");
    }
    updateStatus2(lead + "Sent: " + progressStatus.bytesSent + " Received: " + progressStatus.bytesRecv + " File Size: " + progressStatus.fileSize);
    }

    function deviceOnline() {
    online = true;
    updateStatus1("");
    }

    function deviceOffline() {
    online = false;
    updateStatus1("");
    }


    function onLoad() {
    console.log("EventLogging: onLoad");
    }

    function onBeforeUnload() {
    console.log("EventLogging: onBeforeUnLoad");
    }

    function onUnload() {
    console.log("EventLogging: onUnload");
    }

    function onPause() {
    console.log("EventLogging: onPause");
    }

    function onResume() {
    resumeTime = new Date();
    console.log("EventLogging: onResume");
    }

    function onSapResumeSuccess() {
    console.log("EventLogging: onSapResumeSuccess");
    var endTime = new Date();
    var duration = (endTime - resumeTime)/1000;
    console.log("EventLogging: Time from onresume to onSapResumeSuccess " + duration + " seconds");
    }

    function onSapLogonSuccess() {
    console.log("EventLogging: onSapLogonSuccess");
    }

    function onSapResumeError(error) {
    console.log("EventLogging: onSapResumeError " + JSON.stringify(error));
    }

    function showErrorsSuccessCallback(data, response) {
    updateStatus2("ErrorArchive contains " + data.results.length + " records ");
    console.log(JSON.stringify(data.results));
    clearTable2("ErrorsTable");
    showScreen("ErrorsDiv");
    var errorsForm = document.getElementById("ErrorsForm");
    errorsForm.setAttribute("requestid", "");
    var errorsTable = document.getElementById("ErrorsTable");
    for (var i = 0; i < data.results.length; i++) {
    var row = errorsTable.insertRow(1);
    var product = JSON.parse(data.results[i].RequestBody);
    var cell1 = row.insertCell(0);
    var cell2 = row.insertCell(1);
    var cell3 = row.insertCell(2);
    var cell4 = row.insertCell(3);
    var cell5 = row.insertCell(4);
    var cell6 = row.insertCell(5);
    if (i == 0) { //any requestID will work in the delete call to the ErrorArchive
    errorsForm.setAttribute("requestid", data.results[i].RequestID);
    }

    cell1.innerHTML = data.results[i].RequestMethod;
    cell2.innerHTML = data.results[i].HTTPStatusCode;
    if (product) {
    cell3.innerHTML = product.Name;
    cell4.innerHTML = product.Price;
    }
    cell5.innerHTML = data.results[i].RequestURL;
    cell6.innerHTML = data.results[i].Message;
    }
    }

    //Uses the Error Archive. You can also view rows that are in an error state using a filter or by looking for an annotation. See the read() method
    function showErrors() {
    if (!store) {
    updateStatus2("The store must be opened before viewing the ErrorArchive");
    return;
    }
    updateStatus2("ErrorArchive request started");
    clearTable2("ErrorsTable");
    var sURL = applicationContext.applicationEndpointURL + "/ErrorArchive";
    var oHeaders = {};

    var request = {
    headers : oHeaders,
    requestUri : sURL,
    method : "GET"
    };
    console.log("read using " + sURL);
    OData.read(request, showErrorsSuccessCallback, errorCallback);
    }

    function clearErrors() {
    var requestID = document.forms["ErrorsForm"].getAttribute("requestid");
    if (requestID == "") {
    alert("There are no errors to remove from the ErrorArchive");
    return;
    }

    navigator.notification.confirm(
    "Proceeding will revert all operations that are currently in an error state", // message
    clearErrors2, // callback to invoke with index of button pressed
    "Warning", // title
    ["Continue","Cancel"] // buttonLabels
    );
    }

    function clearErrors2(buttonIndex) {
    if (buttonIndex == 2) {
    return
    }
    console.log("Clearing error!")
    updateStatus2("Clear ErrorArchive entry started");
    var requestID = document.forms["ErrorsForm"].getAttribute("requestid");
    var deleteErrorsURL = applicationContext.applicationEndpointURL + "/ErrorArchive(" + requestID + ")";
    var oHeaders = {};
    oHeaders['Content-Type'] = "application/json";
    oHeaders['accept'] = "application/json";
    oHeaders['If-Match'] = "*"; //Some services needs If-Match Header for Update/delete

    var request = {
    headers : oHeaders,
    requestUri : deleteErrorsURL,
    method : "DELETE"
    };
    console.log(deleteErrorsURL);
    OData.request(request, showErrors, errorCallback);
    }

    function checkRequestQueue() {
    if (!store) {
    updateStatus2("The store must be opened before checking the request queue");
    return;
    }
    store.getRequestQueueStatus(requestQSuccessCallback, errorCallback)
    }

    function requestQSuccessCallback(qStatus) {
    var statusStr = " contains items to be flushed";
    if (qStatus.isEmpty) {
    statusStr = " is empty";
    }
    updateStatus2("Request queue" + statusStr);
    }

    document.addEventListener("deviceready", init, false);
    document.addEventListener("pause", onPause, false);
    document.addEventListener("resume", onResume, false);
    document.addEventListener("online", deviceOnline, false);
    document.addEventListener("offline", deviceOffline, false);
    document.addEventListener("onSapResumeSuccess", onSapResumeSuccess, false);
    document.addEventListener("onSapLogonSuccess", onSapLogonSuccess, false);
    document.addEventListener("onSapResumeError", onSapResumeError, false);

    </script>

    </head>
    <body onload="onLoad()" onunload="onUnload()" onbeforeunload="onBeforeUnload()">
    <div class="screenDiv" id="LoadingDiv">
    <h1>Loading ...</h1>
    </div>

    <div class="screenDiv" id="LockedDiv" style="display: none">
    <h1>Locked</h1>
    <button id="unlock2" onclick="unlock()">Unlock</button>
    </div>

    <div class="screenDiv" id="UpdateDiv" style="display: none">
    <h3>Update</h3>
    <form id="UpdateForm">
    ProductID: <input type="text" name="ID" ><br>
    Name: <input type="text" name="prodName" ><br>
    Currency: <input type="text" name="currency" ><br>
    Description: <input type="text" name="description" ><br>
    Price: <input type="text" name="price"><br>
    <button type=button onclick="updateRecord()">Update</button>
    <button type=button onclick="deleteRecord()">Delete</button>
    <button type=button onclick="showScreen('MainDiv')">Back</button>
    </form>
    </div>

    <div class="screenDiv" id="CreateDiv" style="display: none">
    <h3>Create</h3>
    <form id="CreateForm">
    ProductID: <input type="text" name="ID" ><br>
    Name: <input type="text" name="prodName" ><br>
    Currency: <input type="text" name="currency" ><br>
    Description: <input type="text" name="description" ><br>
    Price: <input type="text" name="price"><br>
    <button type=button onclick="createRecord()">Save</button>
    <button type=button onclick="showScreen('MainDiv')">Back</button>
    </form>
    </div>

    <div class="screenDiv" id="ErrorsDiv" style="display: none">
    <h3>Errors</h3>
    <form id="ErrorsForm">
    <table id="ErrorsTable">
    <tr>
    <th align="left">Op</th>
    <th align="left">Code</th>
    <th align="left">Name</th>
    <th align="left">Price</th>
    <th align="left">URL</th>
    <th align="left">Message</th>
    </tr>
    </table>
    </form>
    <button id="clearError" onclick="clearErrors()">Clear Errors</button>
    <button type=button onclick="showScreen('MainDiv')">Back</button>
    </div>

    <div class="screenDiv" id="MainDiv" style="display: none">
    <h1>Offline Sample 2</h1>
    <button id="unregister" onclick="unRegister()">Unregister</button>
    <button id="read" onclick="readProducts()">Read</button>
    <button id="createItem" onclick="createProducts()">Create</button>
    <button id="clearLog" onclick="sap.Logger.clearLog();updateStatus2('Cleared the Log')">Clear Log</button>
    <button id="viewLog" onclick="viewLog()">View Log</button><br>

    <button id="openStore" onclick="setTimeout(openStore, 10);">Open Offline Store</button>
    <button id="closeStore" onclick="closeStore()">Close Offline Store</button>
    <button id="clearStore" onclick="clearStore()">Clear Offline Store</button><br>
    <button id="readlocal" onclick="readProducts(true)">Read Local</button>
    <button id="read3" onclick="readProducts(false, true)">Filter on Errors</button>
    <button id="sync" onclick="flushStore()">Flush and Refresh</button>
    <button id="showErrors" onclick="showErrors()">Error Archive</button>
    <button id="requestQueue" onclick="checkRequestQueue()">Request Queue</button><br>

    <span id="statusID"></span><br>
    <span id="statusID2"></span>
    <table id="productsTable"><tr><th align='left'>ID</th><th align='left'>Name</th><th align='left'>Description</th><th align='left'>Price</th><th align='left'>Category</th></tr></table>
    </div>
    </body>
    </html>


  • Copy the serverContext.js file from C:\Kapsel_Projects\KapselGSDemo\www to C:\Kapsel_Projects\KapselGSDemo2\www and ensure that the host, port and https values are correct. The port should be 443 and https should be set to true.
    Note the password value will be the same value used to open the SAP Cloud Platform Mobile Services management cockpit.
    Finally, modify the appId to be com.kapsel.gs2.

  • Place a copy of datajs-1.1.2.min.js into the www folder.

  • Modify the config.xml file to add any required settings such as the android-minSdkVersion and windows-target-version.

  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run windows --archs=x86
    or
    cordova run ios

    Note products can now be added to the list, removed or modified. This can occur when the device is online or if it is offline, then when the device is online and the Flush button is pressed, the changes are sent to the OData producer.

  • Changes to entities that have not yet been flushed to the OData producer appear in green.
    Pressing the Read Local button adds the following filter to only show these rows.
    $filter=sap.islocal()



Error Conditions


For additional details see Error Handling.

For each error that occurs while flushing the changes to the backend OData source, the onRequestError method is called.
As well, the rows affected by a failed flush operation are annotated to indicate that they are in an errorstate. The following is an example of an annotation that appears in a record that is in an error state.
{"uri":"https://hcpms-i826567trial.hanatrial.ondemand.com:443/com.kapsel.gs2/Products('HT-101')",
"type":"ESPM.Product"},
"@com.sap.vocabularies.Offline.v1.isLocal":true,
"@com.sap.vocabularies.Offline.v1.inErrorState":true,
"Name":"USB Battery"
...

The read method in index.html calls the applyColor method and searches for three annotations, islocal, inErrorState and isDeleteError and applies the colors green, red, and orange.
The below filter is applied after pressing the Filter on Errors button which limits the results of the read to only include rows that are in an error state.
&$filter=sap.inerrorstate()

Finally, there is an error archive that can be queried which provides additional details of errors that occurred during a flush operation.
Deleting an entry from the Error Archive removes all rows from the Error Archive and reverts all rows that were in an error state to their previous state. The following steps can be followed to further illustrate some error conditions.

Flush Fails due to No Network


The error callback method will be called with the an error like the following.
[-10208] Communication with the server because the host could not be found.

Update Fails in the Backend Due to a Backend Constraint


This type of error can be caught by the onRequestError listener of the store.
store.onrequesterror = onRequestError; //called for each modification error during flush

[-10197] A request failed against the backen OData producer. The value of quantity should be greater than zero.
This may occur if constraint on the backend OData service exists that does not exist on the local offline OData store.
To recover, perform another update with a correct value, flush and refresh or in the Error Archive click on Clear Errors to revert the change.

Attempting to Delete or Update a Non Existent Row


With the offline store open, create a new product. The row appears as green as it has not yet been flushed.
Perform a flush and refresh.
Close the offline store.
Perform a read. Notice the new row exists. Delete the row.
Open the offline store.
Perform a read. Notice that the deleted row still exists in the offline store.
Delete or Update the row.
Flush the offline store which will cause an error as the delete or update request will fail.
[-10197] A request failed against the backend OData producer. Requested entity could not be found.
Perform a read and notice that the row now appears in orange to indicate that there was a problem deleting this row or in red to indicate the row is in an error state.


Click on Check Error Archive to view the contents of the Error Archive. Notice that the Operation was a DELETE request and the Error Code was 404 which indicates that it was unable to delete the entity.
Click on Error Archive.



To correct the problem, close the store, delete the store and reopen the store.

Auto Increment Primary Keys


The previous example requires specifying the ID value for a product when creating a record. If two devices that are both offline specify the same ID value, one of them will fail when flushing the change to the backend OData producer.

One approach to avoiding this problem is to specify that the ID should be an auto increment field. In this way, the ID's for each product are determined by the backend and the offline store will create a temporary ID. This temporary ID can be seen via the uri field of the __metadata of a successful create operation against the offline store or via a read operation against a product that has been added to an offline store but not yet flushed and refreshed. Here is an example showing this.


Notice above that the ID value is null as one was not specified during the create operation and that the temporary assigned ID as shown in the returned uri is http://10.7.171.158:8080/com.mycompany.offline2/DanProd(lodata_sys_eid=X'00737054815011E58000B36030B...'). This URI should be used if any further operations will be performed on this local data.
Once the offline store is flushed and refreshed the value of ID will be updated to reflect the ID assigned from the backend.

Offline and Online in One App


The service root for the offline and always online requests must be different. In this sample requests that are made to
https://hcpms-XXXXtrial.hanatrial.ondemand.com/com.kapsel.gs2/Products

are redirected to the Offline Store when it is open and requests to
https://hcpms-i826567trial.hanatrial.ondemand.com/com.kapsel.gs3/Products

are not redirected to the offline store. The service root is specified when the store is initially opened.
    function openStore() {
...
var properties = {
"name": "CarrierOfflineStore",
"host": applicationContext.registrationContext.serverHost,
"port": applicationContext.registrationContext.serverPort,
"https": applicationContext.registrationContext.https,
"serviceRoot" : appId,

"definingRequests" : {
"ProductsDR" : "/Products",
"StockDR" : "/Stock"
}
};

store = sap.OData.createOfflineStore(properties);

//var options = {};
store.open(openStoreSuccessCallback, errorCallback/*, options*/);
}

The following steps demonstrate this using the previous project.
Note, the techniques used in this sample are for learning purposes only. In a non-demo app, it is recommended that the offline store be used all the time to maintain consistency of the data.

  • Add a second back-end connection to the application com.kapsel.gs2 with a name of com.kapsel.gs3 in the management cockpit that also points to https://hcpms-XXXXtrial.hanatrial.ondemand.com/SampleServices/ESPM.svc.

  • Modify www/index.html and a second read button.
    <button id="read2" onclick="readProducts2()">Read Online</button><br>


  • Copy and paste the readProducts method and rename the copied method to be readProducts2

  • Modify the readProducts2 method. Add this line just after sUrl is defined.
    sUrl = sUrl.replace("gs2", "gs3");  //make the read request against the backend rather than the offline store.


  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run windows --archs=x64
    or
    cordova run ios

    Open the Offline store. Press the Read button and notice the duration the read took. Now press the Read Online button and notice that it takes longer as the read is going to the back-end rather than being retrieved from the offline store.


Questions and Answers


Question


Is there any way to add additional "defining Request" or modify an existing one after an OData offline database has been created?
There is a product collection with 1 million entries. I do not want to download this complete collection, so I am setting my definingrequest to an OData collection with filter to city eq Berlin. This is working and will create the database for me with a few thousand products. Now I am in another city and want to add the products of that other city, e.g. Hamburg into my already existing offlineDB. Is this somehow possible?

Answer


There is no way to add or modify defining requests in an already existing database in SP11. Once a database is created, the defining requests become fixed.
One potential way to work around this is to create multiple stores on your device. By using different storeName values in the store options, you can have multiple stores existing at the same time.
So you can have a store_Hamburg and a store_Berlin and open either depending on where you are.

Question


Is there any way to perform a free text search on the offline database?
I know that in OData V4 a new $search query parameter got introduced, but I guess this one is not yet available in the offline store.

Answer


The offline store does not yet support the $search query parameter.
However, it does support the $filter indexof operation. With this, you can do searches such as:
/Products?$filter=indexof(Name, 'wrench') ge 0

to search for entities containing a string

Question


I know that function imports are not supported in offline. But if there are entities with function imports and without function imports, can we use entities without function imports offline?
Also, what about in the case of variables with size more than 512? If any of the collection having variable size more than 512 can it be used when offline?

Answer


We essentially ignore function imports. So as long as you don’t try to use them, you can offline other entity sets fine.

We currently don’t support KEY properties or REFERENTIAL CONSTRAINT properties with a greater size. However, all other properties can have a greater size.
Note that the maxiumum size of Edm.String key and Edm.binary key properties increased in SP 11. See Offline OData Version Support and Limitations.

Question


I am getting an error when opening my offline store such as
Provided value for the property 'Price' is not compatible with the property

Answer


The V2 spec requires that Edm.Double be represented as a JSON string, but this service is returning them as a JSON floating point number. The parser in the offline server is strict and expects a string so it is failing when it is parsing the Edm.Double properties.

A work around to this issue is to force the offline server to use ATOM when retrieving the defining requests. That can be done by configuring the offline application using an INI file and using that under Applications > com.mycompany.offline > Import Settings.
Name=com.mycompany.offline5
request_format=atom

 

Question


It seems that the OData Offline DB uses a case sensitive search. My customer would like to get results case insensitive.

Answer


The case sensitivity is based on the case sensitivity of the underlying database which is case sensitive by default. Comparison operations can be made case-insensitive using an application config ini file and setting
case_sensitive_offline_db=no

However, using substringof with tolower should work fine. Here is an example of using the tolower method in a filter to avoid case sensitivity.
http://services.odata.org/V2/OData/OData.svc/Products?$format=json&$filter=tolower(Name) eq 'bread'
vs
http://services.odata.org/V2/OData/OData.svc/Products?$format=json&$filter=Name eq 'bread'

 

Question


How many Offline Stores can be open at the same time? The following error is occurring when opening five offline databases. [-10067] Could not create the store. Reason: -685

Answer


There is a limit to the total number of Ultralite databases that can be running concurrently as of SP09, and each offline store has 2 databases (the offline store and the request queue). Currently on a device, the limit is 8 databases which means that a maximum of 4 offline stores can be open at the same time.

 

Question


The following error is seen
09-08 13:12:29.466: I/chromium(13600): [INFO:CONSOLE (1059)] "processMessage failed: Stack: RangeError: Invalid time value
09-08 13:12:29.466: I/chromium(13600): at Date.toISOString (native)
09-08 13:12:29.466: I/chromium(13600): at sap.ui.controller._stringToUTC

Answer


After calling applyHttpClient try adding this line: window.OData.jsonHandler.recognizeDates = true;



 

Question


Is there support for sync/flush operations on the Offline DB when the app is in the background?

Answer


See the topic Background Synchronization.

 

Question


When the offline store opens, the following error is seen.
The conversion from OData metadata to database metadata has failed

In the SMP server log the following is seen.
Property "GenderCode" cannot be supported because it is a key or referential constraint property and its MaxLength facet is missing or has a value that is too large

Answer


A setting was added to SMP 3.0 SP10 that enables this to be worked around if it is not possible to specify a MaxLength for the service being used. See the property named allow_omitting_max_length_facet described in Application Configuration File.

 

Question


I would like to selectively flush records. For example, some records may be partial records that require additional data entry on the device before they are synced.

Answer


There currently is not support for selectively flushing records. Currently this would need to be handled by the application by storing incomplete records in a different entity set that is not part of the defining requests.

 

Question


When we create an entity locally, and the entity uses a server side generated key, then we only have the "lodata_sys_eid" as a reliable form of identification of the entity? After a flush and refresh cycle ... how do we find the actual "real key" from the "lodata_sys_eid"?
An application creates an entity while offline, and after the create takes place the entity is shown to the user ... now the app flushes and refreshes in the background .... now the user wants to edit the just created entity ... does the entity still exist?

Answer


You can still use the generated entity ID after a flush/refresh cycle for "a while". The last 500 generated entity IDs are kept after a flush/refresh cycle for this purpose.

 

Previous   Home   Next
67 Comments