diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..35bfce07
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[BUG] "
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..8403b86c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[FEATURE] "
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..5c1c24d3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,57 @@
+#Common
+/build
+/.idea
+/.dart_tool
+/.vscode
+
+#Flutter
+.packages
+.project
+.flutter-plugins
+.flutter-plugins-dependencies
+
+#Android
+/android/local.properties
+/android/.gradle
+/android/.idea
+/android/.settings
+/android/android.iml
+/android/illinois-client-android.iml
+/android/app/app.iml
+/android/app/android-app.iml
+/android/app/.externalNativeBuild
+/android/app/bin
+/android/app/.idea
+/android/app/.classpath
+/android/app/.settings
+/android/app/gradlew
+/android/app/gradlew.bat
+/android/app/local.properties
+/android/app/gradle
+
+#iOS
+DerivedData
+xcuserdata
+/ios/.symlinks
+/ios/Pods
+/ios/Podfile.lock
+/ios/Flutter/App.framework
+/ios/Flutter/Flutter.framework
+/ios/Flutter/Generated.xcconfig
+/ios/Flutter/flutter_assets/*
+/ios/Flutter/flutter_export_environment.sh
+
+#Secret/Private files
+/.travis.yml
+/secrets.tar.enc
+
+/assets/configs.json.enc
+
+/ios/Runner/GoogleService-Info-Debug.plist
+/ios/Runner/GoogleService-Info-Release.plist
+
+/android/keys.properties
+/android/app/src/debug/google-services.json
+/android/app/src/release/google-services.json
+/android/app/src/profile/google-services.json
+
diff --git a/.metadata b/.metadata
new file mode 100644
index 00000000..033ad2af
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b
+ channel: stable
+
+project_type: app
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..0429379e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Unreleased
+### Added
+- Latest content from the private repository.
+- GitHub Issue templates.
+
+### Changed
+- Update README and repository description.
+- Clean up CHANGELOG.
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 00000000..24140ca5
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,2 @@
+# Default assignment
+* @mihail-varbanov
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..7760cddd
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,34 @@
+# Rokwire Code of Conduct
+
+The Rokwire Code of Conduct outlines expectations of participant behaviors and responsibilities within the Rokwire open source community, as well as steps to reporting unacceptable behavior. This code is not exhaustive or complete. It distills our common understanding of a collaborative, shared environment, and our goals. We expect it to be followed in spirit as much as in the letter.
+
+Our code both protects the integrity of the Rokwire community and establishes boundaries to ensure an inclusive, ethical culture. We are committed to providing a welcoming and inspiring community for all. The expected behaviors described in this code of conduct are communicated, monitored, and administered by the Rokwire open source community, with escalation through the Rokwire Open Source Community Manager.
+
+The Rokwire open source community is:
+
+- **Welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
+- **Friendly and Patient.** You might not be communicating with someone in their primary spoken or programming language, and others may not have your level of understanding.
+- **Considerate.** Your work is used by others, and you depend on the work of others. Your decisions affect users and colleagues, so be sure to take those consequences into account.
+- **Careful and Kind in the Words We Choose.** We are a community of professionals and we conduct ourselves professionally. We do not insult or put down others. Harassment and other exclusionary behavior are not acceptable. This includes, but is not limited to:
+ - Violent threats or language
+ - Discriminatory or derogatory jokes and language
+ - Posting sexually explicit or violent material
+ - Posting, or threatening to post, people's personally identifying information ("doxing")
+ - Insults, especially those using discriminatory terms or slurs
+ - Behavior that could be perceived as sexual attention
+ - Advocating for or encouraging any of the above behaviors
+- **Respectfully Curious about Why We Disagree.** Disagreements, both social and technical, happen all the time. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. We might all experience some frustration now and then, but we do not allow that frustration to turn into a personal attack. We resolve disagreements and differing views constructively instead of blaming each other. Our focus is learning from our mistakes and helping to resolve issues together. The strength of our community comes from its diversity, and a community cannot be productive when people feel uncomfortable or threatened.
+## Reporting Code of Conduct Issues
+If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via rokwire@illinois.edu. In your report please include:
+
+ - Your contact information.
+ - Names (real, usernames or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well.
+ - Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public chat log), please include a link or attachment.
+ - Any additional information that may be helpful.
+
+All reports will be reviewed by a multi-person team and will result in a response that is considered necessary and appropriate to the circumstances. Where additional perspectives are needed, the team may seek insight from others with relevant expertise or experience. The confidentiality of the person reporting the incident will be kept at all times. Involved parties are never part of the review team. Further details of specific policies regarding implementing the code of conduct may be posted separately.
+
+Project maintainers are expected to follow and administer the code of conduct in good faith. Those who refuse to do so may face temporary or permanent repercussions as determined by members of the project's leadership. Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the project’s leadership may take any action they deem appropriate, up to and including a permanent ban from our community without warning.
+
+## Attribution & Acknowledgements
+This code of conduct is based on the template established by the [TODO Group](https://todogroup.org/), the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/), and the [Twitter Open Source Code of Conduct](https://github.com/twitter/code-of-conduct/blob/a4d2e558dc730ba440b2ce95cf87f023c9f3fd3d/code-of-conduct.md).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..7a4a3ea2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index d80dfe8a..1b402c5a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,78 @@
# Safer Illinois App
-Source code repository of the "Safer Illinois" app - the official COVID-19 app of the University of Illinois. Powered by the Rokwire Platform.
+The official COVID-19 app of the University of Illinois. Powered by the [Rokwire Platform](https://rokwire.org/).
+
+## Requirements
+
+### [Flutter](https://flutter.dev/docs/get-started/install) v1.17.5
+
+### [Android Studio](https://developer.android.com/studio) 3.6+
+
+### [xCode](https://apps.apple.com/us/app/xcode/id497799835) 11.5
+
+### [CocoaPods](https://guides.cocoapods.org/using/getting-started.html) 1.9.3+
+
+
+## Build
+
+
+### Clone this repo
+
+### Supply the following private configuration files:
+
+#### • /.travis.yml
+[No description available]
+
+
+#### • /secrets.tar.enc
+[No description available]
+
+#### • /assets/configs.json.enc
+1. JSON data with the following format:
+```
+{
+ "production": {
+ "config_url": "https://api.rokwire.illinois.edu/app/configs",
+ "api_key": "XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXX"
+ },
+ "dev": {
+ "config_url": "https://api-dev.rokwire.illinois.edu/app/configs",
+ "api_key": "XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXX"
+ },
+ "test": {
+ "config_url": "https://api-test.rokwire.illinois.edu/app/configs",
+ "api_key": "XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXX"
+ }
+}
+```
+2. Generate random 16-bytes AES128 key.
+3. AES encrypt the JSON string, CBC mode, PKCS7 padding, using the AES.
+4. Create a data blob contains the AES key at the beginning followed by the encrypted data.
+5. Get a base64 encoded string of the data blob and save it as /assets/configs.json.enc.
+
+Alternatively, you can use AESCrypt.encode from /lib/utils/Crypt.dart to generate content of /assets/configs.json.enc.
+
+#### • /ios/Runner/GoogleService-Info-Debug.plist
+#### • /ios/Runner/GoogleService-Info-Release.plist
+
+The Firebase configuration file for iOS generated from Google Firebase console.
+
+#### • /android/keys.properties
+Contains a GoogleMaps and Android Backup API keys.
+```
+googleMapsApiKey=XXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXX
+androidBackupApiKey=XXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXX
+```
+
+#### • /android/app/src/debug/google-services.json
+#### • /android/app/src/release/google-services.json
+#### • /android/app/src/profile/google-services.json
+The Firebase configuration file for Android generated from Google Firebase console.
+
+### Build the project
+
+```
+$ flutter build apk
+$ flutter build ios
+```
+NB: You may need to update singing & capabilities content for Runner project by opening /ios/Runner.xcworkspace from xCode
+
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 00000000..b37dffce
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'com.github.triplet.play' version '2.2.1'
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+def keysProperties = new Properties()
+def keysPropertiesFile = rootProject.file('keys.properties')
+if (keysPropertiesFile.exists()) {
+ keysPropertiesFile.withReader('UTF-8') { reader ->
+ keysProperties.load(reader)
+ }
+}
+
+apply plugin: 'io.fabric'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+repositories{
+ maven { url 'http://maven.mapsindoors.com/' }
+ mavenCentral()
+}
+
+android {
+ compileSdkVersion 29
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ applicationId "edu.illinois.covid"
+ minSdkVersion 23
+ targetSdkVersion 29
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ manifestPlaceholders = [
+ mapsApiKey : "${keysProperties.getProperty('googleMapsApiKey')}",
+ backupApiKey: "${keysProperties.getProperty('androidBackupApiKey')}"
+ ]
+ }
+
+ def isRunningOnTravis = System.getenv("CI") == "true"
+ if (isRunningOnTravis) {
+ // configure keystore
+ println 'travis-ci release build'
+ signingConfigs {
+ release {
+ storeFile file("../../android-releasekey.keystore")
+ storePassword System.getenv("androidkeystore_password") ?: "androidstore_passwd"
+ keyAlias System.getenv("androidkeystore_alias") ?: "androidkeystore_alias"
+ keyPassword System.getenv("androidkeystore_alias_password") ?: "androidkeystore_alias_password"
+ }
+ }
+ buildTypes {
+ release {
+ signingConfig signingConfigs.release
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+
+ ndk {
+ abiFilters 'arm64-v8a', 'armeabi-v7a'
+ }
+ }
+ }
+ } else {
+ buildTypes {
+ release {
+ // Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ minifyEnabled true
+ shrinkResources true
+
+ ndk {
+ abiFilters 'arm64-v8a', 'armeabi-v7a'
+ }
+ }
+ }
+ }
+}
+
+play {
+ track = 'alpha'
+ serviceAccountCredentials = file("../../google-playstore-apikey.json")
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+
+ //Common dependencies
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'com.google.android.gms:play-services-location:17.0.0'
+ implementation 'com.android.volley:volley:1.1.1'
+ //Firebase
+ implementation 'com.google.firebase:firebase-core:17.2.3'
+ implementation 'com.google.firebase:firebase-analytics:17.2.3'
+ implementation 'com.google.firebase:firebase-messaging:20.1.3'
+ //Crashlytics
+ implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
+ //end Common
+
+ //MapsIndoors
+ implementation 'com.mapspeople.mapsindoors:mapsindoorssdk:3.3.2@aar'
+ implementation 'com.android.support:support-v4:28.0.0'
+ implementation 'com.google.code.gson:gson:2.8.5'
+ implementation 'com.google.android.gms:play-services-maps:17.0.0'
+ //end MapsIndoors
+
+ //Google Maps Utils
+ implementation 'com.google.maps.android:android-maps-utils:0.5'
+ //end Google Maps Utils
+
+ //Zxing
+ implementation 'com.google.zxing:core:3.3.0' //Use zxing 3.3.0 because we have minSdk < 24
+ implementation ('com.journeyapps:zxing-android-embedded:4.1.0@aar') { transitive = false }
+
+ //BlinkID
+ implementation('com.microblink:blinkid:5.3.0@aar') {
+ transitive = true
+ }
+
+ // BLESSED - BLE library used for Exposures
+ implementation 'com.github.weliem:blessed-android:1.19'
+}
+
+apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..da542e10
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a3cc3016
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/at/favre/lib/crypto/HKDF.java b/android/app/src/main/java/at/favre/lib/crypto/HKDF.java
new file mode 100644
index 00000000..5ddf1f50
--- /dev/null
+++ b/android/app/src/main/java/at/favre/lib/crypto/HKDF.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2017 Patrick Favre-Bulle
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package at.favre.lib.crypto;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import java.nio.ByteBuffer;
+
+/**
+ * A standards-compliant implementation of RFC 5869
+ * for HMAC-based Key Derivation Function.
+ *
+ * HKDF follows the "extract-then-expand" paradigm, where the KDF
+ * logically consists of two modules. The first stage takes the input
+ * keying material and "extracts" from it a fixed-length pseudorandom
+ * key K. The second stage "expands" the key K into several additional
+ * pseudorandom keys (the output of the KDF).
+ *
+ * HKDF was first described by Hugo Krawczyk.
+ *
+ * This implementation is thread safe without the need for synchronization.
+ *
+ * Simple Example:
+ *
+ * byte[] pseudoRandomKey = HKDF.fromHmacSha256().extract(null, lowEntropyInput);
+ * byte[] outputKeyingMaterial = HKDF.fromHmacSha256().expand(pseudoRandomKey, null, 64);
+ *
+ *
+ * @see RFC 5869
+ * @see Cryptographic Extraction and Key Derivation:
+ * The HKDF Scheme
+ * @see Wikipedia: HKDF
+ */
+@SuppressWarnings("WeakerAccess")
+public final class HKDF {
+ /**
+ * Cache instances
+ */
+ private static HKDF hkdfHmacSha256;
+ private static HKDF hkdfHmacSha512;
+
+ private final HkdfMacFactory macFactory;
+
+ private HKDF(HkdfMacFactory macFactory) {
+ this.macFactory = macFactory;
+ }
+
+ /**
+ * Return a shared instance using HMAC with Sha256.
+ * Even though shared, this instance is thread-safe.
+ *
+ * @return HKDF instance
+ */
+ public static HKDF fromHmacSha256() {
+ if (hkdfHmacSha256 == null) {
+ hkdfHmacSha256 = from(HkdfMacFactory.Default.hmacSha256());
+ }
+ return hkdfHmacSha256;
+ }
+
+ /**
+ * Return a shared instance using HMAC with Sha512.
+ * Even though shared, this instance is thread-safe.
+ *
+ * @return HKDF instance
+ */
+ public static HKDF fromHmacSha512() {
+ if (hkdfHmacSha512 == null) {
+ hkdfHmacSha512 = from(HkdfMacFactory.Default.hmacSha512());
+ }
+ return hkdfHmacSha512;
+ }
+
+ /**
+ * Create a new HKDF instance for given macFactory.
+ *
+ * @param macFactory used for HKDF
+ * @return a new instance of HKDF
+ */
+ public static HKDF from(HkdfMacFactory macFactory) {
+ return new HKDF(macFactory);
+ }
+
+ /**
+ * Step 1 of RFC 5869 (Section 2.2)
+ *
+ * The first stage takes the input keying material and "extracts" from it a fixed-length pseudorandom
+ * key K. The goal of the "extract" stage is to "concentrate" and provide a more uniformly unbiased and higher entropy but smaller output.
+ * This is done by utilising the diffusion properties of cryptographic MACs.
+ *
+ * About Salts (from RFC 5869):
+ *
+ * HKDF is defined to operate with and without random salt. This is
+ * done to accommodate applications where a salt value is not available.
+ * We stress, however, that the use of salt adds significantly to the
+ * strength of HKDF, ensuring independence between different uses of the
+ * hash function, supporting "source-independent" extraction, and
+ * strengthening the analytical results that back the HKDF design.
+ *
+ *
+ * @param salt optional salt value (a non-secret random value) (can be null)
+ * if not provided, it is set to an array of hash length of zeros.
+ * @param inputKeyingMaterial data to be extracted (IKM)
+ *
+ * @return a new byte array pseudo random key (of hash length in bytes) (PRK) which can be used to expand
+ * @see RFC 5869 Section 2.2
+ */
+ public byte[] extract(byte[] salt, byte[] inputKeyingMaterial) {
+ return extract(macFactory.createSecretKey(salt), inputKeyingMaterial);
+ }
+
+ /**
+ * Use this if you require {@link SecretKey} types by your security framework. See also
+ * {@link HkdfMacFactory#createSecretKey(byte[])}.
+ *
+ * See {@link #extract(byte[], byte[])} for description.
+ *
+ * @param salt optional salt value (a non-secret random value) (can be null)
+ * @param inputKeyingMaterial data to be extracted (IKM)
+ * @return a new byte array pseudo random key (of hash length in bytes) (PRK) which can be used to expand
+ */
+ public byte[] extract(SecretKey salt, byte[] inputKeyingMaterial) {
+ return new Extractor(macFactory).execute(salt, inputKeyingMaterial);
+ }
+
+ /**
+ * Step 2 of RFC 5869 (Section 2.3)
+ *
+ * To "expand" the generated output of an already reasonably random input such as an existing shared key into a larger
+ * cryptographically independent output, thereby producing multiple keys deterministically from that initial shared key,
+ * so that the same process may produce those same secret keys safely on multiple devices, as long as the same inputs
+ * are used.
+ *
+ * About Info (from RFC 5869):
+ *
+ * While the 'info' value is optional in the definition of HKDF, it is
+ * often of great importance in applications. Its main objective is to
+ * bind the derived key material to application- and context-specific
+ * information. For example, 'info' may contain a protocol number,
+ * algorithm identifiers, user identities, etc. In particular, it may
+ * prevent the derivation of the same keying material for different
+ * contexts (when the same input key material (IKM) is used in such
+ * different contexts).
+ *
+ *
+ * @param pseudoRandomKey a pseudo random key of at least hmac hash length in bytes (usually, the output from the extract step)
+ * @param info optional context and application specific information; may be null
+ * @param outLengthBytes length of output keying material in bytes
+ * @return new byte array of output keying material (OKM)
+ * @see RFC 5869 Section 2.3
+ */
+ public byte[] expand(byte[] pseudoRandomKey, byte[] info, int outLengthBytes) {
+ return expand(macFactory.createSecretKey(pseudoRandomKey), info, outLengthBytes);
+ }
+
+ /**
+ * Use this if you require {@link SecretKey} types by your security framework. See also
+ * {@link HkdfMacFactory#createSecretKey(byte[])}.
+ *
+ * See {@link #expand(byte[], byte[], int)} for description.
+ *
+ * @param pseudoRandomKey a pseudo random key of at least hmac hash length in bytes (usually, the output from the extract step)
+ * @param info optional context and application specific information; may be null
+ * @param outLengthBytes length of output keying material in bytes
+ * @return new byte array of output keying material (OKM)
+ */
+ public byte[] expand(SecretKey pseudoRandomKey, byte[] info, int outLengthBytes) {
+ return new Expander(macFactory).execute(pseudoRandomKey, info, outLengthBytes);
+ }
+
+ /**
+ * Convenience method for extract & expand in a single method
+ *
+ * @param saltExtract optional salt value (a non-secret random value);
+ * @param inputKeyingMaterial data to be extracted (IKM)
+ * @param infoExpand optional context and application specific information; may be null
+ * @param outLengthByte length of output keying material in bytes
+ * @return new byte array of output keying material (OKM)
+ */
+ public byte[] extractAndExpand(byte[] saltExtract, byte[] inputKeyingMaterial, byte[] infoExpand, int outLengthByte) {
+ return extractAndExpand(macFactory.createSecretKey(saltExtract), inputKeyingMaterial, infoExpand, outLengthByte);
+ }
+
+ /**
+ * Convenience method for extract & expand in a single method
+ *
+ * @param saltExtract optional salt value (a non-secret random value);
+ * @param inputKeyingMaterial data to be extracted (IKM)
+ * @param infoExpand optional context and application specific information; may be null
+ * @param outLengthByte length of output keying material in bytes
+ * @return new byte array of output keying material (OKM)
+ */
+ public byte[] extractAndExpand(SecretKey saltExtract, byte[] inputKeyingMaterial, byte[] infoExpand, int outLengthByte) {
+ return new Expander(macFactory).execute(macFactory.createSecretKey(
+ new Extractor(macFactory).execute(saltExtract, inputKeyingMaterial)),
+ infoExpand, outLengthByte);
+ }
+
+ /**
+ * Get the used mac factory
+ *
+ * @return factory
+ */
+ HkdfMacFactory getMacFactory() {
+ return macFactory;
+ }
+
+ /* ************************************************************************** IMPL */
+
+ static final class Extractor {
+ private final HkdfMacFactory macFactory;
+
+ Extractor(HkdfMacFactory macFactory) {
+ this.macFactory = macFactory;
+ }
+
+ /**
+ * Step 1 of RFC 5869
+ *
+ * @param salt optional salt value (a non-secret random value);
+ * if not provided, it is set to an array of hash length of zeros.
+ * @param inputKeyingMaterial data to be extracted (IKM)
+ *
+ * @return a new byte array pseudorandom key (of hash length in bytes) (PRK) which can be used to expand
+ */
+ byte[] execute(SecretKey salt, byte[] inputKeyingMaterial) {
+ if (salt == null) {
+ salt = macFactory.createSecretKey(new byte[macFactory.getMacLengthBytes()]);
+ }
+
+ if (inputKeyingMaterial == null || inputKeyingMaterial.length <= 0) {
+ throw new IllegalArgumentException("provided inputKeyingMaterial must be at least of size 1 and not null");
+ }
+
+ Mac mac = macFactory.createInstance(salt);
+ return mac.doFinal(inputKeyingMaterial);
+ }
+ }
+
+ static final class Expander {
+ private final HkdfMacFactory macFactory;
+
+ Expander(HkdfMacFactory macFactory) {
+ this.macFactory = macFactory;
+ }
+
+ /**
+ * Step 2 of RFC 5869.
+ *
+ * @param pseudoRandomKey a pseudorandom key of at least hmac hash length in bytes (usually, the output from the extract step)
+ * @param info optional context and application specific information; may be null
+ * @param outLengthBytes length of output keying material in bytes (must be <= 255 * mac hash length)
+ * @return new byte array of output keying material (OKM)
+ */
+ byte[] execute(SecretKey pseudoRandomKey, byte[] info, int outLengthBytes) {
+
+ if (outLengthBytes <= 0) {
+ throw new IllegalArgumentException("out length bytes must be at least 1");
+ }
+
+ if (pseudoRandomKey == null) {
+ throw new IllegalArgumentException("provided pseudoRandomKey must not be null");
+ }
+
+ Mac hmacHasher = macFactory.createInstance(pseudoRandomKey);
+
+ if (info == null) {
+ info = new byte[0];
+ }
+
+ /*
+ The output OKM is calculated as follows:
+ N = ceil(L/HashLen)
+ T = T(1) | T(2) | T(3) | ... | T(N)
+ OKM = first L bytes of T
+ where:
+ T(0) = empty string (zero length)
+ T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
+ T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
+ T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
+ ...
+ */
+
+ byte[] blockN = new byte[0];
+
+ int iterations = (int) Math.ceil(((double) outLengthBytes) / ((double) hmacHasher.getMacLength()));
+
+ if (iterations > 255) {
+ throw new IllegalArgumentException("out length must be maximal 255 * hash-length; requested: " + outLengthBytes + " bytes");
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(outLengthBytes);
+ int remainingBytes = outLengthBytes;
+ int stepSize;
+
+ for (int i = 0; i < iterations; i++) {
+ hmacHasher.update(blockN);
+ hmacHasher.update(info);
+ hmacHasher.update((byte) (i + 1));
+
+ blockN = hmacHasher.doFinal();
+
+ stepSize = Math.min(remainingBytes, blockN.length);
+
+ buffer.put(blockN, 0, stepSize);
+ remainingBytes -= stepSize;
+ }
+
+ return buffer.array();
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/at/favre/lib/crypto/HkdfMacFactory.java b/android/app/src/main/java/at/favre/lib/crypto/HkdfMacFactory.java
new file mode 100644
index 00000000..29833713
--- /dev/null
+++ b/android/app/src/main/java/at/favre/lib/crypto/HkdfMacFactory.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2017 Patrick Favre-Bulle
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package at.favre.lib.crypto;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+
+/**
+ * Factory class for creating {@link Mac} hashers
+ */
+public interface HkdfMacFactory {
+
+ /**
+ * Creates a new instance of Hmac with given key, i.e. it must already be initialized
+ * with {@link Mac#init(Key)}.
+ *
+ * @param key the key used, must not be null
+ * @return a new mac instance
+ */
+ Mac createInstance(SecretKey key);
+
+ /**
+ * Get the length of the mac output in bytes
+ *
+ * @return the length of mac output in bytes
+ */
+ int getMacLengthBytes();
+
+ /**
+ * Creates a secret key from a byte raw key material to be used with {@link #createInstance(SecretKey)}
+ *
+ * @param rawKeyMaterial the raw key
+ * @return wrapped as secret key instance or null if input is null or empty
+ */
+ SecretKey createSecretKey(byte[] rawKeyMaterial);
+
+ /**
+ * Default implementation
+ */
+ @SuppressWarnings("WeakerAccess")
+ final class Default implements HkdfMacFactory {
+ private final String macAlgorithmName;
+ private final Provider provider;
+
+ /**
+ * Creates a factory creating HMAC with SHA-256
+ *
+ * @return factory
+ */
+ public static HkdfMacFactory hmacSha256() {
+ return new Default("HmacSHA256", null);
+ }
+
+ /**
+ * Creates a factory creating HMAC with SHA-512
+ *
+ * @return factory
+ */
+ public static HkdfMacFactory hmacSha512() {
+ return new Default("HmacSHA512", null);
+ }
+
+ /**
+ * Creates a factory creating HMAC with SHA-1
+ *
+ * @return factory
+ * @deprecated sha1 with HMAC should be fine, but not recommended for new protocols; see https://crypto.stackexchange.com/questions/26510/why-is-hmac-sha1-still-considered-secure
+ */
+ @Deprecated
+ public static HkdfMacFactory hmacSha1() {
+ return new Default("HmacSHA1", null);
+ }
+
+ /**
+ * Creates a mac factory
+ *
+ * @param macAlgorithmName as used by {@link Mac#getInstance(String)}
+ */
+ public Default(String macAlgorithmName) {
+ this(macAlgorithmName, null);
+ }
+
+ /**
+ * Creates a mac factory
+ *
+ * @param macAlgorithmName as used by {@link Mac#getInstance(String)}
+ * @param provider the security provider, see {@link Mac#getInstance(String, Provider)}; may be null to use default
+ */
+ public Default(String macAlgorithmName, Provider provider) {
+ this.macAlgorithmName = macAlgorithmName;
+ this.provider = provider;
+ }
+
+ @Override
+ public Mac createInstance(SecretKey key) {
+ try {
+ Mac mac = createMacInstance();
+ mac.init(key);
+ return mac;
+ } catch (Exception e) {
+ throw new IllegalStateException("could not make hmac hasher in hkdf", e);
+ }
+ }
+
+ private Mac createMacInstance() {
+ try {
+ Mac hmacInstance;
+
+ if (provider == null) {
+ hmacInstance = Mac.getInstance(macAlgorithmName);
+ } else {
+ hmacInstance = Mac.getInstance(macAlgorithmName, provider);
+ }
+
+ return hmacInstance;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("defined mac algorithm was not found", e);
+ } catch (Exception e) {
+ throw new IllegalStateException("could not create mac instance in hkdf", e);
+ }
+ }
+
+ @Override
+ public int getMacLengthBytes() {
+ return createMacInstance().getMacLength();
+ }
+
+ @Override
+ public SecretKey createSecretKey(byte[] rawKeyMaterial) {
+ if (rawKeyMaterial == null || rawKeyMaterial.length <= 0) {
+ return null;
+ }
+ return new SecretKeySpec(rawKeyMaterial, macAlgorithmName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/edu/illinois/covid/App.java b/android/app/src/main/java/edu/illinois/covid/App.java
new file mode 100644
index 00000000..8a281dcf
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/App.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Intent;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import io.flutter.app.FlutterApplication;
+import io.flutter.plugin.common.PluginRegistry;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService;
+
+public class App extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback {
+
+ private static final String NOTIFICATIONS_CHANNEL_ID = "Notifications_Channel_ID";
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ init();
+ }
+
+ private void init() {
+ FlutterFirebaseMessagingService.setPluginRegistrant(this);
+ }
+
+ @Override
+ public void registerWith(PluginRegistry pluginRegistry) {
+ GeneratedPluginRegistrant.registerWith(pluginRegistry);
+ }
+
+ public void showNotification(String title, String contentText) {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
+
+ if (title == null) {
+ title = this.getString(R.string.app_name);
+ }
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATIONS_CHANNEL_ID)
+ .setSmallIcon(R.drawable.app_icon)
+ .setContentTitle(title)
+ .setContentText(contentText)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(pendingIntent);
+ Notification notification = builder.build();
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
+ notificationManager.notify(4, notification);
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java b/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java
new file mode 100644
index 00000000..78967f76
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/AppBackupAgent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupManager;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.Context;
+import android.util.Log;
+
+public class AppBackupAgent extends BackupAgentHelper {
+ private static final String PREFS_BACKUP_KEY = "prefs";
+
+ @Override
+ public void onCreate() {
+ SharedPreferencesBackupHelper healthHelper = new SharedPreferencesBackupHelper(this, Constants.HEALTH_SHARED_PREFS_FILE_NAME);
+ addHelper(PREFS_BACKUP_KEY, healthHelper);
+
+ SharedPreferencesBackupHelper exposureTeksHelper = new SharedPreferencesBackupHelper(this, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME);
+ addHelper(PREFS_BACKUP_KEY, exposureTeksHelper);
+ }
+
+ public static void requestBackup(Context context) {
+ Log.i("AppBackupAgent", "requestBackup");
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/Constants.java b/android/app/src/main/java/edu/illinois/covid/Constants.java
new file mode 100644
index 00000000..fd8d1773
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/Constants.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid;
+
+import android.os.ParcelUuid;
+
+import com.google.android.gms.maps.model.LatLng;
+
+import java.util.UUID;
+
+public class Constants {
+
+ //Flutter communication methods
+ static final String APP_INIT_KEY = "init";
+ static final String MAP_DIRECTIONS_KEY = "directions";
+ static final String MAP_PICK_LOCATION_KEY = "pickLocation";
+ static final String MAP_KEY = "map";
+ static final String SHOW_NOTIFICATION_KEY = "showNotification";
+ static final String APP_DISMISS_SAFARI_VC_KEY = "dismissSafariVC";
+ static final String APP_DISMISS_LAUNCH_SCREEN_KEY = "dismissLaunchScreen";
+ static final String APP_ADD_CARD_TO_WALLET_KEY = "addToWallet";
+ static final String APP_MICRO_BLINK_SCAN_KEY = "microBlinkScan";
+ static final String APP_ENABLED_ORIENTATIONS_KEY = "enabledOrientations";
+ static final String APP_NOTIFICATIONS_AUTHORIZATION = "notifications_authorization";
+ static final String APP_LOCATION_SERVICES_PERMISSION = "location_services_permission";
+ static final String APP_BLUETOOTH_AUTHORIZATION = "bluetooth_authorization";
+ static final String FIREBASE_INFO = "firebaseInfo";
+ static final String DEVICE_ID_KEY = "deviceId";
+ static final String HEALTH_RSI_PRIVATE_KEY = "healthRSAPrivateKey";
+ static final String BARCODE_KEY = "barcode";
+
+ //Maps
+ public static final LatLng DEFAULT_INITIAL_CAMERA_POSITION = new LatLng(40.102116, -88.227129); //Illinois University: Center of Campus //(40.096230, -88.235899); // State Farm Center
+ public static final float DEFAULT_CAMERA_ZOOM = 17.0f;
+ static final float FIRST_THRESHOLD_MARKER_ZOOM = 16.0f;
+ static final float SECOND_THRESHOLD_MARKER_ZOOM = 16.89f;
+ static final int MARKER_TITLE_MAX_SYMBOLS_NUMBER = 15;
+ public static final double EXPLORE_LOCATION_THRESHOLD_DISTANCE = 200.0; //meters
+ static final int SELECT_LOCATION_ACTIVITY_RESULT_CODE = 2;
+ public static final String LOCATION_PICKER_DATA_FORMAT = "{\"location\":{\"latitude\":%f,\"longitude\":%f,\"floor\":%d,\"description\":\"%s\",\"location_id\":\"%s\",\"name\":\"%s\"}}";
+ public static final float INDOORS_BUILDING_ZOOM = 17.0f;
+ public static final String ANALYTICS_ROUTE_LOCATION_FORMAT = "{\"latitude\":%f,\"longitude\":%f,\"floor\":%d}";
+ public static final String ANALYTICS_USER_LOCATION_FORMAT = "{\"latitude\":%f,\"longitude\":%f,\"floor\":%d,\"timestamp\":%d}";
+
+ //Health
+ static final String HEALTH_SHARED_PREFS_FILE_NAME = "health_shared_prefs";
+
+ //Exposure
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_START = "start";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_STOP = "stop";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_TEKS = "TEKs";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_TEK_RPIS = "tekRPIs";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_RPI_LOG = "exposureRPILog";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_THICK = "exposureThick";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_RSSI_LOG = "exposureRSSILog";
+ public static final String EXPOSURE_PLUGIN_METHOD_NAME_EXPIRE_TEK = "expireTEK";
+ public static final String EXPOSURE_PLUGIN_SETTINGS_PARAM_NAME = "settings";
+ public static final String EXPOSURE_PLUGIN_RPI_PARAM_NAME = "rpi";
+ public static final String EXPOSURE_PLUGIN_TEK_METHOD_NAME = "tek";
+ public static final String EXPOSURE_PLUGIN_TEK_PARAM_NAME = "tek";
+ public static final String EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME = "timestamp";
+ public static final String EXPOSURE_PLUGIN_EXPOSURE_METHOD_NAME = "exposure";
+ public static final String EXPOSURE_PLUGIN_DURATION_PARAM_NAME = "duration";
+ public static final String EXPOSURE_PLUGIN_RSSI_PARAM_NAME = "rssi";
+ public static final String EXPOSURE_PLUGIN_ADDRESS_PARAM_NAME = "address";
+ public static final String EXPOSURE_PLUGIN_IOS_RECORD_PARAM_NAME = "isiOSRecord";
+ public static final String EXPOSURE_PLUGIN_PERIPHERAL_UUID_PARAM_NAME = "peripheralUuid";
+ public static final String EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME = "expirestamp";
+ public static final String EXPOSURE_BLE_DEVICE_FOUND = "edu.illinois.rokwire.exposure.ble.FOUND_DEVICE";
+ public static final String EXPOSURE_BLE_ACTION_FOUND = "edu.illinois.rokwire.exposure.ble.scan.ACTION_FOUND";
+ public static final int EXPOSURE_NO_RSSI_VALUE = 127;
+ public static final int EXPOSURE_MIN_RSSI_VALUE = -50;
+ public static final int EXPOSURE_MIN_DURATION_MILLIS = 2 * 60 * 1000; // 2 minutes
+ public static final UUID EXPOSURE_UUID_SERVICE = UUID.fromString("0000CD19-0000-1000-8000-00805F9B34FB");
+ public static final ParcelUuid EXPOSURE_PARCEL_SERVICE_UUID = new ParcelUuid(EXPOSURE_UUID_SERVICE);
+ public static final UUID EXPOSURE_UUID_CHARACTERISTIC = UUID.fromString("1f5bb1de-cdf0-4424-9d43-d8cc81a7f207");
+ public static final int EXPOSURE_CONTRACT_NUMBER_LENGTH = 20;
+ public static final String EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME = "exposure_teks_shared_prefs";
+ public static final String EXPOSURE_TEKS_SHARED_PREFS_KEY = "exposure_teks";
+ public static final String EXPOSURE_TEK_VERSION = "tekDatabaseVersion";
+
+ //Gallery
+ public static final String GALLERY_PLUGIN_METHOD_NAME_STORE = "store";
+ public static final String GALLERY_PLUGIN_PARAM_BYTES = "bytes";
+ public static final String GALLERY_PLUGIN_PARAM_NAME = "name";
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/MainActivity.java b/android/app/src/main/java/edu/illinois/covid/MainActivity.java
new file mode 100644
index 00000000..17fe7899
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/MainActivity.java
@@ -0,0 +1,852 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.Application;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.Base64;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.google.firebase.FirebaseApp;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.MultiFormatWriter;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.journeyapps.barcodescanner.BarcodeEncoder;
+import com.mapsindoors.mapssdk.MapsIndoors;
+import com.microblink.MicroblinkSDK;
+import com.microblink.entities.recognizers.Recognizer;
+import com.microblink.entities.recognizers.RecognizerBundle;
+import com.microblink.entities.recognizers.blinkid.generic.BlinkIdCombinedRecognizer;
+import com.microblink.entities.recognizers.blinkid.generic.DriverLicenseDetailedInfo;
+import com.microblink.entities.recognizers.blinkid.mrtd.MrzResult;
+import com.microblink.entities.recognizers.blinkid.passport.PassportRecognizer;
+import com.microblink.intent.IntentDataTransferMode;
+import com.microblink.recognition.InvalidLicenceKeyException;
+import com.microblink.results.date.Date;
+import com.microblink.uisettings.ActivityRunner;
+import com.microblink.uisettings.BlinkIdUISettings;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+
+import edu.illinois.covid.exposure.ExposurePlugin;
+import edu.illinois.covid.gallery.GalleryPlugin;
+import edu.illinois.covid.maps.MapActivity;
+import edu.illinois.covid.maps.MapDirectionsActivity;
+import edu.illinois.covid.maps.MapViewFactory;
+import edu.illinois.covid.maps.MapPickLocationActivity;
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+public class MainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler {
+
+ private static final String TAG = "MainActivity";
+
+ private final int REQUEST_LOCATION_PERMISSION_CODE = 1;
+
+ private static MethodChannel METHOD_CHANNEL;
+ private static final String NATIVE_CHANNEL = "edu.illinois.covid/core";
+ private static MainActivity instance = null;
+
+ private ExposurePlugin exposurePlugin;
+
+ private static MethodChannel.Result pickLocationResult;
+
+ private HashMap keys;
+
+ private int preferredScreenOrientation;
+ private Set supportedScreenOrientations;
+
+ private RequestLocationCallback rlCallback;
+
+ // BlinkId
+ private static final int BLINK_ID_REQUEST_CODE = 3;
+ private BlinkIdCombinedRecognizer blinkIdCombinedRecognizer;
+ private PassportRecognizer blinkIdPassportRecognizer;
+ private RecognizerBundle blinkIdRecognizerBundle;
+ private boolean scanning = false;
+ private boolean microblinkInitialized = false;
+ private MethodChannel.Result scanMethodChannelResult;
+
+ // Gallery Plugin
+ private GalleryPlugin galleryPlugin;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ registerPlugins();
+ instance = this;
+ initScreenOrientation();
+ initMethodChannel();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // when activity is killed by user through the app manager, stop all exposure-related services
+ if (exposurePlugin != null) {
+ exposurePlugin.handleStop();
+ }
+ }
+
+ public static MainActivity getInstance() {
+ return instance;
+ }
+
+ public App getApp() {
+ Application application = getApplication();
+ return (application instanceof App) ? (App) application : null;
+ }
+
+ public static void invokeFlutterMethod(String methodName, Object arguments) {
+ if (METHOD_CHANNEL != null) {
+ getInstance().runOnUiThread(() -> METHOD_CHANNEL.invokeMethod(methodName, arguments));
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == REQUEST_LOCATION_PERMISSION_CODE) {
+ boolean granted;
+ if (grantResults.length > 1 &&
+ grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "granted");
+ granted = true;
+ } else {
+ Log.d(TAG, "not granted");
+ granted = false;
+ }
+ if (rlCallback != null) {
+ rlCallback.onResult(granted);
+ rlCallback = null;
+ }
+ } else if(requestCode == GalleryPlugin.STORAGE_PERMISSION_REQUEST_CODE){
+ galleryPlugin.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void registerPlugins() {
+ GeneratedPluginRegistrant.registerWith(this);
+
+ // MapView
+ Registrar registrar = registrarFor("MapPlugin");
+ registrar.platformViewRegistry().registerViewFactory("mapview", new MapViewFactory(this, registrar));
+
+ // ExposureNotifications
+ Registrar exposureRegistrar = registrarFor("ExposurePlugin");
+ exposurePlugin = ExposurePlugin.registerWith(exposureRegistrar);
+
+ // GalleryPlugin
+ Registrar galleryRegistrar = registrarFor("GalleryPlugin");
+ galleryPlugin = GalleryPlugin.registerWith(exposureRegistrar);
+ }
+
+ private void initScreenOrientation() {
+ preferredScreenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ supportedScreenOrientations = new HashSet<>(Collections.singletonList(preferredScreenOrientation));
+ }
+
+ private void initMethodChannel() {
+ METHOD_CHANNEL = new MethodChannel(getFlutterView(), NATIVE_CHANNEL);
+ METHOD_CHANNEL.setMethodCallHandler(this);
+ }
+
+ private void initWithParams(Object keys) {
+ HashMap keysMap = null;
+ if (keys instanceof HashMap) {
+ keysMap = (HashMap) keys;
+ }
+ if (keysMap == null) {
+ return;
+ }
+ this.keys = keysMap;
+
+ // Google Maps cannot be initialized dynamically. Its api key has to be in AndroidManifest.xml file.
+ // Read it from config for MapsIndoors.
+ String googleMapsApiKey = Utils.Map.getValueFromPath(keysMap, "google.maps.api_key", null);
+
+ // MapsIndoors
+ String mapsIndoorsApiKey = Utils.Map.getValueFromPath(keysMap, "mapsindoors.api_key", null);
+ if (!Utils.Str.isEmpty(mapsIndoorsApiKey)) {
+ MapsIndoors.initialize(
+ getApplicationContext(),
+ mapsIndoorsApiKey
+ );
+ }
+ if (!Utils.Str.isEmpty(googleMapsApiKey)) {
+ MapsIndoors.setGoogleAPIKey(googleMapsApiKey);
+ }
+ }
+
+ private void launchMapsDirections(Object explore, Object options) {
+ Intent intent = new Intent(this, MapDirectionsActivity.class);
+ if (explore instanceof HashMap) {
+ HashMap singleExplore = (HashMap) explore;
+ intent.putExtra("explore", singleExplore);
+ } else if (explore instanceof ArrayList) {
+ ArrayList exploreList = (ArrayList) explore;
+ intent.putExtra("explore", exploreList);
+ }
+ HashMap optionsMap = (options instanceof HashMap) ? (HashMap) options : null;
+ if (optionsMap != null) {
+ intent.putExtra("options", optionsMap);
+ }
+ startActivity(intent);
+ }
+
+ private void launchMap(Object target, Object options, Object markers) {
+ HashMap targetMap = (target instanceof HashMap) ? (HashMap) target : null;
+ HashMap optionsMap = (options instanceof HashMap) ? (HashMap) options : null;
+ ArrayList markersValues = (markers instanceof ArrayList) ? ( ArrayList) markers : null;
+ Intent intent = new Intent(this, MapActivity.class);
+ Bundle serializableExtras = new Bundle();
+ serializableExtras.putSerializable("target", targetMap);
+ serializableExtras.putSerializable("options", optionsMap);
+ serializableExtras.putSerializable("markers", markersValues);
+ intent.putExtras(serializableExtras);
+ startActivity(intent);
+ }
+
+ private void launchMapsLocationPick(Object exploreParam) {
+ HashMap explore = null;
+ if (exploreParam instanceof HashMap) {
+ explore = (HashMap) exploreParam;
+ }
+ Intent locationPickerIntent = new Intent(this, MapPickLocationActivity.class);
+ locationPickerIntent.putExtra("explore", explore);
+ startActivityForResult(locationPickerIntent, Constants.SELECT_LOCATION_ACTIVITY_RESULT_CODE);
+ }
+
+ private void launchNotification(MethodCall methodCall) {
+ String title = methodCall.argument("title");
+ String body = methodCall.argument("body");
+ App app = getApp();
+ if (app != null) {
+ app.showNotification(title, body);
+ }
+ }
+
+
+ private void requestLocationPermission(MethodChannel.Result result) {
+ //check if granted
+ if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "request permission");
+
+ rlCallback = new RequestLocationCallback() {
+ @Override
+ public void onResult(boolean granted) {
+ if (granted) {
+ result.success("allowed");
+
+ if (exposurePlugin != null) {
+ exposurePlugin.onLocationPermissionGranted();
+ }
+ } else {
+ result.success("denied");
+ }
+ }
+ };
+
+ ActivityCompat.requestPermissions(MainActivity.this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION},
+ REQUEST_LOCATION_PERMISSION_CODE);
+ } else {
+ Log.d(TAG, "already granted");
+ result.success("allowed");
+ }
+ }
+
+ private List handleEnabledOrientations(Object orientations) {
+ List resultList = new ArrayList<>();
+ if (preferredScreenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
+ resultList.add(getScreenOrientationToString(preferredScreenOrientation));
+ }
+ if (supportedScreenOrientations != null && !supportedScreenOrientations.isEmpty()) {
+ for (int supportedOrientation : supportedScreenOrientations) {
+ if (supportedOrientation != preferredScreenOrientation) {
+ resultList.add(getScreenOrientationToString(supportedOrientation));
+ }
+ }
+ }
+ List orientationsList;
+ if (orientations instanceof List) {
+ orientationsList = (List) orientations;
+ int preferredOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ Set supportedOrientations = new HashSet<>();
+ for (String orientationString : orientationsList) {
+ int orientation = getScreenOrientationFromString(orientationString);
+ if (orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
+ supportedOrientations.add(orientation);
+ if (preferredOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
+ preferredOrientation = orientation;
+ }
+ }
+ }
+ if ((preferredOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) && (preferredScreenOrientation != preferredOrientation)) {
+ preferredScreenOrientation = preferredOrientation;
+ }
+ if ((supportedOrientations.size() > 0) && !supportedOrientations.equals(supportedScreenOrientations)) {
+ supportedScreenOrientations = supportedOrientations;
+ int currentOrientation = getRequestedOrientation();
+ if (!supportedScreenOrientations.contains(currentOrientation)) {
+ setRequestedOrientation(preferredScreenOrientation);
+ }
+ }
+ }
+ return resultList;
+ }
+
+ private String getScreenOrientationToString(int orientationValue) {
+ switch (orientationValue) {
+ case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:
+ return "portraitUp";
+ case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT:
+ return "portraitDown";
+ case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
+ return "landscapeLeft";
+ case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
+ return "landscapeRight";
+ default:
+ return null;
+ }
+ }
+
+ private String getDeviceId(){
+ String deviceId = "";
+ try
+ {
+ UUID uuid;
+ final String androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
+ uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
+ deviceId = uuid.toString();
+ }
+ catch (Exception e)
+ {
+ Log.d(TAG, "Failed to generate uuid");
+ }
+ return deviceId;
+ }
+
+ private int getScreenOrientationFromString(String orientationString) {
+ if (Utils.Str.isEmpty(orientationString)) {
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ switch (orientationString) {
+ case "portraitUp":
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ case "portraitDown":
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ case "landscapeLeft":
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ case "landscapeRight":
+ return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ default:
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ }
+
+ private String handleBarcode(Object params) {
+ String barcodeImageData = null;
+ String content = Utils.Map.getValueFromPath(params, "content", null);
+ String format = Utils.Map.getValueFromPath(params, "format", null);
+ int width = Utils.Map.getValueFromPath(params, "width", 0);
+ int height = Utils.Map.getValueFromPath(params, "height", 0);
+ BarcodeFormat barcodeFormat = null;
+ if (!Utils.Str.isEmpty(format)) {
+ switch (format) {
+ case "aztec":
+ barcodeFormat = BarcodeFormat.AZTEC;
+ break;
+ case "codabar":
+ barcodeFormat = BarcodeFormat.CODABAR;
+ break;
+ case "code39":
+ barcodeFormat = BarcodeFormat.CODE_39;
+ break;
+ case "code93":
+ barcodeFormat = BarcodeFormat.CODE_93;
+ break;
+ case "code128":
+ barcodeFormat = BarcodeFormat.CODE_128;
+ break;
+ case "dataMatrix":
+ barcodeFormat = BarcodeFormat.DATA_MATRIX;
+ break;
+ case "ean8":
+ barcodeFormat = BarcodeFormat.EAN_8;
+ break;
+ case "ean13":
+ barcodeFormat = BarcodeFormat.EAN_13;
+ break;
+ case "itf":
+ barcodeFormat = BarcodeFormat.ITF;
+ break;
+ case "maxiCode":
+ barcodeFormat = BarcodeFormat.MAXICODE;
+ break;
+ case "pdf417":
+ barcodeFormat = BarcodeFormat.PDF_417;
+ break;
+ case "qrCode":
+ barcodeFormat = BarcodeFormat.QR_CODE;
+ break;
+ case "rss14":
+ barcodeFormat = BarcodeFormat.RSS_14;
+ break;
+ case "rssExpanded":
+ barcodeFormat = BarcodeFormat.RSS_EXPANDED;
+ break;
+ case "upca":
+ barcodeFormat = BarcodeFormat.UPC_A;
+ break;
+ case "upce":
+ barcodeFormat = BarcodeFormat.UPC_E;
+ break;
+ case "upceanExtension":
+ barcodeFormat = BarcodeFormat.UPC_EAN_EXTENSION;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (barcodeFormat != null) {
+ MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
+ Bitmap bitmap = null;
+ try {
+ BitMatrix bitMatrix = multiFormatWriter.encode(content, barcodeFormat, width, height);
+ BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
+ bitmap = barcodeEncoder.createBitmap(bitMatrix);
+ } catch (WriterException e) {
+ Log.e(TAG, "Failed to encode image:");
+ e.printStackTrace();
+ }
+ if (bitmap != null) {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
+ byte[] byteArray = byteArrayOutputStream.toByteArray();
+ if (byteArray != null) {
+ barcodeImageData = Base64.encodeToString(byteArray, Base64.NO_WRAP);
+ }
+ }
+ }
+ return barcodeImageData;
+ }
+
+ //region BlinkId
+
+ private void handleMicroBlinkScan(Object params) {
+ if (!microblinkInitialized) {
+ initMicroblinkSdk();
+ }
+ if (!microblinkInitialized) {
+ Log.i(TAG, "Cannot start scanning! Microblink has not been initialized!");
+ if (scanMethodChannelResult != null) {
+ scanMethodChannelResult.success(null);
+ }
+ return;
+ }
+ if (scanning) {
+ Log.d(TAG, "Blink Id is currently scanning!");
+ if (scanMethodChannelResult != null) {
+ scanMethodChannelResult.success(null);
+ }
+ } else {
+ scanning = true;
+ List recognizersList = Arrays.asList("combined", "passport"); // by default
+ if (params instanceof HashMap) {
+ HashMap paramsMap = (HashMap) params;
+ Object recognizersObject = paramsMap.get("recognizers");
+ if (recognizersObject instanceof List) {
+ recognizersList = (List) recognizersObject;
+ }
+ }
+ List recognizers = new ArrayList<>();
+ for (String recognizerParam : recognizersList) {
+ if ("combined".equals(recognizerParam)) {
+ blinkIdCombinedRecognizer = new BlinkIdCombinedRecognizer();
+ blinkIdCombinedRecognizer.setEncodeFaceImage(true);
+ recognizers.add(blinkIdCombinedRecognizer);
+ } else if ("passport".equals(recognizerParam)) {
+ blinkIdPassportRecognizer = new PassportRecognizer();
+ blinkIdPassportRecognizer.setEncodeFaceImage(true);
+ recognizers.add(blinkIdPassportRecognizer);
+ }
+ }
+ blinkIdRecognizerBundle = new RecognizerBundle(recognizers);
+ BlinkIdUISettings uiSettings = new BlinkIdUISettings(blinkIdRecognizerBundle);
+ ActivityRunner.startActivityForResult(this, BLINK_ID_REQUEST_CODE, uiSettings);
+ }
+ }
+
+ private void initMicroblinkSdk() {
+ String blinkIdLicenseKey = Utils.Map.getValueFromPath(keys, "microblink.blink_id.license_key", null);
+ if (Utils.Str.isEmpty(blinkIdLicenseKey)) {
+ Log.e(TAG, "Microblink BlinkId license key is missing from config keys!");
+ return;
+ }
+ try {
+ MicroblinkSDK.setLicenseKey(blinkIdLicenseKey, this);
+ MicroblinkSDK.setIntentDataTransferMode(IntentDataTransferMode.PERSISTED_OPTIMISED);
+ microblinkInitialized = true;
+ } catch (InvalidLicenceKeyException | NullPointerException e) {
+ Log.e(TAG, "Microblink failed to initialize:");
+ e.printStackTrace();
+ }
+ }
+
+ private void onBlinkIdScanSuccess(Intent data) {
+ Log.d(TAG, "onBlinkIdScanSuccess");
+ if (blinkIdRecognizerBundle != null) {
+ blinkIdRecognizerBundle.loadFromIntent(data);
+ }
+ if ((blinkIdCombinedRecognizer != null) && (blinkIdCombinedRecognizer.getResult().getResultState() == Recognizer.Result.State.Valid)) {
+ onCombinedRecognizerResult(blinkIdCombinedRecognizer.getResult());
+ } else if ((blinkIdPassportRecognizer != null) && (blinkIdPassportRecognizer.getResult().getResultState() == Recognizer.Result.State.Valid)) {
+ onPassportRecognizerResult(blinkIdPassportRecognizer.getResult());
+ }
+ }
+
+ private void onBlinkIdScanCanceled() {
+ Log.d(TAG, "onBlinkIdScanCanceled");
+ unInitBlinkId();
+ if (scanMethodChannelResult != null) {
+ scanMethodChannelResult.success(null);
+ }
+ }
+
+ private void onCombinedRecognizerResult(BlinkIdCombinedRecognizer.Result combinedRecognizerResult) {
+ Log.d(TAG, "onCombinedRecognizerResult");
+ HashMap scanResult = null;
+ if (combinedRecognizerResult != null) {
+ scanResult = new HashMap<>();
+
+ String base64FaceImage = Base64.encodeToString(combinedRecognizerResult.getEncodedFaceImage(), Base64.NO_WRAP);
+
+ scanResult.put("firstName", Utils.Str.nullIfEmpty(combinedRecognizerResult.getFirstName()));
+ scanResult.put("lastName", Utils.Str.nullIfEmpty(combinedRecognizerResult.getLastName()));
+ scanResult.put("fullName", Utils.Str.nullIfEmpty(combinedRecognizerResult.getFullName()));
+ scanResult.put("sex", Utils.Str.nullIfEmpty(combinedRecognizerResult.getSex()));
+ scanResult.put("address", Utils.Str.nullIfEmpty(combinedRecognizerResult.getAddress()));
+
+ scanResult.put("dateOfBirth", Utils.Str.nullIfEmpty(formatBlinkIdDate(combinedRecognizerResult.getDateOfBirth().getDate())));
+ scanResult.put("dateOfExpiry", Utils.Str.nullIfEmpty(formatBlinkIdDate(combinedRecognizerResult.getDateOfExpiry().getDate())));
+ scanResult.put("dateOfIssue", Utils.Str.nullIfEmpty(formatBlinkIdDate(combinedRecognizerResult.getDateOfIssue().getDate())));
+
+ scanResult.put("documentNumber", Utils.Str.nullIfEmpty(combinedRecognizerResult.getDocumentNumber()));
+
+ scanResult.put("placeOfBirth", Utils.Str.nullIfEmpty(combinedRecognizerResult.getPlaceOfBirth()));
+ scanResult.put("nationality", Utils.Str.nullIfEmpty(combinedRecognizerResult.getNationality()));
+ scanResult.put("race", Utils.Str.nullIfEmpty(combinedRecognizerResult.getRace()));
+ scanResult.put("religion", Utils.Str.nullIfEmpty(combinedRecognizerResult.getReligion()));
+ scanResult.put("profession", Utils.Str.nullIfEmpty(combinedRecognizerResult.getProfession()));
+ scanResult.put("maritalStatus", Utils.Str.nullIfEmpty(combinedRecognizerResult.getMaritalStatus()));
+ scanResult.put("residentialStatus", Utils.Str.nullIfEmpty(combinedRecognizerResult.getResidentialStatus()));
+ scanResult.put("employer", Utils.Str.nullIfEmpty(combinedRecognizerResult.getEmployer()));
+ scanResult.put("personalIdNumber", Utils.Str.nullIfEmpty(combinedRecognizerResult.getPersonalIdNumber()));
+ scanResult.put("documentAdditionalNumber", Utils.Str.nullIfEmpty(combinedRecognizerResult.getDocumentAdditionalNumber()));
+ scanResult.put("issuingAuthority", Utils.Str.nullIfEmpty(combinedRecognizerResult.getIssuingAuthority()));
+
+ scanResult.put("mrz", getScanRezultFromMrz(combinedRecognizerResult.getMrzResult()));
+ scanResult.put("driverLicenseDetailedInfo", getScanResultFromDriverLicenseDetailedInfo(combinedRecognizerResult.getDriverLicenseDetailedInfo()));
+
+ scanResult.put("base64FaceImage", Utils.Str.nullIfEmpty(base64FaceImage));
+ }
+ unInitBlinkId();
+ if (scanMethodChannelResult != null) {
+ scanMethodChannelResult.success(scanResult);
+ }
+ }
+
+ private void onPassportRecognizerResult(PassportRecognizer.Result passportRecognizerResult) {
+ Log.d(TAG, "onPassportRecognizerResult");
+ HashMap scanResult = null;
+ if (passportRecognizerResult != null) {
+ scanResult = new HashMap<>();
+
+ String base64FaceImage = Base64.encodeToString(passportRecognizerResult.getEncodedFaceImage(), Base64.NO_WRAP);
+
+ scanResult.put("firstName", null);
+ scanResult.put("lastName", null);
+ scanResult.put("fullName", null);
+ scanResult.put("sex", null);
+ scanResult.put("address", null);
+
+ scanResult.put("dateOfBirth", null);
+ scanResult.put("dateOfExpiry", null);
+ scanResult.put("dateOfIssue", null);
+
+ scanResult.put("documentNumber", null);
+
+ scanResult.put("placeOfBirth", null);
+ scanResult.put("nationality", null);
+ scanResult.put("race", null);
+ scanResult.put("religion", null);
+ scanResult.put("profession", null);
+ scanResult.put("maritalStatus", null);
+ scanResult.put("residentialStatus", null);
+ scanResult.put("employer", null);
+ scanResult.put("personalIdNumber", null);
+ scanResult.put("documentAdditionalNumber", null);
+ scanResult.put("issuingAuthority", null);
+
+ scanResult.put("mrz", getScanRezultFromMrz(passportRecognizerResult.getMrzResult()));
+ scanResult.put("driverLicenseDetailedInfo", null);
+
+ scanResult.put("base64FaceImage", Utils.Str.nullIfEmpty(base64FaceImage));
+ }
+ unInitBlinkId();
+ if (scanMethodChannelResult != null) {
+ scanMethodChannelResult.success(scanResult);
+ }
+ }
+
+ private HashMap getScanResultFromDriverLicenseDetailedInfo(DriverLicenseDetailedInfo driverLicenseDetailedInfo) {
+ HashMap scanResult = null;
+ if (driverLicenseDetailedInfo != null) {
+ scanResult = new HashMap<>();
+
+ scanResult.put("restrictions", Utils.Str.nullIfEmpty(driverLicenseDetailedInfo.getRestrictions()));
+ scanResult.put("endorsements", Utils.Str.nullIfEmpty(driverLicenseDetailedInfo.getEndorsements()));
+ scanResult.put("vehicleClass", Utils.Str.nullIfEmpty(driverLicenseDetailedInfo.getVehicleClass()));
+ }
+ return scanResult;
+ }
+
+ private HashMap getScanRezultFromMrz(MrzResult mrzRezult) {
+ HashMap scanResult = null;
+ if (mrzRezult != null) {
+ scanResult = new HashMap<>();
+
+ scanResult.put("primaryID", Utils.Str.nullIfEmpty(mrzRezult.getPrimaryId()));
+ scanResult.put("secondaryID", Utils.Str.nullIfEmpty(mrzRezult.getSecondaryId()));
+ scanResult.put("issuer", Utils.Str.nullIfEmpty(mrzRezult.getIssuer()));
+ scanResult.put("issuerName", Utils.Str.nullIfEmpty(mrzRezult.getIssuerName()));
+ scanResult.put("dateOfBirth", Utils.Str.nullIfEmpty(formatBlinkIdDate(mrzRezult.getDateOfBirth().getDate())));
+ scanResult.put("dateOfExpiry", Utils.Str.nullIfEmpty(formatBlinkIdDate(mrzRezult.getDateOfExpiry().getDate())));
+ scanResult.put("documentNumber", Utils.Str.nullIfEmpty(mrzRezult.getDocumentNumber()));
+ scanResult.put("nationality", Utils.Str.nullIfEmpty(mrzRezult.getNationality()));
+ scanResult.put("nationalityName", Utils.Str.nullIfEmpty(mrzRezult.getNationalityName()));
+ scanResult.put("gender", Utils.Str.nullIfEmpty(mrzRezult.getGender()));
+ scanResult.put("documentCode", Utils.Str.nullIfEmpty(mrzRezult.getDocumentCode()));
+ scanResult.put("alienNumber", Utils.Str.nullIfEmpty(mrzRezult.getAlienNumber()));
+ scanResult.put("applicationReceiptNumber", Utils.Str.nullIfEmpty(mrzRezult.getApplicationReceiptNumber()));
+ scanResult.put("immigrantCaseNumber", Utils.Str.nullIfEmpty(mrzRezult.getImmigrantCaseNumber()));
+
+ scanResult.put("opt1", Utils.Str.nullIfEmpty(mrzRezult.getOpt1()));
+ scanResult.put("opt2", Utils.Str.nullIfEmpty(mrzRezult.getOpt2()));
+ scanResult.put("mrzText", Utils.Str.nullIfEmpty(mrzRezult.getMrzText()));
+
+ scanResult.put("sanitizedOpt1", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedOpt1()));
+ scanResult.put("sanitizedOpt2", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedOpt2()));
+ scanResult.put("sanitizedNationality", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedNationality()));
+ scanResult.put("sanitizedIssuer", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedIssuer()));
+ scanResult.put("sanitizedDocumentCode", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedDocumentCode()));
+ scanResult.put("sanitizedDocumentNumber", Utils.Str.nullIfEmpty(mrzRezult.getSanitizedDocumentNumber()));
+ }
+ return scanResult;
+ }
+
+ private void unInitBlinkId() {
+ blinkIdRecognizerBundle = null;
+ blinkIdCombinedRecognizer = null;
+ blinkIdPassportRecognizer = null;
+ scanning = false;
+ }
+
+ private String formatBlinkIdDate(Date date) {
+ if (date == null) {
+ return null;
+ }
+ return String.format(Locale.getDefault(), "%02d/%02d/%4d", date.getMonth(), date.getDay(), date.getYear());
+ }
+
+ //endregion
+
+ private Object handleHealthRsiPrivateKey(Object params) {
+ String userId = null;
+ String value = null;
+ boolean remove = false;
+ if (params instanceof HashMap) {
+ HashMap paramsMap = (HashMap) params;
+ Object userIdObj = paramsMap.get("userId");
+ if (userIdObj instanceof String) {
+ userId = (String) userIdObj;
+ }
+ Object valueObj = paramsMap.get("value");
+ if (valueObj instanceof String) {
+ value = (String) valueObj;
+ }
+ Object removeObj = paramsMap.get("remove");
+ if (removeObj instanceof Boolean) {
+ remove = (Boolean) removeObj;
+ }
+ }
+ if (Utils.Str.isEmpty(userId)) {
+ return null;
+ }
+ if (Utils.Str.isEmpty(value)) {
+ if (remove) {
+ Utils.BackupStorage.remove(this, Constants.HEALTH_SHARED_PREFS_FILE_NAME, userId);
+ return true;
+ } else {
+ return Utils.BackupStorage.getString(this, Constants.HEALTH_SHARED_PREFS_FILE_NAME, userId);
+ }
+ } else {
+ Utils.BackupStorage.saveString(this, Constants.HEALTH_SHARED_PREFS_FILE_NAME, userId, value);
+ return true;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == Constants.SELECT_LOCATION_ACTIVITY_RESULT_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ pickLocationResult.success(data != null ? data.getStringExtra("location") : null);
+ } else {
+ pickLocationResult.success(null);
+ }
+ } else if (requestCode == BLINK_ID_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ onBlinkIdScanSuccess(data);
+ } else {
+ onBlinkIdScanCanceled();
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ /**
+ * Overrides {@link io.flutter.plugin.common.MethodChannel.MethodCallHandler} onMethodCall()
+ */
+ @Override
+ public void onMethodCall(MethodCall methodCall, @NonNull MethodChannel.Result result) {
+ String method = methodCall.method;
+ try {
+ switch (method) {
+ case Constants.APP_INIT_KEY:
+ Object keysObject = methodCall.argument("keys");
+ initWithParams(keysObject);
+ result.success(true);
+ break;
+ case Constants.MAP_DIRECTIONS_KEY:
+ Object explore = methodCall.argument("explore");
+ Object optionsObj = methodCall.argument("options");
+ launchMapsDirections(explore, optionsObj);
+ result.success(true);
+ break;
+ case Constants.MAP_PICK_LOCATION_KEY:
+ pickLocationResult = result;
+ launchMapsLocationPick(methodCall.argument("explore"));
+ // Result is called on latter step
+ break;
+ case Constants.MAP_KEY:
+ Object target = methodCall.argument("target");
+ Object options = methodCall.argument("options");
+ Object markers = methodCall.argument("markers");
+ launchMap(target, options,markers);
+ result.success(true);
+ break;
+ case Constants.SHOW_NOTIFICATION_KEY:
+ launchNotification(methodCall);
+ result.success(true);
+ break;
+ case Constants.APP_DISMISS_SAFARI_VC_KEY:
+ case Constants.APP_DISMISS_LAUNCH_SCREEN_KEY:
+ case Constants.APP_ADD_CARD_TO_WALLET_KEY:
+ result.success(false);
+ break;
+ case Constants.APP_MICRO_BLINK_SCAN_KEY:
+ scanMethodChannelResult = result;
+ handleMicroBlinkScan(methodCall.arguments);
+ // Result is called on latter step
+ break;
+ case Constants.APP_ENABLED_ORIENTATIONS_KEY:
+ Object orientations = methodCall.argument("orientations");
+ List orientationsList = handleEnabledOrientations(orientations);
+ result.success(orientationsList);
+ break;
+ case Constants.APP_NOTIFICATIONS_AUTHORIZATION:
+ result.success(true); // notifications are allowed in Android by default
+ break;
+ case Constants.APP_LOCATION_SERVICES_PERMISSION:
+ requestLocationPermission(result);
+ break;
+ case Constants.APP_BLUETOOTH_AUTHORIZATION:
+ result.success("allowed"); // bluetooth is always enabled in Android by default
+ break;
+ case Constants.FIREBASE_INFO:
+ String projectId = FirebaseApp.getInstance().getOptions().getProjectId();
+ result.success(projectId);
+ break;
+ case Constants.DEVICE_ID_KEY:
+ String deviceId = getDeviceId();
+ result.success(deviceId);
+ break;
+ case Constants.HEALTH_RSI_PRIVATE_KEY:
+ Object healthRsiPrivateKeyResult = handleHealthRsiPrivateKey(methodCall.arguments);
+ result.success(healthRsiPrivateKeyResult);
+ break;
+ case Constants.BARCODE_KEY:
+ String barcodeImageData = handleBarcode(methodCall.arguments);
+ result.success(barcodeImageData);
+ break;
+ default:
+ result.notImplemented();
+ break;
+
+ }
+ } catch (IllegalStateException exception) {
+ String errorMsg = String.format("Ignoring exception '%s'. See https://github.com/flutter/flutter/issues/29092 for details.", exception.toString());
+ Log.e(TAG, errorMsg);
+ exception.printStackTrace();
+ }
+ }
+
+ // RequestLocationCallback
+
+ public static class RequestLocationCallback {
+ public void onResult(boolean granted) {}
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/Utils.java b/android/app/src/main/java/edu/illinois/covid/Utils.java
new file mode 100644
index 00000000..bb6c3355
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/Utils.java
@@ -0,0 +1,741 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid;
+
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.maps.android.ui.IconGenerator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.core.content.ContextCompat;
+import edu.illinois.covid.maps.MapMarkerViewType;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class Utils {
+
+ public static void showDialog(Context context, String title, String message,
+ DialogInterface.OnClickListener positiveListener, String positiveText,
+ DialogInterface.OnClickListener negativeListener, String negativeText,
+ boolean cancelable) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(title);
+ builder.setMessage(message);
+ builder.setCancelable(cancelable);
+
+ if (positiveListener != null)
+ builder.setPositiveButton(positiveText, positiveListener);
+
+ if (negativeListener != null)
+ builder.setNegativeButton(negativeText, negativeListener);
+
+ AlertDialog alertDialog = builder.create();
+ alertDialog.show();
+ }
+
+ public static void enabledBluetooth() {
+ BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (bluetoothAdapter != null) {
+ bluetoothAdapter.enable();
+ }
+ }
+
+ public static class DateTime {
+
+ static Date getDateTime(String dateTimeString) {
+ if (dateTimeString == null || dateTimeString.isEmpty()) {
+ return null;
+ }
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
+ Date dateTime = null;
+ try {
+ dateTime = dateFormat.parse(dateTimeString);
+ } catch (ParseException e) {
+ Log.e("Error", "Failed to parse '" + dateTimeString + "' to date time");
+ e.printStackTrace();
+ }
+ return dateTime;
+ }
+
+ static String formatEventTime(Context context, Date dateTime) {
+ if (dateTime == null) {
+ return null;
+ }
+ Calendar calendarDate = Calendar.getInstance();
+ int minutes = calendarDate.get(Calendar.MINUTE);
+ Calendar today = Calendar.getInstance();
+ calendarDate.setTime(dateTime);
+ boolean zeroMins = (minutes == 0);
+ boolean currentYear = calendarDate.get(Calendar.YEAR) == today.get(Calendar.YEAR);
+ final String defaultStringFormat = String.format("%sMMM dd h%s a", (currentYear ? "" : "yy, "), (zeroMins ? "" : ":mm"));
+ String defaultValue = new SimpleDateFormat(defaultStringFormat, Locale.getDefault()).format(dateTime);
+ SimpleDateFormat dateFormat;
+ String datePrefix;
+ String timeSuffix;
+ String format = zeroMins ? "h a" : "h:mm a";
+ dateFormat = new SimpleDateFormat(format, Locale.getDefault());
+ timeSuffix = dateFormat.format(dateTime);
+ boolean isToday = DateUtils.isToday(dateTime.getTime());
+ if (isToday) {
+ datePrefix = context.getString(R.string.today) + " " + context.getString(R.string.at);
+ } else if (calendarDate.after(today)) {
+ int dateDayOfYear = calendarDate.get(Calendar.DAY_OF_YEAR);
+ int todayDateOfYear = today.get(Calendar.DAY_OF_YEAR);
+ int dateDiff = (dateDayOfYear - todayDateOfYear);
+ boolean sameYear = (today.get(Calendar.YEAR) == calendarDate.get(Calendar.YEAR));
+ if ((dateDiff == 1) && sameYear) {
+ datePrefix = context.getString(R.string.tomorrow) + " " + context.getString(R.string.at);
+ } else if ((dateDiff < 7) && sameYear) {
+ dateFormat = new SimpleDateFormat("EEEE", Locale.getDefault());
+ datePrefix = dateFormat.format(dateTime) + " " + context.getString(R.string.at);
+ } else {
+ return defaultValue;
+ }
+ } else {
+ return defaultValue;
+ }
+ return String.format("%s %s", datePrefix, timeSuffix);
+ }
+
+ public static long getCurrentTimeMillisSince1970() {
+ return System.currentTimeMillis();
+ }
+ }
+
+ public static class Explore {
+
+ public static HashMap optLocation(HashMap explore) {
+ if (explore == null) {
+ return null;
+ }
+ Object locationObj = explore.get("location");
+ if (locationObj instanceof HashMap) {
+ return (HashMap) locationObj;
+ }
+ return null;
+ }
+
+ public static Integer optLocationFloor(HashMap explore) {
+ if (explore == null) {
+ return null;
+ }
+ HashMap location = optLocation(explore);
+ return optFloor(location);
+ }
+
+ public static Integer optFloor(HashMap location) {
+ if (location == null) {
+ return null;
+ }
+ Object floorObj = location.get("floor");
+ if (floorObj instanceof Integer) {
+ return (Integer) floorObj;
+ }
+ return null;
+ }
+
+ public static LatLng optLocationLatLng(HashMap explore) {
+ if (explore == null) {
+ return null;
+ }
+ ExploreType exploreType = getExploreType(explore);
+ if (exploreType == ExploreType.PARKING) {
+ // Json Example:
+ // {"lot_id":"647b7211-9cdf-412b-a682-1fdb68897f86","lot_name":"SFC - E-14 Lot - Illinois","lot_address1":"1800 S. First Street, Champaign, IL 61820","total_spots":"1710","entrance":{"latitude":40.096691,"longitude":-88.238179},"polygon":[{"latitude":40.097938,"longitude":-88.241409},{"latitude":40.09793,"longitude":-88.238657},{"latitude":40.094742,"longitude":-88.238651},{"latitude":40.094733,"longitude":-88.240223},{"latitude":40.095148,"longitude":-88.240245},{"latitude":40.095181,"longitude":-88.24113},{"latitude":40.095636,"longitude":-88.241135},{"latitude":40.095636,"longitude":-88.241393}],"spots_sold":0,"spots_pre_sold":0}
+ Object lotEntranceObj = explore.get("entrance");
+ if (!(lotEntranceObj instanceof HashMap)) {
+ return null;
+ }
+ HashMap lotEntranceMap = (HashMap)lotEntranceObj;
+ return optLatLng(lotEntranceMap);
+ } else {
+ HashMap location = optLocation(explore);
+ return optLatLng(location);
+ }
+ }
+
+ public static LatLng optLatLng(HashMap location) {
+ if (location == null) {
+ return null;
+ }
+ Object latObj = location.get("latitude");
+ Object lngObj = location.get("longitude");
+ if (!(latObj instanceof Double) || !(lngObj instanceof Double)) {
+ return null;
+ }
+ return new LatLng((Double) latObj, (Double) lngObj);
+ }
+
+ public static Integer optMarkerLocationFloor(Marker marker) {
+ JSONObject tag = optMarkerTagJson(marker);
+ Object markerRawData = (tag != null) ? tag.opt("raw_data") : null;
+ HashMap exploreMap = null;
+ if (markerRawData instanceof HashMap) {
+ exploreMap = (HashMap) markerRawData;
+ } else if (markerRawData instanceof ArrayList) {
+ ArrayList explores = (ArrayList) markerRawData;
+ if (explores.size() > 0) {
+ Object exploreObj = explores.get(0);
+ if (exploreObj instanceof HashMap) {
+ exploreMap = (HashMap) exploreObj;
+ }
+ }
+ }
+ return optLocationFloor(exploreMap);
+ }
+
+ public static boolean optSingleExploreMarker(Marker marker) {
+ JSONObject markerTagJson = optMarkerTagJson(marker);
+ if (markerTagJson == null) {
+ return false;
+ }
+ return markerTagJson.optBoolean("single_explore", false);
+ }
+
+ public static MarkerOptions constructMarkerOptions(Context context, Object markerRawObject, View markerLayoutView, View markerGroupLayoutView, IconGenerator iconGenerator) {
+ if (markerRawObject == null || markerLayoutView == null || markerGroupLayoutView == null || iconGenerator == null) {
+ return null;
+ }
+ MapMarkerViewType mapMarkerViewType;
+ HashMap singleExploreMap = null;
+ ArrayList groupExploresJson = null;
+ if (markerRawObject instanceof HashMap) {
+ mapMarkerViewType = MapMarkerViewType.SINGLE;
+ singleExploreMap = (HashMap) markerRawObject;
+ } else if (markerRawObject instanceof ArrayList) {
+ mapMarkerViewType = MapMarkerViewType.GROUP;
+ groupExploresJson = (ArrayList) markerRawObject;
+ Object singleObject = groupExploresJson.get(0);
+ if (singleObject instanceof HashMap) {
+ singleExploreMap = (HashMap) singleObject;
+ }
+ } else {
+ mapMarkerViewType = MapMarkerViewType.UNKNOWN;
+ }
+ if (mapMarkerViewType == MapMarkerViewType.UNKNOWN) {
+ return null;
+ }
+ LatLng markerLocation = optLocationLatLng(singleExploreMap);
+ if (markerLocation == null) {
+ return null;
+ }
+ String markerTitle = getMarkerTitle(mapMarkerViewType, singleExploreMap, groupExploresJson);
+ MarkerOptions markerOptions = new MarkerOptions();
+ markerOptions.position(markerLocation);
+ markerOptions.zIndex(1);
+ markerOptions.title(markerTitle);
+ ExploreType exploreType = getExploreType(markerRawObject);
+ Bitmap markerIcon;
+ if (mapMarkerViewType == MapMarkerViewType.SINGLE) {
+ String markerSnippet = getMarkerSnippet(context, singleExploreMap);
+ if (markerSnippet != null && !markerSnippet.isEmpty()) {
+ markerOptions.snippet(markerSnippet);
+ }
+ int iconResource = getSingleExploreIconResource(exploreType);
+ TextView markerTitleView = markerLayoutView.findViewById(R.id.markerTitleView);
+ markerTitleView.setText(markerTitle);
+ TextView markerSnippetView = markerLayoutView.findViewById(R.id.markerSnippetView);
+ markerSnippetView.setText(markerSnippet);
+ boolean snippetViewVisible = ((markerSnippet != null) && !markerSnippet.isEmpty());
+ markerSnippetView.setVisibility(snippetViewVisible ? VISIBLE : GONE);
+ ImageView iconImageView = markerLayoutView.findViewById(R.id.markerIconView);
+ iconImageView.setImageResource(iconResource);
+ iconGenerator.setContentView(markerLayoutView);
+ markerIcon = iconGenerator.makeIcon();
+ } else {
+ TextView markerTitleView = markerGroupLayoutView.findViewById(R.id.markerGroupTitleView);
+ markerTitleView.setText(markerTitle);
+ String descrLabel = getGroupExploresDescrLabel(context, markerTitle, exploreType);
+ TextView markerDescrView = markerGroupLayoutView.findViewById(R.id.markerGroupDescrView);
+ markerDescrView.setText(descrLabel);
+ ImageView markerCircleView = markerGroupLayoutView.findViewById(R.id.markerGroupCircleView);
+ Drawable circleViewBackground = markerCircleView.getBackground();
+ if (circleViewBackground instanceof GradientDrawable) {
+ int exploreGroupColor = getExploreColorResource(exploreType);
+ GradientDrawable gradientDrawable = (GradientDrawable) circleViewBackground;
+ gradientDrawable.setColor(ContextCompat.getColor(context, exploreGroupColor));
+ }
+ iconGenerator.setContentView(markerGroupLayoutView);
+ markerIcon = iconGenerator.makeIcon();
+ }
+ if (markerIcon != null) {
+ markerOptions.icon(BitmapDescriptorFactory.fromBitmap(markerIcon));
+ }
+ return markerOptions;
+ }
+
+ public static void updateCustomMarkerAppearance(Context context, Marker marker,
+ boolean singleExploreMarker, float currentCameraZoom, float previousCameraZoom,
+ View markerLayoutView, View markerGroupLayoutView, IconGenerator iconGenerator) {
+ if (marker == null) {
+ return;
+ }
+ float minCurrentZoom = Math.min(currentCameraZoom, previousCameraZoom);
+ float maxCurrentZoom = Math.max(currentCameraZoom, previousCameraZoom);
+ boolean changeMarkerIcon = false;
+ //Check if title threshold passed
+ if ((minCurrentZoom <= Constants.FIRST_THRESHOLD_MARKER_ZOOM) &&
+ (Constants.FIRST_THRESHOLD_MARKER_ZOOM < maxCurrentZoom)) {
+ boolean passedFirstThreshold = (currentCameraZoom >= Constants.FIRST_THRESHOLD_MARKER_ZOOM);
+ if (singleExploreMarker) {
+ int textVisibility = passedFirstThreshold ? View.VISIBLE : View.GONE;
+ View textFrameView = markerLayoutView.findViewById(R.id.markerTextFrame);
+ textFrameView.setVisibility(textVisibility);
+ if (passedFirstThreshold) {
+ String shortTitle = Utils.Explore.optExploreMarkerShortTitle(marker);
+ TextView markerTitleView = markerLayoutView.findViewById(R.id.markerTitleView);
+ String markerTitle = marker.getTitle();
+ markerTitleView.setText(shortTitle != null ? shortTitle : markerTitle);
+ }
+ } else {
+ ImageView markerGroupCircleView = markerGroupLayoutView.findViewById(R.id.markerGroupCircleView);
+ int imageViewSize = context.getResources().getDimensionPixelSize(passedFirstThreshold ? R.dimen.group_marker_image_size_first : R.dimen.group_marker_image_size_zero);
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(imageViewSize, imageViewSize);
+ markerGroupCircleView.setLayoutParams(layoutParams);
+ TextView markerGroupTitleView = markerGroupLayoutView.findViewById(R.id.markerGroupTitleView);
+ String markerTitle = marker.getTitle();
+ markerGroupTitleView.setText(markerTitle);
+ }
+ changeMarkerIcon = true;
+ }
+ //Check if snippet threshold passed
+ if ((minCurrentZoom <= Constants.SECOND_THRESHOLD_MARKER_ZOOM) &&
+ (Constants.SECOND_THRESHOLD_MARKER_ZOOM < maxCurrentZoom)) {
+ boolean passedSecondThreshold = (currentCameraZoom > Constants.SECOND_THRESHOLD_MARKER_ZOOM);
+ if (singleExploreMarker) {
+ TextView markerTitleView = markerLayoutView.findViewById(R.id.markerTitleView);
+ String markerTitle = marker.getTitle();
+ String shortTitle = Utils.Explore.optExploreMarkerShortTitle(marker);
+ markerTitleView.setText(passedSecondThreshold ? markerTitle : shortTitle);
+ } else {
+ ImageView markerGroupCircleView = markerGroupLayoutView.findViewById(R.id.markerGroupCircleView);
+ int imageViewSize = context.getResources().getDimensionPixelSize(passedSecondThreshold ? R.dimen.group_marker_image_size_second : R.dimen.group_marker_image_size_first);
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(imageViewSize, imageViewSize);
+ markerGroupCircleView.setLayoutParams(layoutParams);
+ TextView markerGroupTitleView = markerGroupLayoutView.findViewById(R.id.markerGroupTitleView);
+ String markerTitle = marker.getTitle();
+ markerGroupTitleView.setText(markerTitle);
+ TextView groupDescrView = markerGroupLayoutView.findViewById(R.id.markerGroupDescrView);
+ String markerDescription = Utils.Explore.optExploreMarkerDescrLabel(marker);
+ groupDescrView.setText(markerDescription);
+ int descrVisibility = passedSecondThreshold ? View.VISIBLE : View.GONE;
+ groupDescrView.setVisibility(descrVisibility);
+ }
+ changeMarkerIcon = true;
+ }
+ //Change Marker icon only if needed
+ if (changeMarkerIcon) {
+ iconGenerator.setContentView(singleExploreMarker ? markerLayoutView : markerGroupLayoutView);
+ Bitmap icon = iconGenerator.makeIcon();
+ marker.setIcon(BitmapDescriptorFactory.fromBitmap(icon));
+ }
+ }
+
+ public static JSONObject constructMarkerTagJson(Context context, String markerTitle, Object markerRawData) {
+ boolean singleExploreMarker = (markerRawData instanceof HashMap);
+ String shortTitle = (markerTitle != null && markerTitle.length() > Constants.MARKER_TITLE_MAX_SYMBOLS_NUMBER) ?
+ String.format("%s...", markerTitle.substring(0, 15)) : markerTitle;
+ JSONObject markerTagJson = new JSONObject();
+ try {
+ markerTagJson.put("title", markerTitle);
+ markerTagJson.put("short_title", shortTitle);
+ markerTagJson.put("raw_data", markerRawData);
+ markerTagJson.put("single_explore", singleExploreMarker);
+ if (!singleExploreMarker) {
+ ExploreType exploreType = getExploreType(markerRawData);
+ markerTagJson.put("description", getGroupExploresDescrLabel(context, markerTitle, exploreType));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return markerTagJson;
+ }
+
+ public static Object optExploreMarkerRawData(Marker marker) {
+ JSONObject markerTagJson = optMarkerTagJson(marker);
+ return (markerTagJson != null) ? markerTagJson.opt("raw_data") : null;
+ }
+
+ private static String optExploreMarkerShortTitle(Marker marker) {
+ JSONObject markerTagJson = optMarkerTagJson(marker);
+ return (markerTagJson != null) ? markerTagJson.optString("short_title", null) : null;
+ }
+
+ private static String optExploreMarkerDescrLabel(Marker marker) {
+ JSONObject markerTagJson = optMarkerTagJson(marker);
+ return (markerTagJson != null) ? markerTagJson.optString("description", null) : null;
+ }
+
+ public static HashMap createLocationMap(LatLng latLng) {
+ if (latLng == null) {
+ return null;
+ }
+ HashMap location = new HashMap<>();
+ location.put("latitude", latLng.latitude);
+ location.put("longitude", latLng.longitude);
+ return location;
+ }
+
+ public static ExploreType getExploreType(Object explore) {
+ HashMap singleExplore = null;
+ if (explore instanceof HashMap) {
+ singleExplore = (HashMap) explore;
+ } else if (explore instanceof ArrayList) {
+ ArrayList explores = (ArrayList) explore;
+ if (explores.size() > 0) {
+ Object firstExploreObj = explores.get(0);
+ if (firstExploreObj instanceof HashMap) {
+ singleExplore = (HashMap) firstExploreObj;
+ }
+ }
+ }
+ if (singleExplore == null) {
+ return ExploreType.UNKNOWN;
+ }
+ if (singleExplore.get("eventId") != null) {
+ return ExploreType.EVENT;
+ } else if (singleExplore.get("DiningOptionID") != null) {
+ return ExploreType.DINING;
+ } else if (singleExplore.get("campus_name") != null) {
+ return ExploreType.LAUNDRY;
+ } else if (singleExplore.get("lot_id") != null) {
+ return ExploreType.PARKING;
+ } else {
+ return ExploreType.UNKNOWN;
+ }
+ }
+
+ public static List getExplorePolygon(Object explore) {
+ if (getExploreType(explore) != ExploreType.PARKING) {
+ return null;
+ }
+ HashMap parkingLot = (HashMap) explore;
+ Object polygonObject = parkingLot.get("polygon");
+ if (!(polygonObject instanceof List)) {
+ return null;
+ }
+ List polygonMaps = (List) polygonObject;
+ if (polygonMaps.isEmpty()) {
+ return null;
+ }
+ List polygonPoints = new ArrayList<>();
+ for (HashMap point : polygonMaps) {
+ double latitude = Utils.Map.getValueFromPath(point, "latitude", 0.0d);
+ double longitude = Utils.Map.getValueFromPath(point, "longitude", 0.0d);
+ LatLng latLng = new LatLng(latitude, longitude);
+ polygonPoints.add(latLng);
+ }
+ return polygonPoints;
+ }
+
+ public static int getExploreColorResource(ExploreType exploreType) {
+ int colorResource;
+ switch (exploreType) {
+ case EVENT:
+ colorResource = R.color.illinois_orange;
+ break;
+ case DINING:
+ colorResource = R.color.mongo;
+ break;
+ default:
+ colorResource = R.color.teal;
+ break;
+ }
+ return colorResource;
+ }
+
+ private static String getMarkerTitle(MapMarkerViewType mapMarkerViewType, HashMap singleExploreMap, ArrayList groupExploresList) {
+ String markerTitle;
+ if (mapMarkerViewType == MapMarkerViewType.SINGLE) {
+ ExploreType exporeType = getExploreType(singleExploreMap);
+ if (exporeType == ExploreType.PARKING) {
+ // Json Example:
+ // {"lot_id":"647b7211-9cdf-412b-a682-1fdb68897f86","lot_name":"SFC - E-14 Lot - Illinois","lot_address1":"1800 S. First Street, Champaign, IL 61820","total_spots":"1710","entrance":{"latitude":40.096691,"longitude":-88.238179},"polygon":[{"latitude":40.097938,"longitude":-88.241409},{"latitude":40.09793,"longitude":-88.238657},{"latitude":40.094742,"longitude":-88.238651},{"latitude":40.094733,"longitude":-88.240223},{"latitude":40.095148,"longitude":-88.240245},{"latitude":40.095181,"longitude":-88.24113},{"latitude":40.095636,"longitude":-88.241135},{"latitude":40.095636,"longitude":-88.241393}],"spots_sold":0,"spots_pre_sold":0}
+ markerTitle = (String) singleExploreMap.get("lot_name");
+ } else {
+ markerTitle = (String) singleExploreMap.get("title");
+ }
+ } else {
+ markerTitle = String.valueOf(groupExploresList.size());
+ }
+ return markerTitle;
+ }
+
+ private static String getMarkerSnippet(Context context, HashMap exploreMap) {
+ if (exploreMap == null) {
+ return null;
+ }
+ String markerSnippet;
+ String startDateToString = (String) exploreMap.get("startDateLocal");
+ if (startDateToString != null && !startDateToString.isEmpty()) {
+ Date eventStartDate = DateTime.getDateTime(startDateToString);
+ markerSnippet = DateTime.formatEventTime(context, eventStartDate);
+ } else {
+ markerSnippet = (String) exploreMap.get("status");
+ }
+ return markerSnippet;
+ }
+
+ private static int getSingleExploreIconResource(ExploreType exploreType) {
+ int iconResource;
+ switch (exploreType) {
+ case EVENT:
+ iconResource = R.drawable.marker_event;
+ break;
+ case DINING:
+ iconResource = R.drawable.marker_dining;
+ break;
+ default:
+ iconResource = R.drawable.marker_default_teal;
+ break;
+ }
+ return iconResource;
+ }
+
+ private static String getGroupExploresDescrLabel(Context context, String exploresCountString, ExploreType exploreType) {
+ String groupDescrLabel = "";
+ if (exploresCountString != null) {
+ String typeSuffix;
+ switch (exploreType) {
+ case EVENT:
+ typeSuffix = context.getString(R.string.events);
+ break;
+ case DINING:
+ typeSuffix = context.getString(R.string.dinings);
+ break;
+ case LAUNDRY:
+ typeSuffix = context.getString(R.string.laundries);
+ break;
+ case PARKING:
+ typeSuffix = context.getString(R.string.parkings);
+ break;
+ default:
+ typeSuffix = context.getString(R.string.explores);
+ break;
+ }
+ groupDescrLabel = exploresCountString + " " + typeSuffix;
+ }
+ return groupDescrLabel;
+ }
+
+ private static JSONObject optMarkerTagJson(Marker marker) {
+ if (marker == null) {
+ return null;
+ }
+ Object markerTag = marker.getTag();
+ if (!(markerTag instanceof JSONObject)) {
+ return null;
+ }
+ return (JSONObject) markerTag;
+ }
+ }
+
+ public static class Location {
+
+ public static Double getDistanceBetween(LatLng firstLatLng, LatLng secondLatLng) {
+ if (firstLatLng == null || secondLatLng == null) {
+ return null;
+ }
+ android.location.Location firstLocation = new android.location.Location("firstLatLng");
+ firstLocation.setLatitude(firstLatLng.latitude);
+ firstLocation.setLongitude(firstLatLng.longitude);
+ android.location.Location secondLocation = new android.location.Location("secondLatLng");
+ secondLocation.setLatitude(secondLatLng.latitude);
+ secondLocation.setLongitude(secondLatLng.longitude);
+ float distance = firstLocation.distanceTo(secondLocation);
+ return (double) distance;
+ }
+ }
+
+ public static class Map {
+
+ public static String getValueFromPath(Object object, String path, String defaultValue) {
+ Object valueObject = getValueFromPath(object, path);
+ return (valueObject instanceof String) ? (String)valueObject : defaultValue;
+ }
+
+ public static int getValueFromPath(Object object, String path, int defaultValue) {
+ Object valueObject = getValueFromPath(object, path);
+ return (valueObject instanceof Integer) ? (Integer) valueObject : defaultValue;
+ }
+
+ public static long getValueFromPath(Object object, String path, long defaultValue) {
+ Object valueObject = getValueFromPath(object, path);
+ return (valueObject instanceof Long) ? (Long) valueObject : defaultValue;
+ }
+
+ public static double getValueFromPath(Object object, String path, double defaultValue) {
+ Object valueObject = getValueFromPath(object, path);
+ return (valueObject instanceof Double) ? (Double) valueObject : defaultValue;
+ }
+
+ public static boolean getValueFromPath(Object object, String path, boolean defaultValue) {
+ Object valueObject = getValueFromPath(object, path);
+ return (valueObject instanceof Boolean) ? (Boolean) valueObject : defaultValue;
+ }
+
+ private static Object getValueFromPath(Object object, String path) {
+ if (!(object instanceof java.util.Map) || Str.isEmpty(path)) {
+ return null;
+ }
+ java.util.Map map = (java.util.Map) object;
+ int dotFirstIndex = path.indexOf(".");
+ while (dotFirstIndex != -1) {
+ String subPath = path.substring(0, dotFirstIndex);
+ path = path.substring(dotFirstIndex + 1);
+ Object innerObject = (map != null) ? map.get(subPath) : null;
+ map = (innerObject instanceof HashMap) ? (HashMap) innerObject : null;
+ dotFirstIndex = path.indexOf(".");
+ }
+ Object generalValue = (map != null) ? map.get(path) : null;
+ return getPlatformValue(generalValue);
+ }
+
+ private static Object getPlatformValue(Object object) {
+ if (object instanceof HashMap) {
+ HashMap hashMap = (HashMap) object;
+ return hashMap.get("android");
+ } else {
+ return object;
+ }
+ }
+ }
+
+ public static class Str {
+ public static boolean isEmpty(String value) {
+ return (value == null) || value.isEmpty();
+ }
+
+ public static String nullIfEmpty(String value) {
+ if (isEmpty(value)) {
+ return null;
+ }
+ return value;
+ }
+
+ public static byte[] hexStringToByteArray(String s) {
+ if(s != null) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i + 1), 16));
+ }
+ return data;
+ }
+ return null;
+ }
+
+ public static String byteArrayToHexString(byte[] bytes){
+ if(bytes != null) {
+ Formatter formatter = new Formatter();
+ for (byte b : bytes) {
+ formatter.format("%02x", b);
+ }
+ return formatter.toString();
+ }
+ return null;
+ }
+
+ }
+
+ public static class Base64 {
+
+ public static byte[] decode(String value) {
+ if (value != null) {
+ return android.util.Base64.decode(value, android.util.Base64.NO_WRAP);
+ } else {
+ return null;
+ }
+ }
+
+ public static String encode(byte[] bytes) {
+ if (bytes != null) {
+ return android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public static class BackupStorage {
+
+ public static String getString(Context context, String fileName, String key) {
+ if ((context == null) || Str.isEmpty(fileName) || Str.isEmpty(key)) {
+ return null;
+ }
+ SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
+ return sharedPreferences.getString(key, null);
+ }
+
+ public static void saveString(Context context, String fileName, String key, String value) {
+ if ((context == null) || Str.isEmpty(fileName) || Str.isEmpty(key)) {
+ return;
+ }
+ SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString(key, value);
+ editor.apply();
+ AppBackupAgent.requestBackup(context);
+ }
+
+ public static void remove(Context context, String fileName, String key) {
+ if ((context == null) || Str.isEmpty(fileName) || Str.isEmpty(key)) {
+ return;
+ }
+ SharedPreferences sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.remove(key);
+ editor.apply();
+ AppBackupAgent.requestBackup(context);
+ }
+ }
+
+ public enum ExploreType {
+ EVENT, DINING, LAUNDRY, PARKING, UNKNOWN
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java b/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java
new file mode 100644
index 00000000..456764c0
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ExposurePlugin.java
@@ -0,0 +1,1335 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.welie.blessed.BluetoothCentral;
+import com.welie.blessed.BluetoothPeripheral;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+
+import androidx.annotation.NonNull;
+import at.favre.lib.crypto.HKDF;
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.MainActivity;
+import edu.illinois.covid.R;
+import edu.illinois.covid.Utils;
+import edu.illinois.covid.exposure.ble.ExposureClient;
+import edu.illinois.covid.exposure.ble.ExposureServer;
+import edu.illinois.covid.exposure.crypto.AES;
+import edu.illinois.covid.exposure.crypto.AES_CTR;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+
+public class ExposurePlugin implements MethodChannel.MethodCallHandler {
+
+ private static final String TAG = "ExposurePlugin";
+
+ private static ExposurePlugin instance;
+
+ private MainActivity activityContext;
+ private MethodChannel methodChannel;
+
+ private MethodChannel.Result startedResult;
+ private Object settings;
+
+ private ExposureServer exposureServer;
+ private boolean serverStarted;
+ private ExposureClient androidExposureClient;
+ private boolean clientStarted;
+
+ // iOS specific scanner/client
+ private BluetoothCentral iosExposureCentral;
+ private Handler handler = new Handler();
+ private Map peripherals;
+ private Map peripheralsRPIs;
+ private Map iosExposures;
+
+ // iOS background variable scanner
+ private static final int iosbgManufacturerID = 76;
+ private static final byte[] manufacturerDataMask = Utils.Str.hexStringToByteArray("ff00000000000000000000000000002000");
+ private static final byte[] manufacturerData = Utils.Str.hexStringToByteArray("0100000000000000000000000000002000");
+ private BluetoothAdapter iosBgBluetoothAdapter;
+ private BluetoothLeScanner iosBgBluoetoothLeScanner;
+ private List iosBgScanFilters;
+ private ScanSettings iosBgScanSettings;
+ private Map peripherals_bg;
+ private Handler ios_bg_handler = new Handler();
+ private Handler mainHandler = new Handler(Looper.getMainLooper());
+ private Handler callbackHandler = new Handler();
+ private static final String CCC_DESCRIPTOR_UUID = "00002902-0000-1000-8000-00805f9b34fb"; // characteristic notification
+ private Runnable iosBgScanTimeoutRunnable; // for restarting scan in android 9
+
+ // RPI
+ private byte[] rpi;
+ private Timer rpiTimer;
+ private static final int RPI_REFRESH_INTERVAL_SECS = 10 * 60; // 10 minutes
+ private static final int TEKRollingPeriod = 144;
+
+ private Timer exposuresTimer;
+ private long lastNotifyExposureTickTimestamp;
+ private Map androidExposures;
+
+ private Map> i_TEK_map;
+
+ // Exposure Constants
+ private static final int EXPOSURE_TIMEOUT_INTERVAL_MILLIS = 2 * 60 * 1000; // 2 minutes
+ private static final int EXPOSURE_PING_INTERVAL_MILLIS = 60 * 1000; // 1 minute
+ private static final int EXPOSURE_PROCESS_INTERVAL_MILLIS = 10 * 1000; // 10 secs
+ private static final int EXPOSURE_NOTIFY_TICK_INTERVAL_MILLIS = 1000; // 1 secs
+
+ // Exposure Settings
+ private int exposureTimeoutIntervalInMillis;
+ private int exposurePingIntervalInMillis;
+ private int exposureProcessIntervalInMillis;
+ private int exposureMinDurationInMillis;
+ private int exposureMinRssi;
+
+ // Helper constants
+ private static final String TEK_MAP_KEY = "tek";
+ private static final String RPI_MAP_KEY = "rpi";
+ private static final String EN_INTERVAL_NUMBER_MAP_KEY = "ENIntervalNumber";
+ private static final String I_MAP_KEY = "i";
+ private static final String DATABASE_VERSION_KEY = "databaseversion";
+
+ public static ExposurePlugin registerWith(PluginRegistry.Registrar registrar) {
+ final MethodChannel channel = new MethodChannel(registrar.messenger(), "edu.illinois.covid/exposure");
+ ExposurePlugin exposurePlugin = new ExposurePlugin((MainActivity) registrar.activity(), channel);
+ channel.setMethodCallHandler(exposurePlugin);
+ if (instance == null) {
+ instance = exposurePlugin;
+ }
+ return exposurePlugin;
+ }
+
+ private ExposurePlugin(MainActivity activity, MethodChannel methodChannel) {
+ this.activityContext = activity;
+ this.methodChannel = methodChannel;
+ this.methodChannel.setMethodCallHandler(this);
+
+ this.peripherals = new HashMap<>();
+ this.peripheralsRPIs = new HashMap<>();
+ this.iosExposures = new HashMap<>();
+ this.androidExposures = new HashMap<>();
+
+ this.i_TEK_map = loadTeksFromStorage();
+ this.peripherals_bg = new HashMap<>();
+ }
+
+ //region Public APIs Implementation
+
+ private void handleStart(@NonNull MethodChannel.Result result, Object settings) {
+ Log.d(TAG, "handleStart: start plugin");
+ startedResult = result;
+ initSettings(settings);
+ bindExposureServer();
+ bindExposureClient();
+ }
+
+ public void handleStop() {
+ Log.d(TAG, "handleStop: stop plugin");
+ startedResult = null;
+ stop();
+ }
+
+ private void start() {
+ Log.d(TAG, "start");
+ refreshRpi();
+ startAdvertise();
+ startRpiTimer();
+ startScan();
+ }
+
+ private void stop() {
+ Log.d(TAG, "stop");
+ stopAdvertise();
+ stopScan();
+ clearRpi();
+ clearExposures();
+ stopRpiTimer();
+ unBindExposureServer();
+ unBindExposureClient();
+ }
+
+ private void refreshRpi() {
+ Log.d(TAG, "refreshRpi");
+ Map retVal = generateRpi();
+ rpi = (byte[]) retVal.get(RPI_MAP_KEY);
+ byte[] tek = (byte[]) retVal.get(TEK_MAP_KEY);
+ int i = (int) retVal.get(I_MAP_KEY);
+ int enIntervalNumber = (int) retVal.get(EN_INTERVAL_NUMBER_MAP_KEY);
+ Log.d(TAG, "Logged - tek: " + Utils.Base64.encode(tek) + ", i: " + i + ", enIntervalNumber: " + enIntervalNumber);
+ uploadRPIUpdate(rpi, tek, Utils.DateTime.getCurrentTimeMillisSince1970(), i, enIntervalNumber);
+ String rpiEncoded = Utils.Base64.encode(rpi);
+ Log.d(TAG, "final rpi = " + rpiEncoded + "size = " + (rpi != null ? rpi.length : "null"));
+ if (exposureServer != null) {
+ exposureServer.setRpi(rpi);
+ }
+ }
+
+ private Map generateRpi() {
+ long currentTimestampInMillis = Utils.DateTime.getCurrentTimeMillisSince1970();
+ long currentTimeStampInSecs = currentTimestampInMillis / 1000;
+ int timestamp = (int) currentTimeStampInSecs;
+ int ENIntervalNumber = timestamp / RPI_REFRESH_INTERVAL_SECS;
+ Log.d(TAG, "ENIntervalNumber = " + ENIntervalNumber);
+ int i = (ENIntervalNumber / TEKRollingPeriod) * TEKRollingPeriod;
+ int expireIntervalNumber = i + TEKRollingPeriod;
+ Log.d(TAG, "i = " + i);
+
+ /* if new day, generate a new tek */
+ /* if in the rest of the day, using last valid TEK */
+ if ((i_TEK_map != null) && !i_TEK_map.isEmpty()) {
+ Integer lastTimestamp = Collections.max(i_TEK_map.keySet());
+ Map lastTek = i_TEK_map.get(lastTimestamp);
+ Integer lastExpireTime = (lastTek != null) ? lastTek.get(lastTek.keySet().iterator().next()) : null;
+ if ((lastExpireTime != null) && (lastExpireTime == expireIntervalNumber)) {
+ i = lastTimestamp;
+ } else {
+ i = ENIntervalNumber;
+ }
+ }
+
+ Map tek = new HashMap<>();
+ if (i_TEK_map.isEmpty() || !i_TEK_map.containsKey(i)) {
+ byte[] bytes = new byte[16];
+ SecureRandom rand = new SecureRandom(); // generating TEK with a cryptographic random number generator
+ rand.nextBytes(bytes);
+ tek.put(bytes, expireIntervalNumber);
+ i_TEK_map.put(i, tek); // putting the TEK map as a value for the current i
+
+ // handling more than 14 i values in the map
+ if (i_TEK_map.size() >= 14) {
+ Iterator it = i_TEK_map.keySet().iterator();
+ while (it.hasNext()) {
+ int key = it.next();
+ if (key <= i - 14)
+ it.remove();
+ }
+ }
+
+ // Save TEKs to storage
+ saveTeksToStorage(i_TEK_map);
+
+ // Notify TEK
+ long notifyTimestampInMillis = (long) i * RPI_REFRESH_INTERVAL_SECS * 1000; // in millis
+ long expireTime = (long) expireIntervalNumber * RPI_REFRESH_INTERVAL_SECS * 1000;
+ byte[] tekData = tek.keySet().iterator().next();
+ notifyTek(tekData, notifyTimestampInMillis, expireTime);
+ } else {
+ tek = i_TEK_map.get(i);
+ }
+ byte[] rpiTek = (tek != null) ? tek.keySet().iterator().next() : null;
+ byte[] rpi = generateRpiForIntervalNumber(ENIntervalNumber, rpiTek);
+ Map retVal = new HashMap<>();
+ retVal.put(RPI_MAP_KEY, rpi);
+ retVal.put(TEK_MAP_KEY, rpiTek);
+ retVal.put(EN_INTERVAL_NUMBER_MAP_KEY, ENIntervalNumber);
+ retVal.put(I_MAP_KEY, i);
+ return retVal;
+ }
+
+ private byte[] generateRpiForIntervalNumber(int enIntervalNumber, byte[] tek) {
+ // generating RPIK with salt as null and passing in the correct parameters to the extractandexpand function of HKDF class
+ // in the file HKDF.java
+ byte[] salt = null;
+ byte[] info = "EN-RPIK".getBytes();
+ byte[] RPIK = HKDF.fromHmacSha256().extractAndExpand(salt, tek, info, 16);
+
+ // generating AEMK using the same method
+ info = "EN-AEMK".getBytes();
+ byte[] AEMK = HKDF.fromHmacSha256().extractAndExpand(salt, tek, info, 16);
+
+ // creating the padded data for AES encryption of RPIK to get RPI
+ byte[] padded_data = new byte[16];
+ byte[] EN_RPI = "EN-RPI".getBytes();
+ ByteBuffer bb = ByteBuffer.allocate(4);
+ bb.putInt(enIntervalNumber);
+ byte[] ENIN = bb.array();
+ int j = 0;
+ for (byte b : EN_RPI) {
+ padded_data[j] = b;
+ j++;
+ }
+ for (j = 6; j <= 11; j++) {
+ padded_data[j] = 0;
+ }
+ for (byte b : ENIN) {
+ padded_data[j] = b;
+ j++;
+ }
+
+ byte[] rpi_byte = AES.encrypt(RPIK, padded_data);
+
+ if (rpi_byte == null) {
+ Log.w(TAG, "Newly generated rpi_byte is null");
+ return null;
+ }
+
+ byte[] metadata = new byte[4];
+ byte[] AEM_byte = new byte[4];
+ try {
+ AEM_byte = AES_CTR.encrypt(AEMK, rpi_byte, metadata);
+ } catch (Exception e) {
+ System.out.println("Error while encrypting: " + e.toString());
+ }
+
+ byte[] bluetoothpayload = new byte[20];
+ System.arraycopy(rpi_byte, 0, bluetoothpayload, 0, rpi_byte.length);
+ System.arraycopy(AEM_byte, 0, bluetoothpayload, rpi_byte.length, AEM_byte.length);
+
+ return bluetoothpayload;
+ }
+
+ private void uploadRPIUpdate(byte[] rpi, byte[] parentTek, long updateTime, int i, int ENInvertalNumber) {
+ String tekString = Utils.Base64.encode(parentTek);
+ String rpiString = Utils.Base64.encode(rpi);
+ Map rpiParams = new HashMap<>();
+ rpiParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, updateTime);
+ rpiParams.put(Constants.EXPOSURE_PLUGIN_TEK_PARAM_NAME, tekString);
+ rpiParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, rpiString);
+ rpiParams.put("updateType", "");
+ rpiParams.put("_i", i);
+ rpiParams.put("ENInvertalNumber", ENInvertalNumber);
+ invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_METHOD_NAME_RPI_LOG, rpiParams);
+ }
+
+ private void clearRpi() {
+ rpi = null;
+ }
+
+ private void startAdvertise() {
+ if (exposureServer != null) {
+ exposureServer.start();
+ }
+ }
+
+ private void stopAdvertise() {
+ if (exposureServer != null) {
+ exposureServer.stop();
+ }
+ }
+
+ private void startScan() {
+ if (androidExposureClient != null) {
+ androidExposureClient.startScan();
+ }
+ startIosScan();
+ }
+
+ private void stopScan() {
+ if (androidExposureClient != null) {
+ androidExposureClient.stopScan();
+ }
+ stopIosScan();
+ processExposures();
+ }
+
+ private void initSettings(Object settings) {
+ this.settings = settings;
+
+ // Exposure Timeout Interval
+ int timeoutIntervalInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceTimeoutInterval", (EXPOSURE_TIMEOUT_INTERVAL_MILLIS / 1000)); // in seconds
+ this.exposureTimeoutIntervalInMillis = timeoutIntervalInSecs * 1000; //in millis
+
+ // Exposure Ping Interval
+ int pingIntervalInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServicePingInterval", (EXPOSURE_PING_INTERVAL_MILLIS / 1000)); // in seconds
+ this.exposurePingIntervalInMillis = pingIntervalInSecs * 1000; //in millis
+
+ // Exposure Process Interval
+ int processIntervalInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceProcessInterval", (EXPOSURE_PROCESS_INTERVAL_MILLIS / 1000)); // in seconds
+ this.exposureProcessIntervalInMillis = processIntervalInSecs * 1000; //in millis
+
+ // Exposure Min Duration Interval
+ int minDurationInSecs = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceLogMinDuration", (Constants.EXPOSURE_MIN_DURATION_MILLIS / 1000)); // in seconds
+ this.exposureMinDurationInMillis = minDurationInSecs * 1000; //in millis
+
+ // Exposure Min RSSI
+ this.exposureMinRssi = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceMinRSSI", Constants.EXPOSURE_MIN_RSSI_VALUE);
+ }
+
+ //endregion
+
+ //region Single Instance
+
+ public static ExposurePlugin getInstance() {
+ return instance;
+ }
+
+ int getExposureMinRssi() {
+ return exposureMinRssi;
+ }
+
+ //endregion
+
+ //region External RPI implementation
+
+ private void logAndroidExposure(String rpi, int rssi, String deviceAddress) {
+ long currentTimeStamp = Utils.DateTime.getCurrentTimeMillisSince1970();
+ ExposureRecord record = androidExposures.get(rpi);
+ if (record == null) {
+ Log.d(TAG, "registered android rpi: " + rpi);
+ record = new ExposureRecord(currentTimeStamp, rssi);
+ androidExposures.put(rpi, record);
+ updateExposuresTimer();
+ } else {
+ record.updateTimeStamp(currentTimeStamp, rssi);
+ }
+ notifyExposureTick(rpi, rssi);
+ notifyExposureRssiLog(rpi, currentTimeStamp, rssi, false, deviceAddress);
+ }
+
+ private void logIosExposure(String peripheralAddress, int rssi) {
+ if (Utils.Str.isEmpty(peripheralAddress)) {
+ return;
+ }
+ long currentTimestamp = Utils.DateTime.getCurrentTimeMillisSince1970();
+ ExposureRecord record = iosExposures.get(peripheralAddress);
+ if (record == null) {
+ // Create new
+ Log.d(TAG, "Registered ios peripheral: " + peripheralAddress);
+ record = new ExposureRecord(currentTimestamp, rssi);
+ iosExposures.put(peripheralAddress, record);
+ updateExposuresTimer();
+ } else {
+ // Update existing
+ record.updateTimeStamp(currentTimestamp, rssi);
+ }
+ byte[] rpi = peripheralsRPIs.get(peripheralAddress);
+ String encodedRpi = "";
+ if (rpi != null) {
+ encodedRpi = Utils.Base64.encode(rpi);
+ notifyExposureTick(encodedRpi, rssi);
+ }
+ notifyExposureRssiLog(encodedRpi, currentTimestamp, rssi, true, peripheralAddress);
+ }
+
+ private void notifyExposureTick(String rpi, int rssi) {
+ if (Utils.Str.isEmpty(rpi)) {
+ return;
+ }
+ long currentTimestamp = Utils.DateTime.getCurrentTimeMillisSince1970();
+ // Do not allow more than one notification per second
+ if (EXPOSURE_NOTIFY_TICK_INTERVAL_MILLIS <= (currentTimestamp - lastNotifyExposureTickTimestamp)) {
+ Map exposureTickParams = new HashMap<>();
+ exposureTickParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, currentTimestamp);
+ exposureTickParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, rpi);
+ exposureTickParams.put(Constants.EXPOSURE_PLUGIN_RSSI_PARAM_NAME, rssi);
+ invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_METHOD_NAME_THICK, exposureTickParams);
+ lastNotifyExposureTickTimestamp = currentTimestamp;
+ }
+ }
+
+ private void notifyExposureRssiLog(String encodedRpi, long currentTimeStamp, int rssi, boolean isiOS, String address) {
+ Map rssiParams = new HashMap<>();
+ rssiParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, encodedRpi);
+ rssiParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, currentTimeStamp);
+ rssiParams.put(Constants.EXPOSURE_PLUGIN_RSSI_PARAM_NAME, rssi);
+ rssiParams.put(Constants.EXPOSURE_PLUGIN_IOS_RECORD_PARAM_NAME, isiOS);
+ rssiParams.put(Constants.EXPOSURE_PLUGIN_ADDRESS_PARAM_NAME, address);
+ invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_METHOD_NAME_RSSI_LOG, rssiParams);
+ }
+
+ private void processExposures() {
+ Log.d(TAG, "Process Exposures");
+ long currentTimestamp = Utils.DateTime.getCurrentTimeMillisSince1970();
+ Set expiredPeripheralAddress = null;
+
+ // 1. Collect all iOS expired records (not updated after exposureTimeoutIntervalInMillis)
+ if ((iosExposures != null) && !iosExposures.isEmpty()) {
+ for (String peripheralAddress : iosExposures.keySet()) {
+ ExposureRecord record = iosExposures.get(peripheralAddress);
+ if (record != null) {
+ long lastHeardInterval = currentTimestamp - record.getTimestampUpdated();
+ if (exposureTimeoutIntervalInMillis <= lastHeardInterval) {
+ Log.d(TAG, "Expired ios exposure: " + peripheralAddress);
+ if (expiredPeripheralAddress == null) {
+ expiredPeripheralAddress = new HashSet<>();
+ }
+ expiredPeripheralAddress.add(peripheralAddress);
+ } else if(exposurePingIntervalInMillis <= lastHeardInterval) {
+ Log.d(TAG, "ios exposure ping: " + peripheralAddress);
+ BluetoothPeripheral peripheral = (peripherals != null) ? peripherals.get(peripheralAddress) : null;
+ if(peripheral != null) {
+ peripheral.readRemoteRssi();
+ }
+ }
+ }
+ }
+ }
+
+ if ((expiredPeripheralAddress != null) && !expiredPeripheralAddress.isEmpty()) {
+ for (String address : expiredPeripheralAddress) {
+ // remove expired records from iosExposures
+ disconnectIosPeripheral(address);
+ }
+ }
+
+ // 2. Collect all Android expired records (not updated after exposureTimeoutIntervalInMillis)
+ Set expiredRPIs = null;
+ if((androidExposures != null) && !androidExposures.isEmpty()) {
+ for(String encodedRpi : androidExposures.keySet()) {
+ ExposureRecord record = androidExposures.get(encodedRpi);
+ if(record != null) {
+ long lastHeardInterval = currentTimestamp - record.getTimestampUpdated();
+ if(exposureTimeoutIntervalInMillis <= lastHeardInterval) {
+ Log.d(TAG, "Expired android exposure: " + encodedRpi);
+ if(expiredRPIs == null) {
+ expiredRPIs = new HashSet<>();
+ }
+ expiredRPIs.add(encodedRpi);
+ }
+ }
+ }
+ }
+
+ if (expiredRPIs != null) {
+ // remove expired records from androidExposures
+ for (String encodedRpi : expiredRPIs) {
+ removeAndroidRpi(encodedRpi);
+ }
+ }
+ }
+
+ private void clearExposures() {
+ if ((iosExposures != null) && !iosExposures.isEmpty()) {
+ Map iosExposureCopy = new HashMap<>(iosExposures);
+ for (String address : iosExposureCopy.keySet()) {
+ disconnectIosPeripheral(address);
+ }
+ }
+ if ((androidExposures != null) && !androidExposures.isEmpty()) {
+ Map androidExposureCopy = new HashMap<>(androidExposures);
+ for (String encodedRpi : androidExposureCopy.keySet()) {
+ removeAndroidRpi(encodedRpi);
+ }
+ }
+ }
+
+ private void disconnectIosPeripheral(String peripheralAddress) {
+ disconnectIosBgPeripheral(peripheralAddress);
+ }
+
+ private void removeIosPeripheral(String address) {
+ if (Utils.Str.isEmpty(address) || (peripherals == null) || (peripherals.isEmpty())) {
+ return;
+ }
+ peripherals.remove(address);
+ byte[] rpi = (peripheralsRPIs != null) ? peripheralsRPIs.get(address) : null;
+ if (rpi != null) {
+ peripheralsRPIs.remove(address);
+ }
+ if ((iosExposures == null) || iosExposures.isEmpty()) {
+ return;
+ }
+ ExposureRecord record = iosExposures.get(address);
+ if (record != null) {
+ iosExposures.remove(address);
+ updateExposuresTimer();
+ }
+ if ((rpi != null) && (record != null)) {
+ String encodedRpi = Utils.Base64.encode(rpi);
+ notifyExposure(record, encodedRpi, true, address);
+ }
+ }
+
+ private void removeAndroidRpi(String rpi) {
+ if ((androidExposures == null) || androidExposures.isEmpty()) {
+ return;
+ }
+ ExposureRecord record = androidExposures.get(rpi);
+ if (record != null) {
+ androidExposures.remove(rpi);
+ updateExposuresTimer();
+ }
+
+ if ((rpi != null) && (record != null)) {
+ notifyExposure(record, rpi, false, "");
+ }
+ }
+
+ private void notifyExposure(ExposureRecord record, String rpi, boolean isiOS, String peripheralUuid) {
+ if ((record != null) && (exposureMinDurationInMillis <= record.getDuration())) {
+ Map exposureParams = new HashMap<>();
+ exposureParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, record.getTimestampCreated());
+ exposureParams.put(Constants.EXPOSURE_PLUGIN_RPI_PARAM_NAME, rpi);
+ exposureParams.put(Constants.EXPOSURE_PLUGIN_DURATION_PARAM_NAME, record.getDuration());
+ exposureParams.put(Constants.EXPOSURE_PLUGIN_IOS_RECORD_PARAM_NAME, isiOS);
+ exposureParams.put(Constants.EXPOSURE_PLUGIN_PERIPHERAL_UUID_PARAM_NAME, peripheralUuid);
+ invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_EXPOSURE_METHOD_NAME, exposureParams);
+ }
+ }
+
+ //endregion
+
+ //region TEKs
+
+ private void changeTekExpireTime() {
+ i_TEK_map = loadTeksFromStorage();
+ if (i_TEK_map != null) {
+ Integer currentI = Collections.max(i_TEK_map.keySet());
+ Map oldTEK = i_TEK_map.get(currentI);
+ byte[] tek = (oldTEK != null) ? oldTEK.keySet().iterator().next() : null;
+
+ long currentTimestampInMillis = Utils.DateTime.getCurrentTimeMillisSince1970();
+ long currentTimeStampInSecs = currentTimestampInMillis / 1000;
+ int timestamp = (int) currentTimeStampInSecs;
+ int ENIntervalNumber = timestamp / RPI_REFRESH_INTERVAL_SECS;
+ Map newTEK = new HashMap<>();
+ newTEK.put(tek, (ENIntervalNumber + 1));
+
+ i_TEK_map.replace(currentI, newTEK);
+ saveTeksToStorage(i_TEK_map);
+ }
+ }
+
+ private void saveTeksToStorage(Map> teks) {
+ if (teks != null) {
+ Map storageTeks = new HashMap<>();
+ for (Integer key : teks.keySet()) {
+ String storageKey = key != null ? Integer.toString(key) : null;
+ Map value = teks.get(key);
+ byte[] tek = (value != null) ? value.keySet().iterator().next() : null;
+ Integer expire = (value != null) ? value.get(tek) : null;
+ Map tekAndExpireTime = new HashMap<>();
+ tekAndExpireTime.put(Utils.Base64.encode(tek), (expire != null ? Integer.toString(expire) : null));
+ JSONObject jsonTekAndExpireTime = new JSONObject(tekAndExpireTime);
+ String storageValue = jsonTekAndExpireTime.toString();
+ if (storageKey != null) {
+ storageTeks.put(storageKey, storageValue);
+ }
+ }
+ JSONObject teksJson = new JSONObject(storageTeks);
+ String teksString = teksJson.toString();
+ Utils.BackupStorage.saveString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY, teksString);
+ } else {
+ Utils.BackupStorage.remove(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY);
+ }
+ }
+
+ private Map> loadTeksFromStorage() {
+ Log.d(TAG, "entering loadTeksFromStorage function");
+
+ //checking database version
+ boolean dataBaseChangeVersion = false;
+ String databaseVersion = Utils.BackupStorage.getString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEK_VERSION);
+ if (Utils.Str.isEmpty(databaseVersion)) {
+ Log.d(TAG, "no database version found");
+ dataBaseChangeVersion = true;
+ } else {
+ JSONObject jsonDatabaseVersion = null;
+ try {
+ jsonDatabaseVersion = new JSONObject(databaseVersion);
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to parse database version string to json!");
+ e.printStackTrace();
+ }
+ if (jsonDatabaseVersion != null) {
+ String version = jsonDatabaseVersion.optString(DATABASE_VERSION_KEY);
+ Log.d(TAG, "current TEK database version is " + version);
+ if (Utils.Str.isEmpty(version) || Integer.parseInt(version) != 2) {
+ dataBaseChangeVersion = true;
+ }
+ }
+ }
+
+ if (dataBaseChangeVersion) {
+ Utils.BackupStorage.remove(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY);
+ }
+
+ Map> teks = new HashMap<>();
+ String teksString = Utils.BackupStorage.getString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEKS_SHARED_PREFS_KEY);
+ if (!Utils.Str.isEmpty(teksString)) {
+ JSONObject teksJson = null;
+ try {
+ teksJson = new JSONObject(teksString);
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to parse TEKs string to json!");
+ e.printStackTrace();
+ }
+ if (teksJson != null) {
+ Iterator iterator = teksJson.keys();
+ while (iterator.hasNext()) {
+ String storageKey = iterator.next();
+ String storageValue = teksJson.optString(storageKey);
+ Map tekAndExpireTime = new HashMap<>();
+ if (!Utils.Str.isEmpty(storageValue)) {
+ JSONObject jsonTekAndExpireTime = null;
+ try {
+ jsonTekAndExpireTime = new JSONObject(storageValue);
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to parse TEK map string to json!");
+ e.printStackTrace();
+ }
+ if (jsonTekAndExpireTime != null) {
+ Log.d(TAG, "LoadTEK: Found Nested Map");
+ Iterator tekIterator = jsonTekAndExpireTime.keys();
+ if (tekIterator.hasNext()) {
+ String tekString = tekIterator.next();
+ String expireString = jsonTekAndExpireTime.optString(tekString);
+ tekAndExpireTime.put(Utils.Base64.decode(tekString), Integer.parseInt(expireString));
+ }
+ }
+ }
+ teks.put(Integer.parseInt(storageKey), tekAndExpireTime);
+ }
+ }
+ }
+
+ // update database version
+
+ if (dataBaseChangeVersion) {
+ Map databaseVersionToStore = new HashMap<>();
+ databaseVersionToStore.put(DATABASE_VERSION_KEY, Integer.toString(2));
+ JSONObject jsonDatabaseVersion = new JSONObject(databaseVersionToStore);
+ String databaseVersionString = jsonDatabaseVersion.toString();
+ Utils.BackupStorage.saveString(activityContext, Constants.EXPOSURE_TEKS_SHARED_PREFS_FILE_NAME, Constants.EXPOSURE_TEK_VERSION, databaseVersionString);
+ }
+ return teks;
+ }
+
+ private List> getTeksList() {
+ List> teksList = new ArrayList<>();
+ if ((i_TEK_map != null) && !i_TEK_map.isEmpty()) {
+ for (Integer tekKey : i_TEK_map.keySet()) {
+ long timestamp = tekKey.longValue() * RPI_REFRESH_INTERVAL_SECS * 1000; //in millis
+ Map tek = i_TEK_map.get(tekKey);
+ byte[] tekData = (tek != null) ? tek.keySet().iterator().next() : null;
+ String tekString = Utils.Base64.encode(tekData);
+ Integer expireInteger = (tek != null) ? tek.get(tekData) : null;
+ long expireTime = (expireInteger != null) ? expireInteger.longValue() * RPI_REFRESH_INTERVAL_SECS * 1000 : 0; //in millis
+ Map tekMap = new HashMap<>();
+ tekMap.put("timestamp", timestamp);
+ tekMap.put("tek", tekString);
+ tekMap.put(Constants.EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME, expireTime);
+ teksList.add(tekMap);
+ }
+ }
+ return teksList;
+ }
+
+ private Map getRpisForTek(byte[] tek, long timestampInMillis, long expireTime) {
+ long timestampInSecs = timestampInMillis / 1000;
+
+ long expireTimeInSecs = expireTime / 1000;
+
+ int startENIntervalNumber = (int) (timestampInSecs / RPI_REFRESH_INTERVAL_SECS);
+ int endENIntervalNumber = (int) (expireTimeInSecs / RPI_REFRESH_INTERVAL_SECS);
+ Map rpiList = new HashMap<>();
+ for (int intervalIndex = startENIntervalNumber; intervalIndex <= endENIntervalNumber; intervalIndex++) {
+ byte[] rpi = generateRpiForIntervalNumber(intervalIndex, tek);
+ String rpiString = Utils.Base64.encode(rpi);
+ rpiList.put(rpiString, (long) intervalIndex * RPI_REFRESH_INTERVAL_SECS * 1000);
+ }
+ return rpiList;
+ }
+
+ private void notifyTek(byte[] tek, long timestamp, long expireTime) {
+ String tekString = Utils.Base64.encode(tek);
+ Map tekParams = new HashMap<>();
+ tekParams.put(Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, timestamp);
+ tekParams.put(Constants.EXPOSURE_PLUGIN_TEK_PARAM_NAME, tekString);
+ tekParams.put(Constants.EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME, expireTime);
+ invokeFlutterMethod(Constants.EXPOSURE_PLUGIN_TEK_METHOD_NAME, tekParams);
+ }
+
+ //endregion
+
+ //region Exposure Server
+
+ private void bindExposureServer() {
+ Intent intent = new Intent(activityContext, ExposureServer.class);
+ activityContext.bindService(intent, serverConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void unBindExposureServer() {
+ activityContext.unbindService(serverConnection);
+ serverStarted = false;
+ }
+
+ private ServiceConnection serverConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ exposureServer = ((ExposureServer.LocalServerBinder)service).getService();
+ if (exposureServer == null) {
+ return;
+ }
+ serverStarted = true;
+ exposureServer.setCallback(serverCallback);
+ checkStarted();
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ serverStarted = false;
+ exposureServer = null;
+ }
+ };
+
+ private ExposureServer.Callback serverCallback = new ExposureServer.Callback() {
+ @Override
+ public void onRequestBluetoothOn() {
+ ExposurePlugin.this.requestBluetoothOn();
+ }
+ };
+
+ //endregion
+
+ //region Exposure Client
+
+ private void bindExposureClient() {
+ Intent intent = new Intent(activityContext, ExposureClient.class);
+ activityContext.bindService(intent, clientConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void unBindExposureClient() {
+ activityContext.unbindService(clientConnection);
+ clientStarted = false;
+ }
+
+ private ServiceConnection clientConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ androidExposureClient = ((ExposureClient.LocalBinder)service).getService();
+ if (androidExposureClient == null) {
+ return;
+ }
+ androidExposureClient.initSettings(settings);
+ clientStarted = true;
+ androidExposureClient.setRpiCallback(clientRpiCallback);
+ checkStarted();
+ }
+ public void onServiceDisconnected(ComponentName className) {
+ clientStarted = false;
+ androidExposureClient = null;
+ }
+ };
+
+ private ExposureClient.RpiCallback clientRpiCallback = new ExposureClient.RpiCallback() {
+ @Override
+ public void onRpiFound(byte[] rpi, int rssi, String address) {
+ if ((rpi != null) && (rssi != Constants.EXPOSURE_NO_RSSI_VALUE)) {
+ String rpiEncoded = Utils.Base64.encode(rpi);
+ Log.d(TAG, String.format(Locale.getDefault(), "onRpiFound: '%s' / rssi: %d", rpiEncoded, rssi));
+ logAndroidExposure(rpiEncoded, rssi, address);
+ }
+ }
+
+ @Override
+ public void onIOSDeviceFound(ScanResult scanResult) {
+ BluetoothDevice device = (scanResult != null) ? scanResult.getDevice() : null;
+ String devAddress = (device != null) ? device.getAddress() : null;
+ if (!Utils.Str.isEmpty(devAddress)) {
+ if (peripherals_bg.get(devAddress) == null) {
+ Log.d(TAG, ": ios fg attempting to connect");
+ // new device discovered.
+ peripherals_bg.put(devAddress, device.connectGatt(activityContext, false, iOSBackgroundBluetoothGattCallback));
+ }
+ logIosExposure(devAddress, scanResult.getRssi());
+ }
+ }
+ };
+
+ //endregion
+
+ //region iOS specific scanner/client
+
+ private void startIosScan() {
+ startIosBgScan();
+ }
+
+ private void stopIosScan() {
+ stopIosBgScan();
+ }
+
+ //endregion
+
+ //region iOS background specific scanner/client
+
+ private void startIosBgScan() {
+ Log.d(TAG, "startIosBgScan()");
+ try {
+ if (isIosBgScanning()) {
+ stopIosBgScan();
+ }
+ if (iosBgBluetoothAdapter == null) {
+ Object systemService = activityContext.getSystemService(Context.BLUETOOTH_SERVICE);
+ if ((systemService instanceof BluetoothManager)) {
+ iosBgBluetoothAdapter = ((BluetoothManager) systemService).getAdapter();
+ }
+ }
+ if (iosBgBluetoothAdapter == null) {
+ Log.d(TAG, "ios bg scan bluetooth adapter init failure");
+ return;
+ }
+ if (iosBgBluoetoothLeScanner == null) {
+ iosBgBluoetoothLeScanner = iosBgBluetoothAdapter.getBluetoothLeScanner();
+ }
+ if (iosBgBluoetoothLeScanner == null) {
+ Log.d(TAG, "ios bg scan bluetooth scanner init failure");
+ return;
+ }
+ startIosBgScanTimer();
+ ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder()
+ .setManufacturerData(iosbgManufacturerID, manufacturerData, manufacturerDataMask);
+ iosBgScanFilters = Collections.singletonList(scanFilterBuilder.build());
+
+ ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder()
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
+ .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+ .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
+ .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT);
+ iosBgScanSettings = scanSettingsBuilder.build();
+
+ iosBgBluoetoothLeScanner.startScan(iosBgScanFilters, iosBgScanSettings, iOSBackgroundScanCallback);
+ Log.d(TAG, "Start ios bg scan success!");
+ } catch (Exception ex) {
+ Log.e(TAG, ex.toString());
+ ex.printStackTrace();
+ // re-try
+ startIosBgScan();
+ }
+ }
+
+ private void stopIosBgScan() {
+ stopIosBgScanTimer();
+ if (isIosBgScanning()) {
+ iosBgBluoetoothLeScanner.stopScan(iOSBackgroundScanCallback);
+ Log.d(TAG, "stopped ios bg scanning");
+ } else {
+ Log.d(TAG, "no ios bg scanner is scanning");
+ }
+ iosBgBluoetoothLeScanner = null;
+ iosBgBluetoothAdapter = null;
+ iosBgScanFilters = null;
+ iosBgScanSettings = null;
+ }
+
+ private boolean isIosBgScanning() {
+ return ((iosBgBluetoothAdapter != null) && (iosBgBluoetoothLeScanner != null) &&
+ (iosBgScanFilters != null) && (iosBgScanSettings != null));
+ }
+
+ private void startIosBgScanTimer() {
+ stopIosBgScanTimer();
+
+ iosBgScanTimeoutRunnable = () -> {
+ Log.d(TAG, "scanning timeout, restarting scan");
+ stopIosBgScan();
+ // Restart the scan and timer
+ callbackHandler.postDelayed(this::startIosBgScan, 1_000);
+ };
+
+ mainHandler.postDelayed(iosBgScanTimeoutRunnable, 180_000);
+ }
+
+ private void stopIosBgScanTimer() {
+ if (iosBgScanTimeoutRunnable != null) {
+ mainHandler.removeCallbacks(iosBgScanTimeoutRunnable);
+ iosBgScanTimeoutRunnable = null;
+ }
+ }
+
+ private void disconnectIosBgPeripheral(String peripheralAddress) {
+ if (Utils.Str.isEmpty(peripheralAddress) || (peripherals_bg == null)) {
+ return;
+ }
+ BluetoothGatt ble_gatt = peripherals_bg.get(peripheralAddress);
+ if (ble_gatt == null) {
+ Log.d(TAG, "gatt does not exist in bg");
+ return;
+ }
+ // clean up connection
+ ble_gatt.close();
+ ble_gatt.disconnect();
+ removeIosBgPeripheral(peripheralAddress);
+ }
+
+ private void removeIosBgPeripheral(String address) {
+ if (Utils.Str.isEmpty(address) || (peripherals_bg == null) || (peripherals_bg.isEmpty())) {
+ return;
+ }
+ peripherals_bg.remove(address);
+ byte[] rpi = (peripheralsRPIs != null) ? peripheralsRPIs.get(address) : null;
+ if (rpi != null) {
+ peripheralsRPIs.remove(address);
+ }
+ if ((iosExposures == null) || iosExposures.isEmpty()) {
+ return;
+ }
+ ExposureRecord record = iosExposures.get(address);
+ if (record != null) {
+ iosExposures.remove(address);
+ updateExposuresTimer();
+ }
+ if ((rpi != null) && (record != null)) {
+ String encodedRpi = Utils.Base64.encode(rpi);
+ notifyExposure(record, encodedRpi, true, address);
+ }
+ }
+
+ private BluetoothGattCallback iOSBackgroundBluetoothGattCallback = new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ BluetoothGatt a = peripherals_bg.get(gatt.getDevice().getAddress());
+ if (newState == 2 && a != null) {
+ // seems that a delay is needed
+ int delay = 600;
+ ios_bg_handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ boolean result = gatt.discoverServices();
+ if (!result) {
+ Log.d(TAG, "DiscoverServices failed to start");
+ }
+ }
+ }, delay);
+
+ } else if (newState == 0) {
+ peripherals_bg.remove(gatt.getDevice().getAddress());
+ gatt.close();
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ if (status == 0) {
+ BluetoothGatt _local_gatt = peripherals_bg.get(gatt.getDevice().getAddress());
+ BluetoothGattService _local_service = (_local_gatt != null) ? _local_gatt.getService(Constants.EXPOSURE_UUID_SERVICE) : null;
+ if (_local_service == null) {
+ // disconnect? remove from dictionary
+ peripherals_bg.remove(gatt.getDevice().getAddress());
+ gatt.close();
+ } else {
+ // read characteristics from the service
+ // log ios devices.
+ BluetoothGattCharacteristic _local_characeristics = _local_service.getCharacteristic(Constants.EXPOSURE_UUID_CHARACTERISTIC);
+ _local_gatt.readCharacteristic(_local_characeristics);
+ }
+ } else {
+ Log.d(TAG, "service discovery failed.: " + status);
+ }
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
+ int status) {
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ Log.d(TAG, "reading characteristics failed");
+ return;
+ }
+ if (characteristic.getUuid().equals(Constants.EXPOSURE_UUID_CHARACTERISTIC)) {
+ byte[] val = characteristic.getValue();
+ peripheralsRPIs.put(gatt.getDevice().getAddress(), val);
+ // copied from blessed image and modified
+ BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(CCC_DESCRIPTOR_UUID));
+ if (descriptor == null) {
+ peripherals_bg.remove(gatt.getDevice().getAddress());
+ gatt.close();
+ return;
+ }
+ byte[] value;
+ int properties = characteristic.getProperties();
+ if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
+ value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
+ } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
+ value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
+ } else {
+ peripherals_bg.remove(gatt.getDevice().getAddress());
+ gatt.close();
+ return;
+ }
+ final byte[] finalValue = value;
+ // First set notification for Gatt object
+ // turn on or off
+ if (!gatt.setCharacteristicNotification(characteristic, true)) {
+ Log.d(TAG, "setCharacteristicNotification failed for characteristic: "
+ + characteristic.getUuid());
+ }
+ // Then write to descriptor
+ descriptor.setValue(finalValue);
+ boolean result;
+ result = gatt.writeDescriptor(descriptor);
+ if (!result) {
+ peripherals_bg.remove(gatt.getDevice().getAddress());
+ gatt.close();
+ }
+ else{
+ Log.d(TAG, "descriptor written successfully");
+ }
+ }
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ if (characteristic.getUuid().equals(Constants.EXPOSURE_UUID_CHARACTERISTIC)) {
+ byte[] val = characteristic.getValue();
+ String encoded = Utils.Base64.encode(val);
+ Log.d(TAG, "onCharacteristicChange: value: " + encoded + ", device address: " + gatt.getDevice().getAddress());
+ peripheralsRPIs.put(gatt.getDevice().getAddress(), val);
+ }
+ }
+ };
+
+ private final ScanCallback iOSBackgroundScanCallback = new ScanCallback() {
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ Log.d(TAG, "iOSBackgroundScanCallback: onScanFailed: " + errorCode);
+ }
+
+ @Override
+ public void onBatchScanResults(List results) {
+ Log.d(TAG, "iOSBackgroundScanCallback: onBatchScanResults: " + ((results != null) ? results.size() : 0));
+ }
+
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ super.onScanResult(callbackType, result);
+ ScanRecord scanrecord = result.getScanRecord();
+ List parcelUuids = (scanrecord != null) ? scanrecord.getServiceUuids() : null;
+ List serviceList = new ArrayList<>();
+ if (parcelUuids != null) {
+ for (int i = 0; i < parcelUuids.size(); i++) {
+ UUID serviceUUID = parcelUuids.get(i).getUuid();
+ if (!serviceList.contains(serviceUUID))
+ serviceList.add(serviceUUID);
+ }
+ } else {
+ Log.d(TAG, "parcel UUID is null");
+ }
+ BluetoothDevice device = result.getDevice();
+ byte[] manData = (scanrecord != null) ? scanrecord.getManufacturerSpecificData(iosbgManufacturerID) : null;
+ if (manData != null) {
+ // 01
+ if (manData.length >= 17) {
+ if (manData[0] == 0x01) {
+ if (((manData[15] >> 5) & 0x01) == 1) {
+ String devAddress = device.getAddress();
+ // bg device discovered
+ // equivalently ondiscoverperipherals
+ if (peripherals_bg.get(devAddress) == null) {
+ // new device discovered.
+ peripherals_bg.put(devAddress,
+ device.connectGatt(activityContext, false, iOSBackgroundBluetoothGattCallback));
+
+ }
+ // log ios exposure
+ logIosExposure(device.getAddress(), result.getRssi());
+ }
+ }
+ }
+ }
+ }
+ };
+
+ //endregion
+
+ //region Flutter Start Result
+
+ private void checkStarted() {
+ if (serverStarted && clientStarted) {
+ start();
+ if (startedResult != null) {
+ MethodChannel.Result result = startedResult;
+ startedResult = null;
+ result.success(true);
+ }
+ }
+ }
+
+ //endregion
+
+ //region RPI timer
+
+ private void startRpiTimer() {
+ long refreshIntervalInMillis = RPI_REFRESH_INTERVAL_SECS * 1000;
+ stopRpiTimer();
+ rpiTimer = new Timer();
+ rpiTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ refreshRpi();
+ }
+ }, refreshIntervalInMillis, refreshIntervalInMillis);
+ }
+
+ private void stopRpiTimer() {
+ if (rpiTimer != null) {
+ rpiTimer.cancel();
+ }
+ rpiTimer = null;
+ }
+
+ //endregion
+
+ //region Exposures timer
+
+ private void updateExposuresTimer() {
+ int exposuresCount = androidExposures.size() + iosExposures.size();
+ if ((exposuresCount > 0) && (exposuresTimer == null)) {
+ exposuresTimer = new Timer();
+ exposuresTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ processExposures();
+ }
+ }, exposureProcessIntervalInMillis, exposureProcessIntervalInMillis);
+ } else if ((exposuresCount == 0) && (exposuresTimer != null)) {
+ exposuresTimer.cancel();
+ exposuresTimer = null;
+ }
+ }
+
+ //endregion
+
+ //region MethodCall
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ String method = call.method;
+ try {
+ switch (method) {
+ case Constants.EXPOSURE_PLUGIN_METHOD_NAME_START:
+ Object settings = call.argument(Constants.EXPOSURE_PLUGIN_SETTINGS_PARAM_NAME);
+ handleStart(result, settings); // Result is handled on a latter step
+ break;
+ case Constants.EXPOSURE_PLUGIN_METHOD_NAME_STOP:
+ handleStop();
+ result.success(true);
+ break;
+ case Constants.EXPOSURE_PLUGIN_METHOD_NAME_TEKS:
+ boolean removeTeks = Utils.Map.getValueFromPath(call.arguments, "remove", false);
+ if (removeTeks) {
+ saveTeksToStorage(null);
+ result.success(null);
+ } else {
+ List> teksList = getTeksList();
+ result.success(teksList);
+ }
+ break;
+ case Constants.EXPOSURE_PLUGIN_METHOD_NAME_TEK_RPIS:
+ Object parameters = call.arguments;
+ String tekString = Utils.Map.getValueFromPath(parameters, Constants.EXPOSURE_PLUGIN_TEK_PARAM_NAME, null);
+ byte[] tek = Utils.Base64.decode(tekString);
+ long timestamp = Utils.Map.getValueFromPath(parameters, Constants.EXPOSURE_PLUGIN_TIMESTAMP_PARAM_NAME, -1L);
+ long expireTime = Utils.Map.getValueFromPath(parameters, Constants.EXPOSURE_PLUGIN_TEK_EXPIRE_PARAM_NAME, -1L);
+ Map rpis = getRpisForTek(tek, timestamp, expireTime);
+ result.success(rpis);
+ break;
+ case Constants.EXPOSURE_PLUGIN_METHOD_NAME_EXPIRE_TEK:
+ changeTekExpireTime();
+ result.success(null);
+ break;
+ default:
+ result.success(null);
+ break;
+
+ }
+ } catch (IllegalStateException exception) {
+ String errorMsg = String.format("Ignoring exception '%s'. See https://github.com/flutter/flutter/issues/29092 for details.", exception.toString());
+ Log.e(TAG, errorMsg);
+ exception.printStackTrace();
+ }
+ }
+
+ private void invokeFlutterMethod(String methodName, Object arguments) {
+ if (methodChannel != null) {
+ // Run on the ui thread
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(() -> methodChannel.invokeMethod(methodName, arguments));
+ }
+ }
+
+ //endregion
+
+ // region Bluetooth
+
+ public void onLocationPermissionGranted() {
+ Log.d(TAG, "onLocationPermissionGranted");
+ if (androidExposureClient != null) {
+ androidExposureClient.onLocationPermissionGranted();
+ }
+ }
+
+ private void requestBluetoothOn() {
+ Log.d(TAG, "requestBluetoothOn");
+
+ Utils.showDialog(activityContext, activityContext.getString(R.string.app_name),
+ activityContext.getString(R.string.exposure_request_bluetooth_on_message),
+ (dialog, which) -> {
+ //Turn bluetooth on
+ Utils.enabledBluetooth();
+
+ }, "Yes",
+ (dialog, which) -> {
+ }, "No",
+ true);
+ }
+
+ //endregion
+
+ //region Helpers
+
+ private StringBuilder byte_to_hex(byte[] byte_array) {
+ StringBuilder sb = new StringBuilder();
+ if (byte_array != null) {
+ for (byte b : byte_array) {
+ sb.append(String.format("%02X ", b));
+ }
+ }
+ return sb;
+ }
+
+ //endregion
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ExposureRecord.java b/android/app/src/main/java/edu/illinois/covid/exposure/ExposureRecord.java
new file mode 100644
index 00000000..d94f5adf
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ExposureRecord.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure;
+
+import edu.illinois.covid.Constants;
+
+class ExposureRecord {
+ private long timestampCreated;
+ private long timestampUpdated;
+ private int lastRssi;
+ private long durationInterval;
+
+ ExposureRecord(long timestamp, int rssi) {
+ this.timestampCreated = timestamp;
+ this.timestampUpdated = timestamp;
+ this.lastRssi = rssi;
+ this.durationInterval = 0;
+ }
+
+ void updateTimeStamp(long timestamp, int rssi) {
+ int rssiMinValue = Constants.EXPOSURE_MIN_RSSI_VALUE;
+ if (ExposurePlugin.getInstance() != null) {
+ rssiMinValue = ExposurePlugin.getInstance().getExposureMinRssi();
+ }
+ if ((rssiMinValue <= lastRssi) && (lastRssi != Constants.EXPOSURE_NO_RSSI_VALUE)) {
+ durationInterval += (timestamp - timestampUpdated);
+ }
+ lastRssi = rssi;
+ timestampUpdated = timestamp;
+ }
+
+ /**
+ * @return duration in milliseconds
+ */
+ long getDuration() {
+ return durationInterval;
+ }
+
+ long getTimestampCreated() {
+ return timestampCreated;
+ }
+
+ long getTimestampUpdated() {
+ return timestampUpdated;
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java
new file mode 100644
index 00000000..fa1098b0
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureClient.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.ble;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import androidx.core.content.ContextCompat;
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.R;
+import edu.illinois.covid.Utils;
+import edu.illinois.covid.exposure.ble.scan.OreoScanner;
+import edu.illinois.covid.exposure.ble.scan.PreOreoScanner;
+
+public class ExposureClient extends Service {
+ private static final String TAG = "ExposureClient";
+
+ public class LocalBinder extends Binder {
+ public ExposureClient getService() {
+ return ExposureClient.this;
+ }
+ }
+ private final IBinder mBinder = new LocalBinder();
+
+ private BluetoothAdapter mBluetoothAdapter;
+
+ private PreOreoScanner preOreoScanner;
+ private OreoScanner oreoScanner;
+
+ private RpiCallback rpiCallback;
+
+ private AtomicBoolean waitBluetoothOn = new AtomicBoolean(false);
+ private AtomicBoolean waitLocationPermissionGranted = new AtomicBoolean(false);
+
+ private AtomicBoolean isScanning = new AtomicBoolean(false);
+
+ private Object settings;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onCreate() {
+ Object systemService = getSystemService(Context.BLUETOOTH_SERVICE);
+ if (systemService instanceof BluetoothManager) {
+ mBluetoothAdapter = ((BluetoothManager) systemService).getAdapter();
+ }
+ if (mBluetoothAdapter == null) {
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ oreoScanner = new OreoScanner(getApplicationContext(), mBluetoothAdapter);
+ } else {
+ preOreoScanner = new PreOreoScanner(mBluetoothAdapter);
+ }
+ startForegroundClientService();
+ IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ registerReceiver(bluetoothReceiver, filter);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(TAG, "onStartCommand - " + startId);
+
+ if (intent != null) {
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ Object found = extras.get(Constants.EXPOSURE_BLE_DEVICE_FOUND);
+ if (found != null) {
+ if (found instanceof ScanResult) {
+ ScanResult scanResult = ((ScanResult) found);
+ onScanResultFound(scanResult);
+ } else {
+ Log.d(TAG, "found is not ScanResult");
+ }
+ } else {
+ Log.d(TAG, "found is null");
+ }
+ } else {
+ Log.d(TAG, "extras are null");
+ }
+ } else {
+ Log.d(TAG, "The intent is null");
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ stopForeground(true);
+ stopSelf();
+ unregisterReceiver(bluetoothReceiver);
+ }
+
+ public void setRpiCallback(RpiCallback rpiCallback) {
+ this.rpiCallback = rpiCallback;
+ }
+
+ @SuppressLint("NewApi")
+ public void startScan() {
+ Log.d(TAG, "startScan");
+
+ //check if bluetooth is on
+ boolean needsWaitBluetooth = needsWaitBluetooth();
+ if (needsWaitBluetooth) {
+ waitBluetoothOn.set(true);
+ return;
+ }
+ waitBluetoothOn.set(false);
+
+ //check if location permission is granted
+ boolean needsWaitLocationPermission = needsWaitLocationPermission();
+ if (needsWaitLocationPermission) {
+ waitLocationPermissionGranted.set(true);
+ return;
+ }
+ waitLocationPermissionGranted.set(false);
+
+ startForegroundClientService();
+ isScanning.set(true);
+
+ if (preOreoScanner != null) {
+ preOreoScanner.startScan(new PreOreoScanner.ScannerCallback() {
+ @Override
+ public void onDevice(ScanResult result) {
+ super.onDevice(result);
+ onScanResultFound(result);
+ }
+ });
+ }
+ if (oreoScanner != null) {
+ oreoScanner.startScan();
+ }
+ }
+
+ private boolean needsWaitBluetooth() {
+ if ((mBluetoothAdapter == null) || !mBluetoothAdapter.isEnabled()) {
+ Log.d(TAG, "processBluetoothCheck needs to wait for bluetooth");
+ return true;
+ } else {
+ Log.d(TAG, "processBluetoothCheck - bluetooth ready");
+ }
+ return false;
+ }
+
+ private boolean needsWaitLocationPermission() {
+ if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "needsWaitLocationPermission - location is not set");
+ return true;
+ } else {
+ Log.d(TAG, "needsWaitLocationPermission - location ready");
+ }
+ return false;
+ }
+
+ @SuppressLint("NewApi")
+ public void stopScan() {
+ Log.d(TAG, "stopScan");
+
+ if (preOreoScanner != null) {
+ preOreoScanner.stopScan();
+ }
+ if (oreoScanner != null) {
+ oreoScanner.stopScan();
+ }
+ waitBluetoothOn.set(false);
+ waitLocationPermissionGranted.set(false);
+
+ stopForeground(true);
+ stopSelf();
+ isScanning.set(false);
+ }
+
+ public void onLocationPermissionGranted() {
+ Log.d(TAG, "onLocationPermissionGranted");
+
+ //start the advertising if it waits for location
+ Handler handler = new Handler(Looper.getMainLooper());
+ Runnable runnable = () -> {
+ if (waitLocationPermissionGranted.get()) {
+ startScan();
+ }
+ };
+ handler.postDelayed(runnable, 2000);
+ }
+
+ public void initSettings(Object settings) {
+ this.settings = settings;
+ }
+
+ private void onScanResultFound(ScanResult scanResult) {
+ if (scanResult == null) {
+ Log.d(TAG, "onScanResultFound: result is null");
+ return;
+ }
+ ScanRecord scanRecord = scanResult.getScanRecord();
+ if (scanRecord == null) {
+ Log.d(TAG, "onScanResultFound: ScanRecord is null for ScanResult: " + scanResult.toString());
+ return;
+ }
+
+ // Android - check service data
+ byte[] possibleRpi = scanRecord.getServiceData(Constants.EXPOSURE_PARCEL_SERVICE_UUID);
+
+ if ((possibleRpi != null) && possibleRpi.length == Constants.EXPOSURE_CONTRACT_NUMBER_LENGTH) {
+ Log.d(TAG, "onScanResultFound: Bytes are found in Android device!");
+ String rpiEncoded = Utils.Base64.encode(possibleRpi);
+ String deviceAddress = (scanResult.getDevice() != null ? scanResult.getDevice().getAddress() : "");
+ Log.d(TAG, "onScanResultFound: rpiFound: " + rpiEncoded + " from device address: " + deviceAddress);
+ if (rpiCallback != null) {
+ rpiCallback.onRpiFound(possibleRpi, scanResult.getRssi(), deviceAddress);
+ }
+ } else if (possibleRpi == null) {
+ // this could be an ios device
+ Log.d(TAG, "onScanResultFound: might be ios: " + scanResult.getDevice().getAddress());
+ rpiCallback.onIOSDeviceFound(scanResult);
+ }
+ }
+
+ //region BroadcastReceiver
+
+ private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if ((action != null) && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+ final int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.ERROR);
+ if (bluetoothState == BluetoothAdapter.STATE_ON) {
+ Log.d(TAG, "Bluetooth is on");
+ //start the advertising if it waits for bluetooth
+ Handler handler = new Handler(Looper.getMainLooper());
+ Runnable runnable = () -> {
+ if (waitBluetoothOn.get()) {
+ startScan();
+ }
+ };
+ handler.postDelayed(runnable, 2000);
+ }
+ }
+ }
+ };
+
+ //endregion
+
+ //region Foreground service
+
+ private void createNotificationChannelIfNeeded() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ CharSequence name = getString(R.string.app_name);
+ String description = getString(R.string.exposure_notification_channel_description);
+ int importance = NotificationManager.IMPORTANCE_DEFAULT;
+ NotificationChannel channel = new NotificationChannel(
+ NotificationCreator.getChannelId(), name, importance);
+ channel.setDescription(description);
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+ }
+
+ private void startForegroundClientService() {
+ boolean exposureServiceLocalNotificationEnabled = Utils.Map.getValueFromPath(settings, "covid19ExposureServiceLocalNotificationEnabledAndroid", false);
+ if (exposureServiceLocalNotificationEnabled) {
+ createNotificationChannelIfNeeded();
+ startForeground(NotificationCreator.getOngoingNotificationId(),
+ NotificationCreator.getNotification(this));
+ }
+ }
+
+
+ //endregion
+
+ //region RpiCallback
+
+ public static class RpiCallback {
+ public void onRpiFound(byte[] rpi, int rssi, String address) {}
+ public void onIOSDeviceFound(ScanResult scanResult){}
+ }
+
+ //endregion
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureServer.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureServer.java
new file mode 100644
index 00000000..a06fa94e
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/ExposureServer.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.ble;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import androidx.annotation.Nullable;
+import edu.illinois.covid.BuildConfig;
+import edu.illinois.covid.Constants;
+
+public class ExposureServer extends Service {
+
+ //region Member fields
+
+ private static final String TAG = ExposureServer.class.getSimpleName();
+
+ public class LocalServerBinder extends Binder {
+ public ExposureServer getService() {
+ return ExposureServer.this;
+ }
+ }
+
+ private final IBinder binder = new LocalServerBinder();
+
+ private BluetoothAdapter bluetoothAdapter;
+
+ private AtomicBoolean isAdvertising = new AtomicBoolean(false);
+ private AtomicBoolean waitBluetoothOn = new AtomicBoolean(false);
+
+ private byte[] rpi;
+
+ private Callback callback;
+
+ //endregion
+
+ //region Service implementation
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ @Override
+ public void onCreate() {
+ Object systemService = getSystemService(Context.BLUETOOTH_SERVICE);
+ if (systemService instanceof BluetoothManager) {
+ bluetoothAdapter = ((BluetoothManager) systemService).getAdapter();
+ }
+ if (bluetoothAdapter == null) {
+ Log.d(TAG, "onCreate: bluetoothAdapter is null");
+ return;
+ }
+ IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ registerReceiver(bluetoothReceiver, filter);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; }
+
+ @Override
+ public void onDestroy() {
+ stopAdvertising();
+ unregisterReceiver(bluetoothReceiver);
+ }
+
+ //endregion
+
+ //region Public APIs
+
+ public void start() {
+ Log.d(TAG, "start server");
+
+ if (bluetoothAdapter == null) {
+ Log.d(TAG, "start - bluetoothAdapter is null");
+ return;
+ }
+
+ // ask for bluetooth if not set
+ if (!bluetoothAdapter.isEnabled()) {
+ if (callback != null)
+ callback.onRequestBluetoothOn();
+ }
+ startAdvertising();
+ }
+
+ public void stop() {
+ Log.d(TAG, "stop server");
+ stopAdvertising();
+ }
+
+ public void setRpi(byte[] rpi) {
+ Log.d(TAG, "set rpi");
+ this.rpi = rpi;
+ if (isAdvertising.get()) {
+ stopAdvertising();
+ startAdvertising();
+ }
+ }
+
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ //endregion
+
+ //region Advertising
+
+ private void startAdvertising() {
+ if ((rpi == null)) {
+ Log.d(TAG, "startAdvertising: rpi is null! Advertising not started!");
+ return;
+ }
+ if (!bluetoothAdapter.isEnabled()) {
+ Log.d(TAG, "startAdvertising: wait for bluetooth to be enabled");
+ waitBluetoothOn.set(true);
+ return;
+ }
+ waitBluetoothOn.set(false);
+
+ BluetoothLeAdvertiser advertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
+ if (advertiser == null) {
+ Log.w(TAG, "Device does not support BLE advertisement");
+ showToast("Exposure: This device does not support BLE advertisement");
+ return;
+ }
+ showToast("Exposure: Start advertising");
+
+ // Use try catch to handle DeadObject exception
+ try {
+ AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
+ settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED);
+ settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM);
+ settingsBuilder.setConnectable(true);
+ settingsBuilder.setTimeout(0);
+
+ ParcelUuid parceluuid = new ParcelUuid(Constants.EXPOSURE_UUID_SERVICE);
+ AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
+ dataBuilder.setIncludeDeviceName(false);
+ dataBuilder.addServiceUuid(parceluuid);
+ dataBuilder.addServiceData(parceluuid, rpi);
+
+ AdvertiseSettings advertiseSettings = settingsBuilder.build();
+ AdvertiseData advertiseData = dataBuilder.build();
+ advertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback);
+ } catch (Exception ex) {
+ String errMsg = "Exposure: start advertising failed!";
+ Log.e(TAG, errMsg);
+ ex.printStackTrace();
+ showToast(errMsg);
+ // re-try
+ startAdvertising();
+ }
+ }
+
+ private void stopAdvertising() {
+ showToast("Exposure: Stop advertising");
+ waitBluetoothOn.set(false);
+ if (bluetoothAdapter != null) {
+ BluetoothLeAdvertiser advertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
+ if (advertiser != null) {
+ advertiser.stopAdvertising(advertiseCallback);
+ }
+ }
+ stopForeground(true);
+ isAdvertising.set(false);
+ }
+
+ private AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
+
+ @Override
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ super.onStartSuccess(settingsInEffect);
+ Log.d(TAG, "AdvertiseCallback onStartSuccess ");
+
+ showToast("Exposure: Start advertising Succeed");
+
+ isAdvertising.set(true);
+ }
+
+ @Override
+ public void onStartFailure(int errorCode) {
+ super.onStartFailure(errorCode);
+ Log.e(TAG, "AdvertiseCallback onStartFailure " + errorCode);
+
+ showToast("Exposure: Start advertising failed " + errorCode);
+
+ isAdvertising.set(false);
+ }
+ };
+
+ /**
+ * Show toasts only for DEBUG builds
+ * @param message the message that has to be shown
+ */
+ private void showToast(String message) {
+ if (BuildConfig.DEBUG) {
+ new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show());
+ }
+ }
+
+ //endregion
+
+ //region Bluetooth Receiver
+
+ private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if ((action != null) && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+ final int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ if (bluetoothState == BluetoothAdapter.STATE_ON) {
+ Log.d(TAG, "Bluetooth is on");
+ //start the advertising if it waits for bluetooth
+ Handler handler = new Handler(Looper.getMainLooper());
+ Runnable runnable = () -> {
+ if (waitBluetoothOn.get()) {
+ startAdvertising();
+ }
+ };
+ handler.postDelayed(runnable, 2000);
+ }
+ }
+ }
+ };
+
+ //endregion
+
+ //region Exposure Server Callback
+
+ public static class Callback {
+ public void onRequestBluetoothOn() {}
+ }
+
+ //endregion
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java
new file mode 100644
index 00000000..ec77f62f
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/NotificationCreator.java
@@ -0,0 +1,47 @@
+package edu.illinois.covid.exposure.ble;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.core.app.NotificationCompat;
+import edu.illinois.covid.MainActivity;
+import edu.illinois.covid.R;
+
+class NotificationCreator {
+ private static final int ONGOING_NOTIFICATION_ID = 1;
+ private static final String CHANNEL_ID = "RokwireContactTracingNotificationChannel";
+ private static Notification notification;
+
+ static Notification getNotification(Context context) {
+ if (context != null) {
+ if (notification == null) {
+ Intent notificationIntent = new Intent(context, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ notificationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setContentTitle(context.getString(R.string.exposure_notification_title))
+ .setContentText(context.getString(R.string.exposure_notification_message))
+ .setSmallIcon(R.drawable.app_icon)
+ .setContentIntent(pendingIntent)
+ .setTicker(context.getString(R.string.exposure_notification_ticker));
+
+ notification = builder.build();
+ }
+ }
+ return notification;
+ }
+
+ static String getChannelId() {
+ return CHANNEL_ID;
+ }
+
+ static int getOngoingNotificationId() {
+ return ONGOING_NOTIFICATION_ID;
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/ExposureBleReceiver.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/ExposureBleReceiver.java
new file mode 100644
index 00000000..c86fae37
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/ExposureBleReceiver.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.ble.scan;
+
+import android.annotation.TargetApi;
+import android.bluetooth.le.ScanResult;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.exposure.ble.ExposureClient;
+
+public class ExposureBleReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "ExposureBleReceiver";
+
+ @TargetApi(Build.VERSION_CODES.O)
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ Log.d(TAG, "onReceive - intent is null");
+ return;
+ }
+ Log.d(TAG, "onReceive - " + intent.getAction());
+ if (Constants.EXPOSURE_BLE_ACTION_FOUND.equals(intent.getAction())) {
+ ScanResult scanResult = extractData(intent.getExtras());
+ if (scanResult == null) {
+ Log.d(TAG, "The scan result is null");
+ }
+ Intent bleClientIntent = new Intent(context, ExposureClient.class);
+ bleClientIntent.putExtra(Constants.EXPOSURE_BLE_DEVICE_FOUND, scanResult);
+ context.startService(bleClientIntent);
+ }
+ }
+
+ private ScanResult extractData(Bundle extras) {
+ if (extras != null) {
+ Object list = extras.get("android.bluetooth.le.extra.LIST_SCAN_RESULT");
+ if (list != null) {
+ ArrayList l = (ArrayList) list;
+ if (l.size() > 0) {
+ Object firstItem = l.get(0);
+ if (firstItem instanceof ScanResult) {
+ return (ScanResult) firstItem;
+ } else {
+ Log.d(TAG, "first item is not ScanResult");
+ }
+ } else {
+ Log.d(TAG, "list is empty");
+ }
+ } else {
+ Log.d(TAG, "list is null");
+ }
+ } else {
+ Log.d(TAG, "extras are null");
+ }
+ return null;
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/OreoScanner.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/OreoScanner.java
new file mode 100644
index 00000000..cea2acae
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/OreoScanner.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.ble.scan;
+
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import androidx.annotation.RequiresApi;
+import edu.illinois.covid.Constants;
+
+public class OreoScanner {
+
+ private static final String TAG = "OreoScanner";
+
+ private Context context;
+ private BluetoothAdapter bluetoothAdapter;
+
+ private PendingIntent pendingIntent;
+
+ public OreoScanner(Context context, BluetoothAdapter bluetoothAdapter) {
+ this.context = context;
+ this.bluetoothAdapter = bluetoothAdapter;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void startScan() {
+ Log.d(TAG, "Started scan");
+ try {
+ ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder();
+ scanFilterBuilder.setServiceUuid(new ParcelUuid(Constants.EXPOSURE_UUID_SERVICE));
+ List scanFilters = Collections.singletonList(scanFilterBuilder.build());
+ long reportDelay = ((bluetoothAdapter != null) && bluetoothAdapter.isOffloadedScanBatchingSupported()) ? 5 : 0;
+
+ ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder().
+ setScanMode(ScanSettings.SCAN_MODE_LOW_POWER).
+ setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES).
+ setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE).
+ setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT).
+ setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED).
+ setReportDelay(TimeUnit.SECONDS.toMillis(reportDelay)).
+ setLegacy(true);
+
+ ScanSettings scanSettings = scanSettingsBuilder.build();
+
+ Intent intent = new Intent(context, ExposureBleReceiver.class);
+ intent.setAction(Constants.EXPOSURE_BLE_ACTION_FOUND);
+ pendingIntent = PendingIntent.getBroadcast(context, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ if (bluetoothAdapter != null) {
+ bluetoothAdapter.getBluetoothLeScanner().startScan(scanFilters, scanSettings, pendingIntent);
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Start scan failed:");
+ ex.printStackTrace();
+ //re-try
+ startScan();
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public void stopScan() {
+ if ((pendingIntent != null) && (bluetoothAdapter != null)) {
+ bluetoothAdapter.getBluetoothLeScanner().stopScan(pendingIntent);
+ pendingIntent = null;
+ }
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/PreOreoScanner.java b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/PreOreoScanner.java
new file mode 100644
index 00000000..6256c8ed
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/ble/scan/PreOreoScanner.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.ble.scan;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import edu.illinois.covid.Constants;
+
+public class PreOreoScanner {
+
+ private static final String TAG = PreOreoScanner.class.getSimpleName();
+
+ private BluetoothAdapter bluetoothAdapter;
+
+ private ScannerCallback discoverCallback;
+
+ public PreOreoScanner(BluetoothAdapter bluetoothAdapter) {
+ this.bluetoothAdapter = bluetoothAdapter;
+ }
+
+ public void startScan(ScannerCallback callback) {
+ discoverCallback = callback;
+
+ // Use try catch to handle DeadObject exception
+ try {
+ ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder();
+ scanFilterBuilder.setServiceUuid(new ParcelUuid(Constants.EXPOSURE_UUID_SERVICE));
+ List scanFilters = Collections.singletonList(scanFilterBuilder.build());
+ long reportDelay = ((bluetoothAdapter != null) && bluetoothAdapter.isOffloadedScanBatchingSupported()) ? 5 : 0;
+
+ ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder().
+ setScanMode(ScanSettings.SCAN_MODE_LOW_POWER).
+ setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES).
+ setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE).
+ setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT).
+ setReportDelay(TimeUnit.SECONDS.toMillis(reportDelay));
+
+ ScanSettings scanSettings = scanSettingsBuilder.build();
+ if (bluetoothAdapter != null) {
+ bluetoothAdapter.getBluetoothLeScanner().startScan(scanFilters, scanSettings, scanCallback);
+ }
+ Log.d(TAG, "Started scan");
+ } catch (Exception ex) {
+ Log.e(TAG, "Start scan failed:");
+ ex.printStackTrace();
+ // re-try
+ startScan(callback);
+ }
+ }
+
+ public void stopScan() {
+ if (discoverCallback != null) {
+ bluetoothAdapter.getBluetoothLeScanner().stopScan(scanCallback);
+ discoverCallback = null;
+ }
+ }
+
+ private void onResult(final ScanResult result) {
+ if (discoverCallback != null)
+ discoverCallback.onDevice(result);
+ }
+
+ private ScanCallback scanCallback = new ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, final ScanResult result) {
+ super.onScanResult(callbackType, result);
+ onResult(result);
+ }
+
+ @Override
+ public void onBatchScanResults(List results) {
+ super.onBatchScanResults(results);
+ for (ScanResult result : results) {
+ onResult(result);
+ }
+ }
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ super.onScanFailed(errorCode);
+ Log.e(TAG, "onScanFailed errorCode = " + errorCode);
+ if (errorCode == SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) {
+ // re-try
+ startScan(discoverCallback);
+ }
+ }
+
+ };
+
+ //ScannerCallback
+
+ public static abstract class ScannerCallback {
+ public void onDevice(ScanResult result) {
+ }
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES.java b/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES.java
new file mode 100644
index 00000000..3ebf6eec
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.crypto;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AES {
+
+ private static SecretKeySpec secretKey;
+ private static byte[] key;
+
+ public static void setKey(byte[] myKey) {
+ key = myKey;
+ secretKey = new SecretKeySpec(key, "AES");
+ }
+
+ public static byte[] encrypt(byte[] strToEncrypt, byte[] secret) {
+ try {
+ setKey(strToEncrypt);
+ Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+ return (cipher.doFinal(secret));
+ } catch (Exception e) {
+ System.out.println("Error while encrypting: " + e.toString());
+ }
+ return null;
+ }
+
+ public static byte[] decrypt(byte[] strToDecrypt, byte[] secret) {
+ try {
+ setKey(strToDecrypt);
+ Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+ return (cipher.doFinal(secret));
+ } catch (Exception e) {
+ System.out.println("Error while decrypting: " + e.toString());
+ }
+ return null;
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES_CTR.java b/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES_CTR.java
new file mode 100644
index 00000000..2d75dc9d
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/exposure/crypto/AES_CTR.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.exposure.crypto;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AES_CTR {
+ public static String ALGORITHM = "AES";
+ private static String AES_CBS_PADDING = "AES/CTR/NoPadding";
+
+ public static byte[] encrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception {
+ return AES_CTR.encryptDecrypt(Cipher.ENCRYPT_MODE, key, IV, message);
+ }
+
+ public static byte[] decrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception {
+ return AES_CTR.encryptDecrypt(Cipher.DECRYPT_MODE, key, IV, message);
+ }
+
+ private static byte[] encryptDecrypt(final int mode, final byte[] key, final byte[] IV, final byte[] message)
+ throws Exception {
+ final Cipher cipher = Cipher.getInstance(AES_CBS_PADDING);
+ final SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
+ final IvParameterSpec ivSpec = new IvParameterSpec(IV);
+ cipher.init(mode, keySpec, ivSpec);
+ return cipher.doFinal(message);
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/gallery/GalleryPlugin.java b/android/app/src/main/java/edu/illinois/covid/gallery/GalleryPlugin.java
new file mode 100644
index 00000000..d8b9de24
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/gallery/GalleryPlugin.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.gallery;
+
+
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.MainActivity;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+
+public class GalleryPlugin implements MethodChannel.MethodCallHandler {
+
+ private static final String TAG = "GalleryPlugin";
+ public static int STORAGE_PERMISSION_REQUEST_CODE = 100;
+
+ private static GalleryPlugin instance;
+ private final MainActivity activityContext;
+ private final MethodChannel methodChannel;
+
+ // Temp storage between permission request
+ private byte[] bytes;
+ private String name;
+ private MethodChannel.Result channelResult;
+
+ public static GalleryPlugin registerWith(PluginRegistry.Registrar registrar) {
+ final MethodChannel channel = new MethodChannel(registrar.messenger(), "edu.illinois.covid/gallery");
+ GalleryPlugin galleryPlugin = new GalleryPlugin((MainActivity) registrar.activity(), channel);
+ channel.setMethodCallHandler(galleryPlugin);
+ if (instance == null) {
+ instance = galleryPlugin;
+ }
+ return galleryPlugin;
+ }
+
+ private GalleryPlugin(MainActivity activity, MethodChannel methodChannel) {
+ this.activityContext = activity;
+ this.methodChannel = methodChannel;
+ this.methodChannel.setMethodCallHandler(this);
+
+ }
+
+ private void handleStore() {
+ try {
+ if (hasWriteStoragePermission()) {
+ handleStore(this.bytes, this.name, this.channelResult, false);
+ }
+ } finally {
+ clearCache();
+ }
+ }
+
+ private void handleStore(byte[] bytes, String name, @NonNull MethodChannel.Result result) {
+ handleStore(bytes, name, result, true);
+ }
+
+ private void handleStore(byte[] bytes, String name, @NonNull MethodChannel.Result result, boolean performedPermissionCheck) {
+ if (performedPermissionCheck) {
+ if(!hasWriteStoragePermission()) {
+ this.bytes = bytes;
+ this.name = name;
+ this.channelResult = result;
+ requestWriteStoragePermission();
+ return;
+ }
+ }
+ else {
+ if(!hasWriteStoragePermission()){
+ result.success(Boolean.FALSE);
+ return;
+ }
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= 29) {
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/");
+ values.put(MediaStore.Images.Media.IS_PENDING, true);
+ Uri uri = activityContext.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+ if (uri != null) {
+ saveBytesToUri(bytes, uri);
+ values.put(MediaStore.Images.Media.IS_PENDING, false);
+ activityContext.getContentResolver().update(uri, values, null, null);
+ result.success(Boolean.TRUE);
+ return;
+ }
+ } else {
+ String storagePath = Environment.getExternalStorageDirectory().toString();
+ storagePath = storagePath.endsWith("/") ? storagePath : storagePath + "/";
+ File directory = new File( storagePath + "Pictures/");
+ if (!directory.exists()) {
+ directory.mkdirs();
+ }
+ String fileName = System.currentTimeMillis() + ".png";
+ File file = new File(directory, fileName);
+ saveBytesToUri(bytes, Uri.fromFile(file));
+ if (file.getAbsolutePath() != null) {
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
+ activityContext.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+ result.success(Boolean.TRUE);
+ return;
+ }
+ }
+ result.success(Boolean.FALSE);
+ }
+
+ private void saveBytesToUri(byte[] bytes, Uri uri){
+ if(bytes != null && uri != null) {
+ OutputStream out = null;
+ try {
+ out = activityContext.getContentResolver().openOutputStream(uri);
+ if (out != null) {
+ out.write(bytes);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, String.format("Error on write %s", uri.toString()), e);
+ }
+ finally {
+ if(out != null){
+ try {
+ out.flush();
+ } catch (IOException e) {
+ Log.e(TAG, String.format("Error on write %s", uri.toString()), e);
+ }
+ try {
+ out.close();
+ } catch (IOException e) {
+ Log.e(TAG, String.format("Error on write %s", uri.toString()), e);
+ }
+ }
+ }
+ }
+ }
+
+ private void clearCache(){
+ this.bytes = null;
+ this.name = null;
+ this.channelResult = null;
+ }
+
+ private boolean hasWriteStoragePermission() {
+ return activityContext.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private void requestWriteStoragePermission() {
+ activityContext.requestPermissions(new String[] {android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_REQUEST_CODE);
+ }
+
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if(requestCode == STORAGE_PERMISSION_REQUEST_CODE) {
+ for (int i = 0; i < permissions.length && i < grantResults.length; i++) {
+ String permission = permissions[i];
+ int result = grantResults[i];
+ if (android.Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission)){
+ handleStore();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ String method = call.method;
+ try {
+ switch (method) {
+ case Constants.GALLERY_PLUGIN_METHOD_NAME_STORE:
+ byte[] bytes = call.argument(Constants.GALLERY_PLUGIN_PARAM_BYTES);
+ String name = call.argument(Constants.GALLERY_PLUGIN_PARAM_NAME);
+ handleStore(bytes, name, result); // Result is handled on a latter step
+ break;
+ }
+ } catch (Exception e){
+ Log.e(TAG, "Error on reading command", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapActivity.java b/android/app/src/main/java/edu/illinois/covid/maps/MapActivity.java
new file mode 100644
index 00000000..4a1cd126
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapActivity.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+import android.graphics.Color;
+import android.location.Location;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.gms.location.FusedLocationProviderClient;
+import com.google.android.gms.location.LocationCallback;
+import com.google.android.gms.location.LocationResult;
+import com.google.android.gms.location.LocationServices;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.mapsindoors.mapssdk.MPPositionResult;
+import com.mapsindoors.mapssdk.MapControl;
+import com.mapsindoors.mapssdk.MapsIndoors;
+import com.mapsindoors.mapssdk.OnPositionUpdateListener;
+import com.mapsindoors.mapssdk.OnStateChangedListener;
+import com.mapsindoors.mapssdk.PermissionsAndPSListener;
+import com.mapsindoors.mapssdk.Point;
+import com.mapsindoors.mapssdk.PositionProvider;
+import com.mapsindoors.mapssdk.PositionResult;
+import com.mapsindoors.mapssdk.errors.MIError;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.R;
+import edu.illinois.covid.Utils;
+
+public class MapActivity extends AppCompatActivity implements PositionProvider {
+ //region Class fields
+
+ //Google Maps
+ private SupportMapFragment mapFragment;
+ protected GoogleMap googleMap;
+
+ //Android Location
+ private FusedLocationProviderClient fusedLocationClient;
+ protected Location coreLocation;
+ private com.google.android.gms.location.LocationRequest coreLocationRequest;
+ private LocationCallback coreLocationCallback;
+
+ //Location timer
+ private Timer locationTimer;
+ protected long locationTimestamp;
+
+ //MapsIndoors
+ protected MapControl mapControl;
+ protected MPPositionResult mpPositionResult;
+ private boolean isRunning;
+ private OnPositionUpdateListener mpPositionUpdateListener;
+
+ private boolean firstLocationUpdatePassed;
+ private HashMap target;
+ private HashMap options;
+ private ArrayList markers;
+ private TextView debugStatusView;
+ private boolean showDebugLocation;
+
+ //endregion
+
+ //region Activity methods
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.map_layout);
+
+ initHeaderBar();
+ initParameters();
+ initUiViews();
+ initCoreLocation();
+ initMap();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (mapControl != null) {
+ mapControl.onStart();
+ }
+ startMonitor();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mapControl != null) {
+ mapControl.onStop();
+ }
+ stopMonitor();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mapControl != null) {
+ mapControl.onResume();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mapControl != null) {
+ mapControl.onPause();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mapControl != null) {
+ mapControl.onDestroy();
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ if (mapControl != null) {
+ mapControl.onLowMemory();
+ }
+ }
+
+ /**
+ * Handle up (back) navigation button clicked
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ onBackPressed();
+ return true;
+ }
+
+ //endregion
+
+ //region Common initialization
+
+ private void initHeaderBar() {
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowHomeEnabled(true);
+ }
+ }
+
+ private void initParameters() {
+ Serializable targetSerializable = getIntent().getSerializableExtra("target");
+ if (targetSerializable instanceof HashMap) {
+ this.target = (HashMap) targetSerializable;
+ }
+ Serializable optionsSerializable = getIntent().getSerializableExtra("options");
+ if (optionsSerializable instanceof HashMap) {
+ this.options = (HashMap) optionsSerializable;
+ }
+ Serializable markersSerializable = getIntent().getSerializableExtra("markers");
+ if (markersSerializable instanceof List) {
+ this.markers = (ArrayList) markersSerializable;
+ }
+ }
+
+ protected void initUiViews() {
+ showDebugLocation = Utils.Map.getValueFromPath(options, "showDebugLocation", false);
+ if (showDebugLocation) {
+ debugStatusView = findViewById(R.id.debugStatusTextView);
+ debugStatusView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ //endregion
+
+ //region Map views initialization
+
+ private void initMap() {
+ mapFragment = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map_fragment));
+ if (mapFragment != null) {
+ mapFragment.getMapAsync(this::didGetMapAsync);
+ }
+ }
+
+ private void didGetMapAsync(GoogleMap map) {
+ googleMap = map;
+ double latitude = Utils.Map.getValueFromPath(target, "latitude", Constants.DEFAULT_INITIAL_CAMERA_POSITION.latitude);
+ double longitude = Utils.Map.getValueFromPath(target, "longitude", Constants.DEFAULT_INITIAL_CAMERA_POSITION.longitude);
+ double zoom = Utils.Map.getValueFromPath(target, "zoom", Constants.DEFAULT_CAMERA_ZOOM);
+ googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(CameraPosition.fromLatLngZoom(new LatLng(latitude, longitude), (float)zoom)));
+ initMapControl();
+ }
+
+ private void initMapControl() {
+ mapControl = new MapControl(this);
+ mapControl.setGoogleMap(googleMap, mapFragment.getView());
+ MapsIndoors.setPositionProvider(this);
+ mapControl.showUserPosition(true);
+ mapControl.setOnFloorUpdateListener((building, i) -> onFloorChanged(i));
+ mapControl.setOnMarkerClickListener(this::onMarkerClicked);
+ mapControl.addOnCameraIdleListener(this::onCameraIdle);
+ mapControl.init(this::mapControlDidInit);
+ }
+
+ private void mapControlDidInit(MIError error) {
+ Log.d(getLogTag(), "mapControlDidInit()");
+ runOnUiThread(() -> {
+ if (error == null) {
+ afterMapControlInitialized();
+ } else {
+ Log.d(getLogTag(), error.message);
+ }
+ });
+ }
+
+ protected void afterMapControlInitialized() {
+ mapControl.selectFloor(0);
+ boolean hideLevels = Utils.Map.getValueFromPath(options, "hideLevels", false);
+ mapControl.enableFloorSelector(!hideLevels);
+ startPositioning(null);
+ fillMarkers();
+ }
+
+ private void fillMarkers(){
+ if(markers!=null && !markers.isEmpty()){
+ for(HashMap markerData: markers){
+ Object latVal = markerData.get("latitude");
+ Object lngVal = markerData.get("longitude");
+
+ double lat = latVal instanceof Double? (double)latVal :
+ latVal instanceof Integer? Double.valueOf((int)latVal) : 0;
+ double lng = lngVal instanceof Double? (double)lngVal :
+ lngVal instanceof Integer? Double.valueOf((int)lngVal) : 0;
+ String name = markerData.containsKey("name")?(String) markerData.get("name") : "";
+ String description = markerData.containsKey("description") && markerData.get("description")!=null?(String) markerData.get("description") : "";
+
+ googleMap.addMarker(new MarkerOptions()
+ .position(new LatLng(lat, lng))
+ .title(name).snippet(description)).showInfoWindow();
+ }
+ }
+ }
+
+ //endregion
+
+ //region MapsIndoors
+
+ protected void onFloorChanged(int floor) {
+ Log.d(getLogTag(), "MapControl.onFloorUpdate: " + floor);
+ }
+
+ protected void onCameraIdle() {
+ Log.d(getLogTag(), "MapControl.onCameraIdle");
+ }
+
+ protected boolean onMarkerClicked(Marker marker) {
+ Log.d(getLogTag(), "MapControl.onMarkerClicked");
+ return false;
+ }
+
+ /**
+ * PositionProvider interface
+ */
+
+ @NonNull
+ @Override
+ public String[] getRequiredPermissions() {
+ return new String[0];
+ }
+
+ @Override
+ public boolean isPSEnabled() {
+ return true;
+ }
+
+ @Override
+ public void startPositioning(@Nullable String s) {
+ startMonitor();
+ }
+
+ @Override
+ public void stopPositioning(@Nullable String s) {
+ stopMonitor();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return isRunning;
+ }
+
+ @Override
+ public void addOnPositionUpdateListener(@Nullable OnPositionUpdateListener onPositionUpdateListener) {
+ this.mpPositionUpdateListener = onPositionUpdateListener;
+ }
+
+ @Override
+ public void removeOnPositionUpdateListener(@Nullable OnPositionUpdateListener onPositionUpdateListener) {
+ this.mpPositionUpdateListener = null;
+ }
+
+ @Override
+ public void setProviderId(@Nullable String s) {
+ Log.d(getLogTag(), "PositionProvider.setProviderId");
+ }
+
+ @Override
+ public void addOnStateChangedListener(@Nullable OnStateChangedListener onStateChangedListener) {
+ Log.d(getLogTag(), "PositionProvider.addOnStateChangedListener");
+ }
+
+ @Override
+ public void removeOnStateChangedListener(@Nullable OnStateChangedListener onStateChangedListener) {
+ Log.d(getLogTag(), "PositionProvider.removeOnStateChangedListener");
+ }
+
+ @Override
+ public void checkPermissionsAndPSEnabled(PermissionsAndPSListener permissionsAndPSListener) {
+ Log.d(getLogTag(), "PositionProvider.checkPermissionsAndPSEnabled");
+ }
+
+ @Nullable
+ @Override
+ public String getProviderId() {
+ Log.d(getLogTag(), "PositionProvider.getProviderId");
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public PositionResult getLatestPosition() {
+ return mpPositionResult;
+ }
+
+ @Override
+ public void startPositioningAfter(int i, @Nullable String s) {
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ startPositioning(s);
+ }
+ }, i);
+ }
+
+ @Override
+ public void terminate() {
+ Log.d(getLogTag(), "PositionProvider.terminate");
+ }
+
+ //endregion
+
+ //region Core Location
+
+ private void initCoreLocation() {
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
+ createCoreLocationCallback();
+ createCoreLocationRequest();
+ }
+
+ private void notifyCoreLocationUpdate() {
+ if (coreLocation != null) {
+ Point coreLocationPoint = new Point(coreLocation.getLatitude(), coreLocation.getLongitude(), 0);
+ MPPositionResult positionResult = new MPPositionResult(coreLocationPoint, 0, 0, 0);
+ positionResult.setProvider(this);
+ notifyLocationUpdate(positionResult, coreLocation.getTime());
+ }
+ }
+
+ private void createCoreLocationRequest() {
+ coreLocationRequest = com.google.android.gms.location.LocationRequest.create();
+ coreLocationRequest.setInterval(60000); //in millis
+ coreLocationRequest.setFastestInterval(30000); //in millis
+ coreLocationRequest.setPriority(com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY);
+ }
+
+ private void createCoreLocationCallback() {
+ coreLocationCallback = new LocationCallback() {
+ @Override
+ public void onLocationResult(LocationResult locationResult) {
+ if (locationResult == null) {
+ return;
+ }
+ for (Location location : locationResult.getLocations()) {
+ coreLocation = location;
+ }
+ notifyCoreLocationUpdate();
+ }
+ };
+ }
+
+ //endregion
+
+ //region Common Location
+
+ protected void notifyLocationUpdate(MPPositionResult positionResult, long timestamp) {
+ if (positionResult != null) {
+ mpPositionResult = positionResult;
+ locationTimestamp = timestamp;
+ if (mpPositionUpdateListener != null) {
+ mpPositionUpdateListener.onPositionUpdate(positionResult);
+ }
+ if (!firstLocationUpdatePassed) {
+ firstLocationUpdatePassed = true;
+ handleFirstLocationUpdate();
+ }
+ if ((debugStatusView != null) && showDebugLocation) {
+ String sourceAbbr = "CL";
+ int sourceColor = Color.rgb(0, 126, 0);
+ double lat = 0.0d;
+ double lng = 0.0d;
+ int floor = 0;
+ if (mpPositionResult.getPoint() != null) {
+ lat = mpPositionResult.getPoint().getLat();
+ lng = mpPositionResult.getPoint().getLng();
+ floor = mpPositionResult.getFloor();
+ }
+ debugStatusView.setText(String.format(Locale.getDefault(), "%s [%.6f, %.6f] @ %d", sourceAbbr, lat, lng, floor));
+ debugStatusView.setTextColor(sourceColor);
+ }
+ }
+ }
+
+ protected void notifyLocationFail() {
+
+ }
+
+ protected void handleFirstLocationUpdate() {
+
+ }
+
+ private void startMonitor() {
+ if (!isRunning) {
+ if (fusedLocationClient != null) {
+ fusedLocationClient.requestLocationUpdates(coreLocationRequest, coreLocationCallback, Looper.getMainLooper());
+ }
+ isRunning = true;
+ startLocationTimer();
+ }
+ }
+
+ private void stopMonitor() {
+ if (isRunning) {
+ stopLocationTimer();
+ if (fusedLocationClient != null) {
+ fusedLocationClient.removeLocationUpdates(coreLocationCallback);
+ }
+ isRunning = false;
+ }
+ }
+
+ private void startLocationTimer() {
+ stopLocationTimer();
+ locationTimer = new Timer();
+ locationTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ onLocationTimerTimeout();
+ }
+ }, (4000)); //4 secs
+ }
+
+ private void stopLocationTimer() {
+ if (locationTimer != null) {
+ locationTimer.cancel();
+ locationTimer = null;
+ }
+ }
+
+ protected void onLocationTimerTimeout() {
+ stopLocationTimer();
+ }
+
+ //endregion
+
+ //region Utilities
+
+ protected String getLogTag() {
+ return MapActivity.class.getSimpleName();
+ }
+
+ //endregion
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapDirectionsActivity.java b/android/app/src/main/java/edu/illinois/covid/maps/MapDirectionsActivity.java
new file mode 100644
index 00000000..464f4d80
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapDirectionsActivity.java
@@ -0,0 +1,958 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.location.Location;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.text.Html;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.android.gms.maps.model.PolygonOptions;
+import com.google.android.gms.maps.model.Polyline;
+import com.google.android.gms.maps.model.PolylineOptions;
+import com.google.maps.android.ui.IconGenerator;
+import com.mapsindoors.mapssdk.MPDirectionsRenderer;
+import com.mapsindoors.mapssdk.MPPositionResult;
+import com.mapsindoors.mapssdk.MPRoutingProvider;
+import com.mapsindoors.mapssdk.OnLegSelectedListener;
+import com.mapsindoors.mapssdk.OnRouteResultListener;
+import com.mapsindoors.mapssdk.Point;
+import com.mapsindoors.mapssdk.Route;
+import com.mapsindoors.mapssdk.RouteCoordinate;
+import com.mapsindoors.mapssdk.RouteLeg;
+import com.mapsindoors.mapssdk.RoutePolyline;
+import com.mapsindoors.mapssdk.RouteStep;
+import com.mapsindoors.mapssdk.TravelMode;
+import com.mapsindoors.mapssdk.errors.MIError;
+
+import org.json.JSONObject;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.MainActivity;
+import edu.illinois.covid.R;
+import edu.illinois.covid.Utils;
+
+public class MapDirectionsActivity extends MapActivity implements OnRouteResultListener, OnLegSelectedListener {
+
+ //region Class fields
+
+ //Explores - could be Event, Dining, Laundry or ParkingLotInventory
+ private Object explore;
+ private HashMap exploreLocation;
+ private Marker exploreMarker;
+ private IconGenerator iconGenerator;
+ private View markerLayoutView;
+ private View markerGroupLayoutView;
+ private float cameraZoom;
+
+ //Navigation
+ private Route mpRoute;
+ private CameraPosition cameraPosition;
+ private MPDirectionsRenderer mpDirectionsRenderer;
+ private MPRoutingProvider mpRoutingProvider;
+ private MIError mpRouteError;
+ private List mpRouteStepCoordCounts;
+ private Polyline routePolyline;
+ private NavStatus navStatus = NavStatus.UNKNOWN;
+ private boolean navAutoUpdate;
+ private int currentLegIndex = 0;
+ private int currentStepIndex = -1;
+ private boolean buildRouteAfterInitialization;
+
+ //Navigation UI
+ private static final String TRAVEL_MODE_PREFS_KEY = "directions.travelMode";
+ private static final String[] TRAVEL_MODES = {TravelMode.WALKING, TravelMode.BICYCLING,
+ TravelMode.DRIVING, TravelMode.TRANSIT};
+ private String selectedTravelMode;
+ private Map travelModesMap;
+ private View navRefreshButton;
+ private View navTravelModesContainer;
+ private View navAutoUpdateButton;
+ private View navPrevButton;
+ private View navNextButton;
+ private TextView navStepLabel;
+ private View routeLoadingFrame;
+
+ //endregion
+
+ //region Activity methods
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ initExplore();
+ buildTravelModes();
+ }
+
+ //endregion
+
+ //region Map views initialization
+
+ @Override
+ protected void afterMapControlInitialized() {
+ super.afterMapControlInitialized();
+ buildExploreMarker();
+ buildPolygon();
+ if (buildRouteAfterInitialization) {
+ buildRouteAfterInitialization = false;
+ buildRoute();
+ }
+ }
+
+ //endregion
+
+ //region MapsIndoors
+
+ @Override
+ protected boolean onMarkerClicked(Marker marker) {
+ Object exploreMarkerRawData = Utils.Explore.optExploreMarkerRawData(marker);
+ return (exploreMarkerRawData != null);
+ }
+
+ @Override
+ protected void onFloorChanged(int floor) {
+ super.onFloorChanged(floor);
+ updateExploreMarkerVisibility();
+ }
+
+ @Override
+ protected void onCameraIdle() {
+ super.onCameraIdle();
+ updateExploreMarkerAppearance();
+ }
+
+ /**
+ * OnRouteResultListener
+ */
+
+ @Override
+ public void onRouteResult(@Nullable Route route, @Nullable MIError miError) {
+ //DD: Workaround for multiple calling 'onRouteResult' from MapsIndoors sdk
+ if (mpRoutingProvider == null) {
+ return;
+ }
+ mpRoutingProvider.setOnRouteResultListener(null);
+ mpRoutingProvider = null;
+ mpRoute = route;
+ mpRouteError = miError;
+ runOnUiThread(this::didBuildRoute);
+ }
+
+ /**
+ * OnLegSelectedListener
+ */
+
+ @Override
+ public void onLegSelected(int i) {
+ Log.d(getLogTag(), "OnLegSelectedListener.onLegSelected");
+ }
+
+ //endregion
+
+ //region Common Location
+
+ @Override
+ protected void notifyLocationUpdate(MPPositionResult positionResult, long timestamp) {
+ super.notifyLocationUpdate(positionResult, timestamp);
+ if (positionResult != null) {
+ if ((navStatus == NavStatus.PROGRESS) && navAutoUpdate) {
+ updateNavByCurrentLocation();
+ }
+ }
+ }
+
+ @Override
+ protected void notifyLocationFail() {
+ super.notifyLocationFail();
+ if (mpPositionResult == null) {
+ handleFirstLocationUpdate();
+ }
+ }
+
+ @Override
+ protected void onLocationTimerTimeout() {
+ super.onLocationTimerTimeout();
+ if (coreLocation == null) {
+ runOnUiThread(() -> {
+ enableView(navPrevButton, false);
+ enableView(navNextButton, false);
+ showLoadingFrame(false);
+ showAlert(getString(R.string.locationFailedMsg));
+ });
+ }
+ }
+
+ //endregion
+
+ //region Explores
+
+ private void initExplore() {
+ Serializable exploreSeriazible = getIntent().getSerializableExtra("explore");
+ if (exploreSeriazible == null) {
+ return;
+ }
+ if (exploreSeriazible instanceof HashMap) {
+ HashMap singleExplore;
+ singleExplore = (HashMap) exploreSeriazible;
+ this.explore = singleExplore;
+ initExploreLocation(singleExplore);
+ } else if (exploreSeriazible instanceof ArrayList) {
+ ArrayList explores = (ArrayList) exploreSeriazible;
+ this.explore = explores;
+ Object firstExplore = (explores.size() > 0) ? explores.get(0) : null;
+ if (firstExplore instanceof HashMap) {
+ initExploreLocation((HashMap) firstExplore);
+ }
+ }
+ }
+
+ @Override
+ protected void initUiViews() {
+ super.initUiViews();
+ showDirectionsUiViews();
+ iconGenerator = new IconGenerator(this);
+ iconGenerator.setBackground(getDrawable(R.color.transparent));
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ if (inflater != null) {
+ markerLayoutView = inflater.inflate(R.layout.marker_info_layout, null);
+ markerGroupLayoutView = inflater.inflate(R.layout.marker_group_layout, null);
+ }
+ navRefreshButton = findViewById(R.id.navRefreshButton);
+ navTravelModesContainer = findViewById(R.id.navTravelModesContainer);
+ navAutoUpdateButton = findViewById(R.id.navAutoUpdateButton);
+ navPrevButton = findViewById(R.id.navPrevButton);
+ navNextButton = findViewById(R.id.navNextButton);
+ navStepLabel = findViewById(R.id.navStepLabel);
+ routeLoadingFrame = findViewById(R.id.routeLoadingFrame);
+ }
+
+ private void showDirectionsUiViews() {
+ View topNavBar = findViewById(R.id.topNavBar);
+ if (topNavBar != null) {
+ topNavBar.setVisibility(View.VISIBLE);
+ }
+ View bottomNavBar = findViewById(R.id.bottomNavBar);
+ if (topNavBar != null) {
+ bottomNavBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void buildExploreMarker() {
+ if (exploreLocation != null) {
+ MarkerOptions markerOptions = Utils.Explore.constructMarkerOptions(this, explore, markerLayoutView, markerGroupLayoutView, iconGenerator);
+ if (markerOptions != null) {
+ exploreMarker = googleMap.addMarker(markerOptions);
+ JSONObject tagJson = Utils.Explore.constructMarkerTagJson(this, exploreMarker.getTitle(), explore);
+ exploreMarker.setTag(tagJson);
+ }
+ updateExploreMarkerAppearance();
+ }
+ }
+
+ private void updateExploreMarkerAppearance() {
+ float currentCameraZoom = googleMap.getCameraPosition().zoom;
+ boolean updateMarkerInfo = (currentCameraZoom != cameraZoom);
+ if (updateMarkerInfo) {
+ boolean singleExploreMarker = Utils.Explore.optSingleExploreMarker(exploreMarker);
+ Utils.Explore.updateCustomMarkerAppearance(this, exploreMarker, singleExploreMarker, currentCameraZoom, cameraZoom, markerLayoutView, markerGroupLayoutView, iconGenerator);
+ }
+ cameraZoom = currentCameraZoom;
+ }
+
+ private void updateExploreMarkerVisibility() {
+ if (exploreMarker == null) {
+ return;
+ }
+ int currentFloorIndex = (mapControl != null) ? mapControl.getCurrentFloorIndex() : 0;
+ Integer markerFloor = Utils.Explore.optMarkerLocationFloor(exploreMarker);
+ boolean markerVisible = (markerFloor == null) || (currentFloorIndex == markerFloor);
+ exploreMarker.setVisible(markerVisible);
+ }
+
+ private void initExploreLocation(HashMap singleExplore) {
+ Utils.ExploreType exploreType = Utils.Explore.getExploreType(singleExplore);
+ if (exploreType == Utils.ExploreType.PARKING) {
+ LatLng latLng = Utils.Explore.optLocationLatLng(singleExplore);
+ if (latLng != null) {
+ this.exploreLocation = Utils.Explore.createLocationMap(latLng);
+ }
+ } else {
+ this.exploreLocation = Utils.Explore.optLocation(singleExplore);
+ }
+ }
+
+ private void buildPolygon() {
+ if (googleMap == null) {
+ return;
+ }
+ List polygonPoints = Utils.Explore.getExplorePolygon(explore);
+ if ((polygonPoints == null) || polygonPoints.isEmpty()) {
+ return;
+ }
+ Utils.ExploreType exploreType = Utils.Explore.getExploreType(explore);
+ int strokeColor = getResources().getColor(Utils.Explore.getExploreColorResource(exploreType));
+ int fillColor = Color.argb(10, 0, 0, 0);
+ googleMap.addPolygon(new PolygonOptions().addAll(polygonPoints).
+ clickable(false).strokeColor(strokeColor).strokeWidth(5.0f).fillColor(fillColor).zIndex(1.0f));
+ }
+
+ //endregion
+
+ //region Navigation
+
+ public void onRefreshNavClicked(View view) {
+ mpRoute = null;
+ mpRouteError = null;
+ if (routePolyline != null) {
+ routePolyline.remove();
+ routePolyline = null;
+ }
+ mpRouteStepCoordCounts = null;
+ if (mpDirectionsRenderer != null) {
+ mpDirectionsRenderer.clear();
+ mpDirectionsRenderer = null;
+ }
+ navStatus = NavStatus.UNKNOWN;
+ navAutoUpdate = false;
+
+ if (cameraPosition != null && googleMap != null) {
+ googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(cameraPosition.target, cameraPosition.zoom));
+ }
+ cameraPosition = null;
+
+ updateNav();
+ buildRoute();
+ }
+
+ public void onAutoUpdateNavClicked(View view) {
+ if (navStatus == NavStatus.PROGRESS) {
+ MPRouteSegmentPath segmentPath = findNearestRouteSegmentByCurrentLocation();
+ if (isValidSegmentPath(segmentPath)) {
+ currentLegIndex = segmentPath.legIndex;
+ currentStepIndex = segmentPath.stepIndex;
+ moveTo(currentLegIndex, currentStepIndex);
+ navAutoUpdate = true;
+ }
+ updateNav();
+ }
+ }
+
+ public void onWalkTravelModeClicked(View view) {
+ changeSelectedTravelMode(TravelMode.WALKING);
+ }
+
+ public void onBikeTravelModeClicked(View view) {
+ changeSelectedTravelMode(TravelMode.BICYCLING);
+ }
+
+ public void onDriveTravelModeClicked(View view) {
+ changeSelectedTravelMode(TravelMode.DRIVING);
+ }
+
+ public void onTransitTravelModeClicked(View view) {
+ changeSelectedTravelMode(TravelMode.TRANSIT);
+ }
+
+ public void onPrevNavClicked(View view) {
+ if (navStatus == NavStatus.START) {
+ //Do nothing
+ } else if (navStatus == NavStatus.PROGRESS) {
+ if (mpRoute == null) {
+ return;
+ }
+ if (currentStepIndex > 0) {
+ moveTo(currentLegIndex, --currentStepIndex);
+ } else if (currentLegIndex > 0) {
+ currentLegIndex--;
+ List routeLegs = mpRoute.getLegs();
+ RouteLeg currentLeg = routeLegs.get(currentLegIndex);
+ List routeSteps = currentLeg.getSteps();
+ int stepsSize = routeSteps.size();
+ currentStepIndex = stepsSize - 1;
+ moveTo(currentLegIndex, currentStepIndex);
+ } else {
+ navStatus = NavStatus.START;
+ mpDirectionsRenderer.clear();
+ }
+ } else if (navStatus == NavStatus.FINISHED) {
+ navStatus = NavStatus.PROGRESS;
+ moveTo(currentLegIndex, currentStepIndex);
+ }
+ updateNavAutoUpdate();
+ updateNav();
+ }
+
+ public void onNextNavClicked(View view) {
+ if (navStatus == NavStatus.START) {
+ navStatus = NavStatus.PROGRESS;
+ currentLegIndex = 0;
+ currentStepIndex = 0;
+ moveTo(currentLegIndex, currentStepIndex);
+ notifyRouteStart();
+ } else if (navStatus == NavStatus.PROGRESS) {
+ if (mpRoute == null) {
+ return;
+ }
+ List routeLegs = mpRoute.getLegs();
+ int legsSize = routeLegs.size();
+ RouteLeg currentLeg = routeLegs.get(currentLegIndex);
+ List routeSteps = currentLeg.getSteps();
+ int stepsSize = routeSteps.size();
+ if ((currentStepIndex + 1) < stepsSize) {
+ moveTo(currentLegIndex, ++currentStepIndex);
+ } else if ((currentLegIndex + 1) < legsSize) {
+ currentStepIndex = 0;
+ currentLegIndex++;
+ moveTo(currentLegIndex, currentStepIndex);
+ } else {
+ navStatus = NavStatus.FINISHED;
+ mpDirectionsRenderer.clear();
+ notifyRouteFinish();
+ }
+ } else if (navStatus == NavStatus.FINISHED) {/*Do nothing*/}
+ updateNavAutoUpdate();
+ updateNav();
+ }
+
+ private void buildTravelModes() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ String selectedTravelMode = preferences.getString(TRAVEL_MODE_PREFS_KEY, TravelMode.WALKING);
+ travelModesMap = new HashMap<>();
+ for (String currentTravelMode : TRAVEL_MODES) {
+ View travelModeView = null;
+ switch (currentTravelMode) {
+ case TravelMode.WALKING:
+ travelModeView = findViewById(R.id.walkTravelModeButton);
+ break;
+ case TravelMode.BICYCLING:
+ travelModeView = findViewById(R.id.bikeTravelModeButton);
+ break;
+ case TravelMode.DRIVING:
+ travelModeView = findViewById(R.id.driveTravelModeButton);
+ break;
+ case TravelMode.TRANSIT:
+ travelModeView = findViewById(R.id.transitTravelModeButton);
+ break;
+ default:
+ break;
+ }
+ travelModesMap.put(currentTravelMode, travelModeView);
+ if (currentTravelMode.equals(selectedTravelMode)) {
+ this.selectedTravelMode = selectedTravelMode;
+ travelModeView.setBackgroundResource(R.color.grey40);
+ }
+ }
+ }
+
+ private void buildRoute() {
+ if (selectedTravelMode == null) {
+ selectedTravelMode = TravelMode.WALKING;
+ }
+ buildRoute(selectedTravelMode);
+ }
+
+ private void buildRoute(String travelModeValue) {
+ showLoadingFrame(true);
+ if (travelModeValue == null || travelModeValue.isEmpty()) {
+ travelModeValue = TravelMode.WALKING;
+ }
+ Point originPoint = (mpPositionResult != null) ? mpPositionResult.getPoint() : null;
+ Point destinationPoint = getRouteDestinationPoint();
+ if (originPoint == null || destinationPoint == null) {
+ showLoadingFrame(false);
+ Log.e(getLogTag(), "buildRoute() -> origin or destination point is null!");
+ String routeFailedMsg = getString(R.string.routeFailedMsg);
+ showAlert(routeFailedMsg);
+ return;
+ }
+ mpRoutingProvider = new MPRoutingProvider();
+ mpRoutingProvider.setOnRouteResultListener(this);
+ mpRoutingProvider.setTravelMode(travelModeValue);
+ mpRoutingProvider.query(originPoint, destinationPoint);
+ }
+
+ /***
+ * Calculates route destination point based on explore type.
+ * @return parking entrance if explore is Parking, explore location - otherwise
+ */
+ private Point getRouteDestinationPoint() {
+ Utils.ExploreType exploreType = Utils.Explore.getExploreType(explore);
+ LatLng destinationLatLng = null;
+
+ if (exploreType == Utils.ExploreType.PARKING) {
+ HashMap exploreMap = (HashMap) explore;
+ destinationLatLng = Utils.Explore.optLocationLatLng(exploreMap);
+ if (destinationLatLng != null) {
+ return new Point(destinationLatLng.latitude, destinationLatLng.longitude, 0);
+ }
+ }
+ destinationLatLng = Utils.Explore.optLatLng(exploreLocation);
+ if (destinationLatLng != null) {
+ Integer floor = Utils.Explore.optFloor(exploreLocation);
+ return new Point(destinationLatLng.latitude, destinationLatLng.longitude, (floor != null ? floor : 0));
+ } else {
+ return null;
+ }
+ }
+
+ private void changeSelectedTravelMode(String newTravelMode) {
+ if (newTravelMode != null) {
+ mpRoute = null;
+ mpRouteError = null;
+ if (routePolyline != null) {
+ routePolyline.remove();
+ routePolyline = null;
+ }
+ mpRoutingProvider = null;
+ mpRouteStepCoordCounts = null;
+ if (mpDirectionsRenderer != null) {
+ mpDirectionsRenderer.clear();
+ mpDirectionsRenderer = null;
+ }
+ navStatus = NavStatus.UNKNOWN;
+ navAutoUpdate = false;
+ if (travelModesMap != null) {
+ for (String travelMode : travelModesMap.keySet()) {
+ View travelModeView = travelModesMap.get(travelMode);
+ if (travelModeView != null) {
+ int backgroundResource = (newTravelMode.equals(travelMode)) ? R.color.grey40 : 0;
+ travelModeView.setBackgroundResource(backgroundResource);
+ }
+ }
+ }
+ updateNav();
+ selectedTravelMode = newTravelMode;
+ buildRoute(newTravelMode);
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(TRAVEL_MODE_PREFS_KEY, selectedTravelMode);
+ editor.apply();
+ }
+ }
+
+ @Override
+ protected void handleFirstLocationUpdate() {
+ if (exploreLocation == null) {
+ if (mpPositionResult != null) {
+ Location location = mpPositionResult.getAndroidLocation();
+ if (location != null) {
+ LatLng cameraPosition = new LatLng(location.getLatitude(), location.getLongitude());
+ if (googleMap != null) {
+ googleMap.moveCamera(CameraUpdateFactory.newLatLng(cameraPosition));
+ }
+ }
+ }
+ } else if (mpPositionResult == null) {
+ showLoadingFrame(false);
+ LatLng cameraPosition = Utils.Explore.optLatLng(exploreLocation);
+ if ((googleMap != null) && (cameraPosition != null)) {
+ googleMap.moveCamera(CameraUpdateFactory.newLatLng(cameraPosition));
+ }
+ String errorMessage = getString(R.string.locationFailedMsg);
+ showAlert(errorMessage);
+ } else {
+ if ((mpRoutingProvider == null) && (mpRoute == null) && (mpRouteError == null)) {
+ if ((mapControl != null) && mapControl.isReady()) {
+ buildRoute();
+ } else {
+ buildRouteAfterInitialization = true;
+ }
+ }
+ }
+ }
+
+ private void didBuildRoute() {
+ showLoadingFrame(false);
+ if (mpRoute != null) {
+ buildRoutePolyline();
+ mpDirectionsRenderer = new MPDirectionsRenderer(this, googleMap, mapControl, this);
+ mpDirectionsRenderer.setRoute(mpRoute);
+ cameraPosition = googleMap.getCameraPosition();
+ navStatus = NavStatus.START;
+ } else {
+ String routeFailedMsg = getString(R.string.routeFailedMsg);
+ showAlert(routeFailedMsg);
+ }
+
+ updateNav();
+
+ Point point = mpPositionResult.getPoint();
+ if (point != null) {
+ LatLng currentLatLng = point.getLatLng();
+ LatLng exploreLatLng = Utils.Explore.optLatLng(exploreLocation);
+ LatLngBounds.Builder latLngBuilder = new LatLngBounds.Builder();
+ latLngBuilder.include(currentLatLng);
+ latLngBuilder.include(exploreLatLng);
+ if (mpRoute != null && mpRoute.getBounds() != null) {
+ Object routeBoundsObject = mpRoute.getBounds();
+ if (routeBoundsObject instanceof LatLngBounds) {
+ LatLngBounds routeLatLngBounds = (LatLngBounds) routeBoundsObject;
+ latLngBuilder.include(routeLatLngBounds.northeast);
+ latLngBuilder.include(routeLatLngBounds.southwest);
+ }
+ }
+ googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(latLngBuilder.build(), 50));
+ }
+ }
+
+ private void buildRoutePolyline() {
+ mpRouteStepCoordCounts = new ArrayList<>();
+ List routePoints = new ArrayList<>();
+ for (RouteLeg routeLeg : mpRoute.getLegs()) {
+ for (RouteStep routeStep : routeLeg.getSteps()) {
+ RoutePolyline routePolyline = routeStep.getPolyline();
+ if (routePolyline != null) {
+ Point[] polylinePoints = routePolyline.getPoints();
+ for (Point point : polylinePoints) {
+ LatLng latLng = point.getLatLng();
+ routePoints.add(latLng);
+ }
+ mpRouteStepCoordCounts.add(polylinePoints.length);
+ }
+ }
+ }
+ if (googleMap != null) {
+ routePolyline = googleMap.addPolyline(new PolylineOptions()
+ .addAll(routePoints)
+ .color(Color.BLUE));
+ }
+ }
+
+ private void moveTo(int legIndex, int stepIndex) {
+ if (mpDirectionsRenderer != null) {
+ mpDirectionsRenderer.setRouteLegIndex(legIndex, stepIndex);
+ mpDirectionsRenderer.animate(0, true);
+ }
+ }
+
+ private void updateNav() {
+ navRefreshButton.setVisibility(View.VISIBLE);
+ enableView(navRefreshButton, (mpRoutingProvider == null));
+
+ int travelModesVisibility = ((navStatus != NavStatus.UNKNOWN) && (navStatus != NavStatus.START)) ? View.GONE : View.VISIBLE;
+ navTravelModesContainer.setVisibility(travelModesVisibility);
+ enableView(navTravelModesContainer, (mpRoutingProvider == null));
+
+ int autoUpdateVisibility = ((navStatus != NavStatus.PROGRESS) || navAutoUpdate) ? View.GONE : View.VISIBLE;
+ navAutoUpdateButton.setVisibility(autoUpdateVisibility);
+ int navBottomVisibility = (navStatus == NavStatus.UNKNOWN) ? View.GONE : View.VISIBLE;
+ navPrevButton.setVisibility(navBottomVisibility);
+ navNextButton.setVisibility(navBottomVisibility);
+ navStepLabel.setVisibility(navBottomVisibility);
+
+ if (navStatus == NavStatus.START) {
+ String routeDisplayDescription = buildRouteDisplayDescription();
+ boolean hasDescription = (routeDisplayDescription != null) && !routeDisplayDescription.isEmpty();
+ String secondRow = hasDescription ? String.format(" (%s)", routeDisplayDescription) : "";
+ String stepHtmlContent = String.format("%s %s", getString(R.string.start), secondRow);
+ setStepHtml(stepHtmlContent);
+ enableView(navPrevButton, false);
+ enableView(navNextButton, true);
+ } else if (navStatus == NavStatus.PROGRESS) {
+ List routeLegs = mpRoute.getLegs();
+ RouteLeg leg = (currentLegIndex >= 0 && currentLegIndex < routeLegs.size()) ? routeLegs.get(currentLegIndex) : null;
+ List routeSteps = (leg != null) ? leg.getSteps() : null;
+ RouteStep step = ((routeSteps != null) && (currentStepIndex >= 0) && (currentStepIndex < routeSteps.size())) ?
+ routeSteps.get(currentStepIndex) : null;
+ if (step != null) {
+ if (step.getHtmlInstructions() != null) {
+ setStepHtml(step.getHtmlInstructions());
+ } else if (step.getManeuver() != null || !step.getHighway().isEmpty() || !step.getAbutters().isEmpty()) {
+ String maneuver = (step.getManeuver() != null) ? step.getManeuver() : "";
+ String plainStepText = String.format("%s | %s | %s", maneuver, step.getHighway(), step.getAbutters());
+ navStepLabel.setText(plainStepText);
+ } else if (step.getDistance() > 0.0f || step.getDuration() > 0.0f) {
+ String plainStepText = String.format(getString(R.string.routeDistanceDurationFormat), step.getDistance(), step.getDuration());
+ navStepLabel.setText(plainStepText);
+ }
+ } else {
+ String plainStepText = String.format(getString(R.string.routeLegStepFormat), (currentLegIndex + 1), (currentStepIndex + 1));
+ navStepLabel.setText(plainStepText);
+ }
+
+ enableView(navPrevButton, true);
+ enableView(navNextButton, true);
+ if (step != null) {
+ updateCurrentFloor(step.getStartPoint().getZIndex());
+ }
+
+ } else if (navStatus == NavStatus.FINISHED) {
+ String htmlContent = String.format("%s ", getString(R.string.finish));
+ setStepHtml(htmlContent);
+ enableView(navPrevButton, true);
+ enableView(navNextButton, false);
+ }
+ }
+
+ private void updateCurrentFloor(int floor) {
+ if (floor != mapControl.getCurrentFloorIndex()) {
+ mapControl.selectFloor(floor);
+ onFloorChanged(floor);
+ }
+ }
+
+ private void updateNavAutoUpdate() {
+ MPRouteSegmentPath segmentPath = findNearestRouteSegmentByCurrentLocation();
+ navAutoUpdate = (isValidSegmentPath(segmentPath) &&
+ (currentLegIndex == segmentPath.legIndex) &&
+ (currentStepIndex == segmentPath.stepIndex));
+ }
+
+ private void updateNavByCurrentLocation() {
+ if ((navStatus == NavStatus.PROGRESS) && navAutoUpdate &&
+ (mpPositionResult != null) && (mpRoute != null) && (mpDirectionsRenderer != null)) {
+ MPRouteSegmentPath segmentPath = findNearestRouteSegmentByCurrentLocation();
+ if (isValidSegmentPath(segmentPath)) {
+ updateNavFromSegmentPath(segmentPath);
+ }
+ }
+ }
+
+ private void updateNavFromSegmentPath(MPRouteSegmentPath segmentPath) {
+ boolean modified = false;
+ if (currentLegIndex != segmentPath.legIndex) {
+ currentLegIndex = segmentPath.legIndex;
+ modified = true;
+ }
+ if (currentStepIndex != segmentPath.stepIndex) {
+ currentStepIndex = segmentPath.stepIndex;
+ modified = true;
+ }
+ if (modified) {
+ moveTo(currentLegIndex, currentStepIndex);
+ updateNav();
+ }
+ }
+
+ @NonNull
+ private MPRouteSegmentPath findNearestRouteSegmentByCurrentLocation() {
+ MPRouteSegmentPath minRouteSegmentPath = new MPRouteSegmentPath(-1, -1);
+ if (mpPositionResult != null && mpRoute != null) {
+ double minLegDistance = -1;
+ Point mpPoint = mpPositionResult.getPoint();
+ if (mpPoint != null) {
+ LatLng locationLatLng = mpPoint.getLatLng();
+ int globalStepIndex = 0;
+ int locationIndex = 0;
+ List routePolylinePoints = routePolyline.getPoints();
+ List routeLegs = mpRoute.getLegs();
+ for (int legIndex = 0; legIndex < routeLegs.size(); legIndex++) {
+ RouteLeg routeLeg = routeLegs.get(legIndex);
+ List legSteps = routeLeg.getSteps();
+ for (int stepIndex = 0; stepIndex < legSteps.size(); stepIndex++) {
+ int increasedIndex = (globalStepIndex < mpRouteStepCoordCounts.size()) ? mpRouteStepCoordCounts.get(globalStepIndex) : 0;
+ int lastLocationIndex = locationIndex + increasedIndex;
+ while (locationIndex < lastLocationIndex) {
+ LatLng latLng = routePolylinePoints.get(locationIndex);
+ Double coordDistance = Utils.Location.getDistanceBetween(locationLatLng, latLng);
+ if (coordDistance != null && (minLegDistance < 0.0d || coordDistance < minLegDistance)) {
+ minLegDistance = coordDistance;
+ minRouteSegmentPath = new MPRouteSegmentPath(legIndex, stepIndex);
+ locationIndex = lastLocationIndex;
+ break;
+ }
+ locationIndex++;
+ }
+ globalStepIndex++;
+ }
+ }
+ }
+ }
+ return minRouteSegmentPath;
+ }
+
+ private boolean isValidSegmentPath(MPRouteSegmentPath segmentPath) {
+ if (mpRoute == null || segmentPath == null) {
+ return false;
+ }
+ List routeLegs = mpRoute.getLegs();
+ if ((segmentPath.legIndex >= 0) && (segmentPath.legIndex < routeLegs.size())) {
+ RouteLeg leg = routeLegs.get(segmentPath.legIndex);
+ if ((segmentPath.stepIndex >= 0) && segmentPath.stepIndex < leg.getSteps().size()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void setStepHtml(String htmlContent) {
+ String formattedHtml = String.format("%s ", htmlContent);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ navStepLabel.setText(Html.fromHtml(formattedHtml, Html.FROM_HTML_MODE_COMPACT));
+ } else {
+ navStepLabel.setText(Html.fromHtml(formattedHtml));
+ }
+ }
+
+ private String buildRouteDisplayDescription() {
+ if (mpRoute == null) {
+ return null;
+ }
+ StringBuilder descriptionBuilder = new StringBuilder();
+
+ if (mpRoute.getDistance() > 0) {
+ // 1 foot = 0.3048 meters
+ // 1 mile = 1609.34 meters
+
+ long totalMeters = Math.abs(mpRoute.getDistance());
+ double totalMiles = (totalMeters / 1609.34d);
+ if (descriptionBuilder.length() > 0) {
+ descriptionBuilder.append(", ");
+ }
+ descriptionBuilder.append(String.format(Locale.getDefault(), "%.1f %s", totalMiles, getString((totalMiles != 1.0) ? R.string.miles : R.string.mile)));
+ }
+ if (mpRoute.getDuration() > 0) {
+ long totalSeconds = Math.abs(mpRoute.getDuration());
+ long totalMinutes = totalSeconds / 60;
+ long totalHours = totalMinutes / 60;
+ long minutes = totalMinutes % 60;
+
+ if (descriptionBuilder.length() > 0) {
+ descriptionBuilder.append(", ");
+ }
+ String formattedTime;
+ if (totalHours < 1) {
+ formattedTime = String.format(Locale.getDefault(), "%d %s", minutes, getString(R.string.minute));
+ } else if (totalHours < 24) {
+ formattedTime = String.format(Locale.getDefault(), "%d h %2d %s", totalHours, minutes, getString(R.string.minute));
+ } else {
+ formattedTime = String.format(Locale.getDefault(), "%d h", totalHours);
+ }
+ descriptionBuilder.append(formattedTime);
+ }
+
+ String routeSummary = mpRoute.getSummary();
+ if (routeSummary != null && !routeSummary.isEmpty()) {
+ descriptionBuilder.append(routeSummary);
+ }
+ return descriptionBuilder.toString();
+ }
+
+ private void notifyRouteStart() {
+ notifyRouteEvent("map.route.start");
+ }
+
+ private void notifyRouteFinish() {
+ notifyRouteEvent("map.route.finish");
+ }
+
+ private void notifyRouteEvent(String event) {
+ String originString = null;
+ String destinationString = null;
+ String locationString = null;
+ List routeLegs = (mpRoute != null) ? mpRoute.getLegs() : null;
+ int legsCount = (routeLegs != null && routeLegs.size() > 0) ? routeLegs.size() : 0;
+ if (legsCount > 0) {
+ RouteCoordinate origin = routeLegs.get(0).getStartLocation();
+ RouteCoordinate destination = routeLegs.get(legsCount - 1).getEndLocation();
+ int originFloor = (int) origin.getZIndex();
+ int destinationFloor = (int) destination.getZIndex();
+ originString = String.format(Locale.getDefault(), Constants.ANALYTICS_ROUTE_LOCATION_FORMAT, origin.getLat(), origin.getLng(), originFloor);
+ destinationString = String.format(Locale.getDefault(), Constants.ANALYTICS_ROUTE_LOCATION_FORMAT, destination.getLat(), destination.getLng(), destinationFloor);
+ }
+ if (mpPositionResult != null) {
+ Point locationPoint = mpPositionResult.getPoint();
+ int locationFloor = mpPositionResult.getFloor();
+ if (locationPoint != null) {
+ locationString = String.format(Locale.getDefault(), Constants.ANALYTICS_USER_LOCATION_FORMAT, locationPoint.getLat(), locationPoint.getLng(), locationFloor, locationTimestamp);
+ }
+ }
+ String analyticsParam = String.format(Locale.getDefault(), "{\"origin\":%s,\"destination\":%s,\"location\":%s}", originString, destinationString, locationString);
+ MainActivity.invokeFlutterMethod(event, analyticsParam);
+ }
+
+ //endregion
+
+ //region Utilities
+
+ @Override
+ protected String getLogTag() {
+ return MapDirectionsActivity.class.getSimpleName();
+ }
+
+ private void showLoadingFrame(boolean show) {
+ if (routeLoadingFrame != null) {
+ routeLoadingFrame.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void showAlert(String message) {
+ String appName = getString(R.string.app_name);
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this);
+ alertBuilder.setTitle(appName);
+ alertBuilder.setMessage(message);
+ alertBuilder.setPositiveButton(R.string.ok, null);
+ alertBuilder.show();
+ }
+
+ private void enableView(View view, boolean enabled) {
+ if (view == null) {
+ return;
+ }
+ float viewAlpha = enabled ? 1.0f : 0.5f;
+ view.setEnabled(enabled);
+ view.setAlpha(viewAlpha);
+ }
+
+ //endregion
+
+ //region NavStatus
+
+ private enum NavStatus {UNKNOWN, START, PROGRESS, FINISHED}
+
+ //endregion
+
+ //region MPRouteSegmentPath
+
+ private static class MPRouteSegmentPath {
+ private int legIndex;
+ private int stepIndex;
+
+ private MPRouteSegmentPath(int legIndex, int stepIndex) {
+ this.legIndex = legIndex;
+ this.stepIndex = stepIndex;
+ }
+ }
+
+ //endregion
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapMarkerViewType.java b/android/app/src/main/java/edu/illinois/covid/maps/MapMarkerViewType.java
new file mode 100644
index 00000000..7263ac5e
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapMarkerViewType.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+public enum MapMarkerViewType {SINGLE, GROUP, UNKNOWN}
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapPickLocationActivity.java b/android/app/src/main/java/edu/illinois/covid/maps/MapPickLocationActivity.java
new file mode 100644
index 00000000..a94073a8
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapPickLocationActivity.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.SupportMapFragment;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.mapsindoors.mapssdk.MPLocation;
+import com.mapsindoors.mapssdk.MapControl;
+import com.mapsindoors.mapssdk.MapsIndoors;
+import com.mapsindoors.mapssdk.errors.MIError;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.R;
+import edu.illinois.covid.Utils;
+
+public class MapPickLocationActivity extends AppCompatActivity {
+
+ private static final String TAG = MapPickLocationActivity.class.getSimpleName();
+
+ private SupportMapFragment mapFragment;
+ private GoogleMap googleMap;
+ private MapControl mapControl;
+ private TextView locationInfoTextView;
+ private Marker customLocationMarker;
+ private Marker selectedMarker;
+ private HashMap initialLocation;
+ private LatLng initialCameraPosition;
+ private HashMap explore;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.map_pick_location_layout);
+
+ initHeaderBar();
+ initInitialLocation();
+ initMapFragment();
+ locationInfoTextView = findViewById(R.id.locationInfoTextView);
+ updateLocationInfo(null);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (mapControl != null) {
+ mapControl.onStart();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mapControl != null) {
+ mapControl.onStop();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mapControl != null) {
+ mapControl.onResume();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mapControl != null) {
+ mapControl.onPause();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mapControl != null) {
+ mapControl.onDestroy();
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ if (mapControl != null) {
+ mapControl.onLowMemory();
+ }
+ }
+
+ public void onSaveClicked(View view) {
+ if (selectedMarker == null) {
+ Utils.showDialog(this, getString(R.string.app_name),
+ getString(R.string.select_location_msg),
+ (dialog, which) -> dialog.dismiss(),
+ getString(R.string.ok), null, null, false);
+ return;
+ }
+ String resultData = null;
+ if (selectedMarker == customLocationMarker) {
+ resultData = (String) selectedMarker.getTag();
+ } else {
+ MPLocation location = mapControl.getLocation(selectedMarker);
+ if (location != null) {
+ resultData = String.format(Locale.getDefault(), Constants.LOCATION_PICKER_DATA_FORMAT,
+ selectedMarker.getPosition().latitude, selectedMarker.getPosition().longitude,
+ location.getFloor(), (selectedMarker.getSnippet() != null ? selectedMarker.getSnippet() : ""),
+ location.getId(), location.getName());
+ }
+ }
+ Intent resultDataIntent = new Intent();
+ resultDataIntent.putExtra("location", resultData);
+ setResult(RESULT_OK, resultDataIntent);
+ finish();
+ }
+
+ private void initHeaderBar() {
+ setSupportActionBar(findViewById(R.id.toolbar));
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowHomeEnabled(true);
+ }
+ }
+
+ private void initInitialLocation() {
+ Bundle initialLocationArguments = getIntent().getExtras();
+ if (initialLocationArguments != null) {
+ Serializable serializable = initialLocationArguments.getSerializable("explore");
+ if (serializable instanceof HashMap) {
+ explore = (HashMap) serializable;
+ initialLocation = Utils.Explore.optLocation(explore);
+ }
+ }
+ initialCameraPosition = Constants.DEFAULT_INITIAL_CAMERA_POSITION;
+ if (initialLocation != null) {
+ initialCameraPosition = Utils.Explore.optLatLng(initialLocation);
+ }
+ }
+
+ private void initMapFragment() {
+ mapFragment = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map_fragment));
+ if (mapFragment != null) {
+ mapFragment.getMapAsync(this::didGetMapAsync);
+ }
+ }
+
+ private void didGetMapAsync(GoogleMap map) {
+ googleMap = map;
+ googleMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
+ googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(initialCameraPosition, Constants.INDOORS_BUILDING_ZOOM));
+ setupMapsIndoors();
+ loadInitialLocation();
+ }
+
+ private void setupMapsIndoors() {
+ mapControl = new MapControl(this);
+ mapControl.setGoogleMap(googleMap, mapFragment.getView());
+ mapControl.setOnMarkerClickListener(marker -> {
+ setSelectedLocationMarker(marker);
+ return true;
+ });
+ mapControl.setOnMapClickListener(this::onMapClicked);
+ mapControl.setOnFloorUpdateListener((building, i) -> onFloorUpdate());
+ mapControl.init(this::mapControlDidInit);
+ }
+
+ private void mapControlDidInit(MIError error) {
+ runOnUiThread(() -> {
+ if (error == null) {
+ mapControl.selectFloor(0);
+ } else {
+ Log.e(TAG, error.message);
+ }
+ });
+ }
+
+ private void loadInitialLocation() {
+ if (initialLocation != null) {
+ String locationId = null;
+ Object locationIdObj = initialLocation.get("location_id");
+ if (locationIdObj instanceof String) {
+ locationId = (String) locationIdObj;
+ }
+ //MapsIndoors removed mapControl.getMarker(locationId) in version 3.x.x
+ MPLocation mpLocation = (locationId != null) ? MapsIndoors.getLocationById(locationId) : null;
+ int floorIndex = Utils.Explore.optFloor(initialLocation);
+ selectedMarker = createCustomLocationMarker(initialLocation);
+ mapControl.selectFloor(floorIndex);
+ updateLocationInfo(selectedMarker);
+ }
+ }
+
+ private boolean onMapClicked(@NonNull LatLng latLng, @Nullable List list) {
+ runOnUiThread(() -> {
+ if ((selectedMarker != null) || (customLocationMarker != null)) {
+ clearCustomLocationMarker();
+ setSelectedLocationMarker(null);
+ } else {
+ Marker customMarker = createCustomLocationMarker(latLng);
+ setSelectedLocationMarker(customMarker);
+ }
+ });
+ return true;
+ }
+
+ private void onFloorUpdate() {
+ updateCustomLocationMarker();
+ updateSelectedMarker();
+ }
+
+ private void updateCustomLocationMarker() {
+ if (customLocationMarker == null) {
+ return;
+ }
+ int floorIndex;
+ Object userDataObj = customLocationMarker.getTag();
+ if (userDataObj instanceof Integer) {
+ floorIndex = (Integer) userDataObj;
+ } else {
+ floorIndex = getFloorIndexFromMarkerTag(userDataObj);
+ }
+ boolean markerVisible = (floorIndex == mapControl.getCurrentFloorIndex());
+ if (markerVisible && (!customLocationMarker.isInfoWindowShown())) {
+ customLocationMarker.setVisible(true);
+ customLocationMarker.showInfoWindow();
+ } else if (!markerVisible && (customLocationMarker.isInfoWindowShown())) {
+ customLocationMarker.hideInfoWindow();
+ customLocationMarker.setVisible(false);
+ }
+ }
+
+ private int getFloorIndexFromMarkerTag(Object markerTag) {
+ if (!(markerTag instanceof String)) {
+ return 0;
+ }
+ JSONObject tagJson = null;
+ try {
+ tagJson = new JSONObject((String) markerTag);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ Integer floorIndex = null;
+ if (tagJson != null) {
+ floorIndex = tagJson.optInt("floor", 0);
+ }
+ return (floorIndex != null) ? floorIndex : 0;
+ }
+
+ private void updateSelectedMarker() {
+ if (selectedMarker == null) {
+ return;
+ }
+ int floorIndex = 0;
+ if (selectedMarker == customLocationMarker) {
+ floorIndex = getFloorIndexFromMarkerTag(customLocationMarker);
+ } else {
+ MPLocation location = mapControl.getLocation(selectedMarker);
+ if (location != null) {
+ floorIndex = location.getFloor();
+ }
+ }
+ if (floorIndex == mapControl.getCurrentFloorIndex()) {
+ selectedMarker.showInfoWindow();
+ } else {
+ selectedMarker.hideInfoWindow();
+ }
+ }
+
+ private Marker createCustomLocationMarker(HashMap locationMap) {
+ clearCustomLocationMarker();
+ LatLng latLng = Utils.Explore.optLatLng(locationMap);
+ String locationName = null;
+ Object nameObj = locationMap.get("name");
+ if (nameObj instanceof String) {
+ locationName = (String) nameObj;
+ }
+ String locationDesc = null;
+ Object descrObj = locationMap.get("description");
+ if (descrObj instanceof String) {
+ locationDesc = (String) descrObj;
+ }
+ MarkerOptions markerOptions = new MarkerOptions();
+ markerOptions.position(latLng);
+ markerOptions.zIndex(1);
+ markerOptions.title(locationName);
+ markerOptions.snippet(locationDesc);
+ customLocationMarker = googleMap.addMarker(markerOptions);
+ String tag = String.format(Locale.getDefault(), Constants.LOCATION_PICKER_DATA_FORMAT, latLng.latitude, latLng.longitude, Utils.Explore.optFloor(locationMap), locationDesc, "", locationName);
+ customLocationMarker.setTag(tag);
+ customLocationMarker.showInfoWindow();
+ return customLocationMarker;
+ }
+
+ private Marker createCustomLocationMarker(LatLng latLng) {
+ clearCustomLocationMarker();
+ MarkerOptions markerOptions = new MarkerOptions();
+ markerOptions.position(latLng);
+ markerOptions.zIndex(1);
+ String title = (explore != null) ? (String)explore.get("name") : getString(R.string.custom);
+ if (Utils.Str.isEmpty(title) || "null".equals(title)) {
+ title = getString(R.string.custom);
+ }
+ markerOptions.title(title);
+ customLocationMarker = googleMap.addMarker(markerOptions);
+ String userData = String.format(Locale.getDefault(), Constants.LOCATION_PICKER_DATA_FORMAT,
+ latLng.latitude, latLng.longitude, mapControl.getCurrentFloorIndex(),
+ "", "", "");//empty "name", "description" and empty "location_id"
+ customLocationMarker.setTag(userData);
+ customLocationMarker.showInfoWindow();
+ return customLocationMarker;
+ }
+
+ private void setSelectedLocationMarker(Marker marker) {
+ if ((customLocationMarker != null) && (customLocationMarker != marker)) {
+ clearCustomLocationMarker();
+ }
+ selectedMarker = marker;
+ if (selectedMarker != null) {
+ selectedMarker.showInfoWindow();
+ }
+ updateLocationInfo(marker);
+ }
+
+ private void clearCustomLocationMarker() {
+ if (customLocationMarker != null) {
+ if (selectedMarker == customLocationMarker) {
+ selectedMarker.hideInfoWindow();
+ selectedMarker = null;
+ }
+ customLocationMarker.hideInfoWindow();
+ customLocationMarker.remove();
+ customLocationMarker = null;
+ }
+ }
+
+ private void updateLocationInfo(Marker marker) {
+ String locationInfoText;
+ if (marker != null) {
+ MPLocation location = mapControl.getLocation(marker);
+ String locationName = (location != null) ? location.getName() : marker.getTitle();
+ locationInfoText = getString(R.string.location_label, locationName);
+ } else {
+ locationInfoText = getString(R.string.select_location_msg);
+ }
+ locationInfoTextView.setText(locationInfoText);
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapView.java b/android/app/src/main/java/edu/illinois/covid/maps/MapView.java
new file mode 100644
index 00000000..735fd2aa
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapView.java
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+import com.google.android.gms.maps.CameraUpdate;
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.gson.Gson;
+import com.google.maps.android.ui.IconGenerator;
+import com.mapsindoors.mapssdk.FloorSelectorInterface;
+import com.mapsindoors.mapssdk.MPLocation;
+import com.mapsindoors.mapssdk.MapControl;
+import com.mapsindoors.mapssdk.errors.MIError;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import edu.illinois.covid.Constants;
+import edu.illinois.covid.MainActivity;
+import edu.illinois.covid.R;
+import edu.illinois.covid.Utils;
+
+public class MapView extends FrameLayout implements OnMapReadyCallback {
+
+ private Context context;
+ private int mapId;
+ private Object args;
+ private Activity activity;
+ private com.google.android.gms.maps.MapView googleMapView;
+ private GoogleMap googleMap;
+ private MapControl mapControl;
+ private List explores;
+ private List markers;
+
+ private IconGenerator iconGenerator;
+ private View markerLayoutView;
+ private View markerGroupLayoutView;
+ private float cameraZoom;
+
+ private boolean mapLayoutPassed;
+ private boolean enableLocationValue;
+
+ public MapView(Context context, int mapId, Object args) {
+ super(context);
+ this.context = context;
+ this.mapId = mapId;
+ this.args = args;
+ if (context instanceof Activity) {
+ this.activity = (Activity) context;
+ }
+ init();
+ }
+
+ public void onDestroy() {
+ clearMarkers();
+ if (mapControl != null) {
+ mapControl.onDestroy();
+ mapControl = null;
+ }
+ if (googleMapView != null) {
+ googleMapView.onDestroy();
+ }
+ }
+
+ private void onCreate() {
+ if (googleMapView != null) {
+ googleMapView.onCreate(null);
+ }
+ }
+
+ private void onResume() {
+ if (googleMapView != null) {
+ googleMapView.onResume();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ googleMapView.layout(0, 0, r, b);
+ if (!mapLayoutPassed) {
+ mapLayoutPassed = true;
+ showExploresOnMap();
+ }
+ }
+
+ private void init() {
+ initMarkerView();
+ initMapView();
+ }
+
+ private void initMapView() {
+ acknowledgeLocationEnabledFromArgs();
+ googleMapView = new com.google.android.gms.maps.MapView(context);
+ googleMapView.setBackgroundColor(0xFF0000FF);
+ addView(googleMapView);
+ onCreate();
+ googleMapView.getMapAsync(this);
+ }
+
+ private void initMarkerView() {
+ iconGenerator = new IconGenerator(activity);
+ iconGenerator.setBackground(activity.getDrawable(R.color.transparent));
+ LayoutInflater inflater = (activity != null) ? (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) : null;
+ markerLayoutView = (inflater != null) ? inflater.inflate(R.layout.marker_info_layout, null) : null;
+ markerGroupLayoutView = (inflater != null) ? inflater.inflate(R.layout.marker_group_layout, null) : null;
+ }
+
+ private void initMapControl() {
+ mapControl = new MapControl(activity);
+ mapControl.setGoogleMap(googleMap, googleMapView);
+ mapControl.addOnCameraMoveListener(this::updateMarkers);
+ mapControl.setOnMarkerClickListener(this::onMarkerClicked);
+ mapControl.setOnMapClickListener(this::onMapClick);
+ mapControl.setOnFloorUpdateListener((building, i) -> updateMarkersVisibility());
+
+ mapControl.init(this::mapControlDidInit);
+ }
+
+ @Override
+ public void onMapReady(GoogleMap map) {
+ onResume();
+ googleMap = map;
+ enableMyLocation(enableLocationValue);
+ googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(CameraPosition.fromLatLngZoom(Constants.DEFAULT_INITIAL_CAMERA_POSITION, Constants.DEFAULT_CAMERA_ZOOM)));
+ showExploresOnMap();
+ relocateMyLocationButton();
+ initMapControl();
+ }
+
+ private void acknowledgeLocationEnabledFromArgs() {
+ boolean myLocationEnabled = false;
+ if (args instanceof Map) {
+ //{ "myLocationEnabled" : true}
+ Map jsonArgs = (Map) args;
+ Object myLocationEnabledObj = jsonArgs.get("myLocationEnabled");
+ if (myLocationEnabledObj instanceof Boolean) {
+ myLocationEnabled = (Boolean) myLocationEnabledObj;
+ }
+ }
+ this.enableLocationValue = myLocationEnabled;
+ }
+
+ private void mapControlDidInit(MIError error) {
+ if (error != null) {
+ Log.d(MapViewController.class.getCanonicalName(), error.message);
+ } else {
+ if (activity != null) {
+ activity.runOnUiThread(this::mapControlInitIsReady);
+ }
+ }
+ }
+
+ private void mapControlInitIsReady() {
+ if (mapControl != null) {
+ mapControl.selectFloor(0);
+ // =======================================================================================
+ //
+ // This is a workaround for a current issue in the default floor selector. Without it,
+ // the floor selector will only show up once we pan away from our building and back...
+ //
+ // This issue is still present in the current SDK version (3.1.3-beta-4)
+ // =======================================================================================
+ //
+ final FloorSelectorInterface floorSelector = mapControl.getFloorSelector();
+ if (floorSelector != null) {
+ // Hide without animating
+ floorSelector.show(false, false);
+ // ...and show without animating
+ floorSelector.show(true, false);
+ }
+ }
+ }
+
+ private void moveCameraToSpecificPosition() {
+ if ((markers != null) && (markers.size() > 0)) {
+ LatLngBounds.Builder builder = new LatLngBounds.Builder();
+ for (Marker marker : markers) {
+ builder.include(marker.getPosition());
+ }
+ LatLngBounds bounds = builder.build();
+ int width = getResources().getDisplayMetrics().widthPixels;
+ int height = getResources().getDisplayMetrics().heightPixels;
+ int padding = 150; // offset from edges of the map in pixels
+ CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, width, height, padding);
+ googleMap.animateCamera(cu);
+ } else {
+ googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(Constants.DEFAULT_INITIAL_CAMERA_POSITION, Constants.DEFAULT_CAMERA_ZOOM));
+ }
+ }
+
+ public void applyExplores(ArrayList explores, HashMap options) {
+ this.explores = buildExplores(explores, options);
+ if (mapLayoutPassed) {
+ showExploresOnMap();
+ }
+ }
+
+ //This has already been checked in flutter portion of the app
+ @SuppressLint("MissingPermission")
+ public void enableMyLocation(boolean enable) {
+ enableLocationValue = enable;
+ if (googleMap != null) {
+ googleMap.setMyLocationEnabled(enable);
+ }
+ }
+
+ private List buildExplores(ArrayList rawExplores, HashMap options) {
+ if (rawExplores == null || rawExplores.size() == 0) {
+ return null;
+ }
+ Object exploreLocationThresholdParam = (options != null) ? options.get("LocationThresoldDistance") : null;
+ double exploreLocationThresholdDistance = Constants.EXPLORE_LOCATION_THRESHOLD_DISTANCE;
+ if (exploreLocationThresholdParam instanceof Double) {
+ exploreLocationThresholdDistance = (Double) exploreLocationThresholdParam;
+ }
+ List> mappedExploreGroups = new ArrayList<>();
+ int rawExploresCount = rawExplores.size();
+ for (int rawExploresIndex = 0; rawExploresIndex < rawExploresCount; rawExploresIndex++) {
+ Object exploreObject = rawExplores.get(rawExploresIndex);
+ if (exploreObject instanceof HashMap) {
+ HashMap explore = (HashMap) exploreObject;
+ Integer exploreFloor = Utils.Explore.optLocationFloor(explore);
+ LatLng exploreLatLng = Utils.Explore.optLocationLatLng(explore);
+ if (exploreLatLng != null) {
+ boolean exploreMapped = false;
+ for (List mappedExploreGroup : mappedExploreGroups) {
+ for (HashMap mappedExplore : mappedExploreGroup) {
+ LatLng mappedExploreLatLng = Utils.Explore.optLocationLatLng(mappedExplore);
+ Double distance = Utils.Location.getDistanceBetween(exploreLatLng, mappedExploreLatLng);
+ Integer mappedExploreFloor = Utils.Explore.optLocationFloor(mappedExplore);
+ boolean sameFloor = (exploreFloor == null && mappedExploreFloor == null) ||
+ ((exploreFloor != null && mappedExploreFloor != null) && exploreFloor.equals(mappedExploreFloor));
+ if ((distance != null) && (distance < exploreLocationThresholdDistance) && sameFloor) {
+ mappedExploreGroup.add(explore);
+ exploreMapped = true;
+ break;
+ }
+ }
+ if (exploreMapped) {
+ break;
+ }
+ }
+ if (!exploreMapped) {
+ ArrayList mappedExploreGroup = new ArrayList<>(Collections.singletonList(explore));
+ mappedExploreGroups.add(mappedExploreGroup);
+ }
+ }
+ }
+ }
+ List resultExplores = new ArrayList<>();
+ for (List mappedExploreGroup : mappedExploreGroups) {
+ if (mappedExploreGroup.size() == 1) {
+ HashMap firstExplore = mappedExploreGroup.get(0);
+ resultExplores.add(firstExplore);
+ } else {
+ resultExplores.add(mappedExploreGroup);
+ }
+ }
+ return resultExplores;
+ }
+
+ private void showExploresOnMap() {
+ if (googleMap == null || !mapLayoutPassed) {
+ return;
+ }
+ clearMarkers();
+ if (explores != null && explores.size() > 0) {
+ markers = new ArrayList<>();
+ for (Object explore : explores) {
+ MarkerOptions markerOptions = Utils.Explore.constructMarkerOptions(getContext(), explore, markerLayoutView, markerGroupLayoutView, iconGenerator);
+ if (markerOptions != null) {
+ Marker marker = googleMap.addMarker(markerOptions);
+ JSONObject tagJson = Utils.Explore.constructMarkerTagJson(getContext(), marker.getTitle(), explore);
+ marker.setTag(tagJson);
+ markers.add(marker);
+ }
+ }
+ }
+ updateMarkers();
+ moveCameraToSpecificPosition();
+ }
+
+ private synchronized void clearMarkers() {
+ if (markers != null) {
+ for (Marker marker : markers) {
+ marker.remove();
+ }
+ markers.clear();
+ markers = null;
+ }
+ }
+
+ private void updateMarkers() {
+ float currentCameraZoom = googleMap.getCameraPosition().zoom;
+ boolean updateMarkerInfo = (currentCameraZoom != cameraZoom);
+ if (markers != null && !markers.isEmpty()) {
+ for (Marker marker : markers) {
+ if (updateMarkerInfo) {
+ boolean singleExploreMarker = Utils.Explore.optSingleExploreMarker(marker);
+ Utils.Explore.updateCustomMarkerAppearance(getContext(), marker, singleExploreMarker, currentCameraZoom, cameraZoom, markerLayoutView, markerGroupLayoutView, iconGenerator);
+ }
+ }
+ }
+ cameraZoom = currentCameraZoom;
+ }
+
+ private void updateMarkersVisibility() {
+ int currentFloorIndex = (mapControl != null) ? mapControl.getCurrentFloorIndex() : 0;
+ if (markers != null && !markers.isEmpty()) {
+ for (Marker marker : markers) {
+ Integer markerFloor = Utils.Explore.optMarkerLocationFloor(marker);
+ boolean markerVisible = (markerFloor == null) || (currentFloorIndex == markerFloor);
+ marker.setVisible(markerVisible);
+ }
+ }
+ }
+
+ private boolean onMarkerClicked(Marker marker) {
+ Object rawData = Utils.Explore.optExploreMarkerRawData(marker);
+ if (rawData != null) {
+ if (rawData instanceof HashMap) {
+ Gson gson = new Gson();
+ String rawDataToString = gson.toJson(rawData);
+ try {
+ rawData = new JSONObject(rawDataToString);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ } else if (rawData instanceof ArrayList) {
+ ArrayList rawDataList = (ArrayList) rawData;
+ rawData = new JSONArray(rawDataList);
+ }
+ JSONObject jsonArgs = new JSONObject();
+ try {
+ jsonArgs.put("mapId", mapId);
+ jsonArgs.put("explore", rawData);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ String methodArguments = jsonArgs.toString();
+ MainActivity.invokeFlutterMethod("map.explore.select", methodArguments);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean onMapClick(@NonNull LatLng latLng, @Nullable List list) {
+ JSONObject jsonArgs = new JSONObject();
+ try {
+ jsonArgs.put("mapId", mapId);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ String methodArguments = jsonArgs.toString();
+ MainActivity.invokeFlutterMethod("map.explore.clear", methodArguments);
+ return true;
+ }
+
+ private void relocateMyLocationButton() {
+ if (googleMapView == null) {
+ return;
+ }
+ View firstView = googleMapView.findViewById(Integer.parseInt("1"));
+ if (firstView == null) {
+ return;
+ }
+ ViewParent parentView = firstView.getParent();
+ if (!(parentView instanceof View)) {
+ return;
+ }
+ View myLocationButton = ((View) parentView).findViewById(Integer.parseInt("2"));
+ if (myLocationButton == null) {
+ return;
+ }
+ //Place it on bottom right
+ RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams) myLocationButton.getLayoutParams();
+ rlp.addRule(RelativeLayout.ALIGN_PARENT_TOP, 0);
+ rlp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
+ rlp.setMargins(0, 0, 30, 30);
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapViewController.java b/android/app/src/main/java/edu/illinois/covid/maps/MapViewController.java
new file mode 100644
index 00000000..9e182ee3
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapViewController.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+import io.flutter.plugin.platform.PlatformView;
+
+public class MapViewController implements PlatformView, MethodChannel.MethodCallHandler {
+
+ private Context context;
+ private PluginRegistry.Registrar registrar;
+ private MapView mapView;
+ private MethodChannel channel;
+
+ MapViewController(Context activityContext, PluginRegistry.Registrar registrar, int id, Object args) {
+ this.context = activityContext;
+ this.registrar = registrar;
+ mapView = new MapView(context, id, args);
+ channel = new MethodChannel(registrar.messenger(), "edu.illinois.covid/mapview_" + id);
+
+ channel.setMethodCallHandler(this);
+ }
+
+ @Override
+ public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
+ try {
+ if ("placePOIs".equals(methodCall.method)) {
+ showExploresOnMap(methodCall.arguments);
+ result.success(true);
+ } else if ("enable".equals(methodCall.method)) {
+ enableMap(methodCall.arguments);
+ result.success(true);
+ } else if ("enableMyLocation".equals(methodCall.method)) {
+ enableMyLocation(methodCall.arguments);
+ result.success(true);
+ } else {
+ result.notImplemented();
+ }
+ } catch (IllegalStateException exception) {
+ String errorMsg = String.format("Ignoring exception '%s'. See https://github.com/flutter/flutter/issues/29092 for details.", exception.toString());
+ Log.e("MapView", errorMsg);
+ exception.printStackTrace();
+ }
+ }
+
+ @Override
+ public View getView() {
+ return mapView;
+ }
+
+ @Override
+ public void dispose() {
+ if(mapView != null) {
+ mapView.onDestroy();
+ }
+ }
+
+ private void enableMap(Object enableObject) {
+ //Do not hide mapView, as initially GoogleMap shows blue screen for few seconds.
+
+ //Boolean enableBool = ((enableObject instanceof Boolean)) ? ((Boolean) enableObject) : null;
+ //boolean enable = (enableBool != null) && enableBool;
+ //mapView.setVisibility(enable ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ private void enableMyLocation(Object enableObject) {
+ Boolean enableBool = ((enableObject instanceof Boolean)) ? ((Boolean) enableObject) : null;
+ boolean enable = (enableBool != null) && enableBool;
+ mapView.enableMyLocation(enable);
+ }
+
+ private void showExploresOnMap(Object params) {
+ ArrayList explores = null;
+ HashMap options = null;
+ if (params instanceof HashMap) {
+ HashMap map = (HashMap) params;
+ explores = (ArrayList) map.get("explores");
+ options = (HashMap) map.get("options");
+ }
+ if (mapView != null) {
+ mapView.applyExplores(explores, options);
+ }
+ }
+}
diff --git a/android/app/src/main/java/edu/illinois/covid/maps/MapViewFactory.java b/android/app/src/main/java/edu/illinois/covid/maps/MapViewFactory.java
new file mode 100644
index 00000000..b8c58b56
--- /dev/null
+++ b/android/app/src/main/java/edu/illinois/covid/maps/MapViewFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package edu.illinois.covid.maps;
+
+import android.content.Context;
+
+import io.flutter.plugin.common.PluginRegistry;
+import io.flutter.plugin.common.StandardMessageCodec;
+import io.flutter.plugin.platform.PlatformView;
+import io.flutter.plugin.platform.PlatformViewFactory;
+
+public class MapViewFactory extends PlatformViewFactory {
+
+ private Context activityContext;
+ private final PluginRegistry.Registrar mPluginRegistrar;
+
+ public MapViewFactory(Context context, PluginRegistry.Registrar registrar) {
+ super(StandardMessageCodec.INSTANCE);
+ this.activityContext = context;
+ this.mPluginRegistrar = registrar;
+ }
+
+ @Override
+ public PlatformView create(Context context, int i, Object args) {
+ return new MapViewController(activityContext, mPluginRegistrar, i, args);
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
new file mode 100644
index 00000000..43ccd2fc
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
@@ -0,0 +1,65 @@
+package io.flutter.plugins;
+
+import io.flutter.plugin.common.PluginRegistry;
+import de.mintware.barcode_scan.BarcodeScanPlugin;
+import io.flutter.plugins.connectivity.ConnectivityPlugin;
+import io.flutter.plugins.deviceinfo.DeviceInfoPlugin;
+import io.flutter.plugins.firebase.core.FirebaseCorePlugin;
+import io.flutter.plugins.firebase.crashlytics.firebasecrashlytics.FirebaseCrashlyticsPlugin;
+import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin;
+import io.flutter.plugins.firebasemlvision.FirebaseMlVisionPlugin;
+import com.example.flutterimagecompress.FlutterImageCompressPlugin;
+import com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin;
+import com.whelksoft.flutter_native_timezone.FlutterNativeTimezonePlugin;
+import io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin;
+import io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin;
+import io.flutter.plugins.imagepicker.ImagePickerPlugin;
+import com.lyokone.location.LocationPlugin;
+import io.flutter.plugins.packageinfo.PackageInfoPlugin;
+import io.flutter.plugins.pathprovider.PathProviderPlugin;
+import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin;
+import com.tekartik.sqflite.SqflitePlugin;
+import name.avioli.unilinks.UniLinksPlugin;
+import io.flutter.plugins.urllauncher.UrlLauncherPlugin;
+import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin;
+
+/**
+ * Generated file. Do not edit.
+ */
+public final class GeneratedPluginRegistrant {
+ public static void registerWith(PluginRegistry registry) {
+ if (alreadyRegisteredWith(registry)) {
+ return;
+ }
+ BarcodeScanPlugin.registerWith(registry.registrarFor("de.mintware.barcode_scan.BarcodeScanPlugin"));
+ ConnectivityPlugin.registerWith(registry.registrarFor("io.flutter.plugins.connectivity.ConnectivityPlugin"));
+ DeviceInfoPlugin.registerWith(registry.registrarFor("io.flutter.plugins.deviceinfo.DeviceInfoPlugin"));
+ FirebaseCorePlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebase.core.FirebaseCorePlugin"));
+ FirebaseCrashlyticsPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebase.crashlytics.firebasecrashlytics.FirebaseCrashlyticsPlugin"));
+ FirebaseMessagingPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"));
+ FirebaseMlVisionPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebasemlvision.FirebaseMlVisionPlugin"));
+ FlutterImageCompressPlugin.registerWith(registry.registrarFor("com.example.flutterimagecompress.FlutterImageCompressPlugin"));
+ FlutterLocalNotificationsPlugin.registerWith(registry.registrarFor("com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin"));
+ FlutterNativeTimezonePlugin.registerWith(registry.registrarFor("com.whelksoft.flutter_native_timezone.FlutterNativeTimezonePlugin"));
+ FlutterAndroidLifecyclePlugin.registerWith(registry.registrarFor("io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin"));
+ FluttertoastPlugin.registerWith(registry.registrarFor("io.github.ponnamkarthik.toast.fluttertoast.FluttertoastPlugin"));
+ ImagePickerPlugin.registerWith(registry.registrarFor("io.flutter.plugins.imagepicker.ImagePickerPlugin"));
+ LocationPlugin.registerWith(registry.registrarFor("com.lyokone.location.LocationPlugin"));
+ PackageInfoPlugin.registerWith(registry.registrarFor("io.flutter.plugins.packageinfo.PackageInfoPlugin"));
+ PathProviderPlugin.registerWith(registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
+ SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"));
+ SqflitePlugin.registerWith(registry.registrarFor("com.tekartik.sqflite.SqflitePlugin"));
+ UniLinksPlugin.registerWith(registry.registrarFor("name.avioli.unilinks.UniLinksPlugin"));
+ UrlLauncherPlugin.registerWith(registry.registrarFor("io.flutter.plugins.urllauncher.UrlLauncherPlugin"));
+ WebViewFlutterPlugin.registerWith(registry.registrarFor("io.flutter.plugins.webviewflutter.WebViewFlutterPlugin"));
+ }
+
+ private static boolean alreadyRegisteredWith(PluginRegistry registry) {
+ final String key = GeneratedPluginRegistrant.class.getCanonicalName();
+ if (registry.hasPlugin(key)) {
+ return true;
+ }
+ registry.registrarFor(key);
+ return false;
+ }
+}
diff --git a/android/app/src/main/res/drawable-hdpi/splash_image.png b/android/app/src/main/res/drawable-hdpi/splash_image.png
new file mode 100644
index 00000000..a0e8aa5e
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash_image.png differ
diff --git a/android/app/src/main/res/drawable-ldpi/splash_image.png b/android/app/src/main/res/drawable-ldpi/splash_image.png
new file mode 100644
index 00000000..2d9a6a28
Binary files /dev/null and b/android/app/src/main/res/drawable-ldpi/splash_image.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/splash_image.png b/android/app/src/main/res/drawable-mdpi/splash_image.png
new file mode 100644
index 00000000..3990dbed
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash_image.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splash_image.png b/android/app/src/main/res/drawable-xhdpi/splash_image.png
new file mode 100644
index 00000000..19fc7b20
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash_image.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splash_image.png b/android/app/src/main/res/drawable-xxhdpi/splash_image.png
new file mode 100644
index 00000000..170b006a
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash_image.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash_image.png b/android/app/src/main/res/drawable-xxxhdpi/splash_image.png
new file mode 100644
index 00000000..fb024a7c
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash_image.png differ
diff --git a/android/app/src/main/res/drawable/app_icon.png b/android/app/src/main/res/drawable/app_icon.png
new file mode 100644
index 00000000..d233e3a2
Binary files /dev/null and b/android/app/src/main/res/drawable/app_icon.png differ
diff --git a/android/app/src/main/res/drawable/button_icon_nav_clear.png b/android/app/src/main/res/drawable/button_icon_nav_clear.png
new file mode 100644
index 00000000..e1955794
Binary files /dev/null and b/android/app/src/main/res/drawable/button_icon_nav_clear.png differ
diff --git a/android/app/src/main/res/drawable/button_icon_nav_location.png b/android/app/src/main/res/drawable/button_icon_nav_location.png
new file mode 100644
index 00000000..0b1e58e7
Binary files /dev/null and b/android/app/src/main/res/drawable/button_icon_nav_location.png differ
diff --git a/android/app/src/main/res/drawable/button_icon_nav_next.png b/android/app/src/main/res/drawable/button_icon_nav_next.png
new file mode 100644
index 00000000..6499528e
Binary files /dev/null and b/android/app/src/main/res/drawable/button_icon_nav_next.png differ
diff --git a/android/app/src/main/res/drawable/button_icon_nav_prev.png b/android/app/src/main/res/drawable/button_icon_nav_prev.png
new file mode 100644
index 00000000..d4b59a8f
Binary files /dev/null and b/android/app/src/main/res/drawable/button_icon_nav_prev.png differ
diff --git a/android/app/src/main/res/drawable/button_icon_nav_refresh.png b/android/app/src/main/res/drawable/button_icon_nav_refresh.png
new file mode 100644
index 00000000..2ffd0eff
Binary files /dev/null and b/android/app/src/main/res/drawable/button_icon_nav_refresh.png differ
diff --git a/android/app/src/main/res/drawable/chevron_left.png b/android/app/src/main/res/drawable/chevron_left.png
new file mode 100755
index 00000000..0bf34aba
Binary files /dev/null and b/android/app/src/main/res/drawable/chevron_left.png differ
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000..2dcc98c3
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/marker_default_teal.png b/android/app/src/main/res/drawable/marker_default_teal.png
new file mode 100644
index 00000000..b8a56840
Binary files /dev/null and b/android/app/src/main/res/drawable/marker_default_teal.png differ
diff --git a/android/app/src/main/res/drawable/marker_dining.png b/android/app/src/main/res/drawable/marker_dining.png
new file mode 100644
index 00000000..e608e4f0
Binary files /dev/null and b/android/app/src/main/res/drawable/marker_dining.png differ
diff --git a/android/app/src/main/res/drawable/marker_event.png b/android/app/src/main/res/drawable/marker_event.png
new file mode 100644
index 00000000..8646ec86
Binary files /dev/null and b/android/app/src/main/res/drawable/marker_event.png differ
diff --git a/android/app/src/main/res/drawable/marker_group_shape.xml b/android/app/src/main/res/drawable/marker_group_shape.xml
new file mode 100644
index 00000000..92eaaad5
--- /dev/null
+++ b/android/app/src/main/res/drawable/marker_group_shape.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/splash_image.png b/android/app/src/main/res/drawable/splash_image.png
new file mode 100644
index 00000000..a0e8aa5e
Binary files /dev/null and b/android/app/src/main/res/drawable/splash_image.png differ
diff --git a/android/app/src/main/res/drawable/travel_mode_bicycle.png b/android/app/src/main/res/drawable/travel_mode_bicycle.png
new file mode 100644
index 00000000..e3d7dcad
Binary files /dev/null and b/android/app/src/main/res/drawable/travel_mode_bicycle.png differ
diff --git a/android/app/src/main/res/drawable/travel_mode_drive.png b/android/app/src/main/res/drawable/travel_mode_drive.png
new file mode 100644
index 00000000..db3fa67d
Binary files /dev/null and b/android/app/src/main/res/drawable/travel_mode_drive.png differ
diff --git a/android/app/src/main/res/drawable/travel_mode_transit.png b/android/app/src/main/res/drawable/travel_mode_transit.png
new file mode 100644
index 00000000..60d26d2d
Binary files /dev/null and b/android/app/src/main/res/drawable/travel_mode_transit.png differ
diff --git a/android/app/src/main/res/drawable/travel_mode_unknown.png b/android/app/src/main/res/drawable/travel_mode_unknown.png
new file mode 100644
index 00000000..a39d5231
Binary files /dev/null and b/android/app/src/main/res/drawable/travel_mode_unknown.png differ
diff --git a/android/app/src/main/res/drawable/travel_mode_walk.png b/android/app/src/main/res/drawable/travel_mode_walk.png
new file mode 100644
index 00000000..79bd533b
Binary files /dev/null and b/android/app/src/main/res/drawable/travel_mode_walk.png differ
diff --git a/android/app/src/main/res/drawable/white_bg_black_border_shape.xml b/android/app/src/main/res/drawable/white_bg_black_border_shape.xml
new file mode 100644
index 00000000..6a67c1ac
--- /dev/null
+++ b/android/app/src/main/res/drawable/white_bg_black_border_shape.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/font/proximanova_extrabold.otf b/android/app/src/main/res/font/proximanova_extrabold.otf
new file mode 100755
index 00000000..cc2b8c8e
Binary files /dev/null and b/android/app/src/main/res/font/proximanova_extrabold.otf differ
diff --git a/android/app/src/main/res/font/proximanova_semibold.otf b/android/app/src/main/res/font/proximanova_semibold.otf
new file mode 100755
index 00000000..5436663e
Binary files /dev/null and b/android/app/src/main/res/font/proximanova_semibold.otf differ
diff --git a/android/app/src/main/res/layout/map_layout.xml b/android/app/src/main/res/layout/map_layout.xml
new file mode 100644
index 00000000..e48df3c4
--- /dev/null
+++ b/android/app/src/main/res/layout/map_layout.xml
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/map_pick_location_layout.xml b/android/app/src/main/res/layout/map_pick_location_layout.xml
new file mode 100644
index 00000000..f611f999
--- /dev/null
+++ b/android/app/src/main/res/layout/map_pick_location_layout.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/marker_group_layout.xml b/android/app/src/main/res/layout/marker_group_layout.xml
new file mode 100644
index 00000000..c03e2940
--- /dev/null
+++ b/android/app/src/main/res/layout/marker_group_layout.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/marker_info_layout.xml b/android/app/src/main/res/layout/marker_info_layout.xml
new file mode 100644
index 00000000..d22fa51e
--- /dev/null
+++ b/android/app/src/main/res/layout/marker_info_layout.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..bdf8e251
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
new file mode 100644
index 00000000..ee44d111
Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..916e3734
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..1393cf3c
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..7373ee05
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f46e189a
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap/ic_launcher.png b/android/app/src/main/res/mipmap/ic_launcher.png
new file mode 100644
index 00000000..bdf8e251
Binary files /dev/null and b/android/app/src/main/res/mipmap/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..964643cb
--- /dev/null
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ Safer Illinois
+
+ Hoy
+ Mañana
+ a
+
+ Explora
+ Eventos
+ Opciones para comer
+ Lugares
+ Lavanderías
+ Estacionamientos
+ OK
+
+
+ Direcciones
+ No se pudo encontrar la ruta.
+ No se pudo detectar la ubicación actual.
+ COMIENZO
+ TERMINAR
+ %1$d m | %2$d seg
+ Pierna %1$d / Paso %2$d
+ milla
+ millas
+ min
+
+
+ Mapas
+
+
+ Elegir ubicación
+ PERSONALIZADO
+ Por favor, seleccione una ubicación
+ Ubicación: %1$s
+ Salvar
+
+
+ Notificación de seguimiento de contactos del edificio
+ Seguimiento de contactos
+ El seguimiento de contactos para preservar la privacidad se está ejecutando actualmente
+ Seguimiento de contactos
+ Permitir notificaciones de exposición. ¿Quieres encender el bluetooth?
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 00000000..b0d49114
--- /dev/null
+++ b/android/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ Safer Illinois
+
+ 今天
+ 明天
+ 在
+
+ 探索
+ 大事記
+ 餐飲選擇
+ 地方
+ 洗衣房
+ 停車場
+ 好
+
+
+ 方向
+ 找不到路線.
+ 無法檢測到當前位置.
+ 開始
+ 完
+ %1$d 分 | %2$d 秒
+ 腿 %1$d / 步 %2$d
+ 英里
+ 英里
+ 分
+
+
+ 地圖
+
+
+ 選擇地點
+ 定制
+ 請選擇一個位置
+ 位置: %1$s
+ 保存
+
+
+ 建築物聯繫人跟踪通知
+ 聯繫人跟踪
+ 當前正在運行保護隱私的聯繫人跟踪
+ 聯繫人跟踪
+ 允許曝光通知. 您要打開藍牙嗎?
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..8e14fe2d
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,30 @@
+
+
+
+ #ff13294b
+ #ff0e203b
+ #ffe84a27
+ #fff29835
+ #ff5fa7a3
+ #00000000
+ @color/darkBlueGrey
+
+ #ffffff
+ #000000
+ #99ffffff
+ #66606060
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..83823356
--- /dev/null
+++ b/android/app/src/main/res/values/dimens.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ 22dp
+ 26dp
+ 30dp
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e11bd99e
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ Safer Illinois
+
+ Today
+ Tomorrow
+ at
+
+ Explores
+ Events
+ Dining Options
+ Places
+ Laundries
+ Parking lots
+ OK
+
+
+ Directions
+ Failed to find route.
+ Failed to detect current location.
+ START
+ FINISH
+ %1$d m | %2$d sec
+ Leg %1$d / Step %2$d
+ mile
+ miles
+ min
+
+
+ Maps
+
+
+ Pick Location
+ CUSTOM
+ Please, select a location
+ Location: %1$s
+ Save
+
+
+ Building contact tracing notification
+ Contact tracing
+ Privacy-preserving contact tracing is currently running
+ Contact tracing
+ Allow exposure notifications. Do you want to turn the bluetooth on?
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..3ac51923
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..b050c82c
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 00000000..ad5a92b4
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ ext {
+ gradle_version = '3.5.3'
+ kotlin_version = '1.3.61'
+ }
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://maven.fabric.io/public' }
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:$gradle_version"
+ classpath "io.fabric.tools:gradle:1.31.0"
+ classpath "com.google.gms:google-services:4.3.3"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ flatDir {
+ dirs '../lib'
+ }
+ maven { url 'https://maven.microblink.com' }
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 00000000..96af46b9
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,20 @@
+#
+# Copyright 2020 Board of Trustees of the University of Illinois.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.gradle.jvmargs=-Xmx1536M
+android.enableJetifier=true
+android.useAndroidX=true
+android.enableR8=true
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..13372aef
Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..b55a0129
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,22 @@
+#
+# Copyright 2020 Board of Trustees of the University of Illinois.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#Tue Oct 22 15:06:41 EEST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/android/gradlew b/android/gradlew
new file mode 100755
index 00000000..9d82f789
--- /dev/null
+++ b/android/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/android/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/illini_android.iml b/android/illini_android.iml
new file mode 100644
index 00000000..269c4d74
--- /dev/null
+++ b/android/illini_android.iml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 00000000..83b42406
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 Board of Trustees of the University of Illinois.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/assets/assets.json b/assets/assets.json
new file mode 100644
index 00000000..27854803
--- /dev/null
+++ b/assets/assets.json
@@ -0,0 +1,1634 @@
+{
+ "dining": {
+ "food_types": ["Halal", "Kosher", "Sustainable Seafood", "Vegan", "Vegetarian"],
+ "food_ingredients": ["Alcohol", "Coconut", "Corn", "Eggs", "Fish", "Gelatin", "Gluten", "Milk", "MSG", "Peanuts", "Pork", "Red Dye", "Sesame", "Shellfish", "Soy", "Sulfites", "Tree Nuts", "Wheat"],
+ "strings": {
+ "en": {"Halal":"Halal", "Kosher":"Kosher", "Sustainable Seafood":"Sustainable Seafood", "Vegan":"Vegan", "Vegetarian":"Vegetarian", "Alcohol":"Alcohol", "Coconut":"Coconut", "Corn":"Corn", "Eggs":"Eggs", "Fish":"Fish", "Gelatin":"Gelatin", "Gluten":"Gluten", "Milk":"Milk", "MSG":"MSG", "Peanuts":"Peanuts", "Pork":"Pork", "Red Dye":"Red Dye", "Sesame":"Sesame", "Shellfish":"Shellfish", "Soy":"Soy", "Sulfites":"Sulfites", "Tree Nuts":"Tree Nuts", "Wheat":"Wheat", "Serving":"Serving", "Calories":"Calories", "CaloriesFromFat":"Calories From Fat", "SaturatedFat":"Saturated Fat", "Transfat":"Transfat", "PolyUnsaturatedFat":"Poly Unsaturated Fat", "CalciumPercent":"Calcium Percent", "IronPercent":"Iron Percent", "Sugars":"Sugars", "TotalCarbs":"Total Carbs", "DietaryFiber":"Dietary Fiber", "Protein":"Protein", "TotalFat":"Total Fat", "Cholesterol":"Cholesterol", "Sodium":"Sodium"},
+ "es": {"Halal":"Halal", "Kosher":"Kosher", "Sustainable Seafood":"Sustainable Seafood", "Vegan":"Vegan", "Vegetarian":"Vegetarian", "Alcohol":"Alcohol", "Coconut":"Coconut", "Corn":"Corn", "Eggs":"Eggs", "Fish":"Fish", "Gelatin":"Gelatin", "Gluten":"Gluten", "Milk":"Milk", "MSG":"MSG", "Peanuts":"Peanuts", "Pork":"Pork", "Red Dye":"Red Dye", "Sesame":"Sesame", "Shellfish":"Shellfish", "Soy":"Soy", "Sulfites":"Sulfites", "Tree Nuts":"Tree Nuts", "Wheat":"Wheat", "Serving":"Porción", "Calories":"Calorías", "CaloriesFromFat":"Calorías de la grasa", "SaturatedFat":"Grasas saturadas", "Transfat":"Grasa Trans", "PolyUnsaturatedFat":"Grasa poliinsaturada", "CalciumPercent":"Porcentaje de calcio", "IronPercent":"Porcentaje de hierro", "Sugars":"Azúcares", "TotalCarbs":"Carbohidratos totales", "DietaryFiber":"Fibra dietética", "Protein":"Proteína", "TotalFat":"Grasa total", "Cholesterol":"Colesterol", "Sodium":"Sodio"},
+ "zh": {"Halal":"Halal", "Kosher":"Kosher", "Sustainable Seafood":"Sustainable Seafood", "Vegan":"Vegan", "Vegetarian":"Vegetarian", "Alcohol":"Alcohol", "Coconut":"Coconut", "Corn":"Corn", "Eggs":"Eggs", "Fish":"Fish", "Gelatin":"Gelatin", "Gluten":"Gluten", "Milk":"Milk", "MSG":"MSG", "Peanuts":"Peanuts", "Pork":"Pork", "Red Dye":"Red Dye", "Sesame":"Sesame", "Shellfish":"Shellfish", "Soy":"Soy", "Sulfites":"Sulfites", "Tree Nuts":"Tree Nuts", "Wheat":"Wheat", "Serving":"服务", "Calories":"卡路里", "CaloriesFromFat":"来自脂肪的卡路里", "SaturatedFat":"饱和脂肪", "Transfat":"反式脂肪", "PolyUnsaturatedFat":"多不饱和脂肪", "CalciumPercent":"钙百分比", "IronPercent":"铁百分比", "Sugars":"糖", "TotalCarbs":"碳水化合物总量", "DietaryFiber":"膳食纤维", "Protein":"蛋白质", "TotalFat":"总脂肪", "Cholesterol":"胆固醇", "Sodium":"钠"}
+ }
+ },
+
+ "sports": {
+ "types":[
+ {"shortName": "baseball", "name":"Baseball", "custom_name":"Baseball", "hasPosition":true, "hasHeight":true, "hasWeight":true, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"men", "ticketed":false, "icon":"images/athletics-baseball-orange.png"},
+ {"shortName": "mbball", "name":"Men's Basketball", "custom_name":"Basketball", "hasPosition":true, "hasHeight":true, "hasWeight":true, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"men", "ticketed":true, "icon":"images/athletics-basketball-orange.png"},
+ {"shortName": "mcross", "name":"Men's Cross Country", "custom_name":"Cross Country", "hasPosition":false, "hasHeight":false, "hasWeight":false, "hasSortByPosition":false, "hasSortByNumber":false, "gender":"men", "ticketed":false, "icon":"images/athletics-cross-orange.png"},
+ {"shortName": "football", "name":"Football", "custom_name":"Football", "hasPosition":true, "hasHeight":true, "hasWeight":true, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"men", "ticketed":true, "icon":"images/athletics-football-orange.png"},
+ {"shortName": "mgolf", "name":"Men's Golf", "custom_name":"Golf", "hasPosition":false, "hasHeight":false, "hasWeight":false, "hasSortByPosition":false, "hasSortByNumber":false, "gender":"men", "ticketed":false, "icon":"images/athletics-golf-orange.png"},
+ {"shortName": "mgym", "name":"Men's Gymnastics", "custom_name":"Gymnastics", "hasPosition":true, "hasHeight":false, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":true, "gender":"men", "ticketed":false, "icon":"images/athletics-gymnastics-orange.png"},
+ {"shortName": "mten", "name":"Men's Tennis", "custom_name":"Tennis", "hasPosition":false, "hasHeight":true, "hasWeight":false, "hasSortByPosition":false, "hasSortByNumber":false, "hasScores":true, "gender":"men", "ticketed":false, "icon":"images/athletics-tennis-orange.png"},
+ {"shortName": "mtrack", "name":"Men's Track & Field", "custom_name":"Track & Field", "hasPosition":true, "hasHeight":false, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":false, "gender":"men", "ticketed":false, "icon":"images/athletics-track-orange.png"},
+ {"shortName": "wrestling", "name":"Men's Wrestling", "custom_name":"Wrestling", "hasPosition":false, "hasHeight":true, "hasWeight":true, "hasSortByPosition":false, "hasSortByNumber":false, "gender":"men", "ticketed":false, "icon":"images/athletics-wrestling-orange.png"},
+ {"shortName": "wbball", "name":"Women's Basketball", "custom_name":"Basketball", "hasPosition":true, "hasHeight":true, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"women", "ticketed":true, "icon":"images/athletics-basketball-orange.png"},
+ {"shortName": "wcross", "name":"Women's Cross Country", "custom_name":"Cross Country", "hasPosition":false, "hasHeight":false, "hasWeight":false, "hasSortByPosition":false, "hasSortByNumber":false, "gender":"women", "ticketed":false, "icon":"images/athletics-cross-orange.png"},
+ {"shortName": "wgolf", "name":"Women's Golf", "custom_name":"Golf", "hasPosition":false, "hasHeight":false, "hasWeight":false, "hasSortByPosition":false, "hasSortByNumber":false, "gender":"women", "ticketed":false, "icon":"images/athletics-golf-orange.png"},
+ {"shortName": "wgym", "name":"Women's Gymnastics", "custom_name":"Gymnastics", "hasPosition":true, "hasHeight":true, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":false, "gender":"women", "ticketed":false, "icon":"images/athletics-gymnastics-orange.png"},
+ {"shortName": "wsoc", "name":"Women's Soccer", "custom_name":"Soccer", "hasPosition":true, "hasHeight":true, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"women", "ticketed":false, "icon":"images/athletics-soccer-orange.png"},
+ {"shortName": "softball", "name":"Softball", "custom_name":"Softball", "hasPosition":true, "hasHeight":true, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"women", "ticketed":false, "icon":"images/athletics-softball-orange.png"},
+ {"shortName": "wswim", "name":"Women's Swimming & Diving", "custom_name":"Swimming & Diving", "hasPosition":true, "hasHeight":false, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":false, "gender":"women", "ticketed":false, "icon":"images/athletics-swim-orange.png"},
+ {"shortName": "wten", "name":"Women's Tennis", "custom_name":"Tennis", "hasPosition":false, "hasHeight":false, "hasWeight":false, "hasSortByPosition":false, "hasSortByNumber":false, "hasScores":true, "gender":"women", "ticketed":false, "icon":"images/athletics-tennis-orange.png"},
+ {"shortName": "wtrack", "name":"Women's Track & Field", "custom_name":"Track & Field", "hasPosition":true, "hasHeight":false, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":false, "gender":"women", "ticketed":false, "icon":"images/athletics-track-orange.png"},
+ {"shortName": "wvball", "name":"Women's Volleyball", "custom_name":"Volleyball", "hasPosition":true, "hasHeight":true, "hasWeight":false, "hasSortByPosition":true, "hasSortByNumber":true, "hasScores":true, "gender":"women", "ticketed":true, "icon":"images/athletics-volleyball-orange.png"}
+ ]
+ },
+
+ "images": {
+ "random": {
+ "events":{
+ "Academic": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/536ab9a5-c9c8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ba94d55d-c9ca-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/8d80c73d-cab8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/5c8ecdc2-cab8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e7eac331-cab8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/1901c69c-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/41b41fcd-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/41b41fcd-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/98296276-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b2ef73d2-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/d24aa5df-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/fe1ce39d-cab9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/2b12d45f-caba-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/50436052-caba-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/86e656ff-caba-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/bff31abc-caba-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e1f30139-caba-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/02ea99ca-cabb-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/31840f7d-cabb-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/604d7a95-cabb-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ae46c0de-cabb-11e9-88b6-0a58a9feac2a.webp"],
+ "Entertainment": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a200ab0c-c9cc-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/dc059d5d-c9cc-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4bd2d215-cde2-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/960743f8-cde2-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b5c38d95-cde2-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/d752bce5-cde2-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ef962306-cde2-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/0b591886-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/231194d6-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3f742b15-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/6149506f-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/76a50491-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/909f42ab-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a82f30df-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/c43ba19d-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e409138f-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ff7f0567-cde3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/16dd1855-cde4-11e9-88b6-0a58a9feac2a.webp"],
+ "Community": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3adfe066-c9ce-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/6bb3f9ad-c9ce-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4085db95-cdf8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/5a807ff5-cdf8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b2c378cf-cdf8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/cb2c02a8-cdf8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e41bc78a-cdf8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ffaeef8a-cdf8-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/179a9726-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3081cc2e-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/49256c79-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/68254af9-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/860161c1-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/9e2c7cfd-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b2178b24-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/c92a1089-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/df397ce2-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/f8a5f87e-cdf9-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/121d971e-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/2c900ef3-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/42fcbae0-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/5b5335cd-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/707bc956-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/8b8c01c8-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a383594c-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b989b49e-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/d0e119a8-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e5bca07a-cdfa-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/00ac8410-cdfb-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/13aa3f3e-cdfb-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/2978c9dc-cdfb-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3da9c481-cdfb-11e9-88b6-0a58a9feac2a.webp"],
+ "Career Development": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/31c7ffd8-c9d4-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/6f192cf4-c9d4-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/35b8a0a3-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4d7c80f3-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/6a1b897c-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/991422fd-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/aec5f88a-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/d130c6c5-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/efccd5cd-cf63-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/10d5f5af-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3d3741b6-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/5c99bfb1-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/76c5201c-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/8b203e82-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/aa884883-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/c1f1b2f2-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/dd587562-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/f95bc33b-cf64-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/12e89ad3-cf65-11e9-88b6-0a58a9feac2a.webp"],
+ "Athletics": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b475bd11-c9d4-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/d604fc38-c9d4-11e9-88b6-0a58a9feac2a.webp"],
+ "Other": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/2890eff7-c9d5-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/51cb8f85-c9d5-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ec0606d4-d260-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/05874ca8-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/1fa1b6f0-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/36b454f9-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4d715915-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/73410ea5-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/89b66a33-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a06eaea5-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b86cb864-d261-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/04e35b53-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3fc504de-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/59e10e64-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/7a8735e6-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/97131e28-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ad2a0f39-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/c725839a-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e4aa8b4a-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ff7d982d-d262-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/1d45ec8a-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/3b49b1c0-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/53943299-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/73441f37-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/8cada753-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a7fc273c-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/c711f5f2-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/f93611a0-d263-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/108cec19-d264-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/2f74f16b-d264-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4d8e7b51-d264-11e9-88b6-0a58a9feac2a.webp"],
+ "Recreation": ["https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/c085d844-c9d5-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/1d89fdf1-c9d6-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/0ecf5ef2-d265-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/24bba90b-d265-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/1299a8cb-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/37803339-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/51e00fb5-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/740bca52-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/9a3871f5-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b4dd8f5c-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/dd84d091-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/fe42ecf2-d296-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/23cd21e7-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4b499ff8-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/74dbadd4-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/8bc0c32b-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a654ab5d-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/bfd6a22d-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/e2a4b86c-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/fb4c18a2-d297-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/1b26aa99-d298-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/36aada45-d298-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/4fac59ef-d298-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/741709bd-d298-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/8a794bad-d298-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/a2c995d6-d298-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/b8169210-d298-11e9-88b6-0a58a9feac2a.webp"]
+ },
+ "sports":{
+ "baseball": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/7746fb3f-d36f-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/cb753691-d36f-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/e807bab6-d9b0-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/03ed7808-d9b1-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/1fa33439-d9b1-11e9-88b6-0a58a9feac2a.webp"],
+ "mbball": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/01dbdaff-d370-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/25ebd2ee-d370-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/4595ef6b-d370-11e9-88b6-0a58a9feac2a.webp"],
+ "mcross": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/200cd2f3-d29a-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/8d435b9b-d29a-11e9-88b6-0a58a9feac2a.webp"],
+ "football": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/d8cbf697-cab6-11e9-88b6-0a58a9feac2a.webp","https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/038e18c5-cab7-11e9-88b6-0a58a9feac2a.webp","https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/24870af6-cab7-11e9-88b6-0a58a9feac2a.webp","https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/4455ebec-cab7-11e9-88b6-0a58a9feac2a.webp","https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/63fda99f-cab7-11e9-88b6-0a58a9feac2a.webp"],
+ "mgolf": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/8b0c9120-d370-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/aaf66464-d370-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/cb0c922f-d370-11e9-88b6-0a58a9feac2a.webp"],
+ "mgym": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/f677a5c2-d370-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/16e511a6-d371-11e9-88b6-0a58a9feac2a.webp"],
+ "mten": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/5b20eef9-d371-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/769e10f6-d371-11e9-88b6-0a58a9feac2a.webp"],
+ "mtrack": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/trackandfield/tout/c58e5654-d29c-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/trackandfield/tout/aaf95c42-d29d-11e9-88b6-0a58a9feac2a.webp"],
+ "wrestling": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/wrestling/tout/04783cae-d29b-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/wrestling/tout/10cfd476-d29c-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/wrestling/tout/f9da35cb-d9af-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/wrestling/tout/15f81545-d9b0-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/wrestling/tout/2d85f86c-d9b0-11e9-88b6-0a58a9feac2a.webp"],
+ "wbball": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/b663be7a-d371-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/ded21df5-d371-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/fcdac77b-d371-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/611e2428-d816-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/80d688eb-d816-11e9-88b6-0a58a9feac2a.webp"],
+ "wcross": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/33b3ac5b-d372-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/5008312a-d372-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/482d69f4-d9af-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/696e1fe8-d9af-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/81933383-d9af-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/crosscountry/tout/9840bee9-d9af-11e9-88b6-0a58a9feac2a.webp"],
+ "wgolf": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/7a3276a7-d372-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/8dbba2b8-d372-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/be5fce7b-d9ad-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/da5837f3-d9ad-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/f4da3ab2-d9ad-11e9-88b6-0a58a9feac2a.webp"],
+ "wgym": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/ae63d3c2-d372-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/cc48ed21-d372-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/e6d01799-d372-11e9-88b6-0a58a9feac2a.webp"],
+ "wsoc": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/79cdfd2e-d29f-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/119a5d80-d373-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/fc95e333-d8d3-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/1efe4a3a-d8d4-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/football/tout/40140e24-d8d4-11e9-88b6-0a58a9feac2a.webp"],
+ "softball": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/42213b9c-d373-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/golf/tout/5f4c633c-d373-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/62fdc77b-d817-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/93dfecae-d817-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/baseball/tout/b0a2bdb0-d817-11e9-88b6-0a58a9feac2a.webp"],
+ "wswim": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/a027b8dc-d373-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/c0c87e50-d373-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/d81943ef-d373-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/0b6484ab-d8d5-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/3237cfb7-d8d5-11e9-88b6-0a58a9feac2a.webp"],
+ "wten": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/0a965cd0-d374-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/61c1817d-d815-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/85318155-d815-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/ac9c35c6-d815-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/tennis/tout/cfc157cf-d815-11e9-88b6-0a58a9feac2a.webp"],
+ "wtrack": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/trackandfield/tout/3912674b-d374-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/trackandfield/tout/55934f55-d374-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/trackandfield/tout/6f84a592-d374-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/trackandfield/tout/85507adb-d374-11e9-88b6-0a58a9feac2a.webp"],
+ "wvball": ["https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/b5b2c188-d29e-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/gimnastics/tout/e25bc8f6-d29e-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/642c5674-d814-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/907ac2c7-d814-11e9-88b6-0a58a9feac2a.webp", "https://rokwire-images.s3.us-east-2.amazonaws.com/athletics/basketball/tout/b6963a95-d814-11e9-88b6-0a58a9feac2a.webp"]
+ }
+ }
+ },
+
+ "reminders": {
+ "content":[
+ {"id":"ST20200508","date":"2020-05-08","label":"Final Exams Begin (Spring 2020)"},
+ {"id":"ST20200515","date":"2020-05-15","label":"Final Exams End (Spring 2020)"},
+ {"id":"SD20200515","date":"2020-05-15","label":"Deadline to submit a Certified Housing Reciprocal Release Application for Fall 2020 for continuing students"},
+ {"id":"SA20200515","date":"2020-05-15","label":"Student health insurance change/waiver period begins for coverage period May 16, 2020 - August 21, 2020"},
+ {"id":"SD20200516","date":"2020-05-16","label":"University Housing undergraduate residence halls close at 3 pm"},
+ {"id":"2ST0200619","date":"2020-06-19","label":"Student health insurance change/waiver period ends for coverage period May 16, 2020 - August 21, 2020"}
+ ]
+ },
+
+ "geo_fence": {
+ "regions": [
+ {"id":"@MH", "types":["test", "voter"], "name":"633 Trumbull Ave, Novato, CA 94947, USA", "enabled": true, "location": {"latitude": 38.104240, "longitude": -122.604582, "radius": 200}},
+ {"id":"@JP", "types":["test", "voter"], "name":"780 Seale Ave, Palo Alto, CA 94303, USA", "enabled": true, "location": {"latitude": 37.441194, "longitude": -122.137691, "radius": 200}},
+ {"id":"@Rokwire", "types":["test"], "name":"1301 W Springfield Ave, Urbana, IL 61801, USA", "enabled": true, "location": {"latitude": 40.112501, "longitude": -88.226915, "radius": 200}},
+ {"id":"@Misho", "types":["test"], "name":"ul. Bratovan 3B, 1505 Reduta, Sofia", "enabled": true, "location": {"latitude": 42.690307, "longitude": 23.364563, "radius": 200}},
+
+ {"id":"state_farm_center", "types":["stadium"], "name":"State Farm Center", "enabled": true, "location": {"latitude": 40.096247, "longitude": -88.235923, "radius": 280}},
+ {"id":"memorial_stadium", "types":["stadium"], "name":"Memorial Stadium", "enabled": true, "location": {"latitude": 40.099187, "longitude": -88.235956, "radius": 150}},
+
+ {"id":"illini_union", "types":["voter"], "name":"Illini Union", "enabled": true, "location": {"latitude": 40.109211, "longitude": -88.227221, "radius": 100}},
+ {"id":"arc", "types":["voter"], "name":"ARC", "enabled": true, "location": {"latitude": 40.100729, "longitude": -88.235799, "radius": 150}},
+
+ {"id":"MTD", "types":["test", "MTD"], "name":"Champaign–Urbana MTD", "enabled": true, "beacon": {"uuid": "d7d5a51f-ce7f-4a4c-a944-bed73578b201"}}
+ ]
+ },
+
+ "voter": {
+ "rules": [
+ {
+ "date_start": null,
+ "date_end": "2020/02/05",
+ "NRV_title": "widget.voter.nrv_title.registered_to_vote.question",
+ "NRV_text": "widget.voter.nrv_text.register_online.text",
+ "NRV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "rv_yes"
+ },
+ {
+ "label": "widget.voter.option.register_now",
+ "value": "nrv_place"
+ }
+ ],
+ "NRV_place_title": "widget.voter.nrv_place_title.place_to_vote.question",
+ "NRV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "https://ova.elections.il.gov/"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "https://illinois.turbovote.org/"
+ }
+ ],
+ "NRV_alert": "widget.voter.nrv_alert.text",
+ "RV_place_title": "widget.voter.rv_place_title.place_to_vote.question",
+ "RV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "champaign"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "elsewhere"
+ }
+ ],
+ "RV_title": "widget.voter.rv_title.signed_to_vote_by_mail.question",
+ "RV_text": "widget.voter.rv_text.vote_by_mail_ballot.text",
+ "RV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "vbm_yes"
+ },
+ {
+ "label": "widget.voter.option.vote_by_mail",
+ "value": "rv_url"
+ },
+ {
+ "label": "widget.voter.option.no_thanks",
+ "value": "vbm_no"
+ }
+ ],
+ "RV_url": "https://www.champaigncountyclerk.com/elections/vote-by-mail-reside"
+ },
+ {
+ "date_start": "2020/02/06",
+ "date_end": "2020/02/18",
+ "NRV_title": "widget.voter.nrv_title.registered_to_vote.question",
+ "NRV_text": "widget.voter.nrv_text.register_online.text",
+ "NRV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "rv_yes"
+ },
+ {
+ "label": "widget.voter.option.register_now",
+ "value": "nrv_place"
+ }
+ ],
+ "NRV_place_title": "widget.voter.nrv_place_title.place_to_vote.question",
+ "NRV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "https://ova.elections.il.gov/"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "https://illinois.turbovote.org/"
+ }
+ ],
+ "NRV_alert": "widget.voter.nrv_alert.text",
+ "RV_place_title": "widget.voter.rv_place_title.place_to_vote.question",
+ "RV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "champaign"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "elsewhere"
+ }
+ ],
+ "RV_title": "widget.voter.rv_title.signed_to_vote_by_mail.question",
+ "RV_text": "widget.voter.rv_text.vote_by_mail_ballot.text",
+ "RV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "vbm_yes"
+ },
+ {
+ "label": "widget.voter.option.vote_by_mail",
+ "value": "rv_url"
+ },
+ {
+ "label": "widget.voter.option.no_thanks",
+ "value": "vbm_no"
+ }
+ ],
+ "RV_url": "https://www.champaigncountyclerk.com/elections/vote-by-mail-reside",
+ "RV_alert": "widget.voter.rv_alert.vote_early.text",
+ "VBM_text": "widget.voter.vbm_text.early_voting_clerk.text",
+ "VBM_button": "widget.voter.option.more_info",
+ "VBM_url": "https://www.champaigncountyclerk.com/elections/early-voting"
+ },
+ {
+ "date_start": "2020/02/19",
+ "date_end": "2020/03/01",
+ "NRV_title": "widget.voter.nrv_title.registered_to_vote.question",
+ "NRV_text": "widget.voter.nrv_text.register_vote_today.text",
+ "NRV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "rv_yes"
+ }
+ ],
+ "NRV_alert": "widget.voter.nrv_alert.text",
+ "RV_place_title": "widget.voter.rv_place_title.place_to_vote.question",
+ "RV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "champaign"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "elsewhere"
+ }
+ ],
+ "RV_title": "widget.voter.rv_title.signed_to_vote_by_mail.question",
+ "RV_text": "widget.voter.rv_text.vote_by_mail_ballot.text",
+ "RV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "vbm_yes"
+ },
+ {
+ "label": "widget.voter.option.vote_by_mail",
+ "value": "rv_url"
+ },
+ {
+ "label": "widget.voter.option.no_thanks",
+ "value": "vbm_no"
+ }
+ ],
+ "RV_url": "https://www.champaigncountyclerk.com/elections/vote-by-mail-reside",
+ "RV_alert": "widget.voter.rv_alert.vote_early.text",
+ "VBM_text": "widget.voter.vbm_text.early_voting_clerk.text",
+ "VBM_button": "widget.voter.option.more_info",
+ "VBM_url": "https://www.champaigncountyclerk.com/elections/early-voting"
+ },
+ {
+ "date_start": "2020/03/02",
+ "date_end": "2020/03/11",
+ "NRV_title": "widget.voter.nrv_title.registered_to_vote.question",
+ "NRV_text": "widget.voter.nrv_text.register_vote_today.text",
+ "NRV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "rv_yes"
+ }
+ ],
+ "NRV_alert": "widget.voter.nrv_alert.text",
+ "RV_place_title": "widget.voter.rv_place_title.place_to_vote.question",
+ "RV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "champaign"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "elsewhere"
+ }
+ ],
+ "RV_title": "widget.voter.rv_title.signed_to_vote_by_mail.question",
+ "RV_text": "widget.voter.rv_text.vote_by_mail_ballot.text",
+ "RV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "vbm_yes"
+ },
+ {
+ "label": "widget.voter.option.vote_by_mail",
+ "value": "rv_url"
+ },
+ {
+ "label": "widget.voter.option.no_thanks",
+ "value": "vbm_no"
+ }
+ ],
+ "RV_url": "https://www.champaigncountyclerk.com/elections/vote-by-mail-reside",
+ "RV_alert": "widget.voter.rv_alert.vote_early.text",
+ "VBM_text": "widget.voter.vbm_text.no_need_to_wait.text",
+ "VBM_button": "widget.voter.option.more_info",
+ "VBM_url": "https://www.champaigncountyclerk.com/elections/early-voting"
+ },
+ {
+ "date_start": "2020/03/12",
+ "date_end": "2020/03/16",
+ "NRV_title": "widget.voter.nrv_title.registered_to_vote.question",
+ "NRV_text": "widget.voter.nrv_text.grace_period_open.text",
+ "NRV_options": [
+ {
+ "label": "widget.voter.option.yes",
+ "value": "rv_yes"
+ }
+ ],
+ "NRV_alert": "widget.voter.nrv_alert.text",
+ "RV_place_title": "widget.voter.rv_place_title.place_to_vote.question",
+ "RV_place_options": [
+ {
+ "label": "widget.voter.option.champaign_county",
+ "value": "champaign"
+ },
+ {
+ "label": "widget.voter.option.elsewhere",
+ "value": "elsewhere"
+ }
+ ],
+ "RV_title": "widget.voter.rv_title.early_voting.text",
+ "RV_text": "widget.voter.rv_text.vote_now.text",
+ "RV_options": [
+ {
+ "label": "widget.voter.option.more_info",
+ "value": "rv_url"
+ }
+ ],
+ "RV_url": "https://www.champaigncountyclerk.com/elections/early-voting",
+ "RV_alert": "widget.voter.rv_alert.vote_early.text",
+ "hide_for_period": true
+ },
+ {
+ "date_start": "2020/05/18"
+ }
+ ]
+ },
+
+ "wellness": {
+ "panels": {
+ "home": {
+ "header": {
+ "image": "group-4.png",
+ "title": "panel.wellness.home.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.home.description.main_text",
+ "secondary_text": "panel.wellness.home.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.home.eight_dimensions.header.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.home.activities.physical",
+ "font_size": 20,
+ "image": "physical.png",
+ "action": {
+ "name": "panel",
+ "source": "physical"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.mental",
+ "font_size": 20,
+ "image": "mental.png",
+ "action": {
+ "name": "panel",
+ "source": "mental"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.environmental",
+ "image": "environmental.png",
+ "action": {
+ "name": "panel",
+ "source": "environmental"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.financial",
+ "font_size": 20,
+ "image": "financial-2.png",
+ "action": {
+ "name": "panel",
+ "source": "financial"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.spiritual",
+ "font_size": 20,
+ "image": "spiritual.png",
+ "action": {
+ "name": "panel",
+ "source": "spiritual"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.vocational",
+ "font_size": 20,
+ "image": "vocational.png",
+ "action": {
+ "name": "panel",
+ "source": "vocational"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.emotional",
+ "font_size": 20,
+ "image": "emotional.png",
+ "action": {
+ "name": "panel",
+ "source": "emotional"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.social",
+ "font_size": 20,
+ "image": "social.png",
+ "action": {
+ "name": "panel",
+ "source": "social"
+ }
+ }
+ ]
+ },
+ {
+ "header": {
+ "icon": "icon-schedule.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.home.activities.interactive_mood_meter",
+ "image": "group-7.png",
+ "action": {
+ "name": "web",
+ "source": "http://mhcwellness.illinois.edu/interactive-mood-meter"
+ }
+ },
+ {
+ "title": "panel.wellness.home.activities.memory_mix_up",
+ "image": "group-2.png",
+ "action": {
+ "name": "web",
+ "source": "http://mhcwellness.illinois.edu/memory-mixup"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.upace",
+ "image": "group-18.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/group-fitness/upace-app/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.kognito_at_risk",
+ "image": "kognito.png",
+ "action": {
+ "name": "web",
+ "source": "https://counselingcenter.illinois.edu/emergency/kognito-risk-suicide-prevention-training"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "physical": {
+ "header": {
+ "image": "group.png",
+ "title": "panel.wellness.physical.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.physical.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.physical.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.upace",
+ "image": "group-18.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/group-fitness/upace-app/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "title": "panel.wellness.common.resources.campus_recreation.title",
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.physical.resources.personal_training",
+ "url": "https://campusrec.illinois.edu/programs/personal-training/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.physical.resources.group_fitness",
+ "url": "https://campusrec.illinois.edu/programs/group-fitness/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.physical.resources.aquatics",
+ "url": "https://campusrec.illinois.edu/programs/aquatics/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.physical.resources.food_nutrition",
+ "url": "https://campusrec.illinois.edu/programs/student-wellness/nutrition-food/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ],
+ "social_media": [
+ {
+ "type": "instagram",
+ "url": "https://www.instagram.com/illinoiscampusrec/"
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_health_center.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.physical.resources.mymckinley.title",
+ "url":"https://mymckinley.illinois.edu/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.appointments_mckinley",
+ "url":"http://mckinley.illinois.edu/medical-services/appointments",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.immunization",
+ "url":"http://mckinley.illinois.edu/medical-services/immunization-allergy-travel-clinic",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.womens_health",
+ "url":"http://mckinley.illinois.edu/medical-services/womens-health",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.fitness",
+ "url":"http://mckinley.illinois.edu/health-education/fitness",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.nutrition",
+ "url":"http://mckinley.illinois.edu/health-education/nutrition",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.sexual_health",
+ "url":"http://mckinley.illinois.edu/health-education/sexual-health",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.lgbtq",
+ "url":"http://mckinley.illinois.edu/health-education/sexual-health/lgbtq-health",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.lab",
+ "url":"http://mckinley.illinois.edu/medical-services/laboratory",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.radiology",
+ "url":"http://mckinley.illinois.edu/medical-services/radiology",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.pharmacy",
+ "url":"http://mckinley.illinois.edu/pharmacy/about-pharmacy",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ],
+ "social_media": [
+ {
+ "type":"youtube",
+ "url":"https://www.youtube.com/user/MHCMcTV"
+ },
+ {
+ "type":"facebook",
+ "url":"https://www.facebook.com/MckinleyHC/"
+ },
+ {
+ "type":"instagram",
+ "url":"https://www.instagram.com/mckinleyhealthcenter/"
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_wellness_app.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.physical.resources.first_time_visitors",
+ "url":"http://mhcwellness.illinois.edu/first-time-visitors",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.primary_care",
+ "url":"http://mhcwellness.illinois.edu/primary-care",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.schedule_appointment",
+ "url":"http://mhcwellness.illinois.edu/mymckinley-schedule-appointment",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.dial_nurse",
+ "url":"http://mhcwellness.illinois.edu/dial-nurse",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.health_resource_centers",
+ "url":"http://mhcwellness.illinois.edu/health-resource-centers",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.exercise",
+ "url":"http://mhcwellness.illinois.edu/exercise",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.physical.resources.eat_well",
+ "url":"http://mhcwellness.illinois.edu/eat-well",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.sexual_health",
+ "url":"http://mhcwellness.illinois.edu/sexual-health",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ }
+ ]
+ },
+ "mental": {
+ "header": {
+ "image": "group-444.png",
+ "title": "panel.wellness.mental.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.mental.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.mental.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.kognito_at_risk_center",
+ "image": "kognito.png",
+ "action": {
+ "name": "web",
+ "source": "https://counselingcenter.illinois.edu/emergency/kognito-risk-suicide-prevention-training"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.mood_meter",
+ "image": "group-7.png",
+ "action": {
+ "name": "web",
+ "source": "http://mhcwellness.illinois.edu/interactive-mood-meter"
+ }
+ },
+ {
+ "title": "panel.wellness.mental.interactive_activities.activity.mixup",
+ "image": "group-2.png",
+ "action": {
+ "name": "web",
+ "source": "http://mhcwellness.illinois.edu/memory-mixup"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "title": "panel.wellness.common.resources.counseling_center.title",
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.mental.resources.counseling",
+ "url": "https://counselingcenter.illinois.edu/counseling",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.mental.resources.outreach",
+ "url": "https://counselingcenter.illinois.edu/outreach-and-prevention-services",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.common.resources.ace_it",
+ "url": "https://counselingcenter.illinois.edu/aceit",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_health_center.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.mental.resources.mental_services",
+ "url":"http://mckinley.illinois.edu/medical-services/mental-health",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.stress_management",
+ "url":"http://mckinley.illinois.edu/health-education/stress-management",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.relaxation_techniques",
+ "url":"http://mckinley.illinois.edu/health-education/stress-management/relaxation-techniques",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ],
+ "social_media": [
+ {
+ "type":"youtube",
+ "url":"https://www.youtube.com/user/MHCMcTV"
+ },
+ {
+ "type":"facebook",
+ "url":"https://www.facebook.com/MckinleyHC/"
+ },
+ {
+ "type":"instagram",
+ "url":"https://www.instagram.com/mckinleyhealthcenter/"
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_wellness_app.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.mental_health",
+ "url":"http://mhcwellness.illinois.edu/mental-health",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.mental.resources.manage_stress",
+ "url":"http://mhcwellness.illinois.edu/manage-stress",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.mental.resources.get_sleep",
+ "url":"http://mhcwellness.illinois.edu/get-sleep",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ }
+ ]
+ },
+ "environmental": {
+ "header": {
+ "image": "group-9.png",
+ "title": "panel.wellness.environmental.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.environmental.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.environmental.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.environmental.resources.arboretum",
+ "url": "http://arboretum.illinois.edu/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.campus_recreation.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.environmental.resources.campus_bike_center",
+ "url":"https://campusrec.illinois.edu/programs/campus-bike-center/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_wellness_app.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.environmental.resources.transportation_safety",
+ "url":"http://mhcwellness.illinois.edu/safety-transportation",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.environmental.resources.late_night_studying",
+ "url":"http://mhcwellness.illinois.edu/studying-late-night",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ }
+ ]
+ },
+ "financial": {
+ "header": {
+ "image": "financial.png",
+ "title": "panel.wellness.financial.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.financial.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.financial.description.secondary_text"
+ },
+ "resources": [
+ {
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.financial.resources.financial_wellness_program",
+ "url": "https://extension.illinois.edu/cfiv/financial-wellness-college-students",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.financial.resources.your_legal_health",
+ "url": "https://odos.illinois.edu/sls/resources/brochures/docs/your-legal-health.pdf",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.financial.resources.student_money_management",
+ "url": "https://www.studentmoney.uillinois.edu/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_wellness_app.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.financial.resources.budgets",
+ "url":"http://mhcwellness.illinois.edu/budget-finances",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ }
+ ]
+ },
+ "spiritual": {
+ "header": {
+ "image": "group-44.png",
+ "title": "panel.wellness.spiritual.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.spiritual.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.spiritual.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.spiritual.resources.reflection_rooms",
+ "url": "https://www.library.illinois.edu/ugl/about/reflection-rooms/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.spiritual.resources.open_prayer",
+ "url": "https://union.illinois.edu/get-involved/office-of-registered-organizations/student-organization-complex",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.office_inclusion.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.programs",
+ "url":"https://oiir.illinois.edu/programs",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.spiritual.resources.interfaith",
+ "url":"https://oiir.illinois.edu/diversityed/interfaith",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.bnaacc",
+ "url":"https://oiir.illinois.edu/bnaacc",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.aacc",
+ "url":"https://oiir.illinois.edu/aacc",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.la_casa",
+ "url":"https://oiir.illinois.edu/la-casa-cultural-latina",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.native_american_house",
+ "url":"https://oiir.illinois.edu/native-american-house",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.spiritual.resources.dean_students.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.spiritual.resources.religious_worker",
+ "url":"https://odos.illinois.edu/resources/rwa/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_health_center.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.relaxation_techniques",
+ "url":"http://mckinley.illinois.edu/health-education/stress-management/relaxation-techniques",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ],
+ "social_media": [
+ {
+ "type":"youtube",
+ "url":"https://www.youtube.com/user/MHCMcTV"
+ },
+ {
+ "type":"facebook",
+ "url":"https://www.facebook.com/MckinleyHC/"
+ },
+ {
+ "type":"instagram",
+ "url":"https://www.instagram.com/mckinleyhealthcenter/"
+ }
+ ]
+ }
+ ]
+ },
+ "vocational": {
+ "header": {
+ "image": "group-15.png",
+ "title": "panel.wellness.vocational.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.vocational.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.vocational.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.vocational.resources.career_center",
+ "url": "https://www.careercenter.illinois.edu/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.vocational.resources.student_job_board",
+ "url": "https://secure.osfa.illinois.edu/vjb/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.vocational.resources.top_scholars",
+ "url": "https://topscholars.illinois.edu/",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title": "panel.wellness.vocational.resources.i_programs",
+ "url": "http://leadership.illinois.edu/i-programs",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_wellness_app.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.vocational.resources.networking",
+ "url":"http://mhcwellness.illinois.edu/networking",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.vocational.resources.student_assistance_center",
+ "url":"http://mhcwellness.illinois.edu/student-assistance-center%E2%80%A8",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.vocational.resources.academic_advisement",
+ "url":"http://mhcwellness.illinois.edu/academic-advisement",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.vocational.resources.faqs",
+ "url":"http://mhcwellness.illinois.edu/faq",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.vocational.resources.study_tips",
+ "url":"http://mhcwellness.illinois.edu/study-tips",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.vocational.resources.compass",
+ "url":"http://mhcwellness.illinois.edu/what-compass",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.vocational.resources.tutoring",
+ "url":"http://mhcwellness.illinois.edu/tutoring",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.involved",
+ "url":"http://mhcwellness.illinois.edu/getting-involved",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ }
+ ]
+ },
+ "emotional": {
+ "header": {
+ "image": "path.png",
+ "title": "panel.wellness.emotional.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.emotional.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.emotional.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.mood_meter",
+ "image": "group-7.png",
+ "action": {
+ "name": "web",
+ "source": "http://mhcwellness.illinois.edu/interactive-mood-meter"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.emotional.resources.community_care",
+ "url": "https://odos.uiuc.edu/community-of-care/resources/students/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.office_inclusion.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.programs",
+ "url":"https://oiir.illinois.edu/programs",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.women_center",
+ "url":"https://oiir.illinois.edu/womens-center",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.lgbt_center",
+ "url":"https://oiir.illinois.edu/lgbt-resource-center",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.counseling_center.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.interactive_activities.activity.kognito_at_risk_center",
+ "url":"https://counselingcenter.illinois.edu/emergency/kognito-risk-suicide-prevention-training",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.ace_it",
+ "url":"https://counselingcenter.illinois.edu/aceit",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ],
+ "social_media": [
+ {
+ "type":"twitter",
+ "url":"http://twitter.com/UI_Counseling"
+ },
+ {
+ "type":"facebook",
+ "url":"https://www.facebook.com/IllinoisCounselingCenter"
+ },
+ {
+ "type":"youtube",
+ "url":"https://www.youtube.com/channel/UCyxUtefuFNxkk2MuwhcDgig"
+ },
+ {
+ "type":"instagram",
+ "url":"https://www.instagram.com/illinoiscounselingcenter/"
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_health_center.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.stress_management",
+ "url":"http://mckinley.illinois.edu/health-education/stress-management",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.mental_health",
+ "url":"http://mckinley.illinois.edu/medical-services/mental-health",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ],
+ "social_media": [
+ {
+ "type":"youtube",
+ "url":"https://www.youtube.com/user/MHCMcTV"
+ },
+ {
+ "type":"facebook",
+ "url":"https://www.facebook.com/MckinleyHC/"
+ },
+ {
+ "type":"instagram",
+ "url":"https://www.instagram.com/mckinleyhealthcenter/"
+ }
+ ]
+ }
+ ]
+ },
+ "social": {
+ "header": {
+ "image": "group-16.png",
+ "title": "panel.wellness.social.header.title"
+ },
+ "description": {
+ "main_text": "panel.wellness.social.description.main_text",
+ "bullet": "panel.wellness.common.description.bullet",
+ "secondary_text": "panel.wellness.social.description.secondary_text"
+ },
+ "activities": [
+ {
+ "header": {
+ "icon": "campus-tools.png",
+ "title": "panel.wellness.common.interactive_activities.title"
+ },
+ "items": [
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.mood_meter",
+ "image": "group-7.png",
+ "action": {
+ "name": "web",
+ "source": "http://mhcwellness.illinois.edu/interactive-mood-meter"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.reflection",
+ "image": "reflection.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/wellness-reflection-start/"
+ }
+ },
+ {
+ "title": "panel.wellness.common.interactive_activities.activity.pledge",
+ "image": "pledge.png",
+ "action": {
+ "name": "web",
+ "source": "https://campusrec.illinois.edu/programs/student-wellness/take-the-wellness-pledge/"
+ }
+ }
+ ]
+ }
+ ],
+ "resources": [
+ {
+ "ribbon_buttons": [
+ {
+ "title": "panel.wellness.social.resources.engage",
+ "url": "https://illinois.campuslabs.com/engage/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.campus_recreation.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.social.resources.intramural_sports",
+ "url":"https://campusrec.illinois.edu/programs/intramural/",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.social.resources.illini_union.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.social.resources.rso",
+ "url":"https://union.illinois.edu/get-involved/office-of-registered-organizations",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.social.resources.master_calendar",
+ "url":"https://mastercalendar.union.illinois.edu/MasterCalendar/MasterCalendar.aspx",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.office_inclusion.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.bnaacc",
+ "url":"https://oiir.illinois.edu/bnaacc",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.aacc",
+ "url":"https://oiir.illinois.edu/aacc",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.la_casa",
+ "url":"https://oiir.illinois.edu/la-casa-cultural-latina",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.native_american_house",
+ "url":"https://oiir.illinois.edu/native-american-house",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.women_center",
+ "url":"https://oiir.illinois.edu/womens-center",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.common.resources.lgbt_center",
+ "url":"https://oiir.illinois.edu/lgbt-resource-center",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.social.resources.international_education",
+ "url":"https://oiir.illinois.edu/cultural-resource-centers/international-education",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ },
+ {
+ "title": "panel.wellness.common.resources.mckinley_wellness_app.title",
+ "ribbon_buttons": [
+ {
+ "title":"panel.wellness.common.resources.involved",
+ "url":"http://mhcwellness.illinois.edu/getting-involved",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.social.resources.going_out",
+ "url":"http://mhcwellness.illinois.edu/going-out",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.social.resources.peer_pressure",
+ "url":"http://mhcwellness.illinois.edu/peer-pressure",
+ "icon": "link-out.png",
+ "hint": ""
+ },
+ {
+ "title":"panel.wellness.social.resources.roommates",
+ "url":"http://mhcwellness.illinois.edu/roommates",
+ "icon": "link-out.png",
+ "hint": ""
+ }
+ ]
+ }
+ ]
+ }
+ }
+ },
+
+ "covid19_guidelines": {
+ "content": {
+ "green": [
+ {
+ "image": "icon-stay-at-home.png",
+ "description": "Stay at home",
+ "requirement": "Required until April 31"
+ },
+ {
+ "image": "icon-face-mask.png",
+ "description": "Wearing facemask",
+ "requirement": "Recommended"
+ }
+ ],
+ "yellow": [
+ {
+ "image": "icon-social-distance.png",
+ "description": "Social distancing",
+ "requirement": "Required"
+ },
+ {
+ "image": "icon-stay-at-home.png",
+ "description": "Stay at home",
+ "requirement": "Required until April 31"
+ },
+ {
+ "image": "icon-face-mask.png",
+ "description": "Wearing facemask",
+ "requirement": "Recommended"
+ }
+ ],
+ "red": [
+ {
+ "image": "icon-stay-at-home.png",
+ "description": "Stay at home",
+ "requirement": "Required"
+ },
+ {
+ "image": "icon-separate-people.png",
+ "description": "Separate from other people",
+ "requirement": "Required"
+ },
+ {
+ "image": "icon-face-mask.png",
+ "description": "Wearing facemask",
+ "requirement": "Required"
+ }
+ ]
+ },
+ "strings": {
+ "en": {
+ "Stay at home": "Stay at home",
+ "Separate from other people": "Separate from other people",
+ "Wearing facemask": "Wearing facemask",
+ "Social distancing": "Social distancing",
+ "Required": "Required",
+ "Required until April 31": "Required until April 31",
+ "Recommended": "Recommended"
+ },
+ "es": {
+ "Stay at home": "Quédate en casa",
+ "Separate from other people": "Separado de otras personas",
+ "Wearing facemask": "Usar mascarilla",
+ "Social distancing": "Distanciamiento social",
+ "Required": "Necesario",
+ "Required until April 31": "Requerido hasta el 31 de abril",
+ "Recommended": "Recomendado"
+ },
+ "zh": {
+ "Stay at home": "呆在家裡",
+ "Separate from other people": "與其他人分開",
+ "Wearing facemask": "戴口罩",
+ "Social distancing": "社交隔離",
+ "Required": "需要",
+ "Required until April 31": "要求至4月31日",
+ "Recommended": "推薦的"
+ }
+ },
+ "icons": {
+ "home": "icon-stay-at-home.png",
+ "separate": "icon-separate-people.png",
+ "distance": "icon-social-distance.png",
+ "mask": "icon-face-mask.png"
+ }
+ }
+}
+
diff --git a/assets/flexUI.json b/assets/flexUI.json
new file mode 100644
index 00000000..36a46a03
--- /dev/null
+++ b/assets/flexUI.json
@@ -0,0 +1,132 @@
+{
+ "content": {
+ "tabbar": ["home", "athletics", "explore", "wallet", "browse"],
+
+ "home": ["upgrade_version_message", "voter_registration", "covid19_info", "game_day", "campus_tools", "create_poll", "pref_sports", "campus_reminders", "upcoming_events", "recent_items"],
+ "campus_tools": ["events", "covid19", "dining", "athletics", "illini_cash", "laundry", "my_illini"],
+
+ "health.covid19": ["latest_update", "stay_informed", "news", "resources", "general", "faq"],
+
+ "wallet": ["wallet.connect", "wallet.content", "wallet.cards"],
+ "wallet.connect":["netid", "phone"],
+ "wallet.content": ["illini_cash", "meal_plan"],
+ "wallet.cards":["covid19_passport", "mtd", "id", "library"],
+
+ "browse": ["browse.all", "browse.content"],
+ "browse.all": ["athletics", "saved", "covid19", "dining", "events", "quick_polls", "wellness", "groups"],
+ "browse.content": ["settings", "my_illini", "laundry", "illini_cash", "meal_plan", "parking", "feedback", "create_event", "create_stadium_poll", "state_farm_wayfinding"],
+
+ "settings": ["user_info", "connect", "customizations", "connected", "notifications", "covid19", "privacy", "account", "feedback"],
+ "settings.connect": ["netid", "phone"],
+ "settings.customizations": ["roles"],
+ "settings.connected": ["netid", "phone"],
+ "settings.connected.netid": ["info", "disconnect", "connect"],
+ "settings.connected.phone": ["info", "disconnect", "verify"],
+ "settings.notifications": ["covid19"],
+ "settings.covid19": ["exposure_notifications", "provider_test_result", "qr_code"],
+ "settings.privacy": ["statement"],
+ "settings.account": ["personal_info"],
+
+ "onboarding":["get_started", "notifications_auth", "location_auth", "bluetooth_auth", "roles", "login_netid", "login_phone", "verify_phone", "confirm_phone", "resident_info", "review_scan", "covid19_intro", "covid19_how_works", "covid19_consent", "covid19_qrcode", "covid19_final"],
+
+ "features": ["converge", "create_poll", "mtd_bus_number", "parking_lot_directions"]
+ },
+ "rules": {
+ "roles" : {
+ "tabbar.home" : [["NOT", "fan"], "OR", "student", "OR", "employee", "OR", "parent"],
+ "tabbar.athletics" : ["fan", "AND", ["NOT", ["student", "OR", "employee", "OR", "parent"]]],
+ "tabbar.explore" : ["NOT", ["employee", "OR", "student", "OR", "resident"]],
+ "tabbar.wallet" : ["student", "OR", "employee", "OR", "resident"],
+
+ "game_day" : ["fan", "AND", ["student", "OR", "employee", "OR", "parent"], "AND", ["NOT", ["visitor", "OR", "alumni"]]],
+ "pref_sports" : ["fan", "AND", ["student", "OR", "employee", "OR", "parent"], "AND", ["NOT", ["visitor", "OR", "alumni"]]],
+ "campus_reminders" : [["NOT", "alumni"], "OR", "student", "OR", "fan", "OR", "employee", "OR", "parent"],
+
+ "laundry" : ["student"],
+ "my_illini" : ["student"],
+ "illini_cash" : ["student", "OR", "employee", "OR", "parent"],
+ "meal_plan" : ["student"],
+ "create_poll" : ["student", "OR", "employee"],
+ "upgrade_version_message" : ["student", "OR", "employee"],
+ "covid19_info" : ["student", "OR", "employee", "OR", "resident"],
+
+ "onboarding.login_netid" : ["student", "OR", "employee"],
+ "onboarding.login_phone" : ["NOT", ["employee", "OR", "student"]],
+ "onboarding.verify_phone" : ["NOT", ["employee", "OR", "student"]],
+ "onboarding.confirm_phone" : ["NOT", ["employee", "OR", "student"]],
+
+ "onboarding.sport_prefs" : ["fan"],
+
+ "wallet.cards.mtd" : ["NOT",["resident"]]
+ },
+ "privacy" : {
+ "illini_cash" : 3,
+ "laundry" : 4,
+ "interests_selection" : 3,
+ "recent_items" : 4,
+ "saved" : 3,
+ "converge" : 5,
+
+ "settings.user_info" : 4,
+ "settings.connect" : 4,
+ "settings.connected" : 4,
+ "settings.customizations" : 3,
+ "settings.notifications" : 4,
+ "settings.account" : 4,
+
+ "tabbar.wallet" : 4,
+ "wallet.connect" : 4,
+
+ "onboarding.notifications_auth" : 4,
+ "onboarding.location_auth" : 2,
+ "onboarding.bluetooth_auth" : 2,
+ "onboarding.roles" : 3,
+ "onboarding.login_netid" : 3,
+ "onboarding.login_phone" : 3,
+ "onboarding.verify_phone" : 3,
+ "onboarding.confirm_phone" : 3,
+ "onboarding.sport_prefs" : 3
+ },
+ "auth": {
+ "laundry" : { "shibbolethLoggedIn": true },
+ "illini_cash" : { "shibbolethLoggedIn": true },
+ "create_event" : { "shibbolethMemberOf": "urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire event approvers" },
+ "create_stadium_poll" : { "shibbolethMemberOf": "urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire stadium poll manager" },
+ "upgrade_version_message" : { "shibbolethLoggedIn": true },
+
+ "wallet.connect" : { "loggedIn": false },
+ "wallet.content" : { "loggedIn": true },
+ "wallet.cards" : { "loggedIn": true },
+ "wallet.cards.id" : { "iCardNum": true },
+ "wallet.cards.library" : { "iCardLibraryNum": true },
+ "wallet.cards.mtd" : { "shibbolethLoggedIn": true },
+
+ "settings.user_info" : { "loggedIn": true },
+ "settings.connect" : { "loggedIn": false },
+ "settings.connected" : { "loggedIn": true },
+ "settings.account" : { "loggedIn": true },
+ "settings.covid19" : { "loggedIn": true },
+ "settings.connected.netid" : { "shibbolethLoggedIn": true },
+ "settings.connected.phone" : { "phoneLoggedIn": true },
+ "settings.connected.netid.info" : { "shibbolethLoggedIn": true },
+ "settings.connected.netid.disconnect" : { "shibbolethLoggedIn": true },
+ "settings.connected.netid.connect" : { "shibbolethLoggedIn": false },
+ "settings.connected.phone.info" : { "phoneLoggedIn": true },
+ "settings.connected.phone.verify" : { "phoneLoggedIn": false },
+ "settings.connected.phone.disconnect" : { "phoneLoggedIn": true }
+ },
+ "platform":{
+ "onboarding.bluetooth_auth" : { "os": "ios" }
+ },
+ "illini_cash": {
+ "laundry" : { "housingResidenceStatus" : true }
+ },
+ "enable" : {
+ "illini_cash" : true,
+ "create_event" : false,
+ "converge" : true,
+ "mtd_bus_number" : true,
+ "parking_lot_directions" : true
+ }
+ }
+}
diff --git a/assets/sample.groups.json b/assets/sample.groups.json
new file mode 100644
index 00000000..213b635e
--- /dev/null
+++ b/assets/sample.groups.json
@@ -0,0 +1,225 @@
+{
+ "categories": ["Academic/Pre-Professional", "Athletic/Recreation", "Club Sports", "Creative/Media/Performing Arts", "Cultural/Ethnic", "Graduate", "Honorary", "International", "Other Social", "Political", "Religious", "Residence Hall", "Rights/Freedom Issues", "ROTC", "Service/Philanthropy", "Social Fraternity/Sorority", "University Student Governance/Council/Committee"],
+ "types": ["RSO", "College", "Department", "Class", "Study Group", "Other"],
+ "tags": ["Casual", "Professional", "High Engagement", "Competitive", "Low Commitment"],
+ "officerTitles": ["President", "Treasurer", "Assistant"],
+ "groups": [
+ {
+ "id" : "1",
+ "category" : "Academic/Pre-Professional",
+ "type" : "College",
+ "title" : "Aerospace Outreach at Illinois",
+ "creatorUin" : "12345678",
+ "privacy" : "public",
+ "certified": true,
+ "description" : "This is a group for Aerospace fans.",
+ "imageURL" : "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/ae46c0de-cabb-11e9-88b6-0a58a9feac2a.webp",
+ "webURL" : "https://www.inabyte.com",
+ "membersCount": 154,
+ "tags" : ["Professional", "High Engagement"],
+ "membershipQuest" : {
+ "steps": [
+ { "description": "Come to one of three info night at the beginning of each semester.", "eventIds":["5e50aa0d543e12000e910dea", "5e7593d0a5bdb5000bce81f7", "5df06a423f603800094294a4"] },
+ { "description": "Interview with our current members so we can both get to know eachother better and get some questions answered." }
+ ],
+ "questions": [
+ {"question": "Why are you interested in our group?"},
+ {"question": "What is the best way to contact you?"}
+ ]
+ }
+ },
+ {
+ "id" : "2",
+ "category" : "Social Fraternity/Sorority",
+ "type" : "Department",
+ "title" : "Aplha Chi Omega",
+ "creatorUin" : "12345678",
+ "privacy" : "private",
+ "certified": false,
+ "description" : "This is a Social Fraternity/Sorority group for Aplha Chi Omega.",
+ "imageURL" : "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/604d7a95-cabb-11e9-88b6-0a58a9feac2a.webp",
+ "webURL" : "https://www.inabyte.com",
+ "membersCount": 42,
+ "tags" : ["Casual", "Low Commitment"],
+ "membershipQuest" : {
+ "steps": [
+ { "description": "Come to one of three info night at the beginning of each semester.", "eventIds":["5e50aa0d543e12000e910dea", "5e7593d0a5bdb5000bce81f7", "5df06a423f603800094294a4"] },
+ { "description": "Interview with our current members so we can both get to know eachother better and get some questions answered." }
+ ],
+ "questions": [
+ {"question": "Why are you interested in our group?"},
+ {"question": "What is the best way to contact you?"}
+ ]
+ }
+ },
+ {
+ "id" : "3",
+ "category" : "Creative/Media/Performing Arts",
+ "type" : "RSO",
+ "title" : "Altgeld Ringers",
+ "creatorUin" : "12345678",
+ "privacy" : "public",
+ "certified": false,
+ "description" : "This is a Creative/Media/Performing Arts group for Altgeld Ringers.",
+ "imageURL" : "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/31840f7d-cabb-11e9-88b6-0a58a9feac2a.webp",
+ "webURL" : "https://www.inabyte.com",
+ "membersCount": 427,
+ "tags" : ["Competitive", "Professional"],
+ "membershipQuest" : {
+ "steps": [
+ { "description": "Come to one of three info night at the beginning of each semester.", "eventIds":["5e50aa0d543e12000e910dea", "5e7593d0a5bdb5000bce81f7", "5df06a423f603800094294a4"] },
+ { "description": "Interview with our current members so we can both get to know eachother better and get some questions answered." }
+ ],
+ "questions": [
+ {"question": "Why are you interested in our group?"},
+ {"question": "What is the best way to contact you?"}
+ ]
+ }
+ },
+ {
+ "id" : "4",
+ "category" : "Academic/Pre-Professional",
+ "type" : "Class",
+ "title" : "100 STRONG Coordinating Committee",
+ "creatorUin" : "12345678",
+ "privacy" : "public",
+ "certified": true,
+ "description" : "The 100 STRONG program will serve to systemically aclimate incoming African American freshman to the campus environment and to the programs and offices that were created to enhance academic and social success.",
+ "imageURL" : "https://rokwire-images.s3.us-east-2.amazonaws.com/event/tout/02ea99ca-cabb-11e9-88b6-0a58a9feac2a.webp",
+ "webURL" : "https://www.inabyte.com",
+ "membersCount": 3433,
+ "tags" : ["Casual", "High Engagement"],
+ "membershipQuest" : {
+ "steps": [
+ { "description": "Come to one of three info night at the beginning of each semester.", "eventIds":["5e50aa0d543e12000e910dea", "5e7593d0a5bdb5000bce81f7", "5df06a423f603800094294a4"] },
+ { "description": "Interview with our current members so we can both get to know eachother better and get some questions answered." }
+ ],
+ "questions": [
+ {"question": "Why are you interested in our group?"},
+ {"question": "What is the best way to contact you?"}
+ ]
+ }
+ }
+ ],
+
+ "members": [
+ {"uin": "10000000", "name": "Sarah Lee", "email": "sarah.lee@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo4.png", "status": "officer", "officerTitle":"President", "admin": true, "dateAdded":"2020-01-01T12:30:00"},
+ {"uin": "10000001", "name": "Jared Bastian", "email": "jared.bastian@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo2.png", "status": "officer", "officerTitle":"Treasurer", "admin": false, "dateAdded":"2020-01-02T12:30:00"},
+ {"uin": "10000002", "name": "Ron Adams", "email": "ron.adams@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo3.png", "status": "officer", "officerTitle":"Assistant", "admin": false, "dateAdded":"2020-01-03T12:30:00"},
+ {"uin": "10000003", "name": "Anna Robinson", "email": "anna.robinson@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo5.png", "status": "current", "officerTitle":null, "admin": false, "dateAdded":"2020-01-04T12:30:00"},
+ {"uin": "10000004", "name": "Ely London", "email": "ely.london@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo4.png", "status": "current", "officerTitle":null, "admin": false, "dateAdded":"2020-01-05T12:30:00"},
+ {"uin": "10000005", "name": "John Baxter", "email": "john.baxter@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo.png", "status": "inactive", "officerTitle":null, "admin": false, "dateAdded":"2020-01-06T12:30:00"}
+ ],
+ "pending_members": [
+ {"uin": "10000006", "name": "Ian Gillan", "email": "ian.gillan@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo6.png", "membershipRequest": {"dateCreated":"2020-04-10T05:00:00", "answers":[{"answer": "Because so."}, {"answer": "By carrier pigeon."}]} },
+ {"uin": "10000007", "name": "Emily Blunt", "email": "emily.blunt@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo7.png", "membershipRequest": {"dateCreated":"2020-04-11T06:00:00", "answers":[{"answer": "I am not sure."}, {"answer": "By telegraph."}]} }
+ ],
+
+ "events": [
+ {
+ "allDay": false,
+ "calendarId": "1354",
+ "category": "Academic",
+ "contacts": [{"email":"iage@illinois.edu","phone":"217-333-6322"}],
+ "createdBy": "anw@illinois.edu",
+ "dataModified": "2019-12-10T05:00:00",
+ "dataSourceEventId": "33366515",
+ "dateCreated": "2019-12-10T05:00:00",
+ "endDate": "Wed, 08 Apr 2020 15:45:00 GMT",
+ "eventId": "5df06a3f6e33095e1a214d6e",
+ "icalUrl": "https://calendars.illinois.edu/ical/1354/33366515.ics",
+ "id": "5df06a423f603800094294a4",
+ "location": {"description":"International Studies Building 101","latitude": 40.1072128,"longitude": -88.2316263},
+ "longDescription": "Not sure how to get started in the study abroad process? Attend a First Steps Workshop to learn what it means to study abroad, how to select a program, components of an application, and how study abroad can advance your academic, professional, and personal goals. Topics covered in the presentation include:
\n\nacademics \nhousing \nprogram duration/type \nlocations \ncosts \n \nYou will have the opportunity to ask questions of our student staff, called Program Assistants , who have been through the study abroad process. Whether you're deciding if study abroad is for you, assesing program options, or have started an application, we welcome all students to learn more about what study abroad has to offer and how to get started!
",
+ "outlookUrl": "https://calendars.illinois.edu/outlook2010/1354/33366515.ics",
+ "recurrenceId": 18789,
+ "recurringFlag": true,
+ "sourceId": "0",
+ "sponsor": "Illinois Abroad & Global Exchange",
+ "startDate": "Wed, 08 Apr 2020 15:00:00 GMT",
+ "tags": ["application process","information session","international","presentation","research abroad","scholarship","scholarships","study abroad","workshop"],
+ "title": "First Steps Workshop",
+ "comments": [
+ {
+ "text": "This event iss sponsored by IBM! A great opportunity for anyone networking or looking for potential internship opportunities.",
+ "dateCreated": "2020-04-01T05:00:00",
+ "member": {"uin": "10000000", "name": "Sarah Lee", "email": "sarah.lee@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo4.png", "status": "officer", "officerTitle":"President", "admin": true, "dateAdded":"2020-01-01T12:30:00"}
+ },{
+ "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In congue ut metus at bibendum.",
+ "dateCreated": "2020-04-01T04:00:00",
+ "member": {"uin": "10000001", "name": "Jared Bastian", "email": "jared.bastian@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo2.png", "status": "officer", "officerTitle":"Treasurer", "admin": false, "dateAdded":"2020-01-02T12:30:00"}
+ }
+ ]
+ }, {
+ "allDay": false,
+ "calendarId": "5598",
+ "category": "Academic",
+ "contacts": [{"email":"dmclur2@illinois.edu","firstName":"D'Mayza","lastName":"McClure"}],
+ "createdBy": "erink@illinois.edu",
+ "dataModified": "2020-04-01T05:00:00",
+ "dataSourceEventId": "33376772",
+ "dateCreated": "2020-03-20T05:00:00",
+ "endDate": "Wed, 08 Apr 2020 16:00:00 GMT",
+ "eventId": "5e7593cf605f07a6ec47f2ab",
+ "icalUrl": "https://calendars.illinois.edu/ical/5598/33376772.ics",
+ "id": "5e7593d0a5bdb5000bce81f7",
+ "location": {"description":"Talk and Q&A link will be disseminated via email to limited participants","latitude":40.1105875,"longitude":-88.2072697},
+ "longDescription": "Abstract: Self-driving vehicles will bring us safer, cleaner, and more convenient transportation. To make this dream come true, we need our autonomous system to perceive, plan, and execute effectively in unstructured environments and have guaranteed safety. While machine learning has significantly enhanced autonomous capabilities, we are still missing key ingredients to achieve the desired goal. In this talk, I will present our approach towards autonomous driving. The core idea is to systematically integrate learning methods with structured models and human priors of the world. The effectiveness of our integrated approach has been demonstrated at the full spectrum of self-driving tasks, including localization, perception, planning and simulation, and our developed algorithms have been deployed in real-world production systems. Finally, I will give a brief personal outlook on open research topics towards realistically solving self-driving.
\n
\nBio : Shenlong Wang is a PhD student at the University of Toronto under the supervision of Raquel Urtasun. He is also a Senior Research Scientist at the Uber Advanced Technology Group. Shenlong's research interests span the spectrum from computer vision, robotics and machine learning. His recent work involves developing robust algorithms for self-driving and making autonomous vehicles more reliable and scalable. His research has resulted in over 30 papers at top conferences including over 10 oral and spotlight presentations. He was selected as the recipient of the Facebook, Adobe and Royal Bank of Canada Fellowships in 2017.
\nFaculty Host : Derek Hoiem
",
+ "outlookUrl": "https://calendars.illinois.edu/outlook2010/5598/33376772.ics",
+ "recurrenceId": 0,
+ "recurringFlag": false,
+ "sourceId": "0",
+ "sponsor": "Illinois Computer Science",
+ "startDate": "Wed, 08 Apr 2020 15:00:00 GMT",
+ "title": "SPECIAL SEMINAR: Shenglong Wang, University of Toronto, \"Learning to Drive With a Touch of Human Knowledge\"",
+ "comments": [
+ {
+ "text": "Sed nec dignissim lacus. Proin vestibulum, lacus at gravida tincidunt, est sapien tristique orci, imperdiet molestie metus metus in lorem.",
+ "dateCreated": "2020-04-01T05:00:00",
+ "member": {"uin": "10000000", "name": "Sarah Lee", "email": "sarah.lee@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo4.png", "status": "officer", "officerTitle":"President", "admin": true, "dateAdded":"2020-01-01T12:30:00"}
+ },{
+ "text": "Integer ex metus, interdum vitae lacinia at, venenatis ut nisi. Morbi aliquam facilisis ornare.",
+ "dateCreated": "2020-04-01T04:00:00",
+ "member": {"uin": "10000001", "name": "Jared Bastian", "email": "jared.bastian@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo2.png", "status": "officer", "officerTitle":"Treasurer", "admin": false, "dateAdded":"2020-01-02T12:30:00"}
+ },{
+ "text": "Aenean vulputate aliquam mi, vel vestibulum est varius eu. In nec quam ac neque molestie egestas laoreet vitae quam.",
+ "dateCreated": "2020-04-01T03:00:00",
+ "member": {"uin": "10000002", "name": "Ron Adams", "email": "ron.adams@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo3.png", "status": "officer", "officerTitle":"Assistant", "admin": false, "dateAdded":"2020-01-03T12:30:00"}
+ }
+ ]
+ }, {
+ "allDay": false,
+ "calendarId": "3777",
+ "category": "Academic",
+ "contacts": [{"email":"rbeech1@uic.edu","firstName":"Rosie","lastName":"Beechen","phone":"312-996-5580"}],
+ "createdBy": "rbeech1@uic.edu",
+ "dataModified": "2020-02-24T05:00:00",
+ "dataSourceEventId": "33375189",
+ "dateCreated": "2020-02-21T05:00:00",
+ "endDate": "Wed, 08 Apr 2020 16:45:00 GMT",
+ "eventId": "5e50aa09605f07a6ec4072b8",
+ "icalUrl": "https://calendars.illinois.edu/ical/3777/33375189.ics",
+ "id": "5e50aa0d543e12000e910dea",
+ "location": {"description":"3427 ETMSW","latitude":41.8749488,"longitude":-87.6529231},
+ "outlookUrl": "https://calendars.illinois.edu/outlook2010/3777/33375189.ics",
+ "recurrenceId": 0,
+ "recurringFlag": false,
+ "sourceId": "0",
+ "sponsor": "Stacey Horn",
+ "startDate": "Wed, 08 Apr 2020 15:30:00 GMT",
+ "title": "EPSY Faculty Candidate Job Talk",
+ "comments": [
+ {
+ "text": "Cras ultrices interdum eros. In et massa vitae ex pellentesque dignissim ut nec erat. Cras et erat nec augue auctor malesuada at vitae ipsum.",
+ "dateCreated": "2020-04-01T05:00:00",
+ "member": {"uin": "10000000", "name": "Sarah Lee", "email": "sarah.lee@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo4.png", "status": "officer", "officerTitle":"President", "admin": true, "dateAdded":"2020-01-01T12:30:00"}
+ }
+ ]
+ }
+ ],
+
+ "userMembership": {
+ "1": {"uin": "20000000", "name": "Current User", "email": "current.user@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo6.png", "status": "officer", "officerTitle":"President", "admin": true, "dateAdded":"2020-01-01T12:30:00"},
+ "2": {"uin": "20000000", "name": "Current User", "email": "current.user@illinois.edu", "photoURL": "https://rokwire-ios-beta.s3.us-east-2.amazonaws.com/Images/icard_photo6.png", "status": "current", "officerTitle":null, "admin": false, "dateAdded":"2020-01-01T12:30:00"}
+ }
+}
diff --git a/assets/sample.health.rules.json b/assets/sample.health.rules.json
new file mode 100644
index 00000000..74c09e1f
--- /dev/null
+++ b/assets/sample.health.rules.json
@@ -0,0 +1,381 @@
+{
+ "defaults": {
+ "status": {
+ "health_status": "orange",
+ "next_step": "Take a SHIELD Saliva Test when you return to campus."
+ }
+ },
+ "tests" : {
+ "rules": [
+ {
+ "test_type": "Antibody Test A1",
+ "category": "antibody",
+ "results": [
+ {
+ "result": "not present",
+ "category": "antibody.negavitve",
+ "status": "antibody.negavitve"
+ },
+ {
+ "result": "present",
+ "category": "antibody.positive",
+ "status": "antibody.positive"
+ }
+ ]
+ },
+ {
+ "test_type": "Covid-19 test B1",
+ "category": "PCR",
+ "results": [
+ {
+ "result": "positive",
+ "category": "PCR.positive",
+ "status": "PCR.positive"
+ },
+ {
+ "result": "negative",
+ "category": "PCR.negative",
+ "status": "PCR.negative"
+ }
+ ]
+ },
+ {
+ "test_type": "COVID-19 Antibody",
+ "category": "antibody",
+ "results": [
+ {
+ "result": "Positive",
+ "category": "antibody.positive",
+ "status": "antibody.positive"
+ },
+ {
+ "result": "Negative",
+ "category": "antibody.negative",
+ "status": "antibody.negative"
+ }
+ ]
+ },
+ {
+ "test_type": "COVID-19 Antigen",
+ "category": "PCR",
+ "results": [
+ {
+ "result": "Positive",
+ "category": "PCR.positive",
+ "status": "PCR.positive"
+ },
+ {
+ "result": "Negative",
+ "category": "PCR.negative",
+ "status": "PCR.negative"
+ }
+ ]
+ },
+ {
+ "test_type": "SARS-COV-2 BY PCR, BKR",
+ "category": "PCR",
+ "results": [
+ {
+ "result": "DETECTED",
+ "category": "PCR.positive",
+ "status": "PCR.positive"
+ },
+ {
+ "result": "Not Detected",
+ "category": "PCR.negative",
+ "status": "PCR.negative"
+ }
+ ]
+ },
+ {
+ "test_type": "COVID-19 PCR",
+ "category": "PCR",
+ "results": [
+ {
+ "result": "POSITIVE",
+ "category": "PCR.positive",
+ "status": "PCR.positive"
+ },
+ {
+ "result": "NEGATIVE",
+ "category": "PCR.negative",
+ "status": "PCR.negative"
+ },
+ {
+ "result": "INVALID",
+ "category": "PCR.invalid",
+ "status": "PCR.invalid"
+ }
+ ]
+ }
+ ],
+
+ "statuses": {
+ "antibody.negavitve": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 0, "max": 4 },
+ "current_interval": { "min": 0, "max": 4 }
+ },
+ "success": null,
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ },
+ "antibody.positive": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 0, "max": 4 },
+ "current_interval": { "min": 0, "max": 4 }
+ },
+ "success": {
+ "health_status": "green",
+ "priority": 1,
+ "next_step": "Monitor your test results",
+ "next_step_interval": 4
+ },
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ },
+ "PCR.positive": {
+ "health_status": "red",
+ "priority": 11,
+ "next_step": "Isolate at home and call your healthcare provider"
+ },
+ "PCR.negative": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 0, "max": 4 },
+ "current_interval": { "min": 0, "max": 4 }
+ },
+ "success": {
+ "health_status": "yellow",
+ "priority": 1,
+ "next_step": "Monitor your test results"
+ },
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ },
+ "PCR.invalid": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 0, "max": 1 },
+ "current_interval": { "min": 0, "max": 1 }
+ },
+ "success": {
+ "health_status": null,
+ "priority": 1,
+ "next_step": "Get another test asap",
+ "next_step_interval": 1
+ },
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ }
+ }
+ },
+
+ "symptoms": {
+ "rules": [
+ {
+ "counts": {
+ "gr1": { "min": 2 },
+ "gr2": { "min": 1 }
+ },
+ "status": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Take a COVID-19 test now",
+ "reason": "Your status changed to Orange because you self-reported symptoms consistent with the virus."
+ }
+ }
+ ],
+
+ "groups": [
+ {
+ "id": null,
+ "name": "gr0",
+ "symptoms": [
+ {
+ "id": "b669503f-938b-11ea-8f2a-0a58a9feac2a",
+ "name": "No symptoms"
+ }
+ ]
+ },
+ {
+ "id": "0952bb51-937b-11ea-8f2a-0a58a9feac2a",
+ "name": "gr1",
+ "symptoms": [
+ {
+ "id": "b41b12cc-93be-11ea-ae23-0a58a9feac2a",
+ "name": "Fever"
+ },
+ {
+ "id": "8f83787b-93c9-11ea-ae23-0a58a9feac2a",
+ "name": "Chills"
+ },
+ {
+ "id": "191df3ae-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Shaking or Shivering"
+ },
+ {
+ "id": "9ee1831e-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Muscle or joint pain"
+ },
+ {
+ "id": "acda4f1e-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Headache"
+ },
+ {
+ "id": "bad0cc3c-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Sore Throat"
+ },
+ {
+ "id": "d5afe77f-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Loss of taste and/or smell"
+ }
+ ]
+ },
+ {
+ "id": "0952df75-937b-11ea-8f2a-0a58a9feac2a",
+ "name": "gr2",
+ "symptoms": [
+ {
+ "id": "e35c8441-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Cough"
+ },
+ {
+ "id": "f3b23b65-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Shortness of breath"
+ },
+ {
+ "id": "05239c9e-93cb-11ea-ae23-0a58a9feac2a",
+ "name": "Difficulty breathing"
+ }
+ ]
+ }
+ ]
+
+ },
+
+ "contact_trace": {
+ "rules": [
+ {
+ "duration": { "min": 120 },
+ "status": {
+ "condition": "timeout",
+ "params": {
+ "interval": { "min": 0, "max": 4 }
+ },
+ "success": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 5, "max": null }
+ },
+ "success": null,
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ },
+ "fail": {
+ "health_status": "orange",
+ "priority": 2,
+ "next_step": "Take a COVID-19 test after {next_step_date}",
+ "next_step_interval": 4,
+ "reason": "Your status changed to Orange because you received an exposure notification."
+ }
+ }
+ }
+ ]
+ },
+
+ "actions": {
+ "rules": [
+ {
+ "type": "quarantine-on",
+ "status": {
+ "health_status": "orange",
+ "priority": 10,
+ "next_step": "Stay at home and avoid contacts",
+ "reason": "Your status changed to Orange because the Public Health department placed you in Quarantine."
+ }
+ },
+ {
+ "type": "quarantine-off",
+ "status": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 0, "max": 4 },
+ "current_interval": { "min": 0, "max": 4 }
+ },
+ "success": {
+ "health_status": "yellow",
+ "priority": -1,
+ "next_step": "Resume testing on your assigned days"
+ },
+ "fail": {
+ "health_status": "orange",
+ "priority": -1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ }
+ },
+ {
+ "type": "out-of-test-compliance",
+ "status": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": -1, "max": 1 },
+ "current_interval": { "min": -1, "max": 1 }
+ },
+ "success": null,
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ }
+ },
+ {
+ "type": "test_pending",
+ "status": {
+ "condition": "require-test",
+ "params": {
+ "interval": { "min": 0, "max": 4 },
+ "current_interval": { "min": 0, "max": 4 }
+ },
+ "success": {
+ "health_status": "yellow",
+ "priority": 1,
+ "next_step": "Monitor your test results"
+ },
+ "fail": {
+ "health_status": "orange",
+ "priority": 1,
+ "next_step": "Get a test now",
+ "reason": "Your status changed to Orange because you are past due for a test."
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/assets/sample.health.symptoms.json b/assets/sample.health.symptoms.json
new file mode 100644
index 00000000..e1db5ba4
--- /dev/null
+++ b/assets/sample.health.symptoms.json
@@ -0,0 +1,64 @@
+[
+ {
+ "id": null,
+ "name": "gr0",
+ "symptoms": [
+ {
+ "id": "b669503f-938b-11ea-8f2a-0a58a9feac2a",
+ "name": "No symptoms"
+ }
+ ]
+ },
+ {
+ "id": "0952bb51-937b-11ea-8f2a-0a58a9feac2a",
+ "name": "gr1",
+ "symptoms": [
+ {
+ "id": "b41b12cc-93be-11ea-ae23-0a58a9feac2a",
+ "name": "Fever"
+ },
+ {
+ "id": "8f83787b-93c9-11ea-ae23-0a58a9feac2a",
+ "name": "Chills"
+ },
+ {
+ "id": "191df3ae-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Shaking or Shivering"
+ },
+ {
+ "id": "9ee1831e-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Muscle or joint pain"
+ },
+ {
+ "id": "acda4f1e-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Headache"
+ },
+ {
+ "id": "bad0cc3c-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Sore Throat"
+ },
+ {
+ "id": "d5afe77f-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Loss of taste and/or smell"
+ }
+ ]
+ },
+ {
+ "id": "0952df75-937b-11ea-8f2a-0a58a9feac2a",
+ "name": "gr2",
+ "symptoms": [
+ {
+ "id": "e35c8441-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Cough"
+ },
+ {
+ "id": "f3b23b65-93ca-11ea-ae23-0a58a9feac2a",
+ "name": "Shortness of breath"
+ },
+ {
+ "id": "05239c9e-93cb-11ea-ae23-0a58a9feac2a",
+ "name": "Difficulty breathing"
+ }
+ ]
+ }
+]
diff --git a/assets/strings.en.json b/assets/strings.en.json
new file mode 100644
index 00000000..07d9b159
--- /dev/null
+++ b/assets/strings.en.json
@@ -0,0 +1,796 @@
+{
+ "app.title":"Safer Illinois",
+ "app.offline.message.title":"You appear to be offline",
+
+ "dialog.yes.title":"Yes",
+ "dialog.yes.hint":"",
+ "dialog.no.title":"No",
+ "dialog.no.hint":"",
+ "dialog.ok.title":"OK",
+ "dialog.ok.hint":"",
+ "dialog.cancel.title":"Cancel",
+ "dialog.cancel.hint":"",
+ "dialog.continue.title":"Continue",
+ "dialog.continue.hint":"",
+ "dialog.close.title":"Close",
+ "dialog.close.hint":"",
+
+ "tabbar.home.title":"Home",
+ "tabbar.home.hint":"Home Page",
+ "tabbar.explore.title":"Explore",
+ "tabbar.explore.hint":"Explore Page",
+ "tabbar.more.title":"More",
+ "tabbar.more.hint":"More Page",
+ "tabbar.browse.title":"Browse",
+ "tabbar.browse.hint":"Browse Page",
+ "tabbar.wallet.title":"Wallet",
+ "tabbar.wallet.hint":"Wallet Page",
+
+ "headerbar.home.title":"Home",
+ "headerbar.home.hint":"Home Page",
+ "headerbar.menu.title":"Menu",
+ "headerbar.menu.hint":"",
+ "headerbar.search.title":"Search",
+ "headerbar.search.hint":"Type a search term",
+ "headerbar.settings.title":"Settings",
+ "headerbar.settings.hint":"",
+ "headerbar.search.placehlder":"What you are looking for?",
+ "headerbar.back.title":"Back",
+ "headerbar.back.hint":"",
+ "headerbar.close.title":"Close",
+ "headerbar.close.hint":"",
+ "headerbar.saved.title":"Saved",
+ "headerbar.saved.hint":"",
+ "headerbar.teams.title":"Teams",
+
+ "toggle_button.status.checked": "checked",
+ "toggle_button.status.unchecked": "unchecked",
+ "toggle_button.status.checkbox": "checkbox",
+
+ "panel.menu.button.events.title":"Events",
+ "panel.menu.button.events.hint":"",
+ "panel.menu.button.dining.title":"Dining",
+ "panel.menu.button.dining.hint":"",
+ "panel.menu.button.athletics.title":"Athletics",
+ "panel.menu.button.athletics.hint":"",
+ "panel.menu.button.settings.title":"Settings",
+ "panel.menu.button.settings.hint":"",
+ "panel.menu.button.illini_cash.title":"Illini Cash",
+ "panel.menu.button.illini_cash.hint":"",
+ "panel.menu.button.create_event.title":"Create an event",
+ "panel.menu.button.create_event.hint":"",
+ "panel.menu.button.order_history.title":"Order History",
+ "panel.menu.button.order_history.hint":"",
+ "panel.menu.button.sign_out.title":"Sign Out",
+ "panel.menu.button.sign_out.hint":"",
+ "panel.menu.button.sign_in.title":"Sign In",
+ "panel.menu.button.sign_in.hint":"",
+ "panel.menu.button.create_account.title":"Create account",
+ "panel.menu.button.create_account.hint":"",
+ "panel.menu.label.sign_in_as":"Signed in as",
+ "panel.menu.label.verified_as":"Verified as",
+ "panel.menu.label.connected_netid":"Connected NetID",
+ "panel.menu.label.confirm_sign_out":"Are you sure you want to sign out?",
+
+ "panel.onboarding.button.continue.title":"Continue",
+ "panel.onboarding.button.continue.hint":"",
+
+ "panel.onboarding.get_started.image.welcome.title":"Welcome to Illinois",
+ "panel.onboarding.get_started.image.welcome.hint":"",
+ "panel.onboarding.get_started.button.get_started.title":"Get Started",
+ "panel.onboarding.get_started.button.get_started.hint":"",
+ "panel.onboarding.get_started.image.safer_in_illinois.title":"Safer in Illinois",
+ "panel.onboarding.get_started.image.powered.title":"Powered by Rokwire",
+
+ "panel.onboarding.location.label.title":"Turn on Location Services",
+ "panel.onboarding.location.label.title.hint":"Header 1",
+ "panel.onboarding.location.label.description":"Required for exposure notifications to work on your phone",
+ "panel.onboarding.location.button.allow.title":"Share my Location",
+ "panel.onboarding.location.button.allow.hint":"",
+ "panel.onboarding.location.button.dont_allow.title":"Not right now",
+ "panel.onboarding.location.button.dont_allow.hint":"Skip sharing location",
+ "panel.onboarding.location.label.access_granted":"You have already granted access to this app.",
+ "panel.onboarding.location.label.access_denied":"You have already denied access to this app.",
+
+ "panel.onboarding.bluetooth.label.title":"Enable Bluetooth",
+ "panel.onboarding.bluetooth.label.title.hint":"Header 1",
+ "panel.onboarding.bluetooth.label.description":"Use Bluetooth to alert you to potential exposure to COVID-19.",
+ "panel.onboarding.bluetooth.button.allow.title":"Enable Bluetooth",
+ "panel.onboarding.bluetooth.button.allow.hint":"",
+ "panel.onboarding.bluetooth.button.dont_allow.title":"Not right now",
+ "panel.onboarding.bluetooth.button.dont_allow.hint":"Skip enabling Bluetooth",
+ "panel.onboarding.bluetooth.label.access_granted":"You have already granted access to this app.",
+ "panel.onboarding.bluetooth.label.access_denied":"You have already denied access to this app.",
+
+ "panel.onboarding.notifications.label.title":"Info when you need it",
+ "panel.onboarding.notifications.label.title.hint":"Header 1",
+ "panel.onboarding.notifications.label.description":"Get notified about COVID-19 info",
+ "panel.onboarding.notifications.button.allow.title":"Receive Notifications",
+ "panel.onboarding.notifications.button.allow.hint":"",
+ "panel.onboarding.notifications.button.dont_allow.title":"Not right now",
+ "panel.onboarding.notifications.button.dont_allow.hint":"Skip receiving notifications",
+ "panel.onboarding.notifications.label.access_granted":"Your settings have been changed.",
+
+ "panel.onboarding.roles.label.title":"Who are you?",
+ "panel.onboarding.roles.label.title.hint":"Header 1",
+ "panel.onboarding.roles.label.description":"Select all that apply",
+ "panel.onboarding.roles.button.student.title":"University Student",
+ "panel.onboarding.roles.button.student.hint":"",
+ "panel.onboarding.roles.button.employee.title":"Employee/Affiliate",
+ "panel.onboarding.roles.button.employee.hint":"",
+ "panel.onboarding.roles.button.resident.title":"Illinois Resident",
+ "panel.onboarding.roles.button.resident.hint":"",
+ "panel.onboarding.roles.button.continue.enabled.title":"Confirm",
+ "panel.onboarding.roles.button.continue.disabled.title":"Select one",
+ "panel.onboarding.roles.button.continue.hint":"",
+
+ "panel.onboarding.login.netid.label.title":"Connect your NetID",
+ "panel.onboarding.login.phone.label.title":"Verify your phone number",
+ "panel.onboarding.login.netid.label.title.hint":"Header 1",
+ "panel.onboarding.login.phone.label.title.hint":"Header 1",
+ "panel.onboarding.login.netid.label.description":"Log in with your NetID",
+ "panel.onboarding.login.phone.label.description":"This saves your preferences so you can have the same experience on more than one device.",
+ "panel.onboarding.login.label.login_failed":"Unable to login. Please try again later",
+ "panel.onboarding.login.netid.button.continue.title":"Log in with NetID",
+ "panel.onboarding.login.phone.button.continue.title":"Verify My Phone Number",
+ "panel.onboarding.login.netid.button.continue.hint":"",
+ "panel.onboarding.login.phone.button.continue.hint":"",
+ "panel.onboarding.login.netid.button.dont_continue.title":"Not right now",
+ "panel.onboarding.login.phone.button.dont_continue.title":"Not right now",
+ "panel.onboarding.login.netid.button.dont_continue.hint":"Skip verification",
+ "panel.onboarding.login.phone.button.dont_continue.hint":"Skip verification",
+
+ "panel.onboarding.verify_phone.title":"Verify your phone number",
+ "panel.onboarding.verify_phone.title.hint":"",
+ "panel.onboarding.verify_phone.description":"To verify your phone number, choose your preferred contact channel, and we'll send you a one-time authentication code.",
+ "panel.onboarding.verify_phone.description.hint":"",
+ "panel.onboarding.verify_phone.phone_number.label":"Phone number",
+ "panel.onboarding.verify_phone.phone_number.hint":"",
+ "panel.onboarding.verify_phone.text_me.label":"Text me",
+ "panel.onboarding.verify_phone.text_me.hint":"",
+ "panel.onboarding.verify_phone.call_me.label":"Call me",
+ "panel.onboarding.verify_phone.call_me.hint":"",
+ "panel.onboarding.verify_phone.button.next.label":"Next",
+ "panel.onboarding.verify_phone.button.next.hint":"Tap to continue",
+ "panel.onboarding.verify_phone.validation.phone_number.text":"Please, type your phone number",
+ "panel.onboarding.verify_phone.validation.channel_selection.text":"Please, select verification method",
+ "panel.onboarding.verify_phone.validation.server_error.text":"Please enter a valid phone number",
+
+ "panel.onboarding.confirm_phone.title":"Confirm your code",
+ "panel.onboarding.confirm_phone.title.hint":"",
+ "panel.onboarding.confirm_phone.description.send":"A one time code has been sent to %s. Enter your code below to continue.",
+ "panel.onboarding.confirm_phone.description.send.hint":"",
+ "panel.onboarding.confirm_phone.code.label":"One-time code",
+ "panel.onboarding.confirm_phone.code.hint":"",
+ "panel.onboarding.confirm_phone.not_received.text.label":"Didn't receive a text?",
+ "panel.onboarding.confirm_phone.not_received.text.hint":"",
+ "panel.onboarding.confirm_phone.not_received.call.label":"Didn't receive a call?",
+ "panel.onboarding.confirm_phone.not_received.call.hint":"",
+ "panel.onboarding.confirm_phone.button.confirm.label":"Confirm phone number",
+ "panel.onboarding.confirm_phone.button.confirm.hint":"",
+ "panel.onboarding.confirm_phone.validation.phone_number.text":"Please, fill your code",
+ "panel.onboarding.confirm_phone.validation.server_error.text":"Failed to verify code",
+
+ "panel.onboarding.upgrade.required.label.title":"Upgrade Required",
+ "panel.onboarding.upgrade.required.label.description":"%s app version %s requires an upgrade to version %s or later.",
+ "panel.onboarding.upgrade.available.label.title":"Upgrade Available",
+ "panel.onboarding.upgrade.available.label.description":"%s app version %s has newer version %s available.",
+ "panel.onboarding.upgrade.button.upgrade.title":"Upgrade",
+ "panel.onboarding.upgrade.button.upgrade.hint":"",
+ "panel.onboarding.upgrade.button.not_now.title":"Not right now",
+ "panel.onboarding.upgrade.button.not_now.hint":"",
+ "panel.onboarding.upgrade.button.dont_show.title":"Don't show again",
+ "panel.onboarding.upgrade.button.dont_show.hint":"",
+
+ "panel.onboarding.base.not_now.hint":"",
+ "panel.onboarding.base.not_now.title":"Not right now",
+
+ "panel.settings.sports.heading":"Settings",
+ "panel.settings.categories.header.title":"YOUR CATEGORIES",
+ "panel.settings.categories.heading":"Your Categories",
+ "panel.settings.categories.description":"Tap to follow the categories that interest you most",
+
+ "panel.settings.feedback.label.title": "Provide Feedback",
+
+ "panel.settings.privacy_statement.label.title": "Privacy Statement",
+
+ "panel.settings.label.offline.feedback": "Providing a feedback is not available while offline.",
+
+ "panel.settings.home.settings.header":"Settings",
+ "panel.settings.home.connect.not_logged_in.title":"Connect to Illinois",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_1": "Are you a ",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_2": "student",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_3": " or ",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_4": "faculty member",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_5": "? Log in with your NetID to see Illinois information specific to you, like your Illini Cash and meal plan.",
+ "panel.settings.home.connect.not_logged_in.netid.title": "Connect your NetID",
+ "panel.settings.home.connect.not_logged_in.phone.description.part_1": "Don't have a NetID",
+ "panel.settings.home.connect.not_logged_in.phone.description.part_2": "? Verify your phone number to save your preferences and have the same experience on more than one device.",
+ "panel.settings.home.connect.not_logged_in.phone.title": "Verify Your Phone Number",
+ "panel.settings.home.customizations.title":"Customizations",
+ "panel.settings.home.net_id.title":"Illinois NetID",
+ "panel.settings.home.phone_ver.title":"Phone Verification",
+ "panel.settings.home.notifications.title": "Notifications",
+ "panel.settings.home.user_info.title.sufix":"Welcome to Illinois",
+ "panel.settings.home.account.title": "Your Account",
+ "panel.settings.home.account.personal_info.title":"Personal Info",
+ "panel.settings.home.account.options.payment":"Payment",
+ "panel.settings.home.customizations.role.title":"Who you are",
+ "panel.settings.home.customizations.manage_interests.title":"Manage your interests",
+ "panel.settings.home.customizations.food_filters.title":"Food filters",
+ "panel.settings.home.customizations.display_times_in_central_time.title": "Display all times in Central Time",
+ "panel.settings.home.net_id.message":"Connected as ",
+ "panel.settings.home.net_id.button.disconnect":"Disconnect your NetID",
+ "panel.settings.home.net_id.button.connect":"Connect your NetID",
+ "panel.settings.home.phone_ver.message":"Verified as ",
+ "panel.settings.home.phone_ver.button.connect":"Verify Your Phone Number",
+ "panel.settings.home.phone_ver.button.disconnect":"Disconnect your Phone",
+ "panel.settings.home.logout.message":"Are you sure you want to sign out?",
+ "panel.settings.home.logout.button.yes":"Yes",
+ "panel.settings.home.logout.no":"No",
+ "panel.settings.home.security.reset_password":"ResetPassword",
+ "panel.settings.home.security.face_id":"Use Face ID",
+ "panel.settings.home.notifications.reminders":"Event reminders",
+ "panel.settings.home.notifications.athletics_updates":"Athletics updates",
+ "panel.settings.home.notifications.dining":"Dining specials",
+ "panel.settings.home.notifications.covid19":"COVID-19 notifications",
+ "panel.settings.home.privacy.title" :"Privacy",
+ "panel.settings.home.privacy.edit_my_privacy.title":"Edit My Privacy",
+ "panel.settings.home.privacy.privacy_statement.title":"Privacy Statement",
+ "panel.settings.home.button.debug.title":"Debug",
+ "panel.settings.home.button.debug.hint":"",
+ "panel.settings.home.button.test.title":"Test",
+ "panel.settings.home.button.test.hint":"",
+ "panel.settings.home.feedback.title": "We need your ideas!",
+ "panel.settings.home.feedback.description": "Enjoying the app? Missing something? Tap on the bottom to submit your idea.",
+ "panel.settings.home.button.feedback.title": "Submit Feedback",
+ "panel.settings.home.button.feedback.hint": "",
+ "panel.settings.home.covid19.exposure_notifications": "Exposure Notifications",
+ "panel.settings.home.covid19.provider_test_result": "Health Provider Test Results",
+ "panel.settings.home.covid19.title": "COVID-19",
+ "panel.settings.home.covid19.qr_code.button.title": "QR Code",
+ "panel.settings.home.covid19.transfer.button.title": "Transfer",
+ "panel.settings.home.covid19.qr_code.description.title": "COVID-19 Secret QR code",
+ "panel.settings.home.covid19.transfer.description.title": "Transfer the COVID-19 secret from your other phone",
+ "panel.settings.home.covid19.alert.qr_code.scan.failed.msg": "Failed to read QR code.",
+ "panel.settings.home.covid19.alert.qr_code.transfer.succeeded.msg": "COVID-19 secret transferred successfully.",
+ "panel.settings.home.covid19.alert.qr_code.transfer.failed.msg": "Failed to transfer COVID-19 secret.",
+ "panel.settings.home.covid19.alert.reset.prompt": "Doing this will provide you a new COVID-19 Secret QRcode but your previous COVID-19 event history will be lost, continue?",
+ "panel.settings.home.covid19.alert.reset.failed": "Failed to reset the COVID-19 Secret QRcode",
+ "panel.settings.home.covid19.text.user.fail": "Unable to retrieve user COVID-19 settings.",
+ "panel.settings.home.covid19.text.keys.checking": "Checking COVID-19 keys...",
+ "panel.settings.home.covid19.text.keys.missing.public": "Missing COVID-19 public key",
+ "panel.settings.home.covid19.text.keys.missing.private": "Missing COVID-19 private key",
+ "panel.settings.home.covid19.text.keys.mismatch": "COVID-19 keys not paired",
+ "panel.settings.home.covid19.text.keys.paired": "COVID-19 keys valid and paired",
+ "panel.settings.home.covid19.text.keys.reset": "Reset the COVID-19 keys pair.",
+ "panel.settings.home.covid19.text.keys.transfer_or_reset": "Transfer the COVID-19 private key from your other phone or reset the COVID-19 keys pair.",
+ "panel.settings.home.covid19.text.keys.qr_code": "Show your COVID-19 secret QR code.",
+ "panel.settings.home.covid19.button.retry.title": "Retry",
+ "panel.settings.home.covid19.button.reset.title": "Reset",
+ "panel.settings.home.covid19.button.load.title": "Load",
+ "panel.settings.home.covid19.button.scan.title": "Scan",
+ "panel.settings.home.covid19.button.qr_code.title": "QR Code",
+
+ "panel.settings.label.offline.phone_ver":"Verify Your Phone Number is not available while offline.",
+
+ "panel.profile_info.header.title":"PERSONAL INFO",
+ "panel.profile_info.net_id.title":"NetID",
+ "panel.profile_info.full_name.title":"Full Name",
+ "panel.profile_info.first_name.title":"First Name",
+ "panel.profile_info.middle_name.title":"Middle Name",
+ "panel.profile_info.last_name.title":"Last Name",
+ "panel.profile_info.email_address.title":"Email Address",
+ "panel.profile_info.phone_number.title":"Phone Number",
+ "panel.profile_info.button.sign_out.title":"Sign Out",
+ "panel.profile_info.button.sign_out.hint":"",
+ "panel.profile_info.label.remove_my_info.title":"Remove My Info",
+ "panel.profile_info.button.remove_my_information.title":"Remove My Information",
+ "panel.profile_info.button.remove_my_information.hint":"",
+ "panel.profile_info.dialog.remove_my_information.title":"By answering YES all your personal information and preferences will be deleted from our systems. This action can not be recovered. After deleting the information we will return you to the first screen when you installed the app so you can start again or delete the app.",
+ "panel.profile_info.dialog.remove_my_information.subtitle": "Are you sure?",
+ "panel.profile_info.dialog.remove_my_information.yes.title": "Yes",
+ "panel.profile_info.dialog.remove_my_information.no.title": "No",
+
+ "panel.covid19_passport.header.title": "COVID-19",
+ "panel.covid19_passport.button.close.title": "Close",
+ "panel.covid19_passport.button.info_center.title": "Your COVID-19 info center",
+ "panel.covid19_passport.label.status.empty":"No available status for this County",
+ "panel.covid19_passport.label.counties.empty":"No counties available",
+ "panel.covid19_passport.label.county.empty.hint":"Select a county...",
+ "panel.covid19_passport.label.access.heading": "Building Access",
+ "panel.covid19_passport.label.access.granted": "GRANTED",
+ "panel.covid19_passport.label.access.denied": "DENIED",
+ "panel.covid19_passport.message.missing_id_info": "No Illini ID information found. You may have an expired i-card. Please contact the ID Center.",
+
+ "panel.covid19_guidelines.header.title": "County Guidelines",
+ "panel.covid19_guidelines.description.title": "Help stop the spread of COVID-19 by following these current guidelines.",
+ "panel.covid19_guidelines.status.title": "These are based on your %s status in the following county:",
+ "panel.covid19_guidelines.label.county.empty":"Select a county...",
+ "panel.covid19_guidelines.no.status":"There are no specific guidelines for your status in this county.",
+
+ "panel.covid19home.header.title": "Safer Illinois Home",
+ "panel.covid19home.top_heading.title": "Stay Healthy",
+ "panel.covid19home.label.next_step.title": "NEXT STEP",
+ "panel.covid19home.label.schedule_after.title": "Schedule after %s",
+ "panel.covid19home.button.find_test_locations.title": "Find test locations",
+ "panel.covid19home.button.find_test_locations.hint": "",
+ "panel.covid19home.button.country_guidelines.title": "County\nGuidelines",
+ "panel.covid19home.button.country_guidelines.hint": "",
+ "panel.covid19home.button.care_team.title": "Your\nCare Team",
+ "panel.covid19home.button.care_team.hint": "",
+ "panel.covid19home.button.campus_updates.title": "Campus Updates",
+ "panel.covid19home.button.campus_updates.hint": "",
+ "panel.covid19home.button.report_test.title": "Report a Test Result",
+ "panel.covid19home.button.report_test.hint": "",
+ "panel.covid19home.button.test_history.title": "Your Testing History",
+ "panel.covid19home.button.test_history.hint": "",
+ "panel.covid19home.label.health.title":"Your Health",
+ "panel.covid19home.label.resources.title":"Resources",
+ "panel.covid19home.label.check_in.title":"Symptom Check-in",
+ "panel.covid19home.label.check_in.description":"Self-report any symptoms to see if you should get tested or stay home.",
+ "panel.covid19home.label.result.title":"Add Test Result",
+ "panel.covid19home.label.result.description":"To keep your status up-to-date.",
+ "panel.covid19home.label.status.title":"Current Status:",
+ "panel.covid19home.label.status.na":"Not Available",
+ "panel.covid19home.button.show_status_card.title":"Show Status Card",
+ "panel.covid19home.label.campus_updates.title":"Campus Updates",
+ "panel.covid19home.button.about.title":"About",
+ "panel.covid19home.label.most_recent_event.title": "MOST RECENT EVENT",
+ "panel.covid19home.label.provider.self_reported": "Self reported",
+ "panel.covid19home.label.action_required.title": "Action Required",
+ "panel.covid19home.label.contact_trace.title": "Contact Trace",
+ "panel.covid19home.label.reported_symptoms.title": "Self Reported Symptoms",
+
+ "panel.covid19_test_locations.header.title": "Test Locations",
+ "panel.covid19_test_locations.label.contact.title": "Contact",
+ "panel.covid19_test_locations.distance.text": "mi away get directions",
+ "panel.covid19_test_locations.distance.unknown": "unknown distance",
+ "panel.covid19_test_locations.work_time.unknown": "Unknown working time",
+ "panel.covid19_test_locations.work_time.open_until":"Open until",
+ "panel.covid19_test_locations.work_time.closed_until": "Closed until",
+ "panel.covid19_test_locations.all_providers.text": "All Providers",
+ "panel.covid19_test_locations.call.hint": "Call",
+
+ "panel.covid19.header.title": "COVID-19",
+ "panel.covid19.latest_update.title": "Latest Update",
+ "panel.covid19.latest_update.read_more.title": "Read more",
+ "panel.covid19.health_status.title": "Your Health",
+ "panel.covid19.news.title": "COVID-19 News",
+ "panel.covid19.faq.title":"FAQ",
+ "panel.covid19.faq.description":"Answers to your most common questions:",
+ "panel.covid19.faq.update.text":"Updated %s",
+ "panel.covid19.faq.question.hint":"Double tap to show questions",
+ "panel.covid19.faq.title.label":"Frequently asked questions",
+ "panel.covid19.resources.poor_accessibility.hint": "This link takes you to a website outside of the Safer Illinois app",
+ "panel.covid19.label.current_status.label": "Current status:",
+ "panel.covid19.button.show_status_card.title": "Show Status Card",
+ "panel.covid19.button.show_status_card.hint": "",
+ "panel.covid19.button.country_guidelines.title": "County Guidelines",
+ "panel.covid19.button.country_guidelines.hint": "",
+ "panel.covid19.button.campus_updates.title": "Campus Updates",
+ "panel.covid19.button.campus_updates.hint": "",
+ "panel.covid19.button.health_history.title": "View Health History",
+ "panel.covid19.button.health_history.hint": "",
+ "panel.covid19.finish_setup.title": "Finish setup",
+ "panel.covid19.finish_setup_description.title": "To use your official status",
+ "panel.covid19.button.verify_identity.title": "Verify your Identity",
+ "panel.covid19.unverified.title": "Unverified",
+ "panel.covid19.button.covid_wellness_center.title": "COVID-19 Wellness Answer Center",
+ "panel.covid19.button.covid_wellness_center.hint": "",
+
+ "panel.covid19_wellness_center.header.title": "COVID-19 Wellness Center",
+ "panel.covid19_wellness_center.label.description": "If you having issues with the app or getting a test result, contact the COVID Wellness Answer Center for assistance.",
+ "panel.covid19_wellness_center.label.email": "Email the Covid Wellness Answer Center at ",
+ "panel.covid19_wellness_center.label.phone": "Phone the Answer Center at ",
+
+ "panel.covid19_news.header.title": "COVID-19",
+ "panel.covid19_news.news.posted.label": "Posted on %s",
+
+ "panel.covid19_campus_updates.header.title": "Campus updates",
+ "panel.covid19_campus_updates.sub_title.title": "University of Illinois COVID-19 updates",
+
+ "panel.covid19.qr_code.title": "COVID-19 QR Code",
+ "panel.covid19.qr_code.description.heading.1": "If you use more than one device with the Safer Illinois app, use this QR code to transfer the necessary secret to decode your COVID-19 health information.",
+ "panel.covid19.qr_code.description.heading.2": "Save this QR code so that If you lose or replace your phone, you can retrieve your COVID-19 health information on your new phone.",
+ "panel.covid19.qr_code.button.save.title": "Save",
+ "panel.covid19.qr_code.alert.no_qr_code.msg": "There is no QR Code",
+ "panel.covid19.qr_code.alert.save.success.msg": "Successfully saved qr code in gallery",
+ "panel.covid19.qr_code.alert.save.fail.msg": "Failed to save qr code in gallery",
+ "panel.covid19.qr_code.code.hint": "QR code image",
+ "panel.covid19.qr_code.description.on_boarding.heading.1": "Save this QR code so that If you lose or replace your phone, you can retrieve your COVID-19 health information on your new phone.",
+ "panel.covid19.qr_code.description.on_boarding.heading.2": "You can always skip this step for now and do it later in Settings",
+ "panel.covid19.qr_code.button.skip.title.": "Skip",
+ "panel.covid19.qr_code.button.skip.hint.": "Skip saving",
+
+ "panel.covid19.transfer.title": "Transfer Encryption Key",
+ "panel.covid19.transfer.label.qr_image_label": "Safer Illinois COVID-19 Code",
+ "panel.covid19.transfer.label.save_error": "Unable to save the QR code.",
+ "panel.covid19.transfer.button.continue.hint": "",
+ "panel.covid19.transfer.primary.heading.title": "Your COVID-19 Encryption Key",
+ "panel.covid19.transfer.primary.button.save.title": "Save Your Encryption Key",
+ "panel.covid19.transfer.primary.button.save.hint": "",
+ "panel.covid19.transfer.secondary.heading.title": "Missing COVID-19 Encryption Key",
+ "panel.covid19.transfer.secondary.button.scan.heading": "If you are adding a second device:",
+ "panel.covid19.transfer.secondary.button.scan.description": "If you still have access to your primary device, you can directly scan the COVID-19 Encryption Key QR code from that device.",
+ "panel.covid19.transfer.secondary.button.scan.title": "Scan Your QR Code",
+ "panel.covid19.transfer.secondary.button.retrieve.heading": "If you are using a replacement device:",
+ "panel.covid19.transfer.secondary.button.retrieve.description": "If you no longer have access to your primary device, but saved your QR code to a cloud photo service, you can transfer your COVID-19 Encryption Key by retrieving it from your photos.",
+ "panel.covid19.transfer.secondary.button.retrieve.title": "Retrieve Your QR Code",
+ "panel.covid19.transfer.alert.no_qr_code.msg": "There is no QR Code",
+ "panel.covid19.transfer.alert.save.success.msg": "Successfully saved qr code in ",
+ "panel.covid19.transfer.alert.save.success.pictures": "Pictures",
+ "panel.covid19.transfer.alert.save.success.gallery": "Gallery",
+ "panel.covid19.transfer.alert.save.fail.msg": "Failed to save qr code in ",
+ "panel.covid19.transfer.alert.qr_code.scan.failed.msg": "Failed to read QR code.",
+ "panel.covid19.transfer.alert.qr_code.invalid.msg": "Invalid QR code.",
+ "panel.covid19.transfer.alert.qr_code.not_match.msg": "COVID-19 secret key does not match existing public RSA key.",
+ "panel.covid19.transfer.alert.qr_code.transfer.succeeded.msg": "COVID-19 secret transferred successfully.",
+ "panel.covid19.transfer.alert.qr_code.transfer.failed.msg": "Failed to transfer COVID-19 secret.",
+
+ "panel.debug.header.title": "Debug",
+
+ "panel.debug_messaging.header.title": "Messaging",
+
+ "panel.web.offline.message": "You need to be online in order to perform this operation. Please check your Internet connection.",
+
+ "panel.health.onboarding.covid19.intro.label.title": "Join the fight against COVID-19",
+ "panel.health.onboarding.covid19.intro.label.description": "Track and manage your health to help keep our Illinois community safe",
+ "panel.health.onboarding.covid19.intro.button.continue.title": "Continue",
+ "panel.health.onboarding.covid19.intro.button.continue.hint": "",
+
+ "panel.health.onboarding.covid19.how_it_works.heading.title": "How it works",
+ "panel.health.onboarding.covid19.how_it_works.line1.title": "Testing and limiting exposure are key to slowing the spread of COVID-19.",
+ "panel.health.onboarding.covid19.how_it_works.line2.title": "You can use this app to:",
+ "panel.health.onboarding.covid19.how_it_works.line3.title": "Self-diagnose your COVID-19 symptoms and in doing so update your status.",
+ "panel.health.onboarding.covid19.how_it_works.line4.title": "Automatically receive test results from your healthcare provider.",
+ "panel.health.onboarding.covid19.how_it_works.line5.title": "Allow your phone to send exposure notifications when you’ve been in proximity to people who test positive.",
+ "panel.health.onboarding.covid19.how_it_works.button.next.title": "Next",
+ "panel.health.onboarding.covid19.how_it_works.button.next.hint": "",
+
+ "panel.health.onboarding.covid19.consent.label.title": "Consent for COVID-19 features",
+ "panel.health.onboarding.covid19.consent.label.description": "Exposure Notifications",
+ "panel.health.onboarding.covid19.consent.label.content1": "If you consent to exposure notifications, you allow your phone to send an anonymous Bluetooth signal to nearby Safer Illinois app users who are also using this feature. Your phone will receive and record a signal from their phones as well. If one of those users tests positive for COVID-19 in the next 14 days, the app will alert you to your potential exposure and advise you on next steps. Your identity and health status will remain anonymous, as will the identity and health status of all other users.",
+ "panel.health.onboarding.covid19.consent.check_box.label.participate": "I consent to participate in the Exposure Notification System (requires Bluetooth to be ON).",
+ "panel.health.onboarding.covid19.consent.check_box.label.allow":"I consent to allow my healthcare provider to provide my test results.",
+ "panel.health.onboarding.covid19.consent.label.content2": "Automatic Test Results",
+ "panel.health.onboarding.covid19.consent.label.content3": "I consent to connect test results from my healthcare provider with the Safer Illinois app.",
+ "panel.health.onboarding.covid19.consent.label.content4": "Your participation in these COVID-19 features is voluntary, and you can stop at any time",
+ "panel.health.onboarding.covid19.consent.button.consent.title": "Next",
+ "panel.health.onboarding.covid19.consent.button.consent.hint": "",
+ "panel.health.onboarding.covid19.consent.button.scroll_to_continue.title": "Scroll to Continue",
+ "panel.health.onboarding.covid19.consent.label.error.login":"Unable to login in Health",
+
+ "panel.health.onboarding.covid19.resident_info.label.title": "Verify your identity with a government-issued ID",
+ "panel.health.onboarding.covid19.resident_info.label.description": "After verifying you will receive a color-coded health status based on your county guidelines, symptoms, and any COVID-19 related tests.",
+ "panel.health.onboarding.covid19.resident_info.button.passport.title": "Passport",
+ "panel.health.onboarding.covid19.resident_info.button.passport.hint": "",
+ "panel.health.onboarding.covid19.resident_info.button.drivers_license.title": "Driver's License",
+ "panel.health.onboarding.covid19.resident_info.button.drivers_license.hint": "",
+ "panel.health.onboarding.covid19.resident_info.button.verify_later.title": "Verify later",
+
+ "panel.health.onboarding.covid19.review_scan.label.title": "Review your scan",
+ "panel.health.onboarding.covid19.review_scan.label.name.title": "Name",
+ "panel.health.onboarding.covid19.review_scan.label.birth_year.title": "Birth Year",
+ "panel.health.onboarding.covid19.review_scan.message.failed": "Failed to apply scanned data",
+ "panel.health.onboarding.covid19.review_scan.button.rescan.title": "Re-scan",
+ "panel.health.onboarding.covid19.review_scan.button.rescan.hint": "",
+ "panel.health.onboarding.covid19.review_scan.button.use_scan.title": "Use This Scan",
+ "panel.health.onboarding.covid19.review_scan.button.use_scan.hint": "",
+
+ "panel.health.onboarding.covid19.county.label.title": "What counties do you live and work in?",
+ "panel.health.onboarding.covid19.county.label.description": "Select all that apply",
+ "panel.health.onboarding.covid19.county.button.add_county.label": "Add Another County",
+ "panel.health.onboarding.covid19.county.button.add_county.hint": "",
+ "panel.health.onboarding.covid19.county.button.next.title": "Next",
+ "panel.health.onboarding.covid19.county.button.next.hint": "",
+ "panel.health.onboarding.covid19.county.select.label": "SELECT A COUNTY",
+ "panel.health.onboarding.covid19.county.dropdown.select.default.label": "Select a county...",
+ "panel.health.onboarding.covid19.county.alert.unique.message": "Please, select different county in each field!",
+
+ "panel.health.onboarding.covid19.providers.label.title": "Who are your current healthcare providers?",
+ "panel.health.onboarding.covid19.providers.label.description": "Select all that apply",
+ "panel.health.onboarding.covid19.providers.button.next.title": "Next",
+ "panel.health.onboarding.covid19.providers.button.next.hint": "",
+
+ "panel.health.onboarding.covid19.final.label.title": "You’re all set!",
+ "panel.health.onboarding.covid19.final.label.description": "You've been verified, and a status card has been added to your profile.",
+ "panel.health.onboarding.covid19.final.label.bottom.description":"You can now use this app as your companion in the fight against COVID-19.",
+ "panel.health.onboarding.covid19.final.label.unverified.description": "You can now use this app as your companion in the fight against COVID-19.",
+ "panel.health.onboarding.covid19.final.label.unverified.bottom.description":"To access your COVID-19 status, you will need to upload a Government ID. You can add this any time in the COVID-19 settings.",
+ "panel.health.onboarding.covid19.final.button.continue.title": "Get started",
+ "panel.health.onboarding.covid19.final.button.continue.hint": "",
+
+ "panel.health.onboarding.covid19.login.netid.label.title":"Connect your NetID",
+ "panel.health.onboarding.covid19.login.phone.label.title":"Verify your phone number",
+ "panel.health.onboarding.covid19.login.netid.label.title.hint":"Header 1",
+ "panel.health.onboarding.covid19.login.phone.label.title.hint":"Header 1",
+ "panel.health.onboarding.covid19.login.netid.label.description":"Log in with your NetID to use academic and housing specific features.",
+ "panel.health.onboarding.covid19.phone.label.description":"This saves your preferences so you can have the same experience on more than one device.",
+ "panel.health.onboarding.covid19.login.label.login_failed":"Unable to login. Please try again later",
+ "panel.health.onboarding.covid19.login.netid.button.continue.title":"Log in with NetID",
+ "panel.health.onboarding.covid19.login.phone.button.continue.title":"Verify My Phone Number",
+ "panel.health.onboarding.covid19.login.netid.button.continue.hint":"",
+ "panel.health.onboarding.covid19.login.phone.button.continue.hint":"",
+ "panel.health.onboarding.covid19.login.netid.button.dont_continue.title":"Not right now",
+ "panel.health.onboarding.covid19.login.phone.button.dont_continue.title":"Not right now",
+ "panel.health.onboarding.covid19.login.netid.button.dont_continue.hint":"Skip verification",
+ "panel.health.onboarding.covid19.login.phone.button.dont_continue.hint":"Skip verification",
+ "panel.health.onboarding.covid19.login.label.error.login":"Unable to login in Health",
+
+ "panel.health.covid19.about.heading.title":"About",
+
+ "panel.health.covid19.add_test.heading.title":"Add Test Result",
+ "panel.health.covid19.add_test.label.where_question":"Where was the test taken?",
+ "panel.health.covid19.add_test.label.information": "Why is this information needed?",
+ "panel.health.covid19.add_test.label.provider.title":"Healthcare Provider",
+ "panel.health.covid19.add_test.label.provider.empty_hint":"Select a provider",
+ "panel.health.covid19.add_test.button.retreive.title":"Retrieve Results",
+ "panel.health.covid19.add_test.button.enter_manually.title":"Manually Enter",
+ "panel.health.covid19.add_test.label.info.retrieved.text1": "Results ",
+ "panel.health.covid19.add_test.label.info.retrieved.text2": "retrieved ",
+ "panel.health.covid19.add_test.label.info.retrieved.text3": "from your healthcare provider are instantly verified. Any changes to your health status will be reflected instantly.",
+ "panel.health.covid19.add_test.label.info.manually.text1": "Results ",
+ "panel.health.covid19.add_test.label.info.manually.text2": "entered manually ",
+ "panel.health.covid19.add_test.label.info.manually.text3": "will be reviewed and verified by a public healthcare provider. Once verified, status changes may occur.",
+ "panel.health.covid19.add_test.label.manual_tests_disabled":"Test results from this health care provider will automatically appear if you have consented to Health Provider Test Results in settings and you are connected with your NetID.",
+
+ "panel.health.covid19.care_team.heading.title": "Your Care Team",
+ "panel.health.covid19.care_team.label.question": "We’re here to help.",
+ "panel.health.covid19.care_team.label.description": "Reach out to someone on your COVID-19 care team - we're here to help.",
+ "panel.health.covid19.care_team.label.status": "Current Status:",
+ "panel.health.covid19.care_team.label.emergency.text1": "In case of an emergency, ",
+ "panel.health.covid19.care_team.label.emergency.text2": "always call 911.",
+ "panel.health.covid19.care_team.team.title.mc_kinley": "Call McKinley Health",
+ "panel.health.covid19.care_team.team.contact.mc_kinley": "1-217-333-2700",
+ "panel.health.covid19.care_team.team.semantic_contact.mc_kinley": "12173332700",
+ "panel.health.covid19.care_team.team.description.mc_kinley": "Reach out to someone on the “Dial a Nurse Line” to discuss your symptoms and options for clinical care.",
+ "panel.health.covid19.care_team.team.title.osf": "OSF Healthcare",
+ "panel.health.covid19.care_team.team.contact.osf": "1-833-673-5669",
+ "panel.health.covid19.care_team.team.semantic_contact.osf": "18336735669",
+ "panel.health.covid19.care_team.team.description.osf": "We’ve partnered with OSF OnCall Connect program and the Illinois Department of Healthcare and Family Services to support you getting through COVID-19. Call the Nurse Hotline at 1-833-OSF-KNOW (833-673-5669) to learn more about the program, which includes delivery of a care kit and digital visits to monitor you over a 16-day period.",
+ "panel.health.covid19.care_team.label.more_info.title": "More about the OSF OnCall Connect program",
+ "panel.health.covid19.care_team.label.more_info.description": "Members of the OSF OnCall Connect care team are trained OSF HealthCare Mission Partners who connect with you to provide support and work with you and health care providers as you recover from COVID-19, decreasing the risk of further exposure. OSF OnCall Connect team members check on you daily and should your condition worsen, you will be referred to the Acute COVID@Home program where you will receive monitoring equipment that allows us to evaluate your blood pressure, heart rate and pulse ox.",
+ "panel.health.covid19.care_team.label.more_info.hint": "Double tap to show more info",
+ "panel.health.covid19.care_team.label.call.hint": "Call ",
+ "panel.health.covid19.care_team.label.more_info.link": "Learn more",
+
+ "panel.health.covid19.debug.keys.heading.title.":"COVID-19 Keys",
+ "panel.health.covid19.debug.keys.label.public_key":"RSA Public Key:",
+ "panel.health.covid19.debug.keys.label.private_key":"RSA Private Key:",
+ "panel.health.covid19.debug.keys.label.aes_key":"AES Key:",
+ "panel.health.covid19.debug.keys.label.blob":"Blob:",
+ "panel.health.covid19.debug.keys.label.encripted_aes":"Encrypted AES Key:",
+ "panel.health.covid19.debug.keys.label.encripted_blob":"Encrypted Blob:",
+ "panel.health.covid19.debug.keys.label.decripted_aes":"Decrypted AES Key:",
+ "panel.health.covid19.debug.keys.label.decripted_blob":"Decrypted Blob:",
+ "panel.health.covid19.debug.keys.button.refres.title":"Refresh RSA Keys",
+ "panel.health.covid19.debug.keys.button.generate_aes.title":"Generate AES Key",
+ "panel.health.covid19.debug.keys.button.encript.title":"Encrypt",
+ "panel.health.covid19.debug.keys.button.decript.title":"Decrypt",
+ "panel.health.covid19.debug.keys.label.error.refres.title":"Refresh Failed",
+
+ "panel.health.covid19.debug.trace.heading.title":"COVID-19 Contact Trace",
+ "panel.health.covid19.debug.trace.label.contact":"Trace COVID-19 Contact",
+ "panel.health.covid19.debug.trace.label.date":"Date",
+ "panel.health.covid19.debug.trace.label.duration":"Duration",
+ "panel.health.covid19.debug.trace.button.submit.title":"Submit Test Result",
+ "panel.health.covid19.debug.trace.message.date.text":"Please select a date",
+ "panel.health.covid19.debug.trace.message.duration.text": "Please enter an integer duration",
+ "panel.health.covid19.debug.trace.error.submit.text": "Failed to submit contact trace data.",
+
+ "panel.health.covid19.history.header.title":"Your COVID-19 Event History",
+ "panel.health.covid19.history.label.empty.title":"No History",
+ "panel.health.covid19.history.label.description":"View your COVID-19 event history.",
+ "panel.health.covid19.history.label.provider.hint":"provider: ",
+ "panel.health.covid19.history.label.empty.provider":"Unknown",
+ "panel.health.covid19.history.label.self_reported.title":"Self Reported Symptoms",
+ "panel.health.covid19.history.label.self_reported.symptoms":"symptoms: ",
+ "panel.health.covid19.history.label.contact_trace.title":"Contact Trace",
+ "panel.health.covid19.history.label.contact_trace.details":"contact trace: ",
+ "panel.health.covid19.history.label.action.title":"Action Required",
+ "panel.health.covid19.history.label.action.details":"action: ",
+ "panel.health.covid19.history.label.result.title":"Result: ",
+ "panel.health.covid19.history.label.location.title":"Test Location",
+ "panel.health.covid19.history.label.technician_name.title":"Technician Name",
+ "panel.health.covid19.history.label.technician_id.title":"Technician ID",
+ "panel.health.covid19.history.label.more_info.title":"More Info",
+ "panel.health.covid19.history.label.provider.self_reported": "Self reported",
+ "panel.health.covid19.history.label.verified": "Verified",
+ "panel.health.covid19.history.label.verification_pending": "Verification Pending",
+ "panel.health.covid19.history.message.clear_failed": "Failed to clear COVID-19 event history",
+ "panel.health.covid19.history.button.repost_history.title": "Request my latest test again",
+ "panel.health.covid19.history.button.repost_history.hint": "",
+ "panel.health.covid19.history.message.request_tests": "Your request has been submitted. You should receive your latest test within an hour",
+
+ "panel.health.covid19.qr_code.label.qr_image_label": "Safer Illinois COVID-19 Code",
+ "panel.health.covid19.qr_code.label.save_error": "Unable to save the QR code.",
+ "panel.health.covid19.qr_code.button.continue.hint": "",
+ "panel.health.covid19.qr_code.primary.heading.title": "Your COVID-19 Encryption Key",
+ "panel.health.covid19.qr_code.primary.description.1": "For your privacy, your healthcare data used for COVID-19 features is encrypted. The encryption key is stored locally on your phone to keep it secure. \n\nTo use the COVID-19 features on another device, you will need to manually transfer this encryption key using the QR code below.",
+ "panel.health.covid19.qr_code.primary.button.save.title": "Save Your Encryption Key",
+ "panel.health.covid19.qr_code.primary.button.save.hint": "",
+ "panel.health.covid19.qr_code.primary.description.2": "In the event your current device is lost or damaged, we suggest you save a copy of this QR code to a cloud photo storage service, so that it can be retrieved on your replacement device. \n\nYou can access and save this key on this device at any time by accessing \"Transfer Your COVID-19 Encryption Key\" from the COVID-19 info center.",
+ "panel.health.covid19.qr_code.secondary.heading.title": "Looks like you’ve used this feature before on another device",
+ "panel.health.covid19.qr_code.secondary.description.1": "Do you want to transfer your QR encyrption key to this device to retreive your previous health information?\n\nSelect which one applies to you below. You can always transfer a QR encryption key to this device at a later time using the “Transfer Your COVID-19 Encyrption Key” in the COVID-19 info center or in your app settings.",
+ "panel.health.covid19.qr_code.secondary.button.scan.heading": "If you are adding a second device:",
+ "panel.health.covid19.qr_code.secondary.button.scan.description": "If you still have access to your primary device, you can directly scan the COVID-19 Encryption Key QR code from that device.",
+ "panel.health.covid19.qr_code.secondary.button.scan.title": "Scan Your QR Code",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.heading": "If you are using a replacement device:",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.description": "If you no longer have access to your primary device, but saved your QR code to a cloud photo service, you can transfer your COVID-19 Encryption Key by retrieving it from your photos.",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.title": "Retrieve Your QR Code",
+ "panel.health.covid19.qr_code.reset.button.heading": "Reset my COVID-19 Secret QRcode:",
+ "panel.health.covid19.qr_code.reset.button.title": "Reset my COVID-19 Secret QRcode",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.title": "Reset my COVID-19 Secret QRcode",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.description": "Doing this will provide you a new COVID-19 Secret QRcode but your previous COVID-19 event history will be lost, continue?",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.confirm": "Are you sure?",
+
+ "panel.health.covid19.alert.no_qr_code.msg": "There is no QR Code",
+ "panel.health.covid19.alert.save.success.msg": "Successfully saved qr code in ",
+ "panel.health.covid19.alert.save.success.pictures": "Pictures",
+ "panel.health.covid19.alert.save.success.gallery": "Gallery",
+ "panel.health.covid19.alert.save.fail.msg": "Failed to save qr code in gallery",
+ "panel.health.covid19.alert.qr_code.scan.failed.msg": "Failed to read QR code.",
+ "panel.health.covid19.alert.qr_code.invalid.msg": "Invalid QR code.",
+ "panel.health.covid19.alert.qr_code.not_match.msg": "COVID-19 secret key does not match existing public RSA key.",
+ "panel.health.covid19.qr_code.alert.qr_code.transfer.succeeded.msg": "COVID-19 secret transferred successfully.",
+ "panel.health.covid19.alert.qr_code.transfer.failed.msg": "Failed to transfer COVID-19 secret.",
+ "panel.health.covid19.qr_code.button.continue.title": "Continue",
+ "panel.health.covid19.qr_code.button.transfer_later.title": "Transfer Later",
+
+ "panel.health.report_test.heading.title":"Manually Enter Result",
+ "panel.health.report_test.label.date":"TEST DATE AND TIME",
+ "panel.health.report_test.label.provider":"HEALTHCARE PROVIDER",
+ "panel.health.report_test.label.date.location":"TEST LOCATION",
+ "panel.health.report_test.label.location.empty":"Select location…",
+ "panel.health.report_test.label.type":"TEST TYPE",
+ "panel.health.report_test.label.result":"RESULT",
+ "panel.health.report_test.label.result.empty":"Select test result…",
+ "panel.health.report_test.label.image":"ADD TEST RESULT",
+ "panel.health.report_test.label.image.hint":"Upload an image of your test result.",
+ "panel.health.report_test.button.add_image.title":"Upload Image",
+ "panel.health.report_test.button.add_test.title":"Add Test",
+ "panel.health.report_test.button.close.title":"Close",
+ "panel.health.report_test.button.retake.title":"Retake",
+ "panel.health.report_test.label.select_photo":"Select photo",
+ "panel.health.report_test.label.select_photo.description":"Please take a photo of the test result or select it from the gallery",
+ "panel.health.report_test.button.take_photo":"Take photo",
+ "panel.health.report_test.button.select_gallery":"Select from gallery",
+ "panel.health.report_test.button.cancel":"Cancel",
+ "panel.health.report_test.error.create.message":"Unable to create test",
+ "panel.health.report_test.missing.image.message":"Please upload image",
+ "panel.health.report_test.future_date.forbidden.message": "You cannot submit a test in the future",
+
+ "widget.health.onboarding.indicator9.label.hint": "Covid-19 Onboarding process",
+
+ "widget.card.button.favorite.on.title":"Add To Favorites",
+ "widget.card.button.favorite.on.hint":"",
+ "widget.card.button.favorite.off.title":"Remove From Favorites",
+ "widget.card.button.favorite.off.hint":"",
+ "widget.card.label.interests":"Because of your interest in:",
+ "widget.card.label.converge":"match",
+
+ "panel.health.status_update.heading.title":"Status Update",
+ "panel.health.status_update.label.status_change":"Based on your results, your status has changed from ",
+ "panel.health.status_update.label.status_change.to":" to ",
+ "panel.health.status_update.label.next_steps":"NEXT STEPS",
+ "panel.health.status_update.label.asap":"ASAP",
+ "panel.health.status_update.button.find_location.title":"Find location",
+ "panel.health.status_update.label.loading":"Hang tight while we update your status",
+ "panel.health.status_update.button.continue.title":"See Next Steps",
+ "panel.health.status_update.button.continue.hint":"",
+ "panel.health.status_update.info_dialog.label1": "Status color definitions can change depending on different counties.",
+ "panel.health.status_update.info_dialog.label2": "Status colors for ",
+ "panel.health.status_update.info_dialog.label3": "Default status for new users is set to Orange.",
+ "panel.health.status_update.info_dialog.label4": "An up-to-date on-campus negative test result will reset your COVID-19 status to Yellow, and Building Entry will change to Granted.",
+ "panel.health.status_update.label.reason.title":"STATUS CHANGED BECAUSE:",
+ "panel.health.status_update.label.reason.result":"Result:",
+ "panel.health.status_update.label.reason.symptoms.title":"You reported new symptoms",
+ "panel.health.status_update.label.reason.exposed.title": "You were exposed to someone who was likely infected",
+ "panel.health.status_update.label.reason.exposure.detail": "Duration of exposure: ",
+ "panel.health.status_update.label.reason.action.title": "You were required an action by health authorities",
+ "panel.health.status_update.label.reason.action.detail": "Action Required: ",
+
+ "panel.health.next_steps.button.continue.title.find_locatio": "Find location",
+ "panel.health.next_steps.button.continue.title.care_team": "Get in Touch with Care Team",
+ "panel.health.next_steps.label.next_steps": "NEXT STEPS",
+ "panel.health.next_steps.label.asap": "ASAP",
+
+ "panel.health.symptoms.heading.title":"Are you experiencing any of these symptoms?",
+ "panel.health.symptoms.label.error.loading":"Failed to load symptoms.",
+ "panel.health.symptoms.button.submit.title":"Submit",
+ "panel.health.symptoms.label.success.submit.message":"Your symptoms have been processed.",
+ "panel.health.symptoms.label.error.submit":"Failed to submit symptoms.",
+
+ "widget.home_campus_tools.label.campus_tools":"Campus Resources",
+ "widget.home_campus_tools.button.events.title":"Events",
+ "widget.home_campus_tools.button.events.hint":"",
+ "widget.home_campus_tools.button.dining.title":"Dining",
+ "widget.home_campus_tools.button.dining.hint":"",
+ "widget.home_campus_tools.button.athletics.title":"Athletics",
+ "widget.home_campus_tools.button.athletics.hint":"",
+ "widget.home_campus_tools.button.illini_cash.title":"Illini Cash",
+ "widget.home_campus_tools.button.illini_cash.hint":"",
+ "widget.home_campus_tools.button.my_illini.title": "My Illini",
+ "widget.home_campus_tools.header.my_illini.title": "My Illini",
+ "widget.home_campus_tools.button.my_illini.hint": "",
+ "widget.home_campus_tools.button.covid19.title":"COVID-19",
+ "widget.home_campus_tools.button.covid19.hint":"",
+
+ "widget.home_covid19_info.label.covid19": "COVID-19 Info",
+ "widget.home_covid19_info.label.info.title": "Help keep Illinois safe",
+ "widget.home_covid19_info.label.info.description": "Join the fight against COVID-19 by tracking and managing your health.",
+
+ "widget.covid19_news_card.read_more.hint":"Double tap to read more",
+
+ "model.explore.time.today": "Today",
+ "model.explore.time.tomorrow": "Tomorrow",
+ "model.explore.time.at": "at",
+ "model.explore.time.all_day": "All Day",
+
+ "model.user.role.student.title": "Student",
+ "model.user.role.employee.title": "Employee",
+ "model.user.role.resident.title": "Illinois Resident",
+
+ "model.covid19.status.color.green": "Safe",
+ "model.covid19.status.color.yellow": "Caution",
+ "model.covid19.status.color.orange": "Likely Infected",
+ "model.covid19.status.color.red": "Infected",
+
+ "model.covid19.step.initial": "Take a SHIELD Saliva Test when you return to campus.",
+
+ "logic.date_time.greeting.morning": "Good morning",
+ "logic.date_time.greeting.afternoon": "Good afternoon",
+ "logic.date_time.greeting.evening": "Good evening",
+
+ "logic.polls.unable_to_load_poll": "Unable to load poll",
+ "logic.polls.no_polls_with_pin": "There are no polls with this 4 Digit Poll #",
+ "logic.polls.multiple_polls_with_pin": "There are multiple opened polls with this 4 Digit Poll #",
+
+ "logic.general.internal_error": "Internal Error Occured",
+ "logic.general.invalid_response": "Invalid server responce",
+ "logic.general.response_error": "Response Error: %s %s",
+
+ "app.exit_dialog.message":"Are you sure you want to exit?",
+
+ "app.common.heading.hint":"Header ",
+ "app.common.heading.one.hint":"Header 1",
+ "app.common.heading.two.hint":"Header 2",
+ "app.common.heading.three.hint":"Header 3",
+
+ "app.common.label.cancelled":"Cancelled",
+ "app.common.label.other": "Other",
+ "app.common.label.county": "County",
+ "app.common.label.read_more": "Read more",
+
+ "app.common.yes":"Yes",
+ "app.common.no":"No",
+
+ "com.illinois.features2.entry.disable_location_awareness":"Disable location awareness",
+ "com.illinois.features2.entry.dont_store_location":"Don't store your location",
+ "com.illinois.features2.entry.remove_preferences":"Remove your preferences",
+ "com.illinois.features2.entry.log_out":"Log you off system",
+ "com.illinois.features2.entry.disable_notifications":"Turn off notifications",
+ "com.illinois.features2.entry.remove_credit_card":"Remove your credit card info",
+ "com.illinois.features2.entry.dont_share_location":"Don’t share your data with other users",
+
+ "com.illinois.covid19.status.long.orange": "Orange, Test Required",
+ "com.illinois.covid19.status.long.green": "Green, Recent Antibodies",
+ "com.illinois.covid19.status.long.yellow": "Yellow, Recent Negative Test",
+ "com.illinois.covid19.status.long.red": "Red, Positive Test",
+ "com.illinois.covid19.status.long.no change": "Unchanged",
+ "com.illinois.covid19.status.type.orange": "Orange",
+ "com.illinois.covid19.status.type.green": "Green",
+ "com.illinois.covid19.status.type.yellow": "Yellow",
+ "com.illinois.covid19.status.type.red": "Red",
+ "com.illinois.covid19.status.type.no change": "Unchanged",
+ "com.illinois.covid19.status.description.orange": "Test Required",
+ "com.illinois.covid19.status.description.green": "Recent Antibodies",
+ "com.illinois.covid19.status.description.yellow": "Recent Negative Test",
+ "com.illinois.covid19.status.description.red": "Positive Test",
+ "com.illinois.covid19.status.description.no change": "",
+ "com.illinois.covid19.status.info.description.orange": "Orange: First time user, Past due for test, Self-reported symptoms, Received exposure notification or Quarantined",
+ "com.illinois.covid19.status.info.description.green": "Green: Recent antibodies",
+ "com.illinois.covid19.status.info.description.yellow": "Yellow: Recent negative test",
+ "com.illinois.covid19.status.info.description.red": "Red: Positive test"
+}
diff --git a/assets/strings.es.json b/assets/strings.es.json
new file mode 100644
index 00000000..5eefafec
--- /dev/null
+++ b/assets/strings.es.json
@@ -0,0 +1,796 @@
+{
+ "app.title":"Illinois más seguro",
+ "app.offline.message.title":"Parece que estás desconectado",
+
+ "dialog.yes.title":"Sí",
+ "dialog.yes.hint":"",
+ "dialog.no.title":"No",
+ "dialog.no.hint":"",
+ "dialog.ok.title":"OK",
+ "dialog.ok.hint":"",
+ "dialog.cancel.title":"Cancelar",
+ "dialog.cancel.hint":"",
+ "dialog.continue.title":"Seguir",
+ "dialog.continue.hint":"",
+ "dialog.close.title":"Cerrar",
+ "dialog.close.hint":"",
+
+ "tabbar.home.title":"Inicio",
+ "tabbar.home.hint":"Página de inicio",
+ "tabbar.explore.title":"Explorar",
+ "tabbar.explore.hint":"Explorar página",
+ "tabbar.more.title":"Más",
+ "tabbar.more.hint":"Más página",
+ "tabbar.browse.title":"Vistazo",
+ "tabbar.browse.hint":"Vistazo página",
+ "tabbar.wallet.title":"Cartera",
+ "tabbar.wallet.hint":"Página de Cartera",
+
+ "headerbar.home.title":"Inicio",
+ "headerbar.home.hint":"Página de inicio",
+ "headerbar.menu.title":"Menú",
+ "headerbar.menu.hint":"",
+ "headerbar.search.title":"Buscar",
+ "headerbar.search.hint":"Escriba un término de búsqueda",
+ "headerbar.settings.title":"Configuración",
+ "headerbar.settings.hint":"",
+ "headerbar.search.placehlder":"¿Qué buscas?",
+ "headerbar.back.title":"Atrás",
+ "headerbar.back.hint":"",
+ "headerbar.close.title":"Cerrar",
+ "headerbar.close.hint":"",
+ "headerbar.saved.title":"Guardado",
+ "headerbar.saved.hint":"",
+ "headerbar.teams.title":"Equipos",
+
+ "toggle_button.status.checked": "marcado",
+ "toggle_button.status.unchecked": "sin marcar",
+ "toggle_button.status.checkbox": "casilla de verificación",
+
+ "panel.menu.button.events.title":"Eventos",
+ "panel.menu.button.events.hint":"",
+ "panel.menu.button.dining.title":"Comida",
+ "panel.menu.button.dining.hint":"",
+ "panel.menu.button.athletics.title":"Atletismo",
+ "panel.menu.button.athletics.hint":"",
+ "panel.menu.button.settings.title":"Configuración",
+ "panel.menu.button.settings.hint":"",
+ "panel.menu.button.illini_cash.title":"Illini Cash",
+ "panel.menu.button.illini_cash.hint":"",
+ "panel.menu.button.create_event.title":"Crear un evento",
+ "panel.menu.button.create_event.hint":"",
+ "panel.menu.button.order_history.title":"Historial de pedidos",
+ "panel.menu.button.order_history.hint":"",
+ "panel.menu.button.sign_out.title":"Cerrar sesión",
+ "panel.menu.button.sign_out.hint":"",
+ "panel.menu.button.sign_in.title":"Iniciar sesión",
+ "panel.menu.button.sign_in.hint":"",
+ "panel.menu.button.create_account.title":"Crear cuenta",
+ "panel.menu.button.create_account.hint":"",
+ "panel.menu.label.sign_in_as":"Ingresado como",
+ "panel.menu.label.verified_as":"Verificado como ",
+ "panel.menu.label.connected_netid":"NetID conectado",
+ "panel.menu.label.confirm_sign_out":"¿Está seguro de que desea cerrar sesión?",
+
+ "panel.onboarding.button.continue.title":"Continuar",
+ "panel.onboarding.button.continue.hint":"",
+
+ "panel.onboarding.get_started.image.welcome.title":"Bienvenido a Illinois",
+ "panel.onboarding.get_started.image.welcome.hint":"",
+ "panel.onboarding.get_started.button.get_started.title":"Comenzar",
+ "panel.onboarding.get_started.button.get_started.hint":"",
+ "panel.onboarding.get_started.image.safer_in_illinois.title":"Safer in Illinois",
+ "panel.onboarding.get_started.image.powered.title":"Powered by Rokwire",
+
+ "panel.onboarding.location.label.title":"Turn on Location Services",
+ "panel.onboarding.location.label.hint":"Encabezado 1",
+ "panel.onboarding.location.label.description":"Se requiere para que las notificaciones de exposición funcionen en su teléfono",
+ "panel.onboarding.location.button.allow.title":"Compartir mi Ubicación",
+ "panel.onboarding.location.button.allow.hint":"",
+ "panel.onboarding.location.button.dont_allow.title":"No en este momento",
+ "panel.onboarding.location.button.dont_allow.hint":"Omitir ubicación para compartir",
+ "panel.onboarding.location.label.access_granted":"Ya ha otorgado acceso a esta aplicación",
+ "panel.onboarding.location.label.access_denied":"Ya ha denegado el acceso a esta aplicación",
+
+ "panel.onboarding.bluetooth.label.title":"Habilitar Bluetooth",
+ "panel.onboarding.bluetooth.label.title.hint":"Encabezado 1",
+ "panel.onboarding.bluetooth.label.description":"Usar Bluetooth para alertarlo sobre la posible exposición al COVID-19.",
+ "panel.onboarding.bluetooth.button.allow.title":"Habilitar Bluetooth",
+ "panel.onboarding.bluetooth.button.allow.hint":"",
+ "panel.onboarding.bluetooth.button.dont_allow.title":"No en este momento",
+ "panel.onboarding.bluetooth.button.dont_allow.hint":"Omitir habilitar Bluetooth",
+ "panel.onboarding.bluetooth.label.access_granted":"Ya ha otorgado acceso a esta aplicación",
+ "panel.onboarding.bluetooth.label.access_denied":"Ya ha denegado el acceso a esta aplicación",
+
+ "panel.onboarding.notifications.label.title":"Información cuando la necesitas",
+ "panel.onboarding.notifications.label.hint":"Encabezado 1",
+ "panel.onboarding.notifications.label.description":"Reciba notificaciones sobre la información de COVID-19",
+ "panel.onboarding.notifications.button.allow.title":"Recibir notificaciones",
+ "panel.onboarding.notifications.button.allow.hint":"",
+ "panel.onboarding.notifications.button.dont_allow.title":"No en este momento",
+ "panel.onboarding.notifications.button.dont_allow.hint":"Omitir recibir notificaciones",
+ "panel.onboarding.notifications.label.access_granted":"Su configuración ha sido cambiada.",
+
+ "panel.onboarding.roles.label.title":"Quién eres",
+ "panel.onboarding.roles.label.title.hint":"Encabezado 1",
+ "panel.onboarding.roles.label.description":"Seleccione todos los que correspondan",
+ "panel.onboarding.roles.button.student.title":"Estudiante universitario",
+ "panel.onboarding.roles.button.student.hint":"",
+ "panel.onboarding.roles.button.employee.title":"Empleado/Afiliado",
+ "panel.onboarding.roles.button.employee.hint":"",
+ "panel.onboarding.roles.button.resident.title":"Residente de Illinois",
+ "panel.onboarding.roles.button.resident.hint":"",
+ "panel.onboarding.roles.button.continue.enabled.title":"Confirmar",
+ "panel.onboarding.roles.button.continue.disabled.title":"Seleccione uno",
+ "panel.onboarding.roles.button.continue.hint":"",
+
+ "panel.onboarding.login.netid.label.title":"Conecte su NetID",
+ "panel.onboarding.login.phone.label.title":"Verifique su número de teléfono",
+ "panel.onboarding.login.netid.label.title.hint":"Encabezado 1",
+ "panel.onboarding.login.phone.label.title.hint":"Encabezado 1",
+ "panel.onboarding.login.netid.label.description":"Inicie sesión con su NetID",
+ "panel.onboarding.login.phone.label.description":"Esto guarda sus preferencias para que pueda tener la misma experiencia en más de un dispositivo",
+ "panel.onboarding.login.label.login_failed":"No se puede iniciar sesión. Vuelva a intentarlo más tarde",
+ "panel.onboarding.login.netid.button.continue.title":"Inicie sesión con NetID",
+ "panel.onboarding.login.phone.button.continue.title":"Verificar mi número de teléfono",
+ "panel.onboarding.login.netid.button.continue.hint":"",
+ "panel.onboarding.login.phone.button.continue.hint":"",
+ "panel.onboarding.login.netid.button.dont_continue.title":"No en este momento",
+ "panel.onboarding.login.phone.button.dont_continue.title":"No en este momento",
+ "panel.onboarding.login.netid.button.dont_continue.hint":"Omitir verificación",
+ "panel.onboarding.login.phone.button.dont_continue.hint":"Omitir verificación",
+
+ "panel.onboarding.verify_phone.title":"Verifique su número de teléfono",
+ "panel.onboarding.verify_phone.title.hint":"",
+ "panel.onboarding.verify_phone.description":"Para verificar su número de teléfono, elija su canal de contacto preferido y le enviaremos un código de autenticación único",
+ "panel.onboarding.verify_phone.description.hint":"",
+ "panel.onboarding.verify_phone.phone_number.label":"Número de teléfono",
+ "panel.onboarding.verify_phone.phone_number.hint":"",
+ "panel.onboarding.verify_phone.text_me.label":"Envíame un mensaje de texto",
+ "panel.onboarding.verify_phone.text_me.hint":"",
+ "panel.onboarding.verify_phone.call_me.label":"Llámame",
+ "panel.onboarding.verify_phone.call_me.hint":"",
+ "panel.onboarding.verify_phone.button.next.label":"Siguiente",
+ "panel.onboarding.verify_phone.button.next.hint":"Toque para continuar",
+ "panel.onboarding.verify_phone.validation.phone_number.text":"Por favor, escriba su número de teléfono",
+ "panel.onboarding.verify_phone.validation.channel_selection.text":"Por favor, seleccione el método de verificación",
+ "panel.onboarding.verify_phone.validation.server_error.text":"Por favor ingrese un número de teléfono válido",
+
+ "panel.onboarding.confirm_phone.title":"Confirme su código",
+ "panel.onboarding.confirm_phone.title.hint":"",
+ "panel.onboarding.confirm_phone.description.send":"Se ha enviado un código único a %s. Ingrese su código a continuación para continuar.",
+ "panel.onboarding.confirm_phone.description.send.hint":"",
+ "panel.onboarding.confirm_phone.code.label":"Código de una sola vez",
+ "panel.onboarding.confirm_phone.code.hint":"",
+ "panel.onboarding.confirm_phone.not_received.text.label":"¿No recibió un mensaje de texto?",
+ "panel.onboarding.confirm_phone.not_received.text.hint":"",
+ "panel.onboarding.confirm_phone.not_received.call.label":"¿No recibió una llamada?",
+ "panel.onboarding.confirm_phone.not_received.call.hint":"",
+ "panel.onboarding.confirm_phone.button.confirm.label":"Confirmar número de teléfono",
+ "panel.onboarding.confirm_phone.button.confirm.hint":"",
+ "panel.onboarding.confirm_phone.validation.phone_number.text":"Por favor, complete su código",
+ "panel.onboarding.confirm_phone.validation.server_error.text":"Error al verificar el código",
+
+ "panel.onboarding.upgrade.required.label.title":"Actualización requerida",
+ "panel.onboarding.upgrade.required.label.description":"%s versión de la aplicación %s requiere una actualización a la versión %s o posterior",
+ "panel.onboarding.upgrade.available.label.title":"Actualización disponible",
+ "panel.onboarding.upgrade.available.label.description":"%s la versión de la aplicación %s tiene la versión más nueva %s disponible",
+ "panel.onboarding.upgrade.button.upgrade.title":"Actualizar",
+ "panel.onboarding.upgrade.button.upgrade.hint":"",
+ "panel.onboarding.upgrade.button.not_now.title":"No en este momento",
+ "panel.onboarding.upgrade.button.not_now.hint":"",
+ "panel.onboarding.upgrade.button.dont_show.title":"No mostrar de nuevo",
+ "panel.onboarding.upgrade.button.dont_show.hint":"",
+
+ "panel.onboarding.base.not_now.hint":"",
+ "panel.onboarding.base.not_now.title":"No en este momento",
+
+ "panel.settings.sports.heading":"Configuraciónes",
+ "panel.settings.categories.header.title":"SUS CATEGORíAS",
+ "panel.settings.categories.heading":"Sus categorías",
+ "panel.settings.categories.description":"Toque para seguir las categorías que más le interesen",
+
+ "panel.settings.feedback.label.title": "Proporcionar comentarios",
+
+ "panel.settings.privacy_statement.label.title": "Declaración de privacidad",
+
+ "panel.settings.label.offline.feedback": "Proporcionar un comentario no está disponible sin conexión.",
+
+ "panel.settings.home.settings.header":"Configuración",
+ "panel.settings.home.connect.not_logged_in.title":"Conéctate a Illinois",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_1": "Eres un ",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_2": "estudiante",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_3": " o ",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_4": "miembro de facultad",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_5": "? Inicie sesión con su NetID para ver la información de Illinois específica para usted, como su plan Illini Cash y de comidas.",
+ "panel.settings.home.connect.not_logged_in.netid.title": "Conecte su NetID",
+ "panel.settings.home.connect.not_logged_in.phone.description.part_1": "No tengo una NetID",
+ "panel.settings.home.connect.not_logged_in.phone.description.part_2": "? Verifique su número de teléfono para guardar sus preferencias y tenga la misma experiencia en más de un dispositivo.",
+ "panel.settings.home.connect.not_logged_in.phone.title": "Verifica tu numero de telefono",
+ "panel.settings.home.customizations.title":"Personalizaciones",
+ "panel.settings.home.net_id.title":"Illinois NetID",
+ "panel.settings.home.phone_ver.title":"Verificación del teléfono",
+ "panel.settings.home.notifications.title": "Notificaciones",
+ "panel.settings.home.user_info.title.sufix":"Bienvenido a Illinois",
+ "panel.settings.home.account.title": "Su cuenta",
+ "panel.settings.home.account.personal_info.title":"Información personal",
+ "panel.settings.home.account.options.payment":"Pago",
+ "panel.settings.home.customizations.role.title":"Quién eres",
+ "panel.settings.home.customizations.manage_interests.title":"Administre sus intereses",
+ "panel.settings.home.customizations.food_filters.title":"Filtros de alimentos",
+ "panel.settings.home.customizations.display_times_in_central_time.title":"Mostrar todas las horas en hora central",
+ "panel.settings.home.net_id.message":"Conectado como",
+ "panel.settings.home.net_id.button.disconnect":"Desconecte su NetID",
+ "panel.settings.home.net_id.button.connect":"Conecte su NetID",
+ "panel.settings.home.phone_ver.message":"Verificado como",
+ "panel.settings.home.phone_ver.button.connect":"Verifique su número de teléfono",
+ "panel.settings.home.phone_ver.button.disconnect":"Desconecta tu teléfono",
+ "panel.settings.home.logout.message":"¿Está seguro de que desea cerrar sesión?",
+ "panel.settings.home.logout.button.yes":"Sí",
+ "panel.settings.home.logout.no":"No",
+ "panel.settings.home.security.reset_password":"Restablecer la contraseña",
+ "panel.settings.home.security.face_id":"Usar Face ID",
+ "panel.settings.home.notifications.reminders":"Recordatorios de eventos",
+ "panel.settings.home.notifications.athletics_updates":"Actualizaciones de atletismo",
+ "panel.settings.home.notifications.dining":"Especiales de comidas",
+ "panel.settings.home.notifications.covid19":"Notificaciones COVID-19",
+ "panel.settings.home.privacy.title":"Privacidad",
+ "panel.settings.home.privacy.edit_my_privacy.title":"Editar mi privacidad",
+ "panel.settings.home.privacy.privacy_statement.title":"Declaración de privacidad",
+ "panel.settings.home.button.debug.title":"Depurar",
+ "panel.settings.home.button.debug.hint":"",
+ "panel.settings.home.button.test.title":"Prueba",
+ "panel.settings.home.button.test.hint":"",
+ "panel.settings.home.feedback.title": "¡Necesitamos tus ideas!",
+ "panel.settings.home.feedback.description": "¿Estás disfrutando la aplicación? ¿Echando de menos algo? Toque en la parte inferior para enviar su idea",
+ "panel.settings.home.button.feedback.title": "Enviar comentarios",
+ "panel.settings.home.button.feedback.hint": "",
+ "panel.settings.home.covid19.exposure_notifications": "Notificaciones de exposición",
+ "panel.settings.home.covid19.provider_test_result": "Resultados de la prueba del proveedor de salud",
+ "panel.settings.home.covid19.title": "COVID-19",
+ "panel.settings.home.covid19.qr_code.button.title": "Código QR",
+ "panel.settings.home.covid19.transfer.button.title": "Transferir",
+ "panel.settings.home.covid19.qr_code.description.title": "Código QR secreto de COVID-19",
+ "panel.settings.home.covid19.transfer.description.title": "Transfiera el secreto COVID-19 desde su otro teléfono",
+ "panel.settings.home.covid19.alert.qr_code.scan.failed.msg": "No se pudo leer el código QR.",
+ "panel.settings.home.covid19.alert.qr_code.transfer.succeeded.msg": "El secreto COVID-19 se transfirió con éxito.",
+ "panel.settings.home.covid19.alert.qr_code.transfer.failed.msg": "Error al transferir el secreto COVID-19.",
+ "panel.settings.home.covid19.alert.reset.prompt": "Hacer esto le proporcionará un nuevo código QR secreto de COVID-19, pero su historial de eventos de COVID-19 anterior se perderá, ¿continuar?",
+ "panel.settings.home.covid19.alert.reset.failed": "No se pudo restablecer el código QR secreto",
+ "panel.settings.home.covid19.text.user.fail": "No se puede recuperar la configuración de COVID-19 del usuario.",
+ "panel.settings.home.covid19.text.keys.checking": "Comprobando las teclas COVID-19 ...",
+ "panel.settings.home.covid19.text.keys.missing.public": "Falta la clave pública COVID-19",
+ "panel.settings.home.covid19.text.keys.missing.private": "Falta la clave privada COVID-19",
+ "panel.settings.home.covid19.text.keys.mismatch": "Teclas COVID-19 no emparejadas",
+ "panel.settings.home.covid19.text.keys.paired": "Claves COVID-19 válidas y emparejadas",
+ "panel.settings.home.covid19.text.keys.reset": "Reinicie el par de llaves COVID-19.",
+ "panel.settings.home.covid19.text.keys.transfer_or_reset": "Transfiera la clave privada COVID-19 desde su otro teléfono o restablezca el par de claves COVID-19.",
+ "panel.settings.home.covid19.text.keys.qr_code": "Muestre su código QR secreto COVID-19.",
+ "panel.settings.home.covid19.button.retry.title": "Reintentar",
+ "panel.settings.home.covid19.button.reset.title": "Reiniciar",
+ "panel.settings.home.covid19.button.load.title": "Carga",
+ "panel.settings.home.covid19.button.scan.title": "Escanear",
+ "panel.settings.home.covid19.button.qr_code.title": "Código QR",
+
+ "panel.settings.label.offline.phone_ver":"Verifique que su número de teléfono no esté disponible sin conexión.",
+
+ "panel.profile_info.header.title":"INFORMACIÓN PERSONAL",
+ "panel.profile_info.net_id.title":"NetID",
+ "panel.profile_info.full_name.title":"Nombre completo",
+ "panel.profile_info.first_name.title":"Nombre",
+ "panel.profile_info.middle_name.title":"Segundo nombre",
+ "panel.profile_info.last_name.title":"Apellido",
+ "panel.profile_info.email_address.title":"Correo Electrónico",
+ "panel.profile_info.phone_number.title":"Número de teléfono",
+ "panel.profile_info.button.sign_out.title":"Cerrar sesión",
+ "panel.profile_info.button.sign_out.hint":"",
+ "panel.profile_info.label.remove_my_info.title":"Eliminar mi información",
+ "panel.profile_info.button.remove_my_information.title":"Eliminar mi información",
+ "panel.profile_info.button.remove_my_information.hint":"",
+ "panel.profile_info.dialog.remove_my_information.title":"Al responder SÍ, toda su información personal y sus preferencias se eliminarán de nuestros sistemas. Esta acción no se puede recuperar. Después de eliminar la información, volveremos a la primera pantalla cuando instaló la aplicación para que pueda comenzar de nuevo o eliminar la aplicación ",
+ "panel.profile_info.dialog.remove_my_information.subtitle":"¿Está seguro?",
+ "panel.profile_info.dialog.remove_my_information.yes.title":"Sí",
+ "panel.profile_info.dialog.remove_my_information.no.title": "No",
+
+ "panel.covid19_passport.header.title": "COVID-19",
+ "panel.covid19_passport.button.close.title": "Cerrar",
+ "panel.covid19_passport.button.info_center.title": "Su centro de información COVID-19",
+ "panel.covid19_passport.label.status.empty":"No hay estado disponible para este condado",
+ "panel.covid19_passport.label.counties.empty":"No hay condados disponibles",
+ "panel.covid19_passport.label.county.empty.hint":"Seleccione un condado ...",
+ "panel.covid19_passport.label.access.heading": "Acceso al edificio",
+ "panel.covid19_passport.label.access.granted": "CONCEDIDO",
+ "panel.covid19_passport.label.access.denied": "NEGADO",
+ "panel.covid19_passport.message.missing_id_info": "No se encontró información de identificación de Illini. Es posible que tenga una i-card vencida. Comuníquese con el Centro de identificación.",
+
+ "panel.covid19_guidelines.header.title": "Pautas del condado",
+ "panel.covid19_guidelines.description.title": "Ayude a detener la propagación de COVID-19 siguiendo estas pautas actuales.",
+ "panel.covid19_guidelines.status.title": "Estos se basan en su estado% s en el siguiente condado:",
+ "panel.covid19_guidelines.label.county.empty":"Seleccione un condado ...",
+ "panel.covid19_guidelines.no.status":"No hay pautas específicas para su estado en este condado.",
+
+ "panel.covid19home.header.title": "COVID-19",
+ "panel.covid19home.top_heading.title": "No se encontró información de identificación de Illini. Es posible que tenga una i-card vencida. Comuníquese con el Centro de identificación.",
+ "panel.covid19home.label.next_step.title": "PRÓXIMO PASO",
+ "panel.covid19home.label.schedule_after.title": "Programar después de% s",
+ "panel.covid19home.button.find_test_locations.title": "Encuentra ubicaciones de prueba",
+ "panel.covid19home.button.find_test_locations.hint": "",
+ "panel.covid19home.button.country_guidelines.title": "Pautas\ndel condado",
+ "panel.covid19home.button.country_guidelines.hint": "",
+ "panel.covid19home.button.care_team.title": "Your\nCare Team",
+ "panel.covid19home.button.care_team.hint": "",
+ "panel.covid19home.button.campus_updates.title": "Actualizaciones del campus",
+ "panel.covid19home.button.campus_updates.hint": "",
+ "panel.covid19home.button.report_test.title": "Reportar un resultado de prueba",
+ "panel.covid19home.button.report_test.hint": "",
+ "panel.covid19home.button.test_history.title": "Su historial de pruebas",
+ "panel.covid19home.button.test_history.hint": "",
+ "panel.covid19home.label.health.title":"Tu salud",
+ "panel.covid19home.label.resources.title":"Recursos",
+ "panel.covid19home.label.check_in.title":"Registro de síntomas",
+ "panel.covid19home.label.check_in.description":"Autoinforme cualquier síntoma para ver si debe hacerse la prueba o quedarse en casa",
+ "panel.covid19home.label.result.title":"Agregar resultado de prueba",
+ "panel.covid19home.label.result.description":"Para mantener su estado actualizado",
+ "panel.covid19home.label.status.title":"Current Status:",
+ "panel.covid19home.label.status.na":"No disponible",
+ "panel.covid19home.button.show_status_card.title":"Mostrar tarjeta de estado",
+ "panel.covid19home.label.campus_updates.title":"Actualizaciones del campus",
+ "panel.covid19home.button.about.title":"Acerca de",
+ "panel.covid19home.label.most_recent_event.title": "EVENTO MAS RECIENTE",
+ "panel.covid19home.label.provider.self_reported": "Autoinforme",
+ "panel.covid19home.label.action_required.title": "Acción requerida",
+ "panel.covid19home.label.contact_trace.title": "Seguimiento de contacto",
+ "panel.covid19home.label.reported_symptoms.title": "Síntomas autoinformados",
+
+ "panel.covid19_test_locations.header.title": "Lugares de prueba",
+ "panel.covid19_test_locations.label.contact.title": "Contacto",
+ "panel.covid19_test_locations.distance.text": "mi distancia y obtener direcciones",
+ "panel.covid19_test_locations.distance.unknown": "distancia desconocida",
+ "panel.covid19_test_locations.work_time.unknown": "Tiempo de trabajo desconocido",
+ "panel.covid19_test_locations.work_time.open_until":"Abierto hasta",
+ "panel.covid19_test_locations.work_time.closed_until": "Cerrado hasta",
+ "panel.covid19_test_locations.all_providers.text": "Todos los proveedores",
+ "panel.covid19_test_locations.call.hint": "Llamada",
+
+ "panel.covid19.header.title": "COVID-19",
+ "panel.covid19.latest_update.title": "Última actualización",
+ "panel.covid19.latest_update.read_more.title": "Leer mas",
+ "panel.covid19.health_status.title": "Tu Saludo",
+ "panel.covid19.news.title": "Noticias COVID-19",
+ "panel.covid19.faq.title":"Preguntas más frecuentes",
+ "panel.covid19.faq.description":"Respuestas a sus preguntas más comunes:",
+ "panel.covid19.faq.update.text":"actualizado %s",
+ "panel.covid19.faq.question.hint":"Toca dos veces para mostrar preguntas",
+ "panel.covid19.faq.title.label":"Preguntas frecuentes",
+ "panel.covid19.resources.poor_accessibility.hint": "Este enlace lo lleva a un sitio web fuera de la aplicación Safer Illinois",
+ "panel.covid19.label.current_status.label": "Estado actual:",
+ "panel.covid19.button.show_status_card.title": "Mostrar tarjeta de estado",
+ "panel.covid19.button.show_status_card.hint": "",
+ "panel.covid19.button.country_guidelines.title": "Pautas del condado",
+ "panel.covid19.button.country_guidelines.hint": "",
+ "panel.covid19.button.campus_updates.title": "Actualizaciones del campus",
+ "panel.covid19.button.campus_updates.hint": "",
+ "panel.covid19.button.health_history.title": "Ver historial de salud",
+ "panel.covid19.button.health_history.hint": "",
+ "panel.covid19.finish_setup.title": "Finalizar configuración",
+ "panel.covid19.finish_setup_description.title": "Para usar su estado oficial",
+ "panel.covid19.button.verify_identity.title": "Verifica tu identidad",
+ "panel.covid19.unverified.title": "Inconfirmado",
+ "panel.covid19.button.covid_wellness_center.title": "Centro de respuestas de bienestar COVID-19",
+ "panel.covid19.button.covid_wellness_center.hint": "",
+
+ "panel.covid19_wellness_center.header.title": "Centro de bienestar COVID-19",
+ "panel.covid19_wellness_center.label.description": "Si tiene problemas con la aplicación o obtiene un resultado de prueba, comuníquese con el Centro de respuestas de bienestar de COVID para obtener ayuda.",
+ "panel.covid19_wellness_center.label.email": "Envíe un correo electrónico al Centro de respuestas de bienestar de Covid al",
+ "panel.covid19_wellness_center.label.phone": "Llame al Centro de respuestas al",
+
+ "panel.covid19_news.header.title": "COVID-19",
+ "panel.covid19_news.news.posted.label": "Publicado en %s",
+
+ "panel.covid19_campus_updates.header.title": "Actualizaciones del campus",
+ "panel.covid19_campus_updates.sub_title.title": "Universidad de Illinois COVID-19 actualizaciones",
+
+ "panel.covid19.qr_code.title": "Código QR de COVID-19",
+ "panel.covid19.qr_code.description.heading.1": "Si usa más de un dispositivo con la aplicación Safer Illinois, use este código QR para transferir el secreto necesario para decodificar su información de salud COVID-19.",
+ "panel.covid19.qr_code.description.heading.2": "Guarde este código QR para que si pierde o reemplaza su teléfono, puede recuperar su información de salud COVID-19 en su nuevo teléfono.",
+ "panel.covid19.qr_code.button.save.title": "Salvar",
+ "panel.covid19.qr_code.alert.no_qr_code.msg": "No hay código QR",
+ "panel.covid19.qr_code.alert.save.success.msg": "Código qr guardado correctamente en la galería",
+ "panel.covid19.qr_code.alert.save.fail.msg": "Error al guardar el código qr en la galería",
+ "panel.covid19.qr_code.code.hint": "QR code image",
+ "panel.covid19.qr_code.description.on_boarding.heading.1": "Guarde este código QR para que, si pierde o reemplaza su teléfono, pueda recuperar su información de salud COVID-19 en su nuevo teléfono.",
+ "panel.covid19.qr_code.description.on_boarding.heading.2": "Siempre puede omitir este paso por ahora y hacerlo más tarde en Configuración",
+ "panel.covid19.qr_code.button.skip.title.": "Omitir",
+ "panel.covid19.qr_code.button.skip.hint.": "Omitir guardar",
+
+ "panel.covid19.transfer.title": "Transferir clave de cifrado",
+ "panel.covid19.transfer.label.qr_image_label": "Safer Illinois COVID-19 Código",
+ "panel.covid19.transfer.label.save_error": "No se pudo guardar el código QR.",
+ "panel.covid19.transfer.button.continue.hint": "",
+ "panel.covid19.transfer.primary.heading.title": "Su clave de cifrado COVID-19",
+ "panel.covid19.transfer.primary.button.save.title": "Guarde su clave de cifrado",
+ "panel.covid19.transfer.primary.button.save.hint": "",
+ "panel.covid19.transfer.secondary.heading.title": "Falta la clave de cifrado COVID-19",
+ "panel.covid19.transfer.secondary.button.scan.heading": "Si está agregando un segundo dispositivo:",
+ "panel.covid19.transfer.secondary.button.scan.description": "Si aún tiene acceso a su dispositivo principal, puede escanear directamente el código QR de la clave de cifrado COVID-19 desde ese dispositivo.",
+ "panel.covid19.transfer.secondary.button.scan.title": "Escanee su código QR",
+ "panel.covid19.transfer.secondary.button.retrieve.heading": "Si está utilizando un dispositivo de reemplazo:",
+ "panel.covid19.transfer.secondary.button.retrieve.description": "Si ya no tiene acceso a su dispositivo principal, pero guardó su código QR en un servicio de fotos en la nube, puede transferir su clave de cifrado COVID-19 recuperándola de sus fotos.",
+ "panel.covid19.transfer.secondary.button.retrieve.title": "Recupera tu código QR",
+ "panel.covid19.transfer.alert.no_qr_code.msg": "No hay código QR",
+ "panel.covid19.transfer.alert.save.success.msg": "Código qr guardado correctamente en",
+ "panel.covid19.transfer.alert.save.success.pictures": "Imágenes",
+ "panel.covid19.transfer.alert.save.success.gallery": "Galería",
+ "panel.covid19.transfer.alert.save.fail.msg": "No se pudo guardar el código qr en",
+ "panel.covid19.transfer.alert.qr_code.scan.failed.msg": "No se pudo leer el código QR.",
+ "panel.covid19.transfer.alert.qr_code.invalid.msg": "Código QR no es válido.",
+ "panel.covid19.transfer.alert.qr_code.not_match.msg": "La clave secreta COVID-19 no coincide con la clave RSA pública existente.",
+ "panel.covid19.transfer.alert.qr_code.transfer.succeeded.msg": "El secreto de COVID-19 se transfirió con éxito.",
+ "panel.covid19.transfer.alert.qr_code.transfer.failed.msg": "No se pudo transferir el secreto de COVID-19.",
+
+ "panel.debug.header.title": "Depurar",
+
+ "panel.debug_messaging.header.title": "Mensajes",
+
+ "panel.web.offline.message": "Debe estar en línea para realizar esta operación. Por favor revise su conexion a internet.",
+
+ "panel.health.onboarding.covid19.intro.label.title": "Únete a la lucha contra COVID-19",
+ "panel.health.onboarding.covid19.intro.label.description": "Rastree y administre su salud para ayudar a mantener segura a nuestra comunidad de Illinois",
+ "panel.health.onboarding.covid19.intro.button.continue.title": "Seguir",
+ "panel.health.onboarding.covid19.intro.button.continue.hint": "",
+
+ "panel.health.onboarding.covid19.how_it_works.heading.title": "Cómo funciona",
+ "panel.health.onboarding.covid19.how_it_works.line1.title": "Las pruebas y la limitación de la exposición son clave para frenar la propagación de COVID-19.",
+ "panel.health.onboarding.covid19.how_it_works.line2.title": "Proporcione cualquier síntoma de COVID-19 que esté experimentando, reciba o ingrese automáticamente los resultados de las pruebas de su proveedor de atención médica, y permita que su teléfono le envíe notificaciones de exposición a usted y a las personas con las que ha estado en contacto durante los últimos 14 días.",
+ "panel.health.onboarding.covid19.how_it_works.line3.title": "Autodiagnostica los síntomas de COVID-19 y, al hacerlo, actualiza tu estado.",
+ "panel.health.onboarding.covid19.how_it_works.line4.title": "Reciba automáticamente los resultados de las pruebas de su proveedor de atención médica.",
+ "panel.health.onboarding.covid19.how_it_works.line5.title": "Permita que su teléfono envíe notificaciones de exposición cuando haya estado cerca de personas que dieron positivo en la prueba.",
+ "panel.health.onboarding.covid19.how_it_works.button.next.title": "Próximo",
+ "panel.health.onboarding.covid19.how_it_works.button.next.hint": "",
+
+ "panel.health.onboarding.covid19.consent.label.title": "Consentimientos especiales para las características de COVID-19",
+ "panel.health.onboarding.covid19.consent.label.description": "Notificaciones de exposición",
+ "panel.health.onboarding.covid19.consent.label.content1": "Si acepta las notificaciones de exposición, permite que su teléfono envíe una señal anónima de Bluetooth a los usuarios cercanos de la aplicación Safer Illinois que también usan esta función. Su teléfono también recibirá y grabará una señal de sus teléfonos. Si uno de esos usuarios da positivo por COVID-19 en los próximos 14 días, la aplicación lo alertará sobre su posible exposición y le aconsejará sobre los próximos pasos. Su identidad y estado de salud permanecerán anónimos, al igual que la identidad y el estado de salud de todos los demás usuarios.",
+ "panel.health.onboarding.covid19.consent.check_box.label.participate": "Doy mi consentimiento para participar en el Sistema de notificación de exposición (requiere que Bluetooth esté activado).",
+ "panel.health.onboarding.covid19.consent.check_box.label.allow":"Doy mi consentimiento para permitir que mi proveedor de atención médica proporcione los resultados de mi prueba.",
+ "panel.health.onboarding.covid19.consent.label.content2": "Resultados automáticos de prueba",
+ "panel.health.onboarding.covid19.consent.label.content3": "Doy mi consentimiento para conectar los resultados de las pruebas de mi proveedor de atención médica con la aplicación Safer Illinois.",
+ "panel.health.onboarding.covid19.consent.label.content4": "Su participación es voluntaria y puede detenerse en cualquier momento",
+ "panel.health.onboarding.covid19.consent.button.consent.title": "Próximo",
+ "panel.health.onboarding.covid19.consent.button.consent.hint": "",
+ "panel.health.onboarding.covid19.consent.button.scroll_to_continue.title": "Desplácese para continuar",
+ "panel.health.onboarding.covid19.consent.label.error.login":"No se puede iniciar sesión en Health",
+
+ "panel.health.onboarding.covid19.resident_info.label.title": "Verifique su identidad con una identificación emitida por el gobierno",
+ "panel.health.onboarding.covid19.resident_info.label.description": "Después de verificar, recibirá un estado de salud codificado por colores según las pautas, los síntomas y cualquier prueba relacionada con COVID-19 de su condado.",
+ "panel.health.onboarding.covid19.resident_info.button.passport.title": "Pasaporte",
+ "panel.health.onboarding.covid19.resident_info.button.passport.hint": "",
+ "panel.health.onboarding.covid19.resident_info.button.drivers_license.title": "Licencia de conducir",
+ "panel.health.onboarding.covid19.resident_info.button.drivers_license.hint": "",
+ "panel.health.onboarding.covid19.resident_info.button.verify_later.title": "Verificar luego",
+
+ "panel.health.onboarding.covid19.review_scan.label.title": "Revisa tu escaneo",
+ "panel.health.onboarding.covid19.review_scan.label.name.title": "Revisa tu nombre",
+ "panel.health.onboarding.covid19.review_scan.label.birth_year.title": "Año de nacimiento",
+ "panel.health.onboarding.covid19.review_scan.message.failed": "Error al aplicar los datos escaneados",
+ "panel.health.onboarding.covid19.review_scan.button.rescan.title": "Vuelva a escanear",
+ "panel.health.onboarding.covid19.review_scan.button.rescan.hint": "",
+ "panel.health.onboarding.covid19.review_scan.button.use_scan.title": "Use este escaneo",
+ "panel.health.onboarding.covid19.review_scan.button.use_scan.hint": "",
+
+ "panel.health.onboarding.covid19.county.label.title": "¿En qué condados vive y trabaja?",
+ "panel.health.onboarding.covid19.county.label.description": "Seleccione todas las que correspondan",
+ "panel.health.onboarding.covid19.county.button.add_county.label": "Agregar otro condado",
+ "panel.health.onboarding.covid19.county.button.add_county.hint": "",
+ "panel.health.onboarding.covid19.county.button.next.title": "Próximo",
+ "panel.health.onboarding.covid19.county.button.next.hint": "",
+ "panel.health.onboarding.covid19.county.select.label": "SELECCIONA UN CONDADO",
+ "panel.health.onboarding.covid19.county.dropdown.select.default.label": "Seleccione un condado ...",
+ "panel.health.onboarding.covid19.county.alert.unique.message": "¡Por favor, seleccione un condado diferente en cada campo!",
+
+ "panel.health.onboarding.covid19.providers.label.title": "¿Quiénes son sus proveedores de atención médica actuales?",
+ "panel.health.onboarding.covid19.providers.label.description": "Seleccione todas las que correspondan",
+ "panel.health.onboarding.covid19.providers.button.next.title": "Próximo",
+ "panel.health.onboarding.covid19.providers.button.next.hint": "",
+
+ "panel.health.onboarding.covid19.final.label.title": "¡Ya está todo listo!",
+ "panel.health.onboarding.covid19.final.label.description": "Has sido verificado y se ha agregado una tarjeta de estado a tu perfil.",
+ "panel.health.onboarding.covid19.final.label.bottom.description":"Ahora puede usar esta aplicación como su compañero en la lucha contra COVID-19.",
+ "panel.health.onboarding.covid19.final.label.unverified.description": "Ahora puede usar esta aplicación como su compañero en la lucha contra COVID-19.",
+ "panel.health.onboarding.covid19.final.label.unverified.bottom.description":"Para acceder a su estado COVID-19, deberá cargar una identificación gubernamental. Puede agregar esto en cualquier momento en la configuración de COVID-19.",
+ "panel.health.onboarding.covid19.final.button.continue.title": "Empezar",
+ "panel.health.onboarding.covid19.final.button.continue.hint": "",
+
+ "panel.health.onboarding.covid19.login.netid.label.title":"Conecte su NetID",
+ "panel.health.onboarding.covid19.login.phone.label.title":"Verifica tu numero de telefono",
+ "panel.health.onboarding.covid19.login.netid.label.title.hint":"Encabezado 1",
+ "panel.health.onboarding.covid19.login.phone.label.title.hint":"Encabezado 1",
+ "panel.health.onboarding.covid19.login.netid.label.description":"Inicie sesión con su NetID para usar las características académicas y específicas del dormitorio.",
+ "panel.health.onboarding.covid19.phone.label.description":"Esto guarda sus preferencias para que pueda tener la misma experiencia en más de un dispositivo.",
+ "panel.health.onboarding.covid19.login.label.login_failed":"Incapaz de iniciar sesión. Por favor, inténtelo de nuevo más tarde",
+ "panel.health.onboarding.covid19.login.netid.button.continue.title":"Inicie sesión con NetID",
+ "panel.health.onboarding.covid19.login.phone.button.continue.title":"Verificar mi número de teléfono",
+ "panel.health.onboarding.covid19.login.netid.button.continue.hint":"",
+ "panel.health.onboarding.covid19.login.phone.button.continue.hint":"",
+ "panel.health.onboarding.covid19.login.netid.button.dont_continue.title":"No ahora",
+ "panel.health.onboarding.covid19.login.phone.button.dont_continue.title":"No ahora",
+ "panel.health.onboarding.covid19.login.netid.button.dont_continue.hint":"Omitir verificación",
+ "panel.health.onboarding.covid19.login.phone.button.dont_continue.hint":"Omitir verificación",
+ "panel.health.onboarding.covid19.login.label.error.login":"No se puede iniciar sesión en Health",
+
+ "panel.health.covid19.about.heading.title":"Acerca de",
+
+ "panel.health.covid19.add_test.heading.title":"Agregar resultado de prueba",
+ "panel.health.covid19.add_test.label.where_question":"¿Dónde se tomó la prueba?",
+ "panel.health.covid19.add_test.label.information": "¿Por qué se necesita esta información?",
+ "panel.health.covid19.add_test.label.provider.title":"Proveedor de atención sanitaria",
+ "panel.health.covid19.add_test.label.provider.empty_hint":"Selecciona un proveedor",
+ "panel.health.covid19.add_test.button.retreive.title":"Recuperar resultados",
+ "panel.health.covid19.add_test.button.enter_manually.title":"Ingresar manualmente",
+ "panel.health.covid19.add_test.label.info.retrieved.text1": "Resultados ",
+ "panel.health.covid19.add_test.label.info.retrieved.text2": "recuperado ",
+ "panel.health.covid19.add_test.label.info.retrieved.text3": "De su proveedor de atención médica se verifican al instante. Cualquier cambio en su estado de salud se reflejará instantáneamente.",
+ "panel.health.covid19.add_test.label.info.manually.text1": "Resultados ",
+ "panel.health.covid19.add_test.label.info.manually.text2": "ingresado manualmente ",
+ "panel.health.covid19.add_test.label.info.manually.text3": "será revisado y verificado por un proveedor de atención médica pública. Una vez verificado, pueden ocurrir cambios de estado.",
+ "panel.health.covid19.add_test.label.manual_tests_disabled":"Test results from this health care provider will automatically appear if you have consented to Health Provider Test Results in settings and you are connected with your NetID.",
+
+ "panel.health.covid19.care_team.heading.title":"Su equipo de atención",
+ "panel.health.covid19.care_team.label.question":"¿Necesitas algo?",
+ "panel.health.covid19.care_team.label.description":"Comuníquese con alguien de su equipo de atención de COVID-19; estamos aquí para ayudarlo.",
+ "panel.health.covid19.care_team.label.status":"Estado actual:",
+ "panel.health.covid19.care_team.label.emergency.text1": "En caso de emergencia, ",
+ "panel.health.covid19.care_team.label.emergency.text2": "siempre llame al 911.",
+ "panel.health.covid19.care_team.team.title.mc_kinley": "Llame a McKinley Health",
+ "panel.health.covid19.care_team.team.contact.mc_kinley": "1-217-333-2700",
+ "panel.health.covid19.care_team.team.semantic_contact.mc_kinley": "12173332700",
+ "panel.health.covid19.care_team.team.description.mc_kinley": "Comuníquese con alguien en la línea de \"Marque una enfermera\" para hablar sobre sus síntomas y opciones de atención clínica.",
+ "panel.health.covid19.care_team.team.title.osf": "OSF Healthcare",
+ "panel.health.covid19.care_team.team.contact.osf": "1-833-673-5669",
+ "panel.health.covid19.care_team.team.semantic_contact.osf": "18336735669",
+ "panel.health.covid19.care_team.team.description.osf": "We’ve partnered with OSF OnCall Connect program and the Illinois Department of Healthcare and Family Services to support you getting through COVID-19. Call the Nurse Hotline at 1-833-OSF-KNOW (833-673-5669) to learn more about the program, which includes delivery of a care kit and digital visits to monitor you over a 16-day period.",
+ "panel.health.covid19.care_team.label.more_info.title": "Más información sobre el Programa de trabajadores de salud pandémicos",
+ "panel.health.covid19.care_team.label.more_info.description": "Un trabajador de salud pandémica (PHW) es un socio capacitado de OSF HealthCare Mission que está conectado con usted para brindarle apoyo y actuar como una conexión directa entre usted y los proveedores de atención médica a medida que se recupera de COVID-19, lo que disminuye el riesgo de una mayor exposición. Si bien los PHW no son enfermeras registradas, las enfermeras clínicas y los médicos estarán disponibles si su afección empeora o si tiene preguntas clínicas específicas. ",
+ "panel.health.covid19.care_team.label.more_info.hint": "Tocar dos veces para mostrar más información",
+ "panel.health.covid19.care_team.label.call.hint": "Llamada ",
+ "panel.health.covid19.care_team.label.more_info.link": "Learn more",
+
+ "panel.health.covid19.debug.keys.heading.title.":"Claves COVID-19",
+ "panel.health.covid19.debug.keys.label.public_key":"Clave Publica RSA:",
+ "panel.health.covid19.debug.keys.label.private_key":"Clave Privada RSA:",
+ "panel.health.covid19.debug.keys.label.aes_key":"Clave AES:",
+ "panel.health.covid19.debug.keys.label.blob":"Gota:",
+ "panel.health.covid19.debug.keys.label.encripted_aes":"Clave AES cifrada:",
+ "panel.health.covid19.debug.keys.label.encripted_blob":"Blob cifrado:",
+ "panel.health.covid19.debug.keys.label.decripted_aes":"Clave AES descifrada:",
+ "panel.health.covid19.debug.keys.label.decripted_blob":"Blob descifrado:",
+ "panel.health.covid19.debug.keys.button.refres.title":"Actualizar claves RSA",
+ "panel.health.covid19.debug.keys.button.generate_aes.title":"Generar clave AES",
+ "panel.health.covid19.debug.keys.button.encript.title":"Encriptar",
+ "panel.health.covid19.debug.keys.button.decript.title":"Descifrar",
+ "panel.health.covid19.debug.keys.label.error.refres.title":"Actualzación fallida",
+
+ "panel.health.covid19.debug.trace.heading.title":"Seguimiento de contacto COVID-19",
+ "panel.health.covid19.debug.trace.label.contact":"Seguimiento de contacto COVID-19",
+ "panel.health.covid19.debug.trace.label.date":"Fecha",
+ "panel.health.covid19.debug.trace.label.duration":"Duración",
+ "panel.health.covid19.debug.trace.button.submit.title":"Enviar resultado de prueba",
+ "panel.health.covid19.debug.trace.message.date.text":"Por favor seleccione una fecha",
+ "panel.health.covid19.debug.trace.message.duration.text": "Por favor, introduzca una duración entera",
+ "panel.health.covid19.debug.trace.error.submit.text": "Error al enviar datos de seguimiento de contacto.",
+
+ "panel.health.covid19.history.header.title":"Su historial de eventos de COVID-19",
+ "panel.health.covid19.history.label.empty.title":"No historia",
+ "panel.health.covid19.history.label.description":"Ver su historial de eventos de COVID-19.",
+ "panel.health.covid19.history.label.provider.hint":"proveedor: ",
+ "panel.health.covid19.history.label.empty.provider":"Desconocido",
+ "panel.health.covid19.history.label.self_reported.title":"Síntomas autoinformados",
+ "panel.health.covid19.history.label.self_reported.symptoms":"síntomas: ",
+ "panel.health.covid19.history.label.contact_trace.title":"Seguimiento de contacto",
+ "panel.health.covid19.history.label.contact_trace.details":"seguimiento de contacto: ",
+ "panel.health.covid19.history.label.action.title":"Acción requerida",
+ "panel.health.covid19.history.label.action.details":"acción: ",
+ "panel.health.covid19.history.label.result.title":"Resultado: ",
+ "panel.health.covid19.history.label.location.title":"Lugar de prueba",
+ "panel.health.covid19.history.label.technician_name.title":"Nombre del técnico",
+ "panel.health.covid19.history.label.technician_id.title":"ID del técnico",
+ "panel.health.covid19.history.label.more_info.title":"Más información",
+ "panel.health.covid19.history.label.provider.self_reported": "Auto reportado",
+ "panel.health.covid19.history.label.verified": "Verificado",
+ "panel.health.covid19.history.label.verification_pending": "Verificación pendiente",
+ "panel.health.covid19.history.message.clear_failed": "No se pudo borrar el historial de eventos de COVID-19",
+ "panel.health.covid19.history.button.repost_history.title": "Solicitar mi última prueba nuevamente",
+ "panel.health.covid19.history.button.repost_history.hint": "",
+ "panel.health.covid19.history.message.request_tests": "Su solicitud ha sido enviada. Debería recibir su última prueba en una hora",
+
+ "panel.health.covid19.qr_code.label.qr_image_label": "Safer Illinois COVID-19 Código",
+ "panel.health.covid19.qr_code.label.save_error": "No se pudo guardar el código QR.",
+ "panel.health.covid19.qr_code.button.continue.hint": "",
+ "panel.health.covid19.qr_code.primary.heading.title": "Su clave de cifrado COVID-19",
+ "panel.health.covid19.qr_code.primary.description.1": "Para su privacidad, sus datos de atención médica utilizados para las funciones de COVID-19 están encriptados. La clave de cifrado se almacena localmente en su teléfono para mantenerlo seguro. \n\nPara usar las funciones de COVID-19 en otro dispositivo, deberá transferir manualmente esta clave de cifrado utilizando el código QR a continuación.",
+ "panel.health.covid19.qr_code.primary.button.save.title": "Guarde su clave de cifrado",
+ "panel.health.covid19.qr_code.primary.button.save.hint": "",
+ "panel.health.covid19.qr_code.primary.description.2": "En caso de que su dispositivo actual se pierda o se dañe, le sugerimos que guarde una copia de este código QR en un servicio de almacenamiento de fotos en la nube, para que pueda recuperarlo en su dispositivo de reemplazo. \n\nPuede acceder y guardar esta clave en este dispositivo en cualquier momento accediendo a \"Transferir su clave de cifrado COVID-19 \" desde el centro de información COVID-19.",
+ "panel.health.covid19.qr_code.secondary.heading.title": "Parece que ya usaste esta función en otro dispositivo",
+ "panel.health.covid19.qr_code.secondary.description.1": "¿Quiere transferir su clave de cifrado QR a este dispositivo para recuperar su información de salud anterior? \n\nSeleccione cuál se aplica a usted a continuación. Siempre puede transferir una clave de cifrado QR a este dispositivo en un momento posterior utilizando la opción \"Transferir su clave de cifrado COVID-19\" en el centro de información COVID-19 o en la configuración de su aplicación.",
+ "panel.health.covid19.qr_code.secondary.button.scan.heading": "Si está agregando un segundo dispositivo:",
+ "panel.health.covid19.qr_code.secondary.button.scan.description": "Si aún tiene acceso a su dispositivo principal, puede escanear directamente el código QR de la clave de cifrado COVID-19 desde ese dispositivo.",
+ "panel.health.covid19.qr_code.secondary.button.scan.title": "Escanee su código QR",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.heading": "Si está utilizando un dispositivo de reemplazo:",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.description": "Si ya no tiene acceso a su dispositivo principal, pero guardó su código QR en un servicio de fotos en la nube, puede transferir su clave de cifrado COVID-19 recuperándola de sus fotos.",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.title": "Recupera tu código QR",
+ "panel.health.covid19.qr_code.reset.button.heading": "Restablecer mi código QR secreto COVID-19:",
+ "panel.health.covid19.qr_code.reset.button.title": "Restablecer mi código QR secreto COVID-19",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.title": "Restablecer mi código QR secreto COVID-19",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.description": "Hacer esto le proporcionará un nuevo código QR secreto de COVID-19, pero se perderá su historial de eventos de COVID-19 anterior, ¿continuar?",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.confirm": "Estas seguro",
+
+ "panel.health.covid19.alert.no_qr_code.msg": "No hay código QR",
+ "panel.health.covid19.alert.save.success.msg": "Código qr guardado correctamente en",
+ "panel.health.covid19.alert.save.success.pictures": "Imágenes",
+ "panel.health.covid19.alert.save.success.gallery": "Galería",
+ "panel.health.covid19.alert.save.fail.msg": "No se pudo guardar el código qr en la galería",
+ "panel.health.covid19.alert.qr_code.scan.failed.msg": "No se pudo leer el código QR.",
+ "panel.health.covid19.alert.qr_code.invalid.msg": "Código QR no válido.",
+ "panel.health.covid19.alert.qr_code.not_match.msg": "La clave secreta COVID-19 no coincide con la clave RSA pública existente.",
+ "panel.health.covid19.qr_code.alert.qr_code.transfer.succeeded.msg": "El secreto de COVID-19 se transfirió con éxito.",
+ "panel.health.covid19.alert.qr_code.transfer.failed.msg": "No se pudo transferir el secreto de COVID-19.",
+ "panel.health.covid19.qr_code.button.continue.title": "Seguir",
+ "panel.health.covid19.qr_code.button.transfer_later.title": "Transferir más tarde",
+
+ "panel.health.report_test.heading.title":"Introducir manualmente el resultado",
+ "panel.health.report_test.label.date":"TEST DATE AND TIME",
+ "panel.health.report_test.label.provider":"PROVEEDOR DE ATENCIÓN SANITARIA",
+ "panel.health.report_test.label.date.location":"UBICACIÓN DE PRUEBA",
+ "panel.health.report_test.label.location.empty":"Seleccionar ubicación ...",
+ "panel.health.report_test.label.type":"TIPO DE PRUEBA",
+ "panel.health.report_test.label.result":"RESULTADO",
+ "panel.health.report_test.label.result.empty":"Seleccione el resultado de la prueba ...",
+ "panel.health.report_test.label.image":"AGREGAR RESULTADO DE PRUEBA",
+ "panel.health.report_test.label.image.hint":"Sube una imagen del resultado de tu prueba.",
+ "panel.health.report_test.button.add_image.title":"Sube una imagen",
+ "panel.health.report_test.button.add_test.title":"Agregar prueba",
+ "panel.health.report_test.button.close.title":"Cerca",
+ "panel.health.report_test.button.retake.title":"Volver a tomar",
+ "panel.health.report_test.label.select_photo":"Seleccione Foto",
+ "panel.health.report_test.label.select_photo.description":"Tome una foto del resultado de la prueba o selecciónela de la galería",
+ "panel.health.report_test.button.take_photo":"Tomar foto",
+ "panel.health.report_test.button.select_gallery":"Seleccionar de la galería",
+ "panel.health.report_test.button.cancel":"Cancelar",
+ "panel.health.report_test.error.create.message":"No se puede crear la prueba",
+ "panel.health.report_test.missing.image.message":"Por favor sube la imagen",
+ "panel.health.report_test.future_date.forbidden.message": "No puede enviar una prueba en el futuro",
+
+ "widget.health.onboarding.indicator9.label.hint": "Proceso de incorporación de Covid-19",
+
+ "widget.card.button.favorite.on.title":"Agregar a favoritos",
+ "widget.card.button.favorite.on.hint":"",
+ "widget.card.button.favorite.off.title":"Eliminar de favoritos",
+ "widget.card.button.favorite.off.hint":"",
+ "widget.card.label.interests":"Debido a su interés en:",
+ "widget.card.label.converge":"partido",
+
+ "panel.health.status_update.heading.title":"Actualización de estado",
+ "panel.health.status_update.label.status_change":"Según sus resultados, su estado ha cambiado de",
+ "panel.health.status_update.label.status_change.to":" a ",
+ "panel.health.status_update.label.next_steps":"PRÓXIMOS PASOS",
+ "panel.health.status_update.label.asap":"Lo antes posible",
+ "panel.health.status_update.button.find_location.title":"Encontrar ubicacion",
+ "panel.health.status_update.label.loading":"Agárrate fuerte mientras actualizamos tu estado",
+ "panel.health.status_update.button.continue.title":"Ver los siguientes pasos",
+ "panel.health.status_update.button.continue.hint":"",
+ "panel.health.status_update.info_dialog.label1": "Las definiciones de color de estado pueden cambiar según los diferentes condados.",
+ "panel.health.status_update.info_dialog.label2": "Colores de estado para",
+ "panel.health.status_update.info_dialog.label3": "Default status for new users is set to Orange.",
+ "panel.health.status_update.info_dialog.label4": "An up-to-date on-campus negative test result will reset your COVID-19 status to Yellow, and Building Entry will change to Granted.",
+ "panel.health.status_update.label.reason.title":"ESTADO CAMBIÓ PORQUE:",
+ "panel.health.status_update.label.reason.result":"Resultado",
+ "panel.health.status_update.label.reason.symptoms.title":"Reportaste nuevos síntomas",
+ "panel.health.status_update.label.reason.exposed.title": "Estuvo expuesto a alguien que probablemente estaba infectado",
+ "panel.health.status_update.label.reason.exposure.detail": "Duración de exposición:",
+ "panel.health.status_update.label.reason.action.title": "Las autoridades sanitarias le solicitaron una acción",
+ "panel.health.status_update.label.reason.action.detail": "Acción requerida:",
+
+ "panel.health.next_steps.button.continue.title.find_locatio": "Encontrar ubicacion",
+ "panel.health.next_steps.button.continue.title.care_team": "Póngase en contacto con Care Team",
+ "panel.health.next_steps.label.next_steps": "PRÓXIMOS PASOS",
+ "panel.health.next_steps.label.asap": "Lo antes posible",
+
+ "panel.health.symptoms.heading.title":"¿Estás experimentando alguno de estos síntomas?",
+ "panel.health.symptoms.label.error.loading":"Error al cargar los síntomas.",
+ "panel.health.symptoms.button.submit.title":"Enviar",
+ "panel.health.symptoms.label.success.submit.message":"Sus síntomas han sido procesados.",
+ "panel.health.symptoms.label.error.submit":"Error al enviar los síntomas.",
+
+ "widget.home_campus_tools.label.campus_tools":"Recursos del campus",
+ "widget.home_campus_tools.button.events.title":"Eventos",
+ "widget.home_campus_tools.button.events.hint":"",
+ "widget.home_campus_tools.button.dining.title":"Comida",
+ "widget.home_campus_tools.button.dining.hint":"",
+ "widget.home_campus_tools.button.athletics.title":"Atletismo",
+ "widget.home_campus_tools.button.athletics.hint":"",
+ "widget.home_campus_tools.button.illini_cash.title":"Illini Cash",
+ "widget.home_campus_tools.button.illini_cash.hint":"",
+ "widget.home_campus_tools.button.my_illini.title":"My Illini",
+ "widget.home_campus_tools.header.my_illini.title": "Mi Illini",
+ "widget.home_campus_tools.button.my_illini.hint": "",
+ "widget.home_campus_tools.button.covid19.title":"COVID-19",
+ "widget.home_campus_tools.button.covid19.hint":"",
+
+ "widget.home_covid19_info.label.covid19": "COVID-19 Información",
+ "widget.home_covid19_info.label.info.title": "Ayuda a mantener seguro a Illinois",
+ "widget.home_covid19_info.label.info.description": "Únete a la lucha contra COVID-19 siguiendo y gestionando tu salud",
+
+ "widget.covid19_news_card.read_more.hint":"Toca dos veces para leer más",
+
+ "model.explore.time.today": "Hoy",
+ "model.explore.time.tomorrow": "Mañana",
+ "model.explore.time.at": "a",
+ "model.explore.time.all_day": "Todo el dia",
+
+ "model.user.role.student.title": "Alumno",
+ "model.user.role.employee.title": "Empleado",
+ "model.user.role.resident.title": "Residente",
+
+ "model.covid19.status.color.green": "Seguro",
+ "model.covid19.status.color.yellow": "Precaución",
+ "model.covid19.status.color.orange": "Probablemente infectado",
+ "model.covid19.status.color.red": "Infectado",
+
+ "model.covid19.step.initial": "Realice una prueba COVID-19 con su proveedor de atención médica",
+
+ "logic.date_time.greeting.morning": "Buenos días",
+ "logic.date_time.greeting.afternoon": "Buenas tardes",
+ "logic.date_time.greeting.evening": "Buena noches",
+
+ "logic.polls.unable_to_load_poll": "No se puede cargar la encuesta",
+ "logic.polls.no_polls_with_pin": "No hay encuestas con esta encuesta de 4 dígitos #",
+ "logic.polls.multiple_polls_with_pin": "Hay varias encuestas abiertas con esta encuesta de 4 dígitos #",
+
+ "logic.general.internal_error": "Error interno ocurrido",
+ "logic.general.invalid_response": "Servidor inválido",
+ "logic.general.response_error": "Error de respuesta: %s %s",
+
+ "app.exit_dialog.message":"¿Está seguro de que desea salir?",
+
+ "app.common.heading.hint":"Encabezado",
+ "app.common.heading.one.hint":"Encabezado 1",
+ "app.common.heading.two.hint":"Encabezado 2",
+ "app.common.heading.three.hint":"Encabezado 3",
+
+ "app.common.label.cancelled":"Cancelado",
+ "app.common.label.other": "Otro",
+ "app.common.label.county": "Condado",
+ "app.common.label.read_more": "Read more",
+
+ "app.common.yes":"Yes",
+ "app.common.no":"No",
+
+ "com.illinois.features2.entry.disable_location_awareness":"Deshabilitar reconocimiento de ubicación",
+ "com.illinois.features2.entry.dont_store_location":"No guardes tu ubicación",
+ "com.illinois.features2.entry.remove_preferences":"Elimina tus preferencias",
+ "com.illinois.features2.entry.log_out":"Cerrar sesión en el sistema",
+ "com.illinois.features2.entry.disable_notifications":"Desactivar las notificaciones",
+ "com.illinois.features2.entry.remove_credit_card":"Eliminar la información de su tarjeta de crédito",
+ "com.illinois.features2.entry.dont_share_location":"No comparta sus datos con otros usuarios.",
+
+ "com.illinois.covid19.status.long.orange": "Orange, Test Required",
+ "com.illinois.covid19.status.long.green": "Anticuerpos verdes recientes",
+ "com.illinois.covid19.status.long.yellow": "Amarillo, prueba negativa reciente",
+ "com.illinois.covid19.status.long.red": "Prueba roja, positiva",
+ "com.illinois.covid19.status.long.no change": "Sin alterar",
+ "com.illinois.covid19.status.type.orange": "Naranja",
+ "com.illinois.covid19.status.type.green": "Verde",
+ "com.illinois.covid19.status.type.yellow": "Amarillo",
+ "com.illinois.covid19.status.type.red": "Rojo",
+ "com.illinois.covid19.status.type.no change": "Sin alterar",
+ "com.illinois.covid19.status.description.orange": "Test Required",
+ "com.illinois.covid19.status.description.green": "Anticuerpos recientes",
+ "com.illinois.covid19.status.description.yellow": "Prueba negativa reciente",
+ "com.illinois.covid19.status.description.red": "Prueba positiva",
+ "com.illinois.covid19.status.description.no change": "",
+ "com.illinois.covid19.status.info.description.orange": "Orange: First time user, Past due for test, Self-reported symptoms, Received exposure notification or Quarantined",
+ "com.illinois.covid19.status.info.description.green": "Green: Recent antibodies",
+ "com.illinois.covid19.status.info.description.yellow": "Yellow: Recent negative test",
+ "com.illinois.covid19.status.info.description.red": "Red: Positive test"
+}
diff --git a/assets/strings.zh.json b/assets/strings.zh.json
new file mode 100644
index 00000000..f3fcf6d2
--- /dev/null
+++ b/assets/strings.zh.json
@@ -0,0 +1,796 @@
+{
+ "app.title":"伊利诺伊",
+ "app.offline.message.title":"伊利諾伊州更安全",
+
+ "dialog.yes.title":"是的",
+ "dialog.yes.hint":"",
+ "dialog.no.title":"不",
+ "dialog.no.hint":"",
+ "dialog.ok.title":"OK",
+ "dialog.ok.hint":"",
+ "dialog.cancel.title":"取消",
+ "dialog.cancel.hint":"",
+ "dialog.continue.title":"继续",
+ "dialog.continue.hint":"",
+ "dialog.close.title":"关闭",
+ "dialog.close.hint":"",
+
+ "tabbar.home.title":"主页",
+ "tabbar.home.hint":"主页",
+ "tabbar.explore.title":"探索",
+ "tabbar.explore.hint":"探索页面",
+ "tabbar.more.title":"更多",
+ "tabbar.more.hint":"更多页面",
+ "tabbar.browse.title":"浏览",
+ "tabbar.browse.hint":"浏览頁面",
+ "tabbar.wallet.title":"钱包",
+ "tabbar.wallet.hint":"钱包页面",
+
+ "headerbar.home.title":"主页",
+ "headerbar.home.hint":"主页",
+ "headerbar.menu.title":"菜单",
+ "headerbar.menu.hint":"",
+ "headerbar.search.title":"搜索",
+ "headerbar.search.hint":"输入搜索词",
+ "headerbar.settings.title":"设定",
+ "headerbar.settings.hint":"",
+ "headerbar.search.placehlder":"您正在寻找什么?",
+ "headerbar.back.title":"返回",
+ "headerbar.back.hint":"",
+ "headerbar.close.title":"关闭",
+ "headerbar.close.hint":"",
+ "headerbar.saved.title":"已保存",
+ "headerbar.saved.hint":"",
+ "headerbar.teams.title":"队伍",
+
+ "toggle_button.status.checked":"选中",
+ "toggle_button.status.unchecked":"未选中",
+ "toggle_button.status.checkbox":"复选框",
+
+ "panel.menu.button.events.title":"活动",
+ "panel.menu.button.events.hint":"",
+ "panel.menu.button.dining.title":"用餐",
+ "panel.menu.button.dining.hint":"",
+ "panel.menu.button.athletics.title":"体育",
+ "panel.menu.button.athletics.hint":"",
+ "panel.menu.button.settings.title":"设置",
+ "panel.menu.button.settings.hint":"",
+ "panel.menu.button.illini_cash.title":"Illini现金",
+ "panel.menu.button.illini_cash.hint":"",
+ "panel.menu.button.create_event.title":"创建活动",
+ "panel.menu.button.create_event.hint":"",
+ "panel.menu.button.order_history.title":"订单历史记录",
+ "panel.menu.button.order_history.hint":"",
+ "panel.menu.button.sign_out.title":"退出",
+ "panel.menu.button.sign_out.hint":"",
+ "panel.menu.button.sign_in.title":"登录",
+ "panel.menu.button.sign_in.hint":"",
+ "panel.menu.button.create_account.title":"创建帐户",
+ "panel.menu.button.create_account.hint":"",
+ "panel.menu.label.sign_in_as":"登录为",
+ "panel.menu.label.verified_as":"核实为",
+ "panel.menu.label.connected_netid":"连接NetID",
+ "panel.menu.label.confirm_sign_out":"您确定要退出吗?",
+
+ "panel.onboarding.button.continue.title":"继续",
+ "panel.onboarding.button.continue.hint":"",
+
+ "panel.onboarding.get_started.image.welcome.title":"欢迎来到伊利诺伊州",
+ "panel.onboarding.get_started.image.welcome.hint":"",
+ "panel.onboarding.get_started.button.get_started.title":"使用入门",
+ "panel.onboarding.get_started.button.get_started.hint":"",
+ "panel.onboarding.get_started.image.safer_in_illinois.title":"在伊利诺伊州更安全",
+ "panel.onboarding.get_started.image.powered.title":"Powered by Rokwire",
+
+ "panel.onboarding.location.label.title":"打开位置服务",
+ "panel.onboarding.location.label.title.hint":"标题1",
+ "panel.onboarding.location.label.description":"曝光通知在手機上正常工作所必需",
+ "panel.onboarding.location.button.allow.title":"分享我的位置",
+ "panel.onboarding.location.button.allow.hint":"",
+ "panel.onboarding.location.button.dont_allow.title":"暂时不",
+ "panel.onboarding.location.button.dont_allow.hint":"跳过共享位置",
+ "panel.onboarding.location.label.access_granted":"您已经授予对此应用程序的访问权限。",
+ "panel.onboarding.location.label.access_denied":"您已经拒绝访问此应用程序。",
+
+ "panel.onboarding.bluetooth.label.title":"启用蓝牙",
+ "panel.onboarding.bluetooth.label.title.hint":"标题1",
+ "panel.onboarding.bluetooth.label.description":"使用藍牙提醒您可能接觸到COVID-19.",
+ "panel.onboarding.bluetooth.button.allow.title":"启用蓝牙",
+ "panel.onboarding.bluetooth.button.allow.hint":"",
+ "panel.onboarding.bluetooth.button.dont_allow.title":"不是现在",
+ "panel.onboarding.bluetooth.button.dont_allow.hint":"跳过启用蓝牙",
+ "panel.onboarding.bluetooth.label.access_granted":"您已授予访问此应用程序的权限",
+ "panel.onboarding.bluetooth.label.access_denied":"您已授予访问此应用程序的权限",
+
+ "panel.onboarding.notifications.label.title":"需要的信息",
+ "panel.onboarding.notifications.label.hint":"标题1",
+ "panel.onboarding.notifications.label.description":"您將收到COVID-19信息",
+ "panel.onboarding.notifications.button.allow.title":"接收通知",
+ "panel.onboarding.notifications.button.allow.hint":"",
+ "panel.onboarding.notifications.button.dont_allow.title":"暂时不",
+ "panel.onboarding.notifications.button.dont_allow.hint":"跳过接收通知",
+ "panel.onboarding.notifications.label.access_granted":"您的设置已更改.",
+
+ "panel.onboarding.roles.label.title":"你是",
+ "panel.onboarding.roles.label.title.hint":"标题1",
+ "panel.onboarding.roles.label.description":"选择所有适用项",
+ "panel.onboarding.roles.button.student.title":"大学生",
+ "panel.onboarding.roles.button.student.hint":"",
+ "panel.onboarding.roles.button.employee.title":"員工/會員",
+ "panel.onboarding.roles.button.employee.hint":"",
+ "panel.onboarding.roles.button.resident.title":"伊利諾伊州居民",
+ "panel.onboarding.roles.button.resident.hint":"",
+ "panel.onboarding.roles.button.continue.enabled.title":"確認",
+ "panel.onboarding.roles.button.continue.disabled.title":"選擇一個",
+ "panel.onboarding.roles.button.continue.hint":"",
+
+ "panel.onboarding.login.netid.label.title":"连接您的NetID",
+ "panel.onboarding.login.phone.label.title":"验证您的电话号码",
+ "panel.onboarding.login.netid.label.title.hint":"标题1",
+ "panel.onboarding.login.phone.label.title.hint":"标题1",
+ "panel.onboarding.login.netid.label.description":"用您的NETID登录",
+ "panel.onboarding.login.phone.label.description":"这将保存您的首选项,因此您可以在多台设备上获得相同的体验。",
+ "panel.onboarding.login.label.login_failed":"无法登录。请稍后再试",
+ "panel.onboarding.login.netid.button.continue.title":"使用NetID登录",
+ "panel.onboarding.login.phone.button.continue.title":"验证我的电话号码",
+ "panel.onboarding.login.netid.button.continue.hint":"",
+ "panel.onboarding.login.phone.button.continue.hint":"",
+ "panel.onboarding.login.netid.button.dont_continue.title":"暂时不",
+ "panel.onboarding.login.phone.button.dont_continue.title":"暂时不",
+ "panel.onboarding.login.netid.button.dont_continue.hint":"跳过验证",
+ "panel.onboarding.login.phone.button.dont_continue.hint":"跳过验证",
+
+ "panel.onboarding.verify_phone.title":"验证您的电话号码",
+ "panel.onboarding.verify_phone.title.hint":"",
+ "panel.onboarding.verify_phone.description":"要验证您的电话号码,请选择您首选的联系渠道,我们将向您发送一次身份验证码。",
+ "panel.onboarding.verify_phone.description.hint":"",
+ "panel.onboarding.verify_phone.phone_number.label":"电话号码",
+ "panel.onboarding.verify_phone.phone_number.hint":"",
+ "panel.onboarding.verify_phone.text_me.label":"发短信给我",
+ "panel.onboarding.verify_phone.text_me.hint":"",
+ "panel.onboarding.verify_phone.call_me.label":"给我打电话",
+ "panel.onboarding.verify_phone.call_me.hint":"",
+ "panel.onboarding.verify_phone.button.next.label":"下一步",
+ "panel.onboarding.verify_phone.button.next.hint":"点击以继续",
+ "panel.onboarding.verify_phone.validation.phone_number.text":"请输入您的电话号码",
+ "panel.onboarding.verify_phone.validation.channel_selection.text":"请选择验证方法",
+ "panel.onboarding.verify_phone.validation.server_error.text":"请输入有效的电话号码",
+
+ "panel.onboarding.confirm_phone.title":"确认您的代码",
+ "panel.onboarding.confirm_phone.title.hint":"",
+ "panel.onboarding.confirm_phone.description.send":"一次性代碼已發送到%s。 在下面輸入您的代碼以繼續。",
+ "panel.onboarding.confirm_phone.description.send.hint":"",
+ "panel.onboarding.confirm_phone.code.label":"一次性代码",
+ "panel.onboarding.confirm_phone.code.hint":"",
+ "panel.onboarding.confirm_phone.not_received.text.label":"未收到短信吗?",
+ "panel.onboarding.confirm_phone.not_received.text.hint":"",
+ "panel.onboarding.confirm_phone.not_received.call.label":"未接电话吗?",
+ "panel.onboarding.confirm_phone.not_received.call.hint":"",
+ "panel.onboarding.confirm_phone.button.confirm.label":"确认电话号码",
+ "panel.onboarding.confirm_phone.button.confirm.hint":"",
+ "panel.onboarding.confirm_phone.validation.phone_number.text":"请填写您的代码",
+ "panel.onboarding.confirm_phone.validation.server_error.text":"无法验证代码",
+
+ "panel.onboarding.upgrade.required.label.title":"需要升级",
+ "panel.onboarding.upgrade.required.label.description":"%s版本号%s需要升级到%s或者更新的版本。",
+ "panel.onboarding.upgrade.available.label.title":"升级可用",
+ "panel.onboarding.upgrade.available.label.description":"%s版本号%s有一个新版本号%s可用。",
+ "panel.onboarding.upgrade.button.upgrade.title":"升级",
+ "panel.onboarding.upgrade.button.upgrade.hint":"",
+ "panel.onboarding.upgrade.button.not_now.title":"现在不",
+ "panel.onboarding.upgrade.button.not_now.hint":"",
+ "panel.onboarding.upgrade.button.dont_show.title":"不再显示",
+ "panel.onboarding.upgrade.button.dont_show.hint":"",
+
+ "panel.onboarding.base.not_now.hint":"",
+ "panel.onboarding.base.not_now.title":"暂时不",
+
+ "panel.settings.sports.heading":"设置",
+ "panel.settings.categories.header.title":"你的分类",
+ "panel.settings.categories.heading":"您的类别",
+ "panel.settings.categories.description":"点击以遵循您最感兴趣的类别",
+
+ "panel.settings.feedback.label.title":"提供反馈",
+
+ "panel.settings.privacy_statement.label.title":"隐私声明",
+
+ "panel.settings.label.offline.feedback":"离线时无法提供反馈。",
+
+ "panel.settings.home.settings.header":"设置",
+ "panel.settings.home.connect.not_logged_in.title":"連接到伊利諾伊州",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_1": "您是不是一位",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_2": "學生",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_3": "要么",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_4": "教員",
+ "panel.settings.home.connect.not_logged_in.netid.description.part_5": "? 使用您的NetID登錄以查看特定於您的伊利諾伊州信息,例如您的Illini Cash和膳食計劃",
+ "panel.settings.home.connect.not_logged_in.netid.title": "連接您的NetID",
+ "panel.settings.home.connect.not_logged_in.phone.description.part_1": "沒有NetID",
+ "panel.settings.home.connect.not_logged_in.phone.description.part_2": "? 驗證您的電話號碼以保存您的首選項,並在多台設備上獲得相同的體驗",
+ "panel.settings.home.connect.not_logged_in.phone.title": "驗證您的電話號碼",
+ "panel.settings.home.customizations.title":"自定义",
+ "panel.settings.home.net_id.title":"伊利諾伊州NetID",
+ "panel.settings.home.phone_ver.title":"电话验证",
+ "panel.settings.home.notifications.title":"通知",
+ "panel.settings.home.user_info.title.sufix":"欢迎来到伊利诺伊州",
+ "panel.settings.home.account.title": "您的帳戶",
+ "panel.settings.home.account.personal_info.title":"个人信息",
+ "panel.settings.home.account.options.payment":"支付",
+ "panel.settings.home.customizations.role.title":"你是",
+ "panel.settings.home.customizations.manage_interests.title":"管理您的兴趣",
+ "panel.settings.home.customizations.food_filters.title":"食物过滤器",
+ "panel.settings.home.customizations.display_times_in_central_time.title":"以中部时间显示所有时间",
+ "panel.settings.home.net_id.message":"已连接为",
+ "panel.settings.home.net_id.button.disconnect":"断开您的NetID",
+ "panel.settings.home.net_id.button.connect":"连接您的NetID",
+ "panel.settings.home.phone_ver.message":"已验证为",
+ "panel.settings.home.phone_ver.button.connect":"验证您的电话号码",
+ "panel.settings.home.phone_ver.button.disconnect":"断开你的电话",
+ "panel.settings.home.logout.message":"确定要退出吗?",
+ "panel.settings.home.logout.button.yes":"是的",
+ "panel.settings.home.logout.no":"不",
+ "panel.settings.home.security.reset_password":"重置密码",
+ "panel.settings.home.security.face_id":"使用人脸ID",
+ "panel.settings.home.notifications.reminders":"活动提醒",
+ "panel.settings.home.notifications.athletics_updates":"体育更新",
+ "panel.settings.home.notifications.dining":"特价餐厅",
+ "panel.settings.home.notifications.covid19":"COVID-19通知",
+ "panel.settings.home.privacy.title":"隐私",
+ "panel.settings.home.privacy.edit_my_privacy.title":"编辑我的隐私",
+ "panel.settings.home.privacy.privacy_statement.title":"隐私声明",
+ "panel.settings.home.button.debug.title":"调试",
+ "panel.settings.home.button.debug.hint":"",
+ "panel.settings.home.button.test.title":"测试",
+ "panel.settings.home.button.test.hint":"",
+ "panel.settings.home.feedback.title": " 我们需要你的主意!",
+ "panel.settings.home.feedback.description": " 喜欢这个应用程序吗?我们遗漏了什么?点击底部提交您的想法。",
+ "panel.settings.home.button.feedback.title": "提交反馈",
+ "panel.settings.home.button.feedback.hint": "",
+ "panel.settings.home.covid19.exposure_notifications": "接触通知",
+ "panel.settings.home.covid19.provider_test_result": "健康提供者测试结果 ",
+ "panel.settings.home.covid19.title": "COVID-19",
+ "panel.settings.home.covid19.qr_code.button.title": "二維碼",
+ "panel.settings.home.covid19.transfer.button.title": "傳遞",
+ "panel.settings.home.covid19.qr_code.description.title": "COVID-19秘密QR碼",
+ "panel.settings.home.covid19.transfer.description.title": "從您的其他電話轉移COVID-19機密",
+ "panel.settings.home.covid19.alert.qr_code.scan.failed.msg": "無法讀取QR碼.",
+ "panel.settings.home.covid19.alert.qr_code.transfer.succeeded.msg": "COVID-19機密已成功傳輸.",
+ "panel.settings.home.covid19.alert.qr_code.transfer.failed.msg": "無法傳輸COVID-19機密.",
+ "panel.settings.home.covid19.alert.reset.prompt": "这样做将为您提供一个新的COVID-19秘密QRcode,但您以前的COVID-19事件历史记录将丢失,是否继续?",
+ "panel.settings.home.covid19.alert.reset.failed": "重置COVID-19机密QRcode失败",
+ "panel.settings.home.covid19.text.user.fail": "无法检索用户COVID-19设置.",
+ "panel.settings.home.covid19.text.keys.checking": "正在检查COVID-19密钥...",
+ "panel.settings.home.covid19.text.keys.missing.public": "缺少COVID-19公钥",
+ "panel.settings.home.covid19.text.keys.missing.private": "缺少COVID-19私钥",
+ "panel.settings.home.covid19.text.keys.mismatch": "COVID-19钥匙未配对",
+ "panel.settings.home.covid19.text.keys.paired": "COVID-19钥匙有效并配对",
+ "panel.settings.home.covid19.text.keys.reset": "重置COVID-19密钥对.",
+ "panel.settings.home.covid19.text.keys.transfer_or_reset": "从您的另一部手机转移COVID-19私钥或重置COVID-19密钥对.",
+ "panel.settings.home.covid19.text.keys.qr_code": "出示你的COVID-19秘密二维码.",
+ "panel.settings.home.covid19.button.retry.title": "重试",
+ "panel.settings.home.covid19.button.reset.title": "重置",
+ "panel.settings.home.covid19.button.load.title": "加载",
+ "panel.settings.home.covid19.button.scan.title": "扫描",
+ "panel.settings.home.covid19.button.qr_code.title": "QR Code",
+
+ "panel.settings.label.offline.phone_ver":"离线时无法验证电话号码。",
+
+ "panel.profile_info.header.title":"个人信息",
+ "panel.profile_info.net_id.title":"NetID",
+ "panel.profile_info.full_name.title":"全名",
+ "panel.profile_info.first_name.title":"名字",
+ "panel.profile_info.middle_name.title":"中间名",
+ "panel.profile_info.last_name.title":"姓氏",
+ "panel.profile_info.email_address.title":"电子邮件地址",
+ "panel.profile_info.phone_number.title":"电话号码",
+ "panel.profile_info.button.sign_out.title":"退出",
+ "panel.profile_info.button.sign_out.hint":"",
+ "panel.profile_info.label.remove_my_info.title":"删除我的信息",
+ "panel.profile_info.button.remove_my_information.title":"删除我的信息",
+ "panel.profile_info.button.remove_my_information.hint":"",
+ "panel.profile_info.remove_my_information.title":"回答是,您的所有个人信息和偏好将从我们的系统中删除。此操作无法恢复。删除信息后,我们将在安装应用程序时将您带回到第一个屏幕,以便您可以再次启动或删除该应用程序。",
+ "panel.profile_info.dialog.remove_my_information.subtitle":"确定吗?",
+ "panel.profile_info.dialog.remove_my_information.yes.title":"是的",
+ "panel.profile_info.dialog.remove_my_information.no.title":"不",
+
+ "panel.covid19_passport.header.title": "COVID-19",
+ "panel.covid19_passport.button.close.title": "關",
+ "panel.covid19_passport.button.info_center.title": "您的COVID-19信息中心",
+ "panel.covid19_passport.label.status.empty":"此县没有可用状态",
+ "panel.covid19_passport.label.counties.empty":"沒有可用縣",
+ "panel.covid19_passport.label.county.empty.hint":"选择一个县...",
+ "panel.covid19_passport.label.access.heading": "建筑使用权利",
+ "panel.covid19_passport.label.access.granted": "授予",
+ "panel.covid19_passport.label.access.denied": "否认",
+ "panel.covid19_passport.message.missing_id_info": "找不到Illini ID信息. 您的I-Card可能已过期。请与ID中心联系.",
+
+ "panel.covid19_guidelines.header.title": "縣準則",
+ "panel.covid19_guidelines.description.title": "遵循這些當前準則,有助於阻止COVID-19的傳播。",
+ "panel.covid19_guidelines.status.title": "这些是基于您在以下县的%s状态:",
+ "panel.covid19_guidelines.label.county.empty":"选择一个县...",
+ "panel.covid19_guidelines.no.status":"沒有關於您在該縣的身份的特定指南。",
+
+ "panel.covid19home.header.title": "COVID-19",
+ "panel.covid19home.top_heading.title": "保持健康",
+ "panel.covid19home.label.next_step.title": "下一步",
+ "panel.covid19home.label.schedule_after.title": "在%s之后计划",
+ "panel.covid19home.button.find_test_locations.title": "查找测试位置",
+ "panel.covid19home.button.find_test_locations.hint": "",
+ "panel.covid19home.button.country_guidelines.title": "县指南",
+ "panel.covid19home.button.country_guidelines.hint": "",
+ "panel.covid19home.button.care_team.title": "您的\n护理团队",
+ "panel.covid19home.button.care_team.hint": "",
+ "panel.covid19home.button.campus_updates.title": "校园更新",
+ "panel.covid19home.button.campus_updates.hint": "",
+ "panel.covid19home.button.report_test.title": "报告测试结果",
+ "panel.covid19home.button.report_test.hint": "",
+ "panel.covid19home.button.test_history.title": "您的测试历史",
+ "panel.covid19home.button.test_history.hint": "",
+ "panel.covid19home.label.health.title":"您的健康",
+ "panel.covid19home.label.resources.title":"资源",
+ "panel.covid19home.label.check_in.title":"症状签入“",
+ "panel.covid19home.label.check_in.description":"自行报告任何症状,以查看是否应接受检查或待在家中",
+ "panel.covid19home.label.result.title":"添加测试结果",
+ "panel.covid19home.label.result.description":"保持最新状态",
+ "panel.covid19home.label.status.title":"当前状态:",
+ "panel.covid19home.label.status.na":"無法使用",
+ "panel.covid19home.button.show_status_card.title":"显示状态卡",
+ "panel.covid19home.label.campus_updates.title":"校园更新",
+ "panel.covid19home.button.about.title":"关于",
+ "panel.covid19home.label.most_recent_event.title": "最近的事件",
+ "panel.covid19home.label.provider.self_reported": "自我报告",
+ "panel.covid19home.label.action_required.title": "所需操作",
+ "panel.covid19home.label.contact_trace.title": "接触痕迹",
+ "panel.covid19home.label.reported_symptoms.title": "自述症状",
+
+ "panel.covid19_test_locations.header.title": "测试位置",
+ "panel.covid19_test_locations.label.contact.title": "联系人",
+ "panel.covid19_test_locations.distance.text": "寻路",
+ "panel.covid19_test_locations.distance.unknown": "未知距离",
+ "panel.covid19_test_locations.work_time.unknown": "未知工作时间",
+ "panel.covid19_test_locations.work_time.open_until":"打开到",
+ "panel.covid19_test_locations.work_time.closed_until": "关闭到",
+ "panel.covid19_test_locations.all_providers.text": "所有提供者",
+ "panel.covid19_test_locations.call.hint": "呼叫",
+
+ "panel.covid19.header.title": "COVID-19",
+ "panel.covid19.latest_update.title": "最近更新",
+ "panel.covid19.latest_update.read_more.title": "閱讀更多",
+ "panel.covid19.news.title": "COVID-19新聞",
+ "panel.covid19.health_status.title": "你的健康",
+ "panel.covid19.faq.title":"问题解答",
+ "panel.covid19.faq.description":"回答您最常见的问题:",
+ "panel.covid19.faq.update.text":"更新了%s",
+ "panel.covid19.faq.question.hint":"双击可显示问题",
+ "panel.covid19.faq.title.label":"常见问题",
+ "panel.covid19.resources.poor_accessibility.hint": "該鏈接將您帶到伊利諾伊州更安全應用之外的網站",
+ "panel.covid19.label.current_status.label": "当前状态:",
+ "panel.covid19.button.show_status_card.title": "显示状态卡",
+ "panel.covid19.button.show_status_card.hint": "",
+ "panel.covid19.button.country_guidelines.title": "县指南",
+ "panel.covid19.button.country_guidelines.hint": "",
+ "panel.covid19.button.campus_updates.title": "校园更新",
+ "panel.covid19.button.campus_updates.hint": "",
+ "panel.covid19.button.health_history.title": "查看健康历史记录",
+ "panel.covid19.button.health_history.hint": "",
+ "panel.covid19.finish_setup.title": "完成安装",
+ "panel.covid19.finish_setup_description.title": "使用您的官方身份",
+ "panel.covid19.button.verify_identity.title": "验证您的身份",
+ "panel.covid19.unverified.title": "未验证",
+ "panel.covid19.button.covid_wellness_center.title": "COVID-19健康回答中心",
+ "panel.covid19.button.covid_wellness_center.hint": "",
+
+ "panel.covid19_wellness_center.header.title": "COVID-19 健康中心",
+ "panel.covid19_wellness_center.label.description": "如果你对应用程序有问题或得到测试结果,请联系COVID健康回答中心寻求帮助.",
+ "panel.covid19_wellness_center.label.email": "发送电子邮件至Covid健康回答中心 ",
+ "panel.covid19_wellness_center.label.phone": "拨打接听中心的电话 ",
+
+ "panel.covid19_news.header.title": "COVID-19",
+ "panel.covid19_news.news.posted.label": "發表在%s",
+
+ "panel.covid19_campus_updates.header.title": "校園更新",
+ "panel.covid19_campus_updates.sub_title.title": "伊利諾伊大學COVID-19更新",
+
+ "panel.covid19.qr_code.title": "COVID-19 QR碼",
+ "panel.covid19.qr_code.description.heading.1": "如果您在“伊利諾伊州更安全”應用程序中使用多個設備,請使用此QR碼傳輸必要的機密,以解碼您的COVID-19健康信息。",
+ "panel.covid19.qr_code.description.heading.2": "保存此QR碼,以便在丟失或更換手機時,可以在新手機上檢索COVID-19健康信息.",
+ "panel.covid19.qr_code.button.save.title": "保存",
+ "panel.covid19.qr_code.alert.no_qr_code.msg": "沒有二維碼",
+ "panel.covid19.qr_code.alert.save.success.msg": "已成功將二維碼保存在圖庫中",
+ "panel.covid19.qr_code.alert.save.fail.msg": "無法將二維碼保存在圖庫中",
+ "panel.covid19.qr_code.code.hint": "QR code 图像",
+ "panel.covid19.qr_code.description.on_boarding.heading.1": "保存此二维码,以便如果您丢失或更换手机,您可以在新手机上检索您的COVID-19健康信息.",
+ "panel.covid19.qr_code.description.on_boarding.heading.2": "您可以暂时跳过此步骤,以后在“设置”中执行",
+ "panel.covid19.qr_code.button.skip.title.": "跳过",
+ "panel.covid19.qr_code.button.skip.hint.": "跳过保存",
+
+ "panel.covid19.transfer.title": "传输加密密钥",
+ "panel.covid19.transfer.label.qr_image_label": "安全伊利诺伊COVID-19码",
+ "panel.covid19.transfer.label.save_error": "无法保存二维码.",
+ "panel.covid19.transfer.button.continue.hint": "",
+ "panel.covid19.transfer.primary.heading.title": "你的COVID-19加密密钥",
+ "panel.covid19.transfer.primary.button.save.title": "保存加密密钥",
+ "panel.covid19.transfer.primary.button.save.hint": "",
+ "panel.covid19.transfer.secondary.heading.title": "缺少COVID-19加密密钥",
+ "panel.covid19.transfer.secondary.button.scan.heading": "如果你要添加第二台设备:",
+ "panel.covid19.transfer.secondary.button.scan.description": "如果您仍然可以访问您的主设备,您可以直接扫描该设备上的COVID-19加密密钥QR码.",
+ "panel.covid19.transfer.secondary.button.scan.title": "扫描你的二维码",
+ "panel.covid19.transfer.secondary.button.retrieve.heading": "如果您使用的是替换设备:",
+ "panel.covid19.transfer.secondary.button.retrieve.description": "如果您不再能够访问您的主要设备,但将您的二维码保存到云照片服务,您可以通过从照片中检索来传输您的COVID-19加密密钥.",
+ "panel.covid19.transfer.secondary.button.retrieve.title": "检索您的二维码",
+ "panel.covid19.transfer.alert.no_qr_code.msg": "这里没有二维码",
+ "panel.covid19.transfer.alert.save.success.msg": "二维码保存成功",
+ "panel.covid19.transfer.alert.save.success.pictures": "图片",
+ "panel.covid19.transfer.alert.save.success.gallery": "图片库",
+ "panel.covid19.transfer.alert.save.fail.msg": "二维码保存失败 ",
+ "panel.covid19.transfer.alert.qr_code.scan.failed.msg": "读取二维码失败.",
+ "panel.covid19.transfer.alert.qr_code.invalid.msg": "无效二维码.",
+ "panel.covid19.transfer.alert.qr_code.not_match.msg": "COVID-19密钥与现有公钥RSA密钥不匹配.",
+ "panel.covid19.transfer.alert.qr_code.transfer.succeeded.msg": "COVID-19机密传输成功.",
+ "panel.covid19.transfer.alert.qr_code.transfer.failed.msg": "无法传送COVID-19机密.",
+
+ "panel.debug.header.title":"调试",
+
+ "panel.debug_messaging.header.title":"消息",
+
+ "panel.web.offline.message": "您需要在線才能執行此操作。 請檢查您的互聯網連接。",
+
+ "panel.health.onboarding.covid19.intro.label.title": "加入對抗COVID-19的戰鬥",
+ "panel.health.onboarding.covid19.intro.label.description": "跟踪和管理您的健康狀況,以幫助確保我們的伊利諾伊州社區安全",
+ "panel.health.onboarding.covid19.intro.button.continue.title": "繼續",
+ "panel.health.onboarding.covid19.intro.button.continue.hint": "",
+
+ "panel.health.onboarding.covid19.how_it_works.heading.title": "工作原理",
+ "panel.health.onboarding.covid19.how_it_works.line1.title": "测试和限制暴露是减缓COVID-19扩散的关键.",
+ "panel.health.onboarding.covid19.how_it_works.line2.title": "您可以使用此应用程序:",
+ "panel.health.onboarding.covid19.how_it_works.line3.title": "自我诊断你的COVID-19症状,并以此更新你的状态.",
+ "panel.health.onboarding.covid19.how_it_works.line4.title": "自动接收来自医疗保健提供商的测试结果.",
+ "panel.health.onboarding.covid19.how_it_works.line5.title": "当你与检测结果呈阳性的人接触时,允许你的手机发送接触警告.",
+ "panel.health.onboarding.covid19.how_it_works.button.next.title": "下一步",
+ "panel.health.onboarding.covid19.how_it_works.button.next.hint": "",
+
+ "panel.health.onboarding.covid19.consent.label.title": "COVID-19功能的特别许可",
+ "panel.health.onboarding.covid19.consent.label.description": "接触通知",
+ "panel.health.onboarding.covid19.consent.label.content1": "如果您同意接触通知,則允許您的手機向附近的也在使用此功能的Safer Illinois應用程序用戶發送匿名藍牙信號。 您的電話也將接收並記錄來自其電話的信號。 如果其中一個用戶在接下來的14天內檢測出COVID-19呈陽性,則該應用程序將提醒您可能的暴露情況,並為您提供下一步建議。 您的身份和健康狀態以及所有其他用戶的身份和健康狀態將保持匿名。",
+ "panel.health.onboarding.covid19.consent.check_box.label.participate": "我同意参与接触通知系统(需要打开蓝牙).",
+ "panel.health.onboarding.covid19.consent.check_box.label.allow":"我同意让我的医疗保健提供者提供我的测试结果.",
+ "panel.health.onboarding.covid19.consent.label.content2": "自动测试结果",
+ "panel.health.onboarding.covid19.consent.label.content3": "我同意將我的醫療保健提供者的測試結果與Safer Illinois應用程序聯繫起來。",
+ "panel.health.onboarding.covid19.consent.label.content4": "您的参与是自愿的,您可以随时停止.",
+ "panel.health.onboarding.covid19.consent.button.consent.title": "下一個",
+ "panel.health.onboarding.covid19.consent.button.consent.hint": "",
+ "panel.health.onboarding.covid19.consent.button.scroll_to_continue.title": "滚动以继续",
+ "panel.health.onboarding.covid19.consent.label.error.login":"无法健康登录",
+
+ "panel.health.onboarding.covid19.resident_info.label.title": "使用政府頒發的ID驗證您的身份",
+ "panel.health.onboarding.covid19.resident_info.label.description": "驗證後,您將收到根據您所在縣的準則,症狀以及任何與COVID-19相關的測試的帶有顏色編碼的健康狀況.",
+ "panel.health.onboarding.covid19.resident_info.button.passport.title": "護照",
+ "panel.health.onboarding.covid19.resident_info.button.passport.hint": "",
+ "panel.health.onboarding.covid19.resident_info.button.drivers_license.title": "駕駛執照",
+ "panel.health.onboarding.covid19.resident_info.button.drivers_license.hint": "",
+ "panel.health.onboarding.covid19.resident_info.button.verify_later.title": "稍後驗證",
+
+ "panel.health.onboarding.covid19.review_scan.label.title": "查看您的掃描",
+ "panel.health.onboarding.covid19.review_scan.label.name.title": "名稱",
+ "panel.health.onboarding.covid19.review_scan.label.birth_year.title": "出生年",
+ "panel.health.onboarding.covid19.review_scan.message.failed": "無法應用掃描數據",
+ "panel.health.onboarding.covid19.review_scan.button.rescan.title": "重新掃描",
+ "panel.health.onboarding.covid19.review_scan.button.rescan.hint": "",
+ "panel.health.onboarding.covid19.review_scan.button.use_scan.title": "使用此掃描",
+ "panel.health.onboarding.covid19.review_scan.button.use_scan.hint": "",
+
+ "panel.health.onboarding.covid19.county.label.title": "您在哪個縣工作和生活?",
+ "panel.health.onboarding.covid19.county.label.description": "選擇所有符合條件的",
+ "panel.health.onboarding.covid19.county.button.add_county.label": "添加另一個縣",
+ "panel.health.onboarding.covid19.county.button.add_county.hint": "",
+ "panel.health.onboarding.covid19.county.button.next.title": "下一個",
+ "panel.health.onboarding.covid19.county.button.next.hint": "",
+ "panel.health.onboarding.covid19.county.select.label": "選擇一個縣",
+ "panel.health.onboarding.covid19.county.dropdown.select.default.label": "選擇一個縣...",
+ "panel.health.onboarding.covid19.county.alert.unique.message": "請在每個字段中選擇不同的縣!",
+
+ "panel.health.onboarding.covid19.providers.label.title": "您當前的醫療保健提供者是誰?",
+ "panel.health.onboarding.covid19.providers.label.description": "選擇所有符合條件的",
+ "panel.health.onboarding.covid19.providers.button.next.title": "下一個",
+ "panel.health.onboarding.covid19.providers.button.next.hint": "",
+
+ "panel.health.onboarding.covid19.final.label.title": "所有东西都为你准备好了!",
+ "panel.health.onboarding.covid19.final.label.description": "您已通過驗證,並且狀態卡已添加到您的個人資料中.",
+ "panel.health.onboarding.covid19.final.label.bottom.description":"现在您可以将此应用程序用作与COVID-19战斗的伙伴.",
+ "panel.health.onboarding.covid19.final.label.unverified.description": "现在您可以将此应用程序用作与COVID-19战斗的伙伴",
+ "panel.health.onboarding.covid19.final.label.unverified.bottom.description":"要訪問您的COVID-19狀態, 您需要上傳政府ID. 您可以隨時在COVID-19設置中添加它.",
+ "panel.health.onboarding.covid19.final.button.continue.title": "開始使用",
+ "panel.health.onboarding.covid19.final.button.continue.hint": "",
+
+ "panel.health.onboarding.covid19.login.netid.label.title":"连接您的 NetID",
+ "panel.health.onboarding.covid19.login.phone.label.title":"验证您的电话号码",
+ "panel.health.onboarding.covid19.login.netid.label.title.hint":"标题 1",
+ "panel.health.onboarding.covid19.login.phone.label.title.hint":"标题 1",
+ "panel.health.onboarding.covid19.login.netid.label.description":"使用您的NetID登录以使用学院和宿舍的特定功能.",
+ "panel.health.onboarding.covid19.phone.label.description":"这将保存您的首选项,以便您可以在多个设备上拥有相同的体验.",
+ "panel.health.onboarding.covid19.login.label.login_failed":"无法登录。请稍后再试",
+ "panel.health.onboarding.covid19.login.netid.button.continue.title":"使用NetID登录",
+ "panel.health.onboarding.covid19.login.phone.button.continue.title":"验证我的电话号码",
+ "panel.health.onboarding.covid19.login.netid.button.continue.hint":"",
+ "panel.health.onboarding.covid19.login.phone.button.continue.hint":"",
+ "panel.health.onboarding.covid19.login.netid.button.dont_continue.title":"现在不行",
+ "panel.health.onboarding.covid19.login.phone.button.dont_continue.title":"现在不行",
+ "panel.health.onboarding.covid19.login.netid.button.dont_continue.hint":"跳过验证",
+ "panel.health.onboarding.covid19.login.phone.button.dont_continue.hint":"跳过验证",
+ "panel.health.onboarding.covid19.login.label.error.login":"无法健康登录",
+
+ "panel.health.covid19.about.heading.title":"关于",
+
+ "panel.health.covid19.add_test.heading.title":"添加测试结果",
+ "panel.health.covid19.add_test.label.where_question":"测试是在哪里进行的?",
+ "panel.health.covid19.add_test.label.information": "为什么需要这些信息?",
+ "panel.health.covid19.add_test.label.provider.title":"医疗保健提供者",
+ "panel.health.covid19.add_test.label.provider.empty_hint":"选择提供者",
+ "panel.health.covid19.add_test.button.retreive.title":"检索结果",
+ "panel.health.covid19.add_test.button.enter_manually.title":"手工输入",
+ "panel.health.covid19.add_test.label.info.retrieved.text1": "结果",
+ "panel.health.covid19.add_test.label.info.retrieved.text2": "已检索",
+ "panel.health.covid19.add_test.label.info.retrieved.text3": "来自您的医疗保健提供者的信息将立即得到验证。您的健康状况的任何变化都会立即反映出来.",
+ "panel.health.covid19.add_test.label.info.manually.text1": "结果",
+ "panel.health.covid19.add_test.label.info.manually.text2": "手动输入",
+ "panel.health.covid19.add_test.label.info.manually.text3": "将由公共医疗机构进行审查和验证。一旦验证,状态可能会发生更改.",
+ "panel.health.covid19.add_test.label.manual_tests_disabled":"Test results from this health care provider will automatically appear if you have consented to Health Provider Test Results in settings and you are connected with your NetID.",
+
+ "panel.health.covid19.care_team.heading.title": "您的护理团队",
+ "panel.health.covid19.care_team.label.question": "我们是来帮忙的.",
+ "panel.health.covid19.care_team.label.description": "联系你的COVID-19护理小组的人-我们会帮助你的.",
+ "panel.health.covid19.care_team.label.status": "当前状态:",
+ "panel.health.covid19.care_team.label.emergency.text1": "在紧急情况下,",
+ "panel.health.covid19.care_team.label.emergency.text2": "随时拨打911.",
+ "panel.health.covid19.care_team.team.title.mc_kinley": "呼 叫麦金利健康中心",
+ "panel.health.covid19.care_team.team.contact.mc_kinley": "1-217-333-2700",
+ "panel.health.covid19.care_team.team.semantic_contact.mc_kinley": "12173332700",
+ "panel.health.covid19.care_team.team.description.mc_kinley": "与“拨打护士热线”上的人联系,讨论您的症状和临床护理选择.",
+ "panel.health.covid19.care_team.team.title.osf": "OSF 医疗",
+ "panel.health.covid19.care_team.team.contact.osf": "1-833-673-5669",
+ "panel.health.covid19.care_team.team.semantic_contact.osf": "18336735669",
+ "panel.health.covid19.care_team.team.description.osf": "We’ve partnered with OSF OnCall Connect program and the Illinois Department of Healthcare and Family Services to support you getting through COVID-19. Call the Nurse Hotline at 1-833-OSF-KNOW (833-673-5669) to learn more about the program, which includes delivery of a care kit and digital visits to monitor you over a 16-day period.",
+ "panel.health.covid19.care_team.label.more_info.title": "有关大流行卫生工作者计划的更多信息",
+ "panel.health.covid19.care_team.label.more_info.description": "大流行卫生工作者(PHW)是经过培训的OSF医疗任务合作伙伴,在您从COVID-19恢复时,与您建立联系以提供支持,并在您和医疗保健提供者之间起到直接的联系,从而降低进一步暴露的风险。虽然PHW工作者不是注册护士,但如果您的病情恶化或您有具体的临床问题,临床护士和医生将随时待命 ",
+ "panel.health.covid19.care_team.label.more_info.hint": "双击可显示更多信息",
+ "panel.health.covid19.care_team.label.call.hint": "致电 ",
+ "panel.health.covid19.care_team.label.more_info.link": "Learn more",
+
+ "panel.health.covid19.debug.keys.heading.title.":"COVID-19密钥",
+ "panel.health.covid19.debug.keys.label.public_key":"RSA公钥:",
+ "panel.health.covid19.debug.keys.label.private_key":"RSA私钥:",
+ "panel.health.covid19.debug.keys.label.aes_key":"AES 密钥:",
+ "panel.health.covid19.debug.keys.label.blob":"Blob:",
+ "panel.health.covid19.debug.keys.label.encripted_aes":"加密的aes密钥:",
+ "panel.health.covid19.debug.keys.label.encripted_blob":"加密的Blob:",
+ "panel.health.covid19.debug.keys.label.decripted_aes":"解密的AES密钥:",
+ "panel.health.covid19.debug.keys.label.decripted_blob":"解密的Blob:",
+ "panel.health.covid19.debug.keys.button.refres.title":"刷新RSA密钥",
+ "panel.health.covid19.debug.keys.button.generate_aes.title":"生成AES密钥",
+ "panel.health.covid19.debug.keys.button.encript.title":"加密",
+ "panel.health.covid19.debug.keys.button.decript.title":"解密",
+ "panel.health.covid19.debug.keys.label.error.refres.title":"刷新失败",
+
+ "panel.health.covid19.debug.trace.heading.title":"COVID-19接触者追踪",
+ "panel.health.covid19.debug.trace.label.contact":"追踪COVID-19接触者",
+ "panel.health.covid19.debug.trace.label.date":"日期",
+ "panel.health.covid19.debug.trace.label.duration":"持续时间",
+ "panel.health.covid19.debug.trace.button.submit.title":"提交测试结果",
+ "panel.health.covid19.debug.trace.message.date.text":"请选择日期",
+ "panel.health.covid19.debug.trace.message.duration.text": "请输入整数持续时间",
+ "panel.health.covid19.debug.trace.error.submit.text": "提交接触者追踪数据失败.",
+
+ "panel.health.covid19.history.header.title":"你的COVID-19事件记录",
+ "panel.health.covid19.history.label.empty.title":"没有历史记录",
+ "panel.health.covid19.history.label.description":"查看您的COVID-19事件历史记录.",
+ "panel.health.covid19.history.label.provider.hint":"提供者: ",
+ "panel.health.covid19.history.label.empty.provider":"未知",
+ "panel.health.covid19.history.label.self_reported.title":"自报症状",
+ "panel.health.covid19.history.label.self_reported.symptoms":"症状: ",
+ "panel.health.covid19.history.label.contact_trace.title":"接触者追踪",
+ "panel.health.covid19.history.label.contact_trace.details":"接触者追踪: ",
+ "panel.health.covid19.history.label.action.title":"需要採取的行動",
+ "panel.health.covid19.history.label.action.details":"行動:",
+ "panel.health.covid19.history.label.result.title":"结果: ",
+ "panel.health.covid19.history.label.location.title":"测试位置",
+ "panel.health.covid19.history.label.technician_name.title":"技术人员姓名",
+ "panel.health.covid19.history.label.technician_id.title":"技术人员 ID",
+ "panel.health.covid19.history.label.more_info.title":"更多信息",
+ "panel.health.covid19.history.label.provider.self_reported": "自我报告",
+ "panel.health.covid19.history.label.verified": "已验证",
+ "panel.health.covid19.history.label.verification_pending": "等待验证",
+ "panel.health.covid19.history.message.clear_failed": "无法清除COVID-19事件历史记录",
+ "panel.health.covid19.history.button.repost_history.title": "再次请求我的最新测试",
+ "panel.health.covid19.history.button.repost_history.hint": "",
+ "panel.health.covid19.history.message.request_tests": "您的请求已提.你应该在一小时内收到你的最新测验",
+
+ "panel.health.covid19.qr_code.label.qr_image_label": "安全伊利诺伊COVID-19码",
+ "panel.health.covid19.qr_code.label.save_error": "无法保存二维码.",
+ "panel.health.covid19.qr_code.button.continue.hint": "",
+ "panel.health.covid19.qr_code.primary.heading.title": "你的COVID-19加密密钥",
+ "panel.health.covid19.qr_code.primary.description.1": "为了您的隐私,您用于COVID-19功能的医疗数据是加密的。加密密钥存储在您的手机本地,以确保其安全。\n\n要在其他设备上使用COVID-19功能,您需要使用下面的二维码手动传输此加密密钥.",
+ "panel.health.covid19.qr_code.primary.button.save.title": "保存加密密钥",
+ "panel.health.covid19.qr_code.primary.button.save.hint": "",
+ "panel.health.covid19.qr_code.primary.description.2": "如果您当前的设备丢失或损坏,我们建议您将此二维码的副本保存到云照片存储服务中,以便在您的替换设备上检索 \n\n您可以在任何时候通过 \"传输你的COVID-19加密密钥\" 从COVID-19信息中心中.",
+ "panel.health.covid19.qr_code.secondary.heading.title": "看起来你以前在其他设备上使用过此功能",
+ "panel.health.covid19.qr_code.secondary.description.1": "是否要将QR加密密钥传输到此设备以检索以前的健康信息?\n\n请在下面选择适用于您的选项。您可以在以后使用COVID-19信息中心或您的应用程序设置中的“传送您的COVID-19加密密钥”将QR加密密钥传输到此设备.",
+ "panel.health.covid19.qr_code.secondary.button.scan.heading": "如果要添加第二台设备:",
+ "panel.health.covid19.qr_code.secondary.button.scan.description": "如果您仍然可以访问您的主设备,您可以直接扫描该设备上的COVID-19加密密钥二维码.",
+ "panel.health.covid19.qr_code.secondary.button.scan.title": "扫描二维码",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.heading": "如果您使用的是替换设备:",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.description": "如果您不再能够访问您的主要设备,但将您的二维码保存到云照片服务,您可以通过从照片中检索来传输您的COVID-19加密密钥.",
+ "panel.health.covid19.qr_code.secondary.button.retrieve.title": "检索您的二维码",
+ "panel.health.covid19.qr_code.reset.button.heading": "重置我的COVID-19机密二维码:",
+ "panel.health.covid19.qr_code.reset.button.title": "重置我的COVID-19机密二维码",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.title": "重置我的COVID-19机密二维码",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.description": "这样做将为您提供一个新的COVID-19秘密二维码,但您以前的COVID-19事件历史记录将丢失,是否继续?",
+ "panel.health.covid19.qr_code.dialog.refresh_qr_code.confirm": "您确定吗?",
+
+ "panel.health.covid19.alert.no_qr_code.msg": "没有二维码",
+ "panel.health.covid19.alert.save.success.msg": "二维码保存成功 ",
+ "panel.health.covid19.alert.save.success.pictures": "图片",
+ "panel.health.covid19.alert.save.success.gallery": "图片库",
+ "panel.health.covid19.alert.save.fail.msg": "未能在图片库中保存二维码",
+ "panel.health.covid19.alert.qr_code.scan.failed.msg": "读取二维码失败.",
+ "panel.health.covid19.alert.qr_code.invalid.msg": "无效的二维码.",
+ "panel.health.covid19.alert.qr_code.not_match.msg": "COVID-19密钥与现有公钥RSA密钥不匹配.",
+ "panel.health.covid19.qr_code.alert.qr_code.transfer.succeeded.msg": "COVID-19机密传输成功.",
+ "panel.health.covid19.alert.qr_code.transfer.failed.msg": "无法传送COVID-19机密.",
+ "panel.health.covid19.qr_code.button.continue.title": "继续",
+ "panel.health.covid19.qr_code.button.transfer_later.title": "稍后转移",
+
+ "panel.health.report_test.heading.title":"手动输入结果",
+ "panel.health.report_test.label.date":"测试日期和时间",
+ "panel.health.report_test.label.provider":"医疗保健提供者",
+ "panel.health.report_test.label.date.location":"测试位置",
+ "panel.health.report_test.label.location.empty":"选择位置…",
+ "panel.health.report_test.label.type":"测试类型",
+ "panel.health.report_test.label.result":"结果",
+ "panel.health.report_test.label.result.empty":"选择测试结果…",
+ "panel.health.report_test.label.image":"添加测试结果",
+ "panel.health.report_test.label.image.hint":"上载测试结果的图像.",
+ "panel.health.report_test.button.add_image.title":"上传图片",
+ "panel.health.report_test.button.add_test.title":"添加测试",
+ "panel.health.report_test.button.close.title":"关闭",
+ "panel.health.report_test.button.retake.title":"重拍",
+ "panel.health.report_test.label.select_photo":"选择照片",
+ "panel.health.report_test.label.select_photo.description":"请为测试结果拍照或从库中选择",
+ "panel.health.report_test.button.take_photo":"拍照",
+ "panel.health.report_test.button.select_gallery":"从库中选择",
+ "panel.health.report_test.button.cancel":"取消",
+ "panel.health.report_test.error.create.message":"无法创建测试",
+ "panel.health.report_test.missing.image.message":"请上传图片",
+ "panel.health.report_test.future_date.forbidden.message": "今后不能提交测试",
+
+ "widget.health.onboarding.indicator9.label.hint": "Covid-19入职流程",
+
+ "widget.card.button.favorite.on.title":"添加到收藏夹",
+ "widget.card.button.favorite.on.hint":"",
+ "widget.card.button.favorite.off.title":"从收藏夹中删除",
+ "widget.card.button.favorite.off.hint":"",
+ "widget.card.label.interests":"由于您对以下内容感兴趣:",
+ "widget.card.label.converge":"比赛",
+
+ "panel.health.status_update.heading.title":"状态更新",
+ "panel.health.status_update.label.status_change":"根据您的结果,您的状态已从 ",
+ "panel.health.status_update.label.status_change.to":" 到 ",
+ "panel.health.status_update.label.next_steps":"下一步",
+ "panel.health.status_update.label.asap":"尽快",
+ "panel.health.status_update.button.find_location.title":"查找位置",
+ "panel.health.status_update.label.loading":"我们正在更新您的状态,请稍等",
+ "panel.health.status_update.button.continue.title":"查看下一步",
+ "panel.health.status_update.button.continue.hint":"",
+ "panel.health.status_update.info_dialog.label1": "状态颜色定义可能根据不同的县而变化.",
+ "panel.health.status_update.info_dialog.label2": "状态颜色 ",
+ "panel.health.status_update.info_dialog.label3": "Default status for new users is set to Orange.",
+ "panel.health.status_update.info_dialog.label4": "An up-to-date on-campus negative test result will reset your COVID-19 status to Yellow, and Building Entry will change to Granted.",
+ "panel.health.status_update.label.reason.title":"状态已更改,因为:",
+ "panel.health.status_update.label.reason.result":"结果:",
+ "panel.health.status_update.label.reason.symptoms.title":"你报告了新的症状",
+ "panel.health.status_update.label.reason.exposed.title": "你接触过可能被感染的人",
+ "panel.health.status_update.label.reason.exposure.detail": "暴露时间: ",
+ "panel.health.status_update.label.reason.action.title": "卫生部门要求你采取行动",
+ "panel.health.status_update.label.reason.action.detail": "待办行动: ",
+
+ "panel.health.next_steps.button.continue.title.find_locatio": "查找位置",
+ "panel.health.next_steps.button.continue.title.care_team": "联系护理团队",
+ "panel.health.next_steps.label.next_steps": "下一步行动",
+ "panel.health.next_steps.label.asap": "尽快",
+
+ "panel.health.symptoms.heading.title":"你有这些症状吗?",
+ "panel.health.symptoms.label.error.loading":"未能加载症状.",
+ "panel.health.symptoms.button.submit.title":"提交",
+ "panel.health.symptoms.label.success.submit.message":"你的症状已经得到处理.",
+ "panel.health.symptoms.label.error.submit":"未能提交症状.",
+
+ "widget.home_campus_tools.header.my_illini.title":"我的ILLINI",
+ "widget.home_campus_tools.label.campus_tools":"校园资源",
+ "widget.home_campus_tools.button.events.title":"活动",
+ "widget.home_campus_tools.button.events.hint":"",
+ "widget.home_campus_tools.button.dining.title":"用餐",
+ "widget.home_campus_tools.button.dining.hint":"",
+ "widget.home_campus_tools.button.athletics.title":"体育",
+ "widget.home_campus_tools.button.athletics.hint":"",
+ "widget.home_campus_tools.button.illini_cash.title":"Illini现金",
+ "widget.home_campus_tools.button.illini_cash.hint":"",
+ "widget.home_campus_tools.button.my_illini.title":"我的Illini",
+ "widget.home_campus_tools.button.my_illini.hint":"",
+ "widget.home_campus_tools.button.covid19.title":"COVID-19",
+ "widget.home_campus_tools.button.covid19.hint":"",
+
+ "widget.home_covid19_info.label.covid19": "COVID-19信息",
+ "widget.home_covid19_info.label.info.title": "幫助保持伊利諾伊州的安全",
+ "widget.home_covid19_info.label.info.description": "通過跟踪和管理您的健康來加入對抗COVID-19的鬥爭",
+
+ "widget.covid19_news_card.read_more.hint":"双击可阅读更多内容",
+
+ "model.explore.time.today":"今天",
+ "model.explore.time.tomorrow":"明天",
+ "model.explore.time.at":"在",
+ "model.explore.time.all_day": "一整天",
+
+ "model.user.role.student.title": "學生",
+ "model.user.role.employee.title": "僱員",
+ "model.user.role.resident.title": "居民",
+
+ "model.covid19.status.color.green": "安全",
+ "model.covid19.status.color.yellow": "警告",
+ "model.covid19.status.color.orange": "可能感染",
+ "model.covid19.status.color.red": "已感染",
+
+ "model.covid19.step.initial": "與您的醫療保健提供者一起進行COVID-19測試",
+
+ "logic.date_time.greeting.morning": "早上好",
+ "logic.date_time.greeting.afternoon": "下午好",
+ "logic.date_time.greeting.evening": "晚上好",
+
+ "logic.polls.unable_to_load_poll": "无法加载投票",
+ "logic.polls.no_polls_with_pin": "此4位数的投票代码 # 没有投票",
+ "logic.polls.multiple_polls_with_pin": "有多个公开的投票关联到这个四位数投票代码 #",
+
+ "logic.general.internal_error": "发生内部错误",
+ "logic.general.invalid_response": "无效的服务器响应",
+ "logic.general.response_error": "响应错误: %s %s",
+
+ "app.exit_dialog.message":"确定要退出吗?",
+
+ "app.common.heading.hint":"标题",
+ "app.common.heading.one.hint":"标题1",
+ "app.common.heading.two.hint":"标题2",
+ "app.common.heading.three.hint":"标题3",
+
+ "app.common.label.cancelled":"取消",
+ "app.common.label.other": "其他",
+ "app.common.label.county": "县",
+ "app.common.label.read_more": "阅读更多",
+
+ "app.common.yes":"是",
+ "app.common.no":"否",
+
+ "com.illinois.features2.entry.disable_location_awareness":"禁用地点感知",
+ "com.illinois.features2.entry.dont_store_location":"不要存储的我的地点",
+ "com.illinois.features2.entry.remove_preferences":"删除偏好设置",
+ "com.illinois.features2.entry.log_out":"登出",
+ "com.illinois.features2.entry.disable_notifications":"关闭提示",
+ "com.illinois.features2.entry.remove_credit_card":"删除信用卡信息",
+ "com.illinois.features2.entry.dont_share_location":"不和其他人分享我的数据",
+
+ "com.illinois.covid19.status.long.orange": "Orange, Test Required",
+ "com.illinois.covid19.status.long.green": "綠色,最近抗體",
+ "com.illinois.covid19.status.long.yellow": "黃色,最近的陰性測試",
+ "com.illinois.covid19.status.long.red": "紅色,陽性測試",
+ "com.illinois.covid19.status.long.no change": "无变化",
+ "com.illinois.covid19.status.type.orange": "橙色",
+ "com.illinois.covid19.status.type.green": "绿色",
+ "com.illinois.covid19.status.type.yellow": "黄色",
+ "com.illinois.covid19.status.type.red": "红色",
+ "com.illinois.covid19.status.type.no change": "无变化",
+ "com.illinois.covid19.status.description.orange": "Test Required",
+ "com.illinois.covid19.status.description.green": "最近抗體",
+ "com.illinois.covid19.status.description.yellow": "最近的陰性測試",
+ "com.illinois.covid19.status.description.red": "正面測試",
+ "com.illinois.covid19.status.description.no change": "",
+ "com.illinois.covid19.status.info.description.orange": "Orange: First time user, Past due for test, Self-reported symptoms, Received exposure notification or Quarantined",
+ "com.illinois.covid19.status.info.description.green": "Green: Recent antibodies",
+ "com.illinois.covid19.status.info.description.yellow": "Yellow: Recent negative test",
+ "com.illinois.covid19.status.info.description.red": "Red: Positive test"
+}
diff --git a/assets/styles.json b/assets/styles.json
new file mode 100644
index 00000000..bfd4217e
--- /dev/null
+++ b/assets/styles.json
@@ -0,0 +1,77 @@
+{
+ "color": {
+ "fillColorPrimary" :"#002855",
+ "fillColorPrimaryTransparent03" :"#4D002855",
+ "fillColorPrimaryTransparent05" :"#80002855",
+ "fillColorPrimaryTransparent09" :"#E6002855",
+ "fillColorPrimaryTransparent015" :"#26002855",
+ "fillColorPrimaryTransparent80" :"#CC002855",
+ "textColorPrimary" :"#FFFFFF",
+ "fillColorPrimaryVariant" :"#0F2040",
+ "fillColorSecondary" :"#E84A27",
+ "fillColorSecondaryTransparent05" :"#80E84A27",
+ "textColorSecondary" :"#FFFFFF",
+ "fillColorSecondaryVariant" :"#CF3C1B",
+ "textColorSecondaryVariant" :"#FFFFFF",
+
+ "surface" :"#FFFFFF",
+ "surfaceAccent" :"#DADDE1",
+ "background" :"#F5F5F5",
+ "backgroundVariant" :"#E8E9EA",
+ "textSurface" :"#404040",
+ "textSurfaceAccent" :"#404040",
+ "textBackground" :"#404040",
+ "textbackgroundVariant" :"#404040",
+
+ "accentColor1" :"#E84A27",
+ "accentColor2" :"#5FA7A3",
+ "accentColor3" :"#5182CF",
+
+ "iconColor" :"#E84A27",
+
+ "eventColor" :"#E54B30",
+ "diningColor" :"#F09842",
+ "placeColor" :"#62A7A3",
+
+ "white" :"#FFFFFF",
+ "whiteTransparent01" :"#1AFFFFFF",
+ "whiteTransparent06" :"#99ffffff",
+ "blackTransparent06" :"#99000000",
+ "blackTransparent018" :"#30000000",
+
+ "mediumGray" :"#717372",
+ "mediumGray1" :"#535353",
+ "mediumGray2" :"#979797",
+ "lightGray" :"#EDEDED",
+ "disabledTextColor" :"#BDBDBD",
+ "disabledTextColorTwo" :"#868F9D",
+
+ "healthStatusGreen" :"#1ACD00",
+ "healthStatusYellow" :"#FFCF1C",
+ "healthStatusOrange" :"#F29835",
+ "healthStatusRed" :"#FF4F4F",
+
+ "lightBlue" :"#42b9ea"
+ },
+ "font_family": {
+ "black": "ProximaNovaBlack",
+ "black_italic": "ProximaNovaBlackIt",
+ "bold": "ProximaNovaBold",
+ "bold_italic": "ProximaNovaBoldIt",
+ "extra_bold": "ProximaNovaExtraBold",
+ "extra_bold_italic": "ProximaNovaExtraBoldIt",
+ "light": "ProximaNovaLight",
+ "light_italic": "ProximaNovaLightIt",
+ "medium": "ProximaNovaMedium",
+ "medium_italic": "ProximaNovaMediumIt",
+ "regular": "ProximaNovaRegular",
+ "regular_italic": "ProximaNovaRegularIt",
+ "semi_bold": "ProximaNovaSemiBold",
+ "semi_bold_italic": "ProximaNovaSemiBoldIt",
+ "thin": "ProximaNovaThin",
+ "thin_italic": "ProximaNovaThinIt"
+ },
+ "text_style": {
+ "header_bar": { "font_family": "ProximaNovaExtraBold", "size": 16.0, "color": "textColorPrimary"}
+ }
+}
\ No newline at end of file
diff --git a/assets/styles/styles-illinois.json b/assets/styles/styles-illinois.json
new file mode 100644
index 00000000..9cbf49e0
--- /dev/null
+++ b/assets/styles/styles-illinois.json
@@ -0,0 +1,77 @@
+{
+ "color": {
+ "fillColorPrimary" :"#002855",
+ "fillColorPrimaryTransparent03" :"#4D002855",
+ "fillColorPrimaryTransparent05" :"#80002855",
+ "fillColorPrimaryTransparent09" :"#E6002855",
+ "fillColorPrimaryTransparent015" :"#26002855",
+ "fillColorPrimaryTransparent80" :"#CC002855",
+ "textColorPrimary" :"#FFFFFF",
+ "fillColorPrimaryVariant" :"#0F2040",
+ "fillColorSecondary" :"#E84A27",
+ "fillColorSecondaryTransparent05" :"#80E84A27",
+ "textColorSecondary" :"#FFFFFF",
+ "fillColorSecondaryVariant" :"#CF3C1B",
+ "textColorSecondaryVariant" :"#FFFFFF",
+
+ "surface" :"#FFFFFF",
+ "surfaceAccent" :"#DADDE1",
+ "background" :"#F5F5F5",
+ "backgroundVariant" :"#E8E9EA",
+ "textSurface" :"#404040",
+ "textSurfaceAccent" :"#404040",
+ "textBackground" :"#404040",
+ "textbackgroundVariant" :"#404040",
+
+ "accentColor1" :"#E84A27",
+ "accentColor2" :"#5FA7A3",
+ "accentColor3" :"#5182CF",
+
+ "iconColor" :"#E84A27",
+
+ "eventColor" :"#E54B30",
+ "diningColor" :"#F09842",
+ "placeColor" :"#62A7A3",
+
+ "white" :"#FFFFFF",
+ "whiteTransparent01" :"#1AFFFFFF",
+ "whiteTransparent06" :"#99ffffff",
+ "blackTransparent06" :"#99000000",
+ "blackTransparent018" :"#30000000",
+
+ "mediumGray" :"#717372",
+ "mediumGray1" :"#535353",
+ "mediumGray2" :"#979797",
+ "lightGray" :"#EDEDED",
+ "disabledTextColor" :"#BDBDBD",
+ "disabledTextColorTwo" :"#868F9D",
+
+ "healthStatusGreen" :"#1ACD00",
+ "healthStatusYellow" :"#FFCF1C",
+ "healthStatusOrange" :"#E84A27",
+ "healthStatusRed" :"#FF4F4F",
+
+ "lightBlue" :"#42b9ea"
+ },
+ "font_family": {
+ "black": "ProximaNovaBlack",
+ "black_italic": "ProximaNovaBlackIt",
+ "bold": "ProximaNovaBold",
+ "bold_italic": "ProximaNovaBoldIt",
+ "extra_bold": "ProximaNovaExtraBold",
+ "extra_bold_italic": "ProximaNovaExtraBoldIt",
+ "light": "ProximaNovaLight",
+ "light_italic": "ProximaNovaLightIt",
+ "medium": "ProximaNovaMedium",
+ "medium_italic": "ProximaNovaMediumIt",
+ "regular": "ProximaNovaRegular",
+ "regular_italic": "ProximaNovaRegularIt",
+ "semi_bold": "ProximaNovaSemiBold",
+ "semi_bold_italic": "ProximaNovaSemiBoldIt",
+ "thin": "ProximaNovaThin",
+ "thin_italic": "ProximaNovaThinIt"
+ },
+ "text_style": {
+ "header_bar": { "font_family": "ProximaNovaExtraBold", "size": 16.0, "color": "textColorPrimary"}
+ }
+}
\ No newline at end of file
diff --git a/assets/styles/styles-purdue.json b/assets/styles/styles-purdue.json
new file mode 100644
index 00000000..08906edc
--- /dev/null
+++ b/assets/styles/styles-purdue.json
@@ -0,0 +1,76 @@
+{
+ "color": {
+ "fillColorPrimary" : "#191919",
+ "fillColorPrimaryTransparent03" : "#4D191919",
+ "fillColorPrimaryTransparent05" : "#80191919",
+ "fillColorPrimaryTransparent09" : "#E6191919",
+ "fillColorPrimaryTransparent015" : "#26191919",
+ "fillColorPrimaryTransparent80" : "#CC191919",
+ "textColorPrimary" : "#FFFFFF",
+ "fillColorPrimaryVariant" : "#000000",
+ "textColorPrimaryVariant" : "#FFFFFF",
+ "fillColorSecondary" : "#CFB991",
+ "fillColorSecondaryTransparent05" : "#80CFB991",
+ "textColorSecondary" : "#FFFFFF",
+ "fillColorSecondaryVariant" : "#76592C",
+ "textColorSecondaryVariant" : "#FFFFFF",
+
+ "surface" : "#FFFFFF",
+ "surfaceAccent" : "#DADDE1",
+ "background" : "#F5F5F5",
+ "backgroundVariant" : "#E8E9EA",
+ "textSurface" : "#404040",
+ "textSurfaceAccent" : "#404040",
+ "textBackground" : "#404040",
+ "textbackgroundVariant" : "#404040",
+
+ "accentColor1" : "#CFB991",
+ "accentColor2" : "#CFB991",
+ "accentColor3" : "#CFB991",
+
+ "iconColor" : "#CFB991",
+
+ "eventColor" : "#CFB991",
+ "diningColor" : "#CFB991",
+ "placeColor" : "#CFB991",
+
+ "white" : "#FFFFFF",
+ "whiteTransparent01" : "#1AFFFFFF",
+ "whiteTransparent06" : "#99ffffff",
+ "blackTransparent06" : "#99000000",
+ "blackTransparent018" : "#30000000",
+
+ "mediumGray" : "#717372",
+ "lightGray" : "#EDEDED",
+ "disabledTextColor" : "#BDBDBD",
+ "disabledTextColorTwo" : "#868F9D",
+
+ "healthStatusGreen" :"#1ACD00",
+ "healthStatusYellow" :"#FFCF1C",
+ "healthStatusOrange" :"#E84A27",
+ "healthStatusRed" :"#FF4F4F",
+
+ "lightBlue" :"#42b9ea"
+ },
+ "font_family": {
+ "black": "ProximaNovaBlack",
+ "black_italic": "ProximaNovaBlackIt",
+ "bold": "ProximaNovaBold",
+ "bold_italic": "ProximaNovaBoldIt",
+ "extra_bold": "ProximaNovaExtraBold",
+ "extra_bold_italic": "ProximaNovaExtraBoldIt",
+ "light": "ProximaNovaLight",
+ "light_italic": "ProximaNovaLightIt",
+ "medium": "ProximaNovaMedium",
+ "medium_italic": "ProximaNovaMediumIt",
+ "regular": "ProximaNovaRegular",
+ "regular_italic": "ProximaNovaRegularIt",
+ "semi_bold": "ProximaNovaSemiBold",
+ "semi_bold_italic": "ProximaNovaSemiBoldIt",
+ "thin": "ProximaNovaThin",
+ "thin_italic": "ProximaNovaThinIt"
+ },
+ "text_style": {
+ "header_bar": { "font_family": "ProximaNovaExtraBold", "size": 16.0, "color": "textColorPrimary"}
+ }
+}
diff --git a/assets/styles/styles-uic.json b/assets/styles/styles-uic.json
new file mode 100644
index 00000000..94f2dca9
--- /dev/null
+++ b/assets/styles/styles-uic.json
@@ -0,0 +1,77 @@
+{
+ "color": {
+ "fillColorPrimary" :"#001e62",
+ "fillColorPrimaryTransparent03" :"#4D001e62",
+ "fillColorPrimaryTransparent05" :"#80001e62",
+ "fillColorPrimaryTransparent09" :"#E6001e62",
+ "fillColorPrimaryTransparent015" :"#26001e62",
+ "fillColorPrimaryTransparent80" :"#CC001e62",
+ "textColorPrimary" :"#FFFFFF",
+ "fillColorPrimaryVariant" :"#D50032",
+ "fillColorSecondary" :"#D50032",
+ "fillColorSecondaryTransparent05" :"#80D50032",
+ "textColorSecondary" :"#FFFFFF",
+ "fillColorSecondaryVariant" :"#B00029",
+ "textColorSecondaryVariant" :"#FFFFFF",
+
+ "surface" :"#FFFFFF",
+ "surfaceAccent" :"#DADDE1",
+ "background" :"#F5F5F5",
+ "backgroundVariant" :"#E8E9EA",
+ "textSurface" :"#404040",
+ "textSurfaceAccent" :"#404040",
+ "textBackground" :"#404040",
+ "textbackgroundVariant" :"#404040",
+
+ "accentColor1" :"#D50032",
+ "accentColor2" :"#00AEC7",
+ "accentColor3" :"#BB16A3",
+
+ "iconColor" :"#D50032",
+
+ "eventColor" :"#D50032",
+ "diningColor" :"#FF7500",
+ "placeColor" :"#00AEC7",
+
+ "white" :"#FFFFFF",
+ "whiteTransparent01" :"#1AFFFFFF",
+ "whiteTransparent06" :"#99ffffff",
+ "blackTransparent06" :"#99000000",
+ "blackTransparent018" :"#30000000",
+
+ "mediumGray" :"#717372",
+ "mediumGray1" :"#535353",
+ "mediumGray2" :"#979797",
+ "lightGray" :"#EDEDED",
+ "disabledTextColor" :"#BDBDBD",
+ "disabledTextColorTwo" :"#868F9D",
+
+ "healthStatusGreen" :"#1ACD00",
+ "healthStatusYellow" :"#FFCF1C",
+ "healthStatusOrange" :"#E84A27",
+ "healthStatusRed" :"#FF4F4F",
+
+ "lightBlue" :"#42b9ea"
+ },
+ "font_family": {
+ "black": "ProximaNovaBlack",
+ "black_italic": "ProximaNovaBlackIt",
+ "bold": "ProximaNovaBold",
+ "bold_italic": "ProximaNovaBoldIt",
+ "extra_bold": "ProximaNovaExtraBold",
+ "extra_bold_italic": "ProximaNovaExtraBoldIt",
+ "light": "ProximaNovaLight",
+ "light_italic": "ProximaNovaLightIt",
+ "medium": "ProximaNovaMedium",
+ "medium_italic": "ProximaNovaMediumIt",
+ "regular": "ProximaNovaRegular",
+ "regular_italic": "ProximaNovaRegularIt",
+ "semi_bold": "ProximaNovaSemiBold",
+ "semi_bold_italic": "ProximaNovaSemiBoldIt",
+ "thin": "ProximaNovaThin",
+ "thin_italic": "ProximaNovaThinIt"
+ },
+ "text_style": {
+ "header_bar": { "font_family": "ProximaNovaExtraBold", "size": 16.0, "color": "textColorPrimary"}
+ }
+}
\ No newline at end of file
diff --git a/assets/timezone2019a.tzf b/assets/timezone2019a.tzf
new file mode 100644
index 00000000..3e73678e
Binary files /dev/null and b/assets/timezone2019a.tzf differ
diff --git a/fonts/proximanova-black.otf b/fonts/proximanova-black.otf
new file mode 100755
index 00000000..0728bea1
Binary files /dev/null and b/fonts/proximanova-black.otf differ
diff --git a/fonts/proximanova-blackit.otf b/fonts/proximanova-blackit.otf
new file mode 100755
index 00000000..87fca717
Binary files /dev/null and b/fonts/proximanova-blackit.otf differ
diff --git a/fonts/proximanova-bold.otf b/fonts/proximanova-bold.otf
new file mode 100755
index 00000000..d16406eb
Binary files /dev/null and b/fonts/proximanova-bold.otf differ
diff --git a/fonts/proximanova-boldit.otf b/fonts/proximanova-boldit.otf
new file mode 100755
index 00000000..ec72f555
Binary files /dev/null and b/fonts/proximanova-boldit.otf differ
diff --git a/fonts/proximanova-extrabold.otf b/fonts/proximanova-extrabold.otf
new file mode 100755
index 00000000..cc2b8c8e
Binary files /dev/null and b/fonts/proximanova-extrabold.otf differ
diff --git a/fonts/proximanova-extraboldit.otf b/fonts/proximanova-extraboldit.otf
new file mode 100755
index 00000000..650c93fc
Binary files /dev/null and b/fonts/proximanova-extraboldit.otf differ
diff --git a/fonts/proximanova-light.otf b/fonts/proximanova-light.otf
new file mode 100755
index 00000000..2cf99c4c
Binary files /dev/null and b/fonts/proximanova-light.otf differ
diff --git a/fonts/proximanova-lightit.otf b/fonts/proximanova-lightit.otf
new file mode 100755
index 00000000..23f79ea9
Binary files /dev/null and b/fonts/proximanova-lightit.otf differ
diff --git a/fonts/proximanova-medium.otf b/fonts/proximanova-medium.otf
new file mode 100755
index 00000000..3f59ea93
Binary files /dev/null and b/fonts/proximanova-medium.otf differ
diff --git a/fonts/proximanova-mediumit.otf b/fonts/proximanova-mediumit.otf
new file mode 100755
index 00000000..4ff652d8
Binary files /dev/null and b/fonts/proximanova-mediumit.otf differ
diff --git a/fonts/proximanova-regular.otf b/fonts/proximanova-regular.otf
new file mode 100755
index 00000000..56bbf66e
Binary files /dev/null and b/fonts/proximanova-regular.otf differ
diff --git a/fonts/proximanova-regularit.otf b/fonts/proximanova-regularit.otf
new file mode 100755
index 00000000..2edb54d1
Binary files /dev/null and b/fonts/proximanova-regularit.otf differ
diff --git a/fonts/proximanova-semibold.otf b/fonts/proximanova-semibold.otf
new file mode 100755
index 00000000..5436663e
Binary files /dev/null and b/fonts/proximanova-semibold.otf differ
diff --git a/fonts/proximanova-semiboldit.otf b/fonts/proximanova-semiboldit.otf
new file mode 100755
index 00000000..81ef5236
Binary files /dev/null and b/fonts/proximanova-semiboldit.otf differ
diff --git a/fonts/proximanova-thin.otf b/fonts/proximanova-thin.otf
new file mode 100755
index 00000000..4ade8624
Binary files /dev/null and b/fonts/proximanova-thin.otf differ
diff --git a/fonts/proximanova-thinit.otf b/fonts/proximanova-thinit.otf
new file mode 100755
index 00000000..721cbbc5
Binary files /dev/null and b/fonts/proximanova-thinit.otf differ
diff --git a/illini-client.iml b/illini-client.iml
new file mode 100644
index 00000000..7d003501
--- /dev/null
+++ b/illini-client.iml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/images/2.0x/add-to-apple-wallet.png b/images/2.0x/add-to-apple-wallet.png
new file mode 100644
index 00000000..4507957c
Binary files /dev/null and b/images/2.0x/add-to-apple-wallet.png differ
diff --git a/images/2.0x/allow-notifications-header.png b/images/2.0x/allow-notifications-header.png
new file mode 100644
index 00000000..c03f55b5
Binary files /dev/null and b/images/2.0x/allow-notifications-header.png differ
diff --git a/images/2.0x/athletics-baseball-orange.png b/images/2.0x/athletics-baseball-orange.png
new file mode 100644
index 00000000..5364edbc
Binary files /dev/null and b/images/2.0x/athletics-baseball-orange.png differ
diff --git a/images/2.0x/athletics-baseball-white.png b/images/2.0x/athletics-baseball-white.png
new file mode 100644
index 00000000..855e6475
Binary files /dev/null and b/images/2.0x/athletics-baseball-white.png differ
diff --git a/images/2.0x/athletics-basketball-orange.png b/images/2.0x/athletics-basketball-orange.png
new file mode 100644
index 00000000..30d21ffe
Binary files /dev/null and b/images/2.0x/athletics-basketball-orange.png differ
diff --git a/images/2.0x/athletics-basketball-white.png b/images/2.0x/athletics-basketball-white.png
new file mode 100644
index 00000000..9cf08d02
Binary files /dev/null and b/images/2.0x/athletics-basketball-white.png differ
diff --git a/images/2.0x/athletics-cross-orange.png b/images/2.0x/athletics-cross-orange.png
new file mode 100644
index 00000000..45bc6a97
Binary files /dev/null and b/images/2.0x/athletics-cross-orange.png differ
diff --git a/images/2.0x/athletics-cross-white.png b/images/2.0x/athletics-cross-white.png
new file mode 100644
index 00000000..a00d77dd
Binary files /dev/null and b/images/2.0x/athletics-cross-white.png differ
diff --git a/images/2.0x/athletics-football-orange.png b/images/2.0x/athletics-football-orange.png
new file mode 100644
index 00000000..f88f021c
Binary files /dev/null and b/images/2.0x/athletics-football-orange.png differ
diff --git a/images/2.0x/athletics-football-white.png b/images/2.0x/athletics-football-white.png
new file mode 100644
index 00000000..f943dee4
Binary files /dev/null and b/images/2.0x/athletics-football-white.png differ
diff --git a/images/2.0x/athletics-golf-orange.png b/images/2.0x/athletics-golf-orange.png
new file mode 100644
index 00000000..1c2d7abc
Binary files /dev/null and b/images/2.0x/athletics-golf-orange.png differ
diff --git a/images/2.0x/athletics-golf-white.png b/images/2.0x/athletics-golf-white.png
new file mode 100644
index 00000000..3ff21303
Binary files /dev/null and b/images/2.0x/athletics-golf-white.png differ
diff --git a/images/2.0x/athletics-gymnastics-orange.png b/images/2.0x/athletics-gymnastics-orange.png
new file mode 100644
index 00000000..b17cfdef
Binary files /dev/null and b/images/2.0x/athletics-gymnastics-orange.png differ
diff --git a/images/2.0x/athletics-gymnastics-white.png b/images/2.0x/athletics-gymnastics-white.png
new file mode 100644
index 00000000..56f1514c
Binary files /dev/null and b/images/2.0x/athletics-gymnastics-white.png differ
diff --git a/images/2.0x/athletics-handball-orange.png b/images/2.0x/athletics-handball-orange.png
new file mode 100644
index 00000000..7647280b
Binary files /dev/null and b/images/2.0x/athletics-handball-orange.png differ
diff --git a/images/2.0x/athletics-handball-white.png b/images/2.0x/athletics-handball-white.png
new file mode 100644
index 00000000..e040aec5
Binary files /dev/null and b/images/2.0x/athletics-handball-white.png differ
diff --git a/images/2.0x/athletics-soccer-orange.png b/images/2.0x/athletics-soccer-orange.png
new file mode 100644
index 00000000..6ab04519
Binary files /dev/null and b/images/2.0x/athletics-soccer-orange.png differ
diff --git a/images/2.0x/athletics-soccer-white.png b/images/2.0x/athletics-soccer-white.png
new file mode 100644
index 00000000..d392fb8f
Binary files /dev/null and b/images/2.0x/athletics-soccer-white.png differ
diff --git a/images/2.0x/athletics-softball-orange.png b/images/2.0x/athletics-softball-orange.png
new file mode 100644
index 00000000..5364edbc
Binary files /dev/null and b/images/2.0x/athletics-softball-orange.png differ
diff --git a/images/2.0x/athletics-swim-orange.png b/images/2.0x/athletics-swim-orange.png
new file mode 100644
index 00000000..8950ac6b
Binary files /dev/null and b/images/2.0x/athletics-swim-orange.png differ
diff --git a/images/2.0x/athletics-swim-white.png b/images/2.0x/athletics-swim-white.png
new file mode 100644
index 00000000..7fa58d1e
Binary files /dev/null and b/images/2.0x/athletics-swim-white.png differ
diff --git a/images/2.0x/athletics-tennis-orange.png b/images/2.0x/athletics-tennis-orange.png
new file mode 100644
index 00000000..bb50da7f
Binary files /dev/null and b/images/2.0x/athletics-tennis-orange.png differ
diff --git a/images/2.0x/athletics-tennis-white.png b/images/2.0x/athletics-tennis-white.png
new file mode 100644
index 00000000..0cd373d4
Binary files /dev/null and b/images/2.0x/athletics-tennis-white.png differ
diff --git a/images/2.0x/athletics-track-orange.png b/images/2.0x/athletics-track-orange.png
new file mode 100644
index 00000000..f761b5ca
Binary files /dev/null and b/images/2.0x/athletics-track-orange.png differ
diff --git a/images/2.0x/athletics-track-white.png b/images/2.0x/athletics-track-white.png
new file mode 100644
index 00000000..963d33c1
Binary files /dev/null and b/images/2.0x/athletics-track-white.png differ
diff --git a/images/2.0x/athletics-volleyball-orange.png b/images/2.0x/athletics-volleyball-orange.png
new file mode 100644
index 00000000..7647280b
Binary files /dev/null and b/images/2.0x/athletics-volleyball-orange.png differ
diff --git a/images/2.0x/athletics-wrestling-orange.png b/images/2.0x/athletics-wrestling-orange.png
new file mode 100644
index 00000000..80cbfd42
Binary files /dev/null and b/images/2.0x/athletics-wrestling-orange.png differ
diff --git a/images/2.0x/athletics-wrestling-white.png b/images/2.0x/athletics-wrestling-white.png
new file mode 100644
index 00000000..a9f54515
Binary files /dev/null and b/images/2.0x/athletics-wrestling-white.png differ
diff --git a/images/2.0x/background-image.png b/images/2.0x/background-image.png
new file mode 100644
index 00000000..4cc419f7
Binary files /dev/null and b/images/2.0x/background-image.png differ
diff --git a/images/2.0x/background-onboarding-squares-dark.png b/images/2.0x/background-onboarding-squares-dark.png
new file mode 100644
index 00000000..30b5e9a1
Binary files /dev/null and b/images/2.0x/background-onboarding-squares-dark.png differ
diff --git a/images/2.0x/background-onboarding-squares-light.png b/images/2.0x/background-onboarding-squares-light.png
new file mode 100644
index 00000000..48761305
Binary files /dev/null and b/images/2.0x/background-onboarding-squares-light.png differ
diff --git a/images/2.0x/background-onboarding-squares.png b/images/2.0x/background-onboarding-squares.png
new file mode 100644
index 00000000..a160aea6
Binary files /dev/null and b/images/2.0x/background-onboarding-squares.png differ
diff --git a/images/2.0x/button-plus-orange.png b/images/2.0x/button-plus-orange.png
new file mode 100755
index 00000000..ab8717be
Binary files /dev/null and b/images/2.0x/button-plus-orange.png differ
diff --git a/images/2.0x/campus-tools-blue.png b/images/2.0x/campus-tools-blue.png
new file mode 100644
index 00000000..904ae3ba
Binary files /dev/null and b/images/2.0x/campus-tools-blue.png differ
diff --git a/images/2.0x/campus-tools.png b/images/2.0x/campus-tools.png
new file mode 100644
index 00000000..c5461d5d
Binary files /dev/null and b/images/2.0x/campus-tools.png differ
diff --git a/images/2.0x/certified-copy.png b/images/2.0x/certified-copy.png
new file mode 100644
index 00000000..0a7bd989
Binary files /dev/null and b/images/2.0x/certified-copy.png differ
diff --git a/images/2.0x/certified.png b/images/2.0x/certified.png
new file mode 100644
index 00000000..0fde9b92
Binary files /dev/null and b/images/2.0x/certified.png differ
diff --git a/images/2.0x/checkbox-selected.png b/images/2.0x/checkbox-selected.png
new file mode 100644
index 00000000..4aaf4cf7
Binary files /dev/null and b/images/2.0x/checkbox-selected.png differ
diff --git a/images/2.0x/checkbox-small.png b/images/2.0x/checkbox-small.png
new file mode 100644
index 00000000..29b57cbd
Binary files /dev/null and b/images/2.0x/checkbox-small.png differ
diff --git a/images/2.0x/checkbox-unselected.png b/images/2.0x/checkbox-unselected.png
new file mode 100644
index 00000000..84e4a257
Binary files /dev/null and b/images/2.0x/checkbox-unselected.png differ
diff --git a/images/2.0x/chevron-blue-right.png b/images/2.0x/chevron-blue-right.png
new file mode 100755
index 00000000..3030e730
Binary files /dev/null and b/images/2.0x/chevron-blue-right.png differ
diff --git a/images/2.0x/chevron-down.png b/images/2.0x/chevron-down.png
new file mode 100644
index 00000000..8013508f
Binary files /dev/null and b/images/2.0x/chevron-down.png differ
diff --git a/images/2.0x/chevron-left-blue.png b/images/2.0x/chevron-left-blue.png
new file mode 100755
index 00000000..45120d51
Binary files /dev/null and b/images/2.0x/chevron-left-blue.png differ
diff --git a/images/2.0x/chevron-left-white.png b/images/2.0x/chevron-left-white.png
new file mode 100644
index 00000000..7e44fde2
Binary files /dev/null and b/images/2.0x/chevron-left-white.png differ
diff --git a/images/2.0x/chevron-left.png b/images/2.0x/chevron-left.png
new file mode 100644
index 00000000..b2943f8c
Binary files /dev/null and b/images/2.0x/chevron-left.png differ
diff --git a/images/2.0x/chevron-right.png b/images/2.0x/chevron-right.png
new file mode 100755
index 00000000..78dbb6a1
Binary files /dev/null and b/images/2.0x/chevron-right.png differ
diff --git a/images/2.0x/chevron-up.png b/images/2.0x/chevron-up.png
new file mode 100644
index 00000000..b1b1d33c
Binary files /dev/null and b/images/2.0x/chevron-up.png differ
diff --git a/images/2.0x/chevron2-down.png b/images/2.0x/chevron2-down.png
new file mode 100755
index 00000000..c8f49edd
Binary files /dev/null and b/images/2.0x/chevron2-down.png differ
diff --git a/images/2.0x/classic-meal-blue.png b/images/2.0x/classic-meal-blue.png
new file mode 100644
index 00000000..354ecc2d
Binary files /dev/null and b/images/2.0x/classic-meal-blue.png differ
diff --git a/images/2.0x/classic-meal-orange.png b/images/2.0x/classic-meal-orange.png
new file mode 100755
index 00000000..79be1370
Binary files /dev/null and b/images/2.0x/classic-meal-orange.png differ
diff --git a/images/2.0x/close-blue.png b/images/2.0x/close-blue.png
new file mode 100644
index 00000000..ee65956f
Binary files /dev/null and b/images/2.0x/close-blue.png differ
diff --git a/images/2.0x/close-gray.png b/images/2.0x/close-gray.png
new file mode 100644
index 00000000..a91b8ebb
Binary files /dev/null and b/images/2.0x/close-gray.png differ
diff --git a/images/2.0x/close-menu.png b/images/2.0x/close-menu.png
new file mode 100644
index 00000000..27593249
Binary files /dev/null and b/images/2.0x/close-menu.png differ
diff --git a/images/2.0x/close-orange-large.png b/images/2.0x/close-orange-large.png
new file mode 100644
index 00000000..d168859f
Binary files /dev/null and b/images/2.0x/close-orange-large.png differ
diff --git a/images/2.0x/close-orange.png b/images/2.0x/close-orange.png
new file mode 100755
index 00000000..95dcccba
Binary files /dev/null and b/images/2.0x/close-orange.png differ
diff --git a/images/2.0x/close-white-large.png b/images/2.0x/close-white-large.png
new file mode 100644
index 00000000..29ce3829
Binary files /dev/null and b/images/2.0x/close-white-large.png differ
diff --git a/images/2.0x/close-white-shadow.png b/images/2.0x/close-white-shadow.png
new file mode 100644
index 00000000..816df667
Binary files /dev/null and b/images/2.0x/close-white-shadow.png differ
diff --git a/images/2.0x/close-white.png b/images/2.0x/close-white.png
new file mode 100644
index 00000000..d829ad29
Binary files /dev/null and b/images/2.0x/close-white.png differ
diff --git a/images/2.0x/covid.png b/images/2.0x/covid.png
new file mode 100644
index 00000000..e3c04d2b
Binary files /dev/null and b/images/2.0x/covid.png differ
diff --git a/images/2.0x/covid19-header-blue.png b/images/2.0x/covid19-header-blue.png
new file mode 100644
index 00000000..44ce5775
Binary files /dev/null and b/images/2.0x/covid19-header-blue.png differ
diff --git a/images/2.0x/covid19-orange.png b/images/2.0x/covid19-orange.png
new file mode 100644
index 00000000..a1a9d7b5
Binary files /dev/null and b/images/2.0x/covid19-orange.png differ
diff --git a/images/2.0x/deselected-dark.png b/images/2.0x/deselected-dark.png
new file mode 100755
index 00000000..b842b046
Binary files /dev/null and b/images/2.0x/deselected-dark.png differ
diff --git a/images/2.0x/deselected.png b/images/2.0x/deselected.png
new file mode 100755
index 00000000..63508353
Binary files /dev/null and b/images/2.0x/deselected.png differ
diff --git a/images/2.0x/disabled.png b/images/2.0x/disabled.png
new file mode 100755
index 00000000..6fd05d2a
Binary files /dev/null and b/images/2.0x/disabled.png differ
diff --git a/images/2.0x/emotional.png b/images/2.0x/emotional.png
new file mode 100644
index 00000000..ebafdead
Binary files /dev/null and b/images/2.0x/emotional.png differ
diff --git a/images/2.0x/enable-bluetooth-header.png b/images/2.0x/enable-bluetooth-header.png
new file mode 100644
index 00000000..c00597b6
Binary files /dev/null and b/images/2.0x/enable-bluetooth-header.png differ
diff --git a/images/2.0x/environmental.png b/images/2.0x/environmental.png
new file mode 100644
index 00000000..3f13f494
Binary files /dev/null and b/images/2.0x/environmental.png differ
diff --git a/images/2.0x/example.png b/images/2.0x/example.png
new file mode 100644
index 00000000..a54a8e2f
Binary files /dev/null and b/images/2.0x/example.png differ
diff --git a/images/2.0x/explore.png b/images/2.0x/explore.png
new file mode 100644
index 00000000..f69df057
Binary files /dev/null and b/images/2.0x/explore.png differ
diff --git a/images/2.0x/external-link.png b/images/2.0x/external-link.png
new file mode 100644
index 00000000..22f79032
Binary files /dev/null and b/images/2.0x/external-link.png differ
diff --git a/images/2.0x/fb-10x20.png b/images/2.0x/fb-10x20.png
new file mode 100644
index 00000000..ea8a48d4
Binary files /dev/null and b/images/2.0x/fb-10x20.png differ
diff --git a/images/2.0x/fb-12x24.png b/images/2.0x/fb-12x24.png
new file mode 100644
index 00000000..cf3d97c3
Binary files /dev/null and b/images/2.0x/fb-12x24.png differ
diff --git a/images/2.0x/fb-16x32.png b/images/2.0x/fb-16x32.png
new file mode 100755
index 00000000..c8a059fc
Binary files /dev/null and b/images/2.0x/fb-16x32.png differ
diff --git a/images/2.0x/fill-1.png b/images/2.0x/fill-1.png
new file mode 100644
index 00000000..26bb03ea
Binary files /dev/null and b/images/2.0x/fill-1.png differ
diff --git a/images/2.0x/financial-2.png b/images/2.0x/financial-2.png
new file mode 100644
index 00000000..67d031d9
Binary files /dev/null and b/images/2.0x/financial-2.png differ
diff --git a/images/2.0x/game_day_blue.png b/images/2.0x/game_day_blue.png
new file mode 100644
index 00000000..405369b3
Binary files /dev/null and b/images/2.0x/game_day_blue.png differ
diff --git a/images/2.0x/globe.png b/images/2.0x/globe.png
new file mode 100644
index 00000000..f738fe4b
Binary files /dev/null and b/images/2.0x/globe.png differ
diff --git a/images/2.0x/group-10.png b/images/2.0x/group-10.png
new file mode 100644
index 00000000..c89d48d1
Binary files /dev/null and b/images/2.0x/group-10.png differ
diff --git a/images/2.0x/group-15.png b/images/2.0x/group-15.png
new file mode 100644
index 00000000..f33933bd
Binary files /dev/null and b/images/2.0x/group-15.png differ
diff --git a/images/2.0x/group-16.png b/images/2.0x/group-16.png
new file mode 100644
index 00000000..f75c4121
Binary files /dev/null and b/images/2.0x/group-16.png differ
diff --git a/images/2.0x/group-18.png b/images/2.0x/group-18.png
new file mode 100644
index 00000000..14b2fbd8
Binary files /dev/null and b/images/2.0x/group-18.png differ
diff --git a/images/2.0x/group-2.png b/images/2.0x/group-2.png
new file mode 100644
index 00000000..14fdafb1
Binary files /dev/null and b/images/2.0x/group-2.png differ
diff --git a/images/2.0x/group-20.png b/images/2.0x/group-20.png
new file mode 100644
index 00000000..13fd53aa
Binary files /dev/null and b/images/2.0x/group-20.png differ
diff --git a/images/2.0x/group-25.png b/images/2.0x/group-25.png
new file mode 100644
index 00000000..1cc8dede
Binary files /dev/null and b/images/2.0x/group-25.png differ
diff --git a/images/2.0x/group-28.png b/images/2.0x/group-28.png
new file mode 100644
index 00000000..95c018b6
Binary files /dev/null and b/images/2.0x/group-28.png differ
diff --git a/images/2.0x/group-3.png b/images/2.0x/group-3.png
new file mode 100644
index 00000000..f64cb356
Binary files /dev/null and b/images/2.0x/group-3.png differ
diff --git a/images/2.0x/group-4.png b/images/2.0x/group-4.png
new file mode 100644
index 00000000..cdcc66e6
Binary files /dev/null and b/images/2.0x/group-4.png differ
diff --git a/images/2.0x/group-44.png b/images/2.0x/group-44.png
new file mode 100644
index 00000000..84a5b5c8
Binary files /dev/null and b/images/2.0x/group-44.png differ
diff --git a/images/2.0x/group-444.png b/images/2.0x/group-444.png
new file mode 100644
index 00000000..1abd6bc3
Binary files /dev/null and b/images/2.0x/group-444.png differ
diff --git a/images/2.0x/group-5-blue.png b/images/2.0x/group-5-blue.png
new file mode 100644
index 00000000..3330e9d2
Binary files /dev/null and b/images/2.0x/group-5-blue.png differ
diff --git a/images/2.0x/group-5-white.png b/images/2.0x/group-5-white.png
new file mode 100644
index 00000000..a07e7626
Binary files /dev/null and b/images/2.0x/group-5-white.png differ
diff --git a/images/2.0x/group-7.png b/images/2.0x/group-7.png
new file mode 100644
index 00000000..18237f0f
Binary files /dev/null and b/images/2.0x/group-7.png differ
diff --git a/images/2.0x/group-8.png b/images/2.0x/group-8.png
new file mode 100644
index 00000000..115f557d
Binary files /dev/null and b/images/2.0x/group-8.png differ
diff --git a/images/2.0x/group-9.png b/images/2.0x/group-9.png
new file mode 100644
index 00000000..8b7f1111
Binary files /dev/null and b/images/2.0x/group-9.png differ
diff --git a/images/2.0x/group-event-settings.png b/images/2.0x/group-event-settings.png
new file mode 100644
index 00000000..e4c726ee
Binary files /dev/null and b/images/2.0x/group-event-settings.png differ
diff --git a/images/2.0x/group-settings-icon.png b/images/2.0x/group-settings-icon.png
new file mode 100644
index 00000000..e4c726ee
Binary files /dev/null and b/images/2.0x/group-settings-icon.png differ
diff --git a/images/2.0x/group.png b/images/2.0x/group.png
new file mode 100644
index 00000000..47201148
Binary files /dev/null and b/images/2.0x/group.png differ
diff --git a/images/2.0x/happening.png b/images/2.0x/happening.png
new file mode 100644
index 00000000..d07e462f
Binary files /dev/null and b/images/2.0x/happening.png differ
diff --git a/images/2.0x/icon-add-14x14.png b/images/2.0x/icon-add-14x14.png
new file mode 100644
index 00000000..d784d25f
Binary files /dev/null and b/images/2.0x/icon-add-14x14.png differ
diff --git a/images/2.0x/icon-add-20x18.png b/images/2.0x/icon-add-20x18.png
new file mode 100644
index 00000000..72e66e08
Binary files /dev/null and b/images/2.0x/icon-add-20x18.png differ
diff --git a/images/2.0x/icon-all-set-header.png b/images/2.0x/icon-all-set-header.png
new file mode 100644
index 00000000..efe6a0a9
Binary files /dev/null and b/images/2.0x/icon-all-set-header.png differ
diff --git a/images/2.0x/icon-athletics-blue.png b/images/2.0x/icon-athletics-blue.png
new file mode 100644
index 00000000..d47dcd56
Binary files /dev/null and b/images/2.0x/icon-athletics-blue.png differ
diff --git a/images/2.0x/icon-athletics.png b/images/2.0x/icon-athletics.png
new file mode 100644
index 00000000..cafd8b04
Binary files /dev/null and b/images/2.0x/icon-athletics.png differ
diff --git a/images/2.0x/icon-avatar-placeholder.png b/images/2.0x/icon-avatar-placeholder.png
new file mode 100644
index 00000000..adca818d
Binary files /dev/null and b/images/2.0x/icon-avatar-placeholder.png differ
diff --git a/images/2.0x/icon-badge.png b/images/2.0x/icon-badge.png
new file mode 100644
index 00000000..5381608b
Binary files /dev/null and b/images/2.0x/icon-badge.png differ
diff --git a/images/2.0x/icon-big-onboarding-health.png b/images/2.0x/icon-big-onboarding-health.png
new file mode 100644
index 00000000..4b37f2bd
Binary files /dev/null and b/images/2.0x/icon-big-onboarding-health.png differ
diff --git a/images/2.0x/icon-big-onboarding-privacy.png b/images/2.0x/icon-big-onboarding-privacy.png
new file mode 100644
index 00000000..25a61559
Binary files /dev/null and b/images/2.0x/icon-big-onboarding-privacy.png differ
diff --git a/images/2.0x/icon-bluetooth.png b/images/2.0x/icon-bluetooth.png
new file mode 100644
index 00000000..6995ee54
Binary files /dev/null and b/images/2.0x/icon-bluetooth.png differ
diff --git a/images/2.0x/icon-browse-athletics.png b/images/2.0x/icon-browse-athletics.png
new file mode 100644
index 00000000..34749d3d
Binary files /dev/null and b/images/2.0x/icon-browse-athletics.png differ
diff --git a/images/2.0x/icon-browse-covid19.png b/images/2.0x/icon-browse-covid19.png
new file mode 100644
index 00000000..96b62c27
Binary files /dev/null and b/images/2.0x/icon-browse-covid19.png differ
diff --git a/images/2.0x/icon-browse-dinings.png b/images/2.0x/icon-browse-dinings.png
new file mode 100644
index 00000000..fc6d6bb6
Binary files /dev/null and b/images/2.0x/icon-browse-dinings.png differ
diff --git a/images/2.0x/icon-browse-events.png b/images/2.0x/icon-browse-events.png
new file mode 100644
index 00000000..6c3bfa06
Binary files /dev/null and b/images/2.0x/icon-browse-events.png differ
diff --git a/images/2.0x/icon-browse-quick-polls.png b/images/2.0x/icon-browse-quick-polls.png
new file mode 100644
index 00000000..0f2a351d
Binary files /dev/null and b/images/2.0x/icon-browse-quick-polls.png differ
diff --git a/images/2.0x/icon-browse-saved.png b/images/2.0x/icon-browse-saved.png
new file mode 100644
index 00000000..cfd55050
Binary files /dev/null and b/images/2.0x/icon-browse-saved.png differ
diff --git a/images/2.0x/icon-browse-wellness.png b/images/2.0x/icon-browse-wellness.png
new file mode 100644
index 00000000..980febd1
Binary files /dev/null and b/images/2.0x/icon-browse-wellness.png differ
diff --git a/images/2.0x/icon-browse.png b/images/2.0x/icon-browse.png
new file mode 100755
index 00000000..7310657f
Binary files /dev/null and b/images/2.0x/icon-browse.png differ
diff --git a/images/2.0x/icon-calendar.png b/images/2.0x/icon-calendar.png
new file mode 100755
index 00000000..7807542b
Binary files /dev/null and b/images/2.0x/icon-calendar.png differ
diff --git a/images/2.0x/icon-campus-tools-athletics.png b/images/2.0x/icon-campus-tools-athletics.png
new file mode 100755
index 00000000..f3ff0ea7
Binary files /dev/null and b/images/2.0x/icon-campus-tools-athletics.png differ
diff --git a/images/2.0x/icon-campus-tools-dining.png b/images/2.0x/icon-campus-tools-dining.png
new file mode 100755
index 00000000..0545de41
Binary files /dev/null and b/images/2.0x/icon-campus-tools-dining.png differ
diff --git a/images/2.0x/icon-campus-tools-events.png b/images/2.0x/icon-campus-tools-events.png
new file mode 100755
index 00000000..3f5602ea
Binary files /dev/null and b/images/2.0x/icon-campus-tools-events.png differ
diff --git a/images/2.0x/icon-campus-tools-illini-cash.png b/images/2.0x/icon-campus-tools-illini-cash.png
new file mode 100755
index 00000000..b3ded404
Binary files /dev/null and b/images/2.0x/icon-campus-tools-illini-cash.png differ
diff --git a/images/2.0x/icon-campus-tools-laundry.png b/images/2.0x/icon-campus-tools-laundry.png
new file mode 100755
index 00000000..1d2b7877
Binary files /dev/null and b/images/2.0x/icon-campus-tools-laundry.png differ
diff --git a/images/2.0x/icon-campus-tools.png b/images/2.0x/icon-campus-tools.png
new file mode 100755
index 00000000..be783fa6
Binary files /dev/null and b/images/2.0x/icon-campus-tools.png differ
diff --git a/images/2.0x/icon-campus-updates.png b/images/2.0x/icon-campus-updates.png
new file mode 100644
index 00000000..56213ac6
Binary files /dev/null and b/images/2.0x/icon-campus-updates.png differ
diff --git a/images/2.0x/icon-cellphone.png b/images/2.0x/icon-cellphone.png
new file mode 100644
index 00000000..fdbdc723
Binary files /dev/null and b/images/2.0x/icon-cellphone.png differ
diff --git a/images/2.0x/icon-certified.png b/images/2.0x/icon-certified.png
new file mode 100755
index 00000000..0fde9b92
Binary files /dev/null and b/images/2.0x/icon-certified.png differ
diff --git a/images/2.0x/icon-check-example.png b/images/2.0x/icon-check-example.png
new file mode 100755
index 00000000..fe388fd1
Binary files /dev/null and b/images/2.0x/icon-check-example.png differ
diff --git a/images/2.0x/icon-check-simple.png b/images/2.0x/icon-check-simple.png
new file mode 100755
index 00000000..a2947581
Binary files /dev/null and b/images/2.0x/icon-check-simple.png differ
diff --git a/images/2.0x/icon-check.png b/images/2.0x/icon-check.png
new file mode 100644
index 00000000..1e40d475
Binary files /dev/null and b/images/2.0x/icon-check.png differ
diff --git a/images/2.0x/icon-circle-close.png b/images/2.0x/icon-circle-close.png
new file mode 100755
index 00000000..d829ad29
Binary files /dev/null and b/images/2.0x/icon-circle-close.png differ
diff --git a/images/2.0x/icon-close-big.png b/images/2.0x/icon-close-big.png
new file mode 100755
index 00000000..67c61b61
Binary files /dev/null and b/images/2.0x/icon-close-big.png differ
diff --git a/images/2.0x/icon-comment-dots.png b/images/2.0x/icon-comment-dots.png
new file mode 100644
index 00000000..1563b738
Binary files /dev/null and b/images/2.0x/icon-comment-dots.png differ
diff --git a/images/2.0x/icon-copy.png b/images/2.0x/icon-copy.png
new file mode 100644
index 00000000..8a746452
Binary files /dev/null and b/images/2.0x/icon-copy.png differ
diff --git a/images/2.0x/icon-cost.png b/images/2.0x/icon-cost.png
new file mode 100644
index 00000000..8dc7d168
Binary files /dev/null and b/images/2.0x/icon-cost.png differ
diff --git a/images/2.0x/icon-country-guidelines.png b/images/2.0x/icon-country-guidelines.png
new file mode 100644
index 00000000..0e01a527
Binary files /dev/null and b/images/2.0x/icon-country-guidelines.png differ
diff --git a/images/2.0x/icon-create-event.png b/images/2.0x/icon-create-event.png
new file mode 100755
index 00000000..635640e1
Binary files /dev/null and b/images/2.0x/icon-create-event.png differ
diff --git a/images/2.0x/icon-credit.png b/images/2.0x/icon-credit.png
new file mode 100755
index 00000000..87781bca
Binary files /dev/null and b/images/2.0x/icon-credit.png differ
diff --git a/images/2.0x/icon-deselected-checkbox.png b/images/2.0x/icon-deselected-checkbox.png
new file mode 100644
index 00000000..38824250
Binary files /dev/null and b/images/2.0x/icon-deselected-checkbox.png differ
diff --git a/images/2.0x/icon-dining-orange.png b/images/2.0x/icon-dining-orange.png
new file mode 100644
index 00000000..bb440cf9
Binary files /dev/null and b/images/2.0x/icon-dining-orange.png differ
diff --git a/images/2.0x/icon-dining-yellow.png b/images/2.0x/icon-dining-yellow.png
new file mode 100755
index 00000000..1d9f1756
Binary files /dev/null and b/images/2.0x/icon-dining-yellow.png differ
diff --git a/images/2.0x/icon-dining.png b/images/2.0x/icon-dining.png
new file mode 100644
index 00000000..0308ffc0
Binary files /dev/null and b/images/2.0x/icon-dining.png differ
diff --git a/images/2.0x/icon-down-orange.png b/images/2.0x/icon-down-orange.png
new file mode 100755
index 00000000..1f8ab81c
Binary files /dev/null and b/images/2.0x/icon-down-orange.png differ
diff --git a/images/2.0x/icon-down.png b/images/2.0x/icon-down.png
new file mode 100755
index 00000000..b6a8cfe7
Binary files /dev/null and b/images/2.0x/icon-down.png differ
diff --git a/images/2.0x/icon-dryer-big.png b/images/2.0x/icon-dryer-big.png
new file mode 100755
index 00000000..63053a08
Binary files /dev/null and b/images/2.0x/icon-dryer-big.png differ
diff --git a/images/2.0x/icon-dryer-small.png b/images/2.0x/icon-dryer-small.png
new file mode 100755
index 00000000..5b25b2b4
Binary files /dev/null and b/images/2.0x/icon-dryer-small.png differ
diff --git a/images/2.0x/icon-edit.png b/images/2.0x/icon-edit.png
new file mode 100644
index 00000000..866de36d
Binary files /dev/null and b/images/2.0x/icon-edit.png differ
diff --git a/images/2.0x/icon-event.png b/images/2.0x/icon-event.png
new file mode 100644
index 00000000..02c0b45c
Binary files /dev/null and b/images/2.0x/icon-event.png differ
diff --git a/images/2.0x/icon-explore-campus-athletics.png b/images/2.0x/icon-explore-campus-athletics.png
new file mode 100644
index 00000000..f3ff0ea7
Binary files /dev/null and b/images/2.0x/icon-explore-campus-athletics.png differ
diff --git a/images/2.0x/icon-explore-campus-dining.png b/images/2.0x/icon-explore-campus-dining.png
new file mode 100644
index 00000000..0545de41
Binary files /dev/null and b/images/2.0x/icon-explore-campus-dining.png differ
diff --git a/images/2.0x/icon-explore-campus-events.png b/images/2.0x/icon-explore-campus-events.png
new file mode 100644
index 00000000..3f5602ea
Binary files /dev/null and b/images/2.0x/icon-explore-campus-events.png differ
diff --git a/images/2.0x/icon-explore.png b/images/2.0x/icon-explore.png
new file mode 100644
index 00000000..f69df057
Binary files /dev/null and b/images/2.0x/icon-explore.png differ
diff --git a/images/2.0x/icon-face-mask.png b/images/2.0x/icon-face-mask.png
new file mode 100644
index 00000000..b287ed71
Binary files /dev/null and b/images/2.0x/icon-face-mask.png differ
diff --git a/images/2.0x/icon-feedback.png b/images/2.0x/icon-feedback.png
new file mode 100755
index 00000000..6a76f0a5
Binary files /dev/null and b/images/2.0x/icon-feedback.png differ
diff --git a/images/2.0x/icon-gear.png b/images/2.0x/icon-gear.png
new file mode 100755
index 00000000..9b6c265e
Binary files /dev/null and b/images/2.0x/icon-gear.png differ
diff --git a/images/2.0x/icon-health.png b/images/2.0x/icon-health.png
new file mode 100644
index 00000000..bc117fc2
Binary files /dev/null and b/images/2.0x/icon-health.png differ
diff --git a/images/2.0x/icon-hospital.png b/images/2.0x/icon-hospital.png
new file mode 100644
index 00000000..525ad708
Binary files /dev/null and b/images/2.0x/icon-hospital.png differ
diff --git a/images/2.0x/icon-identity.png b/images/2.0x/icon-identity.png
new file mode 100644
index 00000000..04f452fd
Binary files /dev/null and b/images/2.0x/icon-identity.png differ
diff --git a/images/2.0x/icon-illini-cash.png b/images/2.0x/icon-illini-cash.png
new file mode 100755
index 00000000..684930f2
Binary files /dev/null and b/images/2.0x/icon-illini-cash.png differ
diff --git a/images/2.0x/icon-info-orange.png b/images/2.0x/icon-info-orange.png
new file mode 100644
index 00000000..b3e5f9dc
Binary files /dev/null and b/images/2.0x/icon-info-orange.png differ
diff --git a/images/2.0x/icon-key.png b/images/2.0x/icon-key.png
new file mode 100644
index 00000000..75090dee
Binary files /dev/null and b/images/2.0x/icon-key.png differ
diff --git a/images/2.0x/icon-list-view.png b/images/2.0x/icon-list-view.png
new file mode 100644
index 00000000..818d1eac
Binary files /dev/null and b/images/2.0x/icon-list-view.png differ
diff --git a/images/2.0x/icon-listen.png b/images/2.0x/icon-listen.png
new file mode 100755
index 00000000..b4ad9a86
Binary files /dev/null and b/images/2.0x/icon-listen.png differ
diff --git a/images/2.0x/icon-live-stats.png b/images/2.0x/icon-live-stats.png
new file mode 100755
index 00000000..66ed2072
Binary files /dev/null and b/images/2.0x/icon-live-stats.png differ
diff --git a/images/2.0x/icon-location-1.png b/images/2.0x/icon-location-1.png
new file mode 100644
index 00000000..9dda0e17
Binary files /dev/null and b/images/2.0x/icon-location-1.png differ
diff --git a/images/2.0x/icon-location.png b/images/2.0x/icon-location.png
new file mode 100644
index 00000000..014b092d
Binary files /dev/null and b/images/2.0x/icon-location.png differ
diff --git a/images/2.0x/icon-map-view.png b/images/2.0x/icon-map-view.png
new file mode 100644
index 00000000..eb402cc7
Binary files /dev/null and b/images/2.0x/icon-map-view.png differ
diff --git a/images/2.0x/icon-member.png b/images/2.0x/icon-member.png
new file mode 100644
index 00000000..a4c4258b
Binary files /dev/null and b/images/2.0x/icon-member.png differ
diff --git a/images/2.0x/icon-more-info.png b/images/2.0x/icon-more-info.png
new file mode 100755
index 00000000..b4398d3e
Binary files /dev/null and b/images/2.0x/icon-more-info.png differ
diff --git a/images/2.0x/icon-my-illini.png b/images/2.0x/icon-my-illini.png
new file mode 100755
index 00000000..ef5a5c6c
Binary files /dev/null and b/images/2.0x/icon-my-illini.png differ
diff --git a/images/2.0x/icon-near-you.png b/images/2.0x/icon-near-you.png
new file mode 100755
index 00000000..c9c932ac
Binary files /dev/null and b/images/2.0x/icon-near-you.png differ
diff --git a/images/2.0x/icon-news.png b/images/2.0x/icon-news.png
new file mode 100755
index 00000000..1c3896bf
Binary files /dev/null and b/images/2.0x/icon-news.png differ
diff --git a/images/2.0x/icon-notifications-blue.png b/images/2.0x/icon-notifications-blue.png
new file mode 100644
index 00000000..67a8a9ac
Binary files /dev/null and b/images/2.0x/icon-notifications-blue.png differ
diff --git a/images/2.0x/icon-orange-i.png b/images/2.0x/icon-orange-i.png
new file mode 100644
index 00000000..eefb99e7
Binary files /dev/null and b/images/2.0x/icon-orange-i.png differ
diff --git a/images/2.0x/icon-parking.png b/images/2.0x/icon-parking.png
new file mode 100755
index 00000000..05b14b13
Binary files /dev/null and b/images/2.0x/icon-parking.png differ
diff --git a/images/2.0x/icon-passport.png b/images/2.0x/icon-passport.png
new file mode 100644
index 00000000..512e184e
Binary files /dev/null and b/images/2.0x/icon-passport.png differ
diff --git a/images/2.0x/icon-payment-type-apple-pay.png b/images/2.0x/icon-payment-type-apple-pay.png
new file mode 100644
index 00000000..fc9ddbc5
Binary files /dev/null and b/images/2.0x/icon-payment-type-apple-pay.png differ
diff --git a/images/2.0x/icon-payment-type-cache.png b/images/2.0x/icon-payment-type-cache.png
new file mode 100644
index 00000000..06e1198c
Binary files /dev/null and b/images/2.0x/icon-payment-type-cache.png differ
diff --git a/images/2.0x/icon-payment-type-cafe-credit.png b/images/2.0x/icon-payment-type-cafe-credit.png
new file mode 100644
index 00000000..106e2c1e
Binary files /dev/null and b/images/2.0x/icon-payment-type-cafe-credit.png differ
diff --git a/images/2.0x/icon-payment-type-classic-meal.png b/images/2.0x/icon-payment-type-classic-meal.png
new file mode 100644
index 00000000..2d4bd63e
Binary files /dev/null and b/images/2.0x/icon-payment-type-classic-meal.png differ
diff --git a/images/2.0x/icon-payment-type-credit-card.png b/images/2.0x/icon-payment-type-credit-card.png
new file mode 100644
index 00000000..c2616b7d
Binary files /dev/null and b/images/2.0x/icon-payment-type-credit-card.png differ
diff --git a/images/2.0x/icon-payment-type-google-pay.png b/images/2.0x/icon-payment-type-google-pay.png
new file mode 100644
index 00000000..c761f096
Binary files /dev/null and b/images/2.0x/icon-payment-type-google-pay.png differ
diff --git a/images/2.0x/icon-payment-type-ilini-cash.png b/images/2.0x/icon-payment-type-ilini-cash.png
new file mode 100644
index 00000000..39745b24
Binary files /dev/null and b/images/2.0x/icon-payment-type-ilini-cash.png differ
diff --git a/images/2.0x/icon-persona-alumni-normal.png b/images/2.0x/icon-persona-alumni-normal.png
new file mode 100644
index 00000000..0e128218
Binary files /dev/null and b/images/2.0x/icon-persona-alumni-normal.png differ
diff --git a/images/2.0x/icon-persona-alumni-selected.png b/images/2.0x/icon-persona-alumni-selected.png
new file mode 100644
index 00000000..65493fa3
Binary files /dev/null and b/images/2.0x/icon-persona-alumni-selected.png differ
diff --git a/images/2.0x/icon-persona-athletics-normal.png b/images/2.0x/icon-persona-athletics-normal.png
new file mode 100644
index 00000000..931bf12b
Binary files /dev/null and b/images/2.0x/icon-persona-athletics-normal.png differ
diff --git a/images/2.0x/icon-persona-athletics-selected.png b/images/2.0x/icon-persona-athletics-selected.png
new file mode 100644
index 00000000..bf8efd29
Binary files /dev/null and b/images/2.0x/icon-persona-athletics-selected.png differ
diff --git a/images/2.0x/icon-persona-employee-normal.png b/images/2.0x/icon-persona-employee-normal.png
new file mode 100644
index 00000000..178a3d7b
Binary files /dev/null and b/images/2.0x/icon-persona-employee-normal.png differ
diff --git a/images/2.0x/icon-persona-employee-selected.png b/images/2.0x/icon-persona-employee-selected.png
new file mode 100644
index 00000000..995a60dd
Binary files /dev/null and b/images/2.0x/icon-persona-employee-selected.png differ
diff --git a/images/2.0x/icon-persona-parent-normal.png b/images/2.0x/icon-persona-parent-normal.png
new file mode 100644
index 00000000..d9277df1
Binary files /dev/null and b/images/2.0x/icon-persona-parent-normal.png differ
diff --git a/images/2.0x/icon-persona-parent-selected.png b/images/2.0x/icon-persona-parent-selected.png
new file mode 100644
index 00000000..715c63b7
Binary files /dev/null and b/images/2.0x/icon-persona-parent-selected.png differ
diff --git a/images/2.0x/icon-persona-resident-normal.png b/images/2.0x/icon-persona-resident-normal.png
new file mode 100644
index 00000000..aeb541c6
Binary files /dev/null and b/images/2.0x/icon-persona-resident-normal.png differ
diff --git a/images/2.0x/icon-persona-resident-selected.png b/images/2.0x/icon-persona-resident-selected.png
new file mode 100644
index 00000000..8ae614d7
Binary files /dev/null and b/images/2.0x/icon-persona-resident-selected.png differ
diff --git a/images/2.0x/icon-persona-student-normal.png b/images/2.0x/icon-persona-student-normal.png
new file mode 100644
index 00000000..45e02d8a
Binary files /dev/null and b/images/2.0x/icon-persona-student-normal.png differ
diff --git a/images/2.0x/icon-persona-student-selected.png b/images/2.0x/icon-persona-student-selected.png
new file mode 100644
index 00000000..ceac1243
Binary files /dev/null and b/images/2.0x/icon-persona-student-selected.png differ
diff --git a/images/2.0x/icon-persona-visitor-normal.png b/images/2.0x/icon-persona-visitor-normal.png
new file mode 100644
index 00000000..53c0e73f
Binary files /dev/null and b/images/2.0x/icon-persona-visitor-normal.png differ
diff --git a/images/2.0x/icon-persona-visitor-selected.png b/images/2.0x/icon-persona-visitor-selected.png
new file mode 100644
index 00000000..bac78779
Binary files /dev/null and b/images/2.0x/icon-persona-visitor-selected.png differ
diff --git a/images/2.0x/icon-phone.png b/images/2.0x/icon-phone.png
new file mode 100644
index 00000000..4f555f6c
Binary files /dev/null and b/images/2.0x/icon-phone.png differ
diff --git a/images/2.0x/icon-placeholder-blue.png b/images/2.0x/icon-placeholder-blue.png
new file mode 100755
index 00000000..c245b44d
Binary files /dev/null and b/images/2.0x/icon-placeholder-blue.png differ
diff --git a/images/2.0x/icon-placeholder-empty.png b/images/2.0x/icon-placeholder-empty.png
new file mode 100755
index 00000000..225dc5da
Binary files /dev/null and b/images/2.0x/icon-placeholder-empty.png differ
diff --git a/images/2.0x/icon-placeholder-navy.png b/images/2.0x/icon-placeholder-navy.png
new file mode 100755
index 00000000..90abd486
Binary files /dev/null and b/images/2.0x/icon-placeholder-navy.png differ
diff --git a/images/2.0x/icon-placeholder-orange.png b/images/2.0x/icon-placeholder-orange.png
new file mode 100755
index 00000000..1e8eb435
Binary files /dev/null and b/images/2.0x/icon-placeholder-orange.png differ
diff --git a/images/2.0x/icon-placeholder-teal.png b/images/2.0x/icon-placeholder-teal.png
new file mode 100755
index 00000000..4056bd8d
Binary files /dev/null and b/images/2.0x/icon-placeholder-teal.png differ
diff --git a/images/2.0x/icon-placeholder-yellow.png b/images/2.0x/icon-placeholder-yellow.png
new file mode 100755
index 00000000..fd27e331
Binary files /dev/null and b/images/2.0x/icon-placeholder-yellow.png differ
diff --git a/images/2.0x/icon-plus.png b/images/2.0x/icon-plus.png
new file mode 100755
index 00000000..cd86d9ac
Binary files /dev/null and b/images/2.0x/icon-plus.png differ
diff --git a/images/2.0x/icon-poi.png b/images/2.0x/icon-poi.png
new file mode 100644
index 00000000..19adc0a9
Binary files /dev/null and b/images/2.0x/icon-poi.png differ
diff --git a/images/2.0x/icon-privacy.png b/images/2.0x/icon-privacy.png
new file mode 100755
index 00000000..5043a8d6
Binary files /dev/null and b/images/2.0x/icon-privacy.png differ
diff --git a/images/2.0x/icon-quickpoll.png b/images/2.0x/icon-quickpoll.png
new file mode 100755
index 00000000..66ed2072
Binary files /dev/null and b/images/2.0x/icon-quickpoll.png differ
diff --git a/images/2.0x/icon-recurring-event.png b/images/2.0x/icon-recurring-event.png
new file mode 100755
index 00000000..354e0168
Binary files /dev/null and b/images/2.0x/icon-recurring-event.png differ
diff --git a/images/2.0x/icon-reminder.png b/images/2.0x/icon-reminder.png
new file mode 100755
index 00000000..32fa2e85
Binary files /dev/null and b/images/2.0x/icon-reminder.png differ
diff --git a/images/2.0x/icon-report-test.png b/images/2.0x/icon-report-test.png
new file mode 100644
index 00000000..c4e84374
Binary files /dev/null and b/images/2.0x/icon-report-test.png differ
diff --git a/images/2.0x/icon-saved-white.png b/images/2.0x/icon-saved-white.png
new file mode 100755
index 00000000..d723d511
Binary files /dev/null and b/images/2.0x/icon-saved-white.png differ
diff --git a/images/2.0x/icon-saved.png b/images/2.0x/icon-saved.png
new file mode 100755
index 00000000..92482d52
Binary files /dev/null and b/images/2.0x/icon-saved.png differ
diff --git a/images/2.0x/icon-schedule.png b/images/2.0x/icon-schedule.png
new file mode 100755
index 00000000..304e05bf
Binary files /dev/null and b/images/2.0x/icon-schedule.png differ
diff --git a/images/2.0x/icon-search.png b/images/2.0x/icon-search.png
new file mode 100755
index 00000000..994cb22d
Binary files /dev/null and b/images/2.0x/icon-search.png differ
diff --git a/images/2.0x/icon-selected-checkbox.png b/images/2.0x/icon-selected-checkbox.png
new file mode 100644
index 00000000..659ea918
Binary files /dev/null and b/images/2.0x/icon-selected-checkbox.png differ
diff --git a/images/2.0x/icon-selected.png b/images/2.0x/icon-selected.png
new file mode 100755
index 00000000..9d2d6f17
Binary files /dev/null and b/images/2.0x/icon-selected.png differ
diff --git a/images/2.0x/icon-separate-people.png b/images/2.0x/icon-separate-people.png
new file mode 100644
index 00000000..8afd92dc
Binary files /dev/null and b/images/2.0x/icon-separate-people.png differ
diff --git a/images/2.0x/icon-settings.png b/images/2.0x/icon-settings.png
new file mode 100755
index 00000000..23780341
Binary files /dev/null and b/images/2.0x/icon-settings.png differ
diff --git a/images/2.0x/icon-social-distance.png b/images/2.0x/icon-social-distance.png
new file mode 100644
index 00000000..a939a002
Binary files /dev/null and b/images/2.0x/icon-social-distance.png differ
diff --git a/images/2.0x/icon-star-selected.png b/images/2.0x/icon-star-selected.png
new file mode 100755
index 00000000..3739ff68
Binary files /dev/null and b/images/2.0x/icon-star-selected.png differ
diff --git a/images/2.0x/icon-star-solid.png b/images/2.0x/icon-star-solid.png
new file mode 100755
index 00000000..9155c106
Binary files /dev/null and b/images/2.0x/icon-star-solid.png differ
diff --git a/images/2.0x/icon-star-white.png b/images/2.0x/icon-star-white.png
new file mode 100644
index 00000000..273d0e06
Binary files /dev/null and b/images/2.0x/icon-star-white.png differ
diff --git a/images/2.0x/icon-star.png b/images/2.0x/icon-star.png
new file mode 100644
index 00000000..50dcbd03
Binary files /dev/null and b/images/2.0x/icon-star.png differ
diff --git a/images/2.0x/icon-stay-at-home.png b/images/2.0x/icon-stay-at-home.png
new file mode 100644
index 00000000..472d2d1e
Binary files /dev/null and b/images/2.0x/icon-stay-at-home.png differ
diff --git a/images/2.0x/icon-stehoscope.png b/images/2.0x/icon-stehoscope.png
new file mode 100644
index 00000000..2b17299d
Binary files /dev/null and b/images/2.0x/icon-stehoscope.png differ
diff --git a/images/2.0x/icon-team.png b/images/2.0x/icon-team.png
new file mode 100755
index 00000000..e3bbfd42
Binary files /dev/null and b/images/2.0x/icon-team.png differ
diff --git a/images/2.0x/icon-test-history.png b/images/2.0x/icon-test-history.png
new file mode 100644
index 00000000..e1fa82d1
Binary files /dev/null and b/images/2.0x/icon-test-history.png differ
diff --git a/images/2.0x/icon-time.png b/images/2.0x/icon-time.png
new file mode 100644
index 00000000..dc8f99d1
Binary files /dev/null and b/images/2.0x/icon-time.png differ
diff --git a/images/2.0x/icon-unselected.png b/images/2.0x/icon-unselected.png
new file mode 100755
index 00000000..8ebaddbb
Binary files /dev/null and b/images/2.0x/icon-unselected.png differ
diff --git a/images/2.0x/icon-up.png b/images/2.0x/icon-up.png
new file mode 100755
index 00000000..5575e0dc
Binary files /dev/null and b/images/2.0x/icon-up.png differ
diff --git a/images/2.0x/icon-washer-big.png b/images/2.0x/icon-washer-big.png
new file mode 100755
index 00000000..eadc15e5
Binary files /dev/null and b/images/2.0x/icon-washer-big.png differ
diff --git a/images/2.0x/icon-washer-small.png b/images/2.0x/icon-washer-small.png
new file mode 100755
index 00000000..ccbd26a3
Binary files /dev/null and b/images/2.0x/icon-washer-small.png differ
diff --git a/images/2.0x/icon-washer.png b/images/2.0x/icon-washer.png
new file mode 100755
index 00000000..bfa09413
Binary files /dev/null and b/images/2.0x/icon-washer.png differ
diff --git a/images/2.0x/icon-watch.png b/images/2.0x/icon-watch.png
new file mode 100755
index 00000000..e097d7ff
Binary files /dev/null and b/images/2.0x/icon-watch.png differ
diff --git a/images/2.0x/icon-x-orange-small.png b/images/2.0x/icon-x-orange-small.png
new file mode 100755
index 00000000..68b3b7cd
Binary files /dev/null and b/images/2.0x/icon-x-orange-small.png differ
diff --git a/images/2.0x/icon-x-orange.png b/images/2.0x/icon-x-orange.png
new file mode 100755
index 00000000..70fdf9ad
Binary files /dev/null and b/images/2.0x/icon-x-orange.png differ
diff --git a/images/2.0x/icon-your-care-team.png b/images/2.0x/icon-your-care-team.png
new file mode 100644
index 00000000..78fa2acc
Binary files /dev/null and b/images/2.0x/icon-your-care-team.png differ
diff --git a/images/2.0x/ig-20x20.png b/images/2.0x/ig-20x20.png
new file mode 100644
index 00000000..893bc134
Binary files /dev/null and b/images/2.0x/ig-20x20.png differ
diff --git a/images/2.0x/ig-24x24.png b/images/2.0x/ig-24x24.png
new file mode 100644
index 00000000..40391234
Binary files /dev/null and b/images/2.0x/ig-24x24.png differ
diff --git a/images/2.0x/ig-32x32.png b/images/2.0x/ig-32x32.png
new file mode 100755
index 00000000..c2a6ed80
Binary files /dev/null and b/images/2.0x/ig-32x32.png differ
diff --git a/images/2.0x/ilini-cash.png b/images/2.0x/ilini-cash.png
new file mode 100644
index 00000000..3a1f1dce
Binary files /dev/null and b/images/2.0x/ilini-cash.png differ
diff --git a/images/2.0x/kognito.png b/images/2.0x/kognito.png
new file mode 100644
index 00000000..59184199
Binary files /dev/null and b/images/2.0x/kognito.png differ
diff --git a/images/2.0x/link-out.png b/images/2.0x/link-out.png
new file mode 100644
index 00000000..063d6470
Binary files /dev/null and b/images/2.0x/link-out.png differ
diff --git a/images/2.0x/login-header.png b/images/2.0x/login-header.png
new file mode 100644
index 00000000..3efb72f9
Binary files /dev/null and b/images/2.0x/login-header.png differ
diff --git a/images/2.0x/mc-kinley-gray.png b/images/2.0x/mc-kinley-gray.png
new file mode 100644
index 00000000..798ccc01
Binary files /dev/null and b/images/2.0x/mc-kinley-gray.png differ
diff --git a/images/2.0x/member.png b/images/2.0x/member.png
new file mode 100644
index 00000000..e5b74584
Binary files /dev/null and b/images/2.0x/member.png differ
diff --git a/images/2.0x/mental.png b/images/2.0x/mental.png
new file mode 100644
index 00000000..db2faebf
Binary files /dev/null and b/images/2.0x/mental.png differ
diff --git a/images/2.0x/mtd-logo.png b/images/2.0x/mtd-logo.png
new file mode 100644
index 00000000..ebee22fd
Binary files /dev/null and b/images/2.0x/mtd-logo.png differ
diff --git a/images/2.0x/my-illini-orange.png b/images/2.0x/my-illini-orange.png
new file mode 100644
index 00000000..a41dbc94
Binary files /dev/null and b/images/2.0x/my-illini-orange.png differ
diff --git a/images/2.0x/navy.png b/images/2.0x/navy.png
new file mode 100755
index 00000000..613fb5ee
Binary files /dev/null and b/images/2.0x/navy.png differ
diff --git a/images/2.0x/near-you.png b/images/2.0x/near-you.png
new file mode 100644
index 00000000..42e56eec
Binary files /dev/null and b/images/2.0x/near-you.png differ
diff --git a/images/2.0x/onboarding-back-btn.png b/images/2.0x/onboarding-back-btn.png
new file mode 100644
index 00000000..e44e3e7d
Binary files /dev/null and b/images/2.0x/onboarding-back-btn.png differ
diff --git a/images/2.0x/osf-logo-gray.png b/images/2.0x/osf-logo-gray.png
new file mode 100644
index 00000000..e93a5018
Binary files /dev/null and b/images/2.0x/osf-logo-gray.png differ
diff --git a/images/2.0x/path.png b/images/2.0x/path.png
new file mode 100644
index 00000000..64312c28
Binary files /dev/null and b/images/2.0x/path.png differ
diff --git a/images/2.0x/pending.png b/images/2.0x/pending.png
new file mode 100644
index 00000000..485b79f0
Binary files /dev/null and b/images/2.0x/pending.png differ
diff --git a/images/2.0x/physical.png b/images/2.0x/physical.png
new file mode 100644
index 00000000..74736b24
Binary files /dev/null and b/images/2.0x/physical.png differ
diff --git a/images/2.0x/pledge.png b/images/2.0x/pledge.png
new file mode 100644
index 00000000..9e402744
Binary files /dev/null and b/images/2.0x/pledge.png differ
diff --git a/images/2.0x/powered-by.png b/images/2.0x/powered-by.png
new file mode 100644
index 00000000..4a04cf56
Binary files /dev/null and b/images/2.0x/powered-by.png differ
diff --git a/images/2.0x/privacy-header.png b/images/2.0x/privacy-header.png
new file mode 100644
index 00000000..5be65a28
Binary files /dev/null and b/images/2.0x/privacy-header.png differ
diff --git a/images/2.0x/privacy.png b/images/2.0x/privacy.png
new file mode 100644
index 00000000..0d0cecd5
Binary files /dev/null and b/images/2.0x/privacy.png differ
diff --git a/images/2.0x/provider.png b/images/2.0x/provider.png
new file mode 100644
index 00000000..14af573d
Binary files /dev/null and b/images/2.0x/provider.png differ
diff --git a/images/2.0x/reflection.png b/images/2.0x/reflection.png
new file mode 100644
index 00000000..8e7e795a
Binary files /dev/null and b/images/2.0x/reflection.png differ
diff --git a/images/2.0x/reminder.png b/images/2.0x/reminder.png
new file mode 100755
index 00000000..32fa2e85
Binary files /dev/null and b/images/2.0x/reminder.png differ
diff --git a/images/2.0x/safer-illinois.png b/images/2.0x/safer-illinois.png
new file mode 100644
index 00000000..0eb949a4
Binary files /dev/null and b/images/2.0x/safer-illinois.png differ
diff --git a/images/2.0x/schedule-orange.png b/images/2.0x/schedule-orange.png
new file mode 100755
index 00000000..31e7b2af
Binary files /dev/null and b/images/2.0x/schedule-orange.png differ
diff --git a/images/2.0x/selected-orange.png b/images/2.0x/selected-orange.png
new file mode 100644
index 00000000..9d2d6f17
Binary files /dev/null and b/images/2.0x/selected-orange.png differ
diff --git a/images/2.0x/selected.png b/images/2.0x/selected.png
new file mode 100755
index 00000000..918b271a
Binary files /dev/null and b/images/2.0x/selected.png differ
diff --git a/images/2.0x/settings-white.png b/images/2.0x/settings-white.png
new file mode 100644
index 00000000..cd5943d0
Binary files /dev/null and b/images/2.0x/settings-white.png differ
diff --git a/images/2.0x/share-location-header.png b/images/2.0x/share-location-header.png
new file mode 100644
index 00000000..2f999e10
Binary files /dev/null and b/images/2.0x/share-location-header.png differ
diff --git a/images/2.0x/slant-down-right-blue-rotated.png b/images/2.0x/slant-down-right-blue-rotated.png
new file mode 100644
index 00000000..e58d22a0
Binary files /dev/null and b/images/2.0x/slant-down-right-blue-rotated.png differ
diff --git a/images/2.0x/slant-down-right-blue.png b/images/2.0x/slant-down-right-blue.png
new file mode 100644
index 00000000..0a6c05e0
Binary files /dev/null and b/images/2.0x/slant-down-right-blue.png differ
diff --git a/images/2.0x/small-add-orange.png b/images/2.0x/small-add-orange.png
new file mode 100644
index 00000000..5ea9afbb
Binary files /dev/null and b/images/2.0x/small-add-orange.png differ
diff --git a/images/2.0x/small-add.png b/images/2.0x/small-add.png
new file mode 100644
index 00000000..dbaa5bb0
Binary files /dev/null and b/images/2.0x/small-add.png differ
diff --git a/images/2.0x/social.png b/images/2.0x/social.png
new file mode 100644
index 00000000..0ede3c64
Binary files /dev/null and b/images/2.0x/social.png differ
diff --git a/images/2.0x/spiritual.png b/images/2.0x/spiritual.png
new file mode 100644
index 00000000..e338aeba
Binary files /dev/null and b/images/2.0x/spiritual.png differ
diff --git a/images/2.0x/switch-off.png b/images/2.0x/switch-off.png
new file mode 100644
index 00000000..5cf96574
Binary files /dev/null and b/images/2.0x/switch-off.png differ
diff --git a/images/2.0x/switch-on.png b/images/2.0x/switch-on.png
new file mode 100644
index 00000000..151b7394
Binary files /dev/null and b/images/2.0x/switch-on.png differ
diff --git a/images/2.0x/tab-browse-selected.png b/images/2.0x/tab-browse-selected.png
new file mode 100644
index 00000000..b7964394
Binary files /dev/null and b/images/2.0x/tab-browse-selected.png differ
diff --git a/images/2.0x/tab-browse.png b/images/2.0x/tab-browse.png
new file mode 100644
index 00000000..e6e95d45
Binary files /dev/null and b/images/2.0x/tab-browse.png differ
diff --git a/images/2.0x/tab-explore.png b/images/2.0x/tab-explore.png
new file mode 100644
index 00000000..6cb7f355
Binary files /dev/null and b/images/2.0x/tab-explore.png differ
diff --git a/images/2.0x/tab-home-selected.png b/images/2.0x/tab-home-selected.png
new file mode 100755
index 00000000..244b2b08
Binary files /dev/null and b/images/2.0x/tab-home-selected.png differ
diff --git a/images/2.0x/tab-home.png b/images/2.0x/tab-home.png
new file mode 100755
index 00000000..ef752447
Binary files /dev/null and b/images/2.0x/tab-home.png differ
diff --git a/images/2.0x/tab-more-selected.png b/images/2.0x/tab-more-selected.png
new file mode 100755
index 00000000..b108f32a
Binary files /dev/null and b/images/2.0x/tab-more-selected.png differ
diff --git a/images/2.0x/tab-more.png b/images/2.0x/tab-more.png
new file mode 100755
index 00000000..8b9a85d9
Binary files /dev/null and b/images/2.0x/tab-more.png differ
diff --git a/images/2.0x/tab-saved-selected.png b/images/2.0x/tab-saved-selected.png
new file mode 100755
index 00000000..5577b25a
Binary files /dev/null and b/images/2.0x/tab-saved-selected.png differ
diff --git a/images/2.0x/tab-saved.png b/images/2.0x/tab-saved.png
new file mode 100755
index 00000000..92482d52
Binary files /dev/null and b/images/2.0x/tab-saved.png differ
diff --git a/images/2.0x/tab-wallet.png b/images/2.0x/tab-wallet.png
new file mode 100644
index 00000000..4fad57f8
Binary files /dev/null and b/images/2.0x/tab-wallet.png differ
diff --git a/images/2.0x/teal.png b/images/2.0x/teal.png
new file mode 100644
index 00000000..b656e567
Binary files /dev/null and b/images/2.0x/teal.png differ
diff --git a/images/2.0x/tickets_yellow.png b/images/2.0x/tickets_yellow.png
new file mode 100644
index 00000000..52306ffb
Binary files /dev/null and b/images/2.0x/tickets_yellow.png differ
diff --git a/images/2.0x/twitter-20x18.png b/images/2.0x/twitter-20x18.png
new file mode 100644
index 00000000..618b4b73
Binary files /dev/null and b/images/2.0x/twitter-20x18.png differ
diff --git a/images/2.0x/twitter-24x22.png b/images/2.0x/twitter-24x22.png
new file mode 100644
index 00000000..abd78cb8
Binary files /dev/null and b/images/2.0x/twitter-24x22.png differ
diff --git a/images/2.0x/twitter-32x28.png b/images/2.0x/twitter-32x28.png
new file mode 100755
index 00000000..85ed8968
Binary files /dev/null and b/images/2.0x/twitter-32x28.png differ
diff --git a/images/2.0x/u.png b/images/2.0x/u.png
new file mode 100644
index 00000000..7f45bafa
Binary files /dev/null and b/images/2.0x/u.png differ
diff --git a/images/2.0x/upcoming_events_orange.png b/images/2.0x/upcoming_events_orange.png
new file mode 100644
index 00000000..3f5602ea
Binary files /dev/null and b/images/2.0x/upcoming_events_orange.png differ
diff --git a/images/2.0x/user-check.png b/images/2.0x/user-check.png
new file mode 100644
index 00000000..b0a8bc7c
Binary files /dev/null and b/images/2.0x/user-check.png differ
diff --git a/images/2.0x/vocational.png b/images/2.0x/vocational.png
new file mode 100644
index 00000000..f02009ac
Binary files /dev/null and b/images/2.0x/vocational.png differ
diff --git a/images/2.0x/warning-orange.png b/images/2.0x/warning-orange.png
new file mode 100644
index 00000000..e3aee5fa
Binary files /dev/null and b/images/2.0x/warning-orange.png differ
diff --git a/images/2.0x/you-tube-20x15.png b/images/2.0x/you-tube-20x15.png
new file mode 100644
index 00000000..8c52aedb
Binary files /dev/null and b/images/2.0x/you-tube-20x15.png differ
diff --git a/images/2.0x/you-tube-32x24.png b/images/2.0x/you-tube-32x24.png
new file mode 100755
index 00000000..f687a8d6
Binary files /dev/null and b/images/2.0x/you-tube-32x24.png differ
diff --git a/images/3.0x/add-to-apple-wallet.png b/images/3.0x/add-to-apple-wallet.png
new file mode 100644
index 00000000..080908e9
Binary files /dev/null and b/images/3.0x/add-to-apple-wallet.png differ
diff --git a/images/3.0x/athletics-baseball-orange.png b/images/3.0x/athletics-baseball-orange.png
new file mode 100644
index 00000000..fcb4f3bd
Binary files /dev/null and b/images/3.0x/athletics-baseball-orange.png differ
diff --git a/images/3.0x/athletics-baseball-white.png b/images/3.0x/athletics-baseball-white.png
new file mode 100644
index 00000000..b4bff3cf
Binary files /dev/null and b/images/3.0x/athletics-baseball-white.png differ
diff --git a/images/3.0x/athletics-basketball-orange.png b/images/3.0x/athletics-basketball-orange.png
new file mode 100644
index 00000000..a02bae95
Binary files /dev/null and b/images/3.0x/athletics-basketball-orange.png differ
diff --git a/images/3.0x/athletics-basketball-white.png b/images/3.0x/athletics-basketball-white.png
new file mode 100644
index 00000000..21b6238b
Binary files /dev/null and b/images/3.0x/athletics-basketball-white.png differ
diff --git a/images/3.0x/athletics-cross-orange.png b/images/3.0x/athletics-cross-orange.png
new file mode 100644
index 00000000..4f4b4001
Binary files /dev/null and b/images/3.0x/athletics-cross-orange.png differ
diff --git a/images/3.0x/athletics-cross-white.png b/images/3.0x/athletics-cross-white.png
new file mode 100644
index 00000000..04e68054
Binary files /dev/null and b/images/3.0x/athletics-cross-white.png differ
diff --git a/images/3.0x/athletics-football-orange.png b/images/3.0x/athletics-football-orange.png
new file mode 100644
index 00000000..754805ff
Binary files /dev/null and b/images/3.0x/athletics-football-orange.png differ
diff --git a/images/3.0x/athletics-football-white.png b/images/3.0x/athletics-football-white.png
new file mode 100644
index 00000000..448ee2ca
Binary files /dev/null and b/images/3.0x/athletics-football-white.png differ
diff --git a/images/3.0x/athletics-golf-orange.png b/images/3.0x/athletics-golf-orange.png
new file mode 100644
index 00000000..77abc697
Binary files /dev/null and b/images/3.0x/athletics-golf-orange.png differ
diff --git a/images/3.0x/athletics-golf-white.png b/images/3.0x/athletics-golf-white.png
new file mode 100644
index 00000000..b6b9b6dc
Binary files /dev/null and b/images/3.0x/athletics-golf-white.png differ
diff --git a/images/3.0x/athletics-gymnastics-orange.png b/images/3.0x/athletics-gymnastics-orange.png
new file mode 100644
index 00000000..444cb9e1
Binary files /dev/null and b/images/3.0x/athletics-gymnastics-orange.png differ
diff --git a/images/3.0x/athletics-gymnastics-white.png b/images/3.0x/athletics-gymnastics-white.png
new file mode 100644
index 00000000..18a86f44
Binary files /dev/null and b/images/3.0x/athletics-gymnastics-white.png differ
diff --git a/images/3.0x/athletics-handball-orange.png b/images/3.0x/athletics-handball-orange.png
new file mode 100644
index 00000000..ab07f7b8
Binary files /dev/null and b/images/3.0x/athletics-handball-orange.png differ
diff --git a/images/3.0x/athletics-handball-white.png b/images/3.0x/athletics-handball-white.png
new file mode 100644
index 00000000..0eeeb362
Binary files /dev/null and b/images/3.0x/athletics-handball-white.png differ
diff --git a/images/3.0x/athletics-soccer-orange.png b/images/3.0x/athletics-soccer-orange.png
new file mode 100644
index 00000000..a69aa5e3
Binary files /dev/null and b/images/3.0x/athletics-soccer-orange.png differ
diff --git a/images/3.0x/athletics-soccer-white.png b/images/3.0x/athletics-soccer-white.png
new file mode 100644
index 00000000..af24dfdb
Binary files /dev/null and b/images/3.0x/athletics-soccer-white.png differ
diff --git a/images/3.0x/athletics-softball-orange.png b/images/3.0x/athletics-softball-orange.png
new file mode 100644
index 00000000..fcb4f3bd
Binary files /dev/null and b/images/3.0x/athletics-softball-orange.png differ
diff --git a/images/3.0x/athletics-swim-orange.png b/images/3.0x/athletics-swim-orange.png
new file mode 100644
index 00000000..ba505077
Binary files /dev/null and b/images/3.0x/athletics-swim-orange.png differ
diff --git a/images/3.0x/athletics-swim-white.png b/images/3.0x/athletics-swim-white.png
new file mode 100644
index 00000000..47ef4093
Binary files /dev/null and b/images/3.0x/athletics-swim-white.png differ
diff --git a/images/3.0x/athletics-tennis-orange.png b/images/3.0x/athletics-tennis-orange.png
new file mode 100644
index 00000000..6ad665be
Binary files /dev/null and b/images/3.0x/athletics-tennis-orange.png differ
diff --git a/images/3.0x/athletics-tennis-white.png b/images/3.0x/athletics-tennis-white.png
new file mode 100644
index 00000000..2ef4f821
Binary files /dev/null and b/images/3.0x/athletics-tennis-white.png differ
diff --git a/images/3.0x/athletics-track-orange.png b/images/3.0x/athletics-track-orange.png
new file mode 100644
index 00000000..6e3a7843
Binary files /dev/null and b/images/3.0x/athletics-track-orange.png differ
diff --git a/images/3.0x/athletics-track-white.png b/images/3.0x/athletics-track-white.png
new file mode 100644
index 00000000..8a331851
Binary files /dev/null and b/images/3.0x/athletics-track-white.png differ
diff --git a/images/3.0x/athletics-volleyball-orange.png b/images/3.0x/athletics-volleyball-orange.png
new file mode 100644
index 00000000..ab07f7b8
Binary files /dev/null and b/images/3.0x/athletics-volleyball-orange.png differ
diff --git a/images/3.0x/athletics-wrestling-orange.png b/images/3.0x/athletics-wrestling-orange.png
new file mode 100644
index 00000000..6f81a98f
Binary files /dev/null and b/images/3.0x/athletics-wrestling-orange.png differ
diff --git a/images/3.0x/athletics-wrestling-white.png b/images/3.0x/athletics-wrestling-white.png
new file mode 100644
index 00000000..ddbcd357
Binary files /dev/null and b/images/3.0x/athletics-wrestling-white.png differ
diff --git a/images/3.0x/background-image.png b/images/3.0x/background-image.png
new file mode 100644
index 00000000..02ad9701
Binary files /dev/null and b/images/3.0x/background-image.png differ
diff --git a/images/3.0x/background-onboarding-squares-dark.png b/images/3.0x/background-onboarding-squares-dark.png
new file mode 100644
index 00000000..55026f0f
Binary files /dev/null and b/images/3.0x/background-onboarding-squares-dark.png differ
diff --git a/images/3.0x/background-onboarding-squares-light.png b/images/3.0x/background-onboarding-squares-light.png
new file mode 100644
index 00000000..60a97dab
Binary files /dev/null and b/images/3.0x/background-onboarding-squares-light.png differ
diff --git a/images/3.0x/background-onboarding-squares.png b/images/3.0x/background-onboarding-squares.png
new file mode 100644
index 00000000..7e795cba
Binary files /dev/null and b/images/3.0x/background-onboarding-squares.png differ
diff --git a/images/3.0x/button-plus-orange.png b/images/3.0x/button-plus-orange.png
new file mode 100755
index 00000000..f352095a
Binary files /dev/null and b/images/3.0x/button-plus-orange.png differ
diff --git a/images/3.0x/campus-tools-blue.png b/images/3.0x/campus-tools-blue.png
new file mode 100644
index 00000000..ce202e1c
Binary files /dev/null and b/images/3.0x/campus-tools-blue.png differ
diff --git a/images/3.0x/campus-tools-blue.textClipping b/images/3.0x/campus-tools-blue.textClipping
new file mode 100644
index 00000000..67aa32dd
Binary files /dev/null and b/images/3.0x/campus-tools-blue.textClipping differ
diff --git a/images/3.0x/campus-tools.png b/images/3.0x/campus-tools.png
new file mode 100644
index 00000000..6ff8619b
Binary files /dev/null and b/images/3.0x/campus-tools.png differ
diff --git a/images/3.0x/certified-copy.png b/images/3.0x/certified-copy.png
new file mode 100644
index 00000000..5658a3a2
Binary files /dev/null and b/images/3.0x/certified-copy.png differ
diff --git a/images/3.0x/certified.png b/images/3.0x/certified.png
new file mode 100644
index 00000000..fc98fc10
Binary files /dev/null and b/images/3.0x/certified.png differ
diff --git a/images/3.0x/checkbox-selected.png b/images/3.0x/checkbox-selected.png
new file mode 100644
index 00000000..2814357d
Binary files /dev/null and b/images/3.0x/checkbox-selected.png differ
diff --git a/images/3.0x/checkbox-small.png b/images/3.0x/checkbox-small.png
new file mode 100644
index 00000000..84e4a257
Binary files /dev/null and b/images/3.0x/checkbox-small.png differ
diff --git a/images/3.0x/checkbox-unselected.png b/images/3.0x/checkbox-unselected.png
new file mode 100644
index 00000000..63649587
Binary files /dev/null and b/images/3.0x/checkbox-unselected.png differ
diff --git a/images/3.0x/chevron-blue-right.png b/images/3.0x/chevron-blue-right.png
new file mode 100755
index 00000000..f292b193
Binary files /dev/null and b/images/3.0x/chevron-blue-right.png differ
diff --git a/images/3.0x/chevron-down.png b/images/3.0x/chevron-down.png
new file mode 100644
index 00000000..d555b26a
Binary files /dev/null and b/images/3.0x/chevron-down.png differ
diff --git a/images/3.0x/chevron-left-blue.png b/images/3.0x/chevron-left-blue.png
new file mode 100755
index 00000000..d8b25b03
Binary files /dev/null and b/images/3.0x/chevron-left-blue.png differ
diff --git a/images/3.0x/chevron-left-white.png b/images/3.0x/chevron-left-white.png
new file mode 100644
index 00000000..c27bcf84
Binary files /dev/null and b/images/3.0x/chevron-left-white.png differ
diff --git a/images/3.0x/chevron-left.png b/images/3.0x/chevron-left.png
new file mode 100644
index 00000000..68d73f93
Binary files /dev/null and b/images/3.0x/chevron-left.png differ
diff --git a/images/3.0x/chevron-right.png b/images/3.0x/chevron-right.png
new file mode 100755
index 00000000..8225443b
Binary files /dev/null and b/images/3.0x/chevron-right.png differ
diff --git a/images/3.0x/chevron-up.png b/images/3.0x/chevron-up.png
new file mode 100644
index 00000000..082dba13
Binary files /dev/null and b/images/3.0x/chevron-up.png differ
diff --git a/images/3.0x/chevron2-down.png b/images/3.0x/chevron2-down.png
new file mode 100755
index 00000000..1faa038d
Binary files /dev/null and b/images/3.0x/chevron2-down.png differ
diff --git a/images/3.0x/classic-meal-blue.png b/images/3.0x/classic-meal-blue.png
new file mode 100644
index 00000000..eac20d19
Binary files /dev/null and b/images/3.0x/classic-meal-blue.png differ
diff --git a/images/3.0x/classic-meal-orange.png b/images/3.0x/classic-meal-orange.png
new file mode 100755
index 00000000..a8d3bfbf
Binary files /dev/null and b/images/3.0x/classic-meal-orange.png differ
diff --git a/images/3.0x/close-blue.png b/images/3.0x/close-blue.png
new file mode 100644
index 00000000..26390605
Binary files /dev/null and b/images/3.0x/close-blue.png differ
diff --git a/images/3.0x/close-gray.png b/images/3.0x/close-gray.png
new file mode 100644
index 00000000..82ca0729
Binary files /dev/null and b/images/3.0x/close-gray.png differ
diff --git a/images/3.0x/close-menu.png b/images/3.0x/close-menu.png
new file mode 100644
index 00000000..77f328db
Binary files /dev/null and b/images/3.0x/close-menu.png differ
diff --git a/images/3.0x/close-orange-large.png b/images/3.0x/close-orange-large.png
new file mode 100644
index 00000000..30ba4225
Binary files /dev/null and b/images/3.0x/close-orange-large.png differ
diff --git a/images/3.0x/close-orange.png b/images/3.0x/close-orange.png
new file mode 100755
index 00000000..53faf74d
Binary files /dev/null and b/images/3.0x/close-orange.png differ
diff --git a/images/3.0x/close-white-large.png b/images/3.0x/close-white-large.png
new file mode 100644
index 00000000..ab1ac315
Binary files /dev/null and b/images/3.0x/close-white-large.png differ
diff --git a/images/3.0x/close-white-shadow.png b/images/3.0x/close-white-shadow.png
new file mode 100644
index 00000000..61213e6e
Binary files /dev/null and b/images/3.0x/close-white-shadow.png differ
diff --git a/images/3.0x/close-white.png b/images/3.0x/close-white.png
new file mode 100644
index 00000000..65cd0ea1
Binary files /dev/null and b/images/3.0x/close-white.png differ
diff --git a/images/3.0x/covid.png b/images/3.0x/covid.png
new file mode 100644
index 00000000..8efad934
Binary files /dev/null and b/images/3.0x/covid.png differ
diff --git a/images/3.0x/covid19-header-blue.png b/images/3.0x/covid19-header-blue.png
new file mode 100644
index 00000000..e0d5e40a
Binary files /dev/null and b/images/3.0x/covid19-header-blue.png differ
diff --git a/images/3.0x/covid19-orange.png b/images/3.0x/covid19-orange.png
new file mode 100644
index 00000000..47776ae8
Binary files /dev/null and b/images/3.0x/covid19-orange.png differ
diff --git a/images/3.0x/deselected-dark.png b/images/3.0x/deselected-dark.png
new file mode 100755
index 00000000..6ea1bbfe
Binary files /dev/null and b/images/3.0x/deselected-dark.png differ
diff --git a/images/3.0x/deselected.png b/images/3.0x/deselected.png
new file mode 100755
index 00000000..d7c91e5c
Binary files /dev/null and b/images/3.0x/deselected.png differ
diff --git a/images/3.0x/disabled.png b/images/3.0x/disabled.png
new file mode 100755
index 00000000..c48502ea
Binary files /dev/null and b/images/3.0x/disabled.png differ
diff --git a/images/3.0x/emotional.png b/images/3.0x/emotional.png
new file mode 100644
index 00000000..7299e204
Binary files /dev/null and b/images/3.0x/emotional.png differ
diff --git a/images/3.0x/enable-bluetooth-header.png b/images/3.0x/enable-bluetooth-header.png
new file mode 100644
index 00000000..087c86c2
Binary files /dev/null and b/images/3.0x/enable-bluetooth-header.png differ
diff --git a/images/3.0x/environmental.png b/images/3.0x/environmental.png
new file mode 100644
index 00000000..513c46a5
Binary files /dev/null and b/images/3.0x/environmental.png differ
diff --git a/images/3.0x/example.png b/images/3.0x/example.png
new file mode 100644
index 00000000..d54bf375
Binary files /dev/null and b/images/3.0x/example.png differ
diff --git a/images/3.0x/explore.png b/images/3.0x/explore.png
new file mode 100644
index 00000000..1fe85608
Binary files /dev/null and b/images/3.0x/explore.png differ
diff --git a/images/3.0x/external-link.png b/images/3.0x/external-link.png
new file mode 100644
index 00000000..719d3e2e
Binary files /dev/null and b/images/3.0x/external-link.png differ
diff --git a/images/3.0x/fb-10x20.png b/images/3.0x/fb-10x20.png
new file mode 100644
index 00000000..4e59ec72
Binary files /dev/null and b/images/3.0x/fb-10x20.png differ
diff --git a/images/3.0x/fb-12x24.png b/images/3.0x/fb-12x24.png
new file mode 100644
index 00000000..51b6cbff
Binary files /dev/null and b/images/3.0x/fb-12x24.png differ
diff --git a/images/3.0x/fb.png b/images/3.0x/fb.png
new file mode 100755
index 00000000..4ad4ce1e
Binary files /dev/null and b/images/3.0x/fb.png differ
diff --git a/images/3.0x/fill-1.png b/images/3.0x/fill-1.png
new file mode 100644
index 00000000..a44237d8
Binary files /dev/null and b/images/3.0x/fill-1.png differ
diff --git a/images/3.0x/financial-2.png b/images/3.0x/financial-2.png
new file mode 100644
index 00000000..e90eda64
Binary files /dev/null and b/images/3.0x/financial-2.png differ
diff --git a/images/3.0x/game_day_blue.png b/images/3.0x/game_day_blue.png
new file mode 100644
index 00000000..8417a897
Binary files /dev/null and b/images/3.0x/game_day_blue.png differ
diff --git a/images/3.0x/globe.png b/images/3.0x/globe.png
new file mode 100644
index 00000000..24a78d2e
Binary files /dev/null and b/images/3.0x/globe.png differ
diff --git a/images/3.0x/group-10.png b/images/3.0x/group-10.png
new file mode 100644
index 00000000..8d586d68
Binary files /dev/null and b/images/3.0x/group-10.png differ
diff --git a/images/3.0x/group-15.png b/images/3.0x/group-15.png
new file mode 100644
index 00000000..c2e75ff0
Binary files /dev/null and b/images/3.0x/group-15.png differ
diff --git a/images/3.0x/group-16.png b/images/3.0x/group-16.png
new file mode 100644
index 00000000..9af84aef
Binary files /dev/null and b/images/3.0x/group-16.png differ
diff --git a/images/3.0x/group-18.png b/images/3.0x/group-18.png
new file mode 100644
index 00000000..be1151a8
Binary files /dev/null and b/images/3.0x/group-18.png differ
diff --git a/images/3.0x/group-2.png b/images/3.0x/group-2.png
new file mode 100644
index 00000000..d1e061f8
Binary files /dev/null and b/images/3.0x/group-2.png differ
diff --git a/images/3.0x/group-20.png b/images/3.0x/group-20.png
new file mode 100644
index 00000000..dbd14132
Binary files /dev/null and b/images/3.0x/group-20.png differ
diff --git a/images/3.0x/group-25.png b/images/3.0x/group-25.png
new file mode 100644
index 00000000..ca0011ff
Binary files /dev/null and b/images/3.0x/group-25.png differ
diff --git a/images/3.0x/group-28.png b/images/3.0x/group-28.png
new file mode 100644
index 00000000..7dea23ea
Binary files /dev/null and b/images/3.0x/group-28.png differ
diff --git a/images/3.0x/group-3.png b/images/3.0x/group-3.png
new file mode 100644
index 00000000..1484f8cd
Binary files /dev/null and b/images/3.0x/group-3.png differ
diff --git a/images/3.0x/group-4.png b/images/3.0x/group-4.png
new file mode 100644
index 00000000..6eeda93d
Binary files /dev/null and b/images/3.0x/group-4.png differ
diff --git a/images/3.0x/group-44.png b/images/3.0x/group-44.png
new file mode 100644
index 00000000..eaaeb6c2
Binary files /dev/null and b/images/3.0x/group-44.png differ
diff --git a/images/3.0x/group-444.png b/images/3.0x/group-444.png
new file mode 100644
index 00000000..7c1d7584
Binary files /dev/null and b/images/3.0x/group-444.png differ
diff --git a/images/3.0x/group-5-blue.png b/images/3.0x/group-5-blue.png
new file mode 100644
index 00000000..8e7f2846
Binary files /dev/null and b/images/3.0x/group-5-blue.png differ
diff --git a/images/3.0x/group-5-white.png b/images/3.0x/group-5-white.png
new file mode 100644
index 00000000..7767f6e0
Binary files /dev/null and b/images/3.0x/group-5-white.png differ
diff --git a/images/3.0x/group-7.png b/images/3.0x/group-7.png
new file mode 100644
index 00000000..22b5ddc8
Binary files /dev/null and b/images/3.0x/group-7.png differ
diff --git a/images/3.0x/group-8.png b/images/3.0x/group-8.png
new file mode 100644
index 00000000..6923a7ca
Binary files /dev/null and b/images/3.0x/group-8.png differ
diff --git a/images/3.0x/group-9.png b/images/3.0x/group-9.png
new file mode 100644
index 00000000..00dddfe4
Binary files /dev/null and b/images/3.0x/group-9.png differ
diff --git a/images/3.0x/group-event-settings.png b/images/3.0x/group-event-settings.png
new file mode 100644
index 00000000..0d61e501
Binary files /dev/null and b/images/3.0x/group-event-settings.png differ
diff --git a/images/3.0x/group-settings-icon.png b/images/3.0x/group-settings-icon.png
new file mode 100644
index 00000000..0d61e501
Binary files /dev/null and b/images/3.0x/group-settings-icon.png differ
diff --git a/images/3.0x/group.png b/images/3.0x/group.png
new file mode 100644
index 00000000..0900cd3e
Binary files /dev/null and b/images/3.0x/group.png differ
diff --git a/images/3.0x/happening.png b/images/3.0x/happening.png
new file mode 100644
index 00000000..97eac378
Binary files /dev/null and b/images/3.0x/happening.png differ
diff --git a/images/3.0x/icon-add-14x14.png b/images/3.0x/icon-add-14x14.png
new file mode 100644
index 00000000..92618c51
Binary files /dev/null and b/images/3.0x/icon-add-14x14.png differ
diff --git a/images/3.0x/icon-add-20x18.png b/images/3.0x/icon-add-20x18.png
new file mode 100644
index 00000000..f6c4ed18
Binary files /dev/null and b/images/3.0x/icon-add-20x18.png differ
diff --git a/images/3.0x/icon-all-set-header.png b/images/3.0x/icon-all-set-header.png
new file mode 100644
index 00000000..6c3fb910
Binary files /dev/null and b/images/3.0x/icon-all-set-header.png differ
diff --git a/images/3.0x/icon-athletics-blue.png b/images/3.0x/icon-athletics-blue.png
new file mode 100644
index 00000000..d946bce9
Binary files /dev/null and b/images/3.0x/icon-athletics-blue.png differ
diff --git a/images/3.0x/icon-athletics.png b/images/3.0x/icon-athletics.png
new file mode 100644
index 00000000..412b49a1
Binary files /dev/null and b/images/3.0x/icon-athletics.png differ
diff --git a/images/3.0x/icon-avatar-placeholder.png b/images/3.0x/icon-avatar-placeholder.png
new file mode 100644
index 00000000..4c05364b
Binary files /dev/null and b/images/3.0x/icon-avatar-placeholder.png differ
diff --git a/images/3.0x/icon-badge.png b/images/3.0x/icon-badge.png
new file mode 100644
index 00000000..46e224f0
Binary files /dev/null and b/images/3.0x/icon-badge.png differ
diff --git a/images/3.0x/icon-big-onboarding-health.png b/images/3.0x/icon-big-onboarding-health.png
new file mode 100644
index 00000000..210ce381
Binary files /dev/null and b/images/3.0x/icon-big-onboarding-health.png differ
diff --git a/images/3.0x/icon-big-onboarding-privacy.png b/images/3.0x/icon-big-onboarding-privacy.png
new file mode 100644
index 00000000..89ec03ff
Binary files /dev/null and b/images/3.0x/icon-big-onboarding-privacy.png differ
diff --git a/images/3.0x/icon-bluetooth.png b/images/3.0x/icon-bluetooth.png
new file mode 100644
index 00000000..8151dd9e
Binary files /dev/null and b/images/3.0x/icon-bluetooth.png differ
diff --git a/images/3.0x/icon-browse-athletics.png b/images/3.0x/icon-browse-athletics.png
new file mode 100644
index 00000000..600d8a98
Binary files /dev/null and b/images/3.0x/icon-browse-athletics.png differ
diff --git a/images/3.0x/icon-browse-covid19.png b/images/3.0x/icon-browse-covid19.png
new file mode 100644
index 00000000..693a21c8
Binary files /dev/null and b/images/3.0x/icon-browse-covid19.png differ
diff --git a/images/3.0x/icon-browse-dinings.png b/images/3.0x/icon-browse-dinings.png
new file mode 100644
index 00000000..cc0b17c4
Binary files /dev/null and b/images/3.0x/icon-browse-dinings.png differ
diff --git a/images/3.0x/icon-browse-events.png b/images/3.0x/icon-browse-events.png
new file mode 100644
index 00000000..fdecdaff
Binary files /dev/null and b/images/3.0x/icon-browse-events.png differ
diff --git a/images/3.0x/icon-browse-quick-polls.png b/images/3.0x/icon-browse-quick-polls.png
new file mode 100644
index 00000000..69fa054c
Binary files /dev/null and b/images/3.0x/icon-browse-quick-polls.png differ
diff --git a/images/3.0x/icon-browse-saved.png b/images/3.0x/icon-browse-saved.png
new file mode 100644
index 00000000..22b2e70f
Binary files /dev/null and b/images/3.0x/icon-browse-saved.png differ
diff --git a/images/3.0x/icon-browse-wellness.png b/images/3.0x/icon-browse-wellness.png
new file mode 100644
index 00000000..97cb4807
Binary files /dev/null and b/images/3.0x/icon-browse-wellness.png differ
diff --git a/images/3.0x/icon-browse.png b/images/3.0x/icon-browse.png
new file mode 100755
index 00000000..00730a7a
Binary files /dev/null and b/images/3.0x/icon-browse.png differ
diff --git a/images/3.0x/icon-calendar.png b/images/3.0x/icon-calendar.png
new file mode 100755
index 00000000..5573b5d2
Binary files /dev/null and b/images/3.0x/icon-calendar.png differ
diff --git a/images/3.0x/icon-campus-tools-athletics.png b/images/3.0x/icon-campus-tools-athletics.png
new file mode 100755
index 00000000..d0b0805a
Binary files /dev/null and b/images/3.0x/icon-campus-tools-athletics.png differ
diff --git a/images/3.0x/icon-campus-tools-dining.png b/images/3.0x/icon-campus-tools-dining.png
new file mode 100755
index 00000000..ae99eb54
Binary files /dev/null and b/images/3.0x/icon-campus-tools-dining.png differ
diff --git a/images/3.0x/icon-campus-tools-events.png b/images/3.0x/icon-campus-tools-events.png
new file mode 100755
index 00000000..4b85bfb5
Binary files /dev/null and b/images/3.0x/icon-campus-tools-events.png differ
diff --git a/images/3.0x/icon-campus-tools-illini-cash.png b/images/3.0x/icon-campus-tools-illini-cash.png
new file mode 100755
index 00000000..6b866b34
Binary files /dev/null and b/images/3.0x/icon-campus-tools-illini-cash.png differ
diff --git a/images/3.0x/icon-campus-tools-laundry.png b/images/3.0x/icon-campus-tools-laundry.png
new file mode 100755
index 00000000..23f93008
Binary files /dev/null and b/images/3.0x/icon-campus-tools-laundry.png differ
diff --git a/images/3.0x/icon-campus-tools.png b/images/3.0x/icon-campus-tools.png
new file mode 100755
index 00000000..32bc19c9
Binary files /dev/null and b/images/3.0x/icon-campus-tools.png differ
diff --git a/images/3.0x/icon-campus-updates.png b/images/3.0x/icon-campus-updates.png
new file mode 100644
index 00000000..6e53cc43
Binary files /dev/null and b/images/3.0x/icon-campus-updates.png differ
diff --git a/images/3.0x/icon-cellphone.png b/images/3.0x/icon-cellphone.png
new file mode 100644
index 00000000..04fe45e4
Binary files /dev/null and b/images/3.0x/icon-cellphone.png differ
diff --git a/images/3.0x/icon-certified.png b/images/3.0x/icon-certified.png
new file mode 100755
index 00000000..fc98fc10
Binary files /dev/null and b/images/3.0x/icon-certified.png differ
diff --git a/images/3.0x/icon-check-example.png b/images/3.0x/icon-check-example.png
new file mode 100755
index 00000000..57883d46
Binary files /dev/null and b/images/3.0x/icon-check-example.png differ
diff --git a/images/3.0x/icon-check-simple.png b/images/3.0x/icon-check-simple.png
new file mode 100755
index 00000000..6440d319
Binary files /dev/null and b/images/3.0x/icon-check-simple.png differ
diff --git a/images/3.0x/icon-check.png b/images/3.0x/icon-check.png
new file mode 100644
index 00000000..892c66af
Binary files /dev/null and b/images/3.0x/icon-check.png differ
diff --git a/images/3.0x/icon-circle-close.png b/images/3.0x/icon-circle-close.png
new file mode 100755
index 00000000..65cd0ea1
Binary files /dev/null and b/images/3.0x/icon-circle-close.png differ
diff --git a/images/3.0x/icon-close-big.png b/images/3.0x/icon-close-big.png
new file mode 100755
index 00000000..e9f52665
Binary files /dev/null and b/images/3.0x/icon-close-big.png differ
diff --git a/images/3.0x/icon-comment-dots.png b/images/3.0x/icon-comment-dots.png
new file mode 100644
index 00000000..dc020d86
Binary files /dev/null and b/images/3.0x/icon-comment-dots.png differ
diff --git a/images/3.0x/icon-copy.png b/images/3.0x/icon-copy.png
new file mode 100644
index 00000000..fcfcbd21
Binary files /dev/null and b/images/3.0x/icon-copy.png differ
diff --git a/images/3.0x/icon-cost.png b/images/3.0x/icon-cost.png
new file mode 100644
index 00000000..5903f816
Binary files /dev/null and b/images/3.0x/icon-cost.png differ
diff --git a/images/3.0x/icon-country-guidelines.png b/images/3.0x/icon-country-guidelines.png
new file mode 100644
index 00000000..96ed4916
Binary files /dev/null and b/images/3.0x/icon-country-guidelines.png differ
diff --git a/images/3.0x/icon-create-event.png b/images/3.0x/icon-create-event.png
new file mode 100755
index 00000000..96dafa64
Binary files /dev/null and b/images/3.0x/icon-create-event.png differ
diff --git a/images/3.0x/icon-credit.png b/images/3.0x/icon-credit.png
new file mode 100755
index 00000000..1c7f5db9
Binary files /dev/null and b/images/3.0x/icon-credit.png differ
diff --git a/images/3.0x/icon-deselected-checkbox.png b/images/3.0x/icon-deselected-checkbox.png
new file mode 100644
index 00000000..a99ee4f9
Binary files /dev/null and b/images/3.0x/icon-deselected-checkbox.png differ
diff --git a/images/3.0x/icon-dining-orange.png b/images/3.0x/icon-dining-orange.png
new file mode 100644
index 00000000..a9eddd67
Binary files /dev/null and b/images/3.0x/icon-dining-orange.png differ
diff --git a/images/3.0x/icon-dining-yellow.png b/images/3.0x/icon-dining-yellow.png
new file mode 100755
index 00000000..e7103f22
Binary files /dev/null and b/images/3.0x/icon-dining-yellow.png differ
diff --git a/images/3.0x/icon-dining.png b/images/3.0x/icon-dining.png
new file mode 100644
index 00000000..f8bd6332
Binary files /dev/null and b/images/3.0x/icon-dining.png differ
diff --git a/images/3.0x/icon-down-orange.png b/images/3.0x/icon-down-orange.png
new file mode 100755
index 00000000..3953ed30
Binary files /dev/null and b/images/3.0x/icon-down-orange.png differ
diff --git a/images/3.0x/icon-down.png b/images/3.0x/icon-down.png
new file mode 100755
index 00000000..a34742c3
Binary files /dev/null and b/images/3.0x/icon-down.png differ
diff --git a/images/3.0x/icon-dryer-big.png b/images/3.0x/icon-dryer-big.png
new file mode 100755
index 00000000..2d703f9e
Binary files /dev/null and b/images/3.0x/icon-dryer-big.png differ
diff --git a/images/3.0x/icon-dryer-small.png b/images/3.0x/icon-dryer-small.png
new file mode 100755
index 00000000..65261fc0
Binary files /dev/null and b/images/3.0x/icon-dryer-small.png differ
diff --git a/images/3.0x/icon-edit.png b/images/3.0x/icon-edit.png
new file mode 100644
index 00000000..c8e7c663
Binary files /dev/null and b/images/3.0x/icon-edit.png differ
diff --git a/images/3.0x/icon-event.png b/images/3.0x/icon-event.png
new file mode 100644
index 00000000..262fbd22
Binary files /dev/null and b/images/3.0x/icon-event.png differ
diff --git a/images/3.0x/icon-explore-campus-athletics.png b/images/3.0x/icon-explore-campus-athletics.png
new file mode 100644
index 00000000..d0b0805a
Binary files /dev/null and b/images/3.0x/icon-explore-campus-athletics.png differ
diff --git a/images/3.0x/icon-explore-campus-dining.png b/images/3.0x/icon-explore-campus-dining.png
new file mode 100644
index 00000000..ae99eb54
Binary files /dev/null and b/images/3.0x/icon-explore-campus-dining.png differ
diff --git a/images/3.0x/icon-explore-campus-events.png b/images/3.0x/icon-explore-campus-events.png
new file mode 100644
index 00000000..4b85bfb5
Binary files /dev/null and b/images/3.0x/icon-explore-campus-events.png differ
diff --git a/images/3.0x/icon-explore.png b/images/3.0x/icon-explore.png
new file mode 100644
index 00000000..1fe85608
Binary files /dev/null and b/images/3.0x/icon-explore.png differ
diff --git a/images/3.0x/icon-face-mask.png b/images/3.0x/icon-face-mask.png
new file mode 100644
index 00000000..877569f0
Binary files /dev/null and b/images/3.0x/icon-face-mask.png differ
diff --git a/images/3.0x/icon-feedback.png b/images/3.0x/icon-feedback.png
new file mode 100755
index 00000000..05631c4e
Binary files /dev/null and b/images/3.0x/icon-feedback.png differ
diff --git a/images/3.0x/icon-gear.png b/images/3.0x/icon-gear.png
new file mode 100755
index 00000000..9e20bc3c
Binary files /dev/null and b/images/3.0x/icon-gear.png differ
diff --git a/images/3.0x/icon-health.png b/images/3.0x/icon-health.png
new file mode 100644
index 00000000..090bc5e8
Binary files /dev/null and b/images/3.0x/icon-health.png differ
diff --git a/images/3.0x/icon-hospital.png b/images/3.0x/icon-hospital.png
new file mode 100644
index 00000000..3649d7e5
Binary files /dev/null and b/images/3.0x/icon-hospital.png differ
diff --git a/images/3.0x/icon-identity.png b/images/3.0x/icon-identity.png
new file mode 100644
index 00000000..7099723a
Binary files /dev/null and b/images/3.0x/icon-identity.png differ
diff --git a/images/3.0x/icon-illini-cash.png b/images/3.0x/icon-illini-cash.png
new file mode 100755
index 00000000..3c7307d2
Binary files /dev/null and b/images/3.0x/icon-illini-cash.png differ
diff --git a/images/3.0x/icon-info-orange.png b/images/3.0x/icon-info-orange.png
new file mode 100644
index 00000000..2c86cfed
Binary files /dev/null and b/images/3.0x/icon-info-orange.png differ
diff --git a/images/3.0x/icon-key.png b/images/3.0x/icon-key.png
new file mode 100644
index 00000000..33152095
Binary files /dev/null and b/images/3.0x/icon-key.png differ
diff --git a/images/3.0x/icon-list-view.png b/images/3.0x/icon-list-view.png
new file mode 100644
index 00000000..e4899b1a
Binary files /dev/null and b/images/3.0x/icon-list-view.png differ
diff --git a/images/3.0x/icon-listen.png b/images/3.0x/icon-listen.png
new file mode 100755
index 00000000..caa1f08d
Binary files /dev/null and b/images/3.0x/icon-listen.png differ
diff --git a/images/3.0x/icon-live-stats.png b/images/3.0x/icon-live-stats.png
new file mode 100755
index 00000000..879e7414
Binary files /dev/null and b/images/3.0x/icon-live-stats.png differ
diff --git a/images/3.0x/icon-location-1.png b/images/3.0x/icon-location-1.png
new file mode 100644
index 00000000..e5d1d315
Binary files /dev/null and b/images/3.0x/icon-location-1.png differ
diff --git a/images/3.0x/icon-location.png b/images/3.0x/icon-location.png
new file mode 100644
index 00000000..148c051a
Binary files /dev/null and b/images/3.0x/icon-location.png differ
diff --git a/images/3.0x/icon-map-view.png b/images/3.0x/icon-map-view.png
new file mode 100644
index 00000000..a7283c0e
Binary files /dev/null and b/images/3.0x/icon-map-view.png differ
diff --git a/images/3.0x/icon-member.png b/images/3.0x/icon-member.png
new file mode 100644
index 00000000..29118a08
Binary files /dev/null and b/images/3.0x/icon-member.png differ
diff --git a/images/3.0x/icon-more-info.png b/images/3.0x/icon-more-info.png
new file mode 100755
index 00000000..0ea16384
Binary files /dev/null and b/images/3.0x/icon-more-info.png differ
diff --git a/images/3.0x/icon-my-illini.png b/images/3.0x/icon-my-illini.png
new file mode 100755
index 00000000..4893ec5b
Binary files /dev/null and b/images/3.0x/icon-my-illini.png differ
diff --git a/images/3.0x/icon-near-you.png b/images/3.0x/icon-near-you.png
new file mode 100755
index 00000000..1e368f4e
Binary files /dev/null and b/images/3.0x/icon-near-you.png differ
diff --git a/images/3.0x/icon-news.png b/images/3.0x/icon-news.png
new file mode 100755
index 00000000..c9add3d0
Binary files /dev/null and b/images/3.0x/icon-news.png differ
diff --git a/images/3.0x/icon-notifications-blue.png b/images/3.0x/icon-notifications-blue.png
new file mode 100644
index 00000000..442099a0
Binary files /dev/null and b/images/3.0x/icon-notifications-blue.png differ
diff --git a/images/3.0x/icon-orange-i.png b/images/3.0x/icon-orange-i.png
new file mode 100644
index 00000000..22306b2e
Binary files /dev/null and b/images/3.0x/icon-orange-i.png differ
diff --git a/images/3.0x/icon-parking.png b/images/3.0x/icon-parking.png
new file mode 100755
index 00000000..7957ef67
Binary files /dev/null and b/images/3.0x/icon-parking.png differ
diff --git a/images/3.0x/icon-passport.png b/images/3.0x/icon-passport.png
new file mode 100644
index 00000000..5d99e468
Binary files /dev/null and b/images/3.0x/icon-passport.png differ
diff --git a/images/3.0x/icon-payment-type-apple-pay.png b/images/3.0x/icon-payment-type-apple-pay.png
new file mode 100644
index 00000000..2e15e5a8
Binary files /dev/null and b/images/3.0x/icon-payment-type-apple-pay.png differ
diff --git a/images/3.0x/icon-payment-type-cache.png b/images/3.0x/icon-payment-type-cache.png
new file mode 100644
index 00000000..e2cb3853
Binary files /dev/null and b/images/3.0x/icon-payment-type-cache.png differ
diff --git a/images/3.0x/icon-payment-type-cafe-credit.png b/images/3.0x/icon-payment-type-cafe-credit.png
new file mode 100644
index 00000000..e21803b1
Binary files /dev/null and b/images/3.0x/icon-payment-type-cafe-credit.png differ
diff --git a/images/3.0x/icon-payment-type-classic-meal.png b/images/3.0x/icon-payment-type-classic-meal.png
new file mode 100644
index 00000000..ae0d9f9f
Binary files /dev/null and b/images/3.0x/icon-payment-type-classic-meal.png differ
diff --git a/images/3.0x/icon-payment-type-credit-card.png b/images/3.0x/icon-payment-type-credit-card.png
new file mode 100644
index 00000000..314602ad
Binary files /dev/null and b/images/3.0x/icon-payment-type-credit-card.png differ
diff --git a/images/3.0x/icon-payment-type-google-pay.png b/images/3.0x/icon-payment-type-google-pay.png
new file mode 100644
index 00000000..68d46dfa
Binary files /dev/null and b/images/3.0x/icon-payment-type-google-pay.png differ
diff --git a/images/3.0x/icon-payment-type-ilini-cash.png b/images/3.0x/icon-payment-type-ilini-cash.png
new file mode 100644
index 00000000..bf144e6a
Binary files /dev/null and b/images/3.0x/icon-payment-type-ilini-cash.png differ
diff --git a/images/3.0x/icon-persona-alumni-normal.png b/images/3.0x/icon-persona-alumni-normal.png
new file mode 100644
index 00000000..a2d4b746
Binary files /dev/null and b/images/3.0x/icon-persona-alumni-normal.png differ
diff --git a/images/3.0x/icon-persona-alumni-selected.png b/images/3.0x/icon-persona-alumni-selected.png
new file mode 100644
index 00000000..035c709f
Binary files /dev/null and b/images/3.0x/icon-persona-alumni-selected.png differ
diff --git a/images/3.0x/icon-persona-athletics-normal.png b/images/3.0x/icon-persona-athletics-normal.png
new file mode 100644
index 00000000..c74dd0b7
Binary files /dev/null and b/images/3.0x/icon-persona-athletics-normal.png differ
diff --git a/images/3.0x/icon-persona-athletics-selected.png b/images/3.0x/icon-persona-athletics-selected.png
new file mode 100644
index 00000000..00a12382
Binary files /dev/null and b/images/3.0x/icon-persona-athletics-selected.png differ
diff --git a/images/3.0x/icon-persona-employee-normal.png b/images/3.0x/icon-persona-employee-normal.png
new file mode 100644
index 00000000..93ded27f
Binary files /dev/null and b/images/3.0x/icon-persona-employee-normal.png differ
diff --git a/images/3.0x/icon-persona-employee-selected.png b/images/3.0x/icon-persona-employee-selected.png
new file mode 100644
index 00000000..46cc6a0a
Binary files /dev/null and b/images/3.0x/icon-persona-employee-selected.png differ
diff --git a/images/3.0x/icon-persona-parent-normal.png b/images/3.0x/icon-persona-parent-normal.png
new file mode 100644
index 00000000..0f04b97a
Binary files /dev/null and b/images/3.0x/icon-persona-parent-normal.png differ
diff --git a/images/3.0x/icon-persona-parent-selected.png b/images/3.0x/icon-persona-parent-selected.png
new file mode 100644
index 00000000..de23120c
Binary files /dev/null and b/images/3.0x/icon-persona-parent-selected.png differ
diff --git a/images/3.0x/icon-persona-resident-normal.png b/images/3.0x/icon-persona-resident-normal.png
new file mode 100644
index 00000000..8405cce9
Binary files /dev/null and b/images/3.0x/icon-persona-resident-normal.png differ
diff --git a/images/3.0x/icon-persona-resident-selected.png b/images/3.0x/icon-persona-resident-selected.png
new file mode 100644
index 00000000..ad2ea362
Binary files /dev/null and b/images/3.0x/icon-persona-resident-selected.png differ
diff --git a/images/3.0x/icon-persona-student-normal.png b/images/3.0x/icon-persona-student-normal.png
new file mode 100644
index 00000000..62157fb1
Binary files /dev/null and b/images/3.0x/icon-persona-student-normal.png differ
diff --git a/images/3.0x/icon-persona-student-selected.png b/images/3.0x/icon-persona-student-selected.png
new file mode 100644
index 00000000..2767e921
Binary files /dev/null and b/images/3.0x/icon-persona-student-selected.png differ
diff --git a/images/3.0x/icon-persona-visitor-normal.png b/images/3.0x/icon-persona-visitor-normal.png
new file mode 100644
index 00000000..04c65581
Binary files /dev/null and b/images/3.0x/icon-persona-visitor-normal.png differ
diff --git a/images/3.0x/icon-persona-visitor-selected.png b/images/3.0x/icon-persona-visitor-selected.png
new file mode 100644
index 00000000..79936fe6
Binary files /dev/null and b/images/3.0x/icon-persona-visitor-selected.png differ
diff --git a/images/3.0x/icon-phone.png b/images/3.0x/icon-phone.png
new file mode 100644
index 00000000..5bd9e974
Binary files /dev/null and b/images/3.0x/icon-phone.png differ
diff --git a/images/3.0x/icon-placeholder-blue.png b/images/3.0x/icon-placeholder-blue.png
new file mode 100755
index 00000000..e6040d46
Binary files /dev/null and b/images/3.0x/icon-placeholder-blue.png differ
diff --git a/images/3.0x/icon-placeholder-empty.png b/images/3.0x/icon-placeholder-empty.png
new file mode 100755
index 00000000..da281501
Binary files /dev/null and b/images/3.0x/icon-placeholder-empty.png differ
diff --git a/images/3.0x/icon-placeholder-navy.png b/images/3.0x/icon-placeholder-navy.png
new file mode 100755
index 00000000..15fcc5e5
Binary files /dev/null and b/images/3.0x/icon-placeholder-navy.png differ
diff --git a/images/3.0x/icon-placeholder-orange.png b/images/3.0x/icon-placeholder-orange.png
new file mode 100755
index 00000000..79df8dce
Binary files /dev/null and b/images/3.0x/icon-placeholder-orange.png differ
diff --git a/images/3.0x/icon-placeholder-teal.png b/images/3.0x/icon-placeholder-teal.png
new file mode 100755
index 00000000..33dce01b
Binary files /dev/null and b/images/3.0x/icon-placeholder-teal.png differ
diff --git a/images/3.0x/icon-placeholder-yellow.png b/images/3.0x/icon-placeholder-yellow.png
new file mode 100755
index 00000000..833ac84c
Binary files /dev/null and b/images/3.0x/icon-placeholder-yellow.png differ
diff --git a/images/3.0x/icon-plus.png b/images/3.0x/icon-plus.png
new file mode 100755
index 00000000..f4be4e84
Binary files /dev/null and b/images/3.0x/icon-plus.png differ
diff --git a/images/3.0x/icon-poi.png b/images/3.0x/icon-poi.png
new file mode 100644
index 00000000..f9ab8cb6
Binary files /dev/null and b/images/3.0x/icon-poi.png differ
diff --git a/images/3.0x/icon-privacy.png b/images/3.0x/icon-privacy.png
new file mode 100755
index 00000000..440633f9
Binary files /dev/null and b/images/3.0x/icon-privacy.png differ
diff --git a/images/3.0x/icon-quickpoll.png b/images/3.0x/icon-quickpoll.png
new file mode 100755
index 00000000..879e7414
Binary files /dev/null and b/images/3.0x/icon-quickpoll.png differ
diff --git a/images/3.0x/icon-recurring-event.png b/images/3.0x/icon-recurring-event.png
new file mode 100755
index 00000000..02f609d9
Binary files /dev/null and b/images/3.0x/icon-recurring-event.png differ
diff --git a/images/3.0x/icon-reminder.png b/images/3.0x/icon-reminder.png
new file mode 100755
index 00000000..902a3240
Binary files /dev/null and b/images/3.0x/icon-reminder.png differ
diff --git a/images/3.0x/icon-report-test.png b/images/3.0x/icon-report-test.png
new file mode 100644
index 00000000..00dd791c
Binary files /dev/null and b/images/3.0x/icon-report-test.png differ
diff --git a/images/3.0x/icon-saved-white.png b/images/3.0x/icon-saved-white.png
new file mode 100755
index 00000000..805c56ac
Binary files /dev/null and b/images/3.0x/icon-saved-white.png differ
diff --git a/images/3.0x/icon-saved.png b/images/3.0x/icon-saved.png
new file mode 100755
index 00000000..f2ee84e7
Binary files /dev/null and b/images/3.0x/icon-saved.png differ
diff --git a/images/3.0x/icon-schedule.png b/images/3.0x/icon-schedule.png
new file mode 100755
index 00000000..c4a5d49c
Binary files /dev/null and b/images/3.0x/icon-schedule.png differ
diff --git a/images/3.0x/icon-search.png b/images/3.0x/icon-search.png
new file mode 100755
index 00000000..5df2afb5
Binary files /dev/null and b/images/3.0x/icon-search.png differ
diff --git a/images/3.0x/icon-selected-checkbox.png b/images/3.0x/icon-selected-checkbox.png
new file mode 100644
index 00000000..4ea96793
Binary files /dev/null and b/images/3.0x/icon-selected-checkbox.png differ
diff --git a/images/3.0x/icon-selected.png b/images/3.0x/icon-selected.png
new file mode 100755
index 00000000..c6f67cbd
Binary files /dev/null and b/images/3.0x/icon-selected.png differ
diff --git a/images/3.0x/icon-separate-people.png b/images/3.0x/icon-separate-people.png
new file mode 100644
index 00000000..97226231
Binary files /dev/null and b/images/3.0x/icon-separate-people.png differ
diff --git a/images/3.0x/icon-settings.png b/images/3.0x/icon-settings.png
new file mode 100755
index 00000000..33c0a645
Binary files /dev/null and b/images/3.0x/icon-settings.png differ
diff --git a/images/3.0x/icon-social-distance.png b/images/3.0x/icon-social-distance.png
new file mode 100644
index 00000000..edc28477
Binary files /dev/null and b/images/3.0x/icon-social-distance.png differ
diff --git a/images/3.0x/icon-star-selected.png b/images/3.0x/icon-star-selected.png
new file mode 100755
index 00000000..38a89ea8
Binary files /dev/null and b/images/3.0x/icon-star-selected.png differ
diff --git a/images/3.0x/icon-star-solid.png b/images/3.0x/icon-star-solid.png
new file mode 100755
index 00000000..2df150de
Binary files /dev/null and b/images/3.0x/icon-star-solid.png differ
diff --git a/images/3.0x/icon-star-white.png b/images/3.0x/icon-star-white.png
new file mode 100644
index 00000000..67c17972
Binary files /dev/null and b/images/3.0x/icon-star-white.png differ
diff --git a/images/3.0x/icon-star.png b/images/3.0x/icon-star.png
new file mode 100644
index 00000000..0c304e22
Binary files /dev/null and b/images/3.0x/icon-star.png differ
diff --git a/images/3.0x/icon-stay-at-home.png b/images/3.0x/icon-stay-at-home.png
new file mode 100644
index 00000000..bbfe75c2
Binary files /dev/null and b/images/3.0x/icon-stay-at-home.png differ
diff --git a/images/3.0x/icon-stehoscope.png b/images/3.0x/icon-stehoscope.png
new file mode 100644
index 00000000..f88b51b3
Binary files /dev/null and b/images/3.0x/icon-stehoscope.png differ
diff --git a/images/3.0x/icon-team.png b/images/3.0x/icon-team.png
new file mode 100755
index 00000000..fd30c1d8
Binary files /dev/null and b/images/3.0x/icon-team.png differ
diff --git a/images/3.0x/icon-test-history.png b/images/3.0x/icon-test-history.png
new file mode 100644
index 00000000..8ffdeaf9
Binary files /dev/null and b/images/3.0x/icon-test-history.png differ
diff --git a/images/3.0x/icon-time.png b/images/3.0x/icon-time.png
new file mode 100644
index 00000000..a8e76599
Binary files /dev/null and b/images/3.0x/icon-time.png differ
diff --git a/images/3.0x/icon-unselected.png b/images/3.0x/icon-unselected.png
new file mode 100755
index 00000000..3a5f35a6
Binary files /dev/null and b/images/3.0x/icon-unselected.png differ
diff --git a/images/3.0x/icon-up.png b/images/3.0x/icon-up.png
new file mode 100755
index 00000000..3e035501
Binary files /dev/null and b/images/3.0x/icon-up.png differ
diff --git a/images/3.0x/icon-washer-big.png b/images/3.0x/icon-washer-big.png
new file mode 100755
index 00000000..663898dd
Binary files /dev/null and b/images/3.0x/icon-washer-big.png differ
diff --git a/images/3.0x/icon-washer-small.png b/images/3.0x/icon-washer-small.png
new file mode 100755
index 00000000..14b5bc99
Binary files /dev/null and b/images/3.0x/icon-washer-small.png differ
diff --git a/images/3.0x/icon-washer.png b/images/3.0x/icon-washer.png
new file mode 100755
index 00000000..222e9314
Binary files /dev/null and b/images/3.0x/icon-washer.png differ
diff --git a/images/3.0x/icon-watch.png b/images/3.0x/icon-watch.png
new file mode 100755
index 00000000..1cd4eae9
Binary files /dev/null and b/images/3.0x/icon-watch.png differ
diff --git a/images/3.0x/icon-x-orange-small.png b/images/3.0x/icon-x-orange-small.png
new file mode 100755
index 00000000..956db8b9
Binary files /dev/null and b/images/3.0x/icon-x-orange-small.png differ
diff --git a/images/3.0x/icon-x-orange.png b/images/3.0x/icon-x-orange.png
new file mode 100755
index 00000000..b8c766ab
Binary files /dev/null and b/images/3.0x/icon-x-orange.png differ
diff --git a/images/3.0x/icon-your-care-team.png b/images/3.0x/icon-your-care-team.png
new file mode 100644
index 00000000..aed6e605
Binary files /dev/null and b/images/3.0x/icon-your-care-team.png differ
diff --git a/images/3.0x/ig-20x20.png b/images/3.0x/ig-20x20.png
new file mode 100644
index 00000000..d59bca0f
Binary files /dev/null and b/images/3.0x/ig-20x20.png differ
diff --git a/images/3.0x/ig-24x24.png b/images/3.0x/ig-24x24.png
new file mode 100644
index 00000000..a0d04675
Binary files /dev/null and b/images/3.0x/ig-24x24.png differ
diff --git a/images/3.0x/ig.png b/images/3.0x/ig.png
new file mode 100755
index 00000000..6689ebe7
Binary files /dev/null and b/images/3.0x/ig.png differ
diff --git a/images/3.0x/ilini-cash.png b/images/3.0x/ilini-cash.png
new file mode 100644
index 00000000..6c3d9f4c
Binary files /dev/null and b/images/3.0x/ilini-cash.png differ
diff --git a/images/3.0x/kognito.png b/images/3.0x/kognito.png
new file mode 100644
index 00000000..1c1935d3
Binary files /dev/null and b/images/3.0x/kognito.png differ
diff --git a/images/3.0x/link-out.png b/images/3.0x/link-out.png
new file mode 100644
index 00000000..b5d1c897
Binary files /dev/null and b/images/3.0x/link-out.png differ
diff --git a/images/3.0x/login-header.png b/images/3.0x/login-header.png
new file mode 100644
index 00000000..caa6866e
Binary files /dev/null and b/images/3.0x/login-header.png differ
diff --git a/images/3.0x/mc-kinley-gray.png b/images/3.0x/mc-kinley-gray.png
new file mode 100644
index 00000000..e8a24564
Binary files /dev/null and b/images/3.0x/mc-kinley-gray.png differ
diff --git a/images/3.0x/member.png b/images/3.0x/member.png
new file mode 100644
index 00000000..90177842
Binary files /dev/null and b/images/3.0x/member.png differ
diff --git a/images/3.0x/mental.png b/images/3.0x/mental.png
new file mode 100644
index 00000000..96e62310
Binary files /dev/null and b/images/3.0x/mental.png differ
diff --git a/images/3.0x/mtd-logo.png b/images/3.0x/mtd-logo.png
new file mode 100644
index 00000000..2ada4825
Binary files /dev/null and b/images/3.0x/mtd-logo.png differ
diff --git a/images/3.0x/my-illini-orange.png b/images/3.0x/my-illini-orange.png
new file mode 100644
index 00000000..7b03be42
Binary files /dev/null and b/images/3.0x/my-illini-orange.png differ
diff --git a/images/3.0x/navy.png b/images/3.0x/navy.png
new file mode 100755
index 00000000..967cb994
Binary files /dev/null and b/images/3.0x/navy.png differ
diff --git a/images/3.0x/near-you.png b/images/3.0x/near-you.png
new file mode 100644
index 00000000..3c1bbd34
Binary files /dev/null and b/images/3.0x/near-you.png differ
diff --git a/images/3.0x/onboarding-back-btn.png b/images/3.0x/onboarding-back-btn.png
new file mode 100644
index 00000000..d631d518
Binary files /dev/null and b/images/3.0x/onboarding-back-btn.png differ
diff --git a/images/3.0x/osf-logo-gray.png b/images/3.0x/osf-logo-gray.png
new file mode 100644
index 00000000..5da5745b
Binary files /dev/null and b/images/3.0x/osf-logo-gray.png differ
diff --git a/images/3.0x/path.png b/images/3.0x/path.png
new file mode 100644
index 00000000..fcb4b98c
Binary files /dev/null and b/images/3.0x/path.png differ
diff --git a/images/3.0x/pending.png b/images/3.0x/pending.png
new file mode 100644
index 00000000..e8c85275
Binary files /dev/null and b/images/3.0x/pending.png differ
diff --git a/images/3.0x/physical.png b/images/3.0x/physical.png
new file mode 100644
index 00000000..06946728
Binary files /dev/null and b/images/3.0x/physical.png differ
diff --git a/images/3.0x/pledge.png b/images/3.0x/pledge.png
new file mode 100644
index 00000000..965e6844
Binary files /dev/null and b/images/3.0x/pledge.png differ
diff --git a/images/3.0x/powered-by.png b/images/3.0x/powered-by.png
new file mode 100644
index 00000000..e14c349f
Binary files /dev/null and b/images/3.0x/powered-by.png differ
diff --git a/images/3.0x/privacy-header.png b/images/3.0x/privacy-header.png
new file mode 100644
index 00000000..5be65a28
Binary files /dev/null and b/images/3.0x/privacy-header.png differ
diff --git a/images/3.0x/privacy.png b/images/3.0x/privacy.png
new file mode 100644
index 00000000..2bd81e31
Binary files /dev/null and b/images/3.0x/privacy.png differ
diff --git a/images/3.0x/provider.png b/images/3.0x/provider.png
new file mode 100644
index 00000000..fbca851d
Binary files /dev/null and b/images/3.0x/provider.png differ
diff --git a/images/3.0x/reflection.png b/images/3.0x/reflection.png
new file mode 100644
index 00000000..f8afd166
Binary files /dev/null and b/images/3.0x/reflection.png differ
diff --git a/images/3.0x/reminder.png b/images/3.0x/reminder.png
new file mode 100755
index 00000000..902a3240
Binary files /dev/null and b/images/3.0x/reminder.png differ
diff --git a/images/3.0x/safer-illinois.png b/images/3.0x/safer-illinois.png
new file mode 100644
index 00000000..2809a45a
Binary files /dev/null and b/images/3.0x/safer-illinois.png differ
diff --git a/images/3.0x/schedule-orange.png b/images/3.0x/schedule-orange.png
new file mode 100755
index 00000000..c93545a4
Binary files /dev/null and b/images/3.0x/schedule-orange.png differ
diff --git a/images/3.0x/selected-orange.png b/images/3.0x/selected-orange.png
new file mode 100644
index 00000000..c6f67cbd
Binary files /dev/null and b/images/3.0x/selected-orange.png differ
diff --git a/images/3.0x/selected.png b/images/3.0x/selected.png
new file mode 100755
index 00000000..dcdad6ad
Binary files /dev/null and b/images/3.0x/selected.png differ
diff --git a/images/3.0x/settings-white.png b/images/3.0x/settings-white.png
new file mode 100644
index 00000000..cbb15a37
Binary files /dev/null and b/images/3.0x/settings-white.png differ
diff --git a/images/3.0x/slant-down-right-blue-rotated.png b/images/3.0x/slant-down-right-blue-rotated.png
new file mode 100644
index 00000000..acfd15f4
Binary files /dev/null and b/images/3.0x/slant-down-right-blue-rotated.png differ
diff --git a/images/3.0x/slant-down-right-blue.png b/images/3.0x/slant-down-right-blue.png
new file mode 100644
index 00000000..24862c14
Binary files /dev/null and b/images/3.0x/slant-down-right-blue.png differ
diff --git a/images/3.0x/small-add-orange.png b/images/3.0x/small-add-orange.png
new file mode 100644
index 00000000..621d5692
Binary files /dev/null and b/images/3.0x/small-add-orange.png differ
diff --git a/images/3.0x/small-add.png b/images/3.0x/small-add.png
new file mode 100644
index 00000000..e6e58a49
Binary files /dev/null and b/images/3.0x/small-add.png differ
diff --git a/images/3.0x/social.png b/images/3.0x/social.png
new file mode 100644
index 00000000..655cbaf1
Binary files /dev/null and b/images/3.0x/social.png differ
diff --git a/images/3.0x/spiritual.png b/images/3.0x/spiritual.png
new file mode 100644
index 00000000..f07f4eec
Binary files /dev/null and b/images/3.0x/spiritual.png differ
diff --git a/images/3.0x/switch-off.png b/images/3.0x/switch-off.png
new file mode 100644
index 00000000..3b22f8be
Binary files /dev/null and b/images/3.0x/switch-off.png differ
diff --git a/images/3.0x/switch-on.png b/images/3.0x/switch-on.png
new file mode 100644
index 00000000..b76f8626
Binary files /dev/null and b/images/3.0x/switch-on.png differ
diff --git a/images/3.0x/tab-browse-selected.png b/images/3.0x/tab-browse-selected.png
new file mode 100644
index 00000000..5183cebd
Binary files /dev/null and b/images/3.0x/tab-browse-selected.png differ
diff --git a/images/3.0x/tab-browse.png b/images/3.0x/tab-browse.png
new file mode 100644
index 00000000..1b029b29
Binary files /dev/null and b/images/3.0x/tab-browse.png differ
diff --git a/images/3.0x/tab-explore.png b/images/3.0x/tab-explore.png
new file mode 100755
index 00000000..b396b5f5
Binary files /dev/null and b/images/3.0x/tab-explore.png differ
diff --git a/images/3.0x/tab-home-selected.png b/images/3.0x/tab-home-selected.png
new file mode 100755
index 00000000..ef32f501
Binary files /dev/null and b/images/3.0x/tab-home-selected.png differ
diff --git a/images/3.0x/tab-home.png b/images/3.0x/tab-home.png
new file mode 100755
index 00000000..2aed3229
Binary files /dev/null and b/images/3.0x/tab-home.png differ
diff --git a/images/3.0x/tab-more-selected.png b/images/3.0x/tab-more-selected.png
new file mode 100755
index 00000000..00489a69
Binary files /dev/null and b/images/3.0x/tab-more-selected.png differ
diff --git a/images/3.0x/tab-more.png b/images/3.0x/tab-more.png
new file mode 100755
index 00000000..1ded14ea
Binary files /dev/null and b/images/3.0x/tab-more.png differ
diff --git a/images/3.0x/tab-saved-selected.png b/images/3.0x/tab-saved-selected.png
new file mode 100755
index 00000000..50d789b6
Binary files /dev/null and b/images/3.0x/tab-saved-selected.png differ
diff --git a/images/3.0x/tab-saved.png b/images/3.0x/tab-saved.png
new file mode 100755
index 00000000..f2ee84e7
Binary files /dev/null and b/images/3.0x/tab-saved.png differ
diff --git a/images/3.0x/tab-wallet.png b/images/3.0x/tab-wallet.png
new file mode 100644
index 00000000..bafafd72
Binary files /dev/null and b/images/3.0x/tab-wallet.png differ
diff --git a/images/3.0x/teal.png b/images/3.0x/teal.png
new file mode 100644
index 00000000..aeb60873
Binary files /dev/null and b/images/3.0x/teal.png differ
diff --git a/images/3.0x/tickets_yellow.png b/images/3.0x/tickets_yellow.png
new file mode 100644
index 00000000..001a1ea9
Binary files /dev/null and b/images/3.0x/tickets_yellow.png differ
diff --git a/images/3.0x/twitter-20x18.png b/images/3.0x/twitter-20x18.png
new file mode 100644
index 00000000..5e8a9761
Binary files /dev/null and b/images/3.0x/twitter-20x18.png differ
diff --git a/images/3.0x/twitter-24x22.png b/images/3.0x/twitter-24x22.png
new file mode 100644
index 00000000..3c6a6cd5
Binary files /dev/null and b/images/3.0x/twitter-24x22.png differ
diff --git a/images/3.0x/twitter.png b/images/3.0x/twitter.png
new file mode 100755
index 00000000..ca0f7028
Binary files /dev/null and b/images/3.0x/twitter.png differ
diff --git a/images/3.0x/u.png b/images/3.0x/u.png
new file mode 100644
index 00000000..0a5f5ce8
Binary files /dev/null and b/images/3.0x/u.png differ
diff --git a/images/3.0x/upcoming_events_orange.png b/images/3.0x/upcoming_events_orange.png
new file mode 100644
index 00000000..4b85bfb5
Binary files /dev/null and b/images/3.0x/upcoming_events_orange.png differ
diff --git a/images/3.0x/user-check.png b/images/3.0x/user-check.png
new file mode 100644
index 00000000..942da3fd
Binary files /dev/null and b/images/3.0x/user-check.png differ
diff --git a/images/3.0x/vocational.png b/images/3.0x/vocational.png
new file mode 100644
index 00000000..5feaef4e
Binary files /dev/null and b/images/3.0x/vocational.png differ
diff --git a/images/3.0x/warning-orange.png b/images/3.0x/warning-orange.png
new file mode 100644
index 00000000..abfe90f5
Binary files /dev/null and b/images/3.0x/warning-orange.png differ
diff --git a/images/3.0x/you-tube-20x15.png b/images/3.0x/you-tube-20x15.png
new file mode 100644
index 00000000..5280211e
Binary files /dev/null and b/images/3.0x/you-tube-20x15.png differ
diff --git a/images/3.0x/you-tube.png b/images/3.0x/you-tube.png
new file mode 100755
index 00000000..958a543a
Binary files /dev/null and b/images/3.0x/you-tube.png differ
diff --git a/images/activate-menu.png b/images/activate-menu.png
new file mode 100755
index 00000000..2f6a4440
Binary files /dev/null and b/images/activate-menu.png differ
diff --git a/images/add-to-apple-wallet.png b/images/add-to-apple-wallet.png
new file mode 100644
index 00000000..9bc60171
Binary files /dev/null and b/images/add-to-apple-wallet.png differ
diff --git a/images/allow-notifications-header.png b/images/allow-notifications-header.png
new file mode 100644
index 00000000..bec2016f
Binary files /dev/null and b/images/allow-notifications-header.png differ
diff --git a/images/athletics-baseball-orange.png b/images/athletics-baseball-orange.png
new file mode 100644
index 00000000..bd3d9d25
Binary files /dev/null and b/images/athletics-baseball-orange.png differ
diff --git a/images/athletics-baseball-white.png b/images/athletics-baseball-white.png
new file mode 100644
index 00000000..72c433e3
Binary files /dev/null and b/images/athletics-baseball-white.png differ
diff --git a/images/athletics-basketball-orange.png b/images/athletics-basketball-orange.png
new file mode 100644
index 00000000..f4a61487
Binary files /dev/null and b/images/athletics-basketball-orange.png differ
diff --git a/images/athletics-basketball-white.png b/images/athletics-basketball-white.png
new file mode 100644
index 00000000..4195e02b
Binary files /dev/null and b/images/athletics-basketball-white.png differ
diff --git a/images/athletics-cross-orange.png b/images/athletics-cross-orange.png
new file mode 100644
index 00000000..fca63443
Binary files /dev/null and b/images/athletics-cross-orange.png differ
diff --git a/images/athletics-cross-white.png b/images/athletics-cross-white.png
new file mode 100644
index 00000000..4cd1a497
Binary files /dev/null and b/images/athletics-cross-white.png differ
diff --git a/images/athletics-football-orange.png b/images/athletics-football-orange.png
new file mode 100644
index 00000000..88bb2035
Binary files /dev/null and b/images/athletics-football-orange.png differ
diff --git a/images/athletics-football-white.png b/images/athletics-football-white.png
new file mode 100644
index 00000000..63ef3f76
Binary files /dev/null and b/images/athletics-football-white.png differ
diff --git a/images/athletics-golf-orange.png b/images/athletics-golf-orange.png
new file mode 100644
index 00000000..e5d8682c
Binary files /dev/null and b/images/athletics-golf-orange.png differ
diff --git a/images/athletics-golf-white.png b/images/athletics-golf-white.png
new file mode 100644
index 00000000..b5014004
Binary files /dev/null and b/images/athletics-golf-white.png differ
diff --git a/images/athletics-gymnastics-orange.png b/images/athletics-gymnastics-orange.png
new file mode 100644
index 00000000..575ba6db
Binary files /dev/null and b/images/athletics-gymnastics-orange.png differ
diff --git a/images/athletics-gymnastics-white.png b/images/athletics-gymnastics-white.png
new file mode 100644
index 00000000..3cd25275
Binary files /dev/null and b/images/athletics-gymnastics-white.png differ
diff --git a/images/athletics-handball-orange.png b/images/athletics-handball-orange.png
new file mode 100644
index 00000000..058bd92c
Binary files /dev/null and b/images/athletics-handball-orange.png differ
diff --git a/images/athletics-handball-white.png b/images/athletics-handball-white.png
new file mode 100644
index 00000000..fea8f04e
Binary files /dev/null and b/images/athletics-handball-white.png differ
diff --git a/images/athletics-soccer-orange.png b/images/athletics-soccer-orange.png
new file mode 100644
index 00000000..75355ea0
Binary files /dev/null and b/images/athletics-soccer-orange.png differ
diff --git a/images/athletics-soccer-white.png b/images/athletics-soccer-white.png
new file mode 100644
index 00000000..972e0896
Binary files /dev/null and b/images/athletics-soccer-white.png differ
diff --git a/images/athletics-softball-orange.png b/images/athletics-softball-orange.png
new file mode 100644
index 00000000..bd3d9d25
Binary files /dev/null and b/images/athletics-softball-orange.png differ
diff --git a/images/athletics-swim-orange.png b/images/athletics-swim-orange.png
new file mode 100644
index 00000000..3c073c97
Binary files /dev/null and b/images/athletics-swim-orange.png differ
diff --git a/images/athletics-swim-white.png b/images/athletics-swim-white.png
new file mode 100644
index 00000000..d880dd6d
Binary files /dev/null and b/images/athletics-swim-white.png differ
diff --git a/images/athletics-tennis-orange.png b/images/athletics-tennis-orange.png
new file mode 100644
index 00000000..8daba745
Binary files /dev/null and b/images/athletics-tennis-orange.png differ
diff --git a/images/athletics-tennis-white.png b/images/athletics-tennis-white.png
new file mode 100644
index 00000000..c545d9b4
Binary files /dev/null and b/images/athletics-tennis-white.png differ
diff --git a/images/athletics-track-orange.png b/images/athletics-track-orange.png
new file mode 100644
index 00000000..f95e5cee
Binary files /dev/null and b/images/athletics-track-orange.png differ
diff --git a/images/athletics-track-white.png b/images/athletics-track-white.png
new file mode 100644
index 00000000..50048c3b
Binary files /dev/null and b/images/athletics-track-white.png differ
diff --git a/images/athletics-volleyball-orange.png b/images/athletics-volleyball-orange.png
new file mode 100644
index 00000000..058bd92c
Binary files /dev/null and b/images/athletics-volleyball-orange.png differ
diff --git a/images/athletics-wrestling-orange.png b/images/athletics-wrestling-orange.png
new file mode 100644
index 00000000..1be6ef4c
Binary files /dev/null and b/images/athletics-wrestling-orange.png differ
diff --git a/images/athletics-wrestling-white.png b/images/athletics-wrestling-white.png
new file mode 100644
index 00000000..6007de5f
Binary files /dev/null and b/images/athletics-wrestling-white.png differ
diff --git a/images/background-image.png b/images/background-image.png
new file mode 100644
index 00000000..2733ff90
Binary files /dev/null and b/images/background-image.png differ
diff --git a/images/background-onboarding-squares-dark.png b/images/background-onboarding-squares-dark.png
new file mode 100644
index 00000000..2c34de06
Binary files /dev/null and b/images/background-onboarding-squares-dark.png differ
diff --git a/images/background-onboarding-squares-light.png b/images/background-onboarding-squares-light.png
new file mode 100644
index 00000000..2c415841
Binary files /dev/null and b/images/background-onboarding-squares-light.png differ
diff --git a/images/background-onboarding-squares.png b/images/background-onboarding-squares.png
new file mode 100644
index 00000000..b9fa6192
Binary files /dev/null and b/images/background-onboarding-squares.png differ
diff --git a/images/block-i-orange.png b/images/block-i-orange.png
new file mode 100755
index 00000000..d5b9e6a1
Binary files /dev/null and b/images/block-i-orange.png differ
diff --git a/images/button-plus-orange.png b/images/button-plus-orange.png
new file mode 100755
index 00000000..c2e91fa7
Binary files /dev/null and b/images/button-plus-orange.png differ
diff --git a/images/campus-tools-blue.png b/images/campus-tools-blue.png
new file mode 100644
index 00000000..0b405341
Binary files /dev/null and b/images/campus-tools-blue.png differ
diff --git a/images/campus-tools.png b/images/campus-tools.png
new file mode 100644
index 00000000..9fa4e128
Binary files /dev/null and b/images/campus-tools.png differ
diff --git a/images/card-image-placeholder.jpeg b/images/card-image-placeholder.jpeg
new file mode 100644
index 00000000..e7e915ea
Binary files /dev/null and b/images/card-image-placeholder.jpeg differ
diff --git a/images/certified-copy.png b/images/certified-copy.png
new file mode 100644
index 00000000..ba537321
Binary files /dev/null and b/images/certified-copy.png differ
diff --git a/images/certified.png b/images/certified.png
new file mode 100644
index 00000000..2991d5cd
Binary files /dev/null and b/images/certified.png differ
diff --git a/images/checkbox-selected.png b/images/checkbox-selected.png
new file mode 100644
index 00000000..7df5ad48
Binary files /dev/null and b/images/checkbox-selected.png differ
diff --git a/images/checkbox-small.png b/images/checkbox-small.png
new file mode 100644
index 00000000..ff9a1beb
Binary files /dev/null and b/images/checkbox-small.png differ
diff --git a/images/checkbox-unselected.png b/images/checkbox-unselected.png
new file mode 100644
index 00000000..2fea7953
Binary files /dev/null and b/images/checkbox-unselected.png differ
diff --git a/images/chevron-blue-right.png b/images/chevron-blue-right.png
new file mode 100755
index 00000000..e47f6b1d
Binary files /dev/null and b/images/chevron-blue-right.png differ
diff --git a/images/chevron-down.png b/images/chevron-down.png
new file mode 100644
index 00000000..d17a3abc
Binary files /dev/null and b/images/chevron-down.png differ
diff --git a/images/chevron-left-blue.png b/images/chevron-left-blue.png
new file mode 100755
index 00000000..08f5a8fc
Binary files /dev/null and b/images/chevron-left-blue.png differ
diff --git a/images/chevron-left-gray.png b/images/chevron-left-gray.png
new file mode 100644
index 00000000..f12124d6
Binary files /dev/null and b/images/chevron-left-gray.png differ
diff --git a/images/chevron-left-white.png b/images/chevron-left-white.png
new file mode 100644
index 00000000..0bf34aba
Binary files /dev/null and b/images/chevron-left-white.png differ
diff --git a/images/chevron-left.png b/images/chevron-left.png
new file mode 100644
index 00000000..b116c219
Binary files /dev/null and b/images/chevron-left.png differ
diff --git a/images/chevron-right.png b/images/chevron-right.png
new file mode 100755
index 00000000..831a1bcf
Binary files /dev/null and b/images/chevron-right.png differ
diff --git a/images/chevron-up.png b/images/chevron-up.png
new file mode 100644
index 00000000..575f2a91
Binary files /dev/null and b/images/chevron-up.png differ
diff --git a/images/chevron2-down.png b/images/chevron2-down.png
new file mode 100755
index 00000000..d2b7fe7e
Binary files /dev/null and b/images/chevron2-down.png differ
diff --git a/images/classic-meal-blue.png b/images/classic-meal-blue.png
new file mode 100644
index 00000000..74df2598
Binary files /dev/null and b/images/classic-meal-blue.png differ
diff --git a/images/classic-meal-orange.png b/images/classic-meal-orange.png
new file mode 100755
index 00000000..39c55632
Binary files /dev/null and b/images/classic-meal-orange.png differ
diff --git a/images/close-blue.png b/images/close-blue.png
new file mode 100644
index 00000000..b2ef1590
Binary files /dev/null and b/images/close-blue.png differ
diff --git a/images/close-gray.png b/images/close-gray.png
new file mode 100644
index 00000000..6970c0c6
Binary files /dev/null and b/images/close-gray.png differ
diff --git a/images/close-menu.png b/images/close-menu.png
new file mode 100644
index 00000000..437037a0
Binary files /dev/null and b/images/close-menu.png differ
diff --git a/images/close-orange-large.png b/images/close-orange-large.png
new file mode 100644
index 00000000..172e1eb5
Binary files /dev/null and b/images/close-orange-large.png differ
diff --git a/images/close-orange.png b/images/close-orange.png
new file mode 100755
index 00000000..fdf884a4
Binary files /dev/null and b/images/close-orange.png differ
diff --git a/images/close-white-large.png b/images/close-white-large.png
new file mode 100644
index 00000000..e5ac1f93
Binary files /dev/null and b/images/close-white-large.png differ
diff --git a/images/close-white-shadow.png b/images/close-white-shadow.png
new file mode 100644
index 00000000..256a9300
Binary files /dev/null and b/images/close-white-shadow.png differ
diff --git a/images/close-white.png b/images/close-white.png
new file mode 100644
index 00000000..771316e4
Binary files /dev/null and b/images/close-white.png differ
diff --git a/images/covid.png b/images/covid.png
new file mode 100644
index 00000000..1a7a6cf1
Binary files /dev/null and b/images/covid.png differ
diff --git a/images/covid19-header-blue.png b/images/covid19-header-blue.png
new file mode 100644
index 00000000..d2757760
Binary files /dev/null and b/images/covid19-header-blue.png differ
diff --git a/images/covid19-orange.png b/images/covid19-orange.png
new file mode 100644
index 00000000..81bdb97e
Binary files /dev/null and b/images/covid19-orange.png differ
diff --git a/images/deselected-dark.png b/images/deselected-dark.png
new file mode 100755
index 00000000..9b949216
Binary files /dev/null and b/images/deselected-dark.png differ
diff --git a/images/deselected.png b/images/deselected.png
new file mode 100755
index 00000000..a818d1ca
Binary files /dev/null and b/images/deselected.png differ
diff --git a/images/disabled.png b/images/disabled.png
new file mode 100755
index 00000000..2d372bb4
Binary files /dev/null and b/images/disabled.png differ
diff --git a/images/emotional.png b/images/emotional.png
new file mode 100644
index 00000000..47959743
Binary files /dev/null and b/images/emotional.png differ
diff --git a/images/enable-bluetooth-header.png b/images/enable-bluetooth-header.png
new file mode 100644
index 00000000..855b1e8a
Binary files /dev/null and b/images/enable-bluetooth-header.png differ
diff --git a/images/environmental.png b/images/environmental.png
new file mode 100644
index 00000000..d1e7e374
Binary files /dev/null and b/images/environmental.png differ
diff --git a/images/example.png b/images/example.png
new file mode 100644
index 00000000..535a1503
Binary files /dev/null and b/images/example.png differ
diff --git a/images/explore.png b/images/explore.png
new file mode 100644
index 00000000..29b16874
Binary files /dev/null and b/images/explore.png differ
diff --git a/images/external-link.png b/images/external-link.png
new file mode 100644
index 00000000..05fb7ae2
Binary files /dev/null and b/images/external-link.png differ
diff --git a/images/fb-10x20.png b/images/fb-10x20.png
new file mode 100644
index 00000000..691fc278
Binary files /dev/null and b/images/fb-10x20.png differ
diff --git a/images/fb-12x24.png b/images/fb-12x24.png
new file mode 100644
index 00000000..d22ed2a2
Binary files /dev/null and b/images/fb-12x24.png differ
diff --git a/images/fb-16x32.png b/images/fb-16x32.png
new file mode 100755
index 00000000..f6f4477b
Binary files /dev/null and b/images/fb-16x32.png differ
diff --git a/images/fill-1.png b/images/fill-1.png
new file mode 100644
index 00000000..d6a0a00c
Binary files /dev/null and b/images/fill-1.png differ
diff --git a/images/financial-2.png b/images/financial-2.png
new file mode 100644
index 00000000..2e2a1341
Binary files /dev/null and b/images/financial-2.png differ
diff --git a/images/financial.png b/images/financial.png
new file mode 100644
index 00000000..eade2d3d
Binary files /dev/null and b/images/financial.png differ
diff --git a/images/game_day_blue.png b/images/game_day_blue.png
new file mode 100644
index 00000000..6c594496
Binary files /dev/null and b/images/game_day_blue.png differ
diff --git a/images/globe.png b/images/globe.png
new file mode 100644
index 00000000..886ff1f9
Binary files /dev/null and b/images/globe.png differ
diff --git a/images/group-10.png b/images/group-10.png
new file mode 100644
index 00000000..e1d42a99
Binary files /dev/null and b/images/group-10.png differ
diff --git a/images/group-15.png b/images/group-15.png
new file mode 100644
index 00000000..eb4ccd92
Binary files /dev/null and b/images/group-15.png differ
diff --git a/images/group-16.png b/images/group-16.png
new file mode 100644
index 00000000..645ea569
Binary files /dev/null and b/images/group-16.png differ
diff --git a/images/group-18.png b/images/group-18.png
new file mode 100644
index 00000000..42a74b47
Binary files /dev/null and b/images/group-18.png differ
diff --git a/images/group-2.png b/images/group-2.png
new file mode 100644
index 00000000..5330b2c3
Binary files /dev/null and b/images/group-2.png differ
diff --git a/images/group-20.png b/images/group-20.png
new file mode 100644
index 00000000..fd3aa359
Binary files /dev/null and b/images/group-20.png differ
diff --git a/images/group-25.png b/images/group-25.png
new file mode 100644
index 00000000..43fc4d1d
Binary files /dev/null and b/images/group-25.png differ
diff --git a/images/group-28.png b/images/group-28.png
new file mode 100644
index 00000000..2f603ebd
Binary files /dev/null and b/images/group-28.png differ
diff --git a/images/group-3.png b/images/group-3.png
new file mode 100644
index 00000000..de451f21
Binary files /dev/null and b/images/group-3.png differ
diff --git a/images/group-4.png b/images/group-4.png
new file mode 100644
index 00000000..e99bead2
Binary files /dev/null and b/images/group-4.png differ
diff --git a/images/group-44.png b/images/group-44.png
new file mode 100644
index 00000000..e6255092
Binary files /dev/null and b/images/group-44.png differ
diff --git a/images/group-444.png b/images/group-444.png
new file mode 100644
index 00000000..02357db5
Binary files /dev/null and b/images/group-444.png differ
diff --git a/images/group-5-blue.png b/images/group-5-blue.png
new file mode 100644
index 00000000..a39e8c4c
Binary files /dev/null and b/images/group-5-blue.png differ
diff --git a/images/group-5-white.png b/images/group-5-white.png
new file mode 100644
index 00000000..901ba403
Binary files /dev/null and b/images/group-5-white.png differ
diff --git a/images/group-7.png b/images/group-7.png
new file mode 100644
index 00000000..927009a4
Binary files /dev/null and b/images/group-7.png differ
diff --git a/images/group-8.png b/images/group-8.png
new file mode 100644
index 00000000..fe2039bf
Binary files /dev/null and b/images/group-8.png differ
diff --git a/images/group-9.png b/images/group-9.png
new file mode 100644
index 00000000..6ab70e3b
Binary files /dev/null and b/images/group-9.png differ
diff --git a/images/group-event-settings.png b/images/group-event-settings.png
new file mode 100644
index 00000000..5651e117
Binary files /dev/null and b/images/group-event-settings.png differ
diff --git a/images/group-settings-icon.png b/images/group-settings-icon.png
new file mode 100644
index 00000000..5651e117
Binary files /dev/null and b/images/group-settings-icon.png differ
diff --git a/images/group.png b/images/group.png
new file mode 100644
index 00000000..6259cce4
Binary files /dev/null and b/images/group.png differ
diff --git a/images/happening.png b/images/happening.png
new file mode 100644
index 00000000..b33189b2
Binary files /dev/null and b/images/happening.png differ
diff --git a/images/header-get-started.jpg b/images/header-get-started.jpg
new file mode 100644
index 00000000..5a302f35
Binary files /dev/null and b/images/header-get-started.jpg differ
diff --git a/images/icon-add-14x14.png b/images/icon-add-14x14.png
new file mode 100644
index 00000000..aaa7c897
Binary files /dev/null and b/images/icon-add-14x14.png differ
diff --git a/images/icon-add-20x18.png b/images/icon-add-20x18.png
new file mode 100644
index 00000000..f4796171
Binary files /dev/null and b/images/icon-add-20x18.png differ
diff --git a/images/icon-add-more.png b/images/icon-add-more.png
new file mode 100644
index 00000000..4a0e6008
Binary files /dev/null and b/images/icon-add-more.png differ
diff --git a/images/icon-all-set-header.png b/images/icon-all-set-header.png
new file mode 100644
index 00000000..b5d7940f
Binary files /dev/null and b/images/icon-all-set-header.png differ
diff --git a/images/icon-apple-pay.png b/images/icon-apple-pay.png
new file mode 100644
index 00000000..f5272f50
Binary files /dev/null and b/images/icon-apple-pay.png differ
diff --git a/images/icon-athletics-blue.png b/images/icon-athletics-blue.png
new file mode 100644
index 00000000..b514426e
Binary files /dev/null and b/images/icon-athletics-blue.png differ
diff --git a/images/icon-athletics-orange.png b/images/icon-athletics-orange.png
new file mode 100644
index 00000000..dbf04069
Binary files /dev/null and b/images/icon-athletics-orange.png differ
diff --git a/images/icon-athletics.png b/images/icon-athletics.png
new file mode 100644
index 00000000..ebc43ddb
Binary files /dev/null and b/images/icon-athletics.png differ
diff --git a/images/icon-avatar-placeholder.png b/images/icon-avatar-placeholder.png
new file mode 100644
index 00000000..d066b17e
Binary files /dev/null and b/images/icon-avatar-placeholder.png differ
diff --git a/images/icon-badge.png b/images/icon-badge.png
new file mode 100644
index 00000000..d1d7c797
Binary files /dev/null and b/images/icon-badge.png differ
diff --git a/images/icon-big-onboarding-health.png b/images/icon-big-onboarding-health.png
new file mode 100644
index 00000000..9a86bf91
Binary files /dev/null and b/images/icon-big-onboarding-health.png differ
diff --git a/images/icon-big-onboarding-privacy.png b/images/icon-big-onboarding-privacy.png
new file mode 100644
index 00000000..abe5e6dc
Binary files /dev/null and b/images/icon-big-onboarding-privacy.png differ
diff --git a/images/icon-bluetooth.png b/images/icon-bluetooth.png
new file mode 100644
index 00000000..8dbf13c7
Binary files /dev/null and b/images/icon-bluetooth.png differ
diff --git a/images/icon-browse-athletics.png b/images/icon-browse-athletics.png
new file mode 100644
index 00000000..e42758bc
Binary files /dev/null and b/images/icon-browse-athletics.png differ
diff --git a/images/icon-browse-covid19.png b/images/icon-browse-covid19.png
new file mode 100644
index 00000000..ec3daccb
Binary files /dev/null and b/images/icon-browse-covid19.png differ
diff --git a/images/icon-browse-dinings.png b/images/icon-browse-dinings.png
new file mode 100644
index 00000000..91a7c9ec
Binary files /dev/null and b/images/icon-browse-dinings.png differ
diff --git a/images/icon-browse-events.png b/images/icon-browse-events.png
new file mode 100644
index 00000000..28a2fd50
Binary files /dev/null and b/images/icon-browse-events.png differ
diff --git a/images/icon-browse-quick-polls.png b/images/icon-browse-quick-polls.png
new file mode 100644
index 00000000..ba1e89dc
Binary files /dev/null and b/images/icon-browse-quick-polls.png differ
diff --git a/images/icon-browse-saved.png b/images/icon-browse-saved.png
new file mode 100644
index 00000000..f4e2b93d
Binary files /dev/null and b/images/icon-browse-saved.png differ
diff --git a/images/icon-browse-wellness.png b/images/icon-browse-wellness.png
new file mode 100644
index 00000000..63bb28ec
Binary files /dev/null and b/images/icon-browse-wellness.png differ
diff --git a/images/icon-browse.png b/images/icon-browse.png
new file mode 100755
index 00000000..470f648c
Binary files /dev/null and b/images/icon-browse.png differ
diff --git a/images/icon-calendar.png b/images/icon-calendar.png
new file mode 100755
index 00000000..106ca18a
Binary files /dev/null and b/images/icon-calendar.png differ
diff --git a/images/icon-campus-tools-athletics.png b/images/icon-campus-tools-athletics.png
new file mode 100755
index 00000000..787e5f0b
Binary files /dev/null and b/images/icon-campus-tools-athletics.png differ
diff --git a/images/icon-campus-tools-dining.png b/images/icon-campus-tools-dining.png
new file mode 100755
index 00000000..d82ae232
Binary files /dev/null and b/images/icon-campus-tools-dining.png differ
diff --git a/images/icon-campus-tools-events.png b/images/icon-campus-tools-events.png
new file mode 100755
index 00000000..fc598d75
Binary files /dev/null and b/images/icon-campus-tools-events.png differ
diff --git a/images/icon-campus-tools-illini-cash.png b/images/icon-campus-tools-illini-cash.png
new file mode 100755
index 00000000..f43a964d
Binary files /dev/null and b/images/icon-campus-tools-illini-cash.png differ
diff --git a/images/icon-campus-tools-laundry.png b/images/icon-campus-tools-laundry.png
new file mode 100755
index 00000000..876f0a8f
Binary files /dev/null and b/images/icon-campus-tools-laundry.png differ
diff --git a/images/icon-campus-tools.png b/images/icon-campus-tools.png
new file mode 100755
index 00000000..c2837a49
Binary files /dev/null and b/images/icon-campus-tools.png differ
diff --git a/images/icon-campus-updates.png b/images/icon-campus-updates.png
new file mode 100644
index 00000000..267cdafa
Binary files /dev/null and b/images/icon-campus-updates.png differ
diff --git a/images/icon-cellphone.png b/images/icon-cellphone.png
new file mode 100644
index 00000000..c7ae728e
Binary files /dev/null and b/images/icon-cellphone.png differ
diff --git a/images/icon-certified.png b/images/icon-certified.png
new file mode 100755
index 00000000..2991d5cd
Binary files /dev/null and b/images/icon-certified.png differ
diff --git a/images/icon-check-example.png b/images/icon-check-example.png
new file mode 100755
index 00000000..156fe9a3
Binary files /dev/null and b/images/icon-check-example.png differ
diff --git a/images/icon-check-simple.png b/images/icon-check-simple.png
new file mode 100755
index 00000000..e013aeff
Binary files /dev/null and b/images/icon-check-simple.png differ
diff --git a/images/icon-check.png b/images/icon-check.png
new file mode 100644
index 00000000..44e38441
Binary files /dev/null and b/images/icon-check.png differ
diff --git a/images/icon-circle-close.png b/images/icon-circle-close.png
new file mode 100755
index 00000000..771316e4
Binary files /dev/null and b/images/icon-circle-close.png differ
diff --git a/images/icon-close-big.png b/images/icon-close-big.png
new file mode 100755
index 00000000..942e118f
Binary files /dev/null and b/images/icon-close-big.png differ
diff --git a/images/icon-comment-dots.png b/images/icon-comment-dots.png
new file mode 100644
index 00000000..1327789e
Binary files /dev/null and b/images/icon-comment-dots.png differ
diff --git a/images/icon-copy.png b/images/icon-copy.png
new file mode 100644
index 00000000..16956475
Binary files /dev/null and b/images/icon-copy.png differ
diff --git a/images/icon-cost.png b/images/icon-cost.png
new file mode 100644
index 00000000..885e72b7
Binary files /dev/null and b/images/icon-cost.png differ
diff --git a/images/icon-country-guidelines.png b/images/icon-country-guidelines.png
new file mode 100644
index 00000000..7b930970
Binary files /dev/null and b/images/icon-country-guidelines.png differ
diff --git a/images/icon-create-event.png b/images/icon-create-event.png
new file mode 100755
index 00000000..4a0e6008
Binary files /dev/null and b/images/icon-create-event.png differ
diff --git a/images/icon-credit.png b/images/icon-credit.png
new file mode 100755
index 00000000..f4e8f08a
Binary files /dev/null and b/images/icon-credit.png differ
diff --git a/images/icon-deselected-checkbox.png b/images/icon-deselected-checkbox.png
new file mode 100644
index 00000000..f33b2a2c
Binary files /dev/null and b/images/icon-deselected-checkbox.png differ
diff --git a/images/icon-dining-orange.png b/images/icon-dining-orange.png
new file mode 100644
index 00000000..e2b2236d
Binary files /dev/null and b/images/icon-dining-orange.png differ
diff --git a/images/icon-dining-yellow.png b/images/icon-dining-yellow.png
new file mode 100755
index 00000000..6cbed5e9
Binary files /dev/null and b/images/icon-dining-yellow.png differ
diff --git a/images/icon-dining.png b/images/icon-dining.png
new file mode 100644
index 00000000..34dc50bc
Binary files /dev/null and b/images/icon-dining.png differ
diff --git a/images/icon-down-orange.png b/images/icon-down-orange.png
new file mode 100755
index 00000000..ccbdb6a6
Binary files /dev/null and b/images/icon-down-orange.png differ
diff --git a/images/icon-down.png b/images/icon-down.png
new file mode 100755
index 00000000..2182d1ee
Binary files /dev/null and b/images/icon-down.png differ
diff --git a/images/icon-dryer-big.png b/images/icon-dryer-big.png
new file mode 100755
index 00000000..65a8159a
Binary files /dev/null and b/images/icon-dryer-big.png differ
diff --git a/images/icon-dryer-small.png b/images/icon-dryer-small.png
new file mode 100755
index 00000000..0402daff
Binary files /dev/null and b/images/icon-dryer-small.png differ
diff --git a/images/icon-edit.png b/images/icon-edit.png
new file mode 100644
index 00000000..65574c8e
Binary files /dev/null and b/images/icon-edit.png differ
diff --git a/images/icon-event.png b/images/icon-event.png
new file mode 100644
index 00000000..699af337
Binary files /dev/null and b/images/icon-event.png differ
diff --git a/images/icon-explore-campus-athletics.png b/images/icon-explore-campus-athletics.png
new file mode 100644
index 00000000..787e5f0b
Binary files /dev/null and b/images/icon-explore-campus-athletics.png differ
diff --git a/images/icon-explore-campus-dining.png b/images/icon-explore-campus-dining.png
new file mode 100644
index 00000000..d82ae232
Binary files /dev/null and b/images/icon-explore-campus-dining.png differ
diff --git a/images/icon-explore-campus-events.png b/images/icon-explore-campus-events.png
new file mode 100644
index 00000000..fc598d75
Binary files /dev/null and b/images/icon-explore-campus-events.png differ
diff --git a/images/icon-explore.png b/images/icon-explore.png
new file mode 100644
index 00000000..29b16874
Binary files /dev/null and b/images/icon-explore.png differ
diff --git a/images/icon-face-mask.png b/images/icon-face-mask.png
new file mode 100644
index 00000000..21d37afa
Binary files /dev/null and b/images/icon-face-mask.png differ
diff --git a/images/icon-feedback.png b/images/icon-feedback.png
new file mode 100755
index 00000000..12f6b47d
Binary files /dev/null and b/images/icon-feedback.png differ
diff --git a/images/icon-gear.png b/images/icon-gear.png
new file mode 100755
index 00000000..2aa41ffb
Binary files /dev/null and b/images/icon-gear.png differ
diff --git a/images/icon-health.png b/images/icon-health.png
new file mode 100644
index 00000000..f5a70de7
Binary files /dev/null and b/images/icon-health.png differ
diff --git a/images/icon-hospital.png b/images/icon-hospital.png
new file mode 100644
index 00000000..0849cba1
Binary files /dev/null and b/images/icon-hospital.png differ
diff --git a/images/icon-identity.png b/images/icon-identity.png
new file mode 100644
index 00000000..f412b8de
Binary files /dev/null and b/images/icon-identity.png differ
diff --git a/images/icon-illini-cash.png b/images/icon-illini-cash.png
new file mode 100755
index 00000000..4681e316
Binary files /dev/null and b/images/icon-illini-cash.png differ
diff --git a/images/icon-info-orange.png b/images/icon-info-orange.png
new file mode 100644
index 00000000..664af228
Binary files /dev/null and b/images/icon-info-orange.png differ
diff --git a/images/icon-key.png b/images/icon-key.png
new file mode 100644
index 00000000..775852e0
Binary files /dev/null and b/images/icon-key.png differ
diff --git a/images/icon-list-view.png b/images/icon-list-view.png
new file mode 100644
index 00000000..d735b487
Binary files /dev/null and b/images/icon-list-view.png differ
diff --git a/images/icon-listen.png b/images/icon-listen.png
new file mode 100755
index 00000000..ee485a87
Binary files /dev/null and b/images/icon-listen.png differ
diff --git a/images/icon-live-stats.png b/images/icon-live-stats.png
new file mode 100755
index 00000000..12af9e40
Binary files /dev/null and b/images/icon-live-stats.png differ
diff --git a/images/icon-location-1.png b/images/icon-location-1.png
new file mode 100644
index 00000000..38493c0e
Binary files /dev/null and b/images/icon-location-1.png differ
diff --git a/images/icon-location.png b/images/icon-location.png
new file mode 100644
index 00000000..6458b6ac
Binary files /dev/null and b/images/icon-location.png differ
diff --git a/images/icon-map-view.png b/images/icon-map-view.png
new file mode 100644
index 00000000..478e3544
Binary files /dev/null and b/images/icon-map-view.png differ
diff --git a/images/icon-member.png b/images/icon-member.png
new file mode 100644
index 00000000..3d6cbe14
Binary files /dev/null and b/images/icon-member.png differ
diff --git a/images/icon-more-info.png b/images/icon-more-info.png
new file mode 100755
index 00000000..415e89a4
Binary files /dev/null and b/images/icon-more-info.png differ
diff --git a/images/icon-my-illini.png b/images/icon-my-illini.png
new file mode 100755
index 00000000..1f0b7499
Binary files /dev/null and b/images/icon-my-illini.png differ
diff --git a/images/icon-near-you.png b/images/icon-near-you.png
new file mode 100755
index 00000000..77041d81
Binary files /dev/null and b/images/icon-near-you.png differ
diff --git a/images/icon-news.png b/images/icon-news.png
new file mode 100755
index 00000000..222cdd80
Binary files /dev/null and b/images/icon-news.png differ
diff --git a/images/icon-notifications-blue.png b/images/icon-notifications-blue.png
new file mode 100644
index 00000000..65a1b8a3
Binary files /dev/null and b/images/icon-notifications-blue.png differ
diff --git a/images/icon-orange-credit-16x13.png b/images/icon-orange-credit-16x13.png
new file mode 100644
index 00000000..65c17f52
Binary files /dev/null and b/images/icon-orange-credit-16x13.png differ
diff --git a/images/icon-orange-credit.png b/images/icon-orange-credit.png
new file mode 100644
index 00000000..d1926b66
Binary files /dev/null and b/images/icon-orange-credit.png differ
diff --git a/images/icon-orange-i.png b/images/icon-orange-i.png
new file mode 100644
index 00000000..10b69fae
Binary files /dev/null and b/images/icon-orange-i.png differ
diff --git a/images/icon-parking.png b/images/icon-parking.png
new file mode 100755
index 00000000..d19331f5
Binary files /dev/null and b/images/icon-parking.png differ
diff --git a/images/icon-passport.png b/images/icon-passport.png
new file mode 100644
index 00000000..b3de1113
Binary files /dev/null and b/images/icon-passport.png differ
diff --git a/images/icon-payment-type-apple-pay.png b/images/icon-payment-type-apple-pay.png
new file mode 100644
index 00000000..5fdf6d91
Binary files /dev/null and b/images/icon-payment-type-apple-pay.png differ
diff --git a/images/icon-payment-type-cache.png b/images/icon-payment-type-cache.png
new file mode 100644
index 00000000..993fcbe0
Binary files /dev/null and b/images/icon-payment-type-cache.png differ
diff --git a/images/icon-payment-type-cafe-credits.png b/images/icon-payment-type-cafe-credits.png
new file mode 100644
index 00000000..ab316cbe
Binary files /dev/null and b/images/icon-payment-type-cafe-credits.png differ
diff --git a/images/icon-payment-type-classic-meal.png b/images/icon-payment-type-classic-meal.png
new file mode 100644
index 00000000..111aa54a
Binary files /dev/null and b/images/icon-payment-type-classic-meal.png differ
diff --git a/images/icon-payment-type-credit-card.png b/images/icon-payment-type-credit-card.png
new file mode 100644
index 00000000..cb6c25f9
Binary files /dev/null and b/images/icon-payment-type-credit-card.png differ
diff --git a/images/icon-payment-type-google-pay.png b/images/icon-payment-type-google-pay.png
new file mode 100644
index 00000000..6f83940c
Binary files /dev/null and b/images/icon-payment-type-google-pay.png differ
diff --git a/images/icon-payment-type-ilini-cash.png b/images/icon-payment-type-ilini-cash.png
new file mode 100644
index 00000000..cc68cabb
Binary files /dev/null and b/images/icon-payment-type-ilini-cash.png differ
diff --git a/images/icon-persona-alumni-normal.png b/images/icon-persona-alumni-normal.png
new file mode 100644
index 00000000..2e8d011e
Binary files /dev/null and b/images/icon-persona-alumni-normal.png differ
diff --git a/images/icon-persona-alumni-selected.png b/images/icon-persona-alumni-selected.png
new file mode 100644
index 00000000..16549582
Binary files /dev/null and b/images/icon-persona-alumni-selected.png differ
diff --git a/images/icon-persona-athletics-normal.png b/images/icon-persona-athletics-normal.png
new file mode 100644
index 00000000..66586b7a
Binary files /dev/null and b/images/icon-persona-athletics-normal.png differ
diff --git a/images/icon-persona-athletics-selected.png b/images/icon-persona-athletics-selected.png
new file mode 100644
index 00000000..b1b2599f
Binary files /dev/null and b/images/icon-persona-athletics-selected.png differ
diff --git a/images/icon-persona-employee-normal.png b/images/icon-persona-employee-normal.png
new file mode 100644
index 00000000..10bbf312
Binary files /dev/null and b/images/icon-persona-employee-normal.png differ
diff --git a/images/icon-persona-employee-selected.png b/images/icon-persona-employee-selected.png
new file mode 100644
index 00000000..7bbaea92
Binary files /dev/null and b/images/icon-persona-employee-selected.png differ
diff --git a/images/icon-persona-parent-normal.png b/images/icon-persona-parent-normal.png
new file mode 100644
index 00000000..1f99658f
Binary files /dev/null and b/images/icon-persona-parent-normal.png differ
diff --git a/images/icon-persona-parent-selected.png b/images/icon-persona-parent-selected.png
new file mode 100644
index 00000000..b994fcb6
Binary files /dev/null and b/images/icon-persona-parent-selected.png differ
diff --git a/images/icon-persona-resident-normal.png b/images/icon-persona-resident-normal.png
new file mode 100644
index 00000000..26f66524
Binary files /dev/null and b/images/icon-persona-resident-normal.png differ
diff --git a/images/icon-persona-resident-selected.png b/images/icon-persona-resident-selected.png
new file mode 100644
index 00000000..e7985ad3
Binary files /dev/null and b/images/icon-persona-resident-selected.png differ
diff --git a/images/icon-persona-student-normal.png b/images/icon-persona-student-normal.png
new file mode 100644
index 00000000..7d79d22e
Binary files /dev/null and b/images/icon-persona-student-normal.png differ
diff --git a/images/icon-persona-student-selected.png b/images/icon-persona-student-selected.png
new file mode 100644
index 00000000..a612d5d4
Binary files /dev/null and b/images/icon-persona-student-selected.png differ
diff --git a/images/icon-persona-visitor-normal.png b/images/icon-persona-visitor-normal.png
new file mode 100644
index 00000000..e66aacc5
Binary files /dev/null and b/images/icon-persona-visitor-normal.png differ
diff --git a/images/icon-persona-visitor-selected.png b/images/icon-persona-visitor-selected.png
new file mode 100644
index 00000000..d292ae82
Binary files /dev/null and b/images/icon-persona-visitor-selected.png differ
diff --git a/images/icon-phone.png b/images/icon-phone.png
new file mode 100644
index 00000000..3c52b4aa
Binary files /dev/null and b/images/icon-phone.png differ
diff --git a/images/icon-placeholder-blue.png b/images/icon-placeholder-blue.png
new file mode 100755
index 00000000..3df87e54
Binary files /dev/null and b/images/icon-placeholder-blue.png differ
diff --git a/images/icon-placeholder-empty.png b/images/icon-placeholder-empty.png
new file mode 100755
index 00000000..16312000
Binary files /dev/null and b/images/icon-placeholder-empty.png differ
diff --git a/images/icon-placeholder-navy.png b/images/icon-placeholder-navy.png
new file mode 100755
index 00000000..8223140a
Binary files /dev/null and b/images/icon-placeholder-navy.png differ
diff --git a/images/icon-placeholder-orange.png b/images/icon-placeholder-orange.png
new file mode 100755
index 00000000..6f8d4245
Binary files /dev/null and b/images/icon-placeholder-orange.png differ
diff --git a/images/icon-placeholder-teal.png b/images/icon-placeholder-teal.png
new file mode 100755
index 00000000..b600287f
Binary files /dev/null and b/images/icon-placeholder-teal.png differ
diff --git a/images/icon-placeholder-yellow.png b/images/icon-placeholder-yellow.png
new file mode 100755
index 00000000..5321aa95
Binary files /dev/null and b/images/icon-placeholder-yellow.png differ
diff --git a/images/icon-plus.png b/images/icon-plus.png
new file mode 100755
index 00000000..76d6604a
Binary files /dev/null and b/images/icon-plus.png differ
diff --git a/images/icon-poi.png b/images/icon-poi.png
new file mode 100644
index 00000000..4b75bbaa
Binary files /dev/null and b/images/icon-poi.png differ
diff --git a/images/icon-privacy.png b/images/icon-privacy.png
new file mode 100755
index 00000000..0c59212a
Binary files /dev/null and b/images/icon-privacy.png differ
diff --git a/images/icon-quickpoll.png b/images/icon-quickpoll.png
new file mode 100755
index 00000000..12af9e40
Binary files /dev/null and b/images/icon-quickpoll.png differ
diff --git a/images/icon-recurring-event.png b/images/icon-recurring-event.png
new file mode 100755
index 00000000..598d50d5
Binary files /dev/null and b/images/icon-recurring-event.png differ
diff --git a/images/icon-reminder.png b/images/icon-reminder.png
new file mode 100755
index 00000000..b2ff259c
Binary files /dev/null and b/images/icon-reminder.png differ
diff --git a/images/icon-report-test.png b/images/icon-report-test.png
new file mode 100644
index 00000000..3b13beef
Binary files /dev/null and b/images/icon-report-test.png differ
diff --git a/images/icon-saved-white.png b/images/icon-saved-white.png
new file mode 100755
index 00000000..10ac8525
Binary files /dev/null and b/images/icon-saved-white.png differ
diff --git a/images/icon-saved.png b/images/icon-saved.png
new file mode 100755
index 00000000..547a411c
Binary files /dev/null and b/images/icon-saved.png differ
diff --git a/images/icon-schedule.png b/images/icon-schedule.png
new file mode 100755
index 00000000..8872eff0
Binary files /dev/null and b/images/icon-schedule.png differ
diff --git a/images/icon-search.png b/images/icon-search.png
new file mode 100755
index 00000000..e2fa90a1
Binary files /dev/null and b/images/icon-search.png differ
diff --git a/images/icon-selected-checkbox.png b/images/icon-selected-checkbox.png
new file mode 100644
index 00000000..6c6a2bf8
Binary files /dev/null and b/images/icon-selected-checkbox.png differ
diff --git a/images/icon-selected.png b/images/icon-selected.png
new file mode 100755
index 00000000..8be14cdc
Binary files /dev/null and b/images/icon-selected.png differ
diff --git a/images/icon-separate-people.png b/images/icon-separate-people.png
new file mode 100644
index 00000000..04e31f17
Binary files /dev/null and b/images/icon-separate-people.png differ
diff --git a/images/icon-settings.png b/images/icon-settings.png
new file mode 100755
index 00000000..e63caa5a
Binary files /dev/null and b/images/icon-settings.png differ
diff --git a/images/icon-social-distance.png b/images/icon-social-distance.png
new file mode 100644
index 00000000..49b452e5
Binary files /dev/null and b/images/icon-social-distance.png differ
diff --git a/images/icon-star-selected.png b/images/icon-star-selected.png
new file mode 100755
index 00000000..f0b83b4f
Binary files /dev/null and b/images/icon-star-selected.png differ
diff --git a/images/icon-star-solid.png b/images/icon-star-solid.png
new file mode 100755
index 00000000..6bc61cba
Binary files /dev/null and b/images/icon-star-solid.png differ
diff --git a/images/icon-star-white.png b/images/icon-star-white.png
new file mode 100644
index 00000000..03f90d76
Binary files /dev/null and b/images/icon-star-white.png differ
diff --git a/images/icon-star.png b/images/icon-star.png
new file mode 100755
index 00000000..e30202db
Binary files /dev/null and b/images/icon-star.png differ
diff --git a/images/icon-stay-at-home.png b/images/icon-stay-at-home.png
new file mode 100644
index 00000000..930f9a25
Binary files /dev/null and b/images/icon-stay-at-home.png differ
diff --git a/images/icon-stehoscope.png b/images/icon-stehoscope.png
new file mode 100644
index 00000000..2a53a8fd
Binary files /dev/null and b/images/icon-stehoscope.png differ
diff --git a/images/icon-team.png b/images/icon-team.png
new file mode 100755
index 00000000..248ab988
Binary files /dev/null and b/images/icon-team.png differ
diff --git a/images/icon-test-history.png b/images/icon-test-history.png
new file mode 100644
index 00000000..eab6d88b
Binary files /dev/null and b/images/icon-test-history.png differ
diff --git a/images/icon-time.png b/images/icon-time.png
new file mode 100644
index 00000000..2e5a77d7
Binary files /dev/null and b/images/icon-time.png differ
diff --git a/images/icon-unselected.png b/images/icon-unselected.png
new file mode 100755
index 00000000..bbdd90e6
Binary files /dev/null and b/images/icon-unselected.png differ
diff --git a/images/icon-up.png b/images/icon-up.png
new file mode 100755
index 00000000..c38e6607
Binary files /dev/null and b/images/icon-up.png differ
diff --git a/images/icon-washer-big.png b/images/icon-washer-big.png
new file mode 100755
index 00000000..3e84ec28
Binary files /dev/null and b/images/icon-washer-big.png differ
diff --git a/images/icon-washer-small.png b/images/icon-washer-small.png
new file mode 100755
index 00000000..42aef68f
Binary files /dev/null and b/images/icon-washer-small.png differ
diff --git a/images/icon-washer.png b/images/icon-washer.png
new file mode 100755
index 00000000..2c548435
Binary files /dev/null and b/images/icon-washer.png differ
diff --git a/images/icon-watch.png b/images/icon-watch.png
new file mode 100755
index 00000000..edb88be9
Binary files /dev/null and b/images/icon-watch.png differ
diff --git a/images/icon-white-arrow-right.png b/images/icon-white-arrow-right.png
new file mode 100644
index 00000000..9ff9d78e
Binary files /dev/null and b/images/icon-white-arrow-right.png differ
diff --git a/images/icon-x-orange-small.png b/images/icon-x-orange-small.png
new file mode 100755
index 00000000..7e08bd73
Binary files /dev/null and b/images/icon-x-orange-small.png differ
diff --git a/images/icon-x-orange.png b/images/icon-x-orange.png
new file mode 100755
index 00000000..599f4235
Binary files /dev/null and b/images/icon-x-orange.png differ
diff --git a/images/icon-your-care-team.png b/images/icon-your-care-team.png
new file mode 100644
index 00000000..187a710a
Binary files /dev/null and b/images/icon-your-care-team.png differ
diff --git a/images/ig-20x20.png b/images/ig-20x20.png
new file mode 100644
index 00000000..ced6c915
Binary files /dev/null and b/images/ig-20x20.png differ
diff --git a/images/ig-24x24.png b/images/ig-24x24.png
new file mode 100644
index 00000000..6a832485
Binary files /dev/null and b/images/ig-24x24.png differ
diff --git a/images/ig-32x32.png b/images/ig-32x32.png
new file mode 100755
index 00000000..d4c2efb1
Binary files /dev/null and b/images/ig-32x32.png differ
diff --git a/images/ilini-cash.png b/images/ilini-cash.png
new file mode 100644
index 00000000..e4d35117
Binary files /dev/null and b/images/ilini-cash.png differ
diff --git a/images/illini-cash-logo.jpg b/images/illini-cash-logo.jpg
new file mode 100644
index 00000000..e04a583f
Binary files /dev/null and b/images/illini-cash-logo.jpg differ
diff --git a/images/kognito.png b/images/kognito.png
new file mode 100644
index 00000000..620ed214
Binary files /dev/null and b/images/kognito.png differ
diff --git a/images/link-out.png b/images/link-out.png
new file mode 100644
index 00000000..e887c513
Binary files /dev/null and b/images/link-out.png differ
diff --git a/images/login-header.png b/images/login-header.png
new file mode 100644
index 00000000..a695212b
Binary files /dev/null and b/images/login-header.png differ
diff --git a/images/mc-kinley-gray.png b/images/mc-kinley-gray.png
new file mode 100644
index 00000000..8f776ae7
Binary files /dev/null and b/images/mc-kinley-gray.png differ
diff --git a/images/member.png b/images/member.png
new file mode 100644
index 00000000..c666ebd2
Binary files /dev/null and b/images/member.png differ
diff --git a/images/mental.png b/images/mental.png
new file mode 100644
index 00000000..f0cbbd9f
Binary files /dev/null and b/images/mental.png differ
diff --git a/images/mtd-logo.png b/images/mtd-logo.png
new file mode 100644
index 00000000..7fc29a8f
Binary files /dev/null and b/images/mtd-logo.png differ
diff --git a/images/my-illini-orange.png b/images/my-illini-orange.png
new file mode 100644
index 00000000..700ab7f7
Binary files /dev/null and b/images/my-illini-orange.png differ
diff --git a/images/navy.png b/images/navy.png
new file mode 100755
index 00000000..1c3f787d
Binary files /dev/null and b/images/navy.png differ
diff --git a/images/near-you.png b/images/near-you.png
new file mode 100644
index 00000000..96b92727
Binary files /dev/null and b/images/near-you.png differ
diff --git a/images/onboarding-back-btn.png b/images/onboarding-back-btn.png
new file mode 100644
index 00000000..da2d19bb
Binary files /dev/null and b/images/onboarding-back-btn.png differ
diff --git a/images/osf-logo-gray.png b/images/osf-logo-gray.png
new file mode 100644
index 00000000..4fc42c07
Binary files /dev/null and b/images/osf-logo-gray.png differ
diff --git a/images/path.png b/images/path.png
new file mode 100644
index 00000000..cb8fe5fc
Binary files /dev/null and b/images/path.png differ
diff --git a/images/pending.png b/images/pending.png
new file mode 100644
index 00000000..481c1d76
Binary files /dev/null and b/images/pending.png differ
diff --git a/images/physical.png b/images/physical.png
new file mode 100644
index 00000000..9c0c7d98
Binary files /dev/null and b/images/physical.png differ
diff --git a/images/pledge.png b/images/pledge.png
new file mode 100644
index 00000000..a6f60d57
Binary files /dev/null and b/images/pledge.png differ
diff --git a/images/posession.png b/images/posession.png
new file mode 100644
index 00000000..78a1cc41
Binary files /dev/null and b/images/posession.png differ
diff --git a/images/powered-by.png b/images/powered-by.png
new file mode 100644
index 00000000..0ad4d2f5
Binary files /dev/null and b/images/powered-by.png differ
diff --git a/images/privacy-header.png b/images/privacy-header.png
new file mode 100644
index 00000000..5be65a28
Binary files /dev/null and b/images/privacy-header.png differ
diff --git a/images/privacy.png b/images/privacy.png
new file mode 100644
index 00000000..3689f413
Binary files /dev/null and b/images/privacy.png differ
diff --git a/images/provider.png b/images/provider.png
new file mode 100644
index 00000000..2a53a8fd
Binary files /dev/null and b/images/provider.png differ
diff --git a/images/reflection.png b/images/reflection.png
new file mode 100644
index 00000000..aa9ed5ab
Binary files /dev/null and b/images/reflection.png differ
diff --git a/images/reminder.png b/images/reminder.png
new file mode 100755
index 00000000..b2ff259c
Binary files /dev/null and b/images/reminder.png differ
diff --git a/images/roster_photo.png b/images/roster_photo.png
new file mode 100644
index 00000000..203652c7
Binary files /dev/null and b/images/roster_photo.png differ
diff --git a/images/safer-illinois.png b/images/safer-illinois.png
new file mode 100644
index 00000000..614486c9
Binary files /dev/null and b/images/safer-illinois.png differ
diff --git a/images/sample-event-dining.png b/images/sample-event-dining.png
new file mode 100644
index 00000000..28675dc5
Binary files /dev/null and b/images/sample-event-dining.png differ
diff --git a/images/sample-event-event.png b/images/sample-event-event.png
new file mode 100644
index 00000000..9aaa457a
Binary files /dev/null and b/images/sample-event-event.png differ
diff --git a/images/schedule-orange.png b/images/schedule-orange.png
new file mode 100755
index 00000000..6a44030e
Binary files /dev/null and b/images/schedule-orange.png differ
diff --git a/images/selected-black.png b/images/selected-black.png
new file mode 100644
index 00000000..f89ee2df
Binary files /dev/null and b/images/selected-black.png differ
diff --git a/images/selected-gray.png b/images/selected-gray.png
new file mode 100644
index 00000000..3e7e7571
Binary files /dev/null and b/images/selected-gray.png differ
diff --git a/images/selected-orange.png b/images/selected-orange.png
new file mode 100644
index 00000000..8be14cdc
Binary files /dev/null and b/images/selected-orange.png differ
diff --git a/images/selected.png b/images/selected.png
new file mode 100755
index 00000000..64c67b17
Binary files /dev/null and b/images/selected.png differ
diff --git a/images/settings-white.png b/images/settings-white.png
new file mode 100644
index 00000000..ea4401af
Binary files /dev/null and b/images/settings-white.png differ
diff --git a/images/share-location-header.png b/images/share-location-header.png
new file mode 100644
index 00000000..c568b48b
Binary files /dev/null and b/images/share-location-header.png differ
diff --git a/images/slant-down-right-blue-rotated.png b/images/slant-down-right-blue-rotated.png
new file mode 100644
index 00000000..e58d22a0
Binary files /dev/null and b/images/slant-down-right-blue-rotated.png differ
diff --git a/images/slant-down-right-blue.png b/images/slant-down-right-blue.png
new file mode 100644
index 00000000..0a6c05e0
Binary files /dev/null and b/images/slant-down-right-blue.png differ
diff --git a/images/slant-down-right-grey.png b/images/slant-down-right-grey.png
new file mode 100644
index 00000000..e7fe48d2
Binary files /dev/null and b/images/slant-down-right-grey.png differ
diff --git a/images/slant-down-right-rotated.png b/images/slant-down-right-rotated.png
new file mode 100644
index 00000000..23ea0856
Binary files /dev/null and b/images/slant-down-right-rotated.png differ
diff --git a/images/slant-down-right.png b/images/slant-down-right.png
new file mode 100755
index 00000000..3ec1ac73
Binary files /dev/null and b/images/slant-down-right.png differ
diff --git a/images/small-add-orange.png b/images/small-add-orange.png
new file mode 100644
index 00000000..59a8f53c
Binary files /dev/null and b/images/small-add-orange.png differ
diff --git a/images/small-add.png b/images/small-add.png
new file mode 100644
index 00000000..9a970aa9
Binary files /dev/null and b/images/small-add.png differ
diff --git a/images/social.png b/images/social.png
new file mode 100644
index 00000000..61e5fd9a
Binary files /dev/null and b/images/social.png differ
diff --git a/images/spiritual.png b/images/spiritual.png
new file mode 100644
index 00000000..816ddfa2
Binary files /dev/null and b/images/spiritual.png differ
diff --git a/images/switch-off.png b/images/switch-off.png
new file mode 100644
index 00000000..cc802107
Binary files /dev/null and b/images/switch-off.png differ
diff --git a/images/switch-on.png b/images/switch-on.png
new file mode 100644
index 00000000..506be70a
Binary files /dev/null and b/images/switch-on.png differ
diff --git a/images/tab-browse-selected.png b/images/tab-browse-selected.png
new file mode 100644
index 00000000..c4490262
Binary files /dev/null and b/images/tab-browse-selected.png differ
diff --git a/images/tab-browse.png b/images/tab-browse.png
new file mode 100644
index 00000000..d443ade6
Binary files /dev/null and b/images/tab-browse.png differ
diff --git a/images/tab-explore-selected.png b/images/tab-explore-selected.png
new file mode 100644
index 00000000..549bf362
Binary files /dev/null and b/images/tab-explore-selected.png differ
diff --git a/images/tab-explore.png b/images/tab-explore.png
new file mode 100755
index 00000000..433fc078
Binary files /dev/null and b/images/tab-explore.png differ
diff --git a/images/tab-home-selected.png b/images/tab-home-selected.png
new file mode 100755
index 00000000..d137dc11
Binary files /dev/null and b/images/tab-home-selected.png differ
diff --git a/images/tab-home.png b/images/tab-home.png
new file mode 100755
index 00000000..2655cb81
Binary files /dev/null and b/images/tab-home.png differ
diff --git a/images/tab-more-selected.png b/images/tab-more-selected.png
new file mode 100755
index 00000000..0cacb796
Binary files /dev/null and b/images/tab-more-selected.png differ
diff --git a/images/tab-more.png b/images/tab-more.png
new file mode 100755
index 00000000..28c2ae5b
Binary files /dev/null and b/images/tab-more.png differ
diff --git a/images/tab-saved-selected.png b/images/tab-saved-selected.png
new file mode 100755
index 00000000..9c86c9e6
Binary files /dev/null and b/images/tab-saved-selected.png differ
diff --git a/images/tab-saved.png b/images/tab-saved.png
new file mode 100755
index 00000000..547a411c
Binary files /dev/null and b/images/tab-saved.png differ
diff --git a/images/tab-wallet.png b/images/tab-wallet.png
new file mode 100644
index 00000000..dbe24847
Binary files /dev/null and b/images/tab-wallet.png differ
diff --git a/images/teal.png b/images/teal.png
new file mode 100644
index 00000000..160ed90f
Binary files /dev/null and b/images/teal.png differ
diff --git a/images/tickets_yellow.png b/images/tickets_yellow.png
new file mode 100644
index 00000000..1006c96e
Binary files /dev/null and b/images/tickets_yellow.png differ
diff --git a/images/transparent-buss-icon.png b/images/transparent-buss-icon.png
new file mode 100644
index 00000000..08cfe896
Binary files /dev/null and b/images/transparent-buss-icon.png differ
diff --git a/images/twitter-20x18.png b/images/twitter-20x18.png
new file mode 100644
index 00000000..81671424
Binary files /dev/null and b/images/twitter-20x18.png differ
diff --git a/images/twitter-24x22.png b/images/twitter-24x22.png
new file mode 100644
index 00000000..6b9a1e30
Binary files /dev/null and b/images/twitter-24x22.png differ
diff --git a/images/twitter-32x28.png b/images/twitter-32x28.png
new file mode 100755
index 00000000..d740984f
Binary files /dev/null and b/images/twitter-32x28.png differ
diff --git a/images/u.png b/images/u.png
new file mode 100644
index 00000000..6ec9b803
Binary files /dev/null and b/images/u.png differ
diff --git a/images/upcoming_events_orange.png b/images/upcoming_events_orange.png
new file mode 100644
index 00000000..fc598d75
Binary files /dev/null and b/images/upcoming_events_orange.png differ
diff --git a/images/upcoming_events_orange.textClipping b/images/upcoming_events_orange.textClipping
new file mode 100644
index 00000000..0b181a1d
Binary files /dev/null and b/images/upcoming_events_orange.textClipping differ
diff --git a/images/user-check.png b/images/user-check.png
new file mode 100644
index 00000000..0f2e6308
Binary files /dev/null and b/images/user-check.png differ
diff --git a/images/vocational.png b/images/vocational.png
new file mode 100644
index 00000000..9d384ac1
Binary files /dev/null and b/images/vocational.png differ
diff --git a/images/warning-orange.png b/images/warning-orange.png
new file mode 100644
index 00000000..5204b1ee
Binary files /dev/null and b/images/warning-orange.png differ
diff --git a/images/welcome-to-illinois.png b/images/welcome-to-illinois.png
new file mode 100755
index 00000000..995cd06d
Binary files /dev/null and b/images/welcome-to-illinois.png differ
diff --git a/images/you-tube-20x15.png b/images/you-tube-20x15.png
new file mode 100644
index 00000000..5459b259
Binary files /dev/null and b/images/you-tube-20x15.png differ
diff --git a/images/you-tube-32x24.png b/images/you-tube-32x24.png
new file mode 100755
index 00000000..f4ce7e91
Binary files /dev/null and b/images/you-tube-32x24.png differ
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 00000000..9367d483
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 8.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 00000000..e8efba11
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Flutter.podspec b/ios/Flutter/Flutter.podspec
new file mode 100644
index 00000000..5ca30416
--- /dev/null
+++ b/ios/Flutter/Flutter.podspec
@@ -0,0 +1,18 @@
+#
+# NOTE: This podspec is NOT to be published. It is only used as a local source!
+#
+
+Pod::Spec.new do |s|
+ s.name = 'Flutter'
+ s.version = '1.0.0'
+ s.summary = 'High-performance, high-fidelity mobile apps.'
+ s.description = <<-DESC
+Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS.
+ DESC
+ s.homepage = 'https://flutter.io'
+ s.license = { :type => 'MIT' }
+ s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
+ s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
+ s.ios.deployment_target = '8.0'
+ s.vendored_frameworks = 'Flutter.framework'
+end
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 00000000..399e9340
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Locations/Grainger Engineering Library.gpx b/ios/Locations/Grainger Engineering Library.gpx
new file mode 100644
index 00000000..73db27b6
--- /dev/null
+++ b/ios/Locations/Grainger Engineering Library.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
Novato
1301 W Springfield Ave
Urbana
IL
61801
United States
\ No newline at end of file
diff --git a/ios/Locations/MemorialStadium.gpx b/ios/Locations/MemorialStadium.gpx
new file mode 100644
index 00000000..199ed2e9
--- /dev/null
+++ b/ios/Locations/MemorialStadium.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
Memorial Stadium
Fourth and Kirby
Champaign
IL
61820
United States
\ No newline at end of file
diff --git a/ios/Locations/Novato.gpx b/ios/Locations/Novato.gpx
new file mode 100644
index 00000000..f9b79535
--- /dev/null
+++ b/ios/Locations/Novato.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
Novato
633 Trumbull Ave
Novato
CA
94947
United States
\ No newline at end of file
diff --git a/ios/Locations/PaloAlto.gpx b/ios/Locations/PaloAlto.gpx
new file mode 100644
index 00000000..4c12e739
--- /dev/null
+++ b/ios/Locations/PaloAlto.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
Palo Alto
780 Seale Ave
Palo Alto
CA
94303
United States
\ No newline at end of file
diff --git a/ios/Locations/Ruse.gpx b/ios/Locations/Ruse.gpx
new file mode 100644
index 00000000..67c69aa0
--- /dev/null
+++ b/ios/Locations/Ruse.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
Novato
ulitsa Borisova 94b
Ruse
7012
Bulgaria
\ No newline at end of file
diff --git a/ios/Locations/StateFarm-around.gpx b/ios/Locations/StateFarm-around.gpx
new file mode 100644
index 00000000..bfe45626
--- /dev/null
+++ b/ios/Locations/StateFarm-around.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
State Farm Center (around)
Fourth and Kirby
Champaign
IL
61820
United States
\ No newline at end of file
diff --git a/ios/Locations/StateFarm-around2.gpx b/ios/Locations/StateFarm-around2.gpx
new file mode 100644
index 00000000..ecf7977f
--- /dev/null
+++ b/ios/Locations/StateFarm-around2.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
State Farm Center (around 2)
Fourth and Kirby
Champaign
IL
61820
United States
\ No newline at end of file
diff --git a/ios/Locations/StateFarm-around3.gpx b/ios/Locations/StateFarm-around3.gpx
new file mode 100644
index 00000000..7bb2a0ec
--- /dev/null
+++ b/ios/Locations/StateFarm-around3.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
State Farm Center (around 3)
Fourth and Kirby
Champaign
IL
61820
United States
\ No newline at end of file
diff --git a/ios/Locations/StateFarm.gpx b/ios/Locations/StateFarm.gpx
new file mode 100644
index 00000000..3ec31731
--- /dev/null
+++ b/ios/Locations/StateFarm.gpx
@@ -0,0 +1 @@
+
2016-05-12T08:10:58Z
State Farm Center
Fourth and Kirby
Champaign
IL
61820
United States
\ No newline at end of file
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 00000000..1146ba13
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,98 @@
+# Uncomment this line to define a global platform for your project
+# Set platform version to 11.0 because of MeridianSDK 5.10.0
+platform :ios, '11.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def parse_KV_file(file, separator='=')
+ file_abs_path = File.expand_path(file)
+ if !File.exists? file_abs_path
+ return [];
+ end
+ generated_key_values = {}
+ skip_line_start_symbols = ["#", "/"]
+ File.foreach(file_abs_path) do |line|
+ next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
+ plugin = line.split(pattern=separator)
+ if plugin.length == 2
+ podname = plugin[0].strip()
+ path = plugin[1].strip()
+ podpath = File.expand_path("#{path}", file_abs_path)
+ generated_key_values[podname] = podpath
+ else
+ puts "Invalid plugin specification: #{line}"
+ end
+ end
+ generated_key_values
+end
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ pod 'GoogleMaps', '3.3.0'
+ pod 'MapsIndoors', '3.8.0'
+ pod 'ZXingObjC', '3.6.4'
+ pod 'PPBlinkID', '~> 5.3.0'
+ pod 'HKDFKit', '0.0.3'
+ pod 'Firebase/MLVisionBarcodeModel'
+
+ # Flutter Pod
+
+ copied_flutter_dir = File.join(__dir__, 'Flutter')
+ copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
+ copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
+ unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
+ # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
+ # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
+ # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
+
+ generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+ generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
+ cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
+
+ unless File.exist?(copied_framework_path)
+ FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
+ end
+ unless File.exist?(copied_podspec_path)
+ FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
+ end
+ end
+
+ # Keep pod path relative so it can be checked into Podfile.lock.
+ pod 'Flutter', :path => 'Flutter'
+
+ # Plugin Pods
+
+ # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
+ # referring to absolute paths on developers' machines.
+ system('rm -rf .symlinks')
+ system('mkdir -p .symlinks/plugins')
+ plugin_pods = parse_KV_file('../.flutter-plugins')
+ plugin_pods.each do |name, path|
+ symlink = File.join('.symlinks', 'plugins', name)
+ File.symlink(path, symlink)
+ pod name, :path => File.join(symlink, 'ios')
+ end
+end
+
+# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
+install! 'cocoapods', :disable_input_output_paths => true
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['ENABLE_BITCODE'] = 'NO'
+ end
+ end
+end
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..ab021839
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,926 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; settings = {COMPILER_FLAGS = "-w"; }; };
+ 2605FF54236C13A6002F71BE /* GoogleService-Info-Debug.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2605FF52236C13A6002F71BE /* GoogleService-Info-Debug.plist */; };
+ 2605FF55236C13A6002F71BE /* GoogleService-Info-Release.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2605FF53236C13A6002F71BE /* GoogleService-Info-Release.plist */; };
+ 2626D94B22DC99C800F6BC2F /* NSString+InaJson.m in Sources */ = {isa = PBXBuildFile; fileRef = 2626D94922DC99C700F6BC2F /* NSString+InaJson.m */; };
+ 2626D94E22DCB80000F6BC2F /* MapMarkerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2626D94D22DCB80000F6BC2F /* MapMarkerView.m */; };
+ 266FA50323E0388B00F800F5 /* MapController.m in Sources */ = {isa = PBXBuildFile; fileRef = 266FA50223E0388B00F800F5 /* MapController.m */; };
+ 268F30F122E5DD7900547FE1 /* travel-mode-walk@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F30ED22E5DD7900547FE1 /* travel-mode-walk@2x.png */; };
+ 268F30F222E5DD7900547FE1 /* travel-mode-transit@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F30EE22E5DD7900547FE1 /* travel-mode-transit@2x.png */; };
+ 268F30F322E5DD7900547FE1 /* travel-mode-bicycle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F30EF22E5DD7900547FE1 /* travel-mode-bicycle@2x.png */; };
+ 268F30F422E5DD7900547FE1 /* travel-mode-drive@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F30F022E5DD7900547FE1 /* travel-mode-drive@2x.png */; };
+ 268F30F622E5E47600547FE1 /* travel-mode-unknown@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F30F522E5E47600547FE1 /* travel-mode-unknown@2x.png */; };
+ 268F310022E5E7BF00547FE1 /* NSUserDefaults+InaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 268F30FF22E5E7BF00547FE1 /* NSUserDefaults+InaUtils.m */; };
+ 268F647A22DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 268F647922DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.m */; };
+ 268F647D22DF09D900A85AFD /* NSArray+InaTypedValue.m in Sources */ = {isa = PBXBuildFile; fileRef = 268F647B22DF09D900A85AFD /* NSArray+InaTypedValue.m */; };
+ 268F647F22DF15F700A85AFD /* button-icon-nav-refresh.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F647E22DF15F700A85AFD /* button-icon-nav-refresh.png */; };
+ 268F648122DF200300A85AFD /* button-icon-nav-location.png in Resources */ = {isa = PBXBuildFile; fileRef = 268F648022DF200300A85AFD /* button-icon-nav-location.png */; };
+ 26962B542306E8000026240A /* maps-icon-marker-pin-20@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 26962B502306E8000026240A /* maps-icon-marker-pin-20@2x.png */; };
+ 26962B552306E8000026240A /* maps-icon-marker-pin-30@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 26962B512306E8000026240A /* maps-icon-marker-pin-30@2x.png */; };
+ 26962B562306E8000026240A /* maps-icon-marker-pin-10@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 26962B522306E8000026240A /* maps-icon-marker-pin-10@2x.png */; };
+ 26962B572306E8000026240A /* maps-icon-marker-pin-40@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 26962B532306E8000026240A /* maps-icon-marker-pin-40@2x.png */; };
+ 26962B592306EB190026240A /* maps-icon-marker-pin-50@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 26962B582306EB190026240A /* maps-icon-marker-pin-50@2x.png */; };
+ 2696995D22C38B4000B3290E /* AppKeys.m in Sources */ = {isa = PBXBuildFile; fileRef = 2696995922C38B4000B3290E /* AppKeys.m */; };
+ 2696995E22C38B4000B3290E /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2696995A22C38B4000B3290E /* AppDelegate.m */; };
+ 2696996022C38C1D00B3290E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2696995F22C38C1D00B3290E /* main.m */; };
+ 2696998022C3A14A00B3290E /* MapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2696997F22C3A14A00B3290E /* MapView.m */; };
+ 269F83E622D73E7400CC11A4 /* maps-icon-marker-circle-10@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 269F83E222D73E7400CC11A4 /* maps-icon-marker-circle-10@2x.png */; };
+ 269F83E722D73E7400CC11A4 /* maps-icon-marker-circle-30@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 269F83E322D73E7400CC11A4 /* maps-icon-marker-circle-30@2x.png */; };
+ 269F83E822D73E7400CC11A4 /* maps-icon-marker-circle-20@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 269F83E422D73E7400CC11A4 /* maps-icon-marker-circle-20@2x.png */; };
+ 269F83E922D73E7400CC11A4 /* maps-icon-marker-circle-40@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 269F83E522D73E7400CC11A4 /* maps-icon-marker-circle-40@2x.png */; };
+ 269F83EE22D73EB400CC11A4 /* NSDate+UIUCUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 269F83EA22D73EB400CC11A4 /* NSDate+UIUCUtils.m */; };
+ 269F83EF22D73EB400CC11A4 /* UIColor+InaParse.m in Sources */ = {isa = PBXBuildFile; fileRef = 269F83EC22D73EB400CC11A4 /* UIColor+InaParse.m */; };
+ 269F83F222D744E200CC11A4 /* CGGeometry+InaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 269F83F122D744E200CC11A4 /* CGGeometry+InaUtils.m */; };
+ 269F83FC22D77E5500CC11A4 /* MapDirectionsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 269F83FB22D77E5500CC11A4 /* MapDirectionsController.m */; };
+ 26A170EB22DDFB6500299773 /* button-icon-nav-next.png in Resources */ = {isa = PBXBuildFile; fileRef = 26A170E822DDFB6500299773 /* button-icon-nav-next.png */; };
+ 26A170EC22DDFB6500299773 /* button-icon-nav-prev.png in Resources */ = {isa = PBXBuildFile; fileRef = 26A170E922DDFB6500299773 /* button-icon-nav-prev.png */; };
+ 26A170ED22DDFB6500299773 /* button-icon-nav-clear.png in Resources */ = {isa = PBXBuildFile; fileRef = 26A170EA22DDFB6500299773 /* button-icon-nav-clear.png */; };
+ 26B340372331163A0031CF70 /* MapLocationPickerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 26B340362331163A0031CF70 /* MapLocationPickerController.m */; };
+ 26B3403A2331195C0031CF70 /* UILabel+InaMeasure.m in Sources */ = {isa = PBXBuildFile; fileRef = 26B340392331195C0031CF70 /* UILabel+InaMeasure.m */; };
+ 26C07E2E247677A600E28D43 /* ExposurePlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C07E2D247677A600E28D43 /* ExposurePlugin.m */; };
+ 26C90CED2361CF480092E07F /* NSDictionary+InaPathKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C90CEC2361CF480092E07F /* NSDictionary+InaPathKey.m */; };
+ 26C90CF02361D13E0092E07F /* NSDictionary+UIUCConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C90CEE2361D13E0092E07F /* NSDictionary+UIUCConfig.m */; };
+ 26ECB3232487938C00479487 /* CommonCrypto+UIUCUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 26ECB3222487938C00479487 /* CommonCrypto+UIUCUtils.m */; };
+ 26ECB3262487AD0900479487 /* Security+UIUCUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 26ECB3252487AD0900479487 /* Security+UIUCUtils.m */; };
+ 26FA06BE22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m in Sources */ = {isa = PBXBuildFile; fileRef = 26FA06BC22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m */; };
+ 26FAB1C922EB3502008E987C /* NSDictionary+UIUCExplore.m in Sources */ = {isa = PBXBuildFile; fileRef = 26FAB1C822EB3502008E987C /* NSDictionary+UIUCExplore.m */; };
+ 26FAB1CC22EB35C7008E987C /* NSDate+InaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 26FAB1CA22EB35C7008E987C /* NSDate+InaUtils.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ B6201F5B24E2D8080050F7DC /* GalleryPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = B6201F5A24E2D8080050F7DC /* GalleryPlugin.m */; };
+ B681B2B923DEFD9E00093A67 /* NSData+InaHex.m in Sources */ = {isa = PBXBuildFile; fileRef = B681B2B823DEFD9E00093A67 /* NSData+InaHex.m */; };
+ B6BDCD37242CCF5F002F7364 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6BDCD39242CCF5F002F7364 /* Localizable.strings */; };
+ B6EA372423A7EF76001D78A5 /* Bluetooth+InaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = B6EA372323A7EF76001D78A5 /* Bluetooth+InaUtils.m */; };
+ BB7BF12F5D3356D25889D23A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7B5BCF89DF9B7AC1FD247B /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 2605FF52236C13A6002F71BE /* GoogleService-Info-Debug.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Debug.plist"; sourceTree = ""; };
+ 2605FF53236C13A6002F71BE /* GoogleService-Info-Release.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Release.plist"; sourceTree = ""; };
+ 2626D94922DC99C700F6BC2F /* NSString+InaJson.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+InaJson.m"; sourceTree = ""; };
+ 2626D94A22DC99C700F6BC2F /* NSString+InaJson.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+InaJson.h"; sourceTree = ""; };
+ 2626D94C22DCB80000F6BC2F /* MapMarkerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapMarkerView.h; sourceTree = ""; };
+ 2626D94D22DCB80000F6BC2F /* MapMarkerView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapMarkerView.m; sourceTree = ""; };
+ 2668953022E1E074003CAB94 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
+ 266FA50123E0388B00F800F5 /* MapController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapController.h; sourceTree = ""; };
+ 266FA50223E0388B00F800F5 /* MapController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapController.m; sourceTree = ""; };
+ 268F30ED22E5DD7900547FE1 /* travel-mode-walk@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "travel-mode-walk@2x.png"; sourceTree = ""; };
+ 268F30EE22E5DD7900547FE1 /* travel-mode-transit@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "travel-mode-transit@2x.png"; sourceTree = ""; };
+ 268F30EF22E5DD7900547FE1 /* travel-mode-bicycle@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "travel-mode-bicycle@2x.png"; sourceTree = ""; };
+ 268F30F022E5DD7900547FE1 /* travel-mode-drive@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "travel-mode-drive@2x.png"; sourceTree = ""; };
+ 268F30F522E5E47600547FE1 /* travel-mode-unknown@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "travel-mode-unknown@2x.png"; sourceTree = ""; };
+ 268F30FE22E5E7BE00547FE1 /* NSUserDefaults+InaUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSUserDefaults+InaUtils.h"; sourceTree = ""; };
+ 268F30FF22E5E7BF00547FE1 /* NSUserDefaults+InaUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSUserDefaults+InaUtils.m"; sourceTree = ""; };
+ 268F647822DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CLLocationCoordinate2D+InaUtils.h"; sourceTree = ""; };
+ 268F647922DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CLLocationCoordinate2D+InaUtils.m"; sourceTree = ""; };
+ 268F647B22DF09D900A85AFD /* NSArray+InaTypedValue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+InaTypedValue.m"; sourceTree = ""; };
+ 268F647C22DF09D900A85AFD /* NSArray+InaTypedValue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+InaTypedValue.h"; sourceTree = ""; };
+ 268F647E22DF15F700A85AFD /* button-icon-nav-refresh.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "button-icon-nav-refresh.png"; sourceTree = ""; };
+ 268F648022DF200300A85AFD /* button-icon-nav-location.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "button-icon-nav-location.png"; sourceTree = ""; };
+ 26962B482306BA2F0026240A /* InaSymbols.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InaSymbols.h; sourceTree = ""; };
+ 26962B502306E8000026240A /* maps-icon-marker-pin-20@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-pin-20@2x.png"; sourceTree = ""; };
+ 26962B512306E8000026240A /* maps-icon-marker-pin-30@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-pin-30@2x.png"; sourceTree = ""; };
+ 26962B522306E8000026240A /* maps-icon-marker-pin-10@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-pin-10@2x.png"; sourceTree = ""; };
+ 26962B532306E8000026240A /* maps-icon-marker-pin-40@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-pin-40@2x.png"; sourceTree = ""; };
+ 26962B582306EB190026240A /* maps-icon-marker-pin-50@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-pin-50@2x.png"; sourceTree = ""; };
+ 2696995922C38B4000B3290E /* AppKeys.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppKeys.m; sourceTree = ""; };
+ 2696995A22C38B4000B3290E /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
+ 2696995B22C38B4000B3290E /* AppKeys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppKeys.h; sourceTree = ""; };
+ 2696995C22C38B4000B3290E /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
+ 2696995F22C38C1D00B3290E /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
+ 2696997E22C3A14A00B3290E /* MapView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapView.h; sourceTree = ""; };
+ 2696997F22C3A14A00B3290E /* MapView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapView.m; sourceTree = ""; };
+ 269F83E222D73E7400CC11A4 /* maps-icon-marker-circle-10@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-circle-10@2x.png"; sourceTree = ""; };
+ 269F83E322D73E7400CC11A4 /* maps-icon-marker-circle-30@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-circle-30@2x.png"; sourceTree = ""; };
+ 269F83E422D73E7400CC11A4 /* maps-icon-marker-circle-20@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-circle-20@2x.png"; sourceTree = ""; };
+ 269F83E522D73E7400CC11A4 /* maps-icon-marker-circle-40@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "maps-icon-marker-circle-40@2x.png"; sourceTree = ""; };
+ 269F83EA22D73EB400CC11A4 /* NSDate+UIUCUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+UIUCUtils.m"; sourceTree = ""; };
+ 269F83EB22D73EB400CC11A4 /* UIColor+InaParse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+InaParse.h"; sourceTree = ""; };
+ 269F83EC22D73EB400CC11A4 /* UIColor+InaParse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+InaParse.m"; sourceTree = ""; };
+ 269F83ED22D73EB400CC11A4 /* NSDate+UIUCUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+UIUCUtils.h"; sourceTree = ""; };
+ 269F83F022D744E200CC11A4 /* CGGeometry+InaUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGGeometry+InaUtils.h"; sourceTree = ""; };
+ 269F83F122D744E200CC11A4 /* CGGeometry+InaUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGGeometry+InaUtils.m"; sourceTree = ""; };
+ 269F83FA22D77E5500CC11A4 /* MapDirectionsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapDirectionsController.h; sourceTree = ""; };
+ 269F83FB22D77E5500CC11A4 /* MapDirectionsController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapDirectionsController.m; sourceTree = ""; };
+ 26A170E822DDFB6500299773 /* button-icon-nav-next.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "button-icon-nav-next.png"; sourceTree = ""; };
+ 26A170E922DDFB6500299773 /* button-icon-nav-prev.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "button-icon-nav-prev.png"; sourceTree = ""; };
+ 26A170EA22DDFB6500299773 /* button-icon-nav-clear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "button-icon-nav-clear.png"; sourceTree = ""; };
+ 26B34034233112320031CF70 /* FlutterCompletion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FlutterCompletion.h; sourceTree = ""; };
+ 26B340352331163A0031CF70 /* MapLocationPickerController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapLocationPickerController.h; sourceTree = ""; };
+ 26B340362331163A0031CF70 /* MapLocationPickerController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapLocationPickerController.m; sourceTree = ""; };
+ 26B340382331195C0031CF70 /* UILabel+InaMeasure.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UILabel+InaMeasure.h"; sourceTree = ""; };
+ 26B340392331195C0031CF70 /* UILabel+InaMeasure.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UILabel+InaMeasure.m"; sourceTree = ""; };
+ 26C07E2C247677A600E28D43 /* ExposurePlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExposurePlugin.h; sourceTree = ""; };
+ 26C07E2D247677A600E28D43 /* ExposurePlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExposurePlugin.m; sourceTree = ""; };
+ 26C90CEB2361CF480092E07F /* NSDictionary+InaPathKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+InaPathKey.h"; sourceTree = ""; };
+ 26C90CEC2361CF480092E07F /* NSDictionary+InaPathKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+InaPathKey.m"; sourceTree = ""; };
+ 26C90CEE2361D13E0092E07F /* NSDictionary+UIUCConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+UIUCConfig.m"; sourceTree = ""; };
+ 26C90CEF2361D13E0092E07F /* NSDictionary+UIUCConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+UIUCConfig.h"; sourceTree = ""; };
+ 26ECB3212487938C00479487 /* CommonCrypto+UIUCUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CommonCrypto+UIUCUtils.h"; sourceTree = ""; };
+ 26ECB3222487938C00479487 /* CommonCrypto+UIUCUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CommonCrypto+UIUCUtils.m"; sourceTree = ""; };
+ 26ECB3242487AD0900479487 /* Security+UIUCUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Security+UIUCUtils.h"; sourceTree = ""; };
+ 26ECB3252487AD0900479487 /* Security+UIUCUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Security+UIUCUtils.m"; sourceTree = ""; };
+ 26FA06BC22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+InaTypedValue.m"; sourceTree = ""; };
+ 26FA06BD22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+InaTypedValue.h"; sourceTree = ""; };
+ 26FAB1C722EB3502008E987C /* NSDictionary+UIUCExplore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+UIUCExplore.h"; sourceTree = ""; };
+ 26FAB1C822EB3502008E987C /* NSDictionary+UIUCExplore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+UIUCExplore.m"; sourceTree = ""; };
+ 26FAB1CA22EB35C7008E987C /* NSDate+InaUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+InaUtils.m"; sourceTree = ""; };
+ 26FAB1CB22EB35C7008E987C /* NSDate+InaUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+InaUtils.h"; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 819274ECAE180D73781EE669 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ AF2509313F75A25D855964F6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ B6201F5924E2D8080050F7DC /* GalleryPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GalleryPlugin.h; sourceTree = ""; };
+ B6201F5A24E2D8080050F7DC /* GalleryPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GalleryPlugin.m; sourceTree = ""; };
+ B681B2B723DEFD9E00093A67 /* NSData+InaHex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+InaHex.h"; sourceTree = ""; };
+ B681B2B823DEFD9E00093A67 /* NSData+InaHex.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+InaHex.m"; sourceTree = ""; };
+ B6BDCD38242CCF5F002F7364 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
+ B6BDCD3F242CDC89002F7364 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; };
+ B6BDCD40242CDCA6002F7364 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = ""; };
+ B6EA372223A7EF76001D78A5 /* Bluetooth+InaUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bluetooth+InaUtils.h"; sourceTree = ""; };
+ B6EA372323A7EF76001D78A5 /* Bluetooth+InaUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Bluetooth+InaUtils.m"; sourceTree = ""; };
+ CE7B5BCF89DF9B7AC1FD247B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ EBA3000FA94D4892DC3E0208 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ BB7BF12F5D3356D25889D23A /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 2696995822C38A7F00B3290E /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 269F83E122D73DC900CC11A4 /* Images */,
+ 26C0B694230584BF005F0A88 /* Assets */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ B6BDCD39242CCF5F002F7364 /* Localizable.strings */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ 269F83E122D73DC900CC11A4 /* Images */ = {
+ isa = PBXGroup;
+ children = (
+ 268F30ED22E5DD7900547FE1 /* travel-mode-walk@2x.png */,
+ 268F30EF22E5DD7900547FE1 /* travel-mode-bicycle@2x.png */,
+ 268F30F022E5DD7900547FE1 /* travel-mode-drive@2x.png */,
+ 268F30EE22E5DD7900547FE1 /* travel-mode-transit@2x.png */,
+ 268F30F522E5E47600547FE1 /* travel-mode-unknown@2x.png */,
+ 26A170EA22DDFB6500299773 /* button-icon-nav-clear.png */,
+ 268F648022DF200300A85AFD /* button-icon-nav-location.png */,
+ 26A170E822DDFB6500299773 /* button-icon-nav-next.png */,
+ 26A170E922DDFB6500299773 /* button-icon-nav-prev.png */,
+ 268F647E22DF15F700A85AFD /* button-icon-nav-refresh.png */,
+ 269F83E222D73E7400CC11A4 /* maps-icon-marker-circle-10@2x.png */,
+ 269F83E422D73E7400CC11A4 /* maps-icon-marker-circle-20@2x.png */,
+ 269F83E322D73E7400CC11A4 /* maps-icon-marker-circle-30@2x.png */,
+ 269F83E522D73E7400CC11A4 /* maps-icon-marker-circle-40@2x.png */,
+ 26962B522306E8000026240A /* maps-icon-marker-pin-10@2x.png */,
+ 26962B502306E8000026240A /* maps-icon-marker-pin-20@2x.png */,
+ 26962B512306E8000026240A /* maps-icon-marker-pin-30@2x.png */,
+ 26962B532306E8000026240A /* maps-icon-marker-pin-40@2x.png */,
+ 26962B582306EB190026240A /* maps-icon-marker-pin-50@2x.png */,
+ );
+ path = Images;
+ sourceTree = "";
+ };
+ 26C0B694230584BF005F0A88 /* Assets */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Assets;
+ sourceTree = "";
+ };
+ 26FA06BB22CCA8B5003B78E4 /* Utils */ = {
+ isa = PBXGroup;
+ children = (
+ B6EA372223A7EF76001D78A5 /* Bluetooth+InaUtils.h */,
+ B6EA372323A7EF76001D78A5 /* Bluetooth+InaUtils.m */,
+ 269F83F022D744E200CC11A4 /* CGGeometry+InaUtils.h */,
+ 269F83F122D744E200CC11A4 /* CGGeometry+InaUtils.m */,
+ 268F647822DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.h */,
+ 268F647922DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.m */,
+ 26FAB1CB22EB35C7008E987C /* NSDate+InaUtils.h */,
+ 26FAB1CA22EB35C7008E987C /* NSDate+InaUtils.m */,
+ B681B2B723DEFD9E00093A67 /* NSData+InaHex.h */,
+ B681B2B823DEFD9E00093A67 /* NSData+InaHex.m */,
+ 26FA06BD22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.h */,
+ 26FA06BC22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m */,
+ 26C90CEB2361CF480092E07F /* NSDictionary+InaPathKey.h */,
+ 26C90CEC2361CF480092E07F /* NSDictionary+InaPathKey.m */,
+ 268F647C22DF09D900A85AFD /* NSArray+InaTypedValue.h */,
+ 268F647B22DF09D900A85AFD /* NSArray+InaTypedValue.m */,
+ 2626D94A22DC99C700F6BC2F /* NSString+InaJson.h */,
+ 2626D94922DC99C700F6BC2F /* NSString+InaJson.m */,
+ 268F30FE22E5E7BE00547FE1 /* NSUserDefaults+InaUtils.h */,
+ 268F30FF22E5E7BF00547FE1 /* NSUserDefaults+InaUtils.m */,
+ 269F83EB22D73EB400CC11A4 /* UIColor+InaParse.h */,
+ 269F83EC22D73EB400CC11A4 /* UIColor+InaParse.m */,
+ 26B340382331195C0031CF70 /* UILabel+InaMeasure.h */,
+ 26B340392331195C0031CF70 /* UILabel+InaMeasure.m */,
+ 26962B482306BA2F0026240A /* InaSymbols.h */,
+ );
+ path = Utils;
+ sourceTree = "";
+ };
+ 26FAB1C622EB34A3008E987C /* UIUC */ = {
+ isa = PBXGroup;
+ children = (
+ 26FAB1C722EB3502008E987C /* NSDictionary+UIUCExplore.h */,
+ 26FAB1C822EB3502008E987C /* NSDictionary+UIUCExplore.m */,
+ 26C90CEF2361D13E0092E07F /* NSDictionary+UIUCConfig.h */,
+ 26C90CEE2361D13E0092E07F /* NSDictionary+UIUCConfig.m */,
+ 269F83ED22D73EB400CC11A4 /* NSDate+UIUCUtils.h */,
+ 269F83EA22D73EB400CC11A4 /* NSDate+UIUCUtils.m */,
+ 26ECB3212487938C00479487 /* CommonCrypto+UIUCUtils.h */,
+ 26ECB3222487938C00479487 /* CommonCrypto+UIUCUtils.m */,
+ 26ECB3242487AD0900479487 /* Security+UIUCUtils.h */,
+ 26ECB3252487AD0900479487 /* Security+UIUCUtils.m */,
+ );
+ path = UIUC;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ A4FE37309F85C78E8A871D27 /* Pods */,
+ F8180DF05DB99BBF17C34614 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 2696995C22C38B4000B3290E /* AppDelegate.h */,
+ 2696995A22C38B4000B3290E /* AppDelegate.m */,
+ 2696995B22C38B4000B3290E /* AppKeys.h */,
+ 2696995922C38B4000B3290E /* AppKeys.m */,
+ 266FA50123E0388B00F800F5 /* MapController.h */,
+ 266FA50223E0388B00F800F5 /* MapController.m */,
+ 269F83FA22D77E5500CC11A4 /* MapDirectionsController.h */,
+ 269F83FB22D77E5500CC11A4 /* MapDirectionsController.m */,
+ 26B340352331163A0031CF70 /* MapLocationPickerController.h */,
+ 26B340362331163A0031CF70 /* MapLocationPickerController.m */,
+ 2696997E22C3A14A00B3290E /* MapView.h */,
+ 2696997F22C3A14A00B3290E /* MapView.m */,
+ 2626D94C22DCB80000F6BC2F /* MapMarkerView.h */,
+ 2626D94D22DCB80000F6BC2F /* MapMarkerView.m */,
+ 26C07E2C247677A600E28D43 /* ExposurePlugin.h */,
+ 26C07E2D247677A600E28D43 /* ExposurePlugin.m */,
+ B6201F5924E2D8080050F7DC /* GalleryPlugin.h */,
+ B6201F5A24E2D8080050F7DC /* GalleryPlugin.m */,
+ 26B34034233112320031CF70 /* FlutterCompletion.h */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 2696995F22C38C1D00B3290E /* main.m */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 2605FF52236C13A6002F71BE /* GoogleService-Info-Debug.plist */,
+ 2605FF53236C13A6002F71BE /* GoogleService-Info-Release.plist */,
+ 2668953022E1E074003CAB94 /* Runner.entitlements */,
+ 26FAB1C622EB34A3008E987C /* UIUC */,
+ 26FA06BB22CCA8B5003B78E4 /* Utils */,
+ 2696995822C38A7F00B3290E /* Resources */,
+ 97C146F11CF9000F007C117D /* Supporting Files */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = "Supporting Files";
+ sourceTree = "";
+ };
+ A4FE37309F85C78E8A871D27 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 819274ECAE180D73781EE669 /* Pods-Runner.debug.xcconfig */,
+ EBA3000FA94D4892DC3E0208 /* Pods-Runner.release.xcconfig */,
+ AF2509313F75A25D855964F6 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "";
+ };
+ F8180DF05DB99BBF17C34614 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ CE7B5BCF89DF9B7AC1FD247B /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 6B05DA47C845612A0CC4C440 /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ A6EAEE2D144BCA7DA3F58188 /* [CP] Embed Pods Frameworks */,
+ 263F851F234C9C8B00397B65 /* ShellScript */,
+ 7566F6C06D2127BA706144EC /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1130;
+ ORGANIZATIONNAME = "The Chromium Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ DevelopmentTeam = UPV4CB4H6W;
+ LastSwiftMigration = 0910;
+ SystemCapabilities = {
+ com.apple.Push = {
+ enabled = 1;
+ };
+ };
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ es,
+ zh,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 268F30F422E5DD7900547FE1 /* travel-mode-drive@2x.png in Resources */,
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 2605FF55236C13A6002F71BE /* GoogleService-Info-Release.plist in Resources */,
+ 268F30F322E5DD7900547FE1 /* travel-mode-bicycle@2x.png in Resources */,
+ 26962B572306E8000026240A /* maps-icon-marker-pin-40@2x.png in Resources */,
+ 269F83E722D73E7400CC11A4 /* maps-icon-marker-circle-30@2x.png in Resources */,
+ 268F30F122E5DD7900547FE1 /* travel-mode-walk@2x.png in Resources */,
+ 26962B552306E8000026240A /* maps-icon-marker-pin-30@2x.png in Resources */,
+ 268F647F22DF15F700A85AFD /* button-icon-nav-refresh.png in Resources */,
+ 268F648122DF200300A85AFD /* button-icon-nav-location.png in Resources */,
+ 269F83E622D73E7400CC11A4 /* maps-icon-marker-circle-10@2x.png in Resources */,
+ 269F83E922D73E7400CC11A4 /* maps-icon-marker-circle-40@2x.png in Resources */,
+ 26A170EC22DDFB6500299773 /* button-icon-nav-prev.png in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 269F83E822D73E7400CC11A4 /* maps-icon-marker-circle-20@2x.png in Resources */,
+ 26962B542306E8000026240A /* maps-icon-marker-pin-20@2x.png in Resources */,
+ 26962B592306EB190026240A /* maps-icon-marker-pin-50@2x.png in Resources */,
+ 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
+ 26A170ED22DDFB6500299773 /* button-icon-nav-clear.png in Resources */,
+ 268F30F222E5DD7900547FE1 /* travel-mode-transit@2x.png in Resources */,
+ 26962B562306E8000026240A /* maps-icon-marker-pin-10@2x.png in Resources */,
+ 268F30F622E5E47600547FE1 /* travel-mode-unknown@2x.png in Resources */,
+ 26A170EB22DDFB6500299773 /* button-icon-nav-next.png in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ 2605FF54236C13A6002F71BE /* GoogleService-Info-Debug.plist in Resources */,
+ B6BDCD37242CCF5F002F7364 /* Localizable.strings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 263F851F234C9C8B00397B65 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)",
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "./build.sh\n";
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 6B05DA47C845612A0CC4C440 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 7566F6C06D2127BA706144EC /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
+ };
+ A6EAEE2D144BCA7DA3F58188 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 269F83EF22D73EB400CC11A4 /* UIColor+InaParse.m in Sources */,
+ B6201F5B24E2D8080050F7DC /* GalleryPlugin.m in Sources */,
+ 268F310022E5E7BF00547FE1 /* NSUserDefaults+InaUtils.m in Sources */,
+ 26C90CF02361D13E0092E07F /* NSDictionary+UIUCConfig.m in Sources */,
+ 269F83F222D744E200CC11A4 /* CGGeometry+InaUtils.m in Sources */,
+ B6EA372423A7EF76001D78A5 /* Bluetooth+InaUtils.m in Sources */,
+ 269F83FC22D77E5500CC11A4 /* MapDirectionsController.m in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 268F647D22DF09D900A85AFD /* NSArray+InaTypedValue.m in Sources */,
+ 2696995D22C38B4000B3290E /* AppKeys.m in Sources */,
+ 268F647A22DF050400A85AFD /* CLLocationCoordinate2D+InaUtils.m in Sources */,
+ 2626D94E22DCB80000F6BC2F /* MapMarkerView.m in Sources */,
+ 269F83EE22D73EB400CC11A4 /* NSDate+UIUCUtils.m in Sources */,
+ B681B2B923DEFD9E00093A67 /* NSData+InaHex.m in Sources */,
+ 26FA06BE22CCA9B5003B78E4 /* NSDictionary+InaTypedValue.m in Sources */,
+ 2626D94B22DC99C800F6BC2F /* NSString+InaJson.m in Sources */,
+ 26C07E2E247677A600E28D43 /* ExposurePlugin.m in Sources */,
+ 26C90CED2361CF480092E07F /* NSDictionary+InaPathKey.m in Sources */,
+ 26ECB3262487AD0900479487 /* Security+UIUCUtils.m in Sources */,
+ 26FAB1C922EB3502008E987C /* NSDictionary+UIUCExplore.m in Sources */,
+ 266FA50323E0388B00F800F5 /* MapController.m in Sources */,
+ 2696996022C38C1D00B3290E /* main.m in Sources */,
+ 2696995E22C38B4000B3290E /* AppDelegate.m in Sources */,
+ 26B3403A2331195C0031CF70 /* UILabel+InaMeasure.m in Sources */,
+ 26FAB1CC22EB35C7008E987C /* NSDate+InaUtils.m in Sources */,
+ 2696998022C3A14A00B3290E /* MapView.m in Sources */,
+ 26B340372331163A0031CF70 /* MapLocationPickerController.m in Sources */,
+ 26ECB3232487938C00479487 /* CommonCrypto+UIUCUtils.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+ B6BDCD39242CCF5F002F7364 /* Localizable.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ B6BDCD38242CCF5F002F7364 /* en */,
+ B6BDCD3F242CDC89002F7364 /* es */,
+ B6BDCD40242CDCA6002F7364 /* zh */,
+ );
+ name = Localizable.strings;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = UPV4CB4H6W;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = edu.illinois.covid;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 4.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = UPV4CB4H6W;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = edu.illinois.covid;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_SWIFT3_OBJC_INFERENCE = Default;
+ SWIFT_VERSION = 4.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = UPV4CB4H6W;
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = edu.illinois.covid;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_SWIFT3_OBJC_INFERENCE = Default;
+ SWIFT_VERSION = 4.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..1d526a16
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..6b30c745
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,10 @@
+
+
+
+
+ BuildSystemType
+ Original
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..ad6250a1
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..ee73d111
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDETemplateMacros.plist b/ios/Runner.xcworkspace/xcshareddata/IDETemplateMacros.plist
new file mode 100644
index 00000000..a96f0b50
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDETemplateMacros.plist
@@ -0,0 +1,27 @@
+
+
+
+
+ FILEHEADER
+ // ___FILENAME___
+// ___PACKAGENAME___
+//
+// Created by ___FULLUSERNAME___ on ___DATE___.
+// ___COPYRIGHT___
+//
+ COPYRIGHT
+ Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner/AppDelegate.h b/ios/Runner/AppDelegate.h
new file mode 100644
index 00000000..76c46d7b
--- /dev/null
+++ b/ios/Runner/AppDelegate.h
@@ -0,0 +1,31 @@
+//
+// AppDelegate.h
+// Runner
+//
+// Created by Mihail Varbanov on 2/19/19.
+// Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import
+#import
+
+@class FlutterMethodChannel;
+
+@interface AppDelegate : FlutterAppDelegate
+@property (nonatomic, readonly) FlutterMethodChannel* flutterMethodChannel;
+@property (nonatomic, readonly) NSDictionary* keys;
++ (instancetype)sharedInstance;
+@end
+
diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m
new file mode 100644
index 00000000..79cf3afb
--- /dev/null
+++ b/ios/Runner/AppDelegate.m
@@ -0,0 +1,1430 @@
+//
+// AppDelegate.m
+// Runner
+//
+// Created by Mihail Varbanov on 2/19/19.
+// Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "AppDelegate.h"
+#import "GeneratedPluginRegistrant.h"
+#import "AppKeys.h"
+#import "MapView.h"
+#import "MapController.h"
+#import "MapDirectionsController.h"
+#import "MapLocationPickerController.h"
+#import "ExposurePlugin.h"
+#import "GalleryPlugin.h"
+
+#import "NSArray+InaTypedValue.h"
+#import "NSDictionary+InaTypedValue.h"
+#import "NSDictionary+UIUCConfig.h"
+#import "CGGeometry+InaUtils.h"
+#import "UIColor+InaParse.h"
+#import "Bluetooth+InaUtils.h"
+
+#import
+#import
+#import
+#import
+#import
+
+#import
+#import
+#import
+#import
+
+static NSString *const kFIRMessagingFCMTokenNotification = @"com.firebase.iid.notif.fcm-token";
+
+@interface RootNavigationController : UINavigationController
+@end
+
+@interface LaunchScreenView : UIView
+@end
+
+UIInterfaceOrientation _interfaceOrientationFromString(NSString *value);
+NSString* _interfaceOrientationToString(UIInterfaceOrientation value);
+
+UIInterfaceOrientation _interfaceOrientationFromMask(UIInterfaceOrientationMask value);
+UIInterfaceOrientationMask _interfaceOrientationToMask(UIInterfaceOrientation value);
+
+@interface AppDelegate() {
+}
+
+// Flutter
+@property (nonatomic) UINavigationController *navigationViewController;
+@property (nonatomic) FlutterViewController *flutterViewController;
+@property (nonatomic) FlutterMethodChannel *flutterMethodChannel;
+
+// Launch View
+@property (nonatomic) UIView *launchScreenView;
+
+// PassKit
+@property (nonatomic) PKAddPassesViewController *passViewController;
+@property (nonatomic) FlutterResult passFlutterResult;
+
+// BlinkId
+@property (nonatomic) bool blinkSDKInitialized;
+@property (nonatomic) MBBlinkIdCombinedRecognizer *blinkCombinedRecognizer;
+@property (nonatomic) MBPassportRecognizer *blinkPassportRecognizer;
+@property (nonatomic) UIViewController *blinkRecognizerRunnerViewController;
+@property (nonatomic) FlutterResult blinkFlutterResult;
+
+// Init Keys
+@property (nonatomic) NSDictionary* keys;
+
+// Interface Orientations
+@property (nonatomic) NSSet *supportedInterfaceOrientations;
+@property (nonatomic) UIInterfaceOrientation preferredInterfaceOrientation;
+
+// Location Services
+@property (nonatomic) CLLocationManager *clLocationManager;
+@property (nonatomic) NSMutableSet *locationFlutterResults;
+
+// Bluetooth Services
+@property (nonatomic) CBPeripheralManager *peripheralManager;
+@property (nonatomic) NSMutableSet *bluetoothFlutterResults;
+@end
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+
+ __weak typeof(self) weakSelf = self;
+
+// Initialize Google Maps SDK
+// [GMSServices provideAPIKey:kGoogleAPIKey];
+
+// Initialize Maps Indoors SDK
+// [MapsIndoors provideAPIKey:kMapsIndoorsAPIKey googleAPIKey:kGoogleAPIKey];
+
+// Initialize MicroBlink SDK
+// [MBMicroblinkSDK.sharedInstance setLicenseKey:kMicroBlinkLicenseKey];
+
+
+ // Initialize Firebase SDK
+ [FIRApp configure];
+ [FIRMessaging messaging].delegate = self;
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveFCMTokenNotification:) name:kFIRMessagingFCMTokenNotification object:nil];
+
+ // Initialize Flutter plugins
+ [GeneratedPluginRegistrant registerWithRegistry:self];
+
+ // Setup MapPlugin
+ NSObject*registrar = [self registrarForPlugin:@"MapPlugin"];
+ MapViewFactory *factory = [[MapViewFactory alloc] initWithMessenger:registrar.messenger];
+ [registrar registerViewFactory:factory withId:@"mapview"];
+
+ // Setup ExposurePlugin
+ [ExposurePlugin registerWithRegistrar:[self registrarForPlugin:@"ExposurePlugin"]];
+
+ // Setup ExposurePlugin
+ [GalleryPlugin registerWithRegistrar:[self registrarForPlugin:@"GalleryPlugin"]];
+
+ // Setup supported & preffered orientation
+ _preferredInterfaceOrientation = UIInterfaceOrientationPortrait;
+ _supportedInterfaceOrientations = [NSSet setWithObject:@(_preferredInterfaceOrientation)];
+
+ // Setup root ViewController
+ UIViewController *rootViewController = self.window.rootViewController;
+ _flutterViewController = [rootViewController isKindOfClass:[FlutterViewController class]] ? (FlutterViewController*)rootViewController : nil;
+
+ _navigationViewController = [[RootNavigationController alloc] initWithRootViewController:rootViewController];
+ _navigationViewController.navigationBarHidden = YES;
+ _navigationViewController.delegate = self;
+
+ _navigationViewController.navigationBar.translucent = NO;
+ _navigationViewController.navigationBar.barTintColor = [UIColor inaColorWithHex:@"13294b"];
+ _navigationViewController.navigationBar.tintColor = [UIColor whiteColor];
+ _navigationViewController.navigationBar.titleTextAttributes = @{
+ NSForegroundColorAttributeName : [UIColor whiteColor]
+ };
+
+ [self setupLaunchScreen];
+
+ self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+ self.window.rootViewController = _navigationViewController;
+ [self.window makeKeyAndVisible];
+
+ // Listen Method Channel
+ _flutterMethodChannel = [FlutterMethodChannel methodChannelWithName:kFlutterMetodChannelName binaryMessenger:_flutterViewController.binaryMessenger];
+ [_flutterMethodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
+ [weakSelf handleFlutterAPIFromCall:call result:result];
+ }];
+
+ // Push Notifications
+ [UNUserNotificationCenter currentNotificationCenter].delegate = self;
+ [self queryNotificationsAuthorizationStatusWithCompletionHandler:^(bool authorized){
+ if (authorized) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [weakSelf registerForRemoteNotifications];
+ });
+ }
+ }];
+
+ return [super application:application didFinishLaunchingWithOptions:launchOptions];
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+
+ // Push Notifications
+ if (UNUserNotificationCenter.currentNotificationCenter.delegate == self) {
+ UNUserNotificationCenter.currentNotificationCenter.delegate = nil;
+ }
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [super applicationWillTerminate:application];
+}
+
++ (instancetype)sharedInstance {
+ id sharedInstance = [UIApplication sharedApplication].delegate;
+ return [sharedInstance isKindOfClass:self] ? sharedInstance : nil;
+}
+
+#pragma mark LifeCycle
+
+-(void)applicationDidEnterForeground:(UIApplication *)application{
+ NSLog(@"applicationDidEnterForeground:");
+}
+
+-(void)applicationDidEnterBackground:(UIApplication *)application{
+ NSLog(@"applicationDidEnterBackground:");
+}
+
+#pragma mark Launch Screen
+
+- (void)setupLaunchScreen {
+
+ if (_launchScreenView != nil) {
+ [_launchScreenView removeFromSuperview];
+ }
+
+ UIView *parentView = _navigationViewController.viewControllers.firstObject.view;
+ _launchScreenView = [[LaunchScreenView alloc] initWithFrame:CGRectMake(0, 0, parentView.bounds.size.width, parentView.bounds.size.height)];
+ _launchScreenView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ [parentView addSubview:_launchScreenView];
+}
+
+- (void)removeLaunchScreen {
+ if (_launchScreenView != nil) {
+ __weak typeof(self) weakSelf = self;
+ [UIView animateWithDuration:0.5 animations:^{
+ weakSelf.launchScreenView.alpha = 0;
+ } completion:^(BOOL finished) {
+ [weakSelf.launchScreenView removeFromSuperview];
+ weakSelf.launchScreenView = nil;
+ }];
+ }
+}
+
+#pragma mark Flutter APIs
+
+- (void)handleFlutterAPIFromCall:(FlutterMethodCall*)call result:(FlutterResult)result {
+ NSDictionary *parameters = [call.arguments isKindOfClass:[NSDictionary class]] ? call.arguments : nil;
+ if ([call.method isEqualToString:@"init"]) {
+ [self handleInitWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"directions"]) {
+ [self handleDirectionsWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"pickLocation"]) {
+ [self handlePickLocationWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"map"]) {
+ [self handleMapWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"showNotification"]) {
+ [self handleShowNotificationWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"dismissSafariVC"]) {
+ [self handleDismissSafariVCWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"dismissLaunchScreen"]) {
+ [self handleDismissLaunchScreenWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"firebaseInfo"]) {
+ [self handleFirebaseInfoWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"notifications_authorization"]) {
+ [self handleNotificationsWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"location_services_permission"]) {
+ [self handleLocationServicesWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"bluetooth_authorization"]) {
+ [self handleBluetoothAuthorizationWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"addToWallet"]) {
+ [self handleAddToWalletWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"microBlinkScan"]) {
+ [self handleMicroBlinkScanWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"deviceId"]) {
+ [self handleDeviceIdWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"healthRSAPrivateKey"]) {
+ [self handleHealthRSAPrivateKeyWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"enabledOrientations"]) {
+ [self handleEnabledOrientationsWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"barcode"]) {
+ [self handleBarcodeWithParameters:parameters result:result];
+ }
+ else if ([call.method isEqualToString:@"test"]) {
+ [self handleTestWithParameters:parameters result:result];
+ }
+}
+
+- (void)handleInitWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ self.keys = [parameters inaDictForKey:@"keys"];
+
+ // Initialize Google Maps SDK
+ NSString *googleMapsAPIKey = [_keys uiucConfigStringForPathKey:@"google.maps.api_key"];
+ if (0 < googleMapsAPIKey.length) {
+ [GMSServices provideAPIKey:googleMapsAPIKey];
+ }
+
+ // Initialize Maps Indoors SDK
+ NSString *mapsIndoorsAPIKey = [_keys uiucConfigStringForPathKey:@"mapsindoors.api_key"];
+ if ((0 < mapsIndoorsAPIKey.length) && (0 < googleMapsAPIKey.length)) {
+ [MapsIndoors provideAPIKey:mapsIndoorsAPIKey googleAPIKey:googleMapsAPIKey];
+ }
+
+ // Initialize MicroBlink SDK
+ /*NSString *microBlinkLicenseKey = [_keys uiucConfigStringForPathKey:@"microblink.blink_id.license_key.ios"];
+ if (0 < microBlinkLicenseKey.length) {
+ @try {
+ [MBMicroblinkSDK.sharedInstance setLicenseKey:microBlinkLicenseKey];
+ _blinkSDKInitialized = YES;
+ }
+ @catch(NSException *e) { NSLog(@"%@", e); }
+ }*/
+
+ result(@(YES));
+}
+
+- (void)handleDirectionsWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ MapDirectionsController *directionsController = [[MapDirectionsController alloc] initWithParameters:parameters completionHandler:^(id returnValue) {
+ result(returnValue);
+ }];
+ [self.navigationViewController pushViewController:directionsController animated:YES];
+}
+
+- (void)handlePickLocationWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ MapLocationPickerController *pickLocationController = [[MapLocationPickerController alloc] initWithParameters:parameters completionHandler:^(id returnValue) {
+ result(returnValue);
+ }];
+ [self.navigationViewController pushViewController:pickLocationController animated:YES];
+}
+
+- (void)handleMapWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ MapController *mapController = [[MapController alloc] initWithParameters:parameters completionHandler:^(id returnValue) {
+ result(returnValue);
+ }];
+ [self.navigationViewController pushViewController:mapController animated:YES];
+}
+
+- (void)handleShowNotificationWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+ content.title = [parameters inaStringForKey:@"title"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
+ content.subtitle = [parameters inaStringForKey:@"subtitle"];
+ content.body = [parameters inaStringForKey:@"body"];
+ content.sound = [parameters inaBoolForKey:@"sound" defaults:true] ? [UNNotificationSound defaultSound] : nil;
+
+ UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger
+ triggerWithTimeInterval:1 repeats:NO];
+
+ UNNotificationRequest* request = [UNNotificationRequest
+ requestWithIdentifier:@"Poll_Created" content:content trigger:trigger];
+
+ UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
+ [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
+ if (error != nil) {
+ NSLog(@"%@", error.localizedDescription);
+ }
+ }];
+}
+
+- (void)handleDismissSafariVCWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ UIViewController *presentedController = self.flutterViewController.presentedViewController;
+ if ([presentedController isKindOfClass:[SFSafariViewController class]]) {
+ [presentedController dismissViewControllerAnimated:YES completion:^{
+ result(@(YES));
+ }];
+ }
+ else {
+ result(@(NO));
+ }
+}
+
+- (void)handleDismissLaunchScreenWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ [self removeLaunchScreen];
+}
+
+- (void)handleFirebaseInfoWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ FIRApp *firApp = [FIRApp defaultApp];
+ FIROptions *options = (firApp != nil) ? [firApp options] : nil;
+ NSString *projectID = (options != nil) ? [options projectID] : nil;
+ result(projectID);
+}
+
+- (void)handleNotificationsWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ NSString *method = [parameters inaStringForKey:@"method"];
+ if ([method isEqualToString:@"query"]) {
+ [self queryNotificationsAuthorizationWithFlutterResult:result];
+ }
+ else if ([method isEqualToString:@"request"]) {
+ [self requestNotificationsAuthorizationWithFlutterResult:result];
+ }
+ else {
+ result(nil);
+ }
+}
+
+- (void)handleLocationServicesWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ NSString *method = [parameters inaStringForKey:@"method"];
+ if ([method isEqualToString:@"query"]) {
+ [self queryLocationServicesPermisionWithFlutterResult:result];
+ }
+ else if ([method isEqualToString:@"request"]) {
+ [self requestLocationServicesPermisionWithFlutterResult:result];
+ }
+ else {
+ result(nil);
+ }
+}
+
+- (void)handleBluetoothAuthorizationWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ NSString *method = [parameters inaStringForKey:@"method"];
+ if ([method isEqualToString:@"query"]) {
+ [self queryBluetoothAuthorizationWithFlutterResult:result];
+ }
+ else if ([method isEqualToString:@"request"]) {
+ [self requestBluetoothAuthorizationWithFlutterResult:result];
+ }
+ else {
+ result(nil);
+ }
+}
+
+
+- (void)handleAddToWalletWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ NSString *base64CardData = [parameters inaStringForKey:@"cardBase64Data"];
+ NSData *cardData = [[NSData alloc] initWithBase64EncodedString:base64CardData options:0];
+ [self addPassToWallet:cardData result:result];
+ result(nil);
+}
+
+- (void)handleMicroBlinkScanWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ [self microBlinkScanWithParameters:parameters result:result];
+}
+
+
+- (void)handleDeviceIdWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ result(self.deviceUUID.UUIDString);
+}
+
+- (void)handleHealthRSAPrivateKeyWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ result([self healthRSAPrivateKeyWithParameters:parameters]);
+}
+
+- (void)handleTestWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ result(nil);
+}
+
+#pragma mark Barcode
+
+- (void)handleBarcodeWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ NSString *content = [parameters inaStringForKey:@"content"];
+ NSString *formatName = [parameters inaStringForKey:@"format"];
+ int width = [parameters inaIntForKey:@"width"];
+ int height = [parameters inaIntForKey:@"height"];
+
+ ZXBarcodeFormat format = 0;
+ if ([formatName isEqualToString:@"aztec"]) {
+ format = kBarcodeFormatAztec;
+ } else if ([formatName isEqualToString:@"codabar"]) {
+ format = kBarcodeFormatCodabar;
+ } else if ([formatName isEqualToString:@"code39"]) {
+ format = kBarcodeFormatCode39;
+ } else if ([formatName isEqualToString:@"code93"]) {
+ format = kBarcodeFormatCode93;
+ } else if ([formatName isEqualToString:@"code128"]) {
+ format = kBarcodeFormatCode128;
+ } else if ([formatName isEqualToString:@"dataMatrix"]) {
+ format = kBarcodeFormatDataMatrix;
+ } else if ([formatName isEqualToString:@"ean8"]) {
+ format = kBarcodeFormatEan8;
+ } else if ([formatName isEqualToString:@"ean13"]) {
+ format = kBarcodeFormatEan13;
+ } else if ([formatName isEqualToString:@"itf"]) {
+ format = kBarcodeFormatITF;
+ } else if ([formatName isEqualToString:@"maxiCode"]) {
+ format = kBarcodeFormatMaxiCode;
+ } else if ([formatName isEqualToString:@"pdf417"]) {
+ format = kBarcodeFormatPDF417;
+ } else if ([formatName isEqualToString:@"qrCode"]) {
+ format = kBarcodeFormatQRCode;
+ } else if ([formatName isEqualToString:@"rss14"]) {
+ format = kBarcodeFormatRSS14;
+ } else if ([formatName isEqualToString:@"rssExpanded"]) {
+ format = kBarcodeFormatRSSExpanded;
+ } else if ([formatName isEqualToString:@"upca"]) {
+ format = kBarcodeFormatUPCA;
+ } else if ([formatName isEqualToString:@"upce"]) {
+ format = kBarcodeFormatUPCE;
+ } else if ([formatName isEqualToString:@"upceanExtension"]) {
+ format = kBarcodeFormatUPCEANExtension;
+ }
+
+ NSError *error = nil;
+ UIImage *image = nil;
+ ZXEncodeHints *hints = [ZXEncodeHints hints];
+ hints.margin = @(0);
+ ZXBitMatrix* matrix = [[ZXMultiFormatWriter writer] encode:content format:format width:width height:height hints:hints error:&error];
+ if (matrix != nil) {
+ CGImageRef imageRef = CGImageRetain([[ZXImage imageWithMatrix:matrix] cgimage]);
+ image = [UIImage imageWithCGImage:imageRef];
+ CGImageRelease(imageRef);
+ }
+
+ NSData *imageData = (image != nil) ? UIImagePNGRepresentation(image) : nil;
+ NSString *base64ImageData = (imageData != nil) ? [imageData base64EncodedStringWithOptions:0] : nil;
+ result(base64ImageData);
+}
+
+/*
+//#import "NKDBarcodeFramework.h"
+#import "NKDBarcode.h"
+#import "NKDBarcodeOffscreenView.h"
+#import "NKDCode39Barcode.h"
+#import "NKDExtendedCode39Barcode.h"
+#import "NKDInterleavedTwoOfFiveBarcode.h"
+#import "NKDModifiedPlesseyBarcode.h"
+#import "NKDPostnetBarcode.h"
+#import "NKDUPCABarcode.h"
+#import "NKDModifiedPlesseyHexBarcode.h"
+#import "NKDIndustrialTwoOfFiveBarcode.h"
+#import "NKDEAN13Barcode.h"
+#import "NKDCode128Barcode.h"
+#import "NKDCodabarBarcode.h"
+#import "UIImage-NKDBarcode.h"
+#import "UIImage-Normalize.h"
+#import "NKDUPCEBarcode.h"
+#import "NKDEAN8Barcode.h"
+#import "NKDRoyalMailBarcode.h"
+#import "NKDPlanetBarcode.h"
+
+- (void)handleBarcodeWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ NSString *content = [parameters inaStringForKey:@"content"];
+ NSString *formatName = [parameters inaStringForKey:@"format"];
+ float barWidth = [parameters inaFloatForKey:@"barWidth"];
+ float height = [parameters inaFloatForKey:@"height"];
+
+ NKDBarcode *format = nil;
+ if ([formatName isEqualToString:@"codabar"]) {
+ format = [NKDCodabarBarcode alloc];
+ } else if ([formatName isEqualToString:@"code39"]) {
+ format = [NKDCode39Barcode alloc];
+ } else if ([formatName isEqualToString:@"code128"]) {
+ format = [NKDCode128Barcode alloc];
+ } else if ([formatName isEqualToString:@"upca"]) {
+ format = [NKDUPCABarcode alloc];
+ } else if ([formatName isEqualToString:@"upce"]) {
+ format = [NKDUPCEBarcode alloc];
+ } else if ([formatName isEqualToString:@"ean13"]) {
+ format = [NKDEAN13Barcode alloc];
+ } else if ([formatName isEqualToString:@"ean8"]) {
+ format = [NKDEAN8Barcode alloc];
+
+ } else if ([formatName isEqualToString:@"code93ext"]) {
+ format = [NKDExtendedCode39Barcode alloc];
+ } else if ([formatName isEqualToString:@"plesseyMod"]) {
+ format = [NKDModifiedPlesseyBarcode alloc];
+ } else if ([formatName isEqualToString:@"plesseyModHex"]) {
+ format = [NKDModifiedPlesseyHexBarcode alloc];
+ } else if ([formatName isEqualToString:@"postnet"]) {
+ format = [NKDPostnetBarcode alloc];
+ } else if ([formatName isEqualToString:@"industrial"]) {
+ format = [NKDIndustrialTwoOfFiveBarcode alloc];
+ }
+
+ format = [format initWithContent:content printsCaption:NO andBarWidth:barWidth andHeight:height andFontSize:0 andCheckDigit:(char)-1];
+
+ UIImage *image = (format != nil) ? [UIImage imageFromBarcode:format] : nil; // ..or as a less accu
+ NSData *imageData = (image != nil) ? UIImagePNGRepresentation(image) : nil;
+ NSString *base64ImageData = (imageData != nil) ? [imageData base64EncodedStringWithOptions:0] : nil;
+ result(base64ImageData);
+}
+*/
+
+#pragma mark Orientations
+
+- (void)handleEnabledOrientationsWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+
+ NSMutableArray *resultList = [[NSMutableArray alloc] init];
+ if (_preferredInterfaceOrientation != UIInterfaceOrientationUnknown) {
+ [resultList addObject:_interfaceOrientationToString(_preferredInterfaceOrientation)];
+ }
+ for (NSNumber *supportedOrienation in _supportedInterfaceOrientations) {
+ if (supportedOrienation.intValue != _preferredInterfaceOrientation) {
+ [resultList addObject:_interfaceOrientationToString(supportedOrienation.intValue)];
+ }
+ }
+
+ NSArray *orientationsList = [parameters inaArrayForKey:@"orientations"];
+ if (orientationsList != nil) {
+ UIInterfaceOrientation preferredInterfaceOrientation = UIInterfaceOrientationUnknown;
+ NSMutableSet *supportedOrientations = [[NSMutableSet alloc] init];
+ for (NSString *orientationString in orientationsList) {
+ UIInterfaceOrientation orientation = ([orientationString isKindOfClass:[NSString class]]) ? _interfaceOrientationFromString(orientationString) : UIInterfaceOrientationUnknown;
+ if (orientation != UIInterfaceOrientationUnknown) {
+ [supportedOrientations addObject:@(orientation)];
+ if (preferredInterfaceOrientation == UIInterfaceOrientationUnknown) {
+ preferredInterfaceOrientation = orientation;
+ }
+ }
+ }
+
+ if ((preferredInterfaceOrientation != UIInterfaceOrientationUnknown) && (_preferredInterfaceOrientation != preferredInterfaceOrientation)) {
+ _preferredInterfaceOrientation = preferredInterfaceOrientation;
+ }
+
+ if ((0 < supportedOrientations.count) && ![_supportedInterfaceOrientations isEqualToSet:supportedOrientations]) {
+ _supportedInterfaceOrientations = supportedOrientations;
+ UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation];
+ if (![_supportedInterfaceOrientations containsObject:@(currentOrientation)]) {
+ [[UIDevice currentDevice] setValue:@(_preferredInterfaceOrientation) forKey:@"orientation"];
+ }
+ }
+ }
+
+ result(resultList);
+
+}
+
+/*
+[_navigationViewController.topViewController presentViewController:[[UIViewController alloc] init] animated:NO completion:^{
+ [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(closeForceOrientationConrtoller:) userInfo:nil repeats:NO];
+}];
+- (void)closeForceOrientationConrtoller:(NSTimer*)timer {
+ [_navigationViewController.topViewController dismissViewControllerAnimated:NO completion:nil];
+}
+*/
+
+#pragma mark Push Notifications
+
+- (void)queryNotificationsAuthorizationWithFlutterResult:(FlutterResult)result {
+ [self queryNotificationsAuthorizationStatusWithCompletionHandler:^(bool authorized){
+ result(authorized ? @(YES) : @(NO));
+ }];
+}
+
+- (void)queryNotificationsAuthorizationStatusWithCompletionHandler:(void(^)(bool authorized)) completionHandler {
+ [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) {
+ completionHandler((settings.authorizationStatus != UNAuthorizationStatusNotDetermined) && (settings.authorizationStatus != UNAuthorizationStatusDenied));
+ }];
+}
+
+- (void)requestNotificationsAuthorizationWithFlutterResult:(FlutterResult)result {
+ __weak typeof(self) weakSelf = self;
+ [UNUserNotificationCenter.currentNotificationCenter getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) {
+ if (settings.authorizationStatus == UNAuthorizationStatusDenied) {
+ result(@(NO));
+ }
+ else {
+ UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound;
+ [UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error){
+ dispatch_async(dispatch_get_main_queue(), ^{
+ result(granted ? @(YES) : @(NO));
+ if (granted) {
+ [weakSelf registerForRemoteNotifications];
+ }
+ });
+ }];
+ }
+ }];
+}
+
+- (void)registerForRemoteNotifications {
+ [UNUserNotificationCenter currentNotificationCenter].delegate = self;
+ [[UIApplication sharedApplication] registerForRemoteNotifications];
+}
+
+- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
+ NSLog(@"UIApplication didRegisterForRemoteNotificationsWithDeviceToken: %@", [NSString stringWithFormat:@"%@", deviceToken]);
+ [FIRMessaging messaging].APNSToken = deviceToken;
+}
+
+- (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error {
+ NSLog(@"UIApplication didFailToRegisterForRemoteNotificationsWithError: %@", error);
+}
+
+- (void)processPushNotification:(NSDictionary*)userInfo {
+ [[FIRMessaging messaging] appDidReceiveMessage:userInfo];
+}
+
+#pragma mark LocationServices
+
+- (void)queryLocationServicesPermisionWithFlutterResult:(FlutterResult)result {
+ NSString *status = [CLLocationManager locationServicesEnabled] ?
+ [self.class locationServicesPermisionFromAuthorizationStatus:[CLLocationManager authorizationStatus]] :
+ @"disabled";
+ result(status);
+}
+
++ (NSString*)locationServicesPermisionFromAuthorizationStatus:(CLAuthorizationStatus)authorizationStatus {
+ switch (authorizationStatus) {
+ case kCLAuthorizationStatusNotDetermined: return @"not_determined";
+ case kCLAuthorizationStatusRestricted: return @"denied";
+ case kCLAuthorizationStatusDenied: return @"denied";
+ case kCLAuthorizationStatusAuthorizedAlways: return @"allowed";
+ case kCLAuthorizationStatusAuthorizedWhenInUse: return @"allowed";
+ }
+ return nil;
+}
+
+- (void)requestLocationServicesPermisionWithFlutterResult:(FlutterResult)flutterResult {
+ if ([CLLocationManager locationServicesEnabled]) {
+ CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
+ if (status == kCLAuthorizationStatusNotDetermined) {
+ if (_locationFlutterResults == nil) {
+ _locationFlutterResults = [[NSMutableSet alloc] init];
+ }
+ [_locationFlutterResults addObject:flutterResult];
+
+ if (_clLocationManager == nil) {
+ _clLocationManager = [[CLLocationManager alloc] init];
+ _clLocationManager.delegate = self;
+ [_clLocationManager requestAlwaysAuthorization];
+ }
+ }
+ else {
+ flutterResult([self.class locationServicesPermisionFromAuthorizationStatus:status]);
+ }
+ }
+ else {
+ flutterResult([self.class locationServicesPermisionFromAuthorizationStatus:kCLAuthorizationStatusRestricted]);
+ }
+}
+
+#pragma mark CLLocationManagerDelegate
+
+- (void)locationManager:(CLLocationManager*)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
+
+ if (status != kCLAuthorizationStatusNotDetermined) {
+ _clLocationManager.delegate = nil;
+ _clLocationManager = nil;
+
+ NSSet *flutterResults = _locationFlutterResults;
+ _locationFlutterResults = nil;
+
+ for(FlutterResult flutterResult in flutterResults) {
+ flutterResult([self.class locationServicesPermisionFromAuthorizationStatus:status]);
+ }
+ }
+}
+
+#pragma mark Bluetooth Authorization
+
+- (void)queryBluetoothAuthorizationWithFlutterResult:(FlutterResult)flutterResult {
+ flutterResult(InaBluetoothAuthorizationStatusToString(InaBluetooth.peripheralAuthorizationStatus));
+}
+
+- (void)requestBluetoothAuthorizationWithFlutterResult:(FlutterResult)flutterResult {
+ if (InaBluetooth.peripheralAuthorizationStatus == InaBluetoothAuthorizationStatusNotDetermined) {
+ if (_bluetoothFlutterResults == nil) {
+ _bluetoothFlutterResults = [[NSMutableSet alloc] init];
+ }
+ [_bluetoothFlutterResults addObject:flutterResult];
+ if (_peripheralManager == nil) {
+ _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
+ }
+ }
+ else {
+ flutterResult(InaBluetoothAuthorizationStatusToString(InaBluetooth.peripheralAuthorizationStatus));
+ }
+}
+
+- (void)didBluetoothServicesPermision {
+ _peripheralManager.delegate = nil;
+ _peripheralManager = nil;
+
+ NSSet *flutterResults = _bluetoothFlutterResults;
+ _bluetoothFlutterResults = nil;
+
+ NSString *status = InaBluetoothAuthorizationStatusToString(InaBluetooth.peripheralAuthorizationStatus);
+ for (FlutterResult flutterResult in flutterResults) {
+ flutterResult(status);
+ }
+}
+
+#pragma mark CBPeripheralManagerDelegate
+
+- (void)peripheralManagerDidUpdateState:(CBPeripheralManager*)peripheral {
+ [self didBluetoothServicesPermision];
+}
+
+#pragma mark Deep Links
+
+- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
+ NSLog(@"UIApplication handleOpenURL: %@", url.absoluteString);
+ if ([super respondsToSelector:@selector(application:openURL:options:)]) {
+ return [super application:app openURL:url options:options];
+ }
+ else {
+ return FALSE;
+ }
+}
+
+#pragma mark PassKit
+
+- (void)addPassToWallet:(NSData*)passData result:(FlutterResult)result {
+ if (_passViewController != nil) {
+ NSLog(@"PassKit: currently adding a pass");
+ result(@(NO));
+ }
+ else if (!PKAddPassesViewController.canAddPasses) {
+ NSLog(@"PassKit: cannot add passes");
+ result(@(NO));
+ }
+ else {
+ NSError *error = nil;
+ PKPass *pass = [[PKPass alloc] initWithData:passData error:&error];
+ if ((pass != nil) && (error == nil)) {
+ PKAddPassesViewController *passViewController = [[PKAddPassesViewController alloc] initWithPass:pass];
+ if (passViewController != nil) {
+ __weak typeof(self) weakSelf = self;
+ passViewController.delegate = self;
+ [_navigationViewController.topViewController presentViewController:passViewController animated:YES completion:^{
+ weakSelf.passFlutterResult = result;
+ weakSelf.passViewController = passViewController;
+ }];
+ }
+ else {
+ NSLog(@"PassKit: failed to create add pass controller");
+ result(@(NO));
+ }
+ }
+ else {
+ NSLog(@"PassKit: failed to create pass: %@", error.localizedDescription);
+ result(@(NO));
+ }
+ }
+}
+
+#pragma mark MicroBlink
+
+- (void)microBlinkScanWithParameters:(NSDictionary*)parameters result:(FlutterResult)result {
+ if (_blinkFlutterResult != nil) {
+ NSLog(@"BlinkID: currently scanning");
+ result(nil);
+ }
+ else {
+ if (!_blinkSDKInitialized) {
+ NSString *microBlinkLicenseKey = [_keys uiucConfigStringForPathKey:@"microblink.blink_id.license_key.ios"];
+ if (0 < microBlinkLicenseKey.length) {
+ @try {
+ [MBMicroblinkSDK.sharedInstance setLicenseKey:microBlinkLicenseKey];
+ _blinkSDKInitialized = YES;
+ }
+ @catch(NSException *e) { NSLog(@"%@", e); }
+ }
+ }
+
+ if (_blinkSDKInitialized) {
+ _blinkFlutterResult = result;
+ [self invokeBlinkScanWithParameters:parameters];
+ }
+ else {
+ NSLog(@"BlinkID: not initialized");
+ result(nil);
+ }
+ }
+}
+
+- (void)invokeBlinkScanWithParameters:(NSDictionary*)parameters {
+ NSMutableArray *recognizers = [[NSMutableArray alloc] init];
+ NSArray *recognizersParam = [parameters inaArrayForKey:@"recognizers" defaults:@[@"combined", @"passport"]];
+ for (NSString *recognizer in recognizersParam) {
+ if ([recognizer isEqualToString:@"combined"]) {
+ _blinkCombinedRecognizer = [[MBBlinkIdCombinedRecognizer alloc] init];
+ _blinkCombinedRecognizer.encodeFaceImage = TRUE;
+ [recognizers addObject:_blinkCombinedRecognizer];
+ }
+ else if ([recognizer isEqualToString:@"passport"]) {
+ _blinkPassportRecognizer = [[MBPassportRecognizer alloc] init];
+ _blinkPassportRecognizer.encodeFaceImage = TRUE;
+ [recognizers addObject:_blinkPassportRecognizer];
+ }
+ }
+
+ MBBlinkIdOverlaySettings* settings = [[MBBlinkIdOverlaySettings alloc] init];
+ MBRecognizerCollection *recognizerCollection = [[MBRecognizerCollection alloc] initWithRecognizers:recognizers];
+ MBBlinkIdOverlayViewController *overlayViewController = [[MBBlinkIdOverlayViewController alloc] initWithSettings:settings recognizerCollection:recognizerCollection delegate:self];
+ _blinkRecognizerRunnerViewController = [MBViewControllerFactory recognizerRunnerViewControllerWithOverlayViewController:overlayViewController];
+
+
+ [_navigationViewController.topViewController presentViewController:_blinkRecognizerRunnerViewController animated:YES completion:nil];
+}
+
+- (void)didMicroBlinkScanWithResult:(MBRecognizerResult*)result {
+
+ __weak typeof(self) weakSelf = self;
+ [_blinkRecognizerRunnerViewController dismissViewControllerAnimated:YES completion:^{
+
+ FlutterResult flutterResult = weakSelf.blinkFlutterResult;
+
+ weakSelf.blinkFlutterResult = nil;
+ weakSelf.blinkCombinedRecognizer = nil;
+ weakSelf.blinkPassportRecognizer = nil;
+ weakSelf.blinkRecognizerRunnerViewController = nil;
+
+ NSDictionary *scanResult = nil;
+ if ([result isKindOfClass:[MBBlinkIdCombinedRecognizerResult class]]) {
+ scanResult = [self scanResultFromBlinkCombinedResult:(MBBlinkIdCombinedRecognizerResult*)result];
+ }
+ else if ([result isKindOfClass:[MBPassportRecognizerResult class]]) {
+ scanResult = [self scanResultFromBlinkPassportResult:(MBPassportRecognizerResult*)result];
+ }
+
+ if (flutterResult != nil) {
+ flutterResult(scanResult);
+ }
+ }];
+}
+
+- (NSDictionary*)scanResultFromBlinkCombinedResult:(MBBlinkIdCombinedRecognizerResult*)result {
+ NSString *base64FaceImage = [result.encodedFaceImage base64EncodedStringWithOptions:0];
+
+ return (result != nil) ? @{
+ @"firstName": result.firstName ?: [NSNull null],
+ @"lastName": result.lastName ?: [NSNull null],
+ @"fullName": result.fullName ?: [NSNull null],
+ @"sex": result.sex ?: [NSNull null],
+ @"address": result.address ?: [NSNull null],
+
+ @"dateOfBirth": [self scanStringBlinkDate:result.dateOfBirth] ?: [NSNull null],
+ @"dateOfExpiry": [self scanStringBlinkDate:result.dateOfExpiry] ?: [NSNull null],
+ @"dateOfIssue": [self scanStringBlinkDate:result.dateOfIssue] ?: [NSNull null],
+
+ @"documentNumber": result.documentNumber ?: [NSNull null],
+
+ @"placeOfBirth": result.placeOfBirth ?: [NSNull null],
+ @"nationality": result.nationality ?: [NSNull null],
+ @"race": result.race ?: [NSNull null],
+ @"religion": result.religion ?: [NSNull null],
+ @"profession": result.profession ?: [NSNull null],
+ @"maritalStatus": result.maritalStatus ?: [NSNull null],
+ @"residentialStatus": result.residentialStatus ?: [NSNull null],
+ @"employer": result.employer ?: [NSNull null],
+ @"personalIdNumber": result.personalIdNumber ?: [NSNull null],
+ @"documentAdditionalNumber": result.documentAdditionalNumber ?: [NSNull null],
+ @"issuingAuthority": result.issuingAuthority ?: [NSNull null],
+
+ @"mrz" : [self scanResultFromBlinkMrzResult:result.mrzResult] ?: [NSNull null],
+ @"driverLicenseDetailedInfo": [self scanResultFromBlinkDriverLicenseDetailedInfo:result.driverLicenseDetailedInfo] ?: [NSNull null],
+
+ @"base64FaceImage": base64FaceImage ?: [NSNull null],
+ } : nil;
+}
+
+- (NSDictionary*)scanResultFromBlinkPassportResult:(MBPassportRecognizerResult*)result {
+ NSString *base64FaceImage = [result.encodedFaceImage base64EncodedStringWithOptions:0];
+
+ return (result != nil) ? @{
+ @"firstName": [NSNull null],
+ @"lastName": [NSNull null],
+ @"fullName": [NSNull null],
+ @"sex": [NSNull null],
+ @"address": [NSNull null],
+
+ @"dateOfBirth": [NSNull null],
+ @"dateOfExpiry": [NSNull null],
+ @"dateOfIssue": [NSNull null],
+
+ @"documentNumber": [NSNull null],
+
+ @"placeOfBirth": [NSNull null],
+ @"nationality": [NSNull null],
+ @"race": [NSNull null],
+ @"religion": [NSNull null],
+ @"profession": [NSNull null],
+ @"maritalStatus": [NSNull null],
+ @"residentialStatus": [NSNull null],
+ @"employer": [NSNull null],
+ @"personalIdNumber": [NSNull null],
+ @"documentAdditionalNumber": [NSNull null],
+ @"issuingAuthority": [NSNull null],
+
+ @"mrz" : [self scanResultFromBlinkMrzResult:result.mrzResult] ?: [NSNull null],
+ @"driverLicenseDetailedInfo": [NSNull null],
+
+ @"base64FaceImage": base64FaceImage ?: [NSNull null],
+ } : nil;
+}
+
+- (NSDictionary*)scanResultFromBlinkMrzResult:(MBMrzResult*)result {
+ return (result != nil) ? @{
+ @"primaryID" : result.primaryID ?: [NSNull null],
+ @"secondaryID" : result.secondaryID ?: [NSNull null],
+ @"issuer" : result.issuer ?: [NSNull null],
+ @"issuerName" : result.issuerName ?: [NSNull null],
+ @"dateOfBirth": [self scanStringBlinkDate:result.dateOfBirth] ?: [NSNull null],
+ @"dateOfExpiry": [self scanStringBlinkDate:result.dateOfExpiry] ?: [NSNull null],
+ @"documentNumber" : result.documentNumber ?: [NSNull null],
+ @"nationality" : result.nationality ?: [NSNull null],
+ @"nationalityName" : result.nationalityName ?: [NSNull null],
+ @"gender" : result.gender ?: [NSNull null],
+ @"documentCode" : result.documentCode ?: [NSNull null],
+ @"alienNumber" : result.alienNumber ?: [NSNull null],
+ @"applicationReceiptNumber" : result.applicationReceiptNumber ?: [NSNull null],
+ @"immigrantCaseNumber" : result.immigrantCaseNumber ?: [NSNull null],
+
+ @"opt1" : result.opt1 ?: [NSNull null],
+ @"opt2" : result.opt2 ?: [NSNull null],
+ @"mrzText" : result.mrzText ?: [NSNull null],
+
+ @"sanitizedOpt1" : result.sanitizedOpt1 ?: [NSNull null],
+ @"sanitizedOpt2" : result.sanitizedOpt2 ?: [NSNull null],
+ @"sanitizedNationality" : result.sanitizedNationality ?: [NSNull null],
+ @"sanitizedIssuer" : result.sanitizedIssuer ?: [NSNull null],
+ @"sanitizedDocumentCode" : result.sanitizedDocumentCode ?: [NSNull null],
+ @"sanitizedDocumentNumber" : result.sanitizedDocumentNumber ?: [NSNull null],
+ } : nil;
+}
+
+- (NSDictionary*)scanResultFromBlinkDriverLicenseDetailedInfo:(MBDriverLicenseDetailedInfo*)info {
+ return (info != nil) ? @{
+ @"restrictions" : info.restrictions ?: [NSNull null],
+ @"endorsements" : info.endorsements ?: [NSNull null],
+ @"vehicleClass" : info.vehicleClass ?: [NSNull null],
+ } : nil;
+}
+
+- (NSString*)scanStringBlinkDate:(MBDateResult*)blinkDate {
+ return (blinkDate != nil) ? [NSString stringWithFormat:@"%02lu/%02lu/%04lu", blinkDate.month, blinkDate.day, blinkDate.year] : nil;
+}
+
+#pragma mark Device UUID
+
+- (NSUUID*)deviceUUID {
+ static const NSString *deviceUUID = @"deviceUUID";
+
+ NSDictionary *spec = @{
+ (id)kSecClass: (id)kSecClassGenericPassword,
+ (id)kSecAttrAccount: deviceUUID,
+ (id)kSecAttrGeneric: deviceUUID,
+ (id)kSecAttrService: NSBundle.mainBundle.bundleIdentifier,
+ };
+
+ NSMutableDictionary *searchRequest = [NSMutableDictionary dictionaryWithDictionary:spec];
+ [searchRequest setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
+ [searchRequest setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
+ [searchRequest setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
+
+ CFDictionaryRef response = NULL;
+ OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchRequest, (CFTypeRef*)&response);
+ if (status == errSecInteractionNotAllowed) {
+ // Could not access data. Error: errSecInteractionNotAllowed
+ return nil;
+ }
+ else if (status == 0) {
+ NSDictionary *attribs = CFBridgingRelease(response);
+ NSData *data = [attribs objectForKey:(id)kSecValueData];
+ NSString *security = [attribs objectForKey:(id)kSecAttrAccessible];
+
+ // If not always accessible then update it to be so
+ if (![security isEqualToString:(id)kSecAttrAccessibleAlways]) {
+ NSDictionary *update = @{
+ (id)kSecAttrAccessible:(id)kSecAttrAccessibleAlways,
+ (id)kSecValueData:data ?: [[NSData alloc] init],
+ };
+
+ SecItemUpdate((CFDictionaryRef)spec, (CFDictionaryRef)update);
+ }
+
+ if (data.length == sizeof(uuid_t)) {
+ return [[NSUUID alloc] initWithUUIDBytes:data.bytes];
+ }
+ else {
+ // update entry bellow
+ }
+ }
+
+ uuid_t uuidData;
+ int rndStatus = SecRandomCopyBytes(kSecRandomDefault, sizeof(uuidData), uuidData);
+ if (rndStatus == errSecSuccess) {
+ if (status == 0) {
+ // update existing entry
+ NSDictionary *update = @{
+ (id)kSecAttrAccessible:(id)kSecAttrAccessibleAlways,
+ (id)kSecValueData:[NSData dataWithBytes:uuidData length:sizeof(uuidData)]
+ };
+ status = SecItemUpdate((CFDictionaryRef)spec, (CFDictionaryRef)update);
+ }
+ else {
+ // create new entry
+ NSMutableDictionary *createRequest = [NSMutableDictionary dictionaryWithDictionary:spec];
+ [createRequest setObject:[NSData dataWithBytes:uuidData length:sizeof(uuidData)] forKey:(id)kSecValueData];
+ [createRequest setObject:(id)kSecAttrAccessibleAlways forKey:(id)kSecAttrAccessible];
+ status = SecItemAdd((CFDictionaryRef)createRequest, NULL);
+ }
+ return (status == 0) ? [[NSUUID alloc] initWithUUIDBytes:uuidData] : nil;
+ }
+ else {
+ return nil;
+ }
+}
+
+#pragma mark Permanent Value
+
+- (id)healthRSAPrivateKeyWithParameters:(NSDictionary*)parameters {
+ static const NSString *healthRSAPrivateKey = @"healthRSAPrivateKey";
+
+ NSString *userId = [parameters inaStringForKey:@"userId"];
+ if (userId == nil) {
+ return nil;
+ }
+
+ NSDictionary *spec = @{
+ (id)kSecClass: (id)kSecClassGenericPassword,
+ (id)kSecAttrAccount: userId,
+ (id)kSecAttrGeneric: healthRSAPrivateKey,
+ (id)kSecAttrService: NSBundle.mainBundle.bundleIdentifier,
+ };
+
+ NSMutableDictionary *searchRequest = [NSMutableDictionary dictionaryWithDictionary:spec];
+ [searchRequest setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
+ [searchRequest setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
+ [searchRequest setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
+
+ CFDictionaryRef response = NULL;
+ OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchRequest, (CFTypeRef*)&response);
+ NSString *existingValue = nil;
+
+ if (status == errSecInteractionNotAllowed) {
+ // Could not access data. Error: errSecInteractionNotAllowed
+ return nil;
+ }
+ else if (status == 0) {
+ NSDictionary *attribs = CFBridgingRelease(response);
+ NSData *data = [attribs objectForKey:(id)kSecValueData];
+ NSString *security = [attribs objectForKey:(id)kSecAttrAccessible];
+
+ // If not always accessible then update it to be so
+ if (![security isEqualToString:(id)kSecAttrAccessibleAlways]) {
+ NSDictionary *update = @{
+ (id)kSecAttrAccessible:(id)kSecAttrAccessibleAlways,
+ (id)kSecValueData:data ?: [[NSData alloc] init],
+ };
+
+ SecItemUpdate((CFDictionaryRef)spec, (CFDictionaryRef)update);
+ }
+
+ existingValue = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ }
+
+ if([parameters inaBoolForKey:@"remove"]){ // remove
+ status = SecItemDelete((CFDictionaryRef)spec);
+ return [NSNumber numberWithBool:(status == 0)];
+ }
+ else if ([parameters objectForKey:@"value"] == nil) { // getter
+ return existingValue;
+ }
+ else { // setter
+
+ NSString *value = [parameters inaStringForKey:@"value"];
+ if (value != nil) {
+ NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
+
+ if (status == 0) {
+ // update existing entry
+ NSDictionary *update = @{
+ (id)kSecAttrAccessible:(id)kSecAttrAccessibleAlways,
+ (id)kSecValueData:valueData
+ };
+ status = SecItemUpdate((CFDictionaryRef)spec, (CFDictionaryRef)update);
+ }
+ else {
+ // create new entry
+ NSMutableDictionary *createRequest = [NSMutableDictionary dictionaryWithDictionary:spec];
+ [createRequest setObject:valueData forKey:(id)kSecValueData];
+ [createRequest setObject:(id)kSecAttrAccessibleAlways forKey:(id)kSecAttrAccessible];
+ status = SecItemAdd((CFDictionaryRef)createRequest, NULL);
+ }
+
+ return [NSNumber numberWithBool:(status == 0)];
+ }
+ else {
+ if (status == 0) {
+ // delete existing entry
+ status = SecItemDelete((CFDictionaryRef)spec);
+ return [NSNumber numberWithBool:(status == 0)];
+ }
+ else {
+ // nothing to do
+ return [NSNumber numberWithBool:YES];
+ }
+ }
+ }
+}
+
+
+
+#pragma mark PKAddPassesViewControllerDelegate
+
+- (void)addPassesViewControllerDidFinish:(PKAddPassesViewController *)controller {
+ if (controller == _passViewController) {
+ __weak typeof(self) weakSelf = self;
+ [controller dismissViewControllerAnimated:YES completion:^{
+ FlutterResult result = weakSelf.passFlutterResult;
+ weakSelf.passFlutterResult = nil;
+ weakSelf.passViewController = nil;
+ if (result != nil) {
+ result(@(YES));
+ }
+ }];
+ }
+}
+
+#pragma mark MBBlinkIdOverlayViewControllerDelegate
+
+- (void)blinkIdOverlayViewControllerDidFinishScanning:(MBBlinkIdOverlayViewController *)blinkIdOverlayViewController state:(MBRecognizerResultState)state {
+
+ if (state == MBRecognizerResultStateValid) {
+ MBRecognizerResult *result = nil;
+ if ((_blinkCombinedRecognizer.result != nil) && (_blinkCombinedRecognizer.result.resultState == MBRecognizerResultStateValid)) {
+ result = _blinkCombinedRecognizer.result;
+ }
+ else if ((_blinkPassportRecognizer.result != nil) && (_blinkPassportRecognizer.result.resultState == MBRecognizerResultStateValid)) {
+ result = _blinkPassportRecognizer.result;
+ }
+
+ if (result != nil) {
+ [blinkIdOverlayViewController.recognizerRunnerViewController pauseScanning];
+
+ __weak typeof(self) weakSelf = self;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [weakSelf didMicroBlinkScanWithResult:result];
+ });
+ }
+ }
+}
+
+- (void)blinkIdOverlayViewControllerDidTapClose:(nonnull MBBlinkIdOverlayViewController *)blinkIdOverlayViewController {
+
+ __weak typeof(self) weakSelf = self;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [weakSelf didMicroBlinkScanWithResult:nil];
+ });
+}
+
+
+#pragma mark UINavigationControllerDelegate
+
+- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
+ UIViewController *rootViewController = navigationController.viewControllers.firstObject;
+ BOOL navigationBarHidden = (viewController == rootViewController);
+ if (navigationController.navigationBarHidden != navigationBarHidden) {
+ [navigationController setNavigationBarHidden:navigationBarHidden animated:YES];
+ }
+}
+
+- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
+
+}
+
+- (id)navigationController:(UINavigationController *)navigationController
+ animationControllerForOperation:(UINavigationControllerOperation)operation
+ fromViewController:(UIViewController *)fromVC
+ toViewController:(UIViewController *)toVC
+{
+ if ([fromVC conformsToProtocol:@protocol(FlutterCompletionHandler)] && [toVC isKindOfClass:[FlutterViewController class]]) {
+ id directionsVC = (id)fromVC;
+ if (directionsVC.completionHandler != nil) {
+ directionsVC.completionHandler(nil);
+ directionsVC.completionHandler = nil;
+ }
+ }
+
+ return nil;
+}
+
+
+#pragma mark UNUserNotificationCenterDelegate
+
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
+ NSDictionary *userInfo = notification.request.content.userInfo;
+ NSData *userInfoData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:NULL];
+ NSString *userInfoString = [[NSString alloc] initWithData:userInfoData encoding:NSUTF8StringEncoding];
+ NSLog(@"UIApplication: UNUserNotificationCenter willPresentNotification:\n%@", userInfoString);
+
+ completionHandler(UNNotificationPresentationOptionAlert|UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionSound);
+}
+
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
+ NSDictionary *userInfo = response.notification.request.content.userInfo;
+ NSData *userInfoData = [NSJSONSerialization dataWithJSONObject:userInfo options:0 error:NULL];
+ NSString *userInfoString = [[NSString alloc] initWithData:userInfoData encoding:NSUTF8StringEncoding];
+ NSLog(@"UIApplication: UNUserNotificationCenter didReceiveNotificationResponse (%@):\n%@", response.actionIdentifier, userInfoString);
+
+ if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
+ // The user dismissed the notification without taking action.
+ }
+ else if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) {
+ // The user launched the app.
+ [self processPushNotification:response.notification.request.content.userInfo];
+ }
+
+ completionHandler();
+}
+
+#pragma mark FIRMessagingDelegate
+
+- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken {
+ NSLog(@"UIApplication: FIRMessaging: didReceiveRegistrationToken: %@", fcmToken);
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:fcmToken forKey:@"token"];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"FCMToken" object:nil userInfo:userInfo];
+}
+
+#pragma mark NSNotificationCenter
+
+- (void)didReceiveFCMTokenNotification:(NSNotification *)notification {
+ NSString *fcmToken = [notification.object isKindOfClass:[NSString class]] ? notification.object : nil;
+ NSLog(@"UIApplication: didReceiveFCMTokenNotification: %@", fcmToken);
+}
+
+@end
+
+//////////////////////////////////////
+// RootNavigationController
+
+@implementation RootNavigationController
+
+- (BOOL)shouldAutorotate {
+ return true;
+}
+
+- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
+ UIInterfaceOrientationMask result = 0;
+ for (NSNumber *orientation in AppDelegate.sharedInstance.supportedInterfaceOrientations) {
+ result |= _interfaceOrientationToMask(orientation.integerValue);
+ }
+
+ return result;
+}
+
+- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
+ return AppDelegate.sharedInstance.preferredInterfaceOrientation;
+}
+
+@end
+
+//////////////////////////////////////
+// UIInterfaceOrientation
+
+@interface LaunchScreenView()
+@property (nonatomic) UIImageView *imageView;
+@property (nonatomic) UIActivityIndicatorView *activityView;
+@end
+
+@implementation LaunchScreenView
+
+- (id)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ _imageView = [[UIImageView alloc] initWithFrame:frame];
+ _imageView.image = [UIImage imageNamed:@"LaunchImage"];
+ [self addSubview:_imageView];
+
+ _activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
+ [self addSubview:_activityView];
+
+ [_activityView startAnimating];
+ }
+ return self;
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+
+ CGSize contentSize = self.frame.size;
+
+ CGSize imageSize = InaSizeScaleToFill(_imageView.image.size, contentSize);
+ _imageView.frame = CGRectMake((contentSize.width - imageSize.width) / 2, (contentSize.height - imageSize.height) / 2, imageSize.width, imageSize.height);
+
+ CGSize activitySize = [_activityView sizeThatFits:contentSize];
+ _activityView.frame = CGRectMake((contentSize.width - activitySize.width) / 2, 7 * (contentSize.height - activitySize.height) / 10, activitySize.width, activitySize.height);
+
+
+}
+
+@end
+
+
+//////////////////////////////////////
+// UIInterfaceOrientation
+
+UIInterfaceOrientation _interfaceOrientationFromString(NSString *value) {
+ if ([value isEqualToString:@"portraitUp"]) {
+ return UIInterfaceOrientationPortrait;
+ }
+ else if ([value isEqualToString:@"portraitDown"]) {
+ return UIInterfaceOrientationPortraitUpsideDown;
+ }
+ else if ([value isEqualToString:@"landscapeLeft"]) {
+ return UIInterfaceOrientationLandscapeLeft;
+ }
+ else if ([value isEqualToString:@"landscapeRight"]) {
+ return UIInterfaceOrientationLandscapeRight;
+ }
+ else {
+ return UIInterfaceOrientationUnknown;
+ }
+}
+
+NSString* _interfaceOrientationToString(UIInterfaceOrientation value) {
+ switch (value) {
+ case UIInterfaceOrientationPortrait: return @"portraitUp";
+ case UIInterfaceOrientationPortraitUpsideDown: return @"portraitDown";
+ case UIInterfaceOrientationLandscapeLeft: return @"landscapeLeft";
+ case UIInterfaceOrientationLandscapeRight: return @"landscapeRight";
+ default: return nil;
+ }
+}
+
+UIInterfaceOrientation _interfaceOrientationFromMask(UIInterfaceOrientationMask value) {
+ switch (value) {
+ case UIInterfaceOrientationMaskPortrait: return UIInterfaceOrientationPortrait;
+ case UIInterfaceOrientationMaskPortraitUpsideDown: return UIInterfaceOrientationPortraitUpsideDown;
+ case UIInterfaceOrientationMaskLandscapeLeft: return UIInterfaceOrientationLandscapeLeft;
+ case UIInterfaceOrientationMaskLandscapeRight: return UIInterfaceOrientationLandscapeRight;
+ default: return UIInterfaceOrientationUnknown;
+ }
+}
+
+UIInterfaceOrientationMask _interfaceOrientationToMask(UIInterfaceOrientation value) {
+ switch (value) {
+ case UIInterfaceOrientationPortrait: return UIInterfaceOrientationMaskPortrait;
+ case UIInterfaceOrientationPortraitUpsideDown: return UIInterfaceOrientationMaskPortraitUpsideDown;
+ case UIInterfaceOrientationLandscapeLeft: return UIInterfaceOrientationMaskLandscapeLeft;
+ case UIInterfaceOrientationLandscapeRight: return UIInterfaceOrientationMaskLandscapeRight;
+ default: return 0;
+ }
+}
+
diff --git a/ios/Runner/AppKeys.h b/ios/Runner/AppKeys.h
new file mode 100644
index 00000000..e6b8dcb9
--- /dev/null
+++ b/ios/Runner/AppKeys.h
@@ -0,0 +1,31 @@
+//
+// AppKeys.h
+// Runner
+//
+// Created by Mihail Varbanov on 4/25/19.
+// Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import
+#import
+
+extern NSString * const kFlutterMetodChannelName;
+
+extern CLLocationCoordinate2D const kInitialCameraLocation;
+extern float const kInitialCameraZoom;
+extern float const kMarkerThresold1Zoom;
+extern float const kMarkerThresold2Zoom;
+
+extern double const kExploreLocationThresoldDistance;
diff --git a/ios/Runner/AppKeys.m b/ios/Runner/AppKeys.m
new file mode 100644
index 00000000..eb8f2457
--- /dev/null
+++ b/ios/Runner/AppKeys.m
@@ -0,0 +1,35 @@
+//
+// AppKeys.m
+// Runner
+//
+// Created by Mihail Varbanov on 4/25/19.
+// Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "AppKeys.h"
+
+NSString * const kFlutterMetodChannelName = @"edu.illinois.covid/core";
+
+// --------------------------------------------
+
+// Camera: Campus Center
+CLLocationCoordinate2D const kInitialCameraLocation = { 40.102116, -88.227129 };
+float const kInitialCameraZoom = 17;
+float const kMarkerThresold1Zoom = 16.0;
+float const kMarkerThresold2Zoom = 16.89f;
+
+// --------------------------------------------
+
+double const kExploreLocationThresoldDistance = 200.0; // in meters
diff --git a/ios/Runner/ExposurePlugin.h b/ios/Runner/ExposurePlugin.h
new file mode 100644
index 00000000..b76480a7
--- /dev/null
+++ b/ios/Runner/ExposurePlugin.h
@@ -0,0 +1,28 @@
+//
+// ExposurePlugin.h
+// Runner
+//
+// Created by Mihail Varbanov on 5/21/20.
+// Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import
+#import
+
+
+@interface ExposurePlugin : NSObject
+
+@end
+
diff --git a/ios/Runner/ExposurePlugin.m b/ios/Runner/ExposurePlugin.m
new file mode 100644
index 00000000..e8fdfa35
--- /dev/null
+++ b/ios/Runner/ExposurePlugin.m
@@ -0,0 +1,1425 @@
+//
+// ExposurePlugin.m
+// Runner
+//
+// Created by Mihail Varbanov on 5/21/20.
+// Copyright 2020 Board of Trustees of the University of Illinois.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+
+// http://www.apache.org/licenses/LICENSE-2.0
+
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import "ExposurePlugin.h"
+#import
+#import
+#import
+#import
+
+#import "Bluetooth+InaUtils.h"
+#import "NSDictionary+InaTypedValue.h"
+#import "CommonCrypto+UIUCUtils.h"
+#import "Security+UIUCUtils.h"
+
+static NSString* const kMethodChanelName = @"edu.illinois.covid/exposure";
+static NSString* const kServiceUuid = @"CD19";
+static NSString* const kCharacteristicUuid = @"1f5bb1de-cdf0-4424-9d43-d8cc81a7f207";
+static NSString* const kRangingBeaconUuid = @"c965bc2c-5f28-4854-b046-7e68e0e60074";
+static NSString* const kLocalNotificationId = @"exposureNotification";
+static NSString* const kExposureTEK1 = @"exposureTEKs";
+static NSString* const kExposureTEK2 = @"exposureTEK2s";
+
+static NSString* const kStartMethodName = @"start";
+static NSString* const kStopMethodName = @"stop";
+static NSString* const kTEKsMethodName = @"TEKs";
+static NSString* const kTekRPIsMethodName = @"tekRPIs";
+static NSString* const kExpireTEKMethodName = @"expireTEK";
+static NSString* const kRPILogMethodName = @"exposureRPILog";
+static NSString* const kRSSILogMethodName = @"exposureRSSILog";
+static NSString* const kSettingsParamName = @"settings";
+static NSString* const kTEKParamName = @"tek";
+static NSString* const kTimestampParamName = @"timestamp";
+
+static NSString* const kTEKNotificationName = @"tek";
+static NSString* const kTEKTimestampParamName = @"timestamp";
+static NSString* const kTEKExpirestampParamName = @"expirestamp";
+static NSString* const kTEKValueParamName = @"tek";
+
+static NSString* const kExposureNotificationName = @"exposure";
+static NSString* const kExposureThickNotificationName = @"exposureThick";
+static NSString* const kExposureTimestampParamName = @"timestamp";
+static NSString* const kExposureRPIParamName = @"rpi";
+static NSString* const kExposureDurationParamName = @"duration";
+static NSString* const kExposureRSSIParamName = @"rssi";
+
+static NSString* const kExposureTimeoutIntervalSettingName = @"covid19ExposureServiceTimeoutInterval";
+static NSString* const kExposurePingIntervalSettingName = @"covid19ExposureServicePingInterval";
+static NSString* const kExposureProcessIntervalSettingName = @"covid19ExposureServiceProcessInterval";
+static NSString* const kLocalNotificationIntervalSettingName = @"covid19ExposureServiceLocalNotificationInterval";
+static NSString* const kExposureMinDurationSettingName = @"covid19ExposureServiceLogMinDuration";
+static NSString* const kExposureMinRssiSettingName = @"covid19ExposureServiceMinRSSI";
+
+static NSInteger const kRPIRefreshInterval = (10 * 60); // 10 mins
+static NSInteger const kTEKRollingPeriod = (24 * 60 * 60) / kRPIRefreshInterval; // = 144 (kRPIRefreshInterval * kTEKRollingPeriod = 24 hours)
+
+static NSTimeInterval const kExposureTimeoutInterval = 300; // 5 minutes
+static NSTimeInterval const kExposurePingInterval = 60; // 1 minute
+static NSTimeInterval const kExposureProcessInterval = 10; // 10 sec
+static NSTimeInterval const kLocalNotificationInterval = 60; // 1 minute
+static NSTimeInterval const kExposureNotifyTickInterval = 1; // 1 sec
+static NSTimeInterval const kExposureMinDuration = 120; // 2 minutes
+
+static int const kNoRssi = 127;
+static int const kMinRssi = -50;
+
+////////////////////////////////////
+// ExposureRecord
+
+@interface ExposureRecord : NSObject
+@property (nonatomic, readonly) NSInteger timestampCreated;
+@property (nonatomic, readonly) NSTimeInterval timeUpdated;
+@property (nonatomic, readonly) NSInteger duration;
+@property (nonatomic, readonly) NSTimeInterval durationInterval;
+@property (nonatomic, readonly) int rssi;
+
+- (instancetype)initWithTimestamp:(NSTimeInterval)timestamp rssi:(int)rssi;
+- (void)updateTimestamp:(NSTimeInterval)timestamp rssi:(int)rssi;
+@end
+
+////////////////////////////////////
+// TEKRecord
+
+@interface TEKRecord : NSObject
+@property (nonatomic) NSData* tek;
+@property (nonatomic) int expire;
+
+- (instancetype)initWithTEK:(NSData*)tek expire:(int)expire;
+
++ (instancetype)fromJson:(NSDictionary*)json;
+- (NSDictionary*)toJson;
+@end
+
+////////////////////////////////////
+// ExposurePlugin
+
+@interface ExposurePlugin() {
+
+ FlutterMethodChannel* _methodChannel;
+ FlutterResult _startResult;
+
+ NSData* _rpi;
+ NSTimer* _rpiTimer;
+ NSMutableDictionary* _teks;
+
+ CBPeripheralManager* _peripheralManager;
+ CBMutableService* _peripheralService;
+ CBMutableCharacteristic* _peripheralCharacteristic;
+
+ CBCentralManager* _centralManager;
+
+ NSMutableDictionary* _peripherals;
+ NSMutableDictionary* _peripheralRPIs;
+ NSMutableDictionary* _iosExposures;
+ NSMutableDictionary* _androidExposures;
+
+ NSTimer* _exposuresTimer;
+ NSTimeInterval _lastNotifyExposireThickTime;
+
+ CLLocationManager* _locationManager;
+ CLBeaconRegion* _beaconRegion;
+ bool _monitoringLocation;
+
+ NSTimeInterval _lastLocalNotificationTime;
+
+ NSTimeInterval _exposureTimeoutInterval;
+ NSTimeInterval _exposurePingInterval;
+ NSTimeInterval _exposureProcessInterval;
+ NSTimeInterval _localNotificationInterval;
+ NSTimeInterval _exposureMinDuration;
+ int _exposureMinRssi;
+}
+@property (nonatomic, readonly) int exposureMinRssi;
+@property (nonatomic) UIBackgroundTaskIdentifier bgTaskId;
+@end
+
+@implementation ExposurePlugin
+
+static ExposurePlugin *g_Instance = nil;
+
++ (void)registerWithRegistrar:(NSObject *)registrar {
+ FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:kMethodChanelName binaryMessenger:registrar.messenger];
+ ExposurePlugin *instance = [[ExposurePlugin alloc] initWithMethodChannel:channel];
+ [registrar addMethodCallDelegate:instance channel:channel];
+}
+
+- (instancetype)init {
+ if (self = [super init]) {
+ if (g_Instance == nil) {
+ g_Instance = self;
+ }
+ _peripherals = [[NSMutableDictionary alloc] init];
+ _peripheralRPIs = [[NSMutableDictionary alloc] init];
+ _iosExposures = [[NSMutableDictionary alloc] init];
+ _androidExposures = [[NSMutableDictionary alloc] init];
+ _teks = [self.class loadTEK2sFromStorage];
+ _bgTaskId = UIBackgroundTaskInvalid;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if (g_Instance == self) {
+ g_Instance = nil;
+ }
+}
+
+- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)channel {
+ if (self = [self init]) {
+ _methodChannel = channel;
+ }
+ return self;
+}
+
++ (instancetype)sharedInstance {
+ return g_Instance;
+}
+
+#pragma mark MethodCall
+
+- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
+ NSDictionary *parameters = [call.arguments isKindOfClass:[NSDictionary class]] ? call.arguments : nil;
+ if ([call.method isEqualToString:kStartMethodName]) {
+ NSDictionary *settings = [parameters inaDictForKey:kSettingsParamName];
+ [self startWithSettings:settings flutterResult:result];
+ }
+ else if ([call.method isEqualToString:kStopMethodName]) {
+ [self stop];
+ result([NSNumber numberWithBool:YES]);
+ }
+ else if ([call.method isEqualToString:kTEKsMethodName]) {
+ bool remove = [parameters inaBoolForKey:@"remove"];
+ if (remove) {
+ [self.class saveTEK2sToStorage:nil];
+ result(nil);
+ }
+ else {
+ result(self.teksList);
+ }
+ }
+ else if ([call.method isEqualToString:kTekRPIsMethodName]) {
+ NSString *tekString = [parameters inaStringForKey:kTEKParamName];
+ NSData *tek = [[NSData alloc] initWithBase64EncodedString:tekString options:0];
+ NSInteger timestamp = [parameters inaIntegerForKey:kTimestampParamName];
+ NSInteger expirestamp = [parameters inaIntegerForKey:kTEKExpirestampParamName];
+ result([self rpisForTek:tek timestamp:timestamp expirestamp:expirestamp]);
+ }
+ else if ([call.method isEqualToString:kExpireTEKMethodName]) {
+ [self updateTEKExpireTime];
+ result(nil);
+ }
+
+ else {
+ result(nil);
+ }
+}
+
+#pragma mark API
+
+- (void)startWithSettings:(NSDictionary*)settings flutterResult:(FlutterResult)result {
+
+ if (self.isPeripheralAuthorized && self.isCentralAuthorized && (_peripheralManager == nil) && (_centralManager == nil) && (_startResult == nil)) {
+ NSLog(@"ExposurePlugin: Start");
+ _startResult = result;
+ [self initSettings:settings];
+ [self initRPI];
+ [self startPeripheral];
+ [self startCentral];
+ [self startLocationManager];
+ [self connectAppLiveCycleEvents];
+ }
+ else if (result != nil) {
+ result([NSNumber numberWithBool:YES]);
+ }
+}
+
+- (void)checkStarted {
+ if ((_startResult != nil) && self.isStarted) {
+ FlutterResult flutterResult = _startResult;
+ _startResult = nil;
+ flutterResult([NSNumber numberWithBool:YES]);
+ }
+}
+
+- (void)startFailed {
+ if (_startResult != nil) {
+ FlutterResult flutterResult = _startResult;
+ _startResult = nil;
+ flutterResult([NSNumber numberWithBool:NO]);
+ }
+
+}
+
+- (bool)isStarted {
+ return self.isPeripheralStarted && self.isCentralStarted;
+}
+
+- (void)stop {
+ NSLog(@"ExposurePlugin: Stop");
+ [self stopPeripheral];
+ [self stopCentral];
+ [self stopLocationManager];
+ [self clearRPI];
+ [self clearExposures];
+ [self disconnectAppLiveCycleEvents];
+}
+
+#pragma mark Peripheral
+
+- (void)startPeripheral {
+ if (self.isPeripheralAuthorized && (_peripheralManager == nil)) {
+ _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
+ }
+ else {
+ [self startFailed];
+ }
+}
+
+- (void)updatePeripheral {
+ if (self.isPeripheralInitialized && (_rpi != nil) && _peripheralManager.isAdvertising) {
+ [_peripheralManager updateValue:_rpi forCharacteristic:_peripheralCharacteristic onSubscribedCentrals:nil];
+ }
+}
+
+- (void)stopPeripheral {
+
+ if (_peripheralManager != nil) {
+ if (_peripheralManager.isAdvertising) {
+ [_peripheralManager stopAdvertising];
+ }
+
+ if (_peripheralService != nil) {
+ [_peripheralManager removeService:_peripheralService];
+ _peripheralService = nil;
+ }
+
+ _peripheralCharacteristic = nil;
+
+ _peripheralManager.delegate = nil;
+ _peripheralManager = nil;
+ }
+}
+
+- (bool)isPeripheralAuthorized {
+ return InaBluetooth.peripheralAuthorizationStatus == InaBluetoothAuthorizationStatusAuthorized;
+}
+
+- (bool)isPeripheralInitialized {
+ return (_peripheralManager != nil) && (_peripheralManager.state == CBManagerStatePoweredOn) && (_peripheralService != nil) && (_peripheralCharacteristic != nil);
+}
+
+- (bool)isPeripheralStarted {
+ return self.isPeripheralInitialized && _peripheralManager.isAdvertising;
+}
+
+#pragma mark CBPeripheralManagerDelegate
+
+- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
+ NSLog(@"ExposurePlugin: CBPeripheralManager didUpdateState: %@", @(peripheral.state));
+
+ if (_peripheralManager.state == CBManagerStatePoweredOn) {
+ CBUUID *serviceUuid = [CBUUID UUIDWithString:kServiceUuid];
+ CBMutableService *service = [[CBMutableService alloc] initWithType:serviceUuid primary:YES];
+
+ CBUUID *characteristicUuid = [CBUUID UUIDWithString:kCharacteristicUuid];
+ CBMutableCharacteristic *characteristic = [[CBMutableCharacteristic alloc] initWithType:characteristicUuid properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
+ service.characteristics = @[characteristic];
+
+ [_peripheralManager addService:service];
+ }
+ else {
+ [self startFailed];
+ }
+}
+
+- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error {
+ NSLog(@"ExposurePlugin: CBPeripheralManager didAddService");
+ _peripheralService = [service isKindOfClass:[CBMutableService class]] ? ((CBMutableService*)service) : nil;
+ _peripheralCharacteristic = [_peripheralService inaMutableCharacteristicWithUUID:[CBUUID UUIDWithString:kCharacteristicUuid]];
+
+ if (self.isPeripheralInitialized) {
+ [_peripheralManager startAdvertising:
+ @{ CBAdvertisementDataServiceUUIDsKey :@[[CBUUID UUIDWithString:kServiceUuid]],
+ }];
+ }
+ else {
+ [self startFailed];
+ }
+}
+
+- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error {
+ NSLog(@"ExposurePlugin: CBPeripheralManager peripheralManagerDidStartAdvertising");
+ if (self.isPeripheralStarted) {
+ [self checkStarted];
+ }
+ else {
+ [self startFailed];
+ }
+}
+
+- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request{
+ NSLog(@"ExposurePlugin: CBPeripheralManager didReceiveReadRequest");
+ request.value = _rpi;
+ [peripheral respondToRequest:request withResult:CBATTErrorSuccess];
+}
+
+#pragma mark Central
+
+- (void)startCentral {
+ if (self.isCentralAuthorized && (_centralManager == nil)) {
+ _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{
+ CBCentralManagerOptionShowPowerAlertKey : [NSNumber numberWithBool:NO],
+ }];
+ }
+ else {
+ [self startFailed];
+ }
+}
+
+- (void)stopCentral {
+ if (_centralManager != nil) {
+ if (_centralManager.isScanning) {
+ [_centralManager stopScan];
+ }
+
+ _centralManager.delegate = nil;
+ _centralManager = nil;
+ }
+
+ if (_exposuresTimer != nil) {
+ [_exposuresTimer invalidate];
+ _exposuresTimer = nil;
+ }
+}
+
+- (bool)isCentralAuthorized {
+ return InaBluetooth.centralAuthorizationStatus == InaBluetoothAuthorizationStatusAuthorized;
+}
+
+- (bool)isCentralInitialized {
+ return (_centralManager != nil) && (_centralManager.state == CBManagerStatePoweredOn);
+}
+
+- (bool)isCentralStarted {
+ return self.isCentralInitialized && _centralManager.isScanning;
+}
+
+- (void)disconnectPeripheralWithUuid:(NSUUID*)peripheralUuid {
+ [self _disconnectPeripheral:[_peripherals objectForKey:peripheralUuid]];
+ [self _removePeripheralWithUuid:peripheralUuid];
+}
+
+- (void)disconnectPeripheral:(CBPeripheral*)peripheral {
+ [self _disconnectPeripheral:peripheral];
+ [self _removePeripheralWithUuid:peripheral.identifier];
+}
+
+- (void)_disconnectPeripheral:(CBPeripheral*)peripheral {
+ if (peripheral != nil) {
+ peripheral.delegate = nil;
+
+ CBService *service = [peripheral inaServiceWithUUID:[CBUUID UUIDWithString:kServiceUuid]];
+ CBCharacteristic *characteristic = [service inaCharacteristicWithUUID:[CBUUID UUIDWithString:kCharacteristicUuid]];
+ if (characteristic != nil) {
+ [peripheral setNotifyValue:NO forCharacteristic:characteristic];
+ }
+
+ [_centralManager cancelPeripheralConnection:peripheral];
+ }
+}
+
+- (void)_removePeripheralWithUuid:(NSUUID*)peripheralUuid {
+ if (peripheralUuid != nil) {
+ [_peripherals removeObjectForKey:peripheralUuid];
+
+ NSData *rpi = [_peripheralRPIs objectForKey:peripheralUuid];
+ if (rpi != nil) {
+ [_peripheralRPIs removeObjectForKey:peripheralUuid];
+ }
+
+ ExposureRecord *record = [_iosExposures objectForKey:peripheralUuid];
+ if (record != nil) {
+ [_iosExposures removeObjectForKey:peripheralUuid];
+ [self updateExposuresTimer];
+ }
+
+ if ((rpi != nil) && (record != nil)) {
+ [self notifyExposure:record rpi:rpi peripheralUuid:peripheralUuid];
+ }
+ }
+}
+
+- (void)_removeAndroidRpi:(NSData*)rpi {
+ NSUUID *peripheralUuid = [self peripheralUuidForRPI:rpi];
+ [self disconnectPeripheralWithUuid:peripheralUuid];
+
+ ExposureRecord *record = [_androidExposures objectForKey:rpi];
+ if (record != nil) {
+ [_androidExposures removeObjectForKey:rpi];
+ [self updateExposuresTimer];
+ }
+
+ if ((rpi != nil) && (record != nil)) {
+ [self notifyExposure:record rpi:rpi peripheralUuid:nil];
+ }
+}
+
+- (NSUUID*)peripheralUuidForRPI:(NSData*)rpi {
+ for (NSUUID* peripheralUuid in _peripheralRPIs) {
+ NSData *peripheralRpi = [_peripheralRPIs objectForKey:peripheralUuid];
+ if ([peripheralRpi isEqualToData:rpi]) {
+ return peripheralUuid;
+ }
+ }
+ return nil;
+}
+
+#pragma mark CBCentralManagerDelegate
+
+- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
+ NSLog(@"ExposurePlugin: CBCentralManager didUpdateState: %@", @(central.state));
+ if (self.isCentralInitialized) {
+ [_centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUuid]] options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
+ [self checkStarted];
+ }
+ else {
+ [self startFailed];
+ }
+}
+
+- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
+
+ CBUUID *serviceUuid = [CBUUID UUIDWithString:kServiceUuid];
+ if ([advertisementData inaAdvertisementDataContainsServiceWithUuid:serviceUuid]) {
+
+ NSUUID *peripheralUuid = peripheral.identifier;
+ if ([_peripherals objectForKey:peripheralUuid] == nil) {
+ NSLog(@"ExposurePlugin: CBCentralManager didDiscoverPeripheral");
+ [_peripherals setObject:peripheral forKey:peripheralUuid];
+ [_centralManager connectPeripheral:peripheral options:nil];
+ }
+
+ NSDictionary *serviceData = [advertisementData inaDictForKey:CBAdvertisementDataServiceDataKey];
+ NSData *rpiData = (serviceData != nil) ? [serviceData objectForKey:serviceUuid] : nil;
+ if (rpiData != nil) { // Android
+ if ([_peripheralRPIs objectForKey:peripheralUuid] == nil) { // new record
+ NSLog(@"ExposurePlugin: New Android peripheral RPI received");
+ [_peripheralRPIs setObject:rpiData forKey:peripheralUuid];
+ }
+ else if (![rpiData isEqualToData:[_peripheralRPIs objectForKey:peripheralUuid]]) { // update existing record
+ NSLog(@"ExposurePlugin: Connected Android peripheral RPI changed");
+ NSData * rpi = [_peripheralRPIs objectForKey:peripheralUuid];
+ ExposureRecord * record = [_androidExposures objectForKey:rpi];
+ if (record != nil) {
+ [_androidExposures removeObjectForKey:rpi];
+ [self updateExposuresTimer];
+ }
+ if (rpi != nil && record != nil) {
+ [self notifyExposure:record rpi:rpi peripheralUuid:peripheralUuid];
+ }
+ [_peripheralRPIs setObject:rpiData forKey:peripheralUuid];
+ }
+ [self logAndroidExposure:rpiData rssi:RSSI.intValue];
+ }
+ else { // iOS
+ [self logiOSExposure:peripheralUuid rssi:RSSI.intValue];
+ }
+ }
+}
+
+- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(nonnull CBPeripheral *)peripheral {
+ NSLog(@"ExposurePlugin: CBCentralManager didConnectPeripheral");
+ peripheral.delegate = self;
+ [peripheral discoverServices:@[[CBUUID UUIDWithString:kServiceUuid]]];
+}
+
+- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error {
+ NSLog(@"ExposurePlugin: CBCentralManager didFailToConnectPeripheral");
+}
+
+- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
+ NSLog(@"ExposurePlugin: CBCentralManager didDisconnectPeripheral");
+ [self disconnectPeripheral:peripheral];
+}
+
+#pragma mark CBPeripheralDelegate
+
+- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error {
+ NSLog(@"ExposurePlugin: CBPeripheral didDiscoverServices");
+ if (error == nil) {
+ CBService *service = [peripheral inaServiceWithUUID:[CBUUID UUIDWithString:kServiceUuid]];
+ if (service != nil) {
+ [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:kCharacteristicUuid]] forService:service];
+ }
+ }
+}
+
+- (void)peripheral:(CBPeripheral *)peripheral didModifyServices:(NSArray *)invalidatedServices {
+ NSLog(@"ExposurePlugin: CBPeripheral didModifyServices");
+ CBUUID *serviceUuid = [CBUUID UUIDWithString:kServiceUuid];
+ for (CBService *service in invalidatedServices) {
+ if ([service.UUID isEqual:serviceUuid]) {
+ [peripheral discoverServices:@[serviceUuid]];
+ break;
+ }
+ }
+}
+
+- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(nonnull CBService *)service error:(nullable NSError *)error {
+ NSLog(@"ExposurePlugin: CBPeripheral didDiscoverCharacteristicsForService");
+ if (error == nil) {
+ CBCharacteristic *characteristic = [service inaCharacteristicWithUUID:[CBUUID UUIDWithString:kCharacteristicUuid]];
+ if (characteristic != nil) {
+ [peripheral setNotifyValue:YES forCharacteristic:characteristic];
+ [peripheral readValueForCharacteristic:characteristic];
+ }
+ }
+}
+
+- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
+ NSLog(@"ExposurePlugin: CBPeripheral didUpdateValueForCharacteristic");
+ if ((error == nil) && [characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUuid]] && (characteristic.value != nil)) {
+ NSUUID *peripheralUuid = peripheral.identifier;
+ NSData *rpi = [_peripheralRPIs objectForKey:peripheralUuid];
+ if (rpi == nil) {
+ [_peripheralRPIs setObject:characteristic.value forKey:peripheralUuid];
+ }
+ else if (![rpi isEqualToData:characteristic.value]) {
+ // update existing record
+ [_peripheralRPIs setObject:characteristic.value forKey:peripheralUuid];
+
+ NSLog(@"ExposurePlugin: Connected iOS peripheral RPI changed");
+ ExposureRecord *record = [_iosExposures objectForKey:peripheralUuid];
+ if (record != nil) {
+ [_iosExposures removeObjectForKey:peripheralUuid];
+ [self updateExposuresTimer];
+ }
+ if ((rpi != nil) && (record != nil)) {
+ [self notifyExposure:record rpi:rpi peripheralUuid:peripheralUuid];
+ }
+
+ NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970];
+ record = [[ExposureRecord alloc] initWithTimestamp:currentTimestamp rssi:record.rssi];
+ [_iosExposures setObject:record forKey:peripheralUuid];
+ [self updateExposuresTimer];
+ }
+ }
+}
+
+- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error {
+ NSLog(@"ExposurePlugin: CBPeripheral didReadRSSI");
+ if (error == nil) {
+ [self logiOSExposure:peripheral.identifier rssi:RSSI.intValue];
+ }
+}
+
+- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
+}
+
+#pragma mark Location Monitor
+
+- (void)startLocationManager {
+ if (_locationManager == nil) {
+ _locationManager = [[CLLocationManager alloc] init];
+ _locationManager.delegate = self;
+ _locationManager.distanceFilter = kCLDistanceFilterNone;
+ _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
+ _locationManager.pausesLocationUpdatesAutomatically = NO;
+ _locationManager.allowsBackgroundLocationUpdates = YES;
+ [self startBeaconRanging];
+ [self startLocationMonitor];
+ }
+}
+
+- (void)stopLocationManager {
+ if (_locationManager != nil) {
+ [self stopBeaconRanging];
+ [self stopLocationMonitor];
+ _locationManager.delegate = nil;
+ _locationManager = nil;
+ }
+}
+
+#pragma mark Beacon Ranging
+
+//
+// http://www.davidgyoungtech.com/2020/05/07/hacking-the-overflow-area
+//
+
+- (void)startBeaconRanging {
+ if ((_locationManager != nil) && (_beaconRegion == nil) && self.canBeaconRanging) {
+ _beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:kRangingBeaconUuid] identifier:kRangingBeaconUuid];
+ [_locationManager startRangingBeaconsInRegion:_beaconRegion];
+ }
+}
+
+- (void)stopBeaconRanging {
+ if ((_locationManager != nil) && (_beaconRegion != nil)) {
+ [_locationManager stopRangingBeaconsInRegion:_beaconRegion];
+ _beaconRegion = nil;
+ }
+}
+
+- (bool)isBeaconRangingStarted {
+ return (_locationManager != nil) && (_beaconRegion != nil);
+}
+
+- (void)updateBeaconRanging {
+ if (self.canBeaconRanging && !self.isBeaconRangingStarted) {
+ [self startBeaconRanging];
+ }
+ else if (!self.canBeaconRanging && self.isBeaconRangingStarted) {
+ [self stopBeaconRanging];
+ }
+}
+
+- (bool)canBeaconRanging {
+ return self.canLocationMonitor &&
+ [CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]];
+}
+
+
+#pragma mark Location & Heading Monitor
+
+- (void)startLocationMonitor {
+ if ((_locationManager != nil) && !_monitoringLocation && self.canLocationMonitor) {
+ [_locationManager startUpdatingLocation];
+ [_locationManager startUpdatingHeading];
+ [_locationManager startMonitoringSignificantLocationChanges];
+ _monitoringLocation = YES;
+ }
+}
+
+- (void)stopLocationMonitor {
+ if ((_locationManager != nil) && _monitoringLocation) {
+ [_locationManager stopUpdatingLocation];
+ [_locationManager stopUpdatingHeading];
+ _monitoringLocation = NO;
+ }
+}
+
+- (bool)isLocationMonitorStarted {
+ return (_locationManager != nil) && _monitoringLocation;
+}
+
+- (void)updateLocationMonitor {
+ if (self.canLocationMonitor && !self.isLocationMonitorStarted) {
+ [self startLocationMonitor];
+ }
+ else if (!self.canLocationMonitor && self.isLocationMonitorStarted) {
+ [self stopLocationMonitor];
+ }
+}
+
+- (bool)canLocationMonitor {
+ return
+ [CLLocationManager locationServicesEnabled] &&
+ (
+ ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways) ||
+ ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse)
+ );
+}
+
+#pragma mark CLLocationManagerDelegate
+
+- (void)locationManager:(CLLocationManager*)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
+ NSLog(@"ExposurePlugin didChangeAuthorizationStatus: %@", @(status));
+ [self updateBeaconRanging];
+ [self updateLocationMonitor];
+}
+
+- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray*)beacons inRegion:(CLBeaconRegion *)clBeaconRegion {
+ //NSLog(@"ExposurePlugin didRangeBeacons:[<%@>] inRegion: %@", @(beacons.count), clBeaconRegion.identifier);
+ [self updateLocalNotificationRequest];
+}
+
+- (void)locationManager:(CLLocationManager *)manager rangingBeaconsDidFailForRegion:(CLBeaconRegion *)clBeaconRegion withError:(NSError *)error {
+ //NSLog(@"ExposurePlugin rangingBeaconsDidFailForRegion: %@ withError: %@", clBeaconRegion.identifier, error.localizedDescription);
+}
+
+- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
+// UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+// content.body = @"didUpdateLocations";
+// content.sound = nil;
+//
+// UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:kLocalNotificationId content:content trigger:nil];
+// [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError* error) {
+// }];
+}
+
+#pragma mark Local Notifications
+
+- (void)updateLocalNotificationRequest {
+ bool shouldHaveLocalNotificationRequest =
+ (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) &&
+ (UIScreen.mainScreen.brightness == 0) &&
+ (0 < _localNotificationInterval);
+ if (shouldHaveLocalNotificationRequest) {
+ NSTimeInterval currentTimeInterval = [[[NSDate alloc] init] timeIntervalSince1970];
+ if (_lastLocalNotificationTime == 0) {
+ NSLog(@"Exposure: scheduling a local notification");
+ _lastLocalNotificationTime = currentTimeInterval;
+ }
+ else if (_localNotificationInterval <= (currentTimeInterval - _lastLocalNotificationTime)) {
+ NSLog(@"Exposure: triggering a local notification");
+ _lastLocalNotificationTime = currentTimeInterval;
+ [self scheduleLocalNotificationRequest];
+ }
+ }
+ else if (0 < _lastLocalNotificationTime) {
+ NSLog(@"Exposure: stopping local notifications");
+ _lastLocalNotificationTime = 0;
+ }
+}
+
+- (void)scheduleLocalNotificationRequest {
+ UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
+ content.body = @"Checking for exposures...";
+ content.sound = nil;
+
+ UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:kLocalNotificationId content:content trigger:nil];
+ [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError* error) {
+ //UIScreen.mainScreen.brightness = 0.01;
+ }];
+}
+
+#pragma mark App Livecycle Events
+
+- (void)connectAppLiveCycleEvents {
+ NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
+ [notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
+ [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
+ [notificationCenter addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:nil];
+ [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
+}
+
+- (void)disconnectAppLiveCycleEvents {
+ NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
+ [notificationCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
+ [notificationCenter removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
+ [notificationCenter removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
+ [notificationCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
+
+}
+
+- (void)applicationDidEnterBackground {
+ if (_bgTaskId == UIBackgroundTaskInvalid) {
+ __weak typeof(self) weakSelf = self;
+ _bgTaskId = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{
+ weakSelf.bgTaskId = UIBackgroundTaskInvalid;
+ }];
+ }
+}
+
+- (void)applicationWillEnterForeground {
+ if (_bgTaskId != UIBackgroundTaskInvalid) {
+ [UIApplication.sharedApplication endBackgroundTask:_bgTaskId];
+ _bgTaskId = UIBackgroundTaskInvalid;
+ }
+}
+
+- (void)applicationWillResignActive {
+ [UIApplication.sharedApplication beginReceivingRemoteControlEvents];
+ if ((_locationManager != nil) && self.canLocationMonitor && _monitoringLocation) {
+ [_locationManager startUpdatingLocation];
+ [_locationManager startUpdatingHeading];
+ [_locationManager startMonitoringSignificantLocationChanges];
+ }
+}
+
+- (void)applicationDidBecomeActive {
+ [UIApplication.sharedApplication endReceivingRemoteControlEvents];
+}
+
+
+#pragma mark Settings
+
+- (void)initSettings:(NSDictionary*)settings {
+ _exposureTimeoutInterval = (settings != nil) ? [settings inaDoubleForKey:kExposureTimeoutIntervalSettingName defaults:kExposureTimeoutInterval] : kExposureTimeoutInterval;
+ _exposurePingInterval = (settings != nil) ? [settings inaDoubleForKey:kExposurePingIntervalSettingName defaults:kExposurePingInterval] : kExposurePingInterval;
+ _exposureProcessInterval = (settings != nil) ? [settings inaDoubleForKey:kExposureProcessIntervalSettingName defaults:kExposureProcessInterval] : kExposureProcessInterval;
+ _localNotificationInterval = (settings != nil) ? [settings inaDoubleForKey:kLocalNotificationIntervalSettingName defaults:kLocalNotificationInterval] : kLocalNotificationInterval;
+ _exposureMinDuration = (settings != nil) ? [settings inaDoubleForKey:kExposureMinDurationSettingName defaults:kExposureMinDuration] : kExposureMinDuration;
+ _exposureMinRssi = (settings != nil) ? [settings inaIntForKey: kExposureMinRssiSettingName defaults:kMinRssi] : kMinRssi;
+}
+
+#pragma mark RPI
+
+- (void)initRPI {
+ _rpi = [self generateRPI];
+
+ if (_rpiTimer == nil) {
+ _rpiTimer = [NSTimer scheduledTimerWithTimeInterval:kRPIRefreshInterval target:self selector:@selector(refreshRPI) userInfo:nil repeats:YES];
+ }
+}
+
+- (void)refreshRPI {
+ _rpi = [self generateRPI];
+
+ [self updatePeripheral];
+}
+
+- (void)clearRPI {
+ if (_rpi != nil) {
+ _rpi = nil;
+ }
+ if (_rpiTimer != nil) {
+ [_rpiTimer invalidate];
+ _rpiTimer = nil;
+ }
+}
+
+- (NSData*)generateRPI {
+ NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970];
+
+ /* obtain ENInvertalNumber and timestamp i for teks generation */
+ uint32_t ENInvertalNumber = currentTimestamp / kRPIRefreshInterval;
+
+ /* _i : time aligned with TEKRollingPeriod */
+ uint32_t _i = (ENInvertalNumber / kTEKRollingPeriod) * kTEKRollingPeriod;
+ uint32_t _iExpire = _i + kTEKRollingPeriod;
+
+ /* if new day, generate a new tek */
+ /* if in the rest of the day, using last valid TEK */
+ if (_teks != nil) {
+ NSNumber *iMax = [self maxTEKsI];
+ if (iMax != nil) {
+ TEKRecord *maxRecord = [_teks objectForKey:iMax];
+ if (maxRecord.expire == (_iExpire)) {
+ _i = [iMax intValue];
+ } else
+ {
+ _i = ENInvertalNumber;
+ }
+ }
+ }
+
+ //NSLog(@"ExposurePlugin: ENIntervalNumber: %d, i: %d", ENInvertalNumber, _i);
+
+ /* generate tek each day, and store 14 of them in a database with timestamp i */
+ TEKRecord* tekRecord = [_teks objectForKey:[NSNumber numberWithInt: _i]];
+ if (tekRecord == nil) {
+ UInt8 bytes[16];
+ int status = SecRandomCopyBytes(kSecRandomDefault, (sizeof bytes)/(sizeof bytes[0]), &bytes);
+ NSData *tek = (status == errSecSuccess) ? [NSData dataWithBytes:bytes length:sizeof(bytes)] : nil;
+ if (tek != nil) {
+ tekRecord = [[TEKRecord alloc] initWithTEK:tek expire:_iExpire];
+ [_teks setObject:tekRecord forKey:[NSNumber numberWithInt: _i]];
+
+ if (_teks.count > 15) { // [0 - 14] gives 15 entries alltogether
+ uint32_t thresholdI = _i - 14 * kTEKRollingPeriod;
+ for (NSNumber *tekI in _teks.allKeys) {
+ if ([tekI intValue] < thresholdI) {
+ [_teks removeObjectForKey:tekI];
+ }
+ }
+ }
+
+ [self.class saveTEK2sToStorage:_teks];
+
+ NSInteger timestamp = ((NSInteger)_i) * kRPIRefreshInterval * 1000; // in miliseconds
+ NSInteger expirestamp = ((NSInteger)_iExpire) * kRPIRefreshInterval * 1000; // in miliseconds
+ [self notifyTEK:tek timestamp:timestamp expirestamp:expirestamp];
+ }
+ else {
+ //NSLog(@"ExposurePlugin: Failed to generate new tek for i: %d", _i);
+ }
+ }
+ //NSLog(@"ExposurePlugin: Obtain tek {%@}", tek);
+
+ NSData* rpi = [self generateRPIForIntervalNumber:ENInvertalNumber tek:tekRecord.tek];
+ [self notifyRPI:rpi tek:tekRecord.tek updateType:(_rpi != nil) ? @"update" : @"init" timestamp:(currentTimestamp * 1000.0) _i:_i ENInvertalNumber:ENInvertalNumber];
+ return rpi;
+}
+
+- (NSData*)generateRPIForIntervalNumber:(uint32_t)ENInvertalNumber tek:(NSData*)tek {
+ //NSLog(@"ExposurePlugin: Refresh TEK");
+
+ /* generate rpik and aemk based on tek */
+ NSData* rpik = [HKDFKit deriveKey:tek info:[@"EN-RPIK" dataUsingEncoding:NSUTF8StringEncoding] salt:nil outputSize:16];
+ //NSLog(@"ExposurePlugin: Obtain rpik {%@}", rpik);
+
+ NSData* aemk = [HKDFKit deriveKey:tek info:[@"EN-AEMK" dataUsingEncoding:NSUTF8StringEncoding] salt:nil outputSize:16];
+ //NSLog(@"ExposurePlugin: Obtain aemk {%@}", aemk);
+
+ /* generate paddedData for rpi message */
+ NSData* paddedData_0_5 = [@"EN-RPI" dataUsingEncoding:NSUTF8StringEncoding];
+
+ const char char_pd_6_11[6] = "\x00\x00\x00\x00\x00\x00";
+ NSData *paddedData_6_11 = [NSData dataWithBytes:char_pd_6_11 length:6];
+
+ uint32_t reverseENIntervalNumber = 0;
+ reverseENIntervalNumber = CFSwapInt32HostToBig(ENInvertalNumber);
+ NSData *paddedData_12_15 = [NSData dataWithBytes: &reverseENIntervalNumber length: 4];
+
+ NSMutableData* paddedData = [NSMutableData data];
+ [paddedData appendData:paddedData_0_5];
+ [paddedData appendData:paddedData_6_11];
+ [paddedData appendData:paddedData_12_15];
+ //NSLog(@"ExposurePlugin: PaddedData {%@}", paddedData);
+
+ /* generate encrypted en_rpi with AES-128 */
+ NSError *error = nil;
+ NSData* en_rpi = uiuc_aes_operation(paddedData, kCCEncrypt, kCCModeECB, kCCAlgorithmAES, ccNoPadding, kCCKeySizeAES128, nil, rpik, &error);
+ //NSLog(@"ExposurePlugin: RPI_en {%@}", en_rpi);
+
+ /* generate metadata for aem message */
+ NSData *metadata = [NSData dataWithBytes:(char[]){0x00,0x00,0x00,0x00} length:4];
+ //NSLog(@"ExposurePlugin: metadata {%@}", metadata);
+
+ /* generate encrypted en_aem with AES-128-CTR */
+ NSData* en_aem = uiuc_aes_operation(metadata, kCCEncrypt, kCCModeCTR, kCCAlgorithmAES, ccNoPadding, kCCKeySizeAES128, en_rpi, aemk, &error);
+ //NSLog(@"ExposurePlugin: AEM_en {%@}", en_aem);
+
+ /* contaticate en_rpi and en_aem to form the payload */
+ NSMutableData* ble_load = [NSMutableData data];
+ [ble_load appendData:en_rpi];
+ [ble_load appendData:en_aem];
+ //NSLog(@"ExposurePlugin: BLE_Payload {%@}", ble_load);
+ return ble_load;
+}
+
+- (NSNumber*)maxTEKsI {
+ NSNumber *result = nil;
+ if (_teks != nil) {
+ for (NSNumber *i in _teks) {
+ if ((result == nil) || ([result intValue] < [i intValue])) {
+ result = i;
+ }
+ }
+ }
+ return result;
+}
+
+- (NSArray*)teksList {
+ NSMutableArray *teksList = [[NSMutableArray alloc] init];
+ for (NSNumber *tekKey in _teks) {
+ NSInteger _i = [tekKey intValue];
+ TEKRecord *tekRecord = [_teks objectForKey:tekKey];
+ NSInteger timestamp = ((NSInteger)_i) * kRPIRefreshInterval * 1000; // in miliseconds
+ NSInteger expirestamp = ((NSInteger)tekRecord.expire) * kRPIRefreshInterval * 1000; // in miliseconds
+ NSString *tekString = [tekRecord.tek base64EncodedStringWithOptions:0];
+ [teksList addObject:@{
+ kTEKTimestampParamName : [NSNumber numberWithInteger:timestamp],
+ kTEKExpirestampParamName : [NSNumber numberWithInteger:expirestamp],
+ kTEKValueParamName: tekString ?: [NSNull null],
+ }];
+ }
+ return teksList;
+}
+
+- (NSDictionary*)rpisForTek:(NSData*)tek timestamp:(NSInteger)timestamp expirestamp:(NSInteger)expirestamp {
+ NSTimeInterval timestampInterval = (timestamp / 1000.0);
+ NSTimeInterval expirestampInterval = (expirestamp / 1000.0);
+
+ /* obtain start/endENInvertalNumber and timestamp i for teks generation */
+ uint32_t startENInvertalNumber = timestampInterval / kRPIRefreshInterval;
+ uint32_t endENInvertalNumber = expirestampInterval / kRPIRefreshInterval;
+
+ NSMutableDictionary *rpis = [[NSMutableDictionary alloc] init];
+ for (uint32_t intervalIndex = startENInvertalNumber; intervalIndex <= endENInvertalNumber; intervalIndex++) {
+ NSData *rpi = [self generateRPIForIntervalNumber:intervalIndex tek:tek];
+ NSString *rpiString = [rpi base64EncodedStringWithOptions:0];
+ NSInteger interval = (((NSInteger)intervalIndex) * kRPIRefreshInterval * 1000);
+ [rpis setObject:[NSNumber numberWithInteger:interval] forKey:rpiString];
+ }
+ return rpis;
+}
+
+- (void)updateTEKExpireTime {
+ if (_teks != nil) {
+ NSNumber * current_i = [self maxTEKsI];
+ TEKRecord* tekRecord = [_teks objectForKey:current_i];
+ NSTimeInterval currentTimestamp = [[[NSDate alloc] init] timeIntervalSince1970];
+ tekRecord.expire = currentTimestamp / kRPIRefreshInterval;
+ [self.class saveTEK2sToStorage:_teks];
+ }
+}
+
++ (void)saveTEK1sToStorage:(NSDictionary*)teks {
+ if (teks != nil) {
+ NSMutableDictionary *storageTeks = [[NSMutableDictionary alloc] init];
+ for (NSNumber *_i in teks) {
+ NSData *value = [teks objectForKey:_i];
+ NSString *storageKey = [_i stringValue];
+ NSString *storageValue = [value base64EncodedStringWithOptions:0];
+ if ((storageKey != nil) && (storageValue != nil)) {
+ [storageTeks setObject:storageValue forKey:storageKey];
+ }
+ }
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:storageTeks options:0 error:NULL];
+ uiucSecStorageData(kExposureTEK1, kExposureTEK1, jsonData);
+ }
+ else {
+ uiucSecStorageData(kExposureTEK1, kExposureTEK1, [NSNull null]);
+ }
+}
+
++ (NSMutableDictionary*)loadTEK1sFromStorage {
+ NSMutableDictionary* teks = [[NSMutableDictionary alloc] init];
+ NSData *jsonData = uiucSecStorageData(kExposureTEK1, kExposureTEK1, nil);
+ if (jsonData != nil) {
+ NSDictionary *storageTeks = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
+ if ([storageTeks isKindOfClass:[NSDictionary class]]) {
+ for (NSString *storageKey in storageTeks) {
+ NSString *storageValue = [storageTeks inaStringForKey:storageKey];
+ NSData *value = [[NSData alloc] initWithBase64EncodedString:storageValue options:0];
+ if (value != nil) {
+ [teks setObject:value forKey:[NSNumber numberWithInt:[storageKey intValue]]];
+ }
+ }
+ }
+ }
+ return teks;
+}
+
++ (void)saveTEK2sToStorage:(NSDictionary