Skip to content
This repository has been archived by the owner on Jul 18, 2024. It is now read-only.

Latest commit

 

History

History
2437 lines (2044 loc) · 89.1 KB

DETAILED.md

File metadata and controls

2437 lines (2044 loc) · 89.1 KB

Steps

  1. Setup Ionic and MFP CLI
  2. Create Ionic Sample Application
  1. Add pre-emptive login
  1. Fetch data from Cloudant database via MFP Adapter
  1. Use IBM Cloud Object Storage for storing and retrieving images
  1. Show problem details page with location marked on Google Maps
  2. Capture image and geolocation and upload to server

Step 1. Setup Ionic and MFP CLI

$ node --version
v8.6.0
  • Install Cordova
$ sudo npm install -g cordova
$ cordova --version
7.0.1

Note: If you are on Windows, instead of using sudo, run the above command (and the ones below) in a command prompt opened in administrative mode.

  • Install Ionic
$ sudo npm install -g ionic
$ ionic --version
3.19.0
  • Install IBM MobileFirst Platform CLI
$ sudo npm install -g mfpdev-cli
$ mfpdev --version
8.0.0-2017091111

Note: While installing MFP CLI, if you hit an error saying npm ERR! package.json npm can't find a package.json file in your current directory., then it is most likely due to MFP CLI not being supported in your npm version. In such a case, downgrade your npm as below, and then install MFP CLI.

$ sudo npm install -g npm@3.10.10
$ git --version
git version 2.9.3 ...
  • Install Maven: On Mac, you can use brew install for installing Maven as shown below:
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install maven
$ mvn --version
Apache Maven 3.5.0 ...
$ java -version
java version "1.8.0_101"
apm install atom-typescript

Step 2. Create Ionic Sample Application

2.1 Create a new Ionic project

Create a new Ionic project with blank starter template

$ ionic start IonicMobileApp blank
✔ Creating directory ./IonicMobileApp - done!
...
? Would you like to integrate your new app with Cordova to target native iOS and Android? Yes
...
> npm i
...
> git init

? Install the free Ionic Pro SDK and connect your app? No
...
> git add -A
> git commit -m "Initial commit" --no-gpg-sign
...

Change directory to the newly created project:

$ cd ./IonicMobileApp

2.2 Start a local dev server for app dev/testing

To get a preview of the application, Ionic/Cordova provides a feature by the which the application can be launched in a browser by using the cordova serve or ionic serve as shown below:

$ ionic serve -c
[INFO] Starting app-scripts server: --address 0.0.0.0 --port 8100 
       --livereload-port 35729 --dev-logger-port 53703 --consolelogs --nobrowser - Ctrl+C to cancel
[17:20:10]  watch started ... 
[17:20:10]  build dev started ... 
[17:20:10]  clean started ... 
[17:20:10]  clean finished in 1 ms 
[17:20:10]  copy started ... 
[17:20:10]  deeplinks started ... 
[17:20:10]  deeplinks finished in 22 ms 
[17:20:10]  transpile started ... 
[17:20:13]  transpile finished in 3.58 s 
[17:20:13]  preprocess started ... 
[17:20:14]  copy finished in 3.83 s 
[17:20:14]  preprocess finished in 185 ms 
[17:20:14]  webpack started ... 
[17:20:21]  webpack finished in 7.48 s 
[17:20:21]  sass started ... 
[17:20:22]  sass finished in 1.01 s 
[17:20:22]  postprocess started ... 
[17:20:22]  postprocess finished in 5 ms 
[17:20:22]  lint started ... 
[17:20:22]  build dev finished in 12.36 s 
[17:20:22]  watch ready in 12.42 s 
[17:20:22]  dev server running: http://localhost:8100/ 

[INFO] Development server running!
       Local: http://localhost:8100
       External: http://192.xxx.xxx.xxx:8100, http://9.xxx.xxx.xxx:8100
       DevApp: IonicMobileApp@8100 on shivahr

The above command also launches the Cordova live-reload workflow. The live-reload feature watches for changes in your source files and automatically builds the project and reloads the application in browser.

Since the ionic serve command continues to run in foreground, to be able to run further Cordova/Ionic commands open a new terminal and change directory to the project.

2.3 Update App ID, Name and Description

Update IonicMobileApp/config.xml as below. Change id, name, description and author details appropriately.


<?xml version='1.0' encoding='utf-8'?>
<widget id="org.mycity.myward" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:mfp="http://www.ibm.com/mobilefirst/cordova-plugin-mfp">
    <name>MyWard</name>
    <description>Get your civic issues resolved by posting through this app.</description>
    <author email="shivahr@gmail.com" href="https://developer.ibm.com/code/author/shivahr/">Shiva Kumar H R</author>
...

2.4 Run application on Android phone

2.4.1 Install Android Studio and Android SDK platform

  • Download and install Android Studio from https://developer.android.com/studio/index.html
  • Install Android SDK Platform 23 (or higher) as below:
    • Launch Android Studio.
    • Click on Configure -> SDK Manager.
    • Make a note of the Android SDK Location.
    • Under SDK Platforms, select Android 6.0 (Marshmallow) API Level 23 or higher. Click Apply and then click OK. This will install Android SDK Platform on your machine.

2.4.2 Enable developer options and USB debugging on your Android phone

  • Enable USB debugging on your Android phone as per the steps in https://developer.android.com/studio/debug/dev-options.html
    • Launch the Settings app on your phone. Select About Device -> Software Info. Tap Build number 7 times to enable developer options.
    • Return to Settings list. Select Developer options and enable USB debugging.
  • If you are developing on Windows, then you need to install the appropriate USB driver as per instructions in https://developer.android.com/studio/run/oem-usb.html.
  • Connect the Android phone to your development machine by USB cable, and accept allow access on your phone.

2.4.3 Enable Android platform for Ionic application

$ ionic cordova platform add android@6.3.0
> cordova platform add android@6.3.0 --save
...

Note: Make sure the Cordova platform version being added is supported by the MobileFirst plug-ins. Site https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/8.0/application-development/sdk/cordova/ lists the supported levels.

$ cordova platform version
Installed platforms:
  android 6.3.0
Available platforms: 
  blackberry10 ~3.8.0 (deprecated)
  browser ~4.1.0
  ios ~4.4.0
  osx ~4.0.1
  webos ~3.7.0

Cordova Android 6.3.0 targets Android API level of API 26. Instead of that, if you want to target API 23 or a different one, then edit IonicMobileApp/config.xml and add preference for android-targetSdkVersion as shown below.

  <preference name="android-minSdkVersion" value="16" />
  <preference name="android-targetSdkVersion" value="23" />

Note: Please make sure you install the Android SDK Platform for the API level that you use as per instructions in Step 2.4.1.

2.4.4 Build/Run the Ionic application on Android phone

  • Build Android application
$ ionic cordova build android

 Note: In case the Cordova build fails due to missing ANDROID_HOME and JAVA_HOME environment variables, then set those environment variables as per instructions in https://cordova.apache.org/docs/en/latest/guide/platforms/android/#setting-environment-variables. ANDROID_HOME should be set to the Android SDK Location that you noted in Step 2.4.1. Command /usr/libexec/java_home returns the value to be used for setting JAVA_HOME on macOS. On other platforms you could run java -XshowSettings:properties 2>&1 | grep 'java.home' as mentioned here.

  • Run application on Android device
$ ionic cordova run android

Upon app launch, the sample page should get displayed as shown below:

Snapshot of app running on Android device

2.5 Update App Logo and Splash

Reference: Automating Icons and Splash Screens https://blog.ionic.io/automating-icons-and-splash-screens/

Copy your desired app icon to IonicMobileApp/resources/icon.png and app splash to IonicMobileApp/resources/splash.png.

$ cd ../IonicMobileApp
$ ionic cordova resources

For running ionic cordova resources command, you would need to sign up on ionicframework.com and specify the credentials on the command line.

Optionally, install the StatusBar plugin as below:

$ ionic cordova plugin add cordova-plugin-statusbar
> cordova plugin add cordova-plugin-statusbar --save
Installing "cordova-plugin-statusbar" for android

2.6 Fix issue where you see a blank screen after your splash screen disappears

Reference: http://www.codingandclimbing.co.uk/blog/ionic-2-fix-splash-screen-white-screen-issue

In IonicMobileApp/config.xml, add preferences for AutoHideSplashScreen and FadeSplashScreen after SplashScreenDelay as shown below:


...
<widget id=...>
  <preference name="SplashScreenDelay" value="3000" />
  <preference name="AutoHideSplashScreen" value="false" />
  <preference name="FadeSplashScreen" value="false" />
  ...

Update IonicMobileApp/src/app/app.component.ts as below:


...
export class MyApp {
  ...
    platform.ready().then(() => {
      ...
      statusBar.styleDefault();
      setTimeout(() => {
        splashScreen.hide();
      }, 100);
    });
  }
}

Step 3. Add pre-emptive login

3.1 Create login page

3.1.1 Add Login UI

$ ionic generate page login
[OK] Generated a page named login!

Update IonicMobileApp/src/pages/login/login.html as below:


<ion-header>
  <ion-navbar>
    <ion-title>Login</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <form (submit)="processForm()" [formGroup]="form">
    <ion-list>
      <ion-item>
        <ion-label fixed>Username</ion-label>
        <ion-input formControlName="username" type="text"></ion-input>
      </ion-item>
      <ion-item>
        <ion-label fixed>Password</ion-label>
        <ion-input formControlName="password" type="password"></ion-input>
      </ion-item>
    </ion-list>
    <div padding>
      <button ion-button block type="submit">Sign In</button>
    </div>
  </form>
</ion-content>

3.1.2 Handle login action

Add the code for handling pre-emptive login

Update IonicMobileApp/src/pages/login/login.ts as below:


import { Component } from '@angular/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';
import { FormGroup, FormControl, Validators } from '@angular/forms';

 // @IonicPage() 
@Component({
  selector: 'page-login',
  templateUrl: 'login.html',
})
export class LoginPage {
  form;

  constructor(public navCtrl: NavController, public navParams: NavParams,
      public alertCtrl: AlertController) {
    console.log('--> LoginPage constructor() called');
    this.form = new FormGroup({
      username: new FormControl("", Validators.required),
      password: new FormControl("", Validators.required)
    });
  }

  processForm() {
    // Reference: https://github.com/driftyco/ionic-preview-app/blob/master/src/pages/inputs/basic/pages.ts
    let username = this.form.value.username;
    let password = this.form.value.password;
    if (username === "" || password === "") {
      this.showAlert('Login Failure', 'Username and password are required');
      return;
    }
    console.log('--> Sign-in with user: ' + username);
    this.showAlert('Login', 'Signing-in as ' + username);
  }

  showAlert(alertTitle, alertMessage) {
    let prompt = this.alertCtrl.create({
      title: alertTitle,
      message: alertMessage,
      buttons: [{
        text: 'Ok',
      }]
    });
    prompt.present();
  }

  ionViewDidLoad() {
    console.log('--> LoginPage ionViewDidLoad() called');
  }

}

3.1.3 Show login page upon app launch

Update IonicMobileApp/src/app/app.module.ts as below:


...
import { MyApp } from './app.component';
import { LoginPage } from '../pages/login/login'
import { HomePage } from '../pages/home/home'

@NgModule({
  declarations: [
    MyApp,
    LoginPage,
    HomePage
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    LoginPage,
    HomePage
  ],
  providers: [
    ...
  ]
})
export class AppModule {}

Update IonicMobileApp/src/app/app.component.ts as below:


...
import { SplashScreen } from '@ionic-native/splash-screen';
import { LoginPage } from '../pages/login/login'

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage:any = LoginPage;

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) {
    ...
  }
}

3.1.4 Build/Run the Ionic application on Android phone

$ ionic cordova build android
$ ionic cordova run android

Upon app launch, the login page should get displayed as shown below.

MyWard App - Login Page

3.2 Create Mobile Foundation service and configure MFP CLI

In the IBM Cloud Dashboard, click on Catalog and select Mobile Foundation service under Platform -> Mobile. Click on Create as shown below.

Create IBM Mobile Foundation service

In the Mobile Foundation service overview page that gets shown, click on Service credentials. Expand View credentials and make a note of the url, user and password as shown below.

IBM Mobile Foundation service credentials

  • Back on your local machine, configure MFP CLI to work with Mobile Foundation server by running following command in console.

    Note: For Enter the fully qualified URL of this server:, enter the url mentioned in credentials followed by :443 (the default HTTPS port).

$ mfpdev server add
? Enter the name of the new server profile: Cloud-MFP
? Enter the fully qualified URL of this server: https://mobilefoundation-71-hb-server.mybluemix.net:443
? Enter the MobileFirst Server administrator login ID: admin
? Enter the MobileFirst Server administrator password: **********
? Save the administrator password for this server?: Yes
? Enter the context root of the MobileFirst administration services: mfpadmin
? Enter the MobileFirst Server connection timeout in seconds: 30
? Make this server the default?: Yes
Verifying server configuration...
The following runtimes are currently installed on this server: mfp
Server profile 'Cloud-MFP' added successfully.

$ mfpdev server info
Name         URL
--------------------------------------------------------------------------------------
Cloud-MFP  https://mobilefoundation-71-hb-server.mybluemix.net:443        [Default]
--------------------------------------------------------------------------------------

3.3 Add MFP Security Adapter

https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/8.0/authentication-and-security/user-authentication/security-check/

Create directory for MFP Adapters

$ cd ..
$ mkdir MobileFoundationAdapters
$ cd MobileFoundationAdapters

Download UserLogin adapter from https://github.com/MobileFirst-Platform-Developer-Center/SecurityCheckAdapters/tree/release80/UserLogin. This is a simple security adapter that returns success when password equals username.

$ curl -LOk https://github.com/MobileFirst-Platform-Developer-Center/SecurityCheckAdapters/archive/release80.zip
$ unzip release80.zip
$ mv SecurityCheckAdapters-release80/UserLogin/ .
$ rm -rf SecurityCheckAdapters-release80/ release80.zip
$ ls
UserLogin

Edit MobileFoundationAdapters/UserLogin/src/main/adapter-resources/adapter.xml and update defaultValue of successStateExpirationSec and rememberMeDurationSec as below. This is to ensure that a successful login is remembered for 10 minutes. Update defaultValue of blockedStateExpirationSec to 300 (5 minutes).


<mfp:adapter name="UserLogin" ...>
  ...
  <securityCheckDefinition name="UserLogin" class="com.sample.UserLogin">
    <property name="maxAttempts" defaultValue="3" description="How many attempts are allowed" type="integer"/>
    <property name="blockedStateExpirationSec" defaultValue="300" description="How long before the client can try again (seconds)" type="integer"/>
    <property name="successStateExpirationSec" defaultValue="600" description="How long is a successful state valid for (seconds)" type="integer"/>
    <property name="rememberMeDurationSec" defaultValue="600" description="How long is the user remembered when using RememberMe (seconds)" type="integer"/>
  </securityCheckDefinition>
</mfp:adapter>

Build and deploy the UserLogin sample adapter

$ cd ./UserLogin
$ 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

3.4 Add the Cordova plugin for MFP

Make sure you have enabled Android/iOS platform for the Ionic application as mentioned in Step 2.4.3 before continuing with the below steps.

Add Cordova plugin for MFP as shown below.

$ cd ../../IonicMobileApp/
$ cordova plugin add cordova-plugin-mfp
Installing "cordova-plugin-mfp" for android
...

3.5 Register the app to MobileFirst Server

$ mfpdev app register
Verifying server configuration...
Registering to server:'https://mobilefoundation-71-hb-server.mybluemix.net:443' runtime:'mfp'
Updated config.xml file located at: .../Ionic-MFP-App/IonicMobileApp/config.xml
Run 'cordova prepare' to propagate changes.
Registered app for platform: android

Propagate changes by running cordova prepare

$ cordova prepare

3.6 Create a new provider in Ionic mobile app to assist in handling MFP security challenges

Generate a new provider using Ionic CLI

$ ionic generate provider AuthHandler
[OK] Generated a provider named AuthHandler!

Update IonicMobileApp/src/providers/auth-handler.ts as below:


/// <reference path="../../../plugins/cordova-plugin-mfp/typings/worklight.d.ts" />
import { Injectable } from '@angular/core';

@Injectable()
export class AuthHandlerProvider {
  securityCheckName = 'UserLogin';
  userLoginChallengeHandler;
  initialized = false;
  username = null;

  isChallenged = false;
  handleChallengeCallback = null;
  loginSuccessCallback = null;
  loginFailureCallback = null;

  constructor() {
    console.log('--> AuthHandlerProvider constructor() called');
  }

  // Reference: https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/8.0/authentication-and-security/credentials-validation/javascript/
  init() {
    if (this.initialized) {
      return;
    }
    this.initialized = true;
    console.log('--> AuthHandler init() called');
    this.userLoginChallengeHandler = WL.Client.createSecurityCheckChallengeHandler(this.securityCheckName);
    // https://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback
    this.userLoginChallengeHandler.handleChallenge = this.handleChallenge.bind(this);
    this.userLoginChallengeHandler.handleSuccess = this.handleSuccess.bind(this);
    this.userLoginChallengeHandler.handleFailure = this.handleFailure.bind(this);
  }

  setHandleChallengeCallback(onHandleChallenge) {
    console.log('--> AuthHandler setHandleChallengeCallback() called');
    this.handleChallengeCallback = onHandleChallenge;
  }

  setLoginSuccessCallback(onSuccess) {
    console.log('--> AuthHandler setLoginSuccessCallback() called');
    this.loginSuccessCallback = onSuccess;
  }

  setLoginFailureCallback(onFailure) {
    console.log('--> AuthHandler setLoginFailureCallback() called');
    this.loginFailureCallback = onFailure;
  }

  handleChallenge(challenge) {
    console.log('--> AuthHandler handleChallenge called.\n', JSON.stringify(challenge));
    this.isChallenged = true;
    if (challenge.errorMsg !== null && this.loginFailureCallback != null) {
      var statusMsg = 'Remaining attempts = ' + challenge.remainingAttempts + '
' + challenge.errorMsg; this.loginFailureCallback(statusMsg); } else if (this.handleChallengeCallback != null) { this.handleChallengeCallback(); } else { console.log('--> AuthHandler: handleChallengeCallback not set!'); } } handleSuccess(data) { console.log('--> AuthHandler handleSuccess called'); this.isChallenged = false; if (this.loginSuccessCallback != null) { this.loginSuccessCallback(); } else { console.log('--> AuthHandler: loginSuccessCallback not set!'); } } handleFailure(error) { console.log('--> AuthHandler handleFailure called.\n' + JSON.stringify(error)); this.isChallenged = false; if (this.loginFailureCallback != null) { this.loginFailureCallback(error.failure); } else { console.log('--> AuthHandler: loginFailureCallback not set!'); } } // Reference: https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/8.0/authentication-and-security/user-authentication/javascript/ checkIsLoggedIn() { console.log('--> AuthHandler checkIsLoggedIn called'); WLAuthorizationManager.obtainAccessToken('UserLogin') .then( (accessToken) => { console.log('--> AuthHandler: obtainAccessToken onSuccess'); }, (error) => { console.log('--> AuthHandler: obtainAccessToken onFailure: ' + JSON.stringify(error)); } ); } login(username, password) { console.log('--> AuthHandler login called. isChallenged = ', this.isChallenged); this.username = username; if (this.isChallenged) { this.userLoginChallengeHandler.submitChallengeAnswer({'username':username, 'password':password}); } else { // https://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback var self = this; WLAuthorizationManager.login(this.securityCheckName, {'username':username, 'password':password}) .then( (success) => { console.log('--> AuthHandler: login success'); }, (failure) => { console.log('--> AuthHandler: login failure: ' + JSON.stringify(failure)); self.loginFailureCallback(failure.errorMsg); } ); } } logout() { console.log('--> AuthHandler logout called'); WLAuthorizationManager.logout(this.securityCheckName) .then( (success) => { console.log('--> AuthHandler: logout success'); }, (failure) => { console.log('--> AuthHandler: logout failure: ' + JSON.stringify(failure)); } ); }
}

3.7 Initialize AuthHandler after MobileFirst SDK is loaded

Update IonicMobileApp/src/app/app.component.ts as below:


import { Component, Renderer } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';

import { LoginPage } from '../pages/login/login'
import { AuthHandlerProvider } from '../providers/auth-handler/auth-handler';

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage:any = LoginPage;

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen,
    renderer: Renderer, private authHandler: AuthHandlerProvider) {
    console.log('--> MyApp constructor() called');

    renderer.listenGlobal('document', 'mfpjsloaded', () => {
      console.log('--> MyApp mfpjsloaded');
      this.authHandler.init();
    })

    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      console.log('--> MyApp platform.ready() called');
      statusBar.styleDefault();
      splashScreen.hide();
    });
  }

}

3.8 Update Login controller to use MFP based user authentication

Add the code for handling pre-emptive login

Update IonicMobileApp/src/pages/login/login.ts as below:


import { Component } from '@angular/core';
import { NavController, NavParams, AlertController, LoadingController } from 'ionic-angular';
import { FormGroup, FormControl, Validators } from '@angular/forms';

import { AuthHandlerProvider } from '../../providers/auth-handler/auth-handler';
import { HomePage } from '../home/home';

@Component({
  selector: 'page-login',
  templateUrl: 'login.html',
})
export class LoginPage {
  form;
  loader: any;

  constructor(public navCtrl: NavController, public navParams: NavParams,
      public alertCtrl: AlertController, public authHandler:AuthHandlerProvider, public loadingCtrl: LoadingController) {
    console.log('--> LoginPage constructor() called');

    this.form = new FormGroup({
      username: new FormControl("", Validators.required),
      password: new FormControl("", Validators.required)
    });

    this.authHandler.setLoginFailureCallback((error) => {
      this.loader.dismiss();
      if (error !== null) {
        this.showAlert('Login Failure', error);
      } else {
        this.showAlert('Login Failure', 'Failed to login.');
      }
    });
    this.authHandler.setLoginSuccessCallback(() => {
      let view = this.navCtrl.getActive();
      if (!(view.instance instanceof HomePage )) {
        this.navCtrl.setRoot(HomePage);
      }
    });
    this.authHandler.setHandleChallengeCallback(() => {
      this.navCtrl.setRoot(LoginPage);
    });
  }

  processForm() {
    // Reference: https://github.com/driftyco/ionic-preview-app/blob/master/src/pages/inputs/basic/pages.ts
    let username = this.form.value.username;
    let password = this.form.value.password;
    if (username === "" || password === "") {
      this.showAlert('Login Failure', 'Username and password are required');
      return;
    }
    console.log('--> Sign-in with user: ', username);
    this.loader = this.loadingCtrl.create({
      content: 'Signining in. Please wait ...',
      dismissOnPageChange: true
    });
    this.loader.present().then(() => {
      this.authHandler.login(username, password);
    });
  }

  showAlert(alertTitle, alertMessage) {
    ...
  }

  ionViewDidLoad() {
    ...
  }

}

3.9 Test pre-emptive login

Build/Run the Ionic application on Android phone as below:

$ ionic cordova build android
$ ionic cordova run android

Upon app launch, the login page should get displayed as before in Step 3.1.4. Test by specifying any matching username and password (say Username: Test and Password: Test). Login should succeed and the sample home page should get shown as in Step 2.4.4.

Step 4. Fetch data from Cloudant database via MFP Adapter

4.1 Create Cloudant database and populate it with sample data

  • Log in to IBM Cloud Dashboard and create Cloudant NoSQL DB service.
  • From the welcome page of Cloudant service that you just created, launch the Cloudant Dashboard.
  • In the Cloudant dashboard, click on Databases.
  • Click on Create Database. Specify name of database as myward as shown below. Click Create.

Create Database in Cloudant NoSQL DB

Once the database is created, the dashboard will update to show the documents inside myward database (which, as expected, will be empty to begin with).

  • Click Create Document. Under document content, after the auto populated _id field, enter grievance details as shown below. Please note that you need to put a comma (,) after the auto populated _id field.

{
  "_id": "50e9c4a69196a00201463ef2f9ffece5",
  "reportedBy": "shivahr@gmail.com",
  "reportedDateTime": "20171125_152627",
  "picture": {
    "large": "IMG-20171125-WA0012.jpeg",
    "thumbnail": "thumbnail_IMG-20171125-WA0012.jpg"
  },
  "problemDescription": "Car parking on busy market road chocking movement of other vehicles and pedestrians",
  "geoLocation": {
    "type": "Point",
    "coordinates": [
      77.7893168,
      13.0773568
    ]
  },
  "address": "Basaveshwara Temple road (behind Market Road), Hosakote, Bangalore 562114"
}

Click Create Document to create/save the document.

The myward database should now list the six documents as shown below under Table view:

Cloudant database populated with sample data

4.2 Create MFP adapter to query Cloudant data

Reference: https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/8.0/adapters/cloudant/#java-adapters

4.2.1 Download sample MFP Java adapter for Cloudant

Download MFP Java adapter for Cloudant from https://github.com/MobileFirst-Platform-Developer-Center/CloudantAdapter/tree/release80/Adapters/CloudantJava

$ cd ../MobileFoundationAdapters/
$ curl -LOk https://github.com/MobileFirst-Platform-Developer-Center/CloudantAdapter/archive/release80.zip
$ unzip release80.zip
$ mv CloudantAdapter-release80/Adapters/CloudantJava/ ./MyWardData
$ rm -rf CloudantAdapter-release80/ release80.zip
$ ls
MyWardData	UserLogin

4.2.2 Point the MFP adapter to your Cloudant service instance

Generate Cloudant API Key

  • In the Cloudant dashboard, under myward database, click on Permissions and then click on Generate API Key as shown in the snapshot below.
  • Make a note of the Key and Password generated.
  • The newly added key would get listed under Cloudant users with default permission of _reader only. Select the checkbox under _writer next to the new key to give it write permission as well.

Generate Cloudant API Key

Specify Cloudant credentials in MFP adapter

  • Open MobileFoundationAdapters/MyWardData/src/main/adapter-resources/adapter.xml and update the properties key and password as per the newly generated API key.
  • For property account, specify the Cloudant dashboard URL portion upto (and including) -bluemix.cloudant.com as shown in the snapshot above.
  • For property DBName, specify value myward.

4.2.3 Update adapter methods to return MyWard Grievances data

  • Open MobileFoundationAdapters/MyWardData/pom.xml and change the value of fields artifactId and name to MyWardData.

  • Open MobileFoundationAdapters/MyWardData/src/main/adapter-resources/adapter.xml and change the value of mfp:adapter name, displayName and description to MyWardData.

  • Create new file MobileFoundationAdapters/MyWardData/src/main/java/com/sample/MyWardGrievance.java with following contents:

package com.sample;

public class MyWardGrievance {
	public String _id, _rev;
	public String reportedBy;
	public String reportedDateTime;
	public static class PictureInfo {
		public String large;
		public String thumbnail;
	}
	public PictureInfo picture;
	public String problemDescription;
	public static class GeoLocation {
		public String type = "Point";
		public Number[] coordinates = new Number[2];
	}
	public GeoLocation geoLocation;
	public String address;

	boolean hasRequiredFields() {
		if (reportedBy != null && !reportedBy.isEmpty() && reportedDateTime != null && !reportedDateTime.isEmpty()
				&& picture != null && picture.large != null && !picture.large.isEmpty() && picture.thumbnail != null
				&& !picture.thumbnail.isEmpty() && problemDescription != null && !problemDescription.isEmpty()
				&& geoLocation != null && address != null && !address.isEmpty()) {
			return true;
		}
		return false;
	}
}
  • Update MobileFoundationAdapters/MyWardData/src/main/java/com/sample/CloudantJavaResource.java as below:

...
import com.ibm.mfp.adapter.api.OAuthSecurity;

@Path("/")
@OAuthSecurity(scope = "RestrictedData")
public class CloudantJavaResource {
	...

	private Database getDB() throws Exception {
		...
	}

	@POST
	@Consumes(MediaType.APPLICATION_JSON)
	public Response addEntry(MyWardGrievance myWardGrievance) throws Exception {
		if (myWardGrievance != null && myWardGrievance.hasRequiredFields()) {
			getDB().save(myWardGrievance);
			return Response.ok().build();
		} else {
			return Response.status(400).build();
		}
	}

	@DELETE
	@Path("/{id}")
	public Response deleteEntry(@PathParam("id") String id) throws Exception {
		try {
			MyWardGrievance myWardGrievance = getDB().find(MyWardGrievance.class, id);
			getDB().remove(myWardGrievance);
			return Response.ok().build();
		} catch (NoDocumentException e) {
			return Response.status(404).build();
		}
	}

	@GET
	@Produces("application/json")
	public Response getAllEntries() throws Exception {
		List entries = getDB().view("_all_docs").includeDocs(true).query(MyWardGrievance.class);
		return Response.ok(entries).build();
	}
}

Note: We are protecting all the REST APIs of this adapter with a custom security scope of RestrictedData (by using @OAuthSecurity at class level) which we will map to UserLogin security check in Step 4.2.5.

  • Delete MobileFoundationAdapters/MyWardData/src/main/java/com/sample/User.java

4.2.4 Build and Deploy the MFP adapter

$ cd MyWardData/
$ mfpdev adapter build
...
$ mfpdev adapter deploy
...

4.2.5 Map MyWardData's protecting scope to UserLogin security check

Launch MFP Dashboard as below:

  • In the IBM Cloud dashboard, under Cloud Foundry Services, click on the Mobile Foundation service you created in Step 3.2. The service overview page that gets shown, will have the MFP dashboard embedded within it. You can also open the MFP dashboard in a separate browser tab by appending /mfpconsole to the url mentioned in Step 3.2.
  • Inside the MFP dashboard, in the list on the left, you will see the MyWard application, and MyWardData and UserLogin adapters listed.
  • Click on the MyWardData adapter. Click on Resources tab. You should see the various REST APIs exposed by MyWardData adapter. The Security column should show the protecting scope RestrictedData against each REST method.

Map RestrictedData scope to UserLogin security check as below:

  • In the MFP dashboard, under Applications click on MyWard application. Click on Android and click on Security tab. Click on New button under Scope-Elements Mapping as shown below.
  • Specify Scope element as RestrictedData, and under Custom Security Checks select UserLogin as shown below. Click on Add. The new mapping should get created and shown under Scope-Elements Mapping.

The REST APIs of MyWardData adapter are protected by RestrictedData security scope

  • Repeat above steps for Applications -> MyWard -> iOS in case you add Cordova platform for iOS as well.

4.2.6 Test the newly created MFP adapter

Create temporary credentials to test adapter REST API as below:

  • Inside the MFP dashboard, click on Runtime Settings. Click on Confidential Clients. Then click on New.
  • In the form that pops up, specify values for ID and Secret as shown in snapshot below. For Allowed Scope enter ** and click on Add. Finally click on Save.

MFP - Create Confidential Client to test Adapter REST APIs

Test adapter REST API as below:

  • Inside the MFP dashboard, click on the MyWardData adapter. Click on Resources and then click on View Swagger Docs. The Swagger UI for adapter REST APIs will get shown in a new window/tab.
  • Inside the Swagger UI, click on Expand Operations.
  • To test the GET / API, first click on OFF toggle button to enable authentication. Select Default Scope and click on Authorize as shown below. Enter the ID and Secret created above against Username and Password. Click OK. If authentication is successful, the toggle button will switch to ON position.

Authorize Swagger UI for running MFP Adapter REST APIs

  • Now click on Try it out button to run the GET / API. The API response should get shown in the Response Body as shown in snapshot below.

Swagger UI for testing MobileFirst Adapters

Delete the temporary credentials after testing adapter REST API as below:

  • Inside the MFP dashboard, click on Runtime Settings. Click on Confidential Clients.
  • Delete the Client ID created previously.

Step 4.3 Update Ionic app to fetch and display data from MFP Adapter

4.3.1 Create a new provider in Ionic app for calling MFP adapter API

Generate a new provider in Ionic app:

$ cd ../../IonicMobileApp/
$ ionic generate provider MyWardDataProvider
[OK] Generated a provider named MyWardDataProvider!

Update IonicMobileApp/src/providers/my-ward-data/my-ward-data.ts as below:


/// <reference path="../../../plugins/cordova-plugin-mfp/typings/worklight.d.ts" /> 
import { Injectable } from '@angular/core';

@Injectable()
export class MyWardDataProvider {
  data: any = null;

  constructor() {
    console.log('--> MyWardDataProvider constructor() called');
  }

  load() {
    console.log('--> MyWardDataProvider loading data from adapter ...');
    return new Promise((resolve, reject)  => {
      if (this.data) {
        // already loaded data
        return resolve(this.data);
      }
      // don't have the data yet
      let dataRequest = new WLResourceRequest("/adapters/MyWardData", WLResourceRequest.GET);
      dataRequest.send().then(
        (response) => {
          console.log('--> MyWardDataProvider loaded data from adapter\n', response);
          this.data = response.responseJSON;
          resolve(this.data);
        }, (failure) => {
          console.log('--> MyWardDataProvider failed to load data\n', JSON.stringify(failure));
          reject(failure);
        })
    });
}

4.3.2 Modify home page to display the list of problems reported

  • Update src/pages/home/home.ts as below:

import { Component } from '@angular/core';
import { NavController, LoadingController } from 'ionic-angular';
import { MyWardDataProvider } from '../../providers/my-ward-data/my-ward-data';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  loader: any;
  grievances: any;

  constructor(public navCtrl: NavController, public loadingCtrl: LoadingController,
    public myWardDataProvider: MyWardDataProvider) {
    console.log('--> HomePage constructor() called');
  }

  ionViewDidLoad() {
    console.log('--> HomePage ionViewDidLoad() called');
    this.loadData();
  }

  loadData() {
    this.loader = this.loadingCtrl.create({
      content: 'Loading data. Please wait ...',
    });
    this.loader.present().then(() => {
      this.myWardDataProvider.load().then(data => {
        this.loader.dismiss();
        this.grievances = data;
      });
    });
  }

}
  • Update src/pages/home/home.html as below:

<ion-header>
  <ion-navbar>
    <ion-title>
      Problems Reported
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item *ngFor="let grievance of grievances">
      <ion-thumbnail item-left>
        <img src="{{grievance.picture.thumbnail}}">
      </ion-thumbnail>
      <h2 text-wrap>{{grievance.problemDescription}}</h2>
      <p>@ {{grievance.address}}</p>
    </ion-item>
  </ion-list>
</ion-content>

4.3.3 Test updated home page

Build/Run the Ionic application on Android phone as below:

$ ionic cordova build android
$ ionic cordova run android

After app launch and successful login, the home page should display the sample list of problems created in Step 4.1. As of now, the image thumbnails would be blank.

Step 5. Use IBM Cloud Object Storage for storing and retrieving images

5.1 Create IBM Cloud Object Storage service and API key

5.1.1 Create IBM Cloud Object Storage service and populate it with sample data

  • In the IBM Cloud Dashboard, click on Catalog and select Object Storage service under Infrastructure -> Storage. Click on Create as shown below.

    Create IBM Cloud Object Storage service
  • The IBM Cloud Object Storage dashboard will get shown. In the Buckets and objects page, click on Create bucket. Give a unique name for the bucket. Leave the default selections as-is for Resiliency (Cross Region), Location (us-geo) and Storage class (Standard), and click on Create as shown below.

    Create a bucket in IBM Cloud Object Storage
  • The Bucket overview page for the newly created bucket will get shown. Click on Add objects. In Upload obects dialog, click on Add files and select all the images under SampleData directory (the six images and their thumbnails). Click Open. Click on Upload as shown below. Once upload is complete, you should see the images listed under your bucket.

    Upload objects to IBM Cloud Object Storage

5.1.2 Create Service ID and API Key for accessing objects

  • Create Service ID

    • In a separate browser tab/window, launch the IBM Cloud Identity & Access Management dashboard using URL https://console.bluemix.net/iam/.
    • In case you have multiple IBM Cloud accounts, then select the target Account, Region, Organization and Space.
    • Under Identity & Access (on the left side of the page), select Service IDs and click Create. Give a name and description, and click Create.
    • Make a note of the Service ID as shown below.
    Copy Service ID from IBM Cloud Identity and Access Management dashboard
  • Add Cloud Object Storage Writer role to that service ID

    • Back in IBM Cloud Object Storage dashboard, select Bucket permissions under Buckets and objects.
    • Click on Service IDs tab. Under Select a service ID, select the service ID created in the above step. Under Assign a role to this service ID for this bucket, select Writer. Click Create policy as shown below. You should get a confirmation dialog saying “Service permission created“.
    Add Writer role to the Service ID in IBM Cloud Object Storage
  • Create API Key

    • Back in IBM Cloud Identity & Access Management dashboard, under Service IDs, click on the service ID created earlier. Under Access policies, you should see the Writer role for your bucket.
    • Click on API keys tab and then click on Create button. In the Create API key dialog, give a name and description for the API key and click on Create. You should get a confirmation dialog saying API key successfully created as shown below.
    • Click on Download and save the API key as shown below. Note: This is the only time you will see the key. You cannot retrieve it later.
    • Finally click on Close.
    Create API key and download in IBM Cloud Identity and Access Management

5.2 Add function in MFP Adapter to fetch Authorization token from IBM Cloud Object Storage

Add ibm-cos-java-sdk dependency to MobileFoundationAdapters/MyWardData/pom.xml as below:


<?xml version="1.0" encoding="UTF-8"?>
<project ...>
  ...
  <dependencies>
    ...
    <dependency>
      <groupId>com.ibm.cos</groupId>
      <artifactId>ibm-cos-java-sdk</artifactId>
      <version>1.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>4.5.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpcore</artifactId>
      <version>4.4.5</version>
    </dependency>
  </dependencies>
  ...
</project>

Update MobileFoundationAdapters/MyWardData/src/main/adapter-resources/adapter.xml as shown in code snippet below.


<mfp:adapter name="MyWardData" ...>
  ...
  <property name="DBName" displayName="Cloudant DB name" defaultValue="myward"/>

  <property name="endpointURL" displayName="Cloud Object Storage Endpoint Public URL" defaultValue="https://s3-api.us-geo.objectstorage.softlayer.net"/>
  <property name="bucketName" displayName="Cloud Object Storage Bucket Name" defaultValue=""/>
  <property name="serviceId" displayName="Cloud Object Storage Service ID" defaultValue=""  />
  <property name="apiKey" displayName="Cloud Object Storage API Key" defaultValue=""/>
</mfp:adapter>

Add file MobileFoundationAdapters/MyWardData/src/main/java/com/sample/ObjectStorageAccess.java with contents as below:


package com.sample;

public class ObjectStorageAccess {
	public String baseUrl;
	public String authorizationHeader;

	public ObjectStorageAccess(String baseUrl, String authToken) {
		this.baseUrl = baseUrl;
		this.authorizationHeader = "Bearer " + authToken;
	}
}

Edit MobileFoundationAdapters/MyWardData/src/main/java/com/sample/CloudantJavaApplication.java as below:


package com.sample;
...
import com.amazonaws.SDKGlobalConfiguration;
import com.ibm.oauth.BasicIBMOAuthCredentials;
import com.ibm.oauth.IBMOAuthCredentials;
import com.ibm.oauth.OAuthServiceException;

public class CloudantJavaApplication extends MFPJAXRSApplication{
	...
	@Context
	ConfigurationAPI configurationAPI;

	public Database db = null;

	private IBMOAuthCredentials oAuthCreds = null;
	private String baseUrl = "";

	protected void init() throws Exception {
		...

		String endpointURL = configurationAPI.getPropertyValue("endpointURL");
		String bucketName = configurationAPI.getPropertyValue("bucketName");
		String serviceId = configurationAPI.getPropertyValue("serviceId");
		String apiKey = configurationAPI.getPropertyValue("apiKey");

		if (!endpointURL.isEmpty() && !bucketName.isEmpty() && !serviceId.isEmpty() && !apiKey.isEmpty()) {
			try {
				SDKGlobalConfiguration.IAM_ENDPOINT = "https://iam.bluemix.net/oidc/token";
				oAuthCreds = new BasicIBMOAuthCredentials(apiKey, serviceId);
				// initialize fetching and caching of token
				oAuthCreds.getTokenManager().getToken();
				this.baseUrl = endpointURL + "/" + bucketName + "/";
			} catch (OAuthServiceException e) {
				throw new Exception("Unable to connect to Object Storage, check the configuration.");
			}
		}
	}

	public ObjectStorageAccess getObjectStorageAccess() {
		return new ObjectStorageAccess(this.baseUrl, oAuthCreds.getTokenManager().getToken());
	}
	...
}

Edit MobileFoundationAdapters/MyWardData/src/main/java/com/sample/CloudantJavaResource.java as below:


...
@Path("/")
public class CloudantJavaResource {

	@Context
	AdaptersAPI adaptersAPI;

	...

	@GET
	@Path("/objectStorage")
	@Produces("application/json")
	public Response getObjectStorageAccess() throws Exception {
		CloudantJavaApplication app = adaptersAPI.getJaxRsApplication(CloudantJavaApplication.class);
		return Response.ok(app.getObjectStorageAccess()).build();
	}
}

Build and deploy the modified MFP adapter

$ cd ../MobileFoundationAdapters/MyWardData/
$ mfpdev adapter build
$ mfpdev adapter deploy

Test the newly added API as per instructions in Step 4.2.6. The GET API on /objectStorage should return a JSON object containing baseUrl and authorizationHeader as shown below.

Test the newly added API in MFP Adapter for getting Cloud Object Storage Authorization token

5.3 Modify Ionic App to display images

5.3.1 Use imgcache.js for downloading and caching images

For downloading and caching images in the Ionic App, we will use the ng-imgcache library. ng-imgcache uses the popular imgcache.js library that is based on cordova-plugin-file and cordova-plugin-file-transfer plugins.

$ cd ../../IonicMobileApp/
$ npm install ng-imgcache --save
$ ionic cordova plugin add cordova-plugin-file
$ ionic cordova plugin add cordova-plugin-file-transfer

Update IonicMobileApp/src/app/app.module.ts as below:


...
import { StatusBar } from '@ionic-native/status-bar';
import { ImgCacheModule } from 'ng-imgcache';

import { MyApp } from './app.component';
...
@NgModule({
  ...
  imports: [
    BrowserModule,
    ImgCacheModule,
    IonicModule.forRoot(MyApp)
  ],
  ...
})
export class AppModule {}

5.3.2 Add additional function in MyWardDataProvider to call the new MFP adapter function

Update IonicMobileApp/src/providers/my-ward-data/my-ward-data.ts as below.


...
@Injectable()
export class MyWardDataProvider {
  data: any = null;
  objectStorageAccess: any = null;

  constructor() {
    ...
  }

  load() {
    ...
  }

  getObjectStorageAccess() {
    // console.log('--> MyWardDataProvider getting Object Storage AuthToken from adapter ...');
    return new Promise((resolve, reject) => {
      if (this.objectStorageAccess) {
        // already loaded data
        return resolve(this.objectStorageAccess);
      }
      let dataRequest = new WLResourceRequest("/adapters/MyWardData/objectStorage", WLResourceRequest.GET);
      dataRequest.send().then(
        (response) => {
          // console.log('--> MyWardDataProvider got Object Storage AuthToken from adapter ', response);
          this.objectStorageAccess = response.responseJSON;
          resolve(this.objectStorageAccess);
        }, (failure) => {
          console.log('--> MyWardDataProvider failed to get Object Storage AuthToken from adapter\n', JSON.stringify(failure));
          reject(failure);
        })
    });
}

5.3.3 Update Home page to display images

Update IonicMobileApp/src/pages/home/home.ts as below.


import { Component } from '@angular/core';
import { NavController, LoadingController } from 'ionic-angular';
import { ImgCacheService } from 'ng-imgcache';

import { MyWardDataProvider } from '../../providers/my-ward-data/my-ward-data';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
  loader: any;
  grievances: any;
  objectStorageAccess: any;

  constructor(public navCtrl: NavController, public loadingCtrl: LoadingController,
    public myWardDataProvider: MyWardDataProvider, public imgCache: ImgCacheService) {
    console.log('--> HomePage constructor() called');
  }

  ionViewDidLoad() {
    console.log('--> HomePage ionViewDidLoad() called');
    this.loadData();
  }

  loadData() {
    this.loader = this.loadingCtrl.create({
      content: 'Loading data. Please wait ...',
    });
    this.loader.present().then(() => {
      this.myWardDataProvider.load().then(data => {
        this.myWardDataProvider.getObjectStorageAccess().then(objectStorageAccess => {
          this.objectStorageAccess = objectStorageAccess;
          this.imgCache.init({
            headers: {
              'Authorization': this.objectStorageAccess.authorizationHeader
            }
          }).then( () => {
            console.log('--> HomePage initialized imgCache');
            this.loader.dismiss();
            this.grievances = data;
          });
        });
      });
    });
  }
}

Update IonicMobileApp/src/pages/home/home.html as below:


<ion-header>
...
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item *ngFor="let grievance of grievances">
      <ion-thumbnail item-left>
        <img img-cache img-cache-src="{{objectStorageAccess.baseUrl}}{{grievance.picture.thumbnail}}">
      </ion-thumbnail>
      <h2 text-wrap>{{grievance.problemDescription}}</h2>
      <p>@ {{grievance.address}}</p>
    </ion-item>
  </ion-list>
</ion-content>

5.3.4 Build/Run the Ionic application on Android phone

$ ionic cordova build android
$ ionic cordova run android

After login, the home page should display the list of problems reported along with image thumbnails as shown below.

MyWard App - Home Page

Step 6. Show problem details page with location marked on Google Maps

Get an API key for using the Google Maps Android API as per instructions in https://developers.google.com/maps/documentation/android-api/signup.

Install Cordova plugin for Google Maps https://ionicframework.com/docs/native/google-maps/

$ ionic cordova plugin add cordova-plugin-googlemaps --variable API_KEY_FOR_ANDROID="<Your_API_Key_for_using_GoogleMaps_Android_API>"
$ npm install --save @ionic-native/google-maps

Generate a new page for ProblemDetail

$ ionic generate page ProblemDetail
[OK] Generated a page named ProblemDetail!

Update IonicMobileApp/src/pages/home/home.html as below:


<ion-header>
...
</ion-header>

<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.picture.thumbnail}}">
      </ion-thumbnail>
      <h2 text-wrap>{{grievance.problemDescription}}</h2>
      <p>@ {{grievance.address}}</p>
    </button>
  </ion-list>
</ion-content>

Update IonicMobileApp/src/pages/home/home.ts as below:


...
import { ProblemDetailPage } from '../problem-detail/problem-detail';
...
export class HomePage {
  ...

  // https://www.joshmorony.com/a-simple-guide-to-navigation-in-ionic-2/
  itemClick(grievance) {
    this.navCtrl.push(ProblemDetailPage, { grievance: grievance, baseUrl: this.objectStorageAccess.baseUrl });
  }
}

Update IonicMobileApp/src/pages/problem-detail/problem-detail.ts as below.


import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { GoogleMaps, GoogleMap, GoogleMapsEvent, GoogleMapOptions, Marker, LatLng } from '@ionic-native/google-maps';

// @IonicPage()
@Component({
  selector: 'page-problem-detail',
  templateUrl: 'problem-detail.html',
})
export class ProblemDetailPage {
  grievance: any;
  baseUrl: any;
  map: GoogleMap;

  constructor(public navCtrl: NavController, public navParams: NavParams) {
    console.log('--> ProblemDetailPage constructor() called');
    this.grievance = navParams.get('grievance');
    this.baseUrl = navParams.get('baseUrl');
  }

  ionViewDidLoad() {
    console.log('--> ProblemDetailPage ionViewDidLoad() called');
    this.loadMap();
  }

  loadMap() {
    let loc = new LatLng(this.grievance.geoLocation.coordinates[1], this.grievance.geoLocation.coordinates[0]);
    let mapOptions: GoogleMapOptions= {
      camera: {
        target: loc,
        zoom: 15,
        tilt: 10
      }
    };
    this.map = GoogleMaps.create('map', mapOptions);
    this.map.one(GoogleMapsEvent.MAP_READY).then(() => {
      this.map.addMarker({
        title: 'Problem Location',
        position: loc
      }).then((marker: Marker) => {
        marker.showInfoWindow();
      }).catch(err => {
        console.log(err);
      });
    });
  }
}

Delete file IonicMobileApp/src/pages/problem-detail/problem-detail.module.ts.

Update IonicMobileApp/src/pages/problem-detail/problem-detail.html as below:


<ion-header>
  <ion-navbar>
    <ion-title>
      MyWard Problem Details
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <h2 text-wrap>{{grievance.problemDescription}}</h2>
  <p>Reported on: {{grievance.reportedDateTime}}</p>
  <img img-cache img-cache-src="{{baseUrl}}{{grievance.picture.large}}">
  <p text-wrap>@ {{grievance.address}}</p>
  <div id="map"></div>
</ion-content>

Update IonicMobileApp/src/pages/problem-detail/problem-detail.scss as below.


page-problem-detail {
  #map {
    height: 90%;
    width: 90%;
  }
}

Update IonicMobileApp/src/app/app.module.ts as below.


...
import { GoogleMaps } from '@ionic-native/google-maps';
import { ProblemDetailPage } from '../pages/problem-detail/problem-detail';
@NgModule({
  declarations: [
    MyApp,
    LoginPage,
    HomePage,
    ProblemDetailPage
  ],
  imports: [
    BrowserModule,
    ImgCacheModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    LoginPage,
    HomePage,
    ProblemDetailPage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    AuthHandlerProvider,
    MyWardDataProvider,
    GoogleMaps
  ]
})
export class AppModule {}

Build/Run the Ionic application on Android phone

$ ionic cordova build android
$ ionic cordova run android

Upon clicking of on any of the problems reported on the home page, a detail page should open up showing bigger image and the location should be marked on Google Maps as shown below.

MyWard App - Problem Detail Page

Step 7. Capture image and geolocation and upload to server

$ ionic cordova plugin add cordova-plugin-camera
$ npm install --save @ionic-native/camera

$ ionic cordova plugin add info.protonet.imageresizer
$ npm install --save @ionic-native/image-resizer

$ ionic cordova plugin add cordova-plugin-file-transfer
$ npm install --save @ionic-native/file-transfer

$ ionic cordova plugin add cordova-plugin-nativegeocoder
$ npm install --save @ionic-native/native-geocoder

Generate a new page for reporting new problem.

$ ionic generate page ReportNew
[OK] Generated a page named ReportNew!

Update IonicMobileApp/src/pages/home/home.html as below.


<ion-header>
  <ion-navbar>
    <ion-title>
      Problems Reported
    </ion-title>
    <ion-buttons end>
      <button ion-button icon-only (click)="reportNewProblem()">
        <ion-icon name="add"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding>
  ...
</ion-content>

Update IonicMobileApp/src/pages/home/home.ts as below.


...
import { ReportNewPage } from '../report-new/report-new';
...
export class HomePage {
  ...

  reportNewProblem(){
    this.navCtrl.push(ReportNewPage);
  }
}

Update IonicMobileApp/src/app/app.module.ts as below.


...
import { Camera } from '@ionic-native/camera';
import { ImageResizer } from '@ionic-native/image-resizer';
import { FileTransfer } from '@ionic-native/file-transfer';
import { NativeGeocoder } from '@ionic-native/native-geocoder';
import { ReportNewPage } from '../pages/report-new/report-new';
@NgModule({
  declarations: [
    MyApp,
    LoginPage,
    HomePage,
    ProblemDetailPage,
    ReportNewPage
  ],
  imports: [
    BrowserModule,
    ImgCacheModule,
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    LoginPage,
    HomePage,
    ProblemDetailPage,
    ReportNewPage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
    AuthHandlerProvider,
    MyWardDataProvider,
    GoogleMaps,
    Camera,
    ImageResizer,
    FileTransfer,
    NativeGeocoder,
  ]
})
export class AppModule {}

Update IonicMobileApp/src/providers/my-ward-data/my-ward-data.ts as below.


...
import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer';

@Injectable()
export class MyWardDataProvider {
  ...
  constructor(private transfer: FileTransfer) {
    console.log('--> MyWardDataProvider constructor() called');
  }
  ...
  uploadNewGrievance(grievance) {
    return new Promise( (resolve, reject) => {
      console.log('--> MyWardDataProvider: Uploading following new grievance to server ...\n' + JSON.stringify(grievance));
      let dataRequest = new WLResourceRequest("/adapters/MyWardData", WLResourceRequest.POST);
      dataRequest.setHeader("Content-Type","application/json");
      dataRequest.send(grievance).then(
        (response) => {
          console.log('--> MyWardDataProvider: Upload successful:\n', response);
          resolve(response)
        }, (failure) => {
          console.log('--> MyWardDataProvider: Upload failed:\n', failure);
          reject(failure)
        })
    });
  }

  uploadImage(fileName, filePath) {
    return new Promise( (resolve, reject) => {
      let serverUrl = this.objectStorageAccess.baseUrl + fileName;
      console.log('--> MyWardDataProvider: Uploading image (' + filePath + ') to server (' + serverUrl + ') ...');
      let options: FileUploadOptions = {
        fileKey: 'file',
        fileName: fileName,
        httpMethod: 'PUT',
        headers: {
          'Authorization': this.objectStorageAccess.authorizationHeader,
          'Content-Type': 'image/jpeg'
        }
      }
      let fileTransfer: FileTransferObject = this.transfer.create();
      fileTransfer.upload(filePath, serverUrl, options).then((data) => {
        // success
        console.log('--> MyWardDataProvider: Image upload successful:\n', data);
        resolve(data);
      }, (err) => {
        // error
        console.log('--> MyWardDataProvider: Image upload failed:\n', err);
        reject(err);
      })
    });
  }
}

Update IonicMobileApp/src/pages/report-new/report-new.html as below.


<ion-header>
  <ion-navbar>
    <ion-title>Report New Problem</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-list>
    <ion-item>
      <ion-label fixed>Description</ion-label>
      <ion-input type="text" [(ngModel)]="description"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label fixed>Address</ion-label>
      <ion-input type="text" [(ngModel)]="address"></ion-input>
    </ion-item>
  </ion-list>
  <img [src]="capturedImage" *ngIf="capturedImage" />
  <ion-grid>
    <ion-row>
      <ion-col col-6>
        <button ion-button full (click)="takePhoto()" >
          <ion-icon name="camera"></ion-icon>
          Take Photo
        </button>
      </ion-col>
      <ion-col col-6>
        <button ion-button full (click)="captureLocation()">
          <ion-icon name="locate"></ion-icon>
          Get My Location
        </button>
      </ion-col>
    </ion-row>
  </ion-grid>
  <div id="map"></div>
  <button ion-button full (click)="submit()">
    <ion-icon name="cloud-upload"></ion-icon>
    Submit
  </button>
</ion-content>

Update IonicMobileApp/src/pages/report-new/report-new.ts as below.


import { Component, NgZone } from '@angular/core';
import { NavController, NavParams, AlertController, LoadingController, ToastController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera';
import { GoogleMaps, GoogleMap, GoogleMapsEvent, GoogleMapOptions, Marker, LatLng, MyLocation } from '@ionic-native/google-maps';
import { NativeGeocoder, NativeGeocoderReverseResult } from '@ionic-native/native-geocoder';
import { ImageResizer, ImageResizerOptions } from '@ionic-native/image-resizer';

import { MyWardDataProvider } from '../../providers/my-ward-data/my-ward-data';
import { AuthHandlerProvider } from '../../providers/auth-handler/auth-handler';

// @IonicPage()
@Component({
  selector: 'page-report-new',
  templateUrl: 'report-new.html',
})
export class ReportNewPage {
  capturedImage: string = null;
  mapReady: boolean = false;
  map: GoogleMap;
  description: string = '';
  address: string = '';
  location: LatLng = null;
  loader: any;

  constructor(public navCtrl: NavController, public navParams: NavParams, public zone: NgZone,
    private camera: Camera, private alertCtrl: AlertController, private imageResizer: ImageResizer,
    private loadingCtrl: LoadingController, private toastCtrl: ToastController, private nativeGeocoder: NativeGeocoder,
    private myWardDataProvider: MyWardDataProvider, private authHandler:AuthHandlerProvider) {
    console.log('--> ReportNewPage constructor() called');
  }

  ionViewDidLoad() {
    console.log('--> ReportNewPage ionViewDidLoad() called');
    this.createMap();
  }

  // https://ionicframework.com/docs/native/camera/
  takePhoto() {
    const options : CameraOptions = {
      quality: 90, // picture quality
      destinationType: this.camera.DestinationType.FILE_URI,
      encodingType: this.camera.EncodingType.JPEG,
      correctOrientation: true,
      saveToPhotoAlbum: true
    }
    this.camera.getPicture(options) .then((imageData) => {
        // this.capturedImage = "data:image/jpeg;base64," + imageData;
        this.capturedImage = imageData;
      }, (err) => {
        console.log(err);
      }
    );
  }

  createMap() {
    // TODO need to store/retrieve prevLoc in app preferences/local storage
    let prevLoc = new LatLng(13.0768342, 77.7886087);
    let mapOptions: GoogleMapOptions = {
      camera: {
        target: prevLoc,
        zoom: 15,
        tilt: 10
      }
    };
    this.map = GoogleMaps.create('map', mapOptions);
    this.map.one(GoogleMapsEvent.MAP_READY).then(() => {
      console.log('--> ReportNewPage: Map is Ready To Use');
      this.mapReady = true;
      // https://stackoverflow.com/questions/4537164/google-maps-v3-set-single-marker-point-on-map-click
      this.map.on(GoogleMapsEvent.MAP_CLICK).subscribe( event => {
        this.location = event[0];
        console.log('--> ReportNewPage: User clicked location = ' + event[0]);
        this.map.clear();
        this.map.addMarker({
          title: 'Selected location',
          position: event[0]
        }).then((marker: Marker) => {
          this.autoFillAddress();
          marker.showInfoWindow();
        });
      });
    });
  }

  captureLocation() {
    if (!this.mapReady) {
      this.showAlert('Map is not yet ready', 'Map is not ready yet. Please try again.');
      return;
    }
    this.map.clear();

    // Get the location of you
    this.map.getMyLocation().then((location: MyLocation) => {
      this.location = location.latLng;
      console.log('--> ReportNewPage: Device Location = ' + JSON.stringify(location, null, 2));
      // Move the map camera to the location with animation
      this.map.animateCamera({
        target: location.latLng,
        zoom: 17,
        tilt: 30
      }).then(() => {
        // add a marker
        this.map.addMarker({
          title: 'Your device location',
          snippet: 'Accurate to ' + location.accuracy + ' meters!',
          position: location.latLng,
          animation: 'BOUNCE'
        }).then((marker: Marker) => {
          this.autoFillAddress();
          marker.showInfoWindow();
        });
      })
    }).catch(err => {
      this.showAlert('Try again', err.error_message);
      console.log(err);
    });
  }

  autoFillAddress() {
    let lat = this.location.lat;
    let lng = this.location.lng;
    this.nativeGeocoder.reverseGeocode(lat , lng).then((result: NativeGeocoderReverseResult) => {
      console.log('--> ReportNewPage: Result of reverseGeocode(' + lat + ', ' + lng + ') = ' + JSON.stringify(result));
      let address = result[0];
      let str = '';
      if (address.subLocality) {
        str += address.subLocality + ", ";
      }
      if (address.locality) {
        str += address.locality +  ", ";
      }
      if (address.subAdministrativeArea) {
        str += address.subAdministrativeArea + ", ";
      }
      if (address.administrativeArea) {
        str += address.administrativeArea + ", ";
      }
      if (address.countryName) {
        str += address.countryName + ".";
      }
      // https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html
      this.zone.run(() => {
        this.address = str;
      });
      console.log('--> ReportNewPage: Reverse geocoded address = ' + str);
    }) .catch((error: any) => {
      console.log(error)
    });
  }

  showAlert(alertTitle, alertMessage, enableBackdropDismiss: boolean = true, okHandler?) {
    // Disable the map - https://stackoverflow.com/questions/45500031/ionic-3-unable-to-click-on-alert-dialog-shown-above-google-maps
    this.map.setClickable(false);
    let prompt = this.alertCtrl.create({
      title: alertTitle,
      message: alertMessage,
      buttons: [{
        text: 'Ok',
        handler: () => {
          // Enable the map again - https://stackoverflow.com/questions/45500031/ionic-3-unable-to-click-on-alert-dialog-shown-above-google-maps
          this.map.setClickable(true);
          if (okHandler) {
            okHandler();
          }
        }
      }],
      enableBackdropDismiss: enableBackdropDismiss
    });
    prompt.present();
  }

  showToast(message: string) {
    let toast = this.toastCtrl.create({
      message: message,
      duration: 2000,
      position: 'bottom'
    });
    toast.present(toast);
  }

  submit() {
    if (this.description === "") {
      this.showAlert('Missing Description', 'Please add a description for the problem you are reporting.');
      return;
    }
    if (this.address === "") {
      this.showAlert('Missing Address', 'Please specify the address of problem location.');
      return;
    }
    if (this.capturedImage === null) {
      this.showAlert('Missing Photo', 'Please take a photo of the problem location.');
      return;
    }
    if (this.location === null) {
      this.showAlert('Missing Geo Location', 'Please mark the location of problem on Maps.');
      return;
    }

    let username = this.authHandler.username;
    let timestamp = this.getDateTime();
    let imageFilename = timestamp + '_' + username + '.jpeg';
    let thumbnailImageFilename = 'thumbnail_' + imageFilename;
    let grievance = {
      "reportedBy": username,
      "reportedDateTime": timestamp,
      "picture": {
        "large": imageFilename,
        "thumbnail": thumbnailImageFilename
      },
      "problemDescription": this.description,
      "geoLocation": {
        "type": "Point",
        "coordinates": [
          this.location.lng,
          this.location.lat
        ]
      },
      "address": this.address
    }

    this.loader = this.loadingCtrl.create({
      content: 'Uploading data to server. Please wait ...',
      dismissOnPageChange: true
    });
    this.loader.present().then(() => {
      this.myWardDataProvider.uploadNewGrievance(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(
              (response) => {
                this.imageResizer.resize(this.getImageResizerOptions()).then(
                  (filePath: string) => {
                    this.myWardDataProvider.uploadImage(thumbnailImageFilename, filePath).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) => {
                        this.loader.dismiss();
                        this.showAlert('Thumbnail Upload Failed', 'Encountered following error while uploading thumbnail image to server:\n' + failure.errorMsg);
                    });
                  }).catch(e => {
                    console.log(e)
                    this.showAlert('Error Creating Thumbnail', 'Encountered following error while creating thumbnail:\n' + JSON.stringify(e));
                  });
              }, (failure) => {
                this.loader.dismiss();
                this.showAlert('Image Upload Failed', 'Encountered following error while uploading image to server:\n' + failure.errorMsg);
              });
          });
        }, (failure) => {
          this.loader.dismiss();
          this.showAlert('Data Upload Failed', 'Encountered following error while uploading data to server:\n' + failure.errorMsg);
        });
    });
  }

  getImageResizerOptions() {
    let options = {
      uri: this.capturedImage,
      quality: 90,
      width: 400,
      height: 400
    } as ImageResizerOptions;
    return options;
  }

  getDateTime() {
    // https://stackoverflow.com/questions/10211145/getting-current-date-and-time-in-javascript
    let currentdate = new Date();
    let fullYear = currentdate.getFullYear();
    let month = (((currentdate.getMonth()+1) < 10)? "0" : "") + (currentdate.getMonth()+1);
    let date = ((currentdate.getDate() < 10)? "0" : "") + currentdate.getDate();
    let hours = ((currentdate.getHours() < 10)? "0" : "") + currentdate.getHours();
    let minutes = ((currentdate.getMinutes() < 10)? "0" : "") + currentdate.getMinutes();
    let seconds = ((currentdate.getSeconds() < 10)? "0" : "") + currentdate.getSeconds();
    let datetime = fullYear + month + date + "_" + hours + minutes + seconds;
    return datetime;
  } 

}

Update IonicMobileApp/src/pages/report-new/report-new.scss as below.


page-report-new {
  #map {
    height: 90%;
    width: 90%;
  }
}

Delete file IonicMobileApp/src/pages/report-new/report-new.module.ts.

Build/Run the Ionic application on Android phone

$ ionic cordova build android
$ ionic cordova run android

Upon clicking the + button on the home page, the Report New Problem page should show up, allowing the user to specify problem description and address as shown below. User should be able to take a photo of the problem and specify the location of problem either by grabbing device's geo-location or by marking the location on Maps.

MyWard App - Report New Problem Page

Add refresh button in Home page:

Update IonicMobileApp/src/pages/home/home.html as below.


<ion-header>
  <ion-navbar>
    <ion-buttons start>
      <button ion-button icon-only (click)="refresh()">
        <ion-icon name="refresh"></ion-icon>
      </button>
    </ion-buttons>
    <ion-title>
      Problems Reported
    </ion-title>
    <ion-buttons end>
      <button ion-button icon-only (click)="reportNewProblem()">
        <ion-icon name="add"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding>
  ...
</ion-content>

Update IonicMobileApp/src/pages/home/home.ts as below.


...
export class HomePage {
  ...
  refresh() {
    this.myWardDataProvider.data = null;
    this.loadData();
  }
}

Handle login timeout in Report New Problem page and Home page

Update IonicMobileApp/src/pages/login/login.ts as below:


...
export class LoginPage {
  form;
  loader: any;
  isPushed = null;
  isUsernameDisabled: boolean = false;
  fixedUsername = null;

  constructor(public navCtrl: NavController, public navParams: NavParams,
    public alertCtrl: AlertController, public authHandler:AuthHandlerProvider, public loadingCtrl: LoadingController) {
    console.log('--> LoginPage constructor() called');

    this.isPushed = navParams.get('isPushed');
    this.fixedUsername = navParams.get('fixedUsername');
    if (this.fixedUsername != null) {
      this.isUsernameDisabled = true;
    }

    this.form = new FormGroup({
      username: new FormControl({value: this.fixedUsername, disabled: this.isUsernameDisabled}, Validators.required),
      password: new FormControl("", Validators.required)
    });

    this.authHandler.setLoginFailureCallback((error) => {
      this.loader.dismiss();
      if (error !== null) {
        this.showAlert('Login Failure', error);
      } else {
        this.showAlert('Login Failure', 'Failed to login.');
      }
    });
    if (this.isPushed == null) {
      this.authHandler.setLoginSuccessCallback(() => {
        let view = this.navCtrl.getActive();
        if (!(view.instance instanceof HomePage )) {
          this.navCtrl.setRoot(HomePage);
        }
      });
      this.authHandler.setHandleChallengeCallback(() => {
        this.navCtrl.setRoot(LoginPage);
      });
    }
  }

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

  showAlert(alertTitle, alertMessage) {
    ...
  }
  ...
}

Update IonicMobileApp/src/pages/report-new/report-new.ts as below:


...
import { LoginPage } from '../login/login';
...
export class ReportNewPage {
  ...
  ionViewDidLoad() {
    console.log('--> ReportNewPage ionViewDidLoad() called');
    this.createMap();
    this.initAuthChallengeHandler();
  }
  ...
  initAuthChallengeHandler() {
    this.authHandler.setHandleChallengeCallback(() => {
      this.navCtrl.push(LoginPage, { isPushed: true, fixedUsername: this.authHandler.username });
    });
    this.authHandler.setLoginSuccessCallback(() => {
      let view = this.navCtrl.getActive();
      if (view.instance instanceof LoginPage) {
        this.navCtrl.pop().then(() =>{
          this.loader = this.loadingCtrl.create({
            content: 'Uploading data to server. Please wait ...'
          });
          this.loader.present();
        });
      }
    });
  }
}

Update IonicMobileApp/src/pages/home/home.ts as below:


...
import { AuthHandlerProvider } from '../../providers/auth-handler/auth-handler';
import { LoginPage } from '../login/login';
...
export class HomePage {
  ...
  constructor(public navCtrl: NavController, public loadingCtrl: LoadingController,
    public myWardDataProvider: MyWardDataProvider, public imgCache: ImgCacheService,
    private authHandler:AuthHandlerProvider) {
    console.log('--> HomePage constructor() called');
  }

  ...

  ionViewWillEnter() {
    console.log('--> HomePage ionViewWillEnter() called');
    this.initAuthChallengeHandler();
  }

  initAuthChallengeHandler() {
    this.authHandler.setHandleChallengeCallback(() => {
      this.loader.dismiss();
      this.navCtrl.push(LoginPage, { isPushed: true });
    });
    this.authHandler.setLoginSuccessCallback(() => {
      let view = this.navCtrl.getActive();
      if (view.instance instanceof LoginPage) {
        this.navCtrl.pop().then(() =>{
          this.loader = this.loadingCtrl.create({
            content: 'Loading data. Please wait ...'
          });
          this.loader.present();
        });
      }
    });
  }
}