diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml new file mode 100644 index 000000000..2dfef1503 --- /dev/null +++ b/.github/workflows/android-build.yml @@ -0,0 +1,62 @@ +# This is a basic workflow to help you get started with Actions + +name: osx-ubuntu-build-android + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '5 4 * * 0' + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: macos-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Print the java version + run: java -version + + - name: Tries to figure out where android is installed + run: | + echo "Android listed at $ANDROID_HOME" + ls -al /opt/ + + - name: Setup the cordova environment + shell: bash -l {0} + run: | + source setup/setup_android_native.sh + npx cordova -version + npx ionic -version + + - name: Access cordova with a specified shell + shell: bash -l {0} + run: | + npx cordova -version + which gradle + gradle -version + + - name: Build android + shell: bash -l {0} + run: | + echo $PATH + which gradle + gradle -version + echo "Let's rerun the sdkman again" + source ~/.sdkman/bin/sdkman-init.sh + echo $PATH + which gradle + gradle --version + npx cordova build android diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 000000000..d6783e218 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,33 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml new file mode 100644 index 000000000..ebdcfc431 --- /dev/null +++ b/.github/workflows/ios-build.yml @@ -0,0 +1,62 @@ +# This is a basic workflow to help you get started with Actions + +name: osx-build-ios + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '5 4 * * 0' + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: macos-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Print the xcode path + run: xcode-select --print-path + + - name: Print the xcode setup + run: xcodebuild -version -sdk + + - name: Print applications through dmg + run: ls /Applications + + - name: Print applications through brew + run: brew list + + - name: Setup the cordova environment + shell: bash -l {0} + run: | + source setup/setup_ios_native.sh + npx cordova -version + npx ionic -version + + - name: Access cordova directly + run: npx cordova -version + + - name: Access cordova with a specified shell + shell: bash -l {0} + run: npx cordova -version + + - name: Build ios + shell: bash -l {0} + run: npx cordova build ios + + - name: Cleanup the cordova environment + shell: bash -l {0} + run: source setup/teardown_ios_native.sh + diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml new file mode 100644 index 000000000..9efae9f3a --- /dev/null +++ b/.github/workflows/serve-install.yml @@ -0,0 +1,51 @@ +# This is a basic workflow to help you get started with Actions + +name: osx-serve-install + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '5 4 * * 0' + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: macos-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Print the xcode path + run: xcode-select --print-path + + - name: Print the xcode setup + run: xcodebuild -version -sdk + + - name: Print applications through dmg + run: ls /Applications + + - name: Print applications through brew + run: brew list + + - name: Setup the serve environment + shell: bash -l {0} + run: | + source setup/setup_serve.sh + npx cordova -version + npx ionic --version + +# TODO: figure out how to check that a server started correctly +# - name: Try starting it +# run: npx run serve + diff --git a/.gitignore b/.gitignore index c67c60e70..01d1852af 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ app-settings.json .idea/ .io-config.json *.apk +*.app.zip *.ipa www/js/control/collect-settings.js www/js/control/sync-settings.js diff --git a/README.md b/README.md index b4ce133a9..cfbd2813e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,15 @@ e-mission phone app This is the phone component of the e-mission system. -:sparkles: As part of experimenting with a COVID-19 reference app, we upgraded to the most recent cordova version and added CI :sparkles: We are now porting this over to the main e-mission repo. The ported code is currently in https://github.com/e-mission/e-mission-phone/tree/upgrade_to_latest_cordova and the current status is in https://github.com/e-mission/e-mission-docs/issues/519. The branch is usable although not all functionality has been tested. Please contribute your testing results so that we feel confident promoting it to master. +:sparkles: This has now been upgraded to cordova android@8.1.0 and iOS@5.1.1 (details). It also now supports CI, so we should not have any build issues. + +The current limitations are: +- [trip end notifications have some minor issues](https://github.com/e-mission/e-mission-transition-notify/issues/25): I will handle this as part of the upcoming upgrade to even newer cordova versions +- [OpenID auth code has not been tested](https://github.com/e-mission/e-mission-docs/issues/519): It would be great if somebody who is using OpenIDAuth is able to report on testing results +- [Push notifications on iOS require setting `IS_GCM_ENABLED` to `` in `GoogleServices.json`](https://github.com/e-mission/e-mission-docs/issues/437#issuecomment-513506146) + +:construction: There are now newer versions of cordova available. Since I am making these changes anyway, I plan to spend a day or two finishing the upgrade to cordova-android@9.0.0 (released 29 Jun 2020) and cordova-ios@6.1.0 (released 23 Jun 2020). Hopefully, that will keep us going for a while longer without needing maintenance updates. + Additional Documentation --- @@ -14,94 +22,26 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone Updating the UI only --- -If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) e-mission dev app. - -### Dependencies -1. node.js: You probably want to install this using [nvm](https://github.com/creationix/nvm), to ensure that you can pick a particular [version of node](https://github.com/creationix/nvm#usage). - ``` - $ node -v - v9.4.0 - $ npm -v - 6.0.0 - ``` - - Make sure that the permissions are set correctly - npm and node need to be owned by `root` or another admin user. - - ``` - $ which npm - /usr/local/bin/npm - $ ls -al /usr/local/bin/npm - lrwxr-xr-x 1 root wheel 38 May 8 10:04 /usr/local/bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js - $ ls -al /usr/local/lib/node_modules/npm/bin/npm-cli.js - -rwxr-xr-x 1 cusgadmin staff 4295 Oct 26 1985 /usr/local/lib/node_modules/npm/bin/npm-cli.js - ``` - -2. [bower](https://bower.io/): - - ``` - $ bower -v - 1.8.4 - ``` - -### Installation -1. Install the most recent release of the em-devapp (https://github.com/e-mission/e-mission-devapp) - -1. Get the current version of the phone UI code - - 1. Fork this repo using the github UI +[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) - 1. Clone your fork - - ``` - $ git clone - ``` - - ``` - $ cd e-mission-phone - ``` - -1. Create a remote to pull updates from upstream - - ``` - $ git remote add upstream https://github.com/e-mission/e-mission-phone.git - ``` - -1. Setup the config - - ``` - $ ./bin/configure_xml_and_json.js serve - ``` +If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) e-mission dev app. -1. Install all required node modules +### Installing - ``` - $ npm install - ``` - 1. Install javascript dependencies - - ``` - $ bower install - ``` - -1. Configure values if necessary - e.g. +Run the setup script - ``` - $ ls www/json/*.sample - $ cp www/json/setupConfig.json.sample www/json/setupConfig.json - $ cp ..... www/json/connectionConfig.json - ``` - -1. Run the setup script +``` +$ source setup/setup_serve.sh +``` - ``` - $ npm run setup-serve - > edu.berkeley.eecs.emission@2.5.0 setup /private/tmp/e-mission-phone - > ./bin/download_settings_controls.js +**(optional)** Configure by changing the files in `www/json`. +Defaults are in `www/json/*.sample` - Sync collection settings updated - Data collection settings updated - Transition notify settings updated - ``` +``` +$ ls www/json/*.sample +$ cp www/json/startupConfig.json.sample www/json/startupConfig.json +$ cp ..... www/json/connectionConfig.json +``` ### Running @@ -125,7 +65,7 @@ If you want to make only UI changes, (as opposed to modifying the existing plugi - Safari ([enable develop menu](https://support.apple.com/guide/safari/use-the-safari-develop-menu-sfri20948/mac)): Develop -> Simulator -> index.html - Chrome: chrome://inspect -> Remote target (emulator) -**Ta-da!** If you change any of the files in the `www` directory, the app will automatically be re-loaded without manually restarting either the server or the app. +**Ta-da!** :gift: If you change any of the files in the `www` directory, the app will automatically be re-loaded without manually restarting either the server or the app :tada: **Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. @@ -147,157 +87,69 @@ One advantage of using `skip` authentication in development mode is that any use Updating the e-mission-\* plugins or adding new plugins --- +[![osx-build-ios](https://github.com/e-mission/e-mission-phone/workflows/osx-build-ios/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-ubuntu-build-android) +[![osx-ubuntu-build-android](https://github.com/e-mission/e-mission-phone/workflows/osx-ubuntu-build-android/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-build-ios) -Installing +Pre-requisites --- -We are using the ionic v3.19.1 platform, which is a toolchain on top of the apache -cordova project. So the first step is to install ionic using their instructions. -http://ionicframework.com/docs/v1/getting-started/ +- the version of xcode used by the CI + - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) + - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) +- git +- the most recent version of android studio -NOTE: Since we are still on ionic v1, please do not install v2 or v3, as the current codebase will not work with it. -Issue the following commands to install Cordova and Ionic instead of the ones provided in the instruction above. - -``` -$ npm install -g cordova@8.0.0 -$ npm install -g ionic@3.19.1 -``` - -Install gradle (https://gradle.org/install/) for android builds. - -Then, get the current version of our code - -Fork this repo using the github UI - -Clone your fork - -``` -$ git clone -``` - -``` -$ cd e-mission-phone -``` - -Enable platform hooks, including http on iOS9 - -``` -$ git clone https://github.com/driftyco/ionic-package-hooks.git ./package-hooks -``` - -Setup the config - -``` -$ ./bin/configure_xml_and_json.js cordovabuild -``` - -Install all javascript components using bower - -``` -$ bower update -``` - -Make sure to install the other node modules required for the setup scripts. +Important +--- +Most of the recent issues encountered have been due to incompatible setup. We +have now: +- locked down the dependencies, +- created setup and teardown scripts to setup self-contained environments with + those dependencies, and +- CI enabled to validate that they continue work. -``` -npm install -``` +If you have setup failures, please compare the configuration in the passing CI +builds with your configuration. That is almost certainly the source of the error. -Create a remote to pull updates from upstream +Installing +--- +Run the setup script for the platform you want to build ``` -$ git remote add upstream https://github.com/e-mission/e-mission-phone.git +$ source setup/setup_android_native.sh +AND/OR +$ source setup/setup_ios_native.sh ``` -Setup cocoapods. For all versions > 1.9, we need https://cocoapods.org/ support. This is used by the push plugin for the GCM pod, and by the auth plugin to install the GTMOAuth framework. This is a good time to get a cup of your favourite beverage. +**(optional)** Configure by changing the files in `www/json`. +Defaults are in `www/json/*.sample` ``` -$ sudo gem install cocoapods -$ pod setup +$ ls www/json/*.sample +$ cp www/json/startupConfig.json.sample www/json/startupConfig.json +$ cp ..... www/json/connectionConfig.json ``` -To debug the cocoapods install, or make it less resource intensive, check out troubleshooting guide for the push plugin. -https://github.com/phonegap/phonegap-plugin-push/blob/master/docs/INSTALLATION.md#cocoapods - -**Note about cocoapods 1.9, there seems to be an issue which breaks ```pod setup```:** -https://github.com/flutter/flutter/issues/41253 -1.75 seems to work: ```sudo gem install cocoapods -v 1.7.5``` - -Configure values if necessary - e.g. +Run in the emulator ``` -ls www/json/*.sample -cp www/json/setupConfig.json.sample www/json/setupConfig.json -cp ..... www/json/connectionConfig.json +$ npx cordova emulate ios +AND/OR +$ npx cordova emulate android ``` -Restore cordova platforms and plugins - -``` -$ cordova prepare -``` - -**Note:** Sometimes, the `$ cordova prepare` command fails because of errors while cloning plugins (`Failed to restore plugin "..." from config.xml.`). A workaround is at https://github.com/e-mission/e-mission-docs/blob/master/docs/overview/high_level_faq.md#i-get-an-error-while-adding-plugins - -**Note #2:** After the update to the plugins to support api 26, for this repository **only** the first call `$ cordova prepare` fails with the error - - Using cordova-fetch for cordova-android@^6.4.0 - Error: Platform ios already added. -The workaround is to re-run `$cordova prepare`. This not required in the https://github.com/e-mission/e-mission-base repo although the config.xml seems to be the same for both repositories. - - $ cordova prepare - Discovered platform "android@^6.4.0" in config.xml or package.json. Adding it to the project - Using cordova-fetch for cordova-android@^6.4.0 - Adding android project... - Creating Cordova project for the Android platform: - Path: platforms/android - Package: edu.berkeley.eecs.emission - Name: emission - Activity: MainActivity - Android target: android-26 - - -Installation is now complete. You can view the current state of the application in the emulator - - $ cordova emulate ios - - OR - - $ cordova emulate android - -The android build and emulator have improved significantly in the last release -of Android Studio (3.0.1). The build is significantly faster than iOS, the -emulator is just as snappy, and the debugger is better since chrome saves logs -from startup, so you don't have to use tricks like adding alerts to see errors -in startup. - -**Note about Xcode >=10** The cordova build doesn't work super smoothly for iOS anymore. Concretely, you need two additional steps: -- install pods manually. Otherwise you will get a linker error for `-lAppAuth` - ``` - $ cd platform/ios - $ pod install - $ cd ../.. - ``` - -- when you recompile, you will get the following compile error. The workaround is to compile from xcode. I have filed an issue for this (https://github.com/apache/cordova-ios/issues/550) but there have been no recent updates. - - ``` - /Users/shankari/e-mission/e-mission-phone/platforms/ios/Pods/JWT/Classes/Supplement/JWTBase64Coder.m:22:9: fatal error: - 'Base64/MF_Base64Additions.h' file not found - #import - ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 1 error generated. - ``` - -- Also, on Mojave, we have reports that [you may need to manually enable the Legacy Build system in Xcode if you want to run the app on a real device](https://stackoverflow.com/a/52528662/4040267). - - Troubleshooting --- - -Troubleshooting tips have been moved to the e-mission-phone section of the e-mission-docs repo: -https://github.com/e-mission/e-mission-docs/blob/master/docs/e-mission-phone/troubleshooting_tips_faq.md - -Debugging +- Make sure to use `npx ionic` and `npx cordova`. This is + because the setup script installs all the modules locally in a self-contained + environment using `npm install` and not `npm install -g` +- Check the CI to see whether there is a known issue +- Run the commands from the script one by one and see which fails + - compare the failed command with the CI logs +- Another workaround is to delete the local environment and recreate it + - javascript errors: `rm -rf node_modules && npm install` + - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` + +Beta-testing debugging --- If users run into problems, they have the ability to email logs to the maintainer. These logs are in the form of an sqlite3 database, so they have to @@ -316,6 +168,10 @@ $ less /tmp/loggerDB..withdate.log Contributing --- +Add the main repo as upstream + + $ git remote add upstream https://github.com/covid19database/phone-app.git + Create a new branch (IMPORTANT). Please do not submit pull requests from master $ git checkout -b mybranch diff --git a/bin/sign_and_align_keys.sh b/bin/sign_and_align_keys.sh index 17c4eff10..ba0a74055 100644 --- a/bin/sign_and_align_keys.sh +++ b/bin/sign_and_align_keys.sh @@ -7,34 +7,7 @@ fi # Sign and release the L+ version # Make sure the highest supported version has the biggest version code -cordova build android --release -- --minSdkVersion=21 --gradleArg=-PcdvVersionCode=${1}9 -cp platforms/android/build/outputs/apk/release/android-release-unsigned.apk platforms/android/build/outputs/apk/android-L+-release-signed-unaligned.apk -jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/Safety_Infrastructure/MovesConnect/production.keystore ./platforms/android/build/outputs/apk/android-L+-release-signed-unaligned.apk androidproductionkey -~/Library/Android/sdk/build-tools/27.0.3/zipalign -v 4 platforms/android/build/outputs/apk/android-L+-release-signed-unaligned.apk emission-L+-build-$1.apk - -# Re-add the plugin -cordova plugin add cordova-plugin-crosswalk-webview - -# Copy the workaround to make it ready to build with gradle -cp bin/xwalk6-workaround.gradle platforms/android/cordova-plugin-crosswalk-webview -python bin/gradle_workaround.py -a - -# Rebuild the entries with crosswalk -cordova build android --release - -# Sign and release arm7 -cp platforms/android/build/outputs/apk/armv7/release/android-armv7-release-unsigned.apk platforms/android/build/outputs/apk/android-arm7-release-signed-unaligned.apk -jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/Safety_Infrastructure/MovesConnect/production.keystore ./platforms/android/build/outputs/apk/android-arm7-release-signed-unaligned.apk androidproductionkey -~/Library/Android/sdk/build-tools/27.0.3/zipalign -v 4 platforms/android/build/outputs/apk/android-arm7-release-signed-unaligned.apk emission-arm7-build-$1.apk - -# Sign and release x86 -cp platforms/android/build/outputs/apk/x86/release/android-x86-release-unsigned.apk platforms/android/build/outputs/apk/android-x86-release-signed-unaligned.apk -jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/Safety_Infrastructure/MovesConnect/production.keystore ./platforms/android/build/outputs/apk/android-x86-release-signed-unaligned.apk androidproductionkey -~/Library/Android/sdk/build-tools/27.0.3/zipalign -v 4 platforms/android/build/outputs/apk/android-x86-release-signed-unaligned.apk emission-x86-build-$1.apk - -# Remove the plugin -cordova plugin remove cordova-plugin-crosswalk-webview - -# Remove the build workarounds -python bin/gradle_workaround.py -r -rm platforms/android/cordova-plugin-crosswalk-webview/xwalk6-workaround.gradle +npx cordova build android --release -- --minSdkVersion=21 +cp platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk platforms/android/app/build/outputs/apk/app-L+-release-signed-unaligned.apk +jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/Safety_Infrastructure/MovesConnect/production.keystore ./platforms/android/app/build/outputs/apk/app-L+-release-signed-unaligned.apk androidproductionkey +~/Library/Android/sdk/build-tools/27.0.3/zipalign -v 4 platforms/android/app/build/outputs/apk/app-L+-release-signed-unaligned.apk cv-19-track-L+-build-$1.apk diff --git a/build.json b/build.json deleted file mode 100644 index df11bd744..000000000 --- a/build.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "ios": { - "debug": { - "buildFlag": [ - "-UseModernBuildSystem=0" - ] - }, - "release": { - "buildFlag": [ - "-UseModernBuildSystem=0" - ] - } - } -} diff --git a/config.cordovabuild.xml b/config.cordovabuild.xml index 2c8b70bde..e9d7d943c 100644 --- a/config.cordovabuild.xml +++ b/config.cordovabuild.xml @@ -9,13 +9,11 @@ - - - + @@ -45,6 +43,8 @@ + + @@ -80,9 +80,10 @@ - + - + + @@ -106,39 +107,45 @@ - - + + - + - + - - - - + + + + + + + + + + - - - + + + - - - - - - - + + + + + + + diff --git a/hooks/before_build/android/android_set_provider.js b/hooks/before_build/android/android_set_provider.js new file mode 100755 index 000000000..4d55618c4 --- /dev/null +++ b/hooks/before_build/android/android_set_provider.js @@ -0,0 +1,114 @@ +/* + * A hook to change provider in order to match with the application name. + */ + +var fs = require('fs'); +var path = require('path'); +const PROVIDER = "edu.berkeley.eecs.emission.provider"; +const ACCOUNT_TYPE = "eecs.berkeley.edu"; +const LOG_NAME = "Changing Providers: "; + +var changeProvider = function (file, currentName, newName) { + if (fs.existsSync(file)) { + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + + var regEx = new RegExp(currentName, 'g'); + + var result = data.replace(regEx, newName + '.provider'); + + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); + }); + } +} + +var changeAccountType = function (file, currentName, newName) { + if (fs.existsSync(file)) { + + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + + var regEx = new RegExp(currentName, 'g'); + + var result = data.replace(regEx, newName); + + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error('Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); + + + }); + } +} + + +var changeAccountTypeAndProvider = function (file, accountType, providerName, newName) { + if (fs.existsSync(file)) { + + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + throw new Error(LOG_NAME + 'Unable to find ' + file + ': ' + err); + } + + var regEx1 = new RegExp(accountType, 'g'); + var regEx2 = new RegExp(providerName, 'g'); + + var result = data.replace(regEx1, newName); + result = result.replace(regEx2, newName + '.provider'); + + fs.writeFile(file, result, 'utf8', function (err) { + if (err) throw new Error(LOG_NAME + 'Unable to write into ' + file + ': ' + err); + console.log(LOG_NAME + "" + file + " updated...") + }); + }); + } +} + +module.exports = function (context) { + // If Android platform is not installed, don't even execute + if (context.opts.cordova.platforms.indexOf('android') < 0) + return; + + console.log(LOG_NAME + "Retrieving application name...") + var config_xml = path.join(context.opts.projectRoot, 'config.xml'); + var et = context.requireCordovaModule('elementtree'); + var data = fs.readFileSync(config_xml).toString(); + // If no data then no config.xml + if (data) { + var etree = et.parse(data); + var applicationName = etree._root.attrib.id; + console.log(LOG_NAME + "Your application is " + applicationName); + + var platformRoot = path.join(context.opts.projectRoot, 'platforms/android/') + + console.log(LOG_NAME + "Updating AndroidManifest.xml..."); + var androidManifest = path.join(platformRoot, 'AndroidManifest.xml'); + changeProvider(androidManifest, PROVIDER, applicationName); + + console.log(LOG_NAME + "Updating syncadapter.xml"); + var syncAdapter = path.join(platformRoot, 'res/xml/syncadapter.xml'); + changeAccountTypeAndProvider(syncAdapter, ACCOUNT_TYPE, PROVIDER, applicationName); + + console.log(LOG_NAME + "Updating authenticator.xml"); + var authenticator = path.join(platformRoot, 'res/xml/authenticator.xml'); + changeAccountType(authenticator, ACCOUNT_TYPE, applicationName); + + console.log(LOG_NAME + "Updating ServerSyncPlugin.java"); + var serverSyncPlugin = path.join(platformRoot, 'src/edu/berkeley/eecs/emission/cordova/serversync/ServerSyncPlugin.java'); + changeAccountTypeAndProvider(serverSyncPlugin, ACCOUNT_TYPE, PROVIDER, applicationName); + + console.log(LOG_NAME + "Updating android.json"); + var androidJson = path.join(platformRoot, 'android.json'); + changeProvider(androidJson, PROVIDER, applicationName); + } else { + throw new Error(LOG_NAME + "Could not retrieve application name."); + } +} \ No newline at end of file diff --git a/ionic.config.json b/ionic.config.json index 0b8118447..225ea0689 100644 --- a/ionic.config.json +++ b/ionic.config.json @@ -1,6 +1,6 @@ { "name": "e-mission-phone", - "app_id": "e0d8cdec", + "id": "e0d8cdec", "browsers": [ { "platform": "android", diff --git a/node_modules/angular-mocks/angular-mocks.js b/node_modules/angular-mocks/angular-mocks.js deleted file mode 100644 index 9482b702b..000000000 --- a/node_modules/angular-mocks/angular-mocks.js +++ /dev/null @@ -1,2436 +0,0 @@ -/** - * @license AngularJS v1.4.3 - * (c) 2010-2015 Google, Inc. http://angularjs.org - * License: MIT - */ -(function(window, angular, undefined) { - -'use strict'; - -/** - * @ngdoc object - * @name angular.mock - * @description - * - * Namespace from 'angular-mocks.js' which contains testing related code. - */ -angular.mock = {}; - -/** - * ! This is a private undocumented service ! - * - * @name $browser - * - * @description - * This service is a mock implementation of {@link ng.$browser}. It provides fake - * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, - * cookies, etc... - * - * The api of this service is the same as that of the real {@link ng.$browser $browser}, except - * that there are several helper methods available which can be used in tests. - */ -angular.mock.$BrowserProvider = function() { - this.$get = function() { - return new angular.mock.$Browser(); - }; -}; - -angular.mock.$Browser = function() { - var self = this; - - this.isMock = true; - self.$$url = "http://server/"; - self.$$lastUrl = self.$$url; // used by url polling fn - self.pollFns = []; - - // TODO(vojta): remove this temporary api - self.$$completeOutstandingRequest = angular.noop; - self.$$incOutstandingRequestCount = angular.noop; - - - // register url polling fn - - self.onUrlChange = function(listener) { - self.pollFns.push( - function() { - if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) { - self.$$lastUrl = self.$$url; - self.$$lastState = self.$$state; - listener(self.$$url, self.$$state); - } - } - ); - - return listener; - }; - - self.$$applicationDestroyed = angular.noop; - self.$$checkUrlChange = angular.noop; - - self.deferredFns = []; - self.deferredNextId = 0; - - self.defer = function(fn, delay) { - delay = delay || 0; - self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a, b) { return a.time - b.time;}); - return self.deferredNextId++; - }; - - - /** - * @name $browser#defer.now - * - * @description - * Current milliseconds mock time. - */ - self.defer.now = 0; - - - self.defer.cancel = function(deferId) { - var fnIndex; - - angular.forEach(self.deferredFns, function(fn, index) { - if (fn.id === deferId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - self.deferredFns.splice(fnIndex, 1); - return true; - } - - return false; - }; - - - /** - * @name $browser#defer.flush - * - * @description - * Flushes all pending requests and executes the defer callbacks. - * - * @param {number=} number of milliseconds to flush. See {@link #defer.now} - */ - self.defer.flush = function(delay) { - if (angular.isDefined(delay)) { - self.defer.now += delay; - } else { - if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length - 1].time; - } else { - throw new Error('No deferred tasks to be flushed'); - } - } - - while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { - self.deferredFns.shift().fn(); - } - }; - - self.$$baseHref = '/'; - self.baseHref = function() { - return this.$$baseHref; - }; -}; -angular.mock.$Browser.prototype = { - -/** - * @name $browser#poll - * - * @description - * run all fns in pollFns - */ - poll: function poll() { - angular.forEach(this.pollFns, function(pollFn) { - pollFn(); - }); - }, - - url: function(url, replace, state) { - if (angular.isUndefined(state)) { - state = null; - } - if (url) { - this.$$url = url; - // Native pushState serializes & copies the object; simulate it. - this.$$state = angular.copy(state); - return this; - } - - return this.$$url; - }, - - state: function() { - return this.$$state; - }, - - notifyWhenNoOutstandingRequests: function(fn) { - fn(); - } -}; - - -/** - * @ngdoc provider - * @name $exceptionHandlerProvider - * - * @description - * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors - * passed to the `$exceptionHandler`. - */ - -/** - * @ngdoc service - * @name $exceptionHandler - * - * @description - * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed - * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration - * information. - * - * - * ```js - * describe('$exceptionHandlerProvider', function() { - * - * it('should capture log messages and exceptions', function() { - * - * module(function($exceptionHandlerProvider) { - * $exceptionHandlerProvider.mode('log'); - * }); - * - * inject(function($log, $exceptionHandler, $timeout) { - * $timeout(function() { $log.log(1); }); - * $timeout(function() { $log.log(2); throw 'banana peel'; }); - * $timeout(function() { $log.log(3); }); - * expect($exceptionHandler.errors).toEqual([]); - * expect($log.assertEmpty()); - * $timeout.flush(); - * expect($exceptionHandler.errors).toEqual(['banana peel']); - * expect($log.log.logs).toEqual([[1], [2], [3]]); - * }); - * }); - * }); - * ``` - */ - -angular.mock.$ExceptionHandlerProvider = function() { - var handler; - - /** - * @ngdoc method - * @name $exceptionHandlerProvider#mode - * - * @description - * Sets the logging mode. - * - * @param {string} mode Mode of operation, defaults to `rethrow`. - * - * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` - * mode stores an array of errors in `$exceptionHandler.errors`, to allow later - * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and - * {@link ngMock.$log#reset reset()} - * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there - * is a bug in the application or test, so this mock will make these tests fail. - * For any implementations that expect exceptions to be thrown, the `rethrow` mode - * will also maintain a log of thrown errors. - */ - this.mode = function(mode) { - - switch (mode) { - case 'log': - case 'rethrow': - var errors = []; - handler = function(e) { - if (arguments.length == 1) { - errors.push(e); - } else { - errors.push([].slice.call(arguments, 0)); - } - if (mode === "rethrow") { - throw e; - } - }; - handler.errors = errors; - break; - default: - throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); - } - }; - - this.$get = function() { - return handler; - }; - - this.mode('rethrow'); -}; - - -/** - * @ngdoc service - * @name $log - * - * @description - * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays - * (one array per logging level). These arrays are exposed as `logs` property of each of the - * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. - * - */ -angular.mock.$LogProvider = function() { - var debug = true; - - function concat(array1, array2, index) { - return array1.concat(Array.prototype.slice.call(array2, index)); - } - - this.debugEnabled = function(flag) { - if (angular.isDefined(flag)) { - debug = flag; - return this; - } else { - return debug; - } - }; - - this.$get = function() { - var $log = { - log: function() { $log.log.logs.push(concat([], arguments, 0)); }, - warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, - info: function() { $log.info.logs.push(concat([], arguments, 0)); }, - error: function() { $log.error.logs.push(concat([], arguments, 0)); }, - debug: function() { - if (debug) { - $log.debug.logs.push(concat([], arguments, 0)); - } - } - }; - - /** - * @ngdoc method - * @name $log#reset - * - * @description - * Reset all of the logging arrays to empty. - */ - $log.reset = function() { - /** - * @ngdoc property - * @name $log#log.logs - * - * @description - * Array of messages logged using {@link ng.$log#log `log()`}. - * - * @example - * ```js - * $log.log('Some Log'); - * var first = $log.log.logs.unshift(); - * ``` - */ - $log.log.logs = []; - /** - * @ngdoc property - * @name $log#info.logs - * - * @description - * Array of messages logged using {@link ng.$log#info `info()`}. - * - * @example - * ```js - * $log.info('Some Info'); - * var first = $log.info.logs.unshift(); - * ``` - */ - $log.info.logs = []; - /** - * @ngdoc property - * @name $log#warn.logs - * - * @description - * Array of messages logged using {@link ng.$log#warn `warn()`}. - * - * @example - * ```js - * $log.warn('Some Warning'); - * var first = $log.warn.logs.unshift(); - * ``` - */ - $log.warn.logs = []; - /** - * @ngdoc property - * @name $log#error.logs - * - * @description - * Array of messages logged using {@link ng.$log#error `error()`}. - * - * @example - * ```js - * $log.error('Some Error'); - * var first = $log.error.logs.unshift(); - * ``` - */ - $log.error.logs = []; - /** - * @ngdoc property - * @name $log#debug.logs - * - * @description - * Array of messages logged using {@link ng.$log#debug `debug()`}. - * - * @example - * ```js - * $log.debug('Some Error'); - * var first = $log.debug.logs.unshift(); - * ``` - */ - $log.debug.logs = []; - }; - - /** - * @ngdoc method - * @name $log#assertEmpty - * - * @description - * Assert that all of the logging methods have no logged messages. If any messages are present, - * an exception is thrown. - */ - $log.assertEmpty = function() { - var errors = []; - angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { - angular.forEach($log[logLevel].logs, function(log) { - angular.forEach(log, function(logItem) { - errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + - (logItem.stack || '')); - }); - }); - }); - if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " + - "an expected log message was not checked and removed:"); - errors.push(''); - throw new Error(errors.join('\n---------\n')); - } - }; - - $log.reset(); - return $log; - }; -}; - - -/** - * @ngdoc service - * @name $interval - * - * @description - * Mock implementation of the $interval service. - * - * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to - * move forward by `millis` milliseconds and trigger any functions scheduled to run in that - * time. - * - * @param {function()} fn A function that should be called repeatedly. - * @param {number} delay Number of milliseconds between each function call. - * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat - * indefinitely. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. - */ -angular.mock.$IntervalProvider = function() { - this.$get = ['$browser', '$rootScope', '$q', '$$q', - function($browser, $rootScope, $q, $$q) { - var repeatFns = [], - nextRepeatId = 0, - now = 0; - - var $interval = function(fn, delay, count, invokeApply) { - var hasParams = arguments.length > 4, - args = hasParams ? Array.prototype.slice.call(arguments, 4) : [], - iteration = 0, - skipApply = (angular.isDefined(invokeApply) && !invokeApply), - deferred = (skipApply ? $$q : $q).defer(), - promise = deferred.promise; - - count = (angular.isDefined(count)) ? count : 0; - promise.then(null, null, (!hasParams) ? fn : function() { - fn.apply(null, args); - }); - - promise.$$intervalId = nextRepeatId; - - function tick() { - deferred.notify(iteration++); - - if (count > 0 && iteration >= count) { - var fnIndex; - deferred.resolve(iteration); - - angular.forEach(repeatFns, function(fn, index) { - if (fn.id === promise.$$intervalId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - repeatFns.splice(fnIndex, 1); - } - } - - if (skipApply) { - $browser.defer.flush(); - } else { - $rootScope.$apply(); - } - } - - repeatFns.push({ - nextTime:(now + delay), - delay: delay, - fn: tick, - id: nextRepeatId, - deferred: deferred - }); - repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); - - nextRepeatId++; - return promise; - }; - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {promise} promise A promise from calling the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully cancelled. - */ - $interval.cancel = function(promise) { - if (!promise) return false; - var fnIndex; - - angular.forEach(repeatFns, function(fn, index) { - if (fn.id === promise.$$intervalId) fnIndex = index; - }); - - if (fnIndex !== undefined) { - repeatFns[fnIndex].deferred.reject('canceled'); - repeatFns.splice(fnIndex, 1); - return true; - } - - return false; - }; - - /** - * @ngdoc method - * @name $interval#flush - * @description - * - * Runs interval tasks scheduled to be run in the next `millis` milliseconds. - * - * @param {number=} millis maximum timeout amount to flush up until. - * - * @return {number} The amount of time moved forward. - */ - $interval.flush = function(millis) { - now += millis; - while (repeatFns.length && repeatFns[0].nextTime <= now) { - var task = repeatFns[0]; - task.fn(); - task.nextTime += task.delay; - repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); - } - return millis; - }; - - return $interval; - }]; -}; - - -/* jshint -W101 */ -/* The R_ISO8061_STR regex is never going to fit into the 100 char limit! - * This directive should go inside the anonymous function but a bug in JSHint means that it would - * not be enacted early enough to prevent the warning. - */ -var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; - -function jsonStringToDate(string) { - var match; - if (match = string.match(R_ISO8061_STR)) { - var date = new Date(0), - tzHour = 0, - tzMin = 0; - if (match[9]) { - tzHour = toInt(match[9] + match[10]); - tzMin = toInt(match[9] + match[11]); - } - date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); - date.setUTCHours(toInt(match[4] || 0) - tzHour, - toInt(match[5] || 0) - tzMin, - toInt(match[6] || 0), - toInt(match[7] || 0)); - return date; - } - return string; -} - -function toInt(str) { - return parseInt(str, 10); -} - -function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while (num.length < digits) num = '0' + num; - if (trim) { - num = num.substr(num.length - digits); - } - return neg + num; -} - - -/** - * @ngdoc type - * @name angular.mock.TzDate - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. - * - * Mock of the Date type which has its timezone specified via constructor arg. - * - * The main purpose is to create Date-like instances with timezone fixed to the specified timezone - * offset, so that we can test code that depends on local timezone settings without dependency on - * the time zone settings of the machine where the code is running. - * - * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) - * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* - * - * @example - * !!!! WARNING !!!!! - * This is not a complete Date object so only methods that were implemented can be called safely. - * To make matters worse, TzDate instances inherit stuff from Date via a prototype. - * - * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is - * incomplete we might be missing some non-standard methods. This can result in errors like: - * "Date.prototype.foo called on incompatible Object". - * - * ```js - * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); - * newYearInBratislava.getTimezoneOffset() => -60; - * newYearInBratislava.getFullYear() => 2010; - * newYearInBratislava.getMonth() => 0; - * newYearInBratislava.getDate() => 1; - * newYearInBratislava.getHours() => 0; - * newYearInBratislava.getMinutes() => 0; - * newYearInBratislava.getSeconds() => 0; - * ``` - * - */ -angular.mock.TzDate = function(offset, timestamp) { - var self = new Date(0); - if (angular.isString(timestamp)) { - var tsStr = timestamp; - - self.origDate = jsonStringToDate(timestamp); - - timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) { - throw { - name: "Illegal Argument", - message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" - }; - } - } else { - self.origDate = new Date(timestamp); - } - - var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60; - self.date = new Date(timestamp + self.offsetDiff); - - self.getTime = function() { - return self.date.getTime() - self.offsetDiff; - }; - - self.toLocaleDateString = function() { - return self.date.toLocaleDateString(); - }; - - self.getFullYear = function() { - return self.date.getFullYear(); - }; - - self.getMonth = function() { - return self.date.getMonth(); - }; - - self.getDate = function() { - return self.date.getDate(); - }; - - self.getHours = function() { - return self.date.getHours(); - }; - - self.getMinutes = function() { - return self.date.getMinutes(); - }; - - self.getSeconds = function() { - return self.date.getSeconds(); - }; - - self.getMilliseconds = function() { - return self.date.getMilliseconds(); - }; - - self.getTimezoneOffset = function() { - return offset * 60; - }; - - self.getUTCFullYear = function() { - return self.origDate.getUTCFullYear(); - }; - - self.getUTCMonth = function() { - return self.origDate.getUTCMonth(); - }; - - self.getUTCDate = function() { - return self.origDate.getUTCDate(); - }; - - self.getUTCHours = function() { - return self.origDate.getUTCHours(); - }; - - self.getUTCMinutes = function() { - return self.origDate.getUTCMinutes(); - }; - - self.getUTCSeconds = function() { - return self.origDate.getUTCSeconds(); - }; - - self.getUTCMilliseconds = function() { - return self.origDate.getUTCMilliseconds(); - }; - - self.getDay = function() { - return self.date.getDay(); - }; - - // provide this method only on browsers that already have it - if (self.toISOString) { - self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; - }; - } - - //hide all methods not implemented in this mock that the Date prototype exposes - var unimplementedMethods = ['getUTCDay', - 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', - 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', - 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', - 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', - 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; - - angular.forEach(unimplementedMethods, function(methodName) { - self[methodName] = function() { - throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); - }; - }); - - return self; -}; - -//make "tzDateInstance instanceof Date" return true -angular.mock.TzDate.prototype = Date.prototype; -/* jshint +W101 */ - -angular.mock.animate = angular.module('ngAnimateMock', ['ng']) - - .config(['$provide', function($provide) { - - var reflowQueue = []; - $provide.value('$$animateReflow', function(fn) { - var index = reflowQueue.length; - reflowQueue.push(fn); - return function cancel() { - reflowQueue.splice(index, 1); - }; - }); - - $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', - function($delegate, $timeout, $browser, $$rAF) { - var animate = { - queue: [], - cancel: $delegate.cancel, - enabled: $delegate.enabled, - triggerCallbackEvents: function() { - $$rAF.flush(); - }, - triggerCallbackPromise: function() { - $timeout.flush(0); - }, - triggerCallbacks: function() { - this.triggerCallbackEvents(); - this.triggerCallbackPromise(); - }, - triggerReflow: function() { - angular.forEach(reflowQueue, function(fn) { - fn(); - }); - reflowQueue = []; - } - }; - - angular.forEach( - ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { - animate[method] = function() { - animate.queue.push({ - event: method, - element: arguments[0], - options: arguments[arguments.length - 1], - args: arguments - }); - return $delegate[method].apply($delegate, arguments); - }; - }); - - return animate; - }]); - - }]); - - -/** - * @ngdoc function - * @name angular.mock.dump - * @description - * - * *NOTE*: this is not an injectable instance, just a globally available function. - * - * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for - * debugging. - * - * This method is also available on window, where it can be used to display objects on debug - * console. - * - * @param {*} object - any object to turn into string. - * @return {string} a serialized string of the argument - */ -angular.mock.dump = function(object) { - return serialize(object); - - function serialize(object) { - var out; - - if (angular.isElement(object)) { - object = angular.element(object); - out = angular.element('
'); - angular.forEach(object, function(element) { - out.append(angular.element(element).clone()); - }); - out = out.html(); - } else if (angular.isArray(object)) { - out = []; - angular.forEach(object, function(o) { - out.push(serialize(o)); - }); - out = '[ ' + out.join(', ') + ' ]'; - } else if (angular.isObject(object)) { - if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { - out = serializeScope(object); - } else if (object instanceof Error) { - out = object.stack || ('' + object.name + ': ' + object.message); - } else { - // TODO(i): this prevents methods being logged, - // we should have a better way to serialize objects - out = angular.toJson(object, true); - } - } else { - out = String(object); - } - - return out; - } - - function serializeScope(scope, offset) { - offset = offset || ' '; - var log = [offset + 'Scope(' + scope.$id + '): {']; - for (var key in scope) { - if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { - log.push(' ' + key + ': ' + angular.toJson(scope[key])); - } - } - var child = scope.$$childHead; - while (child) { - log.push(serializeScope(child, offset + ' ')); - child = child.$$nextSibling; - } - log.push('}'); - return log.join('\n' + offset); - } -}; - -/** - * @ngdoc service - * @name $httpBackend - * @description - * Fake HTTP backend implementation suitable for unit testing applications that use the - * {@link ng.$http $http service}. - * - * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less - * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. - * - * During unit testing, we want our unit tests to run quickly and have no external dependencies so - * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or - * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is - * to verify whether a certain request has been sent or not, or alternatively just let the - * application make requests, respond with pre-trained responses and assert that the end result is - * what we expect it to be. - * - * This mock implementation can be used to respond with static or dynamic responses via the - * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). - * - * When an Angular application needs some data from a server, it calls the $http service, which - * sends the request to a real server using $httpBackend service. With dependency injection, it is - * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify - * the requests and respond with some testing data without sending a request to a real server. - * - * There are two ways to specify what test data should be returned as http responses by the mock - * backend when the code under test makes http requests: - * - * - `$httpBackend.expect` - specifies a request expectation - * - `$httpBackend.when` - specifies a backend definition - * - * - * # Request Expectations vs Backend Definitions - * - * Request expectations provide a way to make assertions about requests made by the application and - * to define responses for those requests. The test will fail if the expected requests are not made - * or they are made in the wrong order. - * - * Backend definitions allow you to define a fake backend for your application which doesn't assert - * if a particular request was made or not, it just returns a trained response if a request is made. - * The test will pass whether or not the request gets made during testing. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
- * - * In cases where both backend definitions and request expectations are specified during unit - * testing, the request expectations are evaluated first. - * - * If a request expectation has no response specified, the algorithm will search your backend - * definitions for an appropriate response. - * - * If a request didn't match any expectation or if the expectation doesn't have the response - * defined, the backend definitions are evaluated in sequential order to see if any of them match - * the request. The response from the first matched definition is returned. - * - * - * # Flushing HTTP requests - * - * The $httpBackend used in production always responds to requests asynchronously. If we preserved - * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, - * to follow and to maintain. But neither can the testing mock respond synchronously; that would - * change the execution of the code under test. For this reason, the mock $httpBackend has a - * `flush()` method, which allows the test to explicitly flush pending requests. This preserves - * the async api of the backend, while allowing the test to execute synchronously. - * - * - * # Unit testing with mock $httpBackend - * The following code shows how to setup and use the mock backend when unit testing a controller. - * First we create the controller under test: - * - ```js - // The module code - angular - .module('MyApp', []) - .controller('MyController', MyController); - - // The controller code - function MyController($scope, $http) { - var authToken; - - $http.get('/auth.py').success(function(data, status, headers) { - authToken = headers('A-Token'); - $scope.user = data; - }); - - $scope.saveMessage = function(message) { - var headers = { 'Authorization': authToken }; - $scope.status = 'Saving...'; - - $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { - $scope.status = ''; - }).error(function() { - $scope.status = 'ERROR!'; - }); - }; - } - ``` - * - * Now we setup the mock backend and create the test specs: - * - ```js - // testing controller - describe('MyController', function() { - var $httpBackend, $rootScope, createController, authRequestHandler; - - // Set up the module - beforeEach(module('MyApp')); - - beforeEach(inject(function($injector) { - // Set up the mock http service responses - $httpBackend = $injector.get('$httpBackend'); - // backend definition common for all tests - authRequestHandler = $httpBackend.when('GET', '/auth.py') - .respond({userId: 'userX'}, {'A-Token': 'xxx'}); - - // Get hold of a scope (i.e. the root scope) - $rootScope = $injector.get('$rootScope'); - // The $controller service is used to create instances of controllers - var $controller = $injector.get('$controller'); - - createController = function() { - return $controller('MyController', {'$scope' : $rootScope }); - }; - })); - - - afterEach(function() { - $httpBackend.verifyNoOutstandingExpectation(); - $httpBackend.verifyNoOutstandingRequest(); - }); - - - it('should fetch authentication token', function() { - $httpBackend.expectGET('/auth.py'); - var controller = createController(); - $httpBackend.flush(); - }); - - - it('should fail authentication', function() { - - // Notice how you can change the response even after it was set - authRequestHandler.respond(401, ''); - - $httpBackend.expectGET('/auth.py'); - var controller = createController(); - $httpBackend.flush(); - expect($rootScope.status).toBe('Failed...'); - }); - - - it('should send msg to server', function() { - var controller = createController(); - $httpBackend.flush(); - - // now you don’t care about the authentication, but - // the controller will still send the request and - // $httpBackend will respond without you having to - // specify the expectation and response for this request - - $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, ''); - $rootScope.saveMessage('message content'); - expect($rootScope.status).toBe('Saving...'); - $httpBackend.flush(); - expect($rootScope.status).toBe(''); - }); - - - it('should send auth header', function() { - var controller = createController(); - $httpBackend.flush(); - - $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { - // check if the header was sent, if it wasn't the expectation won't - // match the request and the test will fail - return headers['Authorization'] == 'xxx'; - }).respond(201, ''); - - $rootScope.saveMessage('whatever'); - $httpBackend.flush(); - }); - }); - ``` - */ -angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; -}; - -/** - * General factory function for $httpBackend mock. - * Returns instance for unit testing (when no arguments specified): - * - passing through is disabled - * - auto flushing is disabled - * - * Returns instance for e2e testing (when `$delegate` and `$browser` specified): - * - passing through (delegating request to real backend) is enabled - * - auto flushing is enabled - * - * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) - * @param {Object=} $browser Auto-flushing enabled if specified - * @return {Object} Instance of $httpBackend mock - */ -function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { - var definitions = [], - expectations = [], - responses = [], - responsesPush = angular.bind(responses, responses.push), - copy = angular.copy; - - function createResponse(status, data, headers, statusText) { - if (angular.isFunction(status)) return status; - - return function() { - return angular.isNumber(status) - ? [status, data, headers, statusText] - : [200, status, data, headers]; - }; - } - - // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { - var xhr = new MockXhr(), - expectation = expectations[0], - wasExpected = false; - - function prettyPrint(data) { - return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) - ? data - : angular.toJson(data); - } - - function wrapResponse(wrapped) { - if (!$browser && timeout) { - timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout); - } - - return handleResponse; - - function handleResponse() { - var response = wrapped.response(method, url, data, headers); - xhr.$$respHeaders = response[2]; - callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), - copy(response[3] || '')); - } - - function handleTimeout() { - for (var i = 0, ii = responses.length; i < ii; i++) { - if (responses[i] === handleResponse) { - responses.splice(i, 1); - callback(-1, undefined, ''); - break; - } - } - } - } - - if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) { - throw new Error('Expected ' + expectation + ' with different data\n' + - 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); - } - - if (!expectation.matchHeaders(headers)) { - throw new Error('Expected ' + expectation + ' with different headers\n' + - 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + - prettyPrint(headers)); - } - - expectations.shift(); - - if (expectation.response) { - responses.push(wrapResponse(expectation)); - return; - } - wasExpected = true; - } - - var i = -1, definition; - while ((definition = definitions[++i])) { - if (definition.match(method, url, data, headers || {})) { - if (definition.response) { - // if $browser specified, we do auto flush all requests - ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); - } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials); - } else throw new Error('No response defined !'); - return; - } - } - throw wasExpected ? - new Error('No response defined !') : - new Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); - } - - /** - * @ngdoc method - * @name $httpBackend#when - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives - * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - * - * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). The respond method returns the - * `requestHandler` object for possible overrides. - */ - $httpBackend.when = function(method, url, data, headers) { - var definition = new MockHttpExpectation(method, url, data, headers), - chain = { - respond: function(status, data, headers, statusText) { - definition.passThrough = undefined; - definition.response = createResponse(status, data, headers, statusText); - return chain; - } - }; - - if ($browser) { - chain.passThrough = function() { - definition.response = undefined; - definition.passThrough = true; - return chain; - }; - } - - definitions.push(definition); - return chain; - }; - - /** - * @ngdoc method - * @name $httpBackend#whenGET - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenHEAD - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenDELETE - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenPOST - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives - * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenPUT - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives - * data string and returns true if the data is as expected. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#whenJSONP - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - createShortMethods('when'); - - - /** - * @ngdoc method - * @name $httpBackend#expect - * @description - * Creates a new request expectation. - * - * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current expectation. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - * - * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can - * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). The respond method returns the - * `requestHandler` object for possible overrides. - */ - $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers), - chain = { - respond: function(status, data, headers, statusText) { - expectation.response = createResponse(status, data, headers, statusText); - return chain; - } - }; - - expectations.push(expectation); - return chain; - }; - - - /** - * @ngdoc method - * @name $httpBackend#expectGET - * @description - * Creates a new request expectation for GET requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. See #expect for more info. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectHEAD - * @description - * Creates a new request expectation for HEAD requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectDELETE - * @description - * Creates a new request expectation for DELETE requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectPOST - * @description - * Creates a new request expectation for POST requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectPUT - * @description - * Creates a new request expectation for PUT requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectPATCH - * @description - * Creates a new request expectation for PATCH requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that - * receives data string and returns true if the data is as expected, or Object if request body - * is in JSON format. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - - /** - * @ngdoc method - * @name $httpBackend#expectJSONP - * @description - * Creates a new request expectation for JSONP requests. For more info see `expect()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives an url - * and returns true if the url matches the current definition. - * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. You can save this object for later use and invoke `respond` again in - * order to change how a matched request is handled. - */ - createShortMethods('expect'); - - - /** - * @ngdoc method - * @name $httpBackend#flush - * @description - * Flushes all pending requests using the trained responses. - * - * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, - * all pending requests will be flushed. If there are no pending requests when the flush method - * is called an exception is thrown (as this typically a sign of programming error). - */ - $httpBackend.flush = function(count, digest) { - if (digest !== false) $rootScope.$digest(); - if (!responses.length) throw new Error('No pending request to flush !'); - - if (angular.isDefined(count) && count !== null) { - while (count--) { - if (!responses.length) throw new Error('No more pending request to flush !'); - responses.shift()(); - } - } else { - while (responses.length) { - responses.shift()(); - } - } - $httpBackend.verifyNoOutstandingExpectation(digest); - }; - - - /** - * @ngdoc method - * @name $httpBackend#verifyNoOutstandingExpectation - * @description - * Verifies that all of the requests defined via the `expect` api were made. If any of the - * requests were not made, verifyNoOutstandingExpectation throws an exception. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - * ```js - * afterEach($httpBackend.verifyNoOutstandingExpectation); - * ``` - */ - $httpBackend.verifyNoOutstandingExpectation = function(digest) { - if (digest !== false) $rootScope.$digest(); - if (expectations.length) { - throw new Error('Unsatisfied requests: ' + expectations.join(', ')); - } - }; - - - /** - * @ngdoc method - * @name $httpBackend#verifyNoOutstandingRequest - * @description - * Verifies that there are no outstanding requests that need to be flushed. - * - * Typically, you would call this method following each test case that asserts requests using an - * "afterEach" clause. - * - * ```js - * afterEach($httpBackend.verifyNoOutstandingRequest); - * ``` - */ - $httpBackend.verifyNoOutstandingRequest = function() { - if (responses.length) { - throw new Error('Unflushed requests: ' + responses.length); - } - }; - - - /** - * @ngdoc method - * @name $httpBackend#resetExpectations - * @description - * Resets all request expectations, but preserves all backend definitions. Typically, you would - * call resetExpectations during a multiple-phase test when you want to reuse the same instance of - * $httpBackend mock. - */ - $httpBackend.resetExpectations = function() { - expectations.length = 0; - responses.length = 0; - }; - - return $httpBackend; - - - function createShortMethods(prefix) { - angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { - $httpBackend[prefix + method] = function(url, headers) { - return $httpBackend[prefix](method, url, undefined, headers); - }; - }); - - angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { - $httpBackend[prefix + method] = function(url, data, headers) { - return $httpBackend[prefix](method, url, data, headers); - }; - }); - } -} - -function MockHttpExpectation(method, url, data, headers) { - - this.data = data; - this.headers = headers; - - this.match = function(m, u, d, h) { - if (method != m) return false; - if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; - return true; - }; - - this.matchUrl = function(u) { - if (!url) return true; - if (angular.isFunction(url.test)) return url.test(u); - if (angular.isFunction(url)) return url(u); - return url == u; - }; - - this.matchHeaders = function(h) { - if (angular.isUndefined(headers)) return true; - if (angular.isFunction(headers)) return headers(h); - return angular.equals(headers, h); - }; - - this.matchData = function(d) { - if (angular.isUndefined(data)) return true; - if (data && angular.isFunction(data.test)) return data.test(d); - if (data && angular.isFunction(data)) return data(d); - if (data && !angular.isString(data)) { - return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); - } - return data == d; - }; - - this.toString = function() { - return method + ' ' + url; - }; -} - -function createMockXhr() { - return new MockXhr(); -} - -function MockXhr() { - - // hack for testing $http, $httpBackend - MockXhr.$$lastInstance = this; - - this.open = function(method, url, async) { - this.$$method = method; - this.$$url = url; - this.$$async = async; - this.$$reqHeaders = {}; - this.$$respHeaders = {}; - }; - - this.send = function(data) { - this.$$data = data; - }; - - this.setRequestHeader = function(key, value) { - this.$$reqHeaders[key] = value; - }; - - this.getResponseHeader = function(name) { - // the lookup must be case insensitive, - // that's why we try two quick lookups first and full scan last - var header = this.$$respHeaders[name]; - if (header) return header; - - name = angular.lowercase(name); - header = this.$$respHeaders[name]; - if (header) return header; - - header = undefined; - angular.forEach(this.$$respHeaders, function(headerVal, headerName) { - if (!header && angular.lowercase(headerName) == name) header = headerVal; - }); - return header; - }; - - this.getAllResponseHeaders = function() { - var lines = []; - - angular.forEach(this.$$respHeaders, function(value, key) { - lines.push(key + ': ' + value); - }); - return lines.join('\n'); - }; - - this.abort = angular.noop; -} - - -/** - * @ngdoc service - * @name $timeout - * @description - * - * This service is just a simple decorator for {@link ng.$timeout $timeout} service - * that adds a "flush" and "verifyNoPendingTasks" methods. - */ - -angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) { - - /** - * @ngdoc method - * @name $timeout#flush - * @description - * - * Flushes the queue of pending tasks. - * - * @param {number=} delay maximum timeout amount to flush up until - */ - $delegate.flush = function(delay) { - $browser.defer.flush(delay); - }; - - /** - * @ngdoc method - * @name $timeout#verifyNoPendingTasks - * @description - * - * Verifies that there are no pending tasks that need to be flushed. - */ - $delegate.verifyNoPendingTasks = function() { - if ($browser.deferredFns.length) { - throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + - formatPendingTasksAsString($browser.deferredFns)); - } - }; - - function formatPendingTasksAsString(tasks) { - var result = []; - angular.forEach(tasks, function(task) { - result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); - }); - - return result.join(', '); - } - - return $delegate; -}]; - -angular.mock.$RAFDecorator = ['$delegate', function($delegate) { - var queue = []; - var rafFn = function(fn) { - var index = queue.length; - queue.push(fn); - return function() { - queue.splice(index, 1); - }; - }; - - rafFn.supported = $delegate.supported; - - rafFn.flush = function() { - if (queue.length === 0) { - throw new Error('No rAF callbacks present'); - } - - var length = queue.length; - for (var i = 0; i < length; i++) { - queue[i](); - } - - queue = queue.slice(i); - }; - - return rafFn; -}]; - -/** - * - */ -angular.mock.$RootElementProvider = function() { - this.$get = function() { - return angular.element('
'); - }; -}; - -/** - * @ngdoc service - * @name $controller - * @description - * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing - * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}. - * - * - * ## Example - * - * ```js - * - * // Directive definition ... - * - * myMod.directive('myDirective', { - * controller: 'MyDirectiveController', - * bindToController: { - * name: '@' - * } - * }); - * - * - * // Controller definition ... - * - * myMod.controller('MyDirectiveController', ['log', function($log) { - * $log.info(this.name); - * })]; - * - * - * // In a test ... - * - * describe('myDirectiveController', function() { - * it('should write the bound name to the log', inject(function($controller, $log) { - * var ctrl = $controller('MyDirective', { /* no locals */ }, { name: 'Clark Kent' }); - * expect(ctrl.name).toEqual('Clark Kent'); - * expect($log.info.logs).toEqual(['Clark Kent']); - * }); - * }); - * - * ``` - * - * @param {Function|string} constructor If called with a function then it's considered to be the - * controller constructor function. Otherwise it's considered to be a string which is used - * to retrieve the controller constructor using the following steps: - * - * * check if a controller with given name is registered via `$controllerProvider` - * * check if evaluating the string on the current scope returns a constructor - * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (not recommended) - * - * The string can use the `controller as property` syntax, where the controller instance is published - * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this - * to work correctly. - * - * @param {Object} locals Injection locals for Controller. - * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used - * to simulate the `bindToController` feature and simplify certain kinds of tests. - * @return {Object} Instance of given controller. - */ -angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { - return function(expression, locals, later, ident) { - if (later && typeof later === 'object') { - var create = $delegate(expression, locals, true, ident); - angular.extend(create.instance, later); - return create(); - } - return $delegate(expression, locals, later, ident); - }; -}]; - - -/** - * @ngdoc module - * @name ngMock - * @packageName angular-mocks - * @description - * - * # ngMock - * - * The `ngMock` module provides support to inject and mock Angular services into unit tests. - * In addition, ngMock also extends various core ng services such that they can be - * inspected and controlled in a synchronous manner within test code. - * - * - *
- * - */ -angular.module('ngMock', ['ng']).provider({ - $browser: angular.mock.$BrowserProvider, - $exceptionHandler: angular.mock.$ExceptionHandlerProvider, - $log: angular.mock.$LogProvider, - $interval: angular.mock.$IntervalProvider, - $httpBackend: angular.mock.$HttpBackendProvider, - $rootElement: angular.mock.$RootElementProvider -}).config(['$provide', function($provide) { - $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); - $provide.decorator('$$rAF', angular.mock.$RAFDecorator); - $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); - $provide.decorator('$controller', angular.mock.$ControllerDecorator); -}]); - -/** - * @ngdoc module - * @name ngMockE2E - * @module ngMockE2E - * @packageName angular-mocks - * @description - * - * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. - * Currently there is only one mock present in this module - - * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. - */ -angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { - $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); -}]); - -/** - * @ngdoc service - * @name $httpBackend - * @module ngMockE2E - * @description - * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of - * applications that use the {@link ng.$http $http service}. - * - * *Note*: For fake http backend implementation suitable for unit testing please see - * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. - * - * This implementation can be used to respond with static or dynamic responses via the `when` api - * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the - * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch - * templates from a webserver). - * - * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application - * is being developed with the real backend api replaced with a mock, it is often desirable for - * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch - * templates or static files from the webserver). To configure the backend with this behavior - * use the `passThrough` request handler of `when` instead of `respond`. - * - * Additionally, we don't want to manually have to flush mocked out requests like we do during unit - * testing. For this reason the e2e $httpBackend flushes mocked out requests - * automatically, closely simulating the behavior of the XMLHttpRequest object. - * - * To setup the application to run with this http backend, you have to create a module that depends - * on the `ngMockE2E` and your application modules and defines the fake backend: - * - * ```js - * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); - * myAppDev.run(function($httpBackend) { - * phones = [{name: 'phone1'}, {name: 'phone2'}]; - * - * // returns the current list of phones - * $httpBackend.whenGET('/phones').respond(phones); - * - * // adds a new phone to the phones array - * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { - * var phone = angular.fromJson(data); - * phones.push(phone); - * return [200, phone, {}]; - * }); - * $httpBackend.whenGET(/^\/templates\//).passThrough(); - * //... - * }); - * ``` - * - * Afterwards, bootstrap your app with this new module. - */ - -/** - * @ngdoc method - * @name $httpBackend#when - * @module ngMockE2E - * @description - * Creates a new backend definition. - * - * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header - * object and returns true if the headers match the current definition. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - * - * - respond – - * `{function([status,] data[, headers, statusText]) - * | function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string), response headers - * (Object), and the text for the status (string). - * - passThrough – `{function()}` – Any request matching a backend definition with - * `passThrough` handler will be passed through to the real backend (an XHR request will be made - * to the server.) - * - Both methods return the `requestHandler` object for possible overrides. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenGET - * @module ngMockE2E - * @description - * Creates a new backend definition for GET requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenHEAD - * @module ngMockE2E - * @description - * Creates a new backend definition for HEAD requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenDELETE - * @module ngMockE2E - * @description - * Creates a new backend definition for DELETE requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenPOST - * @module ngMockE2E - * @description - * Creates a new backend definition for POST requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenPUT - * @module ngMockE2E - * @description - * Creates a new backend definition for PUT requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenPATCH - * @module ngMockE2E - * @description - * Creates a new backend definition for PATCH requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @param {(string|RegExp)=} data HTTP request body. - * @param {(Object|function(Object))=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ - -/** - * @ngdoc method - * @name $httpBackend#whenJSONP - * @module ngMockE2E - * @description - * Creates a new backend definition for JSONP requests. For more info see `when()`. - * - * @param {string|RegExp|function(string)} url HTTP url or function that receives a url - * and returns true if the url matches the current definition. - * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. You can save this object for later use and invoke - * `respond` or `passThrough` again in order to change how a matched request is handled. - */ -angular.mock.e2e = {}; -angular.mock.e2e.$httpBackendDecorator = - ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; - - -/** - * @ngdoc type - * @name $rootScope.Scope - * @module ngMock - * @description - * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These - * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when - * `ngMock` module is loaded. - * - * In addition to all the regular `Scope` methods, the following helper methods are available: - */ -angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { - - var $rootScopePrototype = Object.getPrototypeOf($delegate); - - $rootScopePrototype.$countChildScopes = countChildScopes; - $rootScopePrototype.$countWatchers = countWatchers; - - return $delegate; - - // ------------------------------------------------------------------------------------------ // - - /** - * @ngdoc method - * @name $rootScope.Scope#$countChildScopes - * @module ngMock - * @description - * Counts all the direct and indirect child scopes of the current scope. - * - * The current scope is excluded from the count. The count includes all isolate child scopes. - * - * @returns {number} Total number of child scopes. - */ - function countChildScopes() { - // jshint validthis: true - var count = 0; // exclude the current scope - var pendingChildHeads = [this.$$childHead]; - var currentScope; - - while (pendingChildHeads.length) { - currentScope = pendingChildHeads.shift(); - - while (currentScope) { - count += 1; - pendingChildHeads.push(currentScope.$$childHead); - currentScope = currentScope.$$nextSibling; - } - } - - return count; - } - - - /** - * @ngdoc method - * @name $rootScope.Scope#$countWatchers - * @module ngMock - * @description - * Counts all the watchers of direct and indirect child scopes of the current scope. - * - * The watchers of the current scope are included in the count and so are all the watchers of - * isolate child scopes. - * - * @returns {number} Total number of watchers. - */ - function countWatchers() { - // jshint validthis: true - var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope - var pendingChildHeads = [this.$$childHead]; - var currentScope; - - while (pendingChildHeads.length) { - currentScope = pendingChildHeads.shift(); - - while (currentScope) { - count += currentScope.$$watchers ? currentScope.$$watchers.length : 0; - pendingChildHeads.push(currentScope.$$childHead); - currentScope = currentScope.$$nextSibling; - } - } - - return count; - } -}]; - - -if (window.jasmine || window.mocha) { - - var currentSpec = null, - annotatedFunctions = [], - isSpecRunning = function() { - return !!currentSpec; - }; - - angular.mock.$$annotate = angular.injector.$$annotate; - angular.injector.$$annotate = function(fn) { - if (typeof fn === 'function' && !fn.$inject) { - annotatedFunctions.push(fn); - } - return angular.mock.$$annotate.apply(this, arguments); - }; - - - (window.beforeEach || window.setup)(function() { - annotatedFunctions = []; - currentSpec = this; - }); - - (window.afterEach || window.teardown)(function() { - var injector = currentSpec.$injector; - - annotatedFunctions.forEach(function(fn) { - delete fn.$inject; - }); - - angular.forEach(currentSpec.$modules, function(module) { - if (module && module.$$hashKey) { - module.$$hashKey = undefined; - } - }); - - currentSpec.$injector = null; - currentSpec.$modules = null; - currentSpec = null; - - if (injector) { - injector.get('$rootElement').off(); - } - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - - /** - * @ngdoc function - * @name angular.mock.module - * @description - * - * *NOTE*: This function is also published on window for easy access.
- * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha - * - * This function registers a module configuration code. It collects the configuration information - * which will be used when the injector is created by {@link angular.mock.inject inject}. - * - * See {@link angular.mock.inject inject} for usage example - * - * @param {...(string|Function|Object)} fns any number of modules which are represented as string - * aliases or as anonymous module initialization functions. The modules are used to - * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an - * object literal is passed they will be registered as values in the module, the key being - * the module name and the value being what is returned. - */ - window.module = angular.mock.module = function() { - var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; - ///////////////////// - function workFn() { - if (currentSpec.$injector) { - throw new Error('Injector already created, can not register a module!'); - } else { - var modules = currentSpec.$modules || (currentSpec.$modules = []); - angular.forEach(moduleFns, function(module) { - if (angular.isObject(module) && !angular.isArray(module)) { - modules.push(function($provide) { - angular.forEach(module, function(value, key) { - $provide.value(key, value); - }); - }); - } else { - modules.push(module); - } - }); - } - } - }; - - /** - * @ngdoc function - * @name angular.mock.inject - * @description - * - * *NOTE*: This function is also published on window for easy access.
- * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha - * - * The inject function wraps a function into an injectable function. The inject() creates new - * instance of {@link auto.$injector $injector} per test, which is then used for - * resolving references. - * - * - * ## Resolving References (Underscore Wrapping) - * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this - * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable - * that is declared in the scope of the `describe()` block. Since we would, most likely, want - * the variable to have the same name of the reference we have a problem, since the parameter - * to the `inject()` function would hide the outer variable. - * - * To help with this, the injected parameters can, optionally, be enclosed with underscores. - * These are ignored by the injector when the reference name is resolved. - * - * For example, the parameter `_myService_` would be resolved as the reference `myService`. - * Since it is available in the function body as _myService_, we can then assign it to a variable - * defined in an outer scope. - * - * ``` - * // Defined out reference variable outside - * var myService; - * - * // Wrap the parameter in underscores - * beforeEach( inject( function(_myService_){ - * myService = _myService_; - * })); - * - * // Use myService in a series of tests. - * it('makes use of myService', function() { - * myService.doStuff(); - * }); - * - * ``` - * - * See also {@link angular.mock.module angular.mock.module} - * - * ## Example - * Example of what a typical jasmine tests looks like with the inject method. - * ```js - * - * angular.module('myApplicationModule', []) - * .value('mode', 'app') - * .value('version', 'v1.0.1'); - * - * - * describe('MyApp', function() { - * - * // You need to load modules that you want to test, - * // it loads only the "ng" module by default. - * beforeEach(module('myApplicationModule')); - * - * - * // inject() is used to inject arguments of all given functions - * it('should provide a version', inject(function(mode, version) { - * expect(version).toEqual('v1.0.1'); - * expect(mode).toEqual('app'); - * })); - * - * - * // The inject and module method can also be used inside of the it or beforeEach - * it('should override a version and test the new version is injected', function() { - * // module() takes functions or strings (module aliases) - * module(function($provide) { - * $provide.value('version', 'overridden'); // override version here - * }); - * - * inject(function(version) { - * expect(version).toEqual('overridden'); - * }); - * }); - * }); - * - * ``` - * - * @param {...Function} fns any number of functions which will be injected using the injector. - */ - - - - var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { - this.message = e.message; - this.name = e.name; - if (e.line) this.line = e.line; - if (e.sourceId) this.sourceId = e.sourceId; - if (e.stack && errorForStack) - this.stack = e.stack + '\n' + errorForStack.stack; - if (e.stackArray) this.stackArray = e.stackArray; - }; - ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; - - window.inject = angular.mock.inject = function() { - var blockFns = Array.prototype.slice.call(arguments, 0); - var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn.call(currentSpec) : workFn; - ///////////////////// - function workFn() { - var modules = currentSpec.$modules || []; - var strictDi = !!currentSpec.$injectorStrict; - modules.unshift('ngMock'); - modules.unshift('ng'); - var injector = currentSpec.$injector; - if (!injector) { - if (strictDi) { - // If strictDi is enabled, annotate the providerInjector blocks - angular.forEach(modules, function(moduleFn) { - if (typeof moduleFn === "function") { - angular.injector.$$annotate(moduleFn); - } - }); - } - injector = currentSpec.$injector = angular.injector(modules, strictDi); - currentSpec.$injectorStrict = strictDi; - } - for (var i = 0, ii = blockFns.length; i < ii; i++) { - if (currentSpec.$injectorStrict) { - // If the injector is strict / strictDi, and the spec wants to inject using automatic - // annotation, then annotate the function here. - injector.annotate(blockFns[i]); - } - try { - /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ - injector.invoke(blockFns[i] || angular.noop, this); - /* jshint +W040 */ - } catch (e) { - if (e.stack && errorForStack) { - throw new ErrorAddingDeclarationLocationStack(e, errorForStack); - } - throw e; - } finally { - errorForStack = null; - } - } - } - }; - - - angular.mock.inject.strictDi = function(value) { - value = arguments.length ? !!value : true; - return isSpecRunning() ? workFn() : workFn; - - function workFn() { - if (value !== currentSpec.$injectorStrict) { - if (currentSpec.$injector) { - throw new Error('Injector already created, can not modify strict annotations'); - } else { - currentSpec.$injectorStrict = value; - } - } - } - }; -} - - -})(window, window.angular); diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 6d26a9ad7..c1929db37 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -7,7 +7,11 @@ "serve": "phonegap --verbose serve" }, "devDependencies": { - "phonegap": "7.1.1" + "phonegap": "7.1.1", + "cordova": "8.0.0", + "ionic": "5.4.16", + "bower": "1.8.8", + "xml2js": "0.4.23" }, "cordova": { "platforms": [ @@ -28,7 +32,7 @@ "cordova-plugin-email-composer": {}, "cordova-plugin-x-socialsharing": {}, "cordova-plugin-inappbrowser": {}, - "de.appplant.cordova.plugin.local-notification-ios9-fix": {}, + "cordova-plugin-local-notification": {}, "cordova-plugin-ionic": { "APP_ID": "e0d8cdec", "CHANNEL_NAME": "Production", @@ -36,6 +40,12 @@ "UPDATE_API": "https://api.ionicjs.com", "MAX_STORE": "2" }, + "cordova-plugin-advanced-http": { + "OKHTTP_VERSION": "3.10.0" + }, + "cordova-plugin-ionic-webview": { + "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+" + }, "cordova-plugin-whitelist": {}, "edu.berkeley.eecs.emission.cordova.auth": {}, "edu.berkeley.eecs.emission.cordova.comm": {}, @@ -50,29 +60,31 @@ } }, "dependencies": { - "cordova-android": "^6.4.0", - "cordova-ios": "^4.5.4", + "cordova-android": "^8.1.0", + "cordova-ios": "^5.1.1", + "cordova-plugin-advanced-http": "^2.4.1", "cordova-plugin-app-version": "~0.1.9", "cordova-plugin-customurlscheme": "~4.3.0", "cordova-plugin-device": "~2.0.1", "cordova-plugin-email-composer": "^0.8.15", - "cordova-plugin-file": "~6.0.1", - "cordova-plugin-inappbrowser": "https://github.com/shankari/cordova-plugin-inappbrowser.git", - "cordova-plugin-ionic": "git+https://github.com/e-mission/cordova-plugin-ionic.git#3.2.0", + "cordova-plugin-file": "~6.0.2", + "cordova-plugin-inappbrowser": "^3.2.0", + "cordova-plugin-ionic-webview": "^4.1.3", + "cordova-plugin-ionic": "5.4.7", "cordova-plugin-whitelist": "~1.3.3", - "cordova-plugin-x-socialsharing": "~5.2.1", - "de.appplant.cordova.plugin.local-notification-ios9-fix": "https://github.com/shankari/cordova-plugin-local-notifications.git", - "e-mission-data-collection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.3.3", - "em-cordova-connection-settings": "git+https://github.com/e-mission/cordova-connection-settings.git#v1.2.0", - "em-cordova-jwt-auth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.5.0", - "em-cordova-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.1", - "em-cordova-server-sync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.1.2", - "em-cordova-transition-notify": "git+https://github.com/e-mission/e-mission-transition-notify.git#v1.2.0", - "em-cordova-unified-logger": "git+https://github.com/e-mission/cordova-unified-logger.git#v1.3.1", - "em-cordova-usercache": "git+https://github.com/e-mission/cordova-usercache.git#v1.1.0", + "cordova-plugin-x-socialsharing": "~5.6.5", + "cordova-plugin-local-notification": "^0.9.0-beta.2", + "e-mission-data-collection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.4.3", + "em-cordova-connection-settings": "git+https://github.com/e-mission/cordova-connection-settings.git#v1.2.1", + "em-cordova-jwt-auth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.6.2-alpha1", + "em-cordova-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.2", + "em-cordova-server-sync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.2.3", + "em-cordova-transition-notify": "git+https://github.com/e-mission/e-mission-transition-notify.git#v1.2.5", + "em-cordova-unified-logger": "git+https://github.com/e-mission/cordova-unified-logger.git#v1.3.2", + "em-cordova-usercache": "git+https://github.com/e-mission/cordova-usercache.git#v1.1.2", "fs-extra": "^5.0.0", "ionic-plugin-keyboard": "~2.2.1", "klaw-sync": "^3.0.2", - "phonegap-plugin-push": "=2.1.2" + "phonegap-plugin-push": "=2.3.0" } } diff --git a/package.serve.json b/package.serve.json index cef8b6847..c9ff9b74a 100644 --- a/package.serve.json +++ b/package.serve.json @@ -7,7 +7,10 @@ "serve": "phonegap --verbose serve" }, "devDependencies": { - "phonegap": "7.1.1" + "phonegap": "7.1.1", + "cordova": "8.0.0", + "ionic": "5.4.16", + "bower": "1.8.8" }, "dependencies": { "fs-extra": "^5.0.0", diff --git a/setup/GoogleService-Info.fake.for_ci.plist b/setup/GoogleService-Info.fake.for_ci.plist new file mode 100644 index 000000000..6d329a814 --- /dev/null +++ b/setup/GoogleService-Info.fake.for_ci.plist @@ -0,0 +1,36 @@ + + + + + AD_UNIT_ID_FOR_BANNER_TEST + ab-cde-fgh-1111111111111111/1111111111 + AD_UNIT_ID_FOR_INTERSTITIAL_TEST + ab-cde-fgh-1111111111111111/1111111111 + CLIENT_ID + 1111111111111-abcdefghijklmnopqrstuvwxyzABCDEF.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.1111111111111-abcdefghijklmnopqrstuvwxyzABCDEF + API_KEY + ABCDEFGHIJKLMNOP-abcdefghijklmnopqrstuv + GCM_SENDER_ID + 1111111111111 + PLIST_VERSION + 1 + BUNDLE_ID + edu.berkeley.eecs.embase + PROJECT_ID + emission-push-both + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1111111111111:ios:1111111111111111 + + diff --git a/setup/google-services.fake.for_ci.json b/setup/google-services.fake.for_ci.json new file mode 100644 index 000000000..5612f6734 --- /dev/null +++ b/setup/google-services.fake.for_ci.json @@ -0,0 +1,42 @@ +{ + "project_info": { + "project_number": "1111111111111", + "firebase_url": "https://fake.for_ci.firebaseio.com", + "project_id": "fake.for_ci", + "storage_bucket": "fake.for_ci.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1111111111111:android:1111111111111111", + "android_client_info": { + "package_name": "edu.berkeley.eecs.emission" + } + }, + "oauth_client": [ + { + "client_id": "1111111111111-11111111111111111111111111111111.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} diff --git a/setup/setup_android_native.sh b/setup/setup_android_native.sh new file mode 100644 index 000000000..a0b3e87be --- /dev/null +++ b/setup/setup_android_native.sh @@ -0,0 +1,38 @@ +echo "Ensure we exit on error" +set -e + +# we can build android on both ubuntu and OSX +# should try both since there may be subtle differences +PLATFORM=`uname -a` + +# both of these have java on Github Actions +# but may not in docker, for example +# should check for the existence of java and die if it doesn't exist +echo "Checking for java in the path" +JAVA_VERSION=`javac -version` +echo "Found java in the path with version $JAVA_VERSION" + +echo "Setting up SDK environment" +ANDROID_BUILD_TOOLS_VERSION=27.0.3 +MIN_SDK_VERSION=21 +TARGET_SDK_VERSION=28 + +# Setup the development environment +source setup/setup_shared.sh + +if [ -z $ANDROID_HOME ]; +then + echo "ANDROID_HOME not set, android SDK not found, exiting" + exit 1 +else + echo "ANDROID_HOME found at $ANDROID_HOME" +fi + +echo "Setting up sdkman" +curl -s "https://get.sdkman.io" | bash +source ~/.sdkman/bin/sdkman-init.sh + +echo "Setting up gradle using SDKMan" +sdk install gradle 4.1 + +source setup/setup_shared_native.sh diff --git a/setup/setup_ios_native.sh b/setup/setup_ios_native.sh new file mode 100644 index 000000000..8c8baeb26 --- /dev/null +++ b/setup/setup_ios_native.sh @@ -0,0 +1,14 @@ +# error out if any command fails +set -e + +COCOAPODS_VERSION=1.9.1 +EXPECTED_PLUGIN_COUNT=15 + +# Setup the development environment +source setup/setup_shared.sh + +echo "Installing cocoapods" +export PATH=~/.gem/ruby/2.6.0/bin:$PATH +gem install --no-document --user-install cocoapods -v $COCOAPODS_VERSION + +source setup/setup_shared_native.sh diff --git a/setup/setup_serve.sh b/setup/setup_serve.sh new file mode 100644 index 000000000..3e455cfb2 --- /dev/null +++ b/setup/setup_serve.sh @@ -0,0 +1,17 @@ +# error out if any command fails +set -e + +# Setup the development environment +source setup/setup_shared.sh + +echo "Configuring the repo for UI development" +./bin/configure_xml_and_json.js serve + +echo "Setting up all npm packages" +npm install + +echo "Updating bower" +npx bower update + +echo "Pulling the plugin-specific UIs" +npm run setup-serve diff --git a/setup/setup_shared.sh b/setup/setup_shared.sh new file mode 100644 index 000000000..a125464ce --- /dev/null +++ b/setup/setup_shared.sh @@ -0,0 +1,25 @@ +export NVM_VERSION=0.35.3 +export NODE_VERSION=13.12.0 +export NPM_VERSION=6.14.4 + +echo "Is this in a CI environment? $CI" +export CI="true" + +echo "Installing the correct version of nvm" +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v$NVM_VERSION/install.sh | bash + +echo "Setting up the variables to run nvm" +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + +echo "Installing the correct node version" +nvm install $NODE_VERSION + +echo "Check the version of npm" +CURR_NPM_VERSION=`npm --version` +if [ $CURR_NPM_VERSION != $NPM_VERSION ]; +then + echo "Invalid npm version, expected $NPM_VERSION, got $CURR_NPM_VERSION" + exit 1 +fi diff --git a/setup/setup_shared_native.sh b/setup/setup_shared_native.sh new file mode 100644 index 000000000..afe1dcb07 --- /dev/null +++ b/setup/setup_shared_native.sh @@ -0,0 +1,43 @@ +./bin/configure_xml_and_json.js cordovabuild + +echo "Copying fake FCM configurations for android and iOS" +cp setup/GoogleService-Info.fake.for_ci.plist GoogleService-Info.plist +cp setup/google-services.fake.for_ci.json google-services.json + +echo "Setting up all npm packages" +npm install + +echo "Updating bower" +npx bower update + +# By default, node doesn't fail if any of the steps fail. This makes it hard to +# use in a CI environment, and leads to people reporting the node error rather +# than the underlying error. One solution is to pass in a command line argument to node +# to turn off that behavior. However, the cordova script automatically invokes node +# and I don't see a .noderc to pass in the config option for all runs +# So for now, I am going to hack this by adding the command line argument to +# the cordova script. If anybody has a better option, they are welcome to share +# it with us! +echo "hack to make the local cordova fail on error" +sed -i -e "s|/usr/bin/env node|/usr/bin/env node --unhandled-rejections=strict|" node_modules/cordova/bin/cordova + +npx cordova prepare + +EXPECTED_COUNT=26 +INSTALLED_COUNT=`npx cordova plugin list | wc -l` +echo "Found $INSTALLED_COUNT plugins, expected $EXPECTED_COUNT" +if [ $INSTALLED_COUNT -lt $EXPECTED_COUNT ]; +then + echo "Found $INSTALLED_COUNT plugins, expected $EXPECTED_COUNT, retrying" + sleep 5 + npx cordova prepare +elif [ $INSTALLED_COUNT -gt $EXPECTED_COUNT ]; +then + echo "Found extra plugins!" + npx cordova plugin list + echo "Failing for investigation" + exit 1 +else + echo "All plugins installed successfully!" +fi + diff --git a/setup/teardown_ios_native.sh b/setup/teardown_ios_native.sh new file mode 100644 index 000000000..9d5ad4a89 --- /dev/null +++ b/setup/teardown_ios_native.sh @@ -0,0 +1,13 @@ +echo "Ensure we exit on error" +set -e + +export COCOAPODS_VERSION=1.9.1 +source setup/teardown_shared.sh + +echo "Uninstalling cocoapods" +gem uninstall cocoapods -v $COCOAPODS_VERSION + +echo "Removing all modules, plugins and platforms to make a fresh start" +rm -rf node_modules +rm -rf plugins +rm -rf platforms diff --git a/setup/teardown_shared.sh b/setup/teardown_shared.sh new file mode 100644 index 000000000..d2b3f8e5e --- /dev/null +++ b/setup/teardown_shared.sh @@ -0,0 +1,8 @@ +export COCOAPODS_VERSION=1.7.5 +export NODE_VERSION=9.4.0 + +echo "Removing .nvm since we installed it" +rm -rf ~/.nvm/$NODE_VERSION + +echo "Removing all the node modules" +rm -rf ./node_modules diff --git a/www/i18n/en.json b/www/i18n/en.json index 04049d222..ed778bb27 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -245,17 +245,31 @@ } }, + "intro": { + "permissions": { + "locationPermExplanation-android-lt-6": "you accepted the permission during installation. You don't need to do anything now.", + "locationPermExplanation-android-gte-6": "please select 'allow'", + "locationPermExplanation-ios-lt-13": "please select 'Always allow'. This allows us to understand your travel even when you are not actively using the app", + "locationPermExplanation-ios-gte-13": "please select 'when in use' now. After a few days, you will be asked whether you want to give the app background permission. Please say 'Always Allow' at that time. This allows us to understand your travel even when you are not actively using the app" + } + }, + "allow_background": { + "samsung": "Disable 'Medium power saving mode'" + }, "consent":{ "button-accept": "I accept", "button-decline": "I refuse" }, - "updatecheck":{ "downloading-update": "Downloading UI-only update", "extracting-update": "Extracting UI-only update", "done": "Update done, reloading...", - "download-new-ui": "Download new UI-only update?.", + "download-new-ui": "Download new UI-only update to build {{build}}?", "download-not-now": "Not now", "download-apply": "Apply" + }, + "sensor_explanation":{ + "button-accept": "OK", + "button-decline": "Stop" } } diff --git a/www/index.html b/www/index.html index 32eeed126..5f0118c37 100644 --- a/www/index.html +++ b/www/index.html @@ -3,7 +3,7 @@ - + diff --git a/www/js/app.js b/www/js/app.js index 78139071a..6340df40a 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -55,18 +55,21 @@ angular.module('emission', ['ionic', throw "blank string instead of missing file on dynamically served app"; } Logger.log("connectionConfigString = "+JSON.stringify(connectionConfig.data)); + $rootScope.connectUrl = connectionConfig.data.connectUrl; window.cordova.plugins.BEMConnectionSettings.setSettings(connectionConfig.data); }).catch(function(err) { // not displaying the error here since we have a backup Logger.log("error "+JSON.stringify(err)+" while reading connection config, reverting to defaults"); window.cordova.plugins.BEMConnectionSettings.getDefaultSettings().then(function(defaultConfig) { Logger.log("defaultConfig = "+JSON.stringify(defaultConfig)); + $rootScope.connectUrl = defaultConfig.connectUrl; window.cordova.plugins.BEMConnectionSettings.setSettings(defaultConfig); }).catch(function(err) { // displaying the error here since we don't have a backup Logger.displayError("Error reading or setting connection defaults", err); }); }); + cordova.plugin.http.setDataSerializer('json'); }); console.log("Ending run"); }) diff --git a/www/js/common/services.js b/www/js/common/services.js index b2e0b3b14..ea6ac57dc 100644 --- a/www/js/common/services.js +++ b/www/js/common/services.js @@ -199,7 +199,7 @@ angular.module('emission.main.common.services', ['emission.plugin.logger']) }; switch (mode) { case 'place': - var url = "http://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.geometry.coordinates[1] + var url = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.geometry.coordinates[1] + "&lon=" + obj.geometry.coordinates[0]; $http.get(url).then(function(response) { console.log("while reading data from nominatim, status = "+response.status @@ -210,7 +210,7 @@ angular.module('emission.main.common.services', ['emission.plugin.logger']) }); break; case 'cplace': - var url = "http://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.location.coordinates[1] + var url = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.location.coordinates[1] + "&lon=" + obj.location.coordinates[0]; $http.get(url).then(function(response) { @@ -222,7 +222,7 @@ angular.module('emission.main.common.services', ['emission.plugin.logger']) }); break; case 'ctrip': - var url0 = "http://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.start_loc.coordinates[1] + var url0 = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.start_loc.coordinates[1] + "&lon=" + obj.start_loc.coordinates[0]; console.log("About to make call "+url0); $http.get(url0).then(function(response) { @@ -232,7 +232,7 @@ angular.module('emission.main.common.services', ['emission.plugin.logger']) }, function(error) { console.log("while reading data from nominatim, error = "+error); }); - var url1 = "http://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.end_loc.coordinates[1] + var url1 = "https://nominatim.openstreetmap.org/reverse?format=json&lat=" + obj.end_loc.coordinates[1] + "&lon=" + obj.end_loc.coordinates[0]; console.log("About to make call "+url1); $http.get(url1).then(function(response) { diff --git a/www/js/diary/current.js b/www/js/diary/current.js index 996f3a453..491047ebb 100644 --- a/www/js/diary/current.js +++ b/www/js/diary/current.js @@ -8,7 +8,7 @@ 'emission.plugin.logger']) .controller('CurrMapCtrl', function($scope, Config, $state, $timeout, $ionicActionSheet,leafletData, - Logger, $window, PostTripManualMarker, CommHelper, $http, KVStore, $ionicPlatform, $translate) { + Logger, $window, PostTripManualMarker, CommHelper, KVStore, $ionicPlatform, $translate) { console.log("controller CurrMapCtrl called from current.js"); var _map; @@ -232,7 +232,9 @@ var getServerIncidents = function() { Logger.log("Getting server incidents with call "+JSON.stringify(incidentServerCalldata)); - $http.post("https://e-mission.eecs.berkeley.edu/result/heatmap/incidents/timestamp", incidentServerCalldata).then(function(res){ + CommHelper.getAggregateData("result/heatmap/incidents/timestamp", incidentServerCalldata) + .then(function(res){ + $scope.$apply(function() { Logger.log("Server incidents result is "+JSON.stringify(res)); // Need to remove existing markers before adding new ones // https://github.com/e-mission/e-mission-phone/pull/263#issuecomment-322669042 @@ -241,6 +243,7 @@ if(res.data.incidents.length > 0) { addIncidents(res.data.incidents, _map, _serverIncidentMarkers); } + }); }, function(error){ Logger.log("Error when getting incidents"); Logger.log(JSON.stringify(error)); diff --git a/www/js/diary/list.js b/www/js/diary/list.js index d7058f129..4b8b8267e 100644 --- a/www/js/diary/list.js +++ b/www/js/diary/list.js @@ -123,30 +123,37 @@ angular.module('emission.main.diary.list',['ui-leaflet', } } - $scope.datepickerObject = { - - todayLabel: $translate.instant('list-datepicker-today'), //Optional - closeLabel: $translate.instant('list-datepicker-close'), //Optional - setLabel: $translate.instant('list-datepicker-set'), //Optional - monthsList: moment.monthsShort(), - weeksList: moment.weekdaysMin(), - titleLabel: $translate.instant('diary.list-pick-a-date'), - setButtonType : 'button-positive', //Optional - todayButtonType : 'button-stable', //Optional - closeButtonType : 'button-stable', //Optional - inputDate: new Date(), //Optional - from: new Date(2015, 1, 1), - to: new Date(), - mondayFirst: true, //Optional - templateType: 'popup', //Optional - showTodayButton: 'true', //Optional - modalHeaderColor: 'bar-positive', //Optional - modalFooterColor: 'bar-positive', //Optional - callback: $scope.setCurrDay, //Mandatory - dateFormat: 'dd MMM yyyy', //Optional - closeOnSelect: true //Optional + $scope.getDatePickerObject = function() { + return { + todayLabel: $translate.instant('list-datepicker-today'), //Optional + closeLabel: $translate.instant('list-datepicker-close'), //Optional + setLabel: $translate.instant('list-datepicker-set'), //Optional + monthsList: moment.monthsShort(), + weeksList: moment.weekdaysMin(), + titleLabel: $translate.instant('diary.list-pick-a-date'), + setButtonType : 'button-positive', //Optional + todayButtonType : 'button-stable', //Optional + closeButtonType : 'button-stable', //Optional + inputDate: new Date(), //Optional + from: new Date(2015, 1, 1), + to: new Date(), + mondayFirst: true, //Optional + templateType: 'popup', //Optional + showTodayButton: 'true', //Optional + modalHeaderColor: 'bar-positive', //Optional + modalFooterColor: 'bar-positive', //Optional + callback: $scope.setCurrDay, //Mandatory + dateFormat: 'dd MMM yyyy', //Optional + closeOnSelect: true //Optional + } }; + $scope.datepickerObject = $scope.getDatePickerObject(); + + $ionicPlatform.on("resume", function() { + $scope.datepickerObject = $scope.getDatePickerObject(); + }); + $scope.pickDay = function() { ionicDatePicker.openDatePicker($scope.datepickerObject); } diff --git a/www/js/heatmap.js b/www/js/heatmap.js index 66d52e7c4..c76f18247 100644 --- a/www/js/heatmap.js +++ b/www/js/heatmap.js @@ -4,8 +4,8 @@ angular.module('emission.main.heatmap',['ui-leaflet', 'emission.services', 'emission.plugin.logger', 'emission.incident.posttrip.manual', 'ng-walkthrough', 'nzTour', 'emission.plugin.kvstore']) -.controller('HeatmapCtrl', function($scope, $ionicLoading, $ionicActionSheet, $http, - leafletData, Logger, Config, PostTripManualMarker, +.controller('HeatmapCtrl', function($scope, $ionicLoading, $ionicActionSheet, + leafletData, Logger, Config, PostTripManualMarker, CommHelper, $window, nzTour, KVStore, $translate) { $scope.mapCtrl = {}; @@ -44,11 +44,13 @@ angular.module('emission.main.heatmap',['ui-leaflet', 'emission.services', sel_region: null }; Logger.log("Sending data "+JSON.stringify(data)); - return $http.post("https://e-mission.eecs.berkeley.edu/result/heatmap/pop.route/local_date", data) + return CommHelper.getAggregateData("result/heatmap/pop.route/local_date", data) .then(function(response) { if (angular.isDefined(response.data.lnglat)) { Logger.log("Got points in heatmap "+response.data.lnglat.length); - $scope.showHeatmap(response.data.lnglat); + $scope.$apply(function() { + $scope.showHeatmap(response.data.lnglat); + }); } else { Logger.log("did not find latlng in response data "+JSON.stringify(response.data)); } @@ -240,7 +242,10 @@ angular.module('emission.main.heatmap',['ui-leaflet', 'emission.services', // Don't set any layer - it will be filled in when the load completes } else { $ionicLoading.hide(); - if (angular.isDefined(selData) && angular.isDefined(selData.layer)) { + if (angular.isDefined(selData) && + angular.isDefined(selData.layer) && + angular.isDefined(selData.bounds) && + selData.bounds.isValid()) { selData.layer.addTo(map); map.fitBounds(selData.bounds); $scope.selData = selData; @@ -286,11 +291,13 @@ angular.module('emission.main.heatmap',['ui-leaflet', 'emission.services', sel_region: null }; Logger.log("Sending data "+JSON.stringify(data)); - return $http.post("https://e-mission.eecs.berkeley.edu/result/heatmap/incidents/local_date", data) + return CommHelper.getAggregateData("result/heatmap/incidents/local_date", data) .then(function(response) { if (angular.isDefined(response.data.incidents)) { - Logger.log("Got incidents"+response.data.incidents.length); - $scope.showIncidents(response.data.incidents); + $scope.$apply(function() { + Logger.log("Got incidents"+response.data.incidents.length); + $scope.showIncidents(response.data.incidents); + }); } else { Logger.log("did not find incidents in response data "+JSON.stringify(response.data)); } diff --git a/www/js/intro.js b/www/js/intro.js index 43da7f16d..bedcc41b0 100644 --- a/www/js/intro.js +++ b/www/js/intro.js @@ -18,27 +18,56 @@ angular.module('emission.intro', ['emission.splash.startprefs', }); }) -.controller('IntroCtrl', function($scope, $state, $ionicSlideBoxDelegate, +.controller('IntroCtrl', function($scope, $state, $window, $ionicSlideBoxDelegate, $ionicPopup, $ionicHistory, ionicToast, $timeout, CommHelper, StartPrefs, $translate, $cordovaFile) { - - $scope.getConsentFile = function () { + + $scope.platform = $window.device.platform; + $scope.osver = $window.device.version.split(".")[0]; + if($scope.platform.toLowerCase() == "android") { + if($scope.osver < 6) { + $scope.locationPermExplanation = $translate.instant('intro.permissions.locationPermExplanation-android-lt-6'); + } else { + $scope.locationPermExplanation = $translate.instant("intro.permissions.locationPermExplanation-android-gte-6"); + } + } + + if($scope.platform.toLowerCase() == "ios") { + if($scope.osver < 13) { + $scope.locationPermExplanation = $translate.instant("intro.permissions.locationPermExplanation-ios-lt-13"); + } else { + $scope.locationPermExplanation = $translate.instant("intro.permissions.locationPermExplanation-ios-gte-13"); + } + } + + $scope.backgroundRestricted = false; + if($window.device.manufacturer.toLowerCase() == "samsung") { + $scope.backgroundRestricted = true; + $scope.allowBackgroundInstructions = $translate.instant("intro.allow_background.samsung"); + } + + console.log("Explanation = "+$scope.locationPermExplanation); + + // The language comes in between the first and second part + $scope.geti18nFile = function (fpFirstPart, fpSecondPart) { var lang = $translate.use(); - $scope.consentFile = "templates/intro/consent.html"; + var defaultVal = fpFirstPart + fpSecondPart; if (lang != 'en') { - var url = "www/i18n/intro/consent-" + lang + ".html"; + var url = fpFirstPart + lang + fpSecondPart; $cordovaFile.checkFile(cordova.file.applicationDirectory, url).then( function(result){ window.Logger.log(window.Logger.LEVEL_DEBUG, "Successfully found the consent file, result is " + JSON.stringify(result)); - $scope.consentFile = url.replace("www/", ""); + return url.replace("www/", ""); }, function (err) { window.Logger.log(window.Logger.LEVEL_DEBUG, "Consent file not found, loading english version, error is " + JSON.stringify(err)); - $scope.consentFile = "templates/intro/consent.html"; + return defaultVal; }); } + return defaultVal; } - $scope.getConsentFile(); + $scope.consentFile = $scope.geti18nFile("templates/intro/consent", ".html"); + $scope.explainFile = $scope.geti18nFile("templates/intro/sensor_explanation", ".html"); $scope.getIntroBox = function() { return $ionicSlideBoxDelegate.$getByHandle('intro-box'); diff --git a/www/js/metrics.js b/www/js/metrics.js index bba469742..3b9cfa26c 100644 --- a/www/js/metrics.js +++ b/www/js/metrics.js @@ -11,7 +11,7 @@ angular.module('emission.main.metrics',['nvd3', CommHelper, $window, $ionicPopup, ionicDatePicker, $ionicPlatform, FootprintHelper, CalorieCal, $ionicModal, $timeout, KVStore, CarbonDatasetHelper, - $rootScope, $location, $state, ReferHelper, $http, Logger, + $rootScope, $location, $state, ReferHelper, Logger, $translate) { var lastTwoWeeksQuery = true; var first = true; @@ -419,9 +419,8 @@ angular.module('emission.main.metrics',['nvd3', delete clonedData.metric; clonedData.metric_list = [DURATION, MEDIAN_SPEED, COUNT, DISTANCE]; clonedData.is_return_aggregate = true; - var getMetricsResult = $http.post( - "https://e-mission.eecs.berkeley.edu/result/metrics/timestamp", - clonedData) + var getMetricsResult = CommHelper.getAggregateData( + "result/metrics/timestamp", clonedData) return getMetricsResult; } @@ -508,16 +507,16 @@ angular.module('emission.main.metrics',['nvd3', $scope.uictrl.hasAggr = true; if (angular.isDefined($scope.chartDataAggr)) { //Only have to check one because // Restore the $apply if/when we go away from $http - // $scope.$apply(function() { + $scope.$apply(function() { if (!$scope.uictrl.showMe) { $scope.showCharts($scope.chartDataAggr); } - // }) + }) } else { - // $scope.$apply(function() { + $scope.$apply(function() { $scope.showCharts([]); console.log("did not find aggregate result in response data "+JSON.stringify(results[2])); - // }); + }); } }) .catch(function(error) { diff --git a/www/js/services.js b/www/js/services.js index 3ac05644d..716660f7d 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -3,7 +3,7 @@ angular.module('emission.services', ['emission.plugin.logger', 'emission.plugin.kvstore']) -.service('CommHelper', function($http) { +.service('CommHelper', function($rootScope) { var getConnectURL = function(successCallback, errorCallback) { window.cordova.plugins.BEMConnectionSettings.getSettings( function(settings) { @@ -159,6 +159,26 @@ angular.module('emission.services', ['emission.plugin.logger', window.cordova.plugins.BEMServerComm.getUserPersonalData("/pipeline/get_complete_ts", resolve, reject); }); }; + + // host is automatically read from $rootScope.connectUrl, which is set in app.js + this.getAggregateData = function(path, data) { + return new Promise(function(resolve, reject) { + const full_url = $rootScope.connectUrl+"/"+path; + console.log("getting aggregate data without user authentication from " + + full_url +" with arguments "+JSON.stringify(data)); + const options = { + method: 'post', + data: data, + responseType: 'json' + } + cordova.plugin.http.sendRequest(full_url, options, + function(response) { + resolve(response); + }, function(error) { + reject(error); + }); + }); + }; }) .service('ReferHelper', function($http) { @@ -564,7 +584,7 @@ angular.module('emission.services', ['emission.plugin.logger', config.getMapTiles = function() { return { - tileLayer: 'http://tile.stamen.com/terrain/{z}/{x}/{y}.png', + tileLayer: 'https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png', tileLayerOptions: { attribution: 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.', opacity: 0.9, diff --git a/www/js/splash/updatecheck.js b/www/js/splash/updatecheck.js index f788f1499..74fbb5274 100644 --- a/www/js/splash/updatecheck.js +++ b/www/js/splash/updatecheck.js @@ -26,13 +26,11 @@ angular.module('emission.splash.updatecheck', ['emission.plugin.logger', Logger.log("currChannel == null, skipping deploy init"); return Promise.resolve(null); } else { - return new Promise(function(resolve, reject) { - var config = { - appId: "e0d8cdec", - channel: currChannel - } - deploy.init(config, resolve, reject); - }); + var config = { + appId: "e0d8cdec", + channel: currChannel + } + return deploy.configure(config); } }; @@ -48,44 +46,22 @@ angular.module('emission.splash.updatecheck', ['emission.plugin.logger', uc.checkPromise = function() { var deploy = $window.IonicCordova.deploy; - return new Promise(function(resolve, reject) { - deploy.check(resolve, reject); - }); + return deploy.checkForUpdate(); }; uc.downloadPromise = function() { var deploy = $window.IonicCordova.deploy; - return new Promise(function(resolve, reject) { - deploy.download(function(res) { - if(res == 'true') { - resolve(res); - } else { - updateProgress(res); - } - }, reject); - }); + return deploy.downloadUpdate(updateProgress); }; uc.extractPromise = function() { var deploy = $window.IonicCordova.deploy; - return new Promise(function(resolve, reject) { - deploy.extract(function(res) { - console.log("extract progress = "+res); - var expectedResult = $window.cordova.platformId == "ios"? "done": "true"; - if(res == expectedResult) { - resolve(res); - } else { - updateProgress(res); - } - }, reject); - }); + return deploy.extractUpdate(updateProgress); }; uc.redirectPromise = function() { var deploy = $window.IonicCordova.deploy; - return new Promise(function(resolve, reject) { - deploy.redirect(resolve, reject); - }); + return deploy.reloadApp(); }; uc.handleClientChangeURL = function(urlComponents) { @@ -166,13 +142,13 @@ angular.module('emission.splash.updatecheck', ['emission.plugin.logger', getChannelToUse().then(function(currChannel) { uc.initChannelPromise(currChannel).then(function() { Logger.log("deploy init complete "); - uc.checkPromise().then(function(hasUpdate) { - Logger.log('Ionic Deploy: Update available: ' + hasUpdate); - if (hasUpdate == 'true') { + uc.checkPromise().then(function(updateResponse) { + Logger.log('Ionic Deploy: Update available: ' + JSON.stringify(updateResponse)); + if (updateResponse.available == true) { Logger.log('Ionic Deploy: found update, asking user: '); $ionicPopup.show({ - title: $translate.instant('updatecheck.download-new-ui'), + title: $translate.instant('updatecheck.download-new-ui', updateResponse), templateUrl: 'templates/splash/release-notes.html', scope: $rootScope, buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer. diff --git a/www/manual_lib/ionic-datepicker/ionic-datepicker.bundle.min.js b/www/manual_lib/ionic-datepicker/ionic-datepicker.bundle.min.js index 69f8959d2..70a4f41b9 100644 --- a/www/manual_lib/ionic-datepicker/ionic-datepicker.bundle.min.js +++ b/www/manual_lib/ionic-datepicker/ionic-datepicker.bundle.min.js @@ -1 +1 @@ -"undefined"!=typeof document&&function(e,t){var n=e.createElement("style");if(e.getElementsByTagName("head")[0].appendChild(n),n.styleSheet)n.styleSheet.disabled||(n.styleSheet.cssText=t);else try{n.innerHTML=t}catch(e){n.innerText=t}}(document,"/**/\n.padding_zero {\n padding: 0;\n}\n\n.ionic_datepicker_popup .font_bold {\n font-weight: bold;\n}\n\n.ionic_datepicker_popup .padding_top_zero {\n padding-top: 0;\n}\n\n.ionic_datepicker_popup .padding_left_5px {\n padding-left: 5px;\n}\n\n.ionic_datepicker_popup .padding_right_5px {\n padding-right: 5px;\n}\n\n.ionic_datepicker_popup .month_year_section {\n padding: 5px 0;\n}\n\n.ionic_datepicker_popup .calendar_grid {\n height: 215px;\n}\n\n.ionic_datepicker_popup .calendar_grid .weeks_row {\n padding: 0;\n}\n\n.ionic_datepicker_popup .today {\n border: 1px solid #009688;\n border-radius: 50%;\n}\n\n.ionic_datepicker_popup .selected_date {\n background-color: #009688;\n border-radius: 50%;\n color: #ffffff;\n font-weight: bold;\n}\n\n.ionic_datepicker_popup .popup-head {\n background-color: #009688;\n display: none;\n}\n\n.ionic_datepicker_popup .popup-head .popup-title {\n color: #ffffff;\n}\n\n.ionic_datepicker_popup .popup-head .popup-sub-title {\n color: #ffffff;\n}\n\n.ionic_datepicker_popup .popup-body {\n background-color: #ffffff;\n}\n\n.ionic_datepicker_popup .popup-body .selected_date_full {\n background-color: #009688;\n margin: -10px -10px 0 -10px;\n height: 45px;\n text-align: center;\n font-weight: bold;\n color: #ffffff;\n line-height: 45px;\n font-size: 18px;\n}\n\n.ionic_datepicker_popup .popup-body .select_section {\n padding: 1px 5px;\n}\n\n.ionic_datepicker_popup .popup-body .pointer_events_none {\n pointer-events: none;\n color: #aaaaaa !important;\n}\n\n.ionic_datepicker_popup .popup-body .month_select, .ionic_datepicker_popup .popup-body .year_select {\n border: none;\n border-bottom: 1px solid #009688;\n padding: 0;\n}\n\n.ionic_datepicker_popup .popup-body .month_select .input-label, .ionic_datepicker_popup .popup-body .year_select .input-label {\n padding: 2px 0;\n width: 0;\n}\n\n.ionic_datepicker_popup .popup-body .month_select select, .ionic_datepicker_popup .popup-body .year_select select {\n left: 10px;\n border: none;\n padding: 0;\n}\n\n.ionic_datepicker_popup .popup-body .month_select:after, .ionic_datepicker_popup .popup-body .year_select:after {\n right: 5px;\n color: #009688;\n}\n\n.ionic_datepicker_popup .popup-body .show_nav {\n padding: 5px 0 0 0;\n}\n\n.ionic_datepicker_popup .popup-body .show_nav .prev_btn_section {\n padding: 5px 0;\n text-align: left;\n}\n\n.ionic_datepicker_popup .popup-body .show_nav .prev_btn_section button {\n padding: 0;\n}\n\n.ionic_datepicker_popup .popup-body .show_nav .next_btn_section {\n padding: 5px 0;\n text-align: right;\n}\n\n.ionic_datepicker_popup .popup-body .show_nav .next_btn_section button {\n padding: 0;\n}\n\n.ionic_datepicker_popup .popup-body .button-clear {\n color: #009688;\n}\n\n.ionic_datepicker_popup .popup-buttons {\n padding: 0;\n min-height: 45px;\n}\n\n.ionic_datepicker_popup .popup-buttons button {\n background-color: #009688;\n border-radius: 0;\n margin-right: 1px;\n color: #ffffff;\n}\n\n.ionic_datepicker_popup .row + .row {\n padding: 0;\n}\n\n.ionic_datepicker_modal .header, .ionic_datepicker_modal .footer {\n background-color: #009688;\n}\n\n.ionic_datepicker_modal .header .title, .ionic_datepicker_modal .header .button, .ionic_datepicker_modal .footer .title, .ionic_datepicker_modal .footer .button {\n color: #ffffff;\n}\n\n.ionic_datepicker_modal .footer .button-block {\n margin: 0;\n}\n\n.ionic_datepicker_modal .today {\n border: 1px solid #009688;\n}\n\n.ionic_datepicker_modal .selected_date {\n background-color: #009688;\n color: #ffffff;\n font-weight: bold;\n}\n\n.ionic_datepicker_modal .pointer_events_none {\n pointer-events: none;\n color: #aaaaaa !important;\n}\n\n.ionic_datepicker_modal .select_section {\n padding: 1px 5px;\n}\n\n.ionic_datepicker_modal .button-clear {\n color: #009688;\n}\n\n.ionic_datepicker_modal .month_select, .ionic_datepicker_modal .year_select {\n border: none;\n border-bottom: 1px solid #009688;\n padding: 0;\n}\n\n.ionic_datepicker_modal .month_select .input-label, .ionic_datepicker_modal .year_select .input-label {\n padding: 2px 0;\n width: 0;\n}\n\n.ionic_datepicker_modal .month_select select, .ionic_datepicker_modal .year_select select {\n left: 10px;\n border: none;\n padding: 0 10px;\n}\n\n.ionic_datepicker_modal .month_select:after, .ionic_datepicker_modal .year_select:after {\n right: 5px;\n color: #009688;\n}\n\n.ionic_datepicker_modal .padding_left_5px {\n padding-left: 5px;\n}\n\n.ionic_datepicker_modal .padding_right_5px {\n padding-right: 5px;\n}\n\n.ionic_datepicker_modal .date_col {\n height: 50px;\n line-height: 50px;\n}\n\n.ionic_datepicker_modal .font_bold {\n font-weight: bold;\n}\n\n.ionic_datepicker_modal .font_22px {\n font-size: 22px;\n}\n\n.platform-android .ionic_datepicker_modal .bar .title.title-left {\n text-align: center;\n}\n\n.platform-android .ionic_datepicker_modal select {\n left: 25%;\n}\n\n.platform-ios .ionic_datepicker_modal select {\n left: 5%;\n}\n\n.date_col {\n cursor: pointer;\n}"),function(t){try{t=angular.module("ionic-datepicker.templates")}catch(e){t=angular.module("ionic-datepicker.templates",[])}t.run(["$templateCache",function(e){e.put("ionic-datepicker-modal.html",'

{{mainObj.titleLabel || selctedDateEpoch | date : mainObj.dateFormat}}

{{dayList[row + col].date}}
')}])}(),function(t){try{t=angular.module("ionic-datepicker.templates")}catch(e){t=angular.module("ionic-datepicker.templates",[])}t.run(["$templateCache",function(e){e.put("ionic-datepicker-popup.html",'
{{mainObj.titleLabel || selctedDateEpoch | date : mainObj.dateFormat}}
{{dayList[row + col].date}}
')}])}(),angular.module("ionic-datepicker",["ionic","ionic-datepicker.service","ionic-datepicker.provider","ionic-datepicker.templates"]),angular.module("ionic-datepicker.provider",[]).provider("ionicDatePicker",function(){var s={titleLabel:null,setLabel:"Set",todayLabel:"Today",closeLabel:"Close",inputDate:new Date,mondayFirst:!0,weeksList:["S","M","T","W","T","F","S"],monthsList:["Jan","Feb","March","April","May","June","July","Aug","Sept","Oct","Nov","Dec"],templateType:"popup",showTodayButton:!1,closeOnSelect:!1,disableWeekdays:[]};this.configDatePicker=function(e){angular.extend(s,e)},this.$get=["$rootScope","$ionicPopup","$ionicModal","IonicDatepickerService",function(e,n,t,r){var a={},l=e.$new();function p(e){return e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0),e}l.today=p(new Date).getTime(),l.disabledDates=[],l.data={},l.prevMonth=function(){1===l.currentDate.getMonth()&&l.currentDate.setFullYear(l.currentDate.getFullYear()),l.currentDate.setMonth(l.currentDate.getMonth()-1),l.data.currentMonth=l.mainObj.monthsList[l.currentDate.getMonth()],l.data.currentYear=l.currentDate.getFullYear(),i(l.currentDate),o()},l.nextMonth=function(){11===l.currentDate.getMonth()&&l.currentDate.setFullYear(l.currentDate.getFullYear()),l.currentDate.setDate(1),l.currentDate.setMonth(l.currentDate.getMonth()+1),l.data.currentMonth=l.mainObj.monthsList[l.currentDate.getMonth()],l.data.currentYear=l.currentDate.getFullYear(),l.monthChanged(l.currentDate.getMonth()),i(new Date),o()};var o=function(){var e=new Date(l.selctedDateEpoch);e.setMonth(l.currentDate.getMonth()),e.setYear(l.currentDate.getFullYear()),l.selctedDateEpoch=e.getTime(),l.mainObj.callback(l.selctedDateEpoch)};function i(e){e=p(e),l.currentDate=angular.copy(e);var t,n,a=new Date(e.getFullYear(),e.getMonth(),1).getDate(),o=new Date(e.getFullYear(),e.getMonth()+1,0).getDate();l.monthsList=[],l.mainObj.monthsList&&12===l.mainObj.monthsList.length?l.monthsList=l.mainObj.monthsList:l.monthsList=r.monthsList,l.yearsList=r.getYearsList(l.mainObj.from,l.mainObj.to),l.dayList=[],l.firstDayEpoch=p(new Date(e.getFullYear(),e.getMonth(),a)).getTime(),l.lastDayEpoch=p(new Date(e.getFullYear(),e.getMonth(),o)).getTime();for(var i=a;i<=o;i++)n=(t=new Date(e.getFullYear(),e.getMonth(),i)).getTime()l.toDate||0<=l.mainObj.disableWeekdays.indexOf(t.getDay()),l.dayList.push({date:t.getDate(),month:t.getMonth(),year:t.getFullYear(),day:t.getDay(),epoch:t.getTime(),disabled:n});var c=l.dayList[0].day-l.mainObj.mondayFirst;c=c<0?6:c;for(var d=0;d

{{mainObj.titleLabel || selctedDateEpoch | date : mainObj.dateFormat}}

{{dayList[row + col].date}}
')}])}(),function(t){try{t=angular.module("ionic-datepicker.templates")}catch(e){t=angular.module("ionic-datepicker.templates",[])}t.run(["$templateCache",function(e){e.put("ionic-datepicker-popup.html",'
{{mainObj.titleLabel || selctedDateEpoch | date : mainObj.dateFormat}}
{{dayList[row + col].date}}
')}])}(),angular.module("ionic-datepicker",["ionic","ionic-datepicker.service","ionic-datepicker.provider","ionic-datepicker.templates"]),angular.module("ionic-datepicker.provider",[]).provider("ionicDatePicker",function(){var s={titleLabel:null,setLabel:"Set",todayLabel:"Today",closeLabel:"Close",inputDate:new Date,mondayFirst:!0,weeksList:["S","M","T","W","T","F","S"],monthsList:["Jan","Feb","March","April","May","June","July","Aug","Sept","Oct","Nov","Dec"],templateType:"popup",showTodayButton:!1,closeOnSelect:!1,disableWeekdays:[]};this.configDatePicker=function(e){angular.extend(s,e)},this.$get=["$rootScope","$ionicPopup","$ionicModal","IonicDatepickerService",function(e,n,t,r){var a={},l=e.$new();function p(e){return e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0),e}l.today=p(new Date).getTime(),l.disabledDates=[],l.data={},l.prevMonth=function(){1===l.currentDate.getMonth()&&l.currentDate.setFullYear(l.currentDate.getFullYear()),l.currentDate.setMonth(l.currentDate.getMonth()-1),l.data.currentMonth=l.mainObj.monthsList[l.currentDate.getMonth()],l.data.currentYear=l.currentDate.getFullYear(),i(l.currentDate),o()},l.nextMonth=function(){11===l.currentDate.getMonth()&&l.currentDate.setFullYear(l.currentDate.getFullYear()),l.currentDate.setDate(1),l.currentDate.setMonth(l.currentDate.getMonth()+1),l.data.currentMonth=l.mainObj.monthsList[l.currentDate.getMonth()],l.data.currentYear=l.currentDate.getFullYear(),l.monthChanged(l.currentDate.getMonth()),i(new Date),o()};var o=function(){var e=new Date(l.selctedDateEpoch);e.setMonth(l.currentDate.getMonth()),e.setYear(l.currentDate.getFullYear()),l.selctedDateEpoch=e.getTime(),l.mainObj.callback(l.selctedDateEpoch)};function i(e){e=p(e),l.currentDate=angular.copy(e);var t,n,a=new Date(e.getFullYear(),e.getMonth(),1).getDate(),o=new Date(e.getFullYear(),e.getMonth()+1,0).getDate();l.monthsList=[],l.mainObj.monthsList&&12===l.mainObj.monthsList.length?l.monthsList=l.mainObj.monthsList:l.monthsList=r.monthsList,l.yearsList=r.getYearsList(l.mainObj.from,l.mainObj.to),l.dayList=[],l.firstDayEpoch=p(new Date(e.getFullYear(),e.getMonth(),a)).getTime(),l.lastDayEpoch=p(new Date(e.getFullYear(),e.getMonth(),o)).getTime();for(var i=a;i<=o;i++)n=(t=new Date(e.getFullYear(),e.getMonth(),i)).getTime()l.toDate||0<=l.mainObj.disableWeekdays.indexOf(t.getDay()),l.dayList.push({date:t.getDate(),month:t.getMonth(),year:t.getFullYear(),day:t.getDay(),epoch:t.getTime(),disabled:n});for(var c=(c=l.dayList[0].day-l.mainObj.mondayFirst)<0?6:c,d=0;dQuestions
- +
diff --git a/www/templates/intro/intro.html b/www/templates/intro/intro.html index 671f660af..f4fdc0e31 100644 --- a/www/templates/intro/intro.html +++ b/www/templates/intro/intro.html @@ -6,6 +6,9 @@ + + + diff --git a/www/templates/intro/sensor_explanation.html b/www/templates/intro/sensor_explanation.html new file mode 100644 index 000000000..d449c2d78 --- /dev/null +++ b/www/templates/intro/sensor_explanation.html @@ -0,0 +1,43 @@ + + + + + + +