- 2.1 Use JSONStore for offline login
- 2.2 Update login page to call JSONStore based login when device is offline
- 3.1 Deploy MFP adapter that synchronizes data between Cloudant and JSONStore
- 3.2 Use JSONStore for offline storage and syncing of data from Cloudant
- 3.3 Update Home page to load data from JSONStore
- 3.4 Update views to take care of data wrapping by JSONStore
- 3.5 Delete redundant code
- 4.1 Add code for upstream sync of data to Cloudant
- 4.2 Add code for upstream sync of images to Cloud Object Storage
- 4.3 Update Report New Problem page to work in offline mode as well
- 4.4 Update Home page to show grievances reported in offline mode as well
- 4.5 Delete redundant code
This project builds on top of the app built in https://github.com/IBM/Ionic-MFP-App. In this code pattern, we will update the app such that it is usable even when the device is offline.
Copy Ionic Mobile app and Mobile Foundation adapters from parent repo as per instructions in http://bit-traveler.blogspot.in/2012/08/git-copy-file-or-directory-from-one.html as shown below.
- Create your repo on github.com and add
README.md
file. Clone your new repo.
$ git clone https://github.com/<your-username>/<your-new-repo-name>.git
- Make a git format-patch for the entire history of the subdirectories that we want as shown below.
$ mkdir gitpatches
$ git clone https://github.com/IBM/Ionic-MFP-App.git
$ cd Ionic-MFP-App
$ git format-patch -o ../gitpatches/ --root IonicMobileApp/ MobileFoundationAdapters/
- Import the patches into your new repository as shown below.
$ cd ../<your-new-repo-name>
$ git am ../gitpatches/*
$ git push
Follow tutorial https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/7.1/advanced-topics/offline-authentication/
Add Cordova plugin for Mobile Foundation JSONStore as below:
$ ionic cordova plugin add cordova-plugin-mfp-jsonstore
Add a new provider for working with JSONStore as below:
$ ionic generate provider JsonStoreHandler
[OK] Generated a provider named JsonStoreHandler!
Update IonicMobileApp/src/providers/json-store-handler/json-store-handler.ts
as below:
/// <reference path="../../../plugins/cordova-plugin-mfp-jsonstore/typings/jsonstore.d.ts" />
import { Injectable } from '@angular/core';
@Injectable()
export class JsonStoreHandlerProvider {
isCollectionInitialized = {};
userCredentialsCollectionName = 'userCredentials';
myCollections = {
userCredentials: {
searchFields: { username: 'string' }
}
}
constructor() {
console.log('--> JsonStoreHandler constructor() called');
}
// https://www.ibm.com/support/knowledgecenter/en/SSHS8R_8.0.0/com.ibm.worklight.apiref.doc/html/refjavascript-client/html/WL.JSONStore.html
initCollections(username, password, isOnline:boolean) {
return new Promise( (resolve, reject) => {
if (username in this.isCollectionInitialized) {
// console.log('--> JsonStoreHandler: collections have already been initialized for username: ' + username);
return resolve();
}
console.log('--> JsonStoreHandler: initCollections called');
let encodedUsername = this.convertToJsonStoreCompatibleUsername(username);
console.log('--> JsonStoreHandler: username after encoding: ' + encodedUsername);
let options = {
username: encodedUsername,
password: password,
localKeyGen: true
}
WL.JSONStore.closeAll({});
WL.JSONStore.init(this.myCollections, options).then((success) => {
console.log('--> JsonStoreHandler: successfully initialized JSONStore collection.');
this.isCollectionInitialized[username] = true;
if (isOnline) {
this.initCollectionForOfflineLogin();
}
resolve();
}, (failure) => {
if (isOnline) {
console.log('--> JsonStoreHandler: password change detected for user: ' + username + ' . Destroying old JSONStore so as to recreate it.\n', JSON.stringify(failure));
WL.JSONStore.destroy(encodedUsername).then(() => {
return resolve(this.initCollections(username, password, isOnline));
});
} else {
console.log('--> JsonStoreHandler: failed to initialize \'' + this.userCredentialsCollectionName + '\' JSONStore collection.\n' + JSON.stringify(failure));
reject(failure);
}
});
});
}
initCollectionForOfflineLogin() {
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.userCredentialsCollectionName);
collectionInstance.count({}, {}).then((countResult) => {
if (countResult == 0) {
collectionInstance.add({ name: this.userCredentialsCollectionName }, {});
console.log('--> JsonStoreHandler: \'' + this.userCredentialsCollectionName + '\' JSONStore collection has been initialized for offlineLogin');
}
})
}
previousLoginExists() {
return new Promise( (resolve, reject) => {
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.userCredentialsCollectionName);
collectionInstance.count({}, {}).then((countResult) => {
if (countResult == 0) {
reject();
} else {
resolve();
}
})
});
}
destroyCollections(username) {
WL.JSONStore.destroy(username);
}
// JSONStore username must be an alphanumeric string ([a-z, A-Z, 0-9]) and start with a letter
convertToJsonStoreCompatibleUsername(str: String) {
// https://stackoverflow.com/questions/21647928/javascript-unicode-string-to-hex
let result = "U"; // start with a letter
for (let i=0; i<str.length; i++) {
let hex = str.charCodeAt(i).toString(16);
result += ("0"+hex).slice(-4); // if you want to support Unicode text, then use ("000"+hex)
}
return result
}
}
Update IonicMobileApp/src/providers/auth-handler/auth-handler.ts
as below:
...
import { JsonStoreHandlerProvider } from '../json-store-handler/json-store-handler';
...
export class AuthHandlerProvider {
...
constructor(private jsonStoreHandler:JsonStoreHandlerProvider) {
console.log('--> AuthHandler constructor() called');
}
init() {
...
this.userLoginChallengeHandler.handleChallenge = this.handleChallenge.bind(this);
// this.userLoginChallengeHandler.handleSuccess = this.handleSuccess.bind(this);
this.userLoginChallengeHandler.handleFailure = this.handleFailure.bind(this);
}
...
// handleSuccess(data) {
// console.log('--> AuthHandler handleSuccess called');
// this.isChallenged = false;
// if (this.loginSuccessCallback != null) {
// this.loginSuccessCallback();
// } else {
// console.log('--> AuthHandler: loginSuccessCallback not set!');
// }
// }
...
login(username, password) {
console.log('--> AuthHandler login called. isChallenged = ' + this.isChallenged);
this.username = username;
this.userLoginChallengeHandler.handleSuccess = () => {
console.log('--> AuthHandler handleSuccess called');
this.isChallenged = false;
this.jsonStoreHandler.initCollections(username, password, true).then(() => {
this.loginSuccessCallback();
});
};
if (this.isChallenged) {
this.userLoginChallengeHandler.submitChallengeAnswer({'username':username, 'password':password});
} else {
WLAuthorizationManager.login(this.securityCheckName, {'username':username, 'password':password})
.then(
(success) => {
console.log('--> AuthHandler login success');
},
(failure) => {
console.log('--> AuthHandler login failure: ' + JSON.stringify(failure));
this.loginFailureCallback(failure.errorMsg);
}
);
}
}
...
offlineLogin(username, password) {
console.log('--> AuthHandler offlineLogin called');
this.jsonStoreHandler.initCollections(username, password, false).then((success) => {
this.jsonStoreHandler.previousLoginExists().then(() => {
console.log('--> AuthHandler offlineLogin success');
this.loginSuccessCallback();
}, () => {
this.jsonStoreHandler.destroyCollections(username);
console.log('--> AuthHandler offlineLogin failed. First time login must be done when internet connection is available');
this.loginFailureCallback('First time login must be done when internet connection is available');
});
}, (failure) => {
console.log('--> AuthHandler offlineLogin failed. Invalid username/password\n', JSON.stringify(failure));
this.loginFailureCallback('Invalid username/password');
})
}
}
https://ionicframework.com/docs/native/network/
Install the Cordova and Ionic plugins for Network information:
$ ionic cordova plugin add cordova-plugin-network-information
$ npm install --save @ionic-native/network
Add the network plugin to your app's module. Update IonicMobileApp/src/app/app.module.ts
as below:
...
import { Network } from '@ionic-native/network';
@NgModule({
...
providers: [
...
ImageResizer,
Network
]
})
...
Copy IonicMobileApp/src/pages/login/login.ts
as below:
...
import { Network } from '@ionic-native/network';
...
export class LoginPage {
...
constructor(public navCtrl: NavController, public navParams: NavParams, private network: Network,
public alertCtrl: AlertController, public authHandler:AuthHandlerProvider, public loadingCtrl: LoadingController) {
...
}
processForm() {
// Reference: https://github.com/driftyco/ionic-preview-app/blob/master/src/pages/inputs/basic/pages.ts
let username = this.fixedUsername != null ? this.fixedUsername : this.form.value.username;
let password = this.form.value.password;
if (username === "" || password === "") {
this.showAlert('Login Failure', 'Username and password are required');
return;
}
this.loader = this.loadingCtrl.create({
content: 'Signing in. Please wait ...',
dismissOnPageChange: true
});
this.loader.present().then(() => {
if (this.hasNetworkConnection()) {
console.log('--> Online sign-in with user: ' + username);
this.authHandler.login(username, password);
} else {
console.log('--> Offline sign-in with user: ' + username);
this.authHandler.offlineLogin(username, password);
}
});
}
hasNetworkConnection() {
// https://ionicframework.com/docs/native/network/
return this.network.type !== 'none';
}
...
}
https://mobilefirstplatform.ibmcloud.com/blog/2018/02/23/jsonstoresync-couchdb-databases/
Download JSONStoreCloudantSync adapter from https://github.com/MobileFirst-Platform-Developer-Center/JSONStoreCloudantSync/. This MobileFirst adapter provides upstream and downstream sync functionality between a Cloudant database and the JSONStore on mobile app.
$ cd ../MobileFoundationAdapters/
$ curl -LOk https://github.com/MobileFirst-Platform-Developer-Center/JSONStoreCloudantSync/archive/master.zip
$ unzip master.zip
$ mv JSONStoreCloudantSync-master/ JSONStoreCloudantSync
$ rm master.zip
$ ls
JSONStoreCloudantSync MyWardData UserLogin
Open MobileFoundationAdapters/JSONStoreCloudantSync/src/main/adapter-resources/adapter.xml
and update the following properties to point to the Cloudant database created in https://github.com/IBM/Ionic-MFP-App.
- Update
username
andpassword
with the Cloudant API key as generated in Ionic-MFP-App#Step 2.2. - For property
host
, specify the Cloudant Dashboard URL portion after (and excluding) https:// and upto (and including) -bluemix.cloudant.com as shown in the snapshot of Ionic-MFP-App#Step 2.2. - For property
protocol
, leave the default value ofhttps
as-is. - For property
port
, leave the default value of443
as-is. - For property
dbname
, leave the default value ofmyward
as-is. - For property
createnewdbifnotexist
, leave the default value offalse
as-is.
<mfp:adapter name="JSONStoreCloudantSync" ...>
<property name="username" displayName="Cloudant Username" defaultValue=""/>
<property name="password" displayName="Cloudant Password" defaultValue=""/>
<property name="host" displayName="Cloudant Host" defaultValue=""/>
<property name="protocol" displayName="DB protocol" defaultValue="https" />
<property name="port" displayName="Db port" defaultValue="443" />
<property name="dbname" displayName="Cloudant Database Name" defaultValue="myward"/>
<property name="createnewdbifnotexist" displayName="Create database if it does not exist?" defaultValue="false" />
...
</mfp:adapter>
Update MobileFoundationAdapters/JSONStoreCloudantSync/src/main/java/com/ibm/mobile/jsonstore/JSONStoreCloudantSyncResource.java
as below:
...
import com.ibm.mfp.adapter.api.OAuthSecurity;
...
@Path("/")
@OAuthSecurity(scope = "UserLogin")
public class JSONStoreCloudantSyncResource {
...
}
Update MobileFoundationAdapters/JSONStoreCloudantSync/src/main/java/com/ibm/mobile/jsonstore/JSONStoreCloudantSyncApplication.java
as below:
...
public class JSONStoreCloudantSyncApplication extends MFPJAXRSApplication {
...
/* Returns a handle to the CouchDB connection */
CouchDbClient connectToDB(String dbName) throws Exception {
// Workaround for bi-directional sync (i.e. both UPSTREAM and DOWNSTREAM sync)
dbName = configurationAPI.getPropertyValue("dbname");
if (dbClients.containsKey(dbName)) {
return dbClients.get(dbName);
} else {
...
}
}
}
Build and deploy the JSONStoreCloudantSync adapter as shown below:
$ cd ./JSONStoreCloudantSync/
$ mfpdev adapter build
Building adapter...
Successfully built adapter
$ mfpdev adapter deploy
Verifying server configuration...
Deploying adapter to runtime mfp on https://mobilefoundation-71-hb-server.mybluemix.net:443/mfpadmin...
Successfully deployed adapter
Update IonicMobileApp/src/providers/json-store-handler/json-store-handler.ts
as below:
...
import { MyWardDataProvider } from '../my-ward-data/my-ward-data';
...
export class JsonStoreHandlerProvider {
isCollectionInitialized = {};
onSyncSuccessCallback = null;
onSyncFailureCallback = null;
objectStorageAccess = null;
userCredentialsCollectionName = 'userCredentials';
myWardCollectionName = 'myward';
objectStorageDetailsCollectionName = 'objectStorageDetails';
myCollections = {
userCredentials: {
searchFields: { username: 'string' }
},
myward: {
searchFields: { reportedBy: 'string' },
sync: {
syncPolicy: 0,
syncAdapterPath: 'JSONStoreCloudantSync',
onSyncSuccess: this.onSyncSuccess.bind(this),
onSyncFailure: this.onSyncFailure.bind(this),
}
},
objectStorageDetails: {
searchFields: { baseUrl: 'string' },
}
}
constructor(public myWardDataProvider: MyWardDataProvider) {
console.log('--> JsonStoreHandler constructor() called');
}
// https://www.ibm.com/support/knowledgecenter/en/SSHS8R_8.0.0/com.ibm.worklight.apiref.doc/html/refjavascript-client/html/WL.JSONStore.html
initCollections(username, password, isOnline:boolean) {
return new Promise( (resolve, reject) => {
if (username in this.isCollectionInitialized) {
// console.log('--> JsonStoreHandler: collections have already been initialized for username: ' + username);
return resolve();
}
console.log('--> JsonStoreHandler: initCollections called');
let encodedUsername = this.convertToJsonStoreCompatibleUsername(username);
console.log('--> JsonStoreHandler: username after encoding: ' + encodedUsername);
let options = {
username: encodedUsername,
password: password,
localKeyGen: true
}
WL.JSONStore.closeAll({});
WL.JSONStore.init(this.myCollections, options).then((success) => {
console.log('--> JsonStoreHandler: successfully initialized JSONStore collections.');
this.isCollectionInitialized[username] = true;
if (isOnline) {
this.initCollectionForOfflineLogin();
this.loadObjectStorageAccess.bind(this)();
}
resolve();
}, (failure) => {
if (isOnline) {
console.log('--> JsonStoreHandler: password change detected for user: ' + username + ' . Destroying old JSONStore so as to recreate it.\n', JSON.stringify(failure));
WL.JSONStore.destroy(encodedUsername).then(() => {
return this.initCollections(username, password, isOnline);
});
} else {
console.log('--> JsonStoreHandler: failed to initialize \'' + this.userCredentialsCollectionName + '\' JSONStore collection.\n' + JSON.stringify(failure));
reject({collectionName: this.userCredentialsCollectionName, failure: failure});
}
});
});
}
...
getData() {
return new Promise( (resolve, reject) => {
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.myWardCollectionName);
collectionInstance.findAll('{}').then((data) => {
console.log('--> JsonStoreHandler: data fetched from JSONStore = \n', data);
resolve(data);
});
});
}
onSyncSuccess(data) {
console.log('--> JsonStoreHandler onSyncSuccess: ' + data);
// TODO onSyncSuccessCallback should be called only if data has changed
if (this.onSyncSuccessCallback != null) {
this.onSyncSuccessCallback();
} else {
console.log('--> JsonStoreHandler: onSyncSuccessCallback not set!');
}
}
onSyncFailure(error) {
console.log('--> JsonStoreHandler: sync failed\n', error);
if (this.onSyncFailureCallback != null) {
this.onSyncFailureCallback(error);
} else {
console.log('--> JsonStoreHandler: onSyncFailureCallback not set!');
}
}
setOnSyncSuccessCallback(onSyncSuccess) {
this.onSyncSuccessCallback = onSyncSuccess;
}
setOnSyncFailureCallback(onSyncFailure) {
this.onSyncFailureCallback = onSyncFailure;
}
syncMyWardData() {
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.myWardCollectionName);
if (collectionInstance != null) {
collectionInstance.sync().then(() => {
console.log('--> JsonStoreHandler downstream sync initiated');
}, (failure) => {
console.log('--> JsonStoreHandler Failed to initiate downstream sync\n' + failure);
});
} else {
console.log('--> JsonStoreHandler Failed to initiate downstream sync\n' + 'Collection ' + this.myWardCollectionName + ' not yet initialized');
}
}
loadObjectStorageAccess() {
this.myWardDataProvider.getObjectStorageAccess().then(objectStorageAccess => {
this.hasObjectStorageAccessChanged(objectStorageAccess).then((hasChanged) => {
if (hasChanged) {
this.objectStorageAccess = objectStorageAccess;
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.objectStorageDetailsCollectionName);
collectionInstance.clear({}).then(() => {
collectionInstance.add(objectStorageAccess, {}).then((noOfDocs) => {
console.log('--> JsonStoreHandler objectStorageAccess successfully updated.');
if (this.onSyncSuccessCallback != null) {
this.onSyncSuccessCallback();
} else {
console.log('--> JsonStoreHandler loadObjectStorageAccess(): onSyncSuccessCallback not set!');
}
}, (failure) => {
console.log('--> JsonStoreHandler loadObjectStorageAccess(): add to JSONStore failed\n', failure);
});
});
} else {
console.log('--> JsonStoreHandler: objectStorageAccess has not changed.');
}
});
});
}
hasObjectStorageAccessChanged(newObjectStorageAccess) {
return new Promise( (resolve, reject) => {
this.getObjectStorageAccess().then((oldObjectStorageAccess: any) => {
if (oldObjectStorageAccess != null && oldObjectStorageAccess.baseUrl == newObjectStorageAccess.baseUrl &&
oldObjectStorageAccess.authorizationHeader == newObjectStorageAccess.authorizationHeader) {
resolve(false);
} else {
resolve(true);
}
});
});
}
getObjectStorageAccess() {
return new Promise( (resolve, reject) => {
if (this.objectStorageAccess) {
// already loaded data
return resolve(this.objectStorageAccess);
}
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.objectStorageDetailsCollectionName);
if (collectionInstance != null) {
collectionInstance.findAll({}).then((results) => {
if (results.length > 0) {
this.objectStorageAccess = results[0].json;
resolve(results[0].json);
} else {
resolve(null);
}
}, (failure) => {
console.log('--> JsonStoreHandler: getObjectStorageAccess failed\n', failure);
reject(failure);
});
} else {
resolve(null);
}
});
}
}
Update IonicMobileApp/src/providers/my-ward-data/my-ward-data.ts
as below:
...
export class MyWardDataProvider {
...
getObjectStorageAccess() {
// console.log('--> MyWardDataProvider getting Object Storage AuthToken from adapter ...');
return new Promise((resolve, reject) => {
let dataRequest = new WLResourceRequest("/adapters/MyWardData/objectStorage", WLResourceRequest.GET);
dataRequest.send().then((response) => {
// console.log('--> MyWardDataProvider got Object Storage AuthToken from adapter ', response);
resolve(response.responseJSON);
}, (failure) => {
console.log('--> MyWardDataProvider failed to get Object Storage AuthToken from adapter\n', JSON.stringify(failure));
reject(failure);
})
});
}
...
}
Update IonicMobileApp/src/pages/home/home.ts
as below:
...
import { JsonStoreHandlerProvider } from '../../providers/json-store-handler/json-store-handler';
...
export class HomePage {
...
reloadData: boolean = false;
constructor(public navCtrl: NavController, public loadingCtrl: LoadingController,
public myWardDataProvider: MyWardDataProvider, public imgCache: ImgCacheService,
private authHandler:AuthHandlerProvider, private jsonStoreHandler:JsonStoreHandlerProvider) {
console.log('--> HomePage constructor() called');
}
ionViewDidLoad() {
console.log('--> HomePage ionViewDidLoad() called');
this.loader = null;
this.loadData();
}
ionViewWillEnter() {
console.log('--> HomePage ionViewWillEnter() called');
this.initAuthChallengeHandler();
this.jsonStoreHandler.setOnSyncSuccessCallback(() => {
let view = this.navCtrl.getActive();
if (view.instance instanceof HomePage) {
console.log('--> HomePage onSyncSuccessCallback() called');
this.loadData();
} else {
this.reloadData = true;
}
});
if (this.reloadData) {
this.reloadData = false;
this.loadData();
}
}
loadData() {
if (this.loader == null) {
console.log('--> HomePage creating new loader');
this.loader = this.loadingCtrl.create({
content: 'Loading data. Please wait ...'
});
this.loader.present().then(() => {
this.loadDataFromJsonStore();
});
} else {
console.log('--> HomePage reusing previous loader');
this.loadDataFromJsonStore();
}
}
loadDataFromJsonStore() {
this.jsonStoreHandler.getObjectStorageAccess().then(objectStorageAccess => {
if (objectStorageAccess != null) {
this.objectStorageAccess = objectStorageAccess;
this.imgCache.init({
headers: {
'Authorization': this.objectStorageAccess.authorizationHeader
}
}).then( () => {
console.log('--> HomePage initialized imgCache');
this.jsonStoreHandler.getData().then(data => {
this.grievances = data;
this.loader.dismiss();
this.loader = null;
});
});
} else {
console.log('--> HomePage objectStorageAccess not yet loaded');
}
});
}
...
refresh() {
this.jsonStoreHandler.syncMyWardData();
}
...
}
JSONStore wraps our data inside a json
element as shown below.
[
{ _id: 1, json: {problemDescription: '...', address: '...', ...} },
{ _id: 2, json: {problemDescription: '...', address: '...', ...} },
...
]
Hence, we need to update the references to our data in views/pages as shown below.
Update IonicMobileApp/src/pages/home/home.html
as below:
...
<ion-content padding>
<ion-list>
<button ion-item (click)="itemClick(grievance)" *ngFor="let grievance of grievances">
<ion-thumbnail item-left>
<img img-cache img-cache-src="{{objectStorageAccess.baseUrl}}{{grievance.json.picture.thumbnail}}">
</ion-thumbnail>
<h2 text-wrap>{{grievance.json.problemDescription}}</h2>
<p>@ {{grievance.json.address}}</p>
</button>
</ion-list>
</ion-content>
Update IonicMobileApp/src/pages/problem-detail/problem-detail.html
as below:
...
<ion-content padding>
<h2 text-wrap>{{grievance.json.problemDescription}}</h2>
<p>Reported on: {{grievance.json.reportedDateTime}}</p>
<img img-cache img-cache-src="{{baseUrl}}{{grievance.json.picture.large}}">
<p text-wrap>@ {{grievance.json.address}}</p>
<div id="map"></div>
</ion-content>
Update IonicMobileApp/src/pages/problem-detail/problem-detail.ts
as below:
...
loadMap() {
let loc = new LatLng(this.grievance.json.geoLocation.coordinates[1], this.grievance.json.geoLocation.coordinates[0]);
...
}
From IonicMobileApp/src/providers/my-ward-data/my-ward-data.ts
delete the function load()
which is now redundant.
Update IonicMobileApp/src/providers/json-store-handler/json-store-handler.ts
as below:
...
import { Network } from '@ionic-native/network';
...
export class JsonStoreHandlerProvider {
...
userCredentialsCollectionName = 'userCredentials';
myWardCollectionName = 'myward';
newProblemsCollectionName = 'newproblems';
objectStorageDetailsCollectionName = 'objectStorageDetails';
myCollections = {
userCredentials: {
searchFields: { username: 'string' }
},
myward: {
searchFields: { reportedBy: 'string' },
sync: {
syncPolicy: 0,
syncAdapterPath: 'JSONStoreCloudantSync',
onSyncSuccess: this.onSyncSuccess.bind(this),
onSyncFailure: this.onSyncFailure.bind(this),
}
},
newproblems: {
searchFields: { problemDescription: 'string' },
sync: {
syncPolicy: 1,
syncAdapterPath: 'JSONStoreCloudantSync',
onSyncSuccess: this.onUpstreamSyncSuccess.bind(this),
onSyncFailure: this.onUpstreamSyncFailure.bind(this),
}
},
objectStorageDetails: {
searchFields: { baseUrl: 'string' },
}
}
constructor(private network: Network, public myWardDataProvider: MyWardDataProvider) {
console.log('--> JsonStoreHandler constructor() called');
this.network.onConnect().subscribe(() => {
console.log('--> JsonStoreHandlerProvider: Network connected!');
// We just got a connection but we need to wait briefly
// before we determine the connection type. Might need to wait.
// prior to doing any api requests as well.
setTimeout(() => {
if (this.network.type != 'none') {
this.initUpstreamSync();
}
}, 3000);
});
}
...
initUpstreamSync() {
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.newProblemsCollectionName);
if (collectionInstance != null) {
collectionInstance.sync().then(() => {
console.log('--> JsonStoreHandler upstream sync initiated');
}, (failure) => {
console.log('--> JsonStoreHandler Failed to initiate upstream sync\n' + failure);
});
} else {
console.log('--> JsonStoreHandler Failed to initiate upstream sync\n' + 'Collection ' + this.newProblemsCollectionName + ' not yet initialized');
}
}
onUpstreamSyncSuccess(data) {
console.log('--> JsonStoreHandler onUpstreamSyncSuccess: ' + data);
this.syncMyWardData();
}
onUpstreamSyncFailure(error) {
console.log('--> JsonStoreHandler: upstream sync failed\n', error);
}
addNewGrievance(grievance) {
return new Promise( (resolve, reject) => {
console.log('--> JsonStoreHandler: adding following new grievance to JSONStore ...\n' + JSON.stringify(grievance));
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.newProblemsCollectionName);
collectionInstance.add(grievance, {}).then((noOfDocs) => {
console.log('--> JsonStoreHandler added new grievance.');
resolve();
}, (failure) => {
console.log('--> JsonStoreHandler addNewGrievance failed\n', failure);
reject(failure);
});
});
}
getUnSyncedData() {
return new Promise( (resolve, reject) => {
let collectionInstance: WL.JSONStore.JSONStoreInstance = WL.JSONStore.get(this.newProblemsCollectionName);
if (collectionInstance != null) {
collectionInstance.getAllDirty('{}').then((data) => {
if (data.length > 0) {
console.log('--> JsonStoreHandler: Data that is not yet synced with Cloudant = \n', data);
}
resolve(data);
});
} else {
resolve({});
}
});
}
}
Install Ionic native plugin for File
as below:
$ cd ../../IonicMobileApp/
$ npm install --save @ionic-native/file
Update IonicMobileApp/src/app/app.module.ts
as below:
...
import { File } from '@ionic-native/file';
@NgModule({
...
providers: [
...
File,
FileTransfer,
]
})
...
Create an Ionic Provider for handling upstream sync of images to Cloud Object Storage as below:
$ ionic generate provider UpstreamImageSync
[OK] Generated a provider named UpstreamImageSync!
Update IonicMobileApp/src/providers/upstream-image-sync/upstream-image-sync.ts
as below:
import { Injectable } from '@angular/core';
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';
import { File } from '@ionic-native/file';
import { Network } from '@ionic-native/network';
import { JsonStoreHandlerProvider } from '../json-store-handler/json-store-handler';
@Injectable()
export class UpstreamImageSyncProvider {
offlineImagesDir : string = 'offlineImagesDir';
offlineImagesDirEntry = null;
constructor(private transfer: FileTransfer, private file: File, private network: Network,
private jsonStoreHandler: JsonStoreHandlerProvider) {
console.log('--> UpstreamImageSyncProvider constructor() called');
this.createOfflineImagesDirIfNotExists();
this.network.onConnect().subscribe(() => {
console.log('--> UpstreamImageSyncProvider: Network connected!');
// We just got a connection but we need to wait briefly
// before we determine the connection type. Might need to wait.
// prior to doing any api requests as well.
setTimeout(() => {
if (this.network.type != 'none') {
this.uploadOfflineImages();
}
}, 3000);
});
}
createOfflineImagesDirIfNotExists() {
this.file.checkDir(this.file.dataDirectory, this.offlineImagesDir).then(_ => {
console.log('--> UpstreamImageSyncProvider: Directory ' + this.offlineImagesDir + ' exists');
this.file.resolveDirectoryUrl(this.file.dataDirectory).then((baseDirEntry) => {
this.file.getDirectory(baseDirEntry, this.offlineImagesDir, {}).then((dirEntry) => {
this.offlineImagesDirEntry = dirEntry;
console.log('--> UpstreamImageSyncProvider: Successfully resolved directory ' + this.offlineImagesDir);
}).catch((err) => {
console.log('--> UpstreamImageSyncProvider: Error getting directory ' + this.offlineImagesDir + ':\n' + JSON.stringify(err));
});
}).catch((err) => {
console.log('--> UpstreamImageSyncProvider: Error resolving directory URL ' + this.file.dataDirectory + ':\n' + JSON.stringify(err));
});
}).catch(err => {
console.log('--> UpstreamImageSyncProvider: Creating directory ' + this.offlineImagesDir + ' ...');
this.file.createDir(this.file.dataDirectory, this.offlineImagesDir, false).then((dirEntry) => {
this.offlineImagesDirEntry = dirEntry;
console.log('--> UpstreamImageSyncProvider: Successfully created directory ' + this.offlineImagesDir);
}).catch(err => {
console.log('--> UpstreamImageSyncProvider: Error creating directory ' + this.offlineImagesDir + ':\n' + JSON.stringify(err));
});
});
}
getOfflineDirPath() {
if (this.offlineImagesDirEntry != null) {
return this.offlineImagesDirEntry.nativeURL;
} else {
return null;
}
}
saveImageInOfflineDir(fileName, filePath) {
return new Promise( (resolve, reject) => {
(window as any).resolveLocalFileSystemURL(filePath, (entry) => {
console.log('--> UpstreamImageSyncProvider: Copying ' + entry.nativeURL + ' to ' + this.file.dataDirectory + this.offlineImagesDir + '/' + fileName + ' ...');
entry.copyTo(this.offlineImagesDirEntry, fileName, (newEntry) => {
console.log('--> UpstreamImageSyncProvider: Successfully copied file to path {{' + newEntry.filesystem.name + '}}/' + newEntry.fullPath);
resolve("");
}, (err) => {
console.log('--> UpstreamImageSyncProvider: copyTo failed: ' + JSON.stringify(err));
reject(err);
});
}, (err) => {
console.log('--> UpstreamImageSyncProvider: Error during resolveLocalFileSystemURL: ' + JSON.stringify(err));
reject(err);
});
});
}
hasNetworkConnection() {
// https://ionicframework.com/docs/native/network/
return this.network.type !== 'none';
}
uploadImage(fileName, filePath) {
return new Promise( (resolve, reject) => {
if (!this.hasNetworkConnection()) {
console.log('--> UpstreamImageSyncProvider: Device offline. Saving image on local file system for later upload to Cloud Object Storage');
resolve(this.saveImageInOfflineDir(fileName, filePath));
} else {
this.jsonStoreHandler.getObjectStorageAccess().then(objectStorageAccess => {
if (objectStorageAccess != null) {
resolve(this.uploadImageToServer(fileName, filePath, objectStorageAccess));
} else {
reject('ObjectStorageAccess not yet initialized');
}
});
}
});
}
uploadImageToServer(fileName, filePath, objectStorageAccess) {
return new Promise( (resolve, reject) => {
let serverUrl = objectStorageAccess.baseUrl + fileName;
console.log('--> UpstreamImageSyncProvider: Uploading image (' + filePath + ') to server (' + serverUrl + ') ...');
let options: FileUploadOptions = {
fileKey: 'file',
fileName: fileName,
httpMethod: 'PUT',
headers: {
'Authorization': objectStorageAccess.authorizationHeader,
'Content-Type': 'image/jpeg'
}
}
let fileTransfer: FileTransferObject = this.transfer.create();
fileTransfer.upload(filePath, serverUrl, options) .then((data) => {
// success
console.log('--> UpstreamImageSyncProvider: Image upload successful:\n', data);
resolve(data);
}, (err) => {
// error
console.log('--> UpstreamImageSyncProvider: Image upload failed:\n', JSON.stringify(err));
reject(err);
})
});
}
uploadOfflineImages() {
if (!this.hasNetworkConnection()) {
return;
}
this.jsonStoreHandler.getObjectStorageAccess().then(objectStorageAccess => {
if (objectStorageAccess != null) {
console.log('--> UpstreamImageSyncProvider: Listing images to be uploaded in ' + this.offlineImagesDir+ ' ...');
this.file.listDir(this.file.dataDirectory, this.offlineImagesDir).then((entries) => {
entries.forEach((entry) => {
console.log('--> UpstreamImageSyncProvider: Uploading image ' + entry.name + ' ...');
this.uploadImageToServer(entry.name, entry.nativeURL, objectStorageAccess).then(() => {
console.log('--> UpstreamImageSyncProvider: Removing cached file ' + entry.nativeURL + ' ...');
entry.remove(() => {
console.log('--> UpstreamImageSyncProvider: Successfully removed cached file ' + entry.nativeURL);
}, (err) => {
console.log('--> UpstreamImageSyncProvider: Error removing cached file:\n' + JSON.stringify(err));
});
}).catch((err) => {
console.log('--> UpstreamImageSyncProvider: Error uploading image to server:\n' + JSON.stringify(err));
});
});
}).catch((err) => {
console.log('--> UpstreamImageSyncProvider: Error listing files in uploadOfflineImages: ' + JSON.stringify(err));
});
}
});
}
}
Update IonicMobileApp/src/pages/report-new/report-new.ts
as below:
...
// import { MyWardDataProvider } from '../../providers/my-ward-data/my-ward-data';
import { JsonStoreHandlerProvider } from '../../providers/json-store-handler/json-store-handler';
import { UpstreamImageSyncProvider } from '../../providers/upstream-image-sync/upstream-image-sync';
...
export class ReportNewPage {
...
constructor(public navCtrl: NavController, public navParams: NavParams,
private camera : Camera, private alertCtrl: AlertController, private imageResizer: ImageResizer,
private loadingCtrl: LoadingController, private toastCtrl: ToastController, private authHandler:AuthHandlerProvider,
private jsonStoreHandler:JsonStoreHandlerProvider, private upstreamImageSync: UpstreamImageSyncProvider) {
console.log('--> ReportNewPage constructor() called');
}
...
submit() {
...
this.loader.present().then(() => {
// this.myWardDataProvider.uploadNewGrievance(grievance).then(
this.jsonStoreHandler.addNewGrievance(grievance).then(
(response) => {
this.loader.dismiss();
this.showToast('Data Uploaded Successfully');
this.loader = this.loadingCtrl.create({
content: 'Uploading image to server. Please wait ...',
dismissOnPageChange: true
});
this.loader.present().then(() => {
// this.myWardDataProvider.uploadImage(imageFilename, this.capturedImage).then(
this.upstreamImageSync.uploadImage(imageFilename, this.capturedImage).then(
(response) => {
this.imageResizer.resize(this.getImageResizerOptions()).then(
(filePath: string) => {
// this.myWardDataProvider.uploadImage(thumbnailImageFilename, filePath).then(
this.upstreamImageSync.uploadImage(imageFilename, this.capturedImage).then(
(response) => {
this.loader.dismiss();
this.showToast('Image Uploaded Successfully');
this.showAlert('Upload Successful', 'Successfully uploaded problem report to server', false, () => {
// this.myWardDataProvider.data.push(grievance);
this.navCtrl.pop();
})
}, (failure) => {
...
});
}).catch(e => {
...
});
}, (failure) => {
...
});
});
}, (failure) => {
...
}
);
});
}
}
Update IonicMobileApp/src/pages/home/home.html
as below:
...
<ion-content padding>
<ion-list>
<button ion-item (click)="itemClick(grievance)" *ngFor="let grievance of grievances">
<ion-thumbnail item-left>
<img img-cache img-cache-src="{{objectStorageAccess.baseUrl}}{{grievance.json.picture.thumbnail}}">
</ion-thumbnail>
<h2 text-wrap>{{grievance.json.problemDescription}}</h2>
<p>@ {{grievance.json.address}}</p>
</button>
</ion-list>
<ion-list>
<button ion-item (click)="itemClickOfflineData(grievance)" *ngFor="let grievance of offlineGrievances">
<ion-thumbnail item-left>
<img src="{{offlineDirPath}}/{{grievance.json.picture.thumbnail}}">
</ion-thumbnail>
<h2 text-wrap>{{grievance.json.problemDescription}}</h2>
<p>@ {{grievance.json.address}}</p>
</button>
</ion-list>
</ion-content>
Update IonicMobileApp/src/pages/home/home.ts
as below:
...
// import { MyWardDataProvider } from '../../providers/my-ward-data/my-ward-data';
import { UpstreamImageSyncProvider } from '../../providers/upstream-image-sync/upstream-image-sync';
...
export class HomePage {
...
offlineGrievances: any;
offlineDirPath: string;
constructor(public navCtrl: NavController, public loadingCtrl: LoadingController,
public imgCache: ImgCacheService, private authHandler:AuthHandlerProvider,
private jsonStoreHandler:JsonStoreHandlerProvider, private upstreamImageSync: UpstreamImageSyncProvider) {
console.log('--> HomePage constructor() called');
}
...
ionViewWillEnter() {
console.log('--> HomePage ionViewWillEnter() called');
this.initAuthChallengeHandler();
this.jsonStoreHandler.setOnSyncSuccessCallback(() => {
let view = this.navCtrl.getActive();
if (view.instance instanceof HomePage) {
console.log('--> HomePage onSyncSuccessCallback() called');
this.loadData();
}
});
this.loadOfflineDataFromJsonStore();
}
loadData() {
if (this.loader == null) {
console.log('--> HomePage creating new loader');
this.loader = this.loadingCtrl.create({
content: 'Loading data. Please wait ...'
});
this.loader.present().then(() => {
this.loadOfflineDataFromJsonStore();
this.loadDataFromJsonStore();
});
} else {
console.log('--> HomePage reusing previous loader');
this.loadOfflineDataFromJsonStore();
this.loadDataFromJsonStore();
}
}
loadDataFromJsonStore() {
this.jsonStoreHandler.getObjectStorageAccess().then(objectStorageAccess => {
if (objectStorageAccess != null) {
this.objectStorageAccess = objectStorageAccess;
this.imgCache.init({
headers: {
'Authorization': this.objectStorageAccess.authorizationHeader
}
}).then( () => {
console.log('--> HomePage initialized imgCache');
this.jsonStoreHandler.getData().then(data => {
this.grievances = data;
this.loader.dismiss();
this.loader = null;
this.upstreamImageSync.uploadOfflineImages();
});
});
} else {
console.log('--> HomePage objectStorageAccess not yet loaded');
}
});
}
...
loadOfflineDataFromJsonStore() {
this.jsonStoreHandler.getUnSyncedData().then(data => {
this.offlineGrievances = data;
this.offlineDirPath = this.upstreamImageSync.getOfflineDirPath();
});
}
itemClickOfflineData(grievance) {
this.navCtrl.push(ProblemDetailPage, { grievance: grievance, baseUrl: this.offlineDirPath });
}
...
}
Delete redundant functions from IonicMobileApp/src/providers/my-ward-data/my-ward-data.ts
which now looks as below:
/// <reference path="../../../plugins/cordova-plugin-mfp/typings/worklight.d.ts" />
import { Injectable } from '@angular/core';
@Injectable()
export class MyWardDataProvider {
constructor() {
console.log('--> MyWardDataProvider constructor() called');
}
getObjectStorageAccess() {
// console.log('--> MyWardDataProvider getting Object Storage AuthToken from adapter ...');
return new Promise((resolve, reject) => {
let dataRequest = new WLResourceRequest("/adapters/MyWardData/objectStorage", WLResourceRequest.GET);
dataRequest.send().then((response) => {
// console.log('--> MyWardDataProvider got Object Storage AuthToken from adapter ', response);
resolve(response.responseJSON);
}, (failure) => {
console.log('--> MyWardDataProvider failed to get Object Storage AuthToken from adapter\n', JSON.stringify(failure));
reject(failure);
})
});
}
}