Implementing GPS Detection in MAF

Oracle MAF does not currently provide an out of the box solution for detecting whether GPS is enabled (at least as of version 2.1.2). This is problematic when using a function such as startLocationMonitor, as when there is no GPS the app will lock up for about 15 seconds and then display an ADF exception that cannot be caught.

This article will show one approach to solving this problem.

Step 1. Create the MAF app

We’re going to set up an app with a single welcome feature, containing a page which displays our gps status. We’ll also put a call to start Location Monitor which will trigger off of the gps status.

Step through the new MAF application wizard – I have called my app GPSTest and given it an application prefix of com.rubiconred.test.gpstest.  Create a feature called welcome and set it to be an amx page. Create the page and call it welcome.amx.

We need to put the call to the Cordova plugin somewhere – so let’s embed that in a new Javascript page. Right click on the View Controller-> Web Content directory and choose to Create Javascript file. Call it gps.js and add this to the feature reference as shown below.

1. create gps.js

2. maf-application.xml

At this point we are missing the crucial piece – the Cordova plugin! The one used in this example can be found at https://github.com/fastrde/phonegap-checkGPS . Note that there seem to now be several different plugins available to do similar things. Extract the zip and then edit the maf-application.xml to point to the plugin directory. Also tick the geolocation plugin as this is a dependency (if this isn’t ticked then the Jdeveloper build will connect and download the plugin).

3. plugins configuration

Update gps.js and paste the following.

CheckGPS.check(function(){

    //GPS is enabled!

    alert("GPS is available");

  },

  function(){

    //GPS is disabled!

alert('GPS is not available');

  });

Finally, to aid in testing, drag the ApplicationFeatures->resetFeature(String) method onto the welcome.amx page to replace the command button in the primary facet.  At the prompt enter the feature id, which in this case is com.rubiconred.test.gpstest.welcome.

<?xml version="1.0" encoding="UTF-8" ?>

<amx:view xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amx="http://xmlns.oracle.com/adf/mf/amx"

xmlns:dvtm="http://xmlns.oracle.com/adf/mf/amx/dvt">

  <amx:panelPage id="pp1">

    <amx:facet name="header">

<amx:outputText value="Header" id="ot1"/>

    </amx:facet>

    <amx:facet name="primary">

      <amx:commandButton actionListener="#{bindings.resetFeature.execute}" text="resetFeature"

disabled="#{!bindings.resetFeature.enabled}" id="cb3"/>

    </amx:facet>

    <amx:facet name="secondary">

<amx:commandButton id="cb2"/>

    </amx:facet>

</amx:panelPage>

</amx:view>

Deploy the app, check that Location is on and launch the app.

4. GPS is available

Turn off location on the device, click resetFeature and the following should be displayed.

5. GPS is not available

Step 2. Extend to a bean

It’ll be easier within the app if this data was available through a managed bean.  Mainly as it is easier to embed into EL expressions and also abstracts us away from a having to invoke the javascript from various places. To do this create a new bean called GPSBean and paste the following:-

public class GPSBean {

    private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

    public GPSBean() {

        super();

    }

    private boolean status;

    public void setStatus(boolean status) {

        boolean oldStatus = this.status;

        this.status = status;

propertyChangeSupport.firePropertyChange("status", oldStatus, status);

    }

    public boolean isStatus() {

             return status;

    }

    public void addPropertyChangeListener(PropertyChangeListener l) {

propertyChangeSupport.addPropertyChangeListener(l);

    }

    public void removePropertyChangeListener(PropertyChangeListener l) {

propertyChangeSupport.removePropertyChangeListener(l);

    }

}

This defines an object with a single field of status.  Update the adfc-mobile-config.xml to add this object into the pageFlowScope as shown below.

6. bean definition

The subtle change to make is to modify the getter for the status field, through adding a line to invoke a Javascript function that will be responsible for deriving the new value. Update the isStatus function as follows:-

    public boolean isStatus() {

        String result = (String)AdfmfContainerUtilities.invokeContainerJavaScriptFunction("com.rubiconred.test.gpstest.welcome","application.checkGPSStatus", new Object[] {} );

        return status;

    }

This makes a call out to a checkGPSStatus method which has to be added to gps.js. Paste the following over the existing javascript. Notice that the old direct call toCheckGPS.check has been removed and is now embedded in the checkGPSStatus function.  This also stops it running each time the page is loaded.

(function() {

if (!window.application) window.application = {};

          application.checkGPSStatus = function() {

CheckGPS.check(function(){

    //GPS is enabled!

adf.mf.api.invokeMethod("com.rubiconred.test.gpstest.mobile.GPSBean", "setGPSStatus", true , onInvokeSuccess, onFail);

  },

  function(){

    //GPS is disabled!

adf.mf.api.invokeMethod("com.rubiconred.test.gpstest.mobile.GPSBean", "setGPSStatus", false , onInvokeSuccess, onFail);

  });

  return true;

};

    function onFail() {

     //   alert("It failed");

    };

    function onInvokeSuccess(param) {

    };

})();

This new Javascript function makes a call to setGPSStatus on the GPSBean. This will be used to trigger the setting of the status field. Copy the following method intoGPSBean

   public void setGPSStatus(boolean status) {

         ValueExpression ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.gpsBean.status}", Boolean.class);

         ve.setValue(AdfmfJavaUtilities.getAdfELContext(), status);

    }

Note: The Javascript function is calling the Class directly (not an Object instance) it is necessary to call out to the pageFlowScope object we are using.

Finally, the previous method of resetting the welcome page was a bit of a kludge. It was needed to force the page to refresh and for the Javascript to re-run on page load. Let’s do this a different way, by adding a button to the page to do the trigger of the status check.  Aligned with this, we’ll add some output text fields that are based on the status of the field, via an EL expression. Copy the following below onto the welcome page.

<amx:outputText value="GPS is ON" id="ot2" rendered="#{pageFlowScope.gpsBean.status}"/>

<amx:outputText value="GPS is OFF" id="ot3" rendered="#{pageFlowScope.gpsBean.status==false}"/>

<amx:commandButton text="refresh status" id="cb1" actionListener="#{pageFlowScope.gpsBean.updateStatus}"/>

Update GPSBean with a new function that the button will call.

    public void updateStatus(ActionEvent actionEvent) {

        // Trigger a check of the GPS

       this.isStatus();

    }

This is to simulate that calling GPSBean.status from anywhere within the application will trigger an update of the status.

Deploy and test with location on.  GPS in ON will show as location is available.  The EL expression #{pageFlowScope.gpsBean.status} is used to show this output text and so it will show when the status is true.

7. gps on

Clicking refresh status will trigger an update and as location is available, it remains as GPS is ON. Turn off Location on the device and click the refresh status button. As shown below the message becomes GPS is OFF. This is due to the evaluation of the status being false.

8. gps off

Step 3. Add call to startLocationMonitor

The original intent was to add a page that was able to call location monitoring without throwing an error. Now that the EL expression exists, it is relatively easy to add the call as required.

Drag the DeviceFeature->startLocationMonitor method from the Data Controls onto the welcome.amx page. Select the option to add as a button and then at the prompt enter

true10000 and pageFlowScope.gpsBean.locationUpdated. This tells the in-built location monitoring control to use high accuracy on the results, to update every 10 seconds and the endpoint to send location details to.

9. startLocationMonitor binding

This will add a button to the page, which will allow location monitoring to be triggered. However, our use case is to trigger automatically; there are several options at this point, but for our use case the easiest is to simply update the setGPSStatus method to execute a binding. Paste the code below over the existing method.

   public void setGPSStatus(boolean status) {

        ValueExpression ve = AdfmfJavaUtilities.getValueExpression("#{pageFlowScope.gpsBean.status}", Boolean.class);

        ve.setValue(AdfmfJavaUtilities.getAdfELContext(), status);

        // check whether locationmonitor should now be triggered

        if (status==true){

                 AdfELContext adfELContext = AdfmfJavaUtilities.getAdfELContext();

                 MethodExpression me = AdfmfJavaUtilities.getMethodExpression("#{bindings.startLocationMonitor.execute}", Object.class, new Class[]{});

                 me.invoke(adfELContext, new Object[]{});

        }

    }

To summarise - these actions have added a call to startLocationMonitor which is controlled through the Managed Bean and the status of the GPS being available or not. The final step is to add in the method called when startLocationMonitor passes back an update. Edit the GPSBean and paste the following at the end of the class:

   private double longitude = 0;

    private double latitude = 0;

    private boolean locationDetermined = false;

    public void setLongitude(double longitude) {

        double oldLongitude = this.longitude;

        this.longitude = longitude;

propertyChangeSupport.firePropertyChange("longitude", oldLongitude, longitude);

    }

    public double getLongitude() {

        return longitude;

    }

    public void setLatitude(double latitude) {

        double oldLatitude = this.latitude;

        this.latitude = latitude;

propertyChangeSupport.firePropertyChange("latitude", oldLatitude, latitude);

    }

    public double getLatitude() {

        return latitude;

    }

    public void setLocationDetermined(boolean locationDetermined) {

        boolean oldLocationDetermined = this.locationDetermined;

this.locationDetermined = locationDetermined;

        propertyChangeSupport.firePropertyChange("locationDetermined", oldLocationDetermined, locationDetermined);

    }

    public boolean isLocationDetermined() {

        return locationDetermined;

    }

    public void locationUpdated(Location currentLocation) {

this.setLatitude(currentLocation.getLatitude());

this.setLongitude(currentLocation.getLongitude());

        // track location has been calculated

        if (this.getLatitude()!=0 && this.getLongitude()!=0) {

                     this.setLocationDetermined(true);

        }

    }

The longitude and latitude fields are used to store location details, with locationDetermined being used to track that an actual reference has been found. This could be used later as a way to show or hide certain fields (e..g if you had a distance to nearest store displayed on the page).

Finally, the welcome page needs to be updated to show these details. Go back to welcome.amx and paste the following under the refresh button. The button forstartLocationMonitor can also be removed as we are executing this via the binding trigger.

<amx:panelGroupLayout id="pgl1" layout="vertical" rendered="#{pageFlowScope.gpsBean.locationDetermined}">

      <amx:outputText value="#{pageFlowScope.gpsBean.longitude}" id="ot4"/>

      <amx:outputText value="Latitude #{pageFlowScope.gpsBean.latitude}" id="ot5"/>

</amx:panelGroupLayout>

Deploy the app and launch with Location On.

10. location details

The latitude and longitude will now trigger automatically.

Close the app, turn off Location on the device and launch again. There is no error displayed as location monitoring has not been triggered.

11. location no error

Turn on Location on the device , click refresh status and in a few seconds the location will be displayed.

12. location after refresh

Note: Testing with the location monitor service shows that the interval is largely ignored (at least on Android).  Updates will only fire when it is determined the device has travelled a distance worth notifying. Equally, these updates can come every 0.5 of a second, rather than every 10 seconds.   If you are struggling with getting the function working - go for a walk! You may need to go a couple of hundred metres depending on the network and whether GPS or Wi-Fi location is being used.

Step 4. Extend to ‘real-time’

There is one further extension that could be added to ensure a ‘real time’ GPS status update.  If this is important to the app then the following change to the gps.jsfunctions will check every 5 seconds for the latest status.

function onInvokeSuccess(param) {

    // set timeout to trigger in 5 seconds

    setTimeout(function(){application.checkGPSStatus()}, 5000);

};

Now when the location is turned on or off on the device the update will flow through automatically within 5 seconds.  However, it would be worth assessing the need for this as this will impact on the battery and performance in general.  It is likely that the app only needs to know when location is available (i.e. when it is possible to call location monitor without an error). In this instance it may be better to move the setTimeout call to only occur when location isn’t available. The Javascript for taking this approach is shown below and replaces the current gps.js :-

(function() {

if (!window.application) window.application = {};

   application.checkGPSStatus = function() {

CheckGPS.check(function(){

    //GPS is enabled!

adf.mf.api.invokeMethod("com.rubiconred.test.gpstest.mobile.GPSBean", "setGPSStatus", true , onInvokeSuccess, onFail);

  },

  function(){

    //GPS is disabled!

adf.mf.api.invokeMethod("com.rubiconred.test.gpstest.mobile.GPSBean", "setGPSStatus", false , onInvokeSuccessDisabled, onFail);

  });

  return true;

};

    function onFail() {

     //   alert("It failed");

      // setTimeout to trigger in 5 seconds

setTimeout(function(){application.checkGPSStatus()}, 5000);

    };

    function onInvokeSuccessDisabled(param) {

       // no location, so try again in 5 seconds

setTimeout(function(){application.checkGPSStatus()}, 5000);  

    };

    function onInvokeSuccess(param) {

    };

})();

Summary

This article has shown how you can build a managed bean method that allows the evaluation of the GPS Status and subsequent triggering of location monitoring.  It also shows a quick method for making this real-time (although care should be taken in doing this).

Using Google OAUTH with MAF

To use Google OAuth inside the MAF app you will require:

  • setting up a project on the Google developer console
  • configuring an API key for web applications
  • configuring MAF to point to the OAuth service

Step 1. Create Project

The assumption here is that you have a registered account for Google Developer Console.  If not, then see https://www.gmail.com/intl/en/mail/help/about.html for instructions on what is required.

Log into the Developer Console and navigate to https://console.developers.google.com/project. This will display a list of your current projects. We’ll use Create Project to generate a new one dedicated to our OAuth authentication.  If your app will use other features, such as Google Maps, then you can also configure those APIs inside this project.

1. New Project.png

Creating a new project will ask for Project Name and will allocate a random project id (this is Google’s internal UNID for your project).  Advanced options allows you to choose your Data Center for App Engine components, however we are not using those, so we’ll ignore and accept the basics.

Creation can take a couple of seconds but once the project has been created the Overview page is shown.

2. Project Dashboard.png

There are a couple of things we need to now do.  When a user goes to log in, they will need to be presented with a screen detailing what information is being requested and by whom.  We also have to configure the API key for the app to securely use. This requires configuring a Consent screen and then setting up the key.

Step 2. Configure Consent Screen

The Consent screen is located under APIs & auth as shown below.

3. consent screen.png

There are various details that need to be provided and which will be shown in the same structure as provided in the example (as copied below).

4. permissions screen.png

The actual items being requested under 'Project Name would like to:' will be based on the scopes you enter (which will be covered off later in this blog post).

For our sample we’ll choose the default ‘Choose your email’ option and enter the following

Field
Value
Product NameRubicon Red OAUTH Demo
Homepage URLLeave empty
Product LogoNeeds to be a URL to an image available on the web and that is square. Let’s put https://pbs.twimg.com/profile_images/586105964/RR_twitter_400x400.PNG
Privacy PolicyTerms of ServiceGoogle+ PageWe’ll leave these as empty but it’s worth considering the effort to generate these if you are releasing a commercial app. They are also optional fields on the Play and Apple stores but are expected to be provided.

Hit Save and the Consent screen is generated.

Step 3. Create API Credentials

Our authentication requirement is to log into the MAF app using a Google account. For this to work requires setting up some configuration that tells Google who is requesting permissions. To do this we need to define a Client ID that the MAF app will use to connect to Google and request authentication.

Under APIs & auth – select the Credential screen to display a list of all configured credentials.

5. credentials.png

We need to create an OAuth client id for authentication. Select ‘Create new Client ID’ and this will display a list of the different types of credentials we can create – Web Application, Service account or Installed application. The trick here is to remember that MAF runs as a layer above the Android and iOS operating systems. To all intents and purposes it actually runs and appears as a web application [The same rule applies when configuring Google Maps].   The default dialog box is shown below.

6. Create Client Id.png

There are only two fields to configure; Authorized Javascript origins and Authorized redirect URIs. This might be the point where you start to struggle and wonder what values need to be provided. For a web application it would logically be the domain name of the site and then the uri to redirect to after a successful login. However, as MAF is running as a web app in a container it isn’t obvious what these values should be….

… it actually doesn’t really seem to make much difference for the javascript origins field. The key thing we do need to ensure is that whatever is defined as the authorized redirect URI will need to match the value configured inside the MAF Security wizard.  So, for our example, let’s go with  https://rubiconred.com and a redirect uri of https://localhost/oauth2callback.  This leaves us with the values shown in the screenshot below.

7. Create Client Id Filled In.png

Selecting ‘Create Client ID’ will then generate the required configuration to use OAuth, with an example shown below.

8. Created Client Id.png

Note: These details should not be shared outside of your organisation as they allow someone to represent themselves as being ‘your company’. I have shown these here only because the project has been deleted and authentication will no longer work.

And that’s it for the Google side of things. We have configured an OAuth provider that we can now use within the app to handle authentication. The next step is to define security within the MAF app.

Step 4. Create MAF Project

We’re going to set up an app that has two pages – a default welcome screen and then a page that is secured with our Google Authentication.  Step through the new MAF app Wizard – I have called my app OAuthTest and given it an application prefix of com.rubiconred.test.googleauth.  Now let’s create two features, one called welcome and one called securedfeature, and then define each as being a simple .amx page with the same name.

9. Features.png

Tick the enable security on the securedfeature – we’ll configure this later.

Edit the pages to set the header title, so that it is clear which page you are on when in the app.

10. Welcome.png

welcome page

11. Secured.png

secured feature page

Step 5. Configure Security

Now we need to configure the application and security settings. Open up maf-application.xml as this is where the settings are changed.  Select Security from the left hand menu and this will display the security configuration.  For this sample, the defaults are ok to leave as-is for the login page, etc.  Notice that under Authentication and Access Control the feature we previously ticked is shown with <application login server>.

12. Security.png

What we need to now do is to add a login configuration – so click the sign next to the application/ configuration login server entry.

This will display the Create MAF Login Connection wizard which allows various security options to be configured.

Select OAuth and the set the Connection Name to GoogleOAuth .

Select the OAuth tab and this will display the fields required to configure OAuth.

13. MAF Oauth Wizard.png

See 29 Securing MAF Applications  for a more detailed description around Authorization Code vs. Resource Owner Credentials and the required fields.  Essentially we need to use Authorization Code as we are trying to log in as an intermediary - i.e. the user will access google to authenticate and google will tell us this is OK and return us a token.

The remaining fields need to be set according to the details provided during the Google Client ID configuration earlier.

Field
Description
Value
Client IdCopy from the google project Client ID for the web application.728124431009-5l9rqpigm25t82amt521bl60t706g0qe.apps.googleusercontent.com
Client SecretCopy from the google project Client ID for the web application1r5pfimWlHYXlx1a0BjuFZNi
Authorization Endpointhttps://accounts.google.com/o/oauth2/auth
Token Endpointhttps://accounts.google.com/o/oauth2/token
Redirect EndpointThis needs to match the value you configured in your project earlier. If this does not match then Google will return an error.https://localhost/oauth2callback
Logout URLGoogle doesn’t seem to support the concept of logout for OAuth. However a value needs to be provided as it is mandatory (even though the dialog box indicates it isn't).https://accounts.google.com/o/oauth2/auth

As we are not trying to do a single sign on across multiple apps or websites we can tick the Enabled Embedded Browser Mode setting.   This makes the experience a bit ‘cleaner’ as the login screen appears in the app (similar to embedded web pages).  The final set up is shown below.

13ii. finalised.png

Scopes

We are only doing a login but you need to configure a minimal option as Google requires scopes to be requested.  If you don’t provide a value then you’ll receive an error 400 – Missing required parameter: scope. https://developers.google.com/+/api/oauth#login-scopes contains details about standard scopes that are supported on login.

The basic login scope is https://www.googleapis.com/auth/plus.login, which can also be entered in the OpenID Connect standard format of profile . It is worth reading up on these to see what options and data becomes available according to the scope used. For our login example we’ll usehttps://www.googleapis.com/auth/userinfo.email.

Finally switch the securedfeature to use this newly configured security.

14. Switch security on.png

Unrelated topic

Note that by default the maf-application.xml will have the application id set to something like com.company.OAuthTest, as shown below.15. App Name.png

Ideally this should be changed to reflect your application package name.  For our example we will switch the value to com.rubiconred.test.googleoauth.

Step 6. Test

OK. At this point we are done - Google has been configured, along with the app. Deploy and launch the app and this will show the Welcome Page by default.

16. Welcome Page.png

Select securedfeature in the bottom menu and the login page will appear.

17. Login.png

Notice from the URL bar that this page is served up by Google and it is not the standard login page that is defined as part of the app. This cannot be edited or changed and there is no option to disable the browser bar either.   If you successfully login with a Google Account, then the authorization page will be shown to acknowledge acceptance of sharing information.

18 . Permissions.png

[The details requested here are as a result of using a scope set to https://www.googleapis.com/auth/userinfo.email].

Click 'Accept' and you will then be successfully logged in, as shown below.

19. secured page.png

Interestingly if you close the app and launch again, the login screen changes to ask whether you wish to allow ‘Have Offline Access’.  There is no need to type the username and password in again.  This seems to vary depending on what scope you have requested. For example, if the scope of email is used (a newer scope to the deprecated https://www.googleapis.com/auth/userinfo.email) then the credentials are not stored and you have to provide username and password each time.

Summary

This blog has taken you through how to configure a MAF application to use Google OAuth for authentication.  It stepped you through the configuration required on Google and highlighted the settings you need to consider for it all to work correctly.  Potential next steps would be to then use the returned token to access data (and perform actions as the user) on any of the Google Apps they have available.