Developer area/Mahara Mobile
From Mahara Wiki
Mahara Mobile is a cross-platform mobile app for Mahara, being developed by Catalyst IT for the Mahara project. It's currently (Sept 2016) still in development, with a potential launch corresponding with the Mahara 16.10 release. The application uses updates to Mahara's webservices, which are only present in Mahara 16.10 and greater.
See Developer_Area/Specifications_in_Development/Mahara_Mobile for the design process behind it.
Development environment
Mahara
You'll need a Mahara site with the updated webservices code. While it's under development, it's in https://github.com/agwells/mahara/tree/mobile . Once it's merged, it'll be in Mahara 16.10+.
You'll need to enable the "mobileapi" module. Once this is done, you should also go into the extension config page for the module, and auto-config webservices to work with it.
SSO testing: It's also useful to set up an SSO method on the Mahara site, so you can test SSO login. See https://simplesamlphp.org/docs/stable/simplesamlphp-idp to set up your own SAML IdP; and then link your Mahara to it.
Android emulator compatibility: If your Mahara dev site's wwwroot is a domain name you set up in your /etc/hosts file, it won't work in the Android emulator. The simulated Android has its own internal /etc/hosts file, that is completely different. Similarly, if your site is at localhost it won't work in the emulator (because to the simulated Android device, localhost means itself).
The simplest way to work around this, is to use your machine's IP address as its wwwroot. Or, you can use the special IP address 10.0.2.2, which the Android emulator will map to the host machine (your dev box). You can use 10.0.2.2 and another hostname in the same Mahara site by adding some switching logic to your config.php:
$cfg->wwwroot = "http://example.mahara.org"; // Your real wwwroot
if (isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] === '10.0.2.2'){
$cfg->wwwroot = "http://10.0.2.2/mahara/htdocs/";
}
build stuff
The README.md file should contain instructions about which build tools you'll need. These include:
- Node.js
- NPM
- Android SDK
IDE
I've been using Microsoft's open-source IDE, VisualStudio Code: https://code.visualstudio.com/
It supports React out of the box. I additionally use these plugins:
- Cordova Tools
- npm Intellisense
- jsx
- Babel ES6/ES7: for syntax highlighting
- Document This: to generate JSDoc-style documentation headers
- ESLint
Source code
The source code is on the Mahara github project: https://github.com/maharaproject/mahara-mobile
Mahara Mobile is a Cordova app, which means it's written in Javascript and runs in a Webview (a GUI element that's basically a stripped-down web browser), which allows the same code to run on multiple native OS's.
It's essentially a single-page web application; a static HTML file on the user's phone, which loads up a bunch of local Javascript files. If you're coming to this from PHP, the big mental shift is that in a Cordova app there is no "server-side" to speak of. The "server-side" is a static HTML file that says which Javascript files to run. The Javascript runs in the local Webview/browser.
The program is designed to send/retrieve data from a Mahara site. It does this through Webservices.
Control flow
Once the app is loaded, the control flow is generally along these lines:
- The app renders a page, using a React component defined in /src/js/components. This code includes setting an event handler on parts of the UI, e.g. onclick
- The user interacts with the page, firing the event handler.
- The "controller logic" of the event handler, is generally written in a function in the component's JS file.
- Any Mahara APIs that the event handler needs, should be separated out into the /src/js/mahara-lib directory.
- The event handler processes the event and takes whatever action is appropriate.
- Once it's done, the event handler calls StateStore.dispatch(action) to update the application's internal state.
- action should be an object with these fields:
- type: a constant declared in src/js/constants.js and matching one of the values in the giant switch statement in src/js/state.js
- (Optionally) additional fields with data to put/update in the state store.
- action should be an object with these fields:
- StateStore enters its giant case-switch statement in src/js/state.js, and processes the data from action into the application's internal data store.
- render() (in src/js/index.js) is then called. It checks the current value of state.page to see which page is being displayed, finds the React component that prints that page, and tells it to render/update itself (passing StateStore as a React "prop" to the component).
- The React component updates the Webview's DOM with any needed changes.
- The webview goes back to waiting for user input, closing the loop.
API's
- HTML is generated by React components under src/js/components
- UI event handlers are also declared in the React components.
- Mahara webservices and other APIs to interact with the remote Mahara, are declared in src/js/mahara-lib
- They're usually accessed in code by doing import {maharaServer} from './state.js' and then calling functions on maharaServer.
- If you're writing a new Mahara API function, make sure to add and bind it to the MaharaServer class in src/js/mahara-lib/mahara-server.js.
- You can call a webservice by using httpLib.callWebservice() in http-lib.js
- state (kind of like Mahara globals or database variables) is handled by the StateStore object in src/js/state.js
- All the page-level components (and most of the rest) receive StateStore as prop.state
- Elsewhere in the code it's accessed via objects ultimately passed from render() in index.js, which receives it from its subscription to StateStore.
- It should also be possible to retrieve the current state by doing StateStore.getState();
- Changing pages: by importing router.js and doing Router.navigate(...) with a PAGE constant from constants.js. This is usually done synchronously after invoking StateStore.dispatch() to update the app's state.
- Language strings: Inside a component you can access language strings via this.gettext().
- This makes use of a gettext() method declared in the MaharaBaseComponent class in src/js/components/base.js.
- Outside of a component, you could import i18n.js and do getLangString(StateStore.getState().lang, langStringId)
External API's
The application uses Mahara's Webservices to communicate with the Mahara app. It also uses a couple of additional scripts in the mobileapi module.
For specific parameters and return values of the mobileapi module, look at the underlying PHP source code.
Webservices
See Developer area/Webservices
module/mobileapi/json/info.php
Returns an array JSON-encoded information about the Mahara site it's on. It's used to return public information about the site, in order to help the application know how it should interact with this site. Because this script requires no authentication, it should not return any sensitive information. All the info returned by the script should be things you could figure out by screen-scraping the site as a logged-out user. The only purpose of the script is to avoid the need for messy screen-scraping.
Some fields in the return data:
- maharaversion: The "series" of the Mahara site, e.g. "16.10". (Can be gleaned by looking at the structure and names of files on the site. It's also printed as a metadata tag on normal pages.)
- wsenabled: Whether webservices are enabled at all.
- wsprotocols: An array of the protocols the webservices plugin accepts for incoming requests
- mobileapienabled: Whether or not the mobileapi module is enabled
- mobileapiversion: The API version number of the mobileapi's maharamobile service group. (You can use this to degrade functionality gracefully should a site be running an older version of the software.)
- logintypes: An array of the logintypes available on the site. Possible return values are:
- basic: The normal "username" and "password" form directly on Mahara.
- This means the app can authenticate using the JSON-based module/mobileapi/json/token.php
- sso: The site has at least one auth method running that doesn't use the Mahara-side login form.
- This means the app can authenticate using the Webview-based module/mobileapi/tokenform.php
- basic: The normal "username" and "password" form directly on Mahara.
The information here can be customized using the function local_webservice_info in /local
module/mobileapi/json/token.php
Provides a JSON-based service to obtain a Webservice access token for a Mahara user. It takes the user's username and password as parameters, along with the component and shortname of the service group which the token should have access to.
You can use this for sites where all users log in via the "username" and "password" fields in the standard Mahara login box. That is, sites without any SSO auth methods. Because it's REST-based, you can simply display username and password fields directly in the mobile app, and send those to the Mahara server on the back-end.
module/mobilapi/tokenform.php
Provides a UI-based way to obtain a Webservice access token. This is to support Mahara sites that are using auth methods that cannot log in via the standard login form. For example, the SAML auth method adds an "SSO" button to the login box; users click on that, are redirected to the SAML identity provider (which is a separate website entirely), and then redirected back to Mahara when they are authenticated.
Because non-standard auth methods are very flexible, this page aims to handle most of them, by simply opening the Mahara login box in a child webview of the mobile app. Specifically, it sends them to the same "transient login page" a user sees when trying to access any Mahara page that is not available to logged-out users.
It does this by simply omitting define(PUBLIC, 1) from its header. This makes it a non-public page. When the child Webview opens (set to clear its session cookies beforehand), the standard Mahara init logic sees the need for a login, redirects the user to the transient login page, and then redirects them back to tokenform.php once they're logged in.
Once logged in, tokenform.php prints the webservice token into a Javascript variable, where the parent page can retrieve it from. In a normal web browser, CORS access rules would prevent this; but Cordova runs in a Webview, which doesn't have to obey all of the CORS rules.
module/mobileapi/download.php
Provides a way to download the user's user profile icon, via webservices token authentication. (Doing this as a normal webservice, would require serializing the profile icon into a JSON string.)
Organization of the git repo
This is a description of the directory structure you'll see if you check out the git repository.
Config files
- config.xml: This is the Cordova project's configuration file. It contains metadata about the project, like its name, description, icons to use in various OS's, which Cordova plugins to include, etc. It also indicates that index.html is the "entry point" for the application; the file to actually load into the WebView when the app launches.
- package.json: This is the NPM package description file. Its most important feature is that it lists all of the NPM modules that we use in the project; both in the project itself, and in its Gulp build script. It also has a scripts tag that configures several useful build commands, such as npm run start.
- gulpfile.js: The Gulp build file. It contains the script that is used in the first step of our build process.
- cordova.json: Not a core cordova config file. It stores some build directives to pass in to cordova when publishing the Android app.
- jsconfig.json: A config file for the VisualStudio Code IDE.
Build products
Step 0: /src
The actual codebase of Mahara Mobile is in the /src directory. Specifically, most of the Javascript is in /src/js.
Step 1: Gulp
The first build step is to run gulp. This then compiles our Javascript in the /src directory (which contains JSX and ES2017 code) into cross-platform-compatible Javascript. It also bundles it up into one big Javascript file, called bundle.js. Build steps are also applied to the CSS files, and to any other resources in the /src directory. These build products are copied into the /www directory.
Step 2: Cordova
The next build step is to run cordova build. This copies the Javascript from the /www directory, and bundles/packages it as needed for each OS we're targetting, along with any included Cordova libraries, plugins, and native code. If you do cordova build android, it compiles it into an Android apk file; if you do cordova build ios, it makes an iOS app; if you do cordova build browser, it just turns it into a local web app on your computer.
These build products are under the /platforms directory: /platforms/android, /platforms/browser, etc.
A tour of /src
/src/index.html
This is the "single page" in our "single page application". It's the HTML file that Cordova loads into the webview when you launch the application. It's named in /config.xml.
Think of this file as the boot loader for the app. All it does is display a simple "Loading..." graphic, and then load in the cordova.js library; and once that's done, Mahara Mobile's ready.js file.
/src/ready.js
If index.html is the boot loader, ready.js is the OS startup sequence. This file locates and processes the language strings stored under /src/i18n. Then it loads and executes all of the Javascript files, under /src/js and /src/libs.
Control then flows to /src/js/index.js, via the IIFE] in that file.
/src/js/index.js
This file declares a render() method, which is sort of like the Event loop for the application's React-based UI. Every time the app's state changes (via the StateStore), render() is called, looks at the value passed to it by the statestore, and uses a case-switch to decide which page to display/update.
This typically will then put the application in a state of waiting for the next user input. If, instead, the app should automatically carry on to some additional step, an "after" method can be triggered here to start that going.
/src/js/state.js
This file contains most of the "controller" logic for the application. Its main feature is the StateStore object, which is a Redux store.
Most of the file is taken up by the "MaharaState" function. This function acts as the "reducer" for the Redux store. Each time the state of the application changes, a dev calls StateStore.dispatch(action), where "action" is an object with information about the state change. This "action" is then passed to the "MaharaState" function, where the giant case-switch statement determines which action was taken, and updates the internal state accordingly.
MaharaState() also mirrors the storage into LocalStorage. This allows for data to persist offline. (Although we should probably re-implement this with proper Cordova libraries, because iOS sometimes reclaims LocalStorage.)
Remember that "render()" in index.js is subscribed to the StateStore. So after each state update, render() is called and updates the UI.
/src/js/router.js
This file declares a Grapnel router. This allows you to call router.navigate(<pageid>) to change pages, using one of the page id constants declared in /src/js/constants.js.
This is the main way to navigate between pages in the app. (What it actually does is fire off a StateStore dispatch, with {type:PAGE.something}. In MaharaState()'s switch-case loop, this updates state.page. And then in render.js the change to state.page is noticed, and causes the appropriate page to be rendered.
/src/js/mahara-lib/
This directory contains most of the code for interacting with Mahara's API. If you make API changes, you'll probably need to update the code here.
The code is mostly organized by having functions declared for export, one per file. The functions are than added as mix-ins to a MaharaServer class in mahara-server.js. A singleton of that class is then instantiated in state.js, and stored into StateStore. So, it acts as a collection of API functions and a place to store information about the remote Mahara server.
The functions are also all bound to the maharaServer object's context, meaning that the keyword this inside of them refers to our MaharaServer singleton.
You normally use it in code by doing import {maharaServer} from './state.js'.
/src/js/components
This directory contains the front-end UI code. Specifically, it's a set of React components.
/src/lib
Contains third-party Javascript libaries that aren't managed by NPM or Cordova.
Software libraries used
Cordova/PhoneGap
The program is written for the Apache Cordova platform. This is also sometimes referred to as PhoneGap; but specifically PhoneGap is a Adobe's distribution of PhoneGap.
Cordova apps are able to run on multiple platforms, because they primarily run as Javascript inside a web browser/webview. Thus any platform that provides a suitable Javascript runtime, can potentially run a Cordova app. The Cordova framework includes additional native libraries that can be accessed from Javascript, to allow the JS code to access native functionality on the device.
Writing a Cordova app is not exactly the same as writing a responsive web application. They often share a lot of the same front-end code. But, Cordova apps typically run in a "Webview" rather than an actual web browser. A "Webview" is a special type of GUI element that can render HTML & JS content, but has some different behaviors than a normal web browser. In particular, Cordova applications are usually single-page applications, with movement between "pages" done by Javascript. This is because, if the webview were pointed to a different URL, the entire Cordova application and all of its Javascript libraries, would have to re-render, with a noticeable delay and loss of all non-persisted variables.
React
The single-page application framework that Mahara Mobile uses is React, written by Facebook.
Traditionally in Mahara we've used one of two methods to update pages via Javascript:
- Reload and replace the entire page/iframe (Easier to code, but slow and ugly for the user)
- Use JQuery to locate specific DOM elements to update, and just change those (Faster for the user, but finnicky and bug-prone to code)
React avoids both of these, by using a "Virtual DOM" and automatically adjusting the "actual DOM" to match it. So, you can write React code that acts as if it's re-printing the entire page, and then React will only update the parts of the page that have actually changed. Despite the extra computation, this actually makes for a faster user experience.
JSX
Taking the place of Mahara's template files, Mahara Mobile uses Facebook's JSX. This is a Javascript syntax extension that lets you write code inside your Javascript files that looks like HTML. A build tool will later compile it into actual Javascript.
// Awkward block of variable declarations
var Form = MyFormComponent;
var FormRow = Form.Row;
var FormLabel = Form.Label;
var FormInput = Form.Input;
var App = (
<Form>
<FormRow>
<FormLabel />
<FormInput />
</FormRow>
</Form>
);
So if you see code like that in the codebase, know that it's JSX.
Redux
The app uses Redux to maintain state. You'll notice a prominent "StateStore" object in the codebase. That comes from Redux.
Grapnel
We also use the JS library Grapnel as a "router". This is basically the way that we manage switching between different "pages" of our single-page application.
Babel & Browserify
Mahara Mobile uses Babel and Browserify in its build script.
Babel is a Javascript compiler. It converts the JSX in our code, into actual Javascript. It also convert's Mahara Mobile's Javascript from bleeding-edge ES2017 syntax, into Javascript that will run in any modern web browser.
Browserify allows us to use the require() statement to pull in Javascript code from other files. It does so mainly by compiling all our separate files together into one big bundle.js file.
MaharaDroid (the old app)
Mahara's predecessor as official mobile app was MaharaDroid, an Android native application. It is no longer being maintained. The code is available here: Developer_Area/Specifications_in_Development/Mahara_Mobile
MaharDroid was written before the Mahara webservices plugin was implemented. So it instead uses the code located under htdocs/api/mobile.php, which has to be activate by enabling the "Allow mobile uploads" site setting.