commit b827cf3ccef72a3d38c6fc37466a99868823540f
Author: covidsafe-support <64945427+covidsafe-support@users.noreply.github.com>
Date: Fri May 8 15:23:03 2020 +1000
COVIDSafe code from version 1.0.16
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..7fc52fa
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,20 @@
+# Terms and Conditions for access to COVIDSafe App code
+By accessing the App Code I accept and agree to the following terms:
+
+1. If I distribute the App Code to anyone else, I will ensure these terms are provided to them and are not deleted.
+2. I agree to access the App Code for the purpose of obtaining information about the COVIDSafe App only.
+3. I understand and agree that the App Code is provided on an as is where is basis, that the App Code may be updated over time, and that the DTA and the Commonwealth have no liability whatsoever in connection with my access to or use of the App Code.
+
+4. I agree to stop all access and use of the App Code if requested by the DTA.
+5. I will not use the App Code for any product development purposes.
+6. I will promptly report to the DTA on any actual or potential security vulnerabilities I become aware of in respect of the COVIDSafe App.
+7. I am responsible for any costs of third party claims associated with my access to the App Code, and must pay those claims on request.
+8. I understand and agree that:
+
+ **a.** the DTA will collect information about me and my access to the App Code, and any feedback, comments, or other information that I post on GitHub in connection with the App Code (and I understand that this information may also be seen or accessed by other users of GitHub who have been given access to the App Code);
+
+ **b.** the DTA may use that information for the purposes of managing my access to the App Code, and to consider any feedback, comments or other information that I provide in relation to the App Code or the COVIDSafe App;
+
+ **c.** the DTA may disclose that information to other Commonwealth agencies and their contractors for the purposes of improving the App Code or the COVIDSafe App, or as required for public accountability and reporting purposes, but DTA will de-identify personal information before disclosure wherever reasonable and practicable (GitHub, a company based in the US, may also handle your personal information in accordance with the GitHub Terms and Conditions); and
+
+ **d.** further information about how DTA will handle personal information, and my rights to complain or access or correct my personal information, is available at DTA's Privacy Policy.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fe7084e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# COVIDSafe app
+
+# [Terms and Conditions for access to COVIDSafe App code](https://github.com/AU-COVIDSafe/mobile-android/blob/master/LICENSE.md)
+By accessing the App Code I accept and agree to the following terms:
+
+1. If I distribute the App Code to anyone else, I will ensure these terms are provided to them and are not deleted.
+2. I agree to access the App Code for the purpose of obtaining information about the COVIDSafe App only.
+3. I understand and agree that the App Code is provided on an as is where is basis, that the App Code may be updated over time, and that the DTA and the Commonwealth have no liability whatsoever in connection with my access to or use of the App Code.
+
+4. I agree to stop all access and use of the App Code if requested by the DTA.
+5. I will not use the App Code for any product development purposes.
+6. I will promptly report to the DTA on any actual or potential security vulnerabilities I become aware of in respect of the COVIDSafe App.
+7. I am responsible for any costs of third party claims associated with my access to the App Code, and must pay those claims on request.
+8. I understand and agree that:
+
+ **a.** the DTA will collect information about me and my access to the App Code, and any feedback, comments, or other information that I post on GitHub in connection with the App Code (and I understand that this information may also be seen or accessed by other users of GitHub who have been given access to the App Code);
+
+ **b.** the DTA may use that information for the purposes of managing my access to the App Code, and to consider any feedback, comments or other information that I provide in relation to the App Code or the COVIDSafe App;
+
+ **c.** the DTA may disclose that information to other Commonwealth agencies and their contractors for the purposes of improving the App Code or the COVIDSafe App, or as required for public accountability and reporting purposes, but DTA will de-identify personal information before disclosure wherever reasonable and practicable (GitHub, a company based in the US, may also handle your personal information in accordance with the GitHub Terms and Conditions); and
+
+ **d.** further information about how DTA will handle personal information, and my rights to complain or access or correct my personal information, is available at DTA's Privacy Policy.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..b783f5d
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,217 @@
+apply plugin: 'com.android.application'
+
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+apply plugin: 'kotlin-kapt'
+
+apply plugin: "androidx.navigation.safeargs.kotlin"
+
+buildscript {
+ repositories {
+ google()
+
+ }
+}
+
+def getGitHash = { ->
+ def stdout = new ByteArrayOutputStream()
+ exec {
+ commandLine 'git', 'rev-parse', '--short', 'HEAD'
+ standardOutput = stdout
+ }
+ return stdout.toString().trim()
+}
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ defaultConfig {
+ applicationId "au.gov.health.covidsafe"
+ resValue "string", "build_config_package", "au.gov.health.covidsafe"
+ minSdkVersion 23
+ targetSdkVersion 29
+ versionCode 16
+ versionName "1.0.16"
+ buildConfigField "String", "GITHASH", "\"${getGitHash()}\""
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments = ["room.schemaLocation":
+ "$projectDir/schemas".toString()]
+ }
+ }
+
+ buildConfigField "String", "ORG", ORG
+ buildConfigField "int", "PROTOCOL_VERSION", PROTOCOL_VERSION
+ buildConfigField "int", "SERVICE_FOREGROUND_NOTIFICATION_ID", SERVICE_FOREGROUND_NOTIFICATION_ID
+ buildConfigField "String", "SERVICE_FOREGROUND_CHANNEL_ID", SERVICE_FOREGROUND_CHANNEL_ID
+ buildConfigField "String", "SERVICE_FOREGROUND_CHANNEL_NAME", SERVICE_FOREGROUND_CHANNEL_NAME
+
+ buildConfigField "int", "PUSH_NOTIFICATION_ID", PUSH_NOTIFICATION_ID
+ buildConfigField "String", "PUSH_NOTIFICATION_CHANNEL_NAME", PUSH_NOTIFICATION_CHANNEL_NAME
+
+ buildConfigField "long", "SCAN_DURATION", SCAN_DURATION
+ buildConfigField "long", "MIN_SCAN_INTERVAL", MIN_SCAN_INTERVAL
+ buildConfigField "long", "MAX_SCAN_INTERVAL", MAX_SCAN_INTERVAL
+ buildConfigField "long", "MAX_QUEUE_TIME", MAX_QUEUE_TIME
+ buildConfigField "long", "BM_CHECK_INTERVAL", BM_CHECK_INTERVAL
+ buildConfigField "long", "HEALTH_CHECK_INTERVAL", HEALTH_CHECK_INTERVAL
+ buildConfigField "long", "CONNECTION_TIMEOUT", CONNECTION_TIMEOUT
+ buildConfigField "long", "BLACKLIST_DURATION", BLACKLIST_DURATION
+
+ buildConfigField "long", "ADVERTISING_DURATION", ADVERTISING_DURATION
+ buildConfigField "long", "ADVERTISING_INTERVAL", ADVERTISING_INTERVAL
+
+ buildConfigField "boolean", "ENABLE_DEBUG_SCREEN", "false"
+ }
+
+ signingConfigs {
+
+ release {
+ }
+
+ staging {
+ }
+
+ debug {
+ }
+ }
+
+ buildTypes {
+ debug {
+ buildConfigField "String", "BLE_SSID", STAGING_SERVICE_UUID
+ buildConfigField "boolean", "ENABLE_DEBUG_SCREEN", "true"
+ buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX
+ buildConfigField "String", "BASE_URL", TEST_BASE_URL
+
+
+ String ssid = STAGING_SERVICE_UUID
+ versionNameSuffix "-debug-${getGitHash()}-${ssid.substring(ssid.length() - 5,ssid.length() - 1 )}"
+ resValue "string", "app_name", "COVIDSafe Debug"
+ applicationIdSuffix "debug"
+ signingConfig signingConfigs.debug
+ }
+
+ staging {
+ buildConfigField "String", "BLE_SSID", STAGING_SERVICE_UUID
+ buildConfigField "boolean", "ENABLE_DEBUG_SCREEN", "true"
+ buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX
+ buildConfigField "String", "BASE_URL", STAGING_BASE_URL
+
+
+ // Retrieve bluetooth ssid from staging's strings.xml
+ String ssid = STAGING_SERVICE_UUID
+ versionNameSuffix "-beta-${getGitHash()}-${ssid.substring(ssid.length() - 5,ssid.length() - 1 )}"
+ debuggable false
+
+ applicationIdSuffix "beta"
+ resValue "string", "app_name", "COVIDSafe beta"
+
+ lintOptions {
+ // Ignore lint errors for now
+ abortOnError false
+ }
+ matchingFallbacks = ['release']
+ signingConfig signingConfigs.staging
+ }
+ release {
+
+ buildConfigField "String", "BLE_SSID", PRODUCTION_SERVICE_UUID
+ buildConfigField "String", "END_POINT_PREFIX", PRODUCTION_END_POINT_PREFIX
+ buildConfigField "String", "BASE_URL", PROD_BASE_URL
+
+ debuggable false
+ jniDebuggable false
+ renderscriptDebuggable false
+ minifyEnabled false
+ shrinkResources false
+ multiDexEnabled false
+ zipAlignEnabled true
+ resValue "string", "app_name", "COVIDSafe"
+
+ lintOptions {
+ // Ignore lint errors for now
+ abortOnError false
+ }
+ signingConfig signingConfigs.release
+
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ packagingOptions {
+ exclude 'META-INF/atomicfu.kotlin_module'
+ }
+
+}
+
+repositories {
+ jcenter()
+}
+
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
+ implementation project(":feedback-android")
+ // kotlin
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ def kotlin_coroutines_version = "1.3.5"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
+
+ //androidx
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+
+ implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'pub.devrel:easypermissions:3.0.0'
+ implementation 'com.google.code.gson:gson:2.8.6'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ // room
+ def room_version = "2.2.5"
+ kapt "androidx.room:room-compiler:$room_version"
+ implementation "androidx.room:room-runtime:$room_version"
+ implementation "androidx.room:room-ktx:$room_version"
+
+ // http
+ implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
+ implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
+ implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
+ implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
+
+ // rx
+ implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
+
+ //bottom navigation
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
+
+ //cardview
+ implementation 'androidx.cardview:cardview:1.0.0'
+
+ //Lottie
+ implementation 'com.airbnb.android:lottie:3.4.0'
+
+ implementation 'com.google.guava:guava:28.2-android'
+ implementation "androidx.security:security-crypto:1.0.0-beta01"
+ implementation "androidx.lifecycle:lifecycle-service:2.2.0"
+ implementation 'com.github.razir.progressbutton:progressbutton:2.0.1'
+}
diff --git a/app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json b/app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json
new file mode 100644
index 0000000..3315850
--- /dev/null
+++ b/app/schemas/au.gov.dta.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json
@@ -0,0 +1,114 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "9a95fc8ad88c160bf76c0ba4747db316",
+ "entities": [
+ {
+ "tableName": "record_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `msg` TEXT NOT NULL, `org` TEXT NOT NULL, `modelP` TEXT NOT NULL, `modelC` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `txPower` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "v",
+ "columnName": "v",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msg",
+ "columnName": "msg",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "org",
+ "columnName": "org",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelP",
+ "columnName": "modelP",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelC",
+ "columnName": "modelC",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "txPower",
+ "columnName": "txPower",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `msg` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msg",
+ "columnName": "msg",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a95fc8ad88c160bf76c0ba4747db316')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json b/app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json
new file mode 100644
index 0000000..3315850
--- /dev/null
+++ b/app/schemas/au.gov.dta.covidtrace.streetpass.persistence.StreetPassRecordDatabase/2.json
@@ -0,0 +1,114 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "9a95fc8ad88c160bf76c0ba4747db316",
+ "entities": [
+ {
+ "tableName": "record_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `msg` TEXT NOT NULL, `org` TEXT NOT NULL, `modelP` TEXT NOT NULL, `modelC` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `txPower` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "v",
+ "columnName": "v",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msg",
+ "columnName": "msg",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "org",
+ "columnName": "org",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelP",
+ "columnName": "modelP",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelC",
+ "columnName": "modelC",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "txPower",
+ "columnName": "txPower",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `msg` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msg",
+ "columnName": "msg",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a95fc8ad88c160bf76c0ba4747db316')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json b/app/schemas/au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json
new file mode 100644
index 0000000..3315850
--- /dev/null
+++ b/app/schemas/au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase/2.json
@@ -0,0 +1,114 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "9a95fc8ad88c160bf76c0ba4747db316",
+ "entities": [
+ {
+ "tableName": "record_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `msg` TEXT NOT NULL, `org` TEXT NOT NULL, `modelP` TEXT NOT NULL, `modelC` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `txPower` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "v",
+ "columnName": "v",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msg",
+ "columnName": "msg",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "org",
+ "columnName": "org",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelP",
+ "columnName": "modelP",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelC",
+ "columnName": "modelC",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rssi",
+ "columnName": "rssi",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "txPower",
+ "columnName": "txPower",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "status_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `msg` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "msg",
+ "columnName": "msg",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9a95fc8ad88c160bf76c0ba4747db316')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..91bf3ec
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/src/debug/res/values/mp_feedback_config.xml b/app/src/debug/res/values/mp_feedback_config.xml
new file mode 100644
index 0000000..24247e1
--- /dev/null
+++ b/app/src/debug/res/values/mp_feedback_config.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ - Android
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..32b7112
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/loading_upload.json b/app/src/main/assets/loading_upload.json
new file mode 100644
index 0000000..06dab39
--- /dev/null
+++ b/app/src/main/assets/loading_upload.json
@@ -0,0 +1,4650 @@
+{
+ "v": "5.6.9",
+ "fr": 30,
+ "ip": 0,
+ "op": 600,
+ "w": 260,
+ "h": 260,
+ "nm": "Spinner_Inside2",
+ "ddd": 0,
+ "assets": [
+ {
+ "id": "comp_0",
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "Spinner_Outside Outlines 3",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 1.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.647
+ ],
+ [
+ 1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.732,
+ 0
+ ],
+ [
+ 0,
+ -1.739
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.647
+ ],
+ [
+ -1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 1.737
+ ],
+ [
+ 1.733,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.732,
+ 0
+ ],
+ [
+ 0,
+ -1.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.739
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ -111.574,
+ 8.189
+ ],
+ [
+ -108.391,
+ 11.299
+ ],
+ [
+ -105.207,
+ 8.189
+ ],
+ [
+ -105.207,
+ 3.11
+ ],
+ [
+ -99.871,
+ 3.11
+ ],
+ [
+ -96.781,
+ 0.09
+ ],
+ [
+ -99.871,
+ -2.929
+ ],
+ [
+ -105.161,
+ -2.929
+ ],
+ [
+ -105.161,
+ -8.007
+ ],
+ [
+ -108.344,
+ -11.118
+ ],
+ [
+ -111.574,
+ -8.007
+ ],
+ [
+ -111.574,
+ -2.929
+ ],
+ [
+ -116.91,
+ -2.929
+ ],
+ [
+ -120,
+ 0.09
+ ],
+ [
+ -116.91,
+ 3.11
+ ],
+ [
+ -111.574,
+ 3.11
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 2",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.086000001197,
+ 0.086000001197,
+ 0.086000001197,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 11.076,
+ 120
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ -108.924,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 599,
+ "s": [
+ -360
+ ]
+ }
+ ],
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "Spinner_Outside Outlines 4",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.779,
+ 0
+ ],
+ [
+ 0,
+ -1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.647
+ ],
+ [
+ -1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.731,
+ 0
+ ],
+ [
+ 0,
+ 1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.647
+ ],
+ [
+ 1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.778,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 1.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.739
+ ],
+ [
+ 1.779,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.731,
+ 0
+ ],
+ [
+ 0,
+ -1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.693
+ ]
+ ],
+ "v": [
+ [
+ 108.391,
+ -11.164
+ ],
+ [
+ 105.208,
+ -8.053
+ ],
+ [
+ 105.208,
+ -2.975
+ ],
+ [
+ 99.871,
+ -2.975
+ ],
+ [
+ 96.783,
+ 0.045
+ ],
+ [
+ 99.871,
+ 3.065
+ ],
+ [
+ 105.208,
+ 3.065
+ ],
+ [
+ 105.208,
+ 8.142
+ ],
+ [
+ 108.391,
+ 11.253
+ ],
+ [
+ 111.575,
+ 8.142
+ ],
+ [
+ 111.575,
+ 3.065
+ ],
+ [
+ 116.911,
+ 3.065
+ ],
+ [
+ 120,
+ 0.045
+ ],
+ [
+ 116.911,
+ -2.975
+ ],
+ [
+ 111.575,
+ -2.975
+ ],
+ [
+ 111.575,
+ -8.053
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.086000001197,
+ 0.086000001197,
+ 0.086000001197,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 228.201,
+ 120
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 108.201,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 599,
+ "s": [
+ -360
+ ]
+ }
+ ],
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 4,
+ "nm": "Spinner_Outside Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 2,
+ "ty": "sh",
+ "ix": 3,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.327,
+ 0.823
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.935,
+ -0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.328,
+ -0.87
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.89,
+ 0.275
+ ],
+ [
+ 0.282,
+ 0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.891,
+ 0.275
+ ],
+ [
+ 0.281,
+ 0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.889,
+ 0.275
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.328,
+ -0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ 0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.328,
+ 0.823
+ ],
+ [
+ 0.843,
+ -0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.328,
+ 0.823
+ ],
+ [
+ 0.795,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.328,
+ 0.823
+ ],
+ [
+ 0.703,
+ -0.366
+ ]
+ ],
+ "v": [
+ [
+ 119.439,
+ -31.705
+ ],
+ [
+ 112.652,
+ -49.685
+ ],
+ [
+ 110.358,
+ -50.691
+ ],
+ [
+ 84.143,
+ -41.221
+ ],
+ [
+ 83.067,
+ -38.979
+ ],
+ [
+ 89.948,
+ -20.771
+ ],
+ [
+ 92.054,
+ -19.811
+ ],
+ [
+ 93.037,
+ -21.869
+ ],
+ [
+ 86.811,
+ -38.385
+ ],
+ [
+ 97.157,
+ -42.136
+ ],
+ [
+ 102.633,
+ -27.679
+ ],
+ [
+ 104.741,
+ -26.719
+ ],
+ [
+ 105.723,
+ -28.777
+ ],
+ [
+ 100.246,
+ -43.234
+ ],
+ [
+ 110.358,
+ -46.894
+ ],
+ [
+ 116.537,
+ -30.561
+ ],
+ [
+ 118.642,
+ -29.601
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 3",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 3,
+ "ty": "sh",
+ "ix": 4,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.561,
+ -0.778
+ ],
+ [
+ -0.842,
+ 0.549
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 0.503
+ ],
+ [
+ 0.515,
+ 0.687
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.749,
+ 0.503
+ ],
+ [
+ 0.516,
+ 0.686
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.795,
+ -0.595
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0.563,
+ 0.778
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.516,
+ 0.732
+ ],
+ [
+ 0.749,
+ -0.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.516,
+ 0.732
+ ],
+ [
+ 0.749,
+ -0.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.563,
+ -0.732
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.843,
+ 0.458
+ ]
+ ],
+ "v": [
+ [
+ 69.678,
+ -58.423
+ ],
+ [
+ 72.206,
+ -58.011
+ ],
+ [
+ 82.13,
+ -64.919
+ ],
+ [
+ 91.258,
+ -52.43
+ ],
+ [
+ 93.505,
+ -52.063
+ ],
+ [
+ 93.879,
+ -54.26
+ ],
+ [
+ 84.751,
+ -66.749
+ ],
+ [
+ 93.926,
+ -73.154
+ ],
+ [
+ 104.225,
+ -59.063
+ ],
+ [
+ 106.518,
+ -58.697
+ ],
+ [
+ 106.893,
+ -60.893
+ ],
+ [
+ 95.566,
+ -76.402
+ ],
+ [
+ 93.084,
+ -76.768
+ ],
+ [
+ 70.147,
+ -60.756
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 4",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 4,
+ "ty": "sh",
+ "ix": 5,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 54.606,
+ -85.781
+ ],
+ [
+ 66.402,
+ -77.5
+ ],
+ [
+ 69.959,
+ -94.427
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 5",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 5,
+ "ty": "sh",
+ "ix": 6,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.374,
+ 0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.641
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.235,
+ -1.007
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.14,
+ -0.184
+ ],
+ [
+ 0.749,
+ 0.549
+ ],
+ [
+ -0.189,
+ 0.777
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.655,
+ 0.458
+ ],
+ [
+ -0.516,
+ 0.686
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.938,
+ -0.549
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.888,
+ 0.641
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.094,
+ 0.321
+ ],
+ [
+ -0.562,
+ 0.731
+ ],
+ [
+ -0.702,
+ -0.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 0.412
+ ],
+ [
+ -0.749,
+ -0.503
+ ],
+ [
+ 0.093,
+ -0.183
+ ]
+ ],
+ "v": [
+ [
+ 42.856,
+ -82.944
+ ],
+ [
+ 69.631,
+ -97.813
+ ],
+ [
+ 72.393,
+ -97.767
+ ],
+ [
+ 72.535,
+ -97.676
+ ],
+ [
+ 73.423,
+ -95.159
+ ],
+ [
+ 67.478,
+ -65.697
+ ],
+ [
+ 67.151,
+ -64.873
+ ],
+ [
+ 64.763,
+ -64.507
+ ],
+ [
+ 64.109,
+ -66.566
+ ],
+ [
+ 65.7,
+ -74.206
+ ],
+ [
+ 51.657,
+ -84.088
+ ],
+ [
+ 44.681,
+ -80.154
+ ],
+ [
+ 42.576,
+ -80.154
+ ],
+ [
+ 42.201,
+ -82.35
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 6",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 6,
+ "ty": "sh",
+ "ix": 7,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -4.401,
+ -1.327
+ ],
+ [
+ -1.637,
+ 5.032
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.227,
+ 3.523
+ ],
+ [
+ -0.89,
+ 2.791
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.933,
+ -1.19
+ ],
+ [
+ -1.967,
+ -2.562
+ ],
+ [
+ -0.375,
+ -0.137
+ ],
+ [
+ -0.281,
+ 0.869
+ ],
+ [
+ 0.234,
+ 0.366
+ ],
+ [
+ 3.978,
+ 1.236
+ ],
+ [
+ 1.451,
+ -4.621
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.46,
+ -3.568
+ ],
+ [
+ 0.889,
+ -2.745
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 4.074,
+ 1.236
+ ],
+ [
+ 2.059,
+ 3.202
+ ],
+ [
+ 0.421,
+ 0.137
+ ],
+ [
+ 0.28,
+ -0.915
+ ],
+ [
+ -0.28,
+ -0.366
+ ]
+ ],
+ "o": [
+ [
+ 5.992,
+ 1.83
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.451,
+ -4.484
+ ],
+ [
+ -6.179,
+ -3.431
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.841,
+ -2.654
+ ],
+ [
+ 2.807,
+ 0.869
+ ],
+ [
+ 0.235,
+ 0.32
+ ],
+ [
+ 0.89,
+ 0.275
+ ],
+ [
+ 0.188,
+ -0.641
+ ],
+ [
+ -2.152,
+ -2.79
+ ],
+ [
+ -5.711,
+ -1.738
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.545,
+ 4.85
+ ],
+ [
+ 5.899,
+ 3.248
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ 2.882
+ ],
+ [
+ -3.837,
+ -1.189
+ ],
+ [
+ -0.141,
+ -0.275
+ ],
+ [
+ -0.891,
+ -0.275
+ ],
+ [
+ -0.189,
+ 0.549
+ ],
+ [
+ 2.388,
+ 3.706
+ ]
+ ],
+ "v": [
+ [
+ 28.532,
+ -84.454
+ ],
+ [
+ 41.498,
+ -89.623
+ ],
+ [
+ 41.545,
+ -89.715
+ ],
+ [
+ 34.429,
+ -101.061
+ ],
+ [
+ 28.111,
+ -109.296
+ ],
+ [
+ 28.158,
+ -109.387
+ ],
+ [
+ 36.116,
+ -112.178
+ ],
+ [
+ 43.09,
+ -107.237
+ ],
+ [
+ 43.979,
+ -106.643
+ ],
+ [
+ 46.132,
+ -107.74
+ ],
+ [
+ 45.851,
+ -109.296
+ ],
+ [
+ 37.192,
+ -115.152
+ ],
+ [
+ 24.741,
+ -110.165
+ ],
+ [
+ 24.694,
+ -110.074
+ ],
+ [
+ 32.089,
+ -98.499
+ ],
+ [
+ 38.128,
+ -90.493
+ ],
+ [
+ 38.081,
+ -90.401
+ ],
+ [
+ 29.748,
+ -87.474
+ ],
+ [
+ 21.37,
+ -93.878
+ ],
+ [
+ 20.481,
+ -94.564
+ ],
+ [
+ 18.328,
+ -93.421
+ ],
+ [
+ 18.561,
+ -91.957
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 7",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 7,
+ "ty": "sh",
+ "ix": 8,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.094,
+ 5.49
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 5.664,
+ -0.137
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.094,
+ -5.444
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 5.664,
+ -0.092
+ ]
+ ],
+ "v": [
+ [
+ 6.39,
+ -104.675
+ ],
+ [
+ 6.39,
+ -104.767
+ ],
+ [
+ -3.299,
+ -113.871
+ ],
+ [
+ -8.683,
+ -113.779
+ ],
+ [
+ -8.309,
+ -95.205
+ ],
+ [
+ -2.925,
+ -95.296
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 8",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 8,
+ "ty": "sh",
+ "ix": 9,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.187,
+ -8.646
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.924,
+ -0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.046,
+ 1.922
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.966,
+ 0.046
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.188,
+ 8.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.92,
+ 0.046
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.047,
+ -1.876
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.925,
+ -0.183
+ ]
+ ],
+ "v": [
+ [
+ 13.645,
+ -104.996
+ ],
+ [
+ 13.645,
+ -104.904
+ ],
+ [
+ -2.832,
+ -89.166
+ ],
+ [
+ -11.679,
+ -88.983
+ ],
+ [
+ -15.236,
+ -92.323
+ ],
+ [
+ -15.705,
+ -116.387
+ ],
+ [
+ -12.288,
+ -119.864
+ ],
+ [
+ -3.441,
+ -120.001
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 9",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 9,
+ "ty": "sh",
+ "ix": 10,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.872,
+ -0.549
+ ],
+ [
+ -0.561,
+ -1.83
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.872,
+ 0.549
+ ],
+ [
+ 0.562,
+ 1.83
+ ]
+ ],
+ "o": [
+ [
+ -0.562,
+ -1.83
+ ],
+ [
+ -1.872,
+ 0.549
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.562,
+ 1.83
+ ],
+ [
+ 1.873,
+ -0.549
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ -29.888,
+ -113.185
+ ],
+ [
+ -34.195,
+ -115.472
+ ],
+ [
+ -36.536,
+ -111.263
+ ],
+ [
+ -29.467,
+ -87.657
+ ],
+ [
+ -25.161,
+ -85.369
+ ],
+ [
+ -22.82,
+ -89.578
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 10",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 10,
+ "ty": "sh",
+ "ix": 11,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.467,
+ -0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.638,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.188,
+ 1.83
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.281,
+ 0.458
+ ],
+ [
+ 1.638,
+ -0.915
+ ],
+ [
+ -0.188,
+ -1.143
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.545,
+ -0.869
+ ],
+ [
+ -0.983,
+ -1.647
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.497,
+ 1.099
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.639,
+ -0.915
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.047,
+ -0.411
+ ],
+ [
+ -0.936,
+ -1.601
+ ],
+ [
+ -1.452,
+ 0.823
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.171,
+ -0.915
+ ],
+ [
+ -1.685,
+ 0.961
+ ],
+ [
+ 0.281,
+ 0.595
+ ]
+ ],
+ "v": [
+ [
+ -70.521,
+ -93.238
+ ],
+ [
+ -49.502,
+ -77.409
+ ],
+ [
+ -44.728,
+ -76.997
+ ],
+ [
+ -44.353,
+ -77.226
+ ],
+ [
+ -42.294,
+ -81.48
+ ],
+ [
+ -45.57,
+ -107.466
+ ],
+ [
+ -46.038,
+ -108.93
+ ],
+ [
+ -50.672,
+ -110.119
+ ],
+ [
+ -52.451,
+ -106.643
+ ],
+ [
+ -49.128,
+ -85.232
+ ],
+ [
+ -66.12,
+ -98.728
+ ],
+ [
+ -70.38,
+ -99.094
+ ],
+ [
+ -71.691,
+ -94.519
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 11",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 11,
+ "ty": "sh",
+ "ix": 12,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 4.026,
+ 3.523
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 3.698,
+ -4.071
+ ],
+ [
+ -4.025,
+ -3.522
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.698,
+ 4.072
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -4.026,
+ -3.523
+ ],
+ [
+ -3.698,
+ 4.072
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 4.026,
+ 3.523
+ ],
+ [
+ 3.745,
+ -4.117
+ ]
+ ],
+ "v": [
+ [
+ -73.095,
+ -76.448
+ ],
+ [
+ -73.142,
+ -76.494
+ ],
+ [
+ -86.904,
+ -75.991
+ ],
+ [
+ -85.875,
+ -62.678
+ ],
+ [
+ -85.828,
+ -62.632
+ ],
+ [
+ -72.065,
+ -63.135
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 12",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 12,
+ "ty": "sh",
+ "ix": 13,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 6.506,
+ -7.137
+ ],
+ [
+ 6.741,
+ 5.856
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.507,
+ 7.091
+ ],
+ [
+ -6.741,
+ -5.856
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -6.46,
+ 7.091
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.694,
+ -5.856
+ ],
+ [
+ 6.506,
+ -7.091
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.694,
+ 5.81
+ ]
+ ],
+ "v": [
+ [
+ -67.337,
+ -58.926
+ ],
+ [
+ -90.602,
+ -57.279
+ ],
+ [
+ -90.649,
+ -57.325
+ ],
+ [
+ -91.632,
+ -80.199
+ ],
+ [
+ -68.367,
+ -81.846
+ ],
+ [
+ -68.32,
+ -81.801
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 13",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 13,
+ "ty": "sh",
+ "ix": 14,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -8.332,
+ -3.248
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.418,
+ 8.463
+ ],
+ [
+ 1.217,
+ 3.477
+ ],
+ [
+ 0.982,
+ 0.412
+ ],
+ [
+ 0.656,
+ -1.601
+ ],
+ [
+ -0.235,
+ -0.64
+ ],
+ [
+ 1.077,
+ -2.699
+ ],
+ [
+ 5.009,
+ 1.922
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.966,
+ 4.85
+ ],
+ [
+ -2.06,
+ 1.236
+ ],
+ [
+ -0.375,
+ 0.869
+ ],
+ [
+ 1.732,
+ 0.686
+ ],
+ [
+ 0.749,
+ -0.458
+ ],
+ [
+ 1.779,
+ -4.301
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 8.426,
+ 3.248
+ ],
+ [
+ 1.872,
+ -4.575
+ ],
+ [
+ -0.281,
+ -0.778
+ ],
+ [
+ -1.639,
+ -0.64
+ ],
+ [
+ -0.281,
+ 0.732
+ ],
+ [
+ 0.749,
+ 2.379
+ ],
+ [
+ -2.012,
+ 4.896
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -5.009,
+ -1.968
+ ],
+ [
+ 0.936,
+ -2.241
+ ],
+ [
+ 0.468,
+ -0.32
+ ],
+ [
+ 0.702,
+ -1.739
+ ],
+ [
+ -1.124,
+ -0.457
+ ],
+ [
+ -2.762,
+ 1.692
+ ],
+ [
+ -3.651,
+ 8.829
+ ]
+ ],
+ "v": [
+ [
+ -104.365,
+ -25.575
+ ],
+ [
+ -104.271,
+ -25.529
+ ],
+ [
+ -83.206,
+ -34.404
+ ],
+ [
+ -82.738,
+ -46.025
+ ],
+ [
+ -84.61,
+ -47.992
+ ],
+ [
+ -88.777,
+ -46.208
+ ],
+ [
+ -88.823,
+ -44.058
+ ],
+ [
+ -89.058,
+ -36.921
+ ],
+ [
+ -101.65,
+ -32.163
+ ],
+ [
+ -101.743,
+ -32.208
+ ],
+ [
+ -107.501,
+ -44.058
+ ],
+ [
+ -103.007,
+ -49.09
+ ],
+ [
+ -101.603,
+ -50.737
+ ],
+ [
+ -103.475,
+ -55.083
+ ],
+ [
+ -106.518,
+ -54.854
+ ],
+ [
+ -113.399,
+ -46.436
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 14",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 14,
+ "ty": "sh",
+ "ix": 15,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.59,
+ 0.595
+ ],
+ [
+ 0.609,
+ -1.555
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.592,
+ 0.641
+ ],
+ [
+ 0.608,
+ -1.556
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.592,
+ 0.64
+ ],
+ [
+ 0.607,
+ -1.556
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.778,
+ -0.686
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 1.738
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.592,
+ -0.595
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.608,
+ -1.556
+ ],
+ [
+ -1.59,
+ -0.594
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.608,
+ -1.555
+ ],
+ [
+ -1.591,
+ -0.594
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 1.784
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.826,
+ 0.686
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.655,
+ -1.601
+ ]
+ ],
+ "v": [
+ [
+ 116.865,
+ 30.651
+ ],
+ [
+ 112.838,
+ 32.389
+ ],
+ [
+ 107.642,
+ 45.428
+ ],
+ [
+ 101.463,
+ 43.095
+ ],
+ [
+ 105.817,
+ 32.115
+ ],
+ [
+ 104.084,
+ 28.18
+ ],
+ [
+ 100.059,
+ 29.919
+ ],
+ [
+ 95.705,
+ 40.899
+ ],
+ [
+ 89.76,
+ 38.611
+ ],
+ [
+ 94.863,
+ 25.801
+ ],
+ [
+ 93.13,
+ 21.867
+ ],
+ [
+ 89.106,
+ 23.606
+ ],
+ [
+ 82.739,
+ 39.526
+ ],
+ [
+ 84.704,
+ 43.918
+ ],
+ [
+ 107.642,
+ 52.656
+ ],
+ [
+ 112.136,
+ 50.735
+ ],
+ [
+ 118.549,
+ 34.585
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 15",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 15,
+ "ty": "sh",
+ "ix": 16,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.359,
+ 1.052
+ ],
+ [
+ 1.124,
+ -1.327
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.358,
+ 1.098
+ ],
+ [
+ 1.124,
+ -1.326
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.498,
+ -1.191
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.217,
+ 1.464
+ ],
+ [
+ 1.498,
+ 1.19
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.357,
+ -1.098
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.124,
+ -1.327
+ ],
+ [
+ -1.357,
+ -1.098
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.218,
+ 1.464
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.498,
+ 1.19
+ ],
+ [
+ 1.216,
+ -1.464
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.123,
+ -1.327
+ ]
+ ],
+ "v": [
+ [
+ 91.164,
+ 57.506
+ ],
+ [
+ 86.717,
+ 57.963
+ ],
+ [
+ 79.04,
+ 67.204
+ ],
+ [
+ 73.798,
+ 63.087
+ ],
+ [
+ 82.739,
+ 52.29
+ ],
+ [
+ 82.27,
+ 47.944
+ ],
+ [
+ 77.824,
+ 48.401
+ ],
+ [
+ 66.777,
+ 61.806
+ ],
+ [
+ 67.291,
+ 66.565
+ ],
+ [
+ 86.624,
+ 81.844
+ ],
+ [
+ 91.493,
+ 81.341
+ ],
+ [
+ 90.977,
+ 76.583
+ ],
+ [
+ 83.956,
+ 71.047
+ ],
+ [
+ 91.633,
+ 61.806
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 16",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 16,
+ "ty": "sh",
+ "ix": 17,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 65.607,
+ 86.373
+ ],
+ [
+ 56.151,
+ 80.38
+ ],
+ [
+ 58.398,
+ 91.223
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 17",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 17,
+ "ty": "sh",
+ "ix": 18,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.233,
+ -0.366
+ ],
+ [
+ 1.545,
+ -1.007
+ ],
+ [
+ 1.358,
+ 0.824
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.216,
+ -0.823
+ ],
+ [
+ 1.031,
+ 1.464
+ ],
+ [
+ 0.093,
+ 0.503
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.593,
+ 1.007
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.592,
+ -0.961
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 1.03,
+ 1.51
+ ],
+ [
+ -1.357,
+ 0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.28,
+ 1.419
+ ],
+ [
+ -1.499,
+ 1.007
+ ],
+ [
+ -0.281,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.329,
+ -1.784
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.546,
+ -1.053
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.516,
+ 0.275
+ ]
+ ],
+ "v": [
+ [
+ 80.632,
+ 88.34
+ ],
+ [
+ 79.789,
+ 92.824
+ ],
+ [
+ 75.67,
+ 92.732
+ ],
+ [
+ 71.129,
+ 89.897
+ ],
+ [
+ 59.662,
+ 97.536
+ ],
+ [
+ 60.738,
+ 102.934
+ ],
+ [
+ 59.287,
+ 106.457
+ ],
+ [
+ 54.792,
+ 105.588
+ ],
+ [
+ 54.278,
+ 104.17
+ ],
+ [
+ 49.41,
+ 78.047
+ ],
+ [
+ 51.236,
+ 73.609
+ ],
+ [
+ 51.562,
+ 73.381
+ ],
+ [
+ 56.431,
+ 73.381
+ ],
+ [
+ 79.508,
+ 87.425
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 18",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 18,
+ "ty": "sh",
+ "ix": 19,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.6,
+ -0.321
+ ],
+ [
+ 0.608,
+ 1.922
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.527,
+ 0.778
+ ],
+ [
+ -2.621,
+ -0.595
+ ],
+ [
+ -0.657,
+ 0.183
+ ],
+ [
+ 0.516,
+ 1.693
+ ],
+ [
+ 0.891,
+ 0.183
+ ],
+ [
+ 3.652,
+ -1.144
+ ],
+ [
+ -1.686,
+ -5.353
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.554,
+ 0.366
+ ],
+ [
+ -0.516,
+ -1.601
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.714,
+ -0.823
+ ],
+ [
+ 2.81,
+ 0.961
+ ],
+ [
+ 0.841,
+ -0.275
+ ],
+ [
+ -0.514,
+ -1.692
+ ],
+ [
+ -0.889,
+ -0.275
+ ],
+ [
+ -3.978,
+ 1.19
+ ],
+ [
+ 1.872,
+ 5.856
+ ]
+ ],
+ "o": [
+ [
+ -1.639,
+ -5.124
+ ],
+ [
+ -5.617,
+ 0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ -1.418
+ ],
+ [
+ 2.06,
+ -0.64
+ ],
+ [
+ 0.61,
+ 0.137
+ ],
+ [
+ 1.731,
+ -0.503
+ ],
+ [
+ -0.421,
+ -1.281
+ ],
+ [
+ -3.229,
+ -0.87
+ ],
+ [
+ -6.131,
+ 1.875
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.873,
+ 5.856
+ ],
+ [
+ 5.429,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.561,
+ 1.738
+ ],
+ [
+ -2.95,
+ 0.869
+ ],
+ [
+ -0.514,
+ -0.183
+ ],
+ [
+ -1.733,
+ 0.503
+ ],
+ [
+ 0.328,
+ 1.007
+ ],
+ [
+ 4.12,
+ 1.327
+ ],
+ [
+ 6.507,
+ -1.967
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 45.899,
+ 102.34
+ ],
+ [
+ 33.54,
+ 96.439
+ ],
+ [
+ 25.817,
+ 94.562
+ ],
+ [
+ 25.769,
+ 94.471
+ ],
+ [
+ 28.812,
+ 90.719
+ ],
+ [
+ 35.787,
+ 90.765
+ ],
+ [
+ 37.614,
+ 90.719
+ ],
+ [
+ 39.766,
+ 86.785
+ ],
+ [
+ 37.472,
+ 84.681
+ ],
+ [
+ 27.08,
+ 85.001
+ ],
+ [
+ 19.357,
+ 97.079
+ ],
+ [
+ 19.404,
+ 97.17
+ ],
+ [
+ 32.277,
+ 103.117
+ ],
+ [
+ 39.533,
+ 104.993
+ ],
+ [
+ 39.579,
+ 105.085
+ ],
+ [
+ 36.116,
+ 109.202
+ ],
+ [
+ 27.548,
+ 108.882
+ ],
+ [
+ 25.49,
+ 108.882
+ ],
+ [
+ 23.335,
+ 112.816
+ ],
+ [
+ 25.348,
+ 114.875
+ ],
+ [
+ 37.8,
+ 114.966
+ ],
+ [
+ 45.946,
+ 102.477
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 19",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 19,
+ "ty": "sh",
+ "ix": 20,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 7.959,
+ 0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.187,
+ 6.817
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 7.958,
+ 0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.141,
+ -6.817
+ ]
+ ],
+ "v": [
+ [
+ -4.143,
+ 92.412
+ ],
+ [
+ -11.539,
+ 92.229
+ ],
+ [
+ -12.147,
+ 116.613
+ ],
+ [
+ -4.751,
+ 116.796
+ ],
+ [
+ 8.637,
+ 104.993
+ ],
+ [
+ 8.637,
+ 104.902
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 20",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 20,
+ "ty": "sh",
+ "ix": 21,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.235,
+ -8.647
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.925,
+ 0.229
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.983,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.234,
+ 8.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.046
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.048,
+ -0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.924,
+ 0.275
+ ]
+ ],
+ "v": [
+ [
+ 12.288,
+ 104.902
+ ],
+ [
+ 12.288,
+ 104.993
+ ],
+ [
+ -4.892,
+ 119.999
+ ],
+ [
+ -14.067,
+ 119.77
+ ],
+ [
+ -15.798,
+ 117.986
+ ],
+ [
+ -15.097,
+ 90.674
+ ],
+ [
+ -13.271,
+ 88.935
+ ],
+ [
+ -4.096,
+ 89.164
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 21",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 21,
+ "ty": "sh",
+ "ix": 22,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.936,
+ 0.275
+ ],
+ [
+ 0.28,
+ -0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.229
+ ],
+ [
+ -0.281,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.89,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.281,
+ 0.915
+ ],
+ [
+ 0.936,
+ 0.275
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.233,
+ -0.869
+ ]
+ ],
+ "v": [
+ [
+ -24.458,
+ 85.641
+ ],
+ [
+ -26.658,
+ 86.877
+ ],
+ [
+ -34.429,
+ 113.594
+ ],
+ [
+ -33.212,
+ 115.744
+ ],
+ [
+ -30.965,
+ 114.509
+ ],
+ [
+ -23.194,
+ 87.791
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 22",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 22,
+ "ty": "sh",
+ "ix": 23,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.141,
+ 0.229
+ ],
+ [
+ 0.843,
+ 0.457
+ ],
+ [
+ 0.608,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.796,
+ 0.457
+ ],
+ [
+ 0.421,
+ -0.732
+ ],
+ [
+ 0,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.503
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.796,
+ 0.549
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0.467,
+ -0.777
+ ],
+ [
+ -0.702,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.093,
+ -0.823
+ ],
+ [
+ -0.842,
+ -0.504
+ ],
+ [
+ -0.14,
+ 0.275
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.14,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.889,
+ 0.503
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.14,
+ -0.275
+ ]
+ ],
+ "v": [
+ [
+ -33.071,
+ 85.824
+ ],
+ [
+ -33.727,
+ 83.537
+ ],
+ [
+ -35.88,
+ 83.766
+ ],
+ [
+ -58.443,
+ 100.693
+ ],
+ [
+ -54.885,
+ 73.243
+ ],
+ [
+ -55.822,
+ 71.185
+ ],
+ [
+ -58.256,
+ 71.779
+ ],
+ [
+ -58.49,
+ 72.557
+ ],
+ [
+ -61.907,
+ 102.294
+ ],
+ [
+ -60.831,
+ 104.581
+ ],
+ [
+ -60.69,
+ 104.673
+ ],
+ [
+ -58.162,
+ 104.444
+ ],
+ [
+ -33.493,
+ 86.465
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 23",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 23,
+ "ty": "sh",
+ "ix": 24,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 4.775,
+ 5.307
+ ],
+ [
+ 5.383,
+ -4.62
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -4.775,
+ -5.261
+ ],
+ [
+ -5.384,
+ 4.621
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -4.775,
+ -5.307
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -5.384,
+ 4.621
+ ],
+ [
+ 4.775,
+ 5.307
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 5.384,
+ -4.621
+ ]
+ ],
+ "v": [
+ [
+ -70.146,
+ 60.754
+ ],
+ [
+ -88.028,
+ 60.067
+ ],
+ [
+ -88.074,
+ 60.113
+ ],
+ [
+ -89.619,
+ 77.635
+ ],
+ [
+ -71.737,
+ 78.321
+ ],
+ [
+ -71.691,
+ 78.276
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 24",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 24,
+ "ty": "sh",
+ "ix": 25,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -6.366,
+ -7.045
+ ],
+ [
+ 6.507,
+ -5.581
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.366,
+ 7.045
+ ],
+ [
+ -6.507,
+ 5.627
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 6.366,
+ 7.045
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.506,
+ 5.627
+ ],
+ [
+ -6.366,
+ -7.045
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.553,
+ -5.581
+ ]
+ ],
+ "v": [
+ [
+ -67.618,
+ 58.695
+ ],
+ [
+ -69.163,
+ 80.929
+ ],
+ [
+ -69.21,
+ 80.975
+ ],
+ [
+ -92.147,
+ 79.694
+ ],
+ [
+ -90.602,
+ 57.414
+ ],
+ [
+ -90.555,
+ 57.368
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 25",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 25,
+ "ty": "sh",
+ "ix": 26,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -2.528,
+ -6.314
+ ],
+ [
+ -3.324,
+ -1.555
+ ],
+ [
+ -0.187,
+ -0.412
+ ],
+ [
+ 0.843,
+ -0.32
+ ],
+ [
+ 0.374,
+ 0.182
+ ],
+ [
+ 1.873,
+ 4.712
+ ],
+ [
+ -8.426,
+ 3.249
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.418,
+ -8.463
+ ],
+ [
+ 1.17,
+ -3.524
+ ],
+ [
+ 0.608,
+ -0.228
+ ],
+ [
+ 0.327,
+ 0.869
+ ],
+ [
+ -0.094,
+ 0.274
+ ],
+ [
+ 1.451,
+ 3.523
+ ],
+ [
+ 6.741,
+ -2.608
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 1.451,
+ 3.568
+ ],
+ [
+ 0.327,
+ 0.138
+ ],
+ [
+ 0.327,
+ 0.823
+ ],
+ [
+ -0.515,
+ 0.183
+ ],
+ [
+ -3.792,
+ -1.831
+ ],
+ [
+ -3.37,
+ -8.281
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 8.285,
+ -3.203
+ ],
+ [
+ 1.872,
+ 4.621
+ ],
+ [
+ -0.14,
+ 0.456
+ ],
+ [
+ -0.89,
+ 0.321
+ ],
+ [
+ -0.187,
+ -0.458
+ ],
+ [
+ 1.123,
+ -2.974
+ ],
+ [
+ -2.575,
+ -6.359
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.788,
+ 2.562
+ ]
+ ],
+ "v": [
+ [
+ -110.45,
+ 45.291
+ ],
+ [
+ -103.428,
+ 52.427
+ ],
+ [
+ -102.539,
+ 53.297
+ ],
+ [
+ -103.522,
+ 55.447
+ ],
+ [
+ -104.926,
+ 55.356
+ ],
+ [
+ -113.54,
+ 46.389
+ ],
+ [
+ -104.271,
+ 26.167
+ ],
+ [
+ -104.177,
+ 26.122
+ ],
+ [
+ -83.112,
+ 34.768
+ ],
+ [
+ -82.785,
+ 46.481
+ ],
+ [
+ -83.861,
+ 47.669
+ ],
+ [
+ -86.155,
+ 46.663
+ ],
+ [
+ -86.202,
+ 45.474
+ ],
+ [
+ -86.202,
+ 35.958
+ ],
+ [
+ -102.726,
+ 29.507
+ ],
+ [
+ -102.82,
+ 29.553
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 26",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "mm",
+ "mm": 1,
+ "nm": "Merge Paths 1",
+ "mn": "ADBE Vector Filter - Merge",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.086000001197,
+ 0.086000001197,
+ 0.086000001197,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 30,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ }
+ ]
+ }
+ ],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 0,
+ "nm": "Spinner_Outside Outlines Comp 1",
+ "refId": "comp_0",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 599,
+ "s": [
+ 360
+ ]
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 130,
+ 130,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "w": 240,
+ "h": 240,
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
\ No newline at end of file
diff --git a/app/src/main/assets/spinner_home.json b/app/src/main/assets/spinner_home.json
new file mode 100644
index 0000000..779aef8
--- /dev/null
+++ b/app/src/main/assets/spinner_home.json
@@ -0,0 +1,5500 @@
+{
+ "v": "5.6.9",
+ "fr": 30,
+ "ip": 0,
+ "op": 600,
+ "w": 260,
+ "h": 260,
+ "nm": "Spinner_Inside2",
+ "ddd": 0,
+ "assets": [
+ {
+ "id": "comp_0",
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "Spinner_Outside Outlines 3",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 1.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.647
+ ],
+ [
+ 1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.732,
+ 0
+ ],
+ [
+ 0,
+ -1.739
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.647
+ ],
+ [
+ -1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 1.737
+ ],
+ [
+ 1.733,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.732,
+ 0
+ ],
+ [
+ 0,
+ -1.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.739
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ -111.574,
+ 8.189
+ ],
+ [
+ -108.391,
+ 11.299
+ ],
+ [
+ -105.207,
+ 8.189
+ ],
+ [
+ -105.207,
+ 3.11
+ ],
+ [
+ -99.871,
+ 3.11
+ ],
+ [
+ -96.781,
+ 0.09
+ ],
+ [
+ -99.871,
+ -2.929
+ ],
+ [
+ -105.161,
+ -2.929
+ ],
+ [
+ -105.161,
+ -8.007
+ ],
+ [
+ -108.344,
+ -11.118
+ ],
+ [
+ -111.574,
+ -8.007
+ ],
+ [
+ -111.574,
+ -2.929
+ ],
+ [
+ -116.91,
+ -2.929
+ ],
+ [
+ -120,
+ 0.09
+ ],
+ [
+ -116.91,
+ 3.11
+ ],
+ [
+ -111.574,
+ 3.11
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 2",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.086000001197,
+ 0.086000001197,
+ 0.086000001197,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 11.076,
+ 120
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ -108.924,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 599,
+ "s": [
+ -360
+ ]
+ }
+ ],
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "Spinner_Outside Outlines 4",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.779,
+ 0
+ ],
+ [
+ 0,
+ -1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.647
+ ],
+ [
+ -1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.731,
+ 0
+ ],
+ [
+ 0,
+ 1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.647
+ ],
+ [
+ 1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.778,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ 0
+ ],
+ [
+ 0,
+ 1.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 1.739
+ ],
+ [
+ 1.779,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.731,
+ 0
+ ],
+ [
+ 0,
+ -1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ -1.693
+ ]
+ ],
+ "v": [
+ [
+ 108.391,
+ -11.164
+ ],
+ [
+ 105.208,
+ -8.053
+ ],
+ [
+ 105.208,
+ -2.975
+ ],
+ [
+ 99.871,
+ -2.975
+ ],
+ [
+ 96.783,
+ 0.045
+ ],
+ [
+ 99.871,
+ 3.065
+ ],
+ [
+ 105.208,
+ 3.065
+ ],
+ [
+ 105.208,
+ 8.142
+ ],
+ [
+ 108.391,
+ 11.253
+ ],
+ [
+ 111.575,
+ 8.142
+ ],
+ [
+ 111.575,
+ 3.065
+ ],
+ [
+ 116.911,
+ 3.065
+ ],
+ [
+ 120,
+ 0.045
+ ],
+ [
+ 116.911,
+ -2.975
+ ],
+ [
+ 111.575,
+ -2.975
+ ],
+ [
+ 111.575,
+ -8.053
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.086000001197,
+ 0.086000001197,
+ 0.086000001197,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 228.201,
+ 120
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 108.201,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 599,
+ "s": [
+ -360
+ ]
+ }
+ ],
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 4,
+ "nm": "Spinner_Outside Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 2,
+ "ty": "sh",
+ "ix": 3,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.327,
+ 0.823
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.935,
+ -0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.328,
+ -0.87
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.89,
+ 0.275
+ ],
+ [
+ 0.282,
+ 0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.891,
+ 0.275
+ ],
+ [
+ 0.281,
+ 0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.889,
+ 0.275
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.328,
+ -0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ 0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.328,
+ 0.823
+ ],
+ [
+ 0.843,
+ -0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.328,
+ 0.823
+ ],
+ [
+ 0.795,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.328,
+ 0.823
+ ],
+ [
+ 0.703,
+ -0.366
+ ]
+ ],
+ "v": [
+ [
+ 119.439,
+ -31.705
+ ],
+ [
+ 112.652,
+ -49.685
+ ],
+ [
+ 110.358,
+ -50.691
+ ],
+ [
+ 84.143,
+ -41.221
+ ],
+ [
+ 83.067,
+ -38.979
+ ],
+ [
+ 89.948,
+ -20.771
+ ],
+ [
+ 92.054,
+ -19.811
+ ],
+ [
+ 93.037,
+ -21.869
+ ],
+ [
+ 86.811,
+ -38.385
+ ],
+ [
+ 97.157,
+ -42.136
+ ],
+ [
+ 102.633,
+ -27.679
+ ],
+ [
+ 104.741,
+ -26.719
+ ],
+ [
+ 105.723,
+ -28.777
+ ],
+ [
+ 100.246,
+ -43.234
+ ],
+ [
+ 110.358,
+ -46.894
+ ],
+ [
+ 116.537,
+ -30.561
+ ],
+ [
+ 118.642,
+ -29.601
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 3",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 3,
+ "ty": "sh",
+ "ix": 4,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.561,
+ -0.778
+ ],
+ [
+ -0.842,
+ 0.549
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 0.503
+ ],
+ [
+ 0.515,
+ 0.687
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.749,
+ 0.503
+ ],
+ [
+ 0.516,
+ 0.686
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.795,
+ -0.595
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0.563,
+ 0.778
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.516,
+ 0.732
+ ],
+ [
+ 0.749,
+ -0.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.516,
+ 0.732
+ ],
+ [
+ 0.749,
+ -0.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.563,
+ -0.732
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.843,
+ 0.458
+ ]
+ ],
+ "v": [
+ [
+ 69.678,
+ -58.423
+ ],
+ [
+ 72.206,
+ -58.011
+ ],
+ [
+ 82.13,
+ -64.919
+ ],
+ [
+ 91.258,
+ -52.43
+ ],
+ [
+ 93.505,
+ -52.063
+ ],
+ [
+ 93.879,
+ -54.26
+ ],
+ [
+ 84.751,
+ -66.749
+ ],
+ [
+ 93.926,
+ -73.154
+ ],
+ [
+ 104.225,
+ -59.063
+ ],
+ [
+ 106.518,
+ -58.697
+ ],
+ [
+ 106.893,
+ -60.893
+ ],
+ [
+ 95.566,
+ -76.402
+ ],
+ [
+ 93.084,
+ -76.768
+ ],
+ [
+ 70.147,
+ -60.756
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 4",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 4,
+ "ty": "sh",
+ "ix": 5,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 54.606,
+ -85.781
+ ],
+ [
+ 66.402,
+ -77.5
+ ],
+ [
+ 69.959,
+ -94.427
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 5",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 5,
+ "ty": "sh",
+ "ix": 6,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.374,
+ 0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.641
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.235,
+ -1.007
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.14,
+ -0.184
+ ],
+ [
+ 0.749,
+ 0.549
+ ],
+ [
+ -0.189,
+ 0.777
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.655,
+ 0.458
+ ],
+ [
+ -0.516,
+ 0.686
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.938,
+ -0.549
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.888,
+ 0.641
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.094,
+ 0.321
+ ],
+ [
+ -0.562,
+ 0.731
+ ],
+ [
+ -0.702,
+ -0.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 0.412
+ ],
+ [
+ -0.749,
+ -0.503
+ ],
+ [
+ 0.093,
+ -0.183
+ ]
+ ],
+ "v": [
+ [
+ 42.856,
+ -82.944
+ ],
+ [
+ 69.631,
+ -97.813
+ ],
+ [
+ 72.393,
+ -97.767
+ ],
+ [
+ 72.535,
+ -97.676
+ ],
+ [
+ 73.423,
+ -95.159
+ ],
+ [
+ 67.478,
+ -65.697
+ ],
+ [
+ 67.151,
+ -64.873
+ ],
+ [
+ 64.763,
+ -64.507
+ ],
+ [
+ 64.109,
+ -66.566
+ ],
+ [
+ 65.7,
+ -74.206
+ ],
+ [
+ 51.657,
+ -84.088
+ ],
+ [
+ 44.681,
+ -80.154
+ ],
+ [
+ 42.576,
+ -80.154
+ ],
+ [
+ 42.201,
+ -82.35
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 6",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 6,
+ "ty": "sh",
+ "ix": 7,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -4.401,
+ -1.327
+ ],
+ [
+ -1.637,
+ 5.032
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.227,
+ 3.523
+ ],
+ [
+ -0.89,
+ 2.791
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.933,
+ -1.19
+ ],
+ [
+ -1.967,
+ -2.562
+ ],
+ [
+ -0.375,
+ -0.137
+ ],
+ [
+ -0.281,
+ 0.869
+ ],
+ [
+ 0.234,
+ 0.366
+ ],
+ [
+ 3.978,
+ 1.236
+ ],
+ [
+ 1.451,
+ -4.621
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.46,
+ -3.568
+ ],
+ [
+ 0.889,
+ -2.745
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 4.074,
+ 1.236
+ ],
+ [
+ 2.059,
+ 3.202
+ ],
+ [
+ 0.421,
+ 0.137
+ ],
+ [
+ 0.28,
+ -0.915
+ ],
+ [
+ -0.28,
+ -0.366
+ ]
+ ],
+ "o": [
+ [
+ 5.992,
+ 1.83
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.451,
+ -4.484
+ ],
+ [
+ -6.179,
+ -3.431
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.841,
+ -2.654
+ ],
+ [
+ 2.807,
+ 0.869
+ ],
+ [
+ 0.235,
+ 0.32
+ ],
+ [
+ 0.89,
+ 0.275
+ ],
+ [
+ 0.188,
+ -0.641
+ ],
+ [
+ -2.152,
+ -2.79
+ ],
+ [
+ -5.711,
+ -1.738
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.545,
+ 4.85
+ ],
+ [
+ 5.899,
+ 3.248
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ 2.882
+ ],
+ [
+ -3.837,
+ -1.189
+ ],
+ [
+ -0.141,
+ -0.275
+ ],
+ [
+ -0.891,
+ -0.275
+ ],
+ [
+ -0.189,
+ 0.549
+ ],
+ [
+ 2.388,
+ 3.706
+ ]
+ ],
+ "v": [
+ [
+ 28.532,
+ -84.454
+ ],
+ [
+ 41.498,
+ -89.623
+ ],
+ [
+ 41.545,
+ -89.715
+ ],
+ [
+ 34.429,
+ -101.061
+ ],
+ [
+ 28.111,
+ -109.296
+ ],
+ [
+ 28.158,
+ -109.387
+ ],
+ [
+ 36.116,
+ -112.178
+ ],
+ [
+ 43.09,
+ -107.237
+ ],
+ [
+ 43.979,
+ -106.643
+ ],
+ [
+ 46.132,
+ -107.74
+ ],
+ [
+ 45.851,
+ -109.296
+ ],
+ [
+ 37.192,
+ -115.152
+ ],
+ [
+ 24.741,
+ -110.165
+ ],
+ [
+ 24.694,
+ -110.074
+ ],
+ [
+ 32.089,
+ -98.499
+ ],
+ [
+ 38.128,
+ -90.493
+ ],
+ [
+ 38.081,
+ -90.401
+ ],
+ [
+ 29.748,
+ -87.474
+ ],
+ [
+ 21.37,
+ -93.878
+ ],
+ [
+ 20.481,
+ -94.564
+ ],
+ [
+ 18.328,
+ -93.421
+ ],
+ [
+ 18.561,
+ -91.957
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 7",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 7,
+ "ty": "sh",
+ "ix": 8,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.094,
+ 5.49
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 5.664,
+ -0.137
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.094,
+ -5.444
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 5.664,
+ -0.092
+ ]
+ ],
+ "v": [
+ [
+ 6.39,
+ -104.675
+ ],
+ [
+ 6.39,
+ -104.767
+ ],
+ [
+ -3.299,
+ -113.871
+ ],
+ [
+ -8.683,
+ -113.779
+ ],
+ [
+ -8.309,
+ -95.205
+ ],
+ [
+ -2.925,
+ -95.296
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 8",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 8,
+ "ty": "sh",
+ "ix": 9,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.187,
+ -8.646
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.924,
+ -0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.046,
+ 1.922
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.966,
+ 0.046
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.188,
+ 8.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.92,
+ 0.046
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.047,
+ -1.876
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.925,
+ -0.183
+ ]
+ ],
+ "v": [
+ [
+ 13.645,
+ -104.996
+ ],
+ [
+ 13.645,
+ -104.904
+ ],
+ [
+ -2.832,
+ -89.166
+ ],
+ [
+ -11.679,
+ -88.983
+ ],
+ [
+ -15.236,
+ -92.323
+ ],
+ [
+ -15.705,
+ -116.387
+ ],
+ [
+ -12.288,
+ -119.864
+ ],
+ [
+ -3.441,
+ -120.001
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 9",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 9,
+ "ty": "sh",
+ "ix": 10,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.872,
+ -0.549
+ ],
+ [
+ -0.561,
+ -1.83
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.872,
+ 0.549
+ ],
+ [
+ 0.562,
+ 1.83
+ ]
+ ],
+ "o": [
+ [
+ -0.562,
+ -1.83
+ ],
+ [
+ -1.872,
+ 0.549
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.562,
+ 1.83
+ ],
+ [
+ 1.873,
+ -0.549
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ -29.888,
+ -113.185
+ ],
+ [
+ -34.195,
+ -115.472
+ ],
+ [
+ -36.536,
+ -111.263
+ ],
+ [
+ -29.467,
+ -87.657
+ ],
+ [
+ -25.161,
+ -85.369
+ ],
+ [
+ -22.82,
+ -89.578
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 10",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 10,
+ "ty": "sh",
+ "ix": 11,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.467,
+ -0.32
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.638,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.188,
+ 1.83
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.281,
+ 0.458
+ ],
+ [
+ 1.638,
+ -0.915
+ ],
+ [
+ -0.188,
+ -1.143
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.545,
+ -0.869
+ ],
+ [
+ -0.983,
+ -1.647
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.497,
+ 1.099
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.639,
+ -0.915
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.047,
+ -0.411
+ ],
+ [
+ -0.936,
+ -1.601
+ ],
+ [
+ -1.452,
+ 0.823
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.171,
+ -0.915
+ ],
+ [
+ -1.685,
+ 0.961
+ ],
+ [
+ 0.281,
+ 0.595
+ ]
+ ],
+ "v": [
+ [
+ -70.521,
+ -93.238
+ ],
+ [
+ -49.502,
+ -77.409
+ ],
+ [
+ -44.728,
+ -76.997
+ ],
+ [
+ -44.353,
+ -77.226
+ ],
+ [
+ -42.294,
+ -81.48
+ ],
+ [
+ -45.57,
+ -107.466
+ ],
+ [
+ -46.038,
+ -108.93
+ ],
+ [
+ -50.672,
+ -110.119
+ ],
+ [
+ -52.451,
+ -106.643
+ ],
+ [
+ -49.128,
+ -85.232
+ ],
+ [
+ -66.12,
+ -98.728
+ ],
+ [
+ -70.38,
+ -99.094
+ ],
+ [
+ -71.691,
+ -94.519
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 11",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 11,
+ "ty": "sh",
+ "ix": 12,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 4.026,
+ 3.523
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 3.698,
+ -4.071
+ ],
+ [
+ -4.025,
+ -3.522
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.698,
+ 4.072
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -4.026,
+ -3.523
+ ],
+ [
+ -3.698,
+ 4.072
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 4.026,
+ 3.523
+ ],
+ [
+ 3.745,
+ -4.117
+ ]
+ ],
+ "v": [
+ [
+ -73.095,
+ -76.448
+ ],
+ [
+ -73.142,
+ -76.494
+ ],
+ [
+ -86.904,
+ -75.991
+ ],
+ [
+ -85.875,
+ -62.678
+ ],
+ [
+ -85.828,
+ -62.632
+ ],
+ [
+ -72.065,
+ -63.135
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 12",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 12,
+ "ty": "sh",
+ "ix": 13,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 6.506,
+ -7.137
+ ],
+ [
+ 6.741,
+ 5.856
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.507,
+ 7.091
+ ],
+ [
+ -6.741,
+ -5.856
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -6.46,
+ 7.091
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.694,
+ -5.856
+ ],
+ [
+ 6.506,
+ -7.091
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.694,
+ 5.81
+ ]
+ ],
+ "v": [
+ [
+ -67.337,
+ -58.926
+ ],
+ [
+ -90.602,
+ -57.279
+ ],
+ [
+ -90.649,
+ -57.325
+ ],
+ [
+ -91.632,
+ -80.199
+ ],
+ [
+ -68.367,
+ -81.846
+ ],
+ [
+ -68.32,
+ -81.801
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 13",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 13,
+ "ty": "sh",
+ "ix": 14,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -8.332,
+ -3.248
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.418,
+ 8.463
+ ],
+ [
+ 1.217,
+ 3.477
+ ],
+ [
+ 0.982,
+ 0.412
+ ],
+ [
+ 0.656,
+ -1.601
+ ],
+ [
+ -0.235,
+ -0.64
+ ],
+ [
+ 1.077,
+ -2.699
+ ],
+ [
+ 5.009,
+ 1.922
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.966,
+ 4.85
+ ],
+ [
+ -2.06,
+ 1.236
+ ],
+ [
+ -0.375,
+ 0.869
+ ],
+ [
+ 1.732,
+ 0.686
+ ],
+ [
+ 0.749,
+ -0.458
+ ],
+ [
+ 1.779,
+ -4.301
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 8.426,
+ 3.248
+ ],
+ [
+ 1.872,
+ -4.575
+ ],
+ [
+ -0.281,
+ -0.778
+ ],
+ [
+ -1.639,
+ -0.64
+ ],
+ [
+ -0.281,
+ 0.732
+ ],
+ [
+ 0.749,
+ 2.379
+ ],
+ [
+ -2.012,
+ 4.896
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -5.009,
+ -1.968
+ ],
+ [
+ 0.936,
+ -2.241
+ ],
+ [
+ 0.468,
+ -0.32
+ ],
+ [
+ 0.702,
+ -1.739
+ ],
+ [
+ -1.124,
+ -0.457
+ ],
+ [
+ -2.762,
+ 1.692
+ ],
+ [
+ -3.651,
+ 8.829
+ ]
+ ],
+ "v": [
+ [
+ -104.365,
+ -25.575
+ ],
+ [
+ -104.271,
+ -25.529
+ ],
+ [
+ -83.206,
+ -34.404
+ ],
+ [
+ -82.738,
+ -46.025
+ ],
+ [
+ -84.61,
+ -47.992
+ ],
+ [
+ -88.777,
+ -46.208
+ ],
+ [
+ -88.823,
+ -44.058
+ ],
+ [
+ -89.058,
+ -36.921
+ ],
+ [
+ -101.65,
+ -32.163
+ ],
+ [
+ -101.743,
+ -32.208
+ ],
+ [
+ -107.501,
+ -44.058
+ ],
+ [
+ -103.007,
+ -49.09
+ ],
+ [
+ -101.603,
+ -50.737
+ ],
+ [
+ -103.475,
+ -55.083
+ ],
+ [
+ -106.518,
+ -54.854
+ ],
+ [
+ -113.399,
+ -46.436
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 14",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 14,
+ "ty": "sh",
+ "ix": 15,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.59,
+ 0.595
+ ],
+ [
+ 0.609,
+ -1.555
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.592,
+ 0.641
+ ],
+ [
+ 0.608,
+ -1.556
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.592,
+ 0.64
+ ],
+ [
+ 0.607,
+ -1.556
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.778,
+ -0.686
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 1.738
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.592,
+ -0.595
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.608,
+ -1.556
+ ],
+ [
+ -1.59,
+ -0.594
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.608,
+ -1.555
+ ],
+ [
+ -1.591,
+ -0.594
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 1.784
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.826,
+ 0.686
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.655,
+ -1.601
+ ]
+ ],
+ "v": [
+ [
+ 116.865,
+ 30.651
+ ],
+ [
+ 112.838,
+ 32.389
+ ],
+ [
+ 107.642,
+ 45.428
+ ],
+ [
+ 101.463,
+ 43.095
+ ],
+ [
+ 105.817,
+ 32.115
+ ],
+ [
+ 104.084,
+ 28.18
+ ],
+ [
+ 100.059,
+ 29.919
+ ],
+ [
+ 95.705,
+ 40.899
+ ],
+ [
+ 89.76,
+ 38.611
+ ],
+ [
+ 94.863,
+ 25.801
+ ],
+ [
+ 93.13,
+ 21.867
+ ],
+ [
+ 89.106,
+ 23.606
+ ],
+ [
+ 82.739,
+ 39.526
+ ],
+ [
+ 84.704,
+ 43.918
+ ],
+ [
+ 107.642,
+ 52.656
+ ],
+ [
+ 112.136,
+ 50.735
+ ],
+ [
+ 118.549,
+ 34.585
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 15",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 15,
+ "ty": "sh",
+ "ix": 16,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.359,
+ 1.052
+ ],
+ [
+ 1.124,
+ -1.327
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.358,
+ 1.098
+ ],
+ [
+ 1.124,
+ -1.326
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.498,
+ -1.191
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.217,
+ 1.464
+ ],
+ [
+ 1.498,
+ 1.19
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.357,
+ -1.098
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.124,
+ -1.327
+ ],
+ [
+ -1.357,
+ -1.098
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.218,
+ 1.464
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.498,
+ 1.19
+ ],
+ [
+ 1.216,
+ -1.464
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.123,
+ -1.327
+ ]
+ ],
+ "v": [
+ [
+ 91.164,
+ 57.506
+ ],
+ [
+ 86.717,
+ 57.963
+ ],
+ [
+ 79.04,
+ 67.204
+ ],
+ [
+ 73.798,
+ 63.087
+ ],
+ [
+ 82.739,
+ 52.29
+ ],
+ [
+ 82.27,
+ 47.944
+ ],
+ [
+ 77.824,
+ 48.401
+ ],
+ [
+ 66.777,
+ 61.806
+ ],
+ [
+ 67.291,
+ 66.565
+ ],
+ [
+ 86.624,
+ 81.844
+ ],
+ [
+ 91.493,
+ 81.341
+ ],
+ [
+ 90.977,
+ 76.583
+ ],
+ [
+ 83.956,
+ 71.047
+ ],
+ [
+ 91.633,
+ 61.806
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 16",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 16,
+ "ty": "sh",
+ "ix": 17,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 65.607,
+ 86.373
+ ],
+ [
+ 56.151,
+ 80.38
+ ],
+ [
+ 58.398,
+ 91.223
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 17",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 17,
+ "ty": "sh",
+ "ix": 18,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.233,
+ -0.366
+ ],
+ [
+ 1.545,
+ -1.007
+ ],
+ [
+ 1.358,
+ 0.824
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.216,
+ -0.823
+ ],
+ [
+ 1.031,
+ 1.464
+ ],
+ [
+ 0.093,
+ 0.503
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.593,
+ 1.007
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.592,
+ -0.961
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 1.03,
+ 1.51
+ ],
+ [
+ -1.357,
+ 0.869
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.28,
+ 1.419
+ ],
+ [
+ -1.499,
+ 1.007
+ ],
+ [
+ -0.281,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.329,
+ -1.784
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.546,
+ -1.053
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.516,
+ 0.275
+ ]
+ ],
+ "v": [
+ [
+ 80.632,
+ 88.34
+ ],
+ [
+ 79.789,
+ 92.824
+ ],
+ [
+ 75.67,
+ 92.732
+ ],
+ [
+ 71.129,
+ 89.897
+ ],
+ [
+ 59.662,
+ 97.536
+ ],
+ [
+ 60.738,
+ 102.934
+ ],
+ [
+ 59.287,
+ 106.457
+ ],
+ [
+ 54.792,
+ 105.588
+ ],
+ [
+ 54.278,
+ 104.17
+ ],
+ [
+ 49.41,
+ 78.047
+ ],
+ [
+ 51.236,
+ 73.609
+ ],
+ [
+ 51.562,
+ 73.381
+ ],
+ [
+ 56.431,
+ 73.381
+ ],
+ [
+ 79.508,
+ 87.425
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 18",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 18,
+ "ty": "sh",
+ "ix": 19,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.6,
+ -0.321
+ ],
+ [
+ 0.608,
+ 1.922
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.527,
+ 0.778
+ ],
+ [
+ -2.621,
+ -0.595
+ ],
+ [
+ -0.657,
+ 0.183
+ ],
+ [
+ 0.516,
+ 1.693
+ ],
+ [
+ 0.891,
+ 0.183
+ ],
+ [
+ 3.652,
+ -1.144
+ ],
+ [
+ -1.686,
+ -5.353
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.554,
+ 0.366
+ ],
+ [
+ -0.516,
+ -1.601
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.714,
+ -0.823
+ ],
+ [
+ 2.81,
+ 0.961
+ ],
+ [
+ 0.841,
+ -0.275
+ ],
+ [
+ -0.514,
+ -1.692
+ ],
+ [
+ -0.889,
+ -0.275
+ ],
+ [
+ -3.978,
+ 1.19
+ ],
+ [
+ 1.872,
+ 5.856
+ ]
+ ],
+ "o": [
+ [
+ -1.639,
+ -5.124
+ ],
+ [
+ -5.617,
+ 0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.468,
+ -1.418
+ ],
+ [
+ 2.06,
+ -0.64
+ ],
+ [
+ 0.61,
+ 0.137
+ ],
+ [
+ 1.731,
+ -0.503
+ ],
+ [
+ -0.421,
+ -1.281
+ ],
+ [
+ -3.229,
+ -0.87
+ ],
+ [
+ -6.131,
+ 1.875
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.873,
+ 5.856
+ ],
+ [
+ 5.429,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.561,
+ 1.738
+ ],
+ [
+ -2.95,
+ 0.869
+ ],
+ [
+ -0.514,
+ -0.183
+ ],
+ [
+ -1.733,
+ 0.503
+ ],
+ [
+ 0.328,
+ 1.007
+ ],
+ [
+ 4.12,
+ 1.327
+ ],
+ [
+ 6.507,
+ -1.967
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 45.899,
+ 102.34
+ ],
+ [
+ 33.54,
+ 96.439
+ ],
+ [
+ 25.817,
+ 94.562
+ ],
+ [
+ 25.769,
+ 94.471
+ ],
+ [
+ 28.812,
+ 90.719
+ ],
+ [
+ 35.787,
+ 90.765
+ ],
+ [
+ 37.614,
+ 90.719
+ ],
+ [
+ 39.766,
+ 86.785
+ ],
+ [
+ 37.472,
+ 84.681
+ ],
+ [
+ 27.08,
+ 85.001
+ ],
+ [
+ 19.357,
+ 97.079
+ ],
+ [
+ 19.404,
+ 97.17
+ ],
+ [
+ 32.277,
+ 103.117
+ ],
+ [
+ 39.533,
+ 104.993
+ ],
+ [
+ 39.579,
+ 105.085
+ ],
+ [
+ 36.116,
+ 109.202
+ ],
+ [
+ 27.548,
+ 108.882
+ ],
+ [
+ 25.49,
+ 108.882
+ ],
+ [
+ 23.335,
+ 112.816
+ ],
+ [
+ 25.348,
+ 114.875
+ ],
+ [
+ 37.8,
+ 114.966
+ ],
+ [
+ 45.946,
+ 102.477
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 19",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 19,
+ "ty": "sh",
+ "ix": 20,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 7.959,
+ 0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.187,
+ 6.817
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 7.958,
+ 0.183
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.141,
+ -6.817
+ ]
+ ],
+ "v": [
+ [
+ -4.143,
+ 92.412
+ ],
+ [
+ -11.539,
+ 92.229
+ ],
+ [
+ -12.147,
+ 116.613
+ ],
+ [
+ -4.751,
+ 116.796
+ ],
+ [
+ 8.637,
+ 104.993
+ ],
+ [
+ 8.637,
+ 104.902
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 20",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 20,
+ "ty": "sh",
+ "ix": 21,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.235,
+ -8.647
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.925,
+ 0.229
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.983,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.234,
+ 8.692
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.046
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.048,
+ -0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 9.924,
+ 0.275
+ ]
+ ],
+ "v": [
+ [
+ 12.288,
+ 104.902
+ ],
+ [
+ 12.288,
+ 104.993
+ ],
+ [
+ -4.892,
+ 119.999
+ ],
+ [
+ -14.067,
+ 119.77
+ ],
+ [
+ -15.798,
+ 117.986
+ ],
+ [
+ -15.097,
+ 90.674
+ ],
+ [
+ -13.271,
+ 88.935
+ ],
+ [
+ -4.096,
+ 89.164
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 21",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 21,
+ "ty": "sh",
+ "ix": 22,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0.936,
+ 0.275
+ ],
+ [
+ 0.28,
+ -0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.229
+ ],
+ [
+ -0.281,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -0.89,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.281,
+ 0.915
+ ],
+ [
+ 0.936,
+ 0.275
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.233,
+ -0.869
+ ]
+ ],
+ "v": [
+ [
+ -24.458,
+ 85.641
+ ],
+ [
+ -26.658,
+ 86.877
+ ],
+ [
+ -34.429,
+ 113.594
+ ],
+ [
+ -33.212,
+ 115.744
+ ],
+ [
+ -30.965,
+ 114.509
+ ],
+ [
+ -23.194,
+ 87.791
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 22",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 22,
+ "ty": "sh",
+ "ix": 23,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -0.141,
+ 0.229
+ ],
+ [
+ 0.843,
+ 0.457
+ ],
+ [
+ 0.608,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.796,
+ 0.457
+ ],
+ [
+ 0.421,
+ -0.732
+ ],
+ [
+ 0,
+ -0.274
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.936,
+ -0.503
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.796,
+ 0.549
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0.467,
+ -0.777
+ ],
+ [
+ -0.702,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.093,
+ -0.823
+ ],
+ [
+ -0.842,
+ -0.504
+ ],
+ [
+ -0.14,
+ 0.275
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.14,
+ 0.961
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.889,
+ 0.503
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.14,
+ -0.275
+ ]
+ ],
+ "v": [
+ [
+ -33.071,
+ 85.824
+ ],
+ [
+ -33.727,
+ 83.537
+ ],
+ [
+ -35.88,
+ 83.766
+ ],
+ [
+ -58.443,
+ 100.693
+ ],
+ [
+ -54.885,
+ 73.243
+ ],
+ [
+ -55.822,
+ 71.185
+ ],
+ [
+ -58.256,
+ 71.779
+ ],
+ [
+ -58.49,
+ 72.557
+ ],
+ [
+ -61.907,
+ 102.294
+ ],
+ [
+ -60.831,
+ 104.581
+ ],
+ [
+ -60.69,
+ 104.673
+ ],
+ [
+ -58.162,
+ 104.444
+ ],
+ [
+ -33.493,
+ 86.465
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 23",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 23,
+ "ty": "sh",
+ "ix": 24,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 4.775,
+ 5.307
+ ],
+ [
+ 5.383,
+ -4.62
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -4.775,
+ -5.261
+ ],
+ [
+ -5.384,
+ 4.621
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -4.775,
+ -5.307
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -5.384,
+ 4.621
+ ],
+ [
+ 4.775,
+ 5.307
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 5.384,
+ -4.621
+ ]
+ ],
+ "v": [
+ [
+ -70.146,
+ 60.754
+ ],
+ [
+ -88.028,
+ 60.067
+ ],
+ [
+ -88.074,
+ 60.113
+ ],
+ [
+ -89.619,
+ 77.635
+ ],
+ [
+ -71.737,
+ 78.321
+ ],
+ [
+ -71.691,
+ 78.276
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 24",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 24,
+ "ty": "sh",
+ "ix": 25,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -6.366,
+ -7.045
+ ],
+ [
+ 6.507,
+ -5.581
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.366,
+ 7.045
+ ],
+ [
+ -6.507,
+ 5.627
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 6.366,
+ 7.045
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.506,
+ 5.627
+ ],
+ [
+ -6.366,
+ -7.045
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 6.553,
+ -5.581
+ ]
+ ],
+ "v": [
+ [
+ -67.618,
+ 58.695
+ ],
+ [
+ -69.163,
+ 80.929
+ ],
+ [
+ -69.21,
+ 80.975
+ ],
+ [
+ -92.147,
+ 79.694
+ ],
+ [
+ -90.602,
+ 57.414
+ ],
+ [
+ -90.555,
+ 57.368
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 25",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 25,
+ "ty": "sh",
+ "ix": 26,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -2.528,
+ -6.314
+ ],
+ [
+ -3.324,
+ -1.555
+ ],
+ [
+ -0.187,
+ -0.412
+ ],
+ [
+ 0.843,
+ -0.32
+ ],
+ [
+ 0.374,
+ 0.182
+ ],
+ [
+ 1.873,
+ 4.712
+ ],
+ [
+ -8.426,
+ 3.249
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.418,
+ -8.463
+ ],
+ [
+ 1.17,
+ -3.524
+ ],
+ [
+ 0.608,
+ -0.228
+ ],
+ [
+ 0.327,
+ 0.869
+ ],
+ [
+ -0.094,
+ 0.274
+ ],
+ [
+ 1.451,
+ 3.523
+ ],
+ [
+ 6.741,
+ -2.608
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 1.451,
+ 3.568
+ ],
+ [
+ 0.327,
+ 0.138
+ ],
+ [
+ 0.327,
+ 0.823
+ ],
+ [
+ -0.515,
+ 0.183
+ ],
+ [
+ -3.792,
+ -1.831
+ ],
+ [
+ -3.37,
+ -8.281
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 8.285,
+ -3.203
+ ],
+ [
+ 1.872,
+ 4.621
+ ],
+ [
+ -0.14,
+ 0.456
+ ],
+ [
+ -0.89,
+ 0.321
+ ],
+ [
+ -0.187,
+ -0.458
+ ],
+ [
+ 1.123,
+ -2.974
+ ],
+ [
+ -2.575,
+ -6.359
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -6.788,
+ 2.562
+ ]
+ ],
+ "v": [
+ [
+ -110.45,
+ 45.291
+ ],
+ [
+ -103.428,
+ 52.427
+ ],
+ [
+ -102.539,
+ 53.297
+ ],
+ [
+ -103.522,
+ 55.447
+ ],
+ [
+ -104.926,
+ 55.356
+ ],
+ [
+ -113.54,
+ 46.389
+ ],
+ [
+ -104.271,
+ 26.167
+ ],
+ [
+ -104.177,
+ 26.122
+ ],
+ [
+ -83.112,
+ 34.768
+ ],
+ [
+ -82.785,
+ 46.481
+ ],
+ [
+ -83.861,
+ 47.669
+ ],
+ [
+ -86.155,
+ 46.663
+ ],
+ [
+ -86.202,
+ 45.474
+ ],
+ [
+ -86.202,
+ 35.958
+ ],
+ [
+ -102.726,
+ 29.507
+ ],
+ [
+ -102.82,
+ 29.553
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 26",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "mm",
+ "mm": 1,
+ "nm": "Merge Paths 1",
+ "mn": "ADBE Vector Filter - Merge",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.086000001197,
+ 0.086000001197,
+ 0.086000001197,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 120,
+ 120
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 30,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ }
+ ]
+ }
+ ],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "Spinner_Inside2 Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 130,
+ 130,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.685,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.17,
+ 1.235
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.575,
+ 2.333
+ ],
+ [
+ -2.388,
+ -2.516
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.48,
+ -2.379
+ ],
+ [
+ 2.435,
+ -2.425
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.732,
+ -0.046
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.387,
+ -2.47
+ ],
+ [
+ 2.528,
+ -2.334
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.433,
+ -2.425
+ ],
+ [
+ 2.482,
+ 2.379
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.217,
+ 1.143
+ ]
+ ],
+ "v": [
+ [
+ -21.112,
+ 38.727
+ ],
+ [
+ -21.206,
+ 38.727
+ ],
+ [
+ -25.699,
+ 36.76
+ ],
+ [
+ -48.918,
+ 12.467
+ ],
+ [
+ -48.59,
+ 3.775
+ ],
+ [
+ -39.696,
+ 4.095
+ ],
+ [
+ -20.925,
+ 23.721
+ ],
+ [
+ 39.93,
+ -36.301
+ ],
+ [
+ 48.823,
+ -36.347
+ ],
+ [
+ 48.87,
+ -27.656
+ ],
+ [
+ -16.618,
+ 36.943
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0,
+ 0.501999978458,
+ 0.090000002992,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 138.056,
+ 111.632
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -1.731,
+ 2.974
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 3.511,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.733,
+ -2.927
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.732,
+ -2.973
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -3.464,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.778,
+ 2.974
+ ]
+ ],
+ "v": [
+ [
+ 42.528,
+ 58.17
+ ],
+ [
+ 45.946,
+ 52.359
+ ],
+ [
+ 42.012,
+ 45.726
+ ],
+ [
+ 35.178,
+ 45.726
+ ],
+ [
+ 31.247,
+ 52.359
+ ],
+ [
+ 34.665,
+ 58.17
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 1,
+ "ty": "sh",
+ "ix": 2,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -4.072,
+ 3.705
+ ],
+ [
+ -3.792,
+ -3.934
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.655,
+ -2.607
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.639,
+ 0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.421,
+ 1.601
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.966,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.562,
+ -0.458
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.03,
+ -2.333
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -4.727,
+ 2.837
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -3.791,
+ -3.98
+ ],
+ [
+ 4.073,
+ -3.706
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.31,
+ -2.379
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.421,
+ 1.602
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.638,
+ -0.412
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.468,
+ -1.875
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.702,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.013,
+ 1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.153,
+ 4.986
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ -33.4,
+ 6.656
+ ],
+ [
+ -32.885,
+ -7.251
+ ],
+ [
+ -18.654,
+ -6.794
+ ],
+ [
+ -2.598,
+ 10.042
+ ],
+ [
+ 47.021,
+ -38.91
+ ],
+ [
+ 35.975,
+ -58.765
+ ],
+ [
+ 30.405,
+ -58.079
+ ],
+ [
+ 25.958,
+ -40.557
+ ],
+ [
+ 22.306,
+ -38.407
+ ],
+ [
+ 6.765,
+ -42.158
+ ],
+ [
+ 4.564,
+ -45.726
+ ],
+ [
+ 6.062,
+ -51.674
+ ],
+ [
+ 3.114,
+ -55.333
+ ],
+ [
+ -12.241,
+ -55.333
+ ],
+ [
+ -14.206,
+ -54.601
+ ],
+ [
+ -66.4,
+ -10.866
+ ],
+ [
+ -68.04,
+ -4.095
+ ],
+ [
+ -54.37,
+ 27.381
+ ],
+ [
+ -41.311,
+ 31.452
+ ],
+ [
+ -21.229,
+ 19.42
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 2",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 2,
+ "ty": "sh",
+ "ix": 3,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.842,
+ -1.693
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.294,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.123,
+ 1.189
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0.935,
+ 1.647
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.982,
+ 2.058
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.686,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 68.088,
+ -0.984
+ ],
+ [
+ 68.228,
+ 4.369
+ ],
+ [
+ 51.516,
+ 38.681
+ ],
+ [
+ 46.087,
+ 42.066
+ ],
+ [
+ 30.546,
+ 42.066
+ ],
+ [
+ 26.145,
+ 40.191
+ ],
+ [
+ 11.166,
+ 24.499
+ ],
+ [
+ 57.088,
+ -20.793
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 3",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "mm",
+ "mm": 1,
+ "nm": "Merge Paths 1",
+ "mn": "ADBE Vector Filter - Merge",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0,
+ 0.635000011968,
+ 0.097999999102,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 119.775,
+ 119.958
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 2",
+ "np": 5,
+ "cix": 2,
+ "bm": 0,
+ "ix": 2,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 0,
+ "nm": "Spinner_Outside Outlines Comp 1",
+ "refId": "comp_0",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 599,
+ "s": [
+ 360
+ ]
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 130,
+ 130,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 120,
+ 120,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "w": 240,
+ "h": 240,
+ "ip": 0,
+ "op": 600,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
\ No newline at end of file
diff --git a/app/src/main/assets/spinner_home_upload_complete.json b/app/src/main/assets/spinner_home_upload_complete.json
new file mode 100644
index 0000000..2edead2
--- /dev/null
+++ b/app/src/main/assets/spinner_home_upload_complete.json
@@ -0,0 +1 @@
+{"v":"5.6.9","fr":30,"ip":0,"op":600,"w":260,"h":260,"nm":"Logo_Inside","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Logo_Outside Outlines 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":599,"s":[-360]}],"ix":10},"p":{"a":0,"k":[11.5,120,0],"ix":2},"a":{"a":0,"k":[11.5,120,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.732,0],[0,1.692],[0,0],[0,0],[0,1.647],[1.685,0],[0,0],[0,0],[1.732,0],[0,-1.739],[0,0],[0,0],[0,-1.647],[-1.685,0],[0,0]],"o":[[0,1.737],[1.733,0],[0,0],[0,0],[1.732,0],[0,-1.692],[0,0],[0,0],[0,-1.739],[-1.732,0],[0,0],[0,0],[-1.732,0],[0,1.693],[0,0],[0,0]],"v":[[-111.574,8.189],[-108.391,11.299],[-105.207,8.189],[-105.207,3.11],[-99.871,3.11],[-96.781,0.09],[-99.871,-2.929],[-105.161,-2.929],[-105.161,-8.007],[-108.344,-11.118],[-111.574,-8.007],[-111.574,-2.929],[-116.91,-2.929],[-120,0.09],[-116.91,3.11],[-111.574,3.11]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[120,120],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Logo_Outside Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":599,"s":[-360]}],"ix":10},"p":{"a":0,"k":[228,120,0],"ix":2},"a":{"a":0,"k":[228,120,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.779,0],[0,-1.693],[0,0],[0,0],[0,-1.647],[-1.685,0],[0,0],[0,0],[-1.731,0],[0,1.693],[0,0],[0,0],[0,1.647],[1.685,0],[0,0],[0,0]],"o":[[-1.778,0],[0,0],[0,0],[-1.732,0],[0,1.692],[0,0],[0,0],[0,1.739],[1.779,0],[0,0],[0,0],[1.731,0],[0,-1.693],[0,0],[0,0],[0,-1.693]],"v":[[108.391,-11.164],[105.208,-8.053],[105.208,-2.975],[99.871,-2.975],[96.783,0.045],[99.871,3.065],[105.208,3.065],[105.208,8.142],[108.391,11.253],[111.575,8.142],[111.575,3.065],[116.911,3.065],[120,0.045],[116.911,-2.975],[111.575,-2.975],[111.575,-8.053]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[120,120],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Logo_Outside Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":360,"ix":10},"p":{"a":0,"k":[120,120,0],"ix":2},"a":{"a":0,"k":[120,120,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.327,0.823],[0,0],[0.935,-0.32],[0,0],[-0.328,-0.87],[0,0],[-0.89,0.275],[0.282,0.869],[0,0],[0,0],[0,0],[-0.891,0.275],[0.281,0.869],[0,0],[0,0],[0,0],[-0.889,0.275]],"o":[[0,0],[-0.328,-0.869],[0,0],[-0.936,0.32],[0,0],[0.328,0.823],[0.843,-0.32],[0,0],[0,0],[0,0],[0.328,0.823],[0.795,-0.274],[0,0],[0,0],[0,0],[0.328,0.823],[0.703,-0.366]],"v":[[119.439,-31.705],[112.652,-49.685],[110.358,-50.691],[84.143,-41.221],[83.067,-38.979],[89.948,-20.771],[92.054,-19.811],[93.037,-21.869],[86.811,-38.385],[97.157,-42.136],[102.633,-27.679],[104.741,-26.719],[105.723,-28.777],[100.246,-43.234],[110.358,-46.894],[116.537,-30.561],[118.642,-29.601]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.561,-0.778],[-0.842,0.549],[0,0],[0,0],[-0.702,0.503],[0.515,0.687],[0,0],[0,0],[0,0],[-0.749,0.503],[0.516,0.686],[0,0],[0.795,-0.595],[0,0]],"o":[[0.563,0.778],[0,0],[0,0],[0.516,0.732],[0.749,-0.504],[0,0],[0,0],[0,0],[0.516,0.732],[0.749,-0.504],[0,0],[-0.563,-0.732],[0,0],[-0.843,0.458]],"v":[[69.678,-58.423],[72.206,-58.011],[82.13,-64.919],[91.258,-52.43],[93.505,-52.063],[93.879,-54.26],[84.751,-66.749],[93.926,-73.154],[104.225,-59.063],[106.518,-58.697],[106.893,-60.893],[95.566,-76.402],[93.084,-76.768],[70.147,-60.756]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[54.606,-85.781],[66.402,-77.5],[69.959,-94.427]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-0.374,0.183],[0,0],[-0.936,-0.641],[0,0],[0.235,-1.007],[0,0],[0.14,-0.184],[0.749,0.549],[-0.189,0.777],[0,0],[0,0],[0,0],[0.655,0.458],[-0.516,0.686]],"o":[[0,0],[0.938,-0.549],[0,0],[0.888,0.641],[0,0],[-0.094,0.321],[-0.562,0.731],[-0.702,-0.504],[0,0],[0,0],[0,0],[-0.702,0.412],[-0.749,-0.503],[0.093,-0.183]],"v":[[42.856,-82.944],[69.631,-97.813],[72.393,-97.767],[72.535,-97.676],[73.423,-95.159],[67.478,-65.697],[67.151,-64.873],[64.763,-64.507],[64.109,-66.566],[65.7,-74.206],[51.657,-84.088],[44.681,-80.154],[42.576,-80.154],[42.201,-82.35]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[-4.401,-1.327],[-1.637,5.032],[0,0],[6.227,3.523],[-0.89,2.791],[0,0],[-3.933,-1.19],[-1.967,-2.562],[-0.375,-0.137],[-0.281,0.869],[0.234,0.366],[3.978,1.236],[1.451,-4.621],[0,0],[-6.46,-3.568],[0.889,-2.745],[0,0],[4.074,1.236],[2.059,3.202],[0.421,0.137],[0.28,-0.915],[-0.28,-0.366]],"o":[[5.992,1.83],[0,0],[1.451,-4.484],[-6.179,-3.431],[0,0],[0.841,-2.654],[2.807,0.869],[0.235,0.32],[0.89,0.275],[0.188,-0.641],[-2.152,-2.79],[-5.711,-1.738],[0,0],[-1.545,4.85],[5.899,3.248],[0,0],[-0.936,2.882],[-3.837,-1.189],[-0.141,-0.275],[-0.891,-0.275],[-0.189,0.549],[2.388,3.706]],"v":[[28.532,-84.454],[41.498,-89.623],[41.545,-89.715],[34.429,-101.061],[28.111,-109.296],[28.158,-109.387],[36.116,-112.178],[43.09,-107.237],[43.979,-106.643],[46.132,-107.74],[45.851,-109.296],[37.192,-115.152],[24.741,-110.165],[24.694,-110.074],[32.089,-98.499],[38.128,-90.493],[38.081,-90.401],[29.748,-87.474],[21.37,-93.878],[20.481,-94.564],[18.328,-93.421],[18.561,-91.957]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0.094,5.49],[0,0],[5.664,-0.137],[0,0],[0,0],[0,0]],"o":[[0,0],[-0.094,-5.444],[0,0],[0,0],[0,0],[5.664,-0.092]],"v":[[6.39,-104.675],[6.39,-104.767],[-3.299,-113.871],[-8.683,-113.779],[-8.309,-95.205],[-2.925,-95.296]],"c":true},"ix":2},"nm":"Path 8","mn":"ADBE Vector Shape - Group","hd":false},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[-0.187,-8.646],[0,0],[9.924,-0.183],[0,0],[0.046,1.922],[0,0],[-1.966,0.046],[0,0]],"o":[[0,0],[0.188,8.693],[0,0],[-1.92,0.046],[0,0],[-0.047,-1.876],[0,0],[9.925,-0.183]],"v":[[13.645,-104.996],[13.645,-104.904],[-2.832,-89.166],[-11.679,-88.983],[-15.236,-92.323],[-15.705,-116.387],[-12.288,-119.864],[-3.441,-120.001]],"c":true},"ix":2},"nm":"Path 9","mn":"ADBE Vector Shape - Group","hd":false},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[0,0],[1.872,-0.549],[-0.561,-1.83],[0,0],[-1.872,0.549],[0.562,1.83]],"o":[[-0.562,-1.83],[-1.872,0.549],[0,0],[0.562,1.83],[1.873,-0.549],[0,0]],"v":[[-29.888,-113.185],[-34.195,-115.472],[-36.536,-111.263],[-29.467,-87.657],[-25.161,-85.369],[-22.82,-89.578]],"c":true},"ix":2},"nm":"Path 10","mn":"ADBE Vector Shape - Group","hd":false},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[-0.467,-0.32],[0,0],[-1.638,0.961],[0,0],[0.188,1.83],[0,0],[0.281,0.458],[1.638,-0.915],[-0.188,-1.143],[0,0],[0,0],[1.545,-0.869],[-0.983,-1.647]],"o":[[0,0],[1.497,1.099],[0,0],[1.639,-0.915],[0,0],[-0.047,-0.411],[-0.936,-1.601],[-1.452,0.823],[0,0],[0,0],[-1.171,-0.915],[-1.685,0.961],[0.281,0.595]],"v":[[-70.521,-93.238],[-49.502,-77.409],[-44.728,-76.997],[-44.353,-77.226],[-42.294,-81.48],[-45.57,-107.466],[-46.038,-108.93],[-50.672,-110.119],[-52.451,-106.643],[-49.128,-85.232],[-66.12,-98.728],[-70.38,-99.094],[-71.691,-94.519]],"c":true},"ix":2},"nm":"Path 11","mn":"ADBE Vector Shape - Group","hd":false},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[4.026,3.523],[0,0],[3.698,-4.071],[-4.025,-3.522],[0,0],[-3.698,4.072]],"o":[[0,0],[-4.026,-3.523],[-3.698,4.072],[0,0],[4.026,3.523],[3.745,-4.117]],"v":[[-73.095,-76.448],[-73.142,-76.494],[-86.904,-75.991],[-85.875,-62.678],[-85.828,-62.632],[-72.065,-63.135]],"c":true},"ix":2},"nm":"Path 12","mn":"ADBE Vector Shape - Group","hd":false},{"ind":10,"ty":"sh","ix":11,"ks":{"a":0,"k":{"i":[[6.506,-7.137],[6.741,5.856],[0,0],[-6.507,7.091],[-6.741,-5.856],[0,0]],"o":[[-6.46,7.091],[0,0],[-6.694,-5.856],[6.506,-7.091],[0,0],[6.694,5.81]],"v":[[-67.337,-58.926],[-90.602,-57.279],[-90.649,-57.325],[-91.632,-80.199],[-68.367,-81.846],[-68.32,-81.801]],"c":true},"ix":2},"nm":"Path 13","mn":"ADBE Vector Shape - Group","hd":false},{"ind":11,"ty":"sh","ix":12,"ks":{"a":0,"k":{"i":[[-8.332,-3.248],[0,0],[-3.418,8.463],[1.217,3.477],[0.982,0.412],[0.656,-1.601],[-0.235,-0.64],[1.077,-2.699],[5.009,1.922],[0,0],[-1.966,4.85],[-2.06,1.236],[-0.375,0.869],[1.732,0.686],[0.749,-0.458],[1.779,-4.301]],"o":[[0,0],[8.426,3.248],[1.872,-4.575],[-0.281,-0.778],[-1.639,-0.64],[-0.281,0.732],[0.749,2.379],[-2.012,4.896],[0,0],[-5.009,-1.968],[0.936,-2.241],[0.468,-0.32],[0.702,-1.739],[-1.124,-0.457],[-2.762,1.692],[-3.651,8.829]],"v":[[-104.365,-25.575],[-104.271,-25.529],[-83.206,-34.404],[-82.738,-46.025],[-84.61,-47.992],[-88.777,-46.208],[-88.823,-44.058],[-89.058,-36.921],[-101.65,-32.163],[-101.743,-32.208],[-107.501,-44.058],[-103.007,-49.09],[-101.603,-50.737],[-103.475,-55.083],[-106.518,-54.854],[-113.399,-46.436]],"c":true},"ix":2},"nm":"Path 14","mn":"ADBE Vector Shape - Group","hd":false},{"ind":12,"ty":"sh","ix":13,"ks":{"a":0,"k":{"i":[[1.59,0.595],[0.609,-1.555],[0,0],[0,0],[0,0],[1.592,0.641],[0.608,-1.556],[0,0],[0,0],[0,0],[1.592,0.64],[0.607,-1.556],[0,0],[-1.778,-0.686],[0,0],[-0.702,1.738],[0,0]],"o":[[-1.592,-0.595],[0,0],[0,0],[0,0],[0.608,-1.556],[-1.59,-0.594],[0,0],[0,0],[0,0],[0.608,-1.555],[-1.591,-0.594],[0,0],[-0.702,1.784],[0,0],[1.826,0.686],[0,0],[0.655,-1.601]],"v":[[116.865,30.651],[112.838,32.389],[107.642,45.428],[101.463,43.095],[105.817,32.115],[104.084,28.18],[100.059,29.919],[95.705,40.899],[89.76,38.611],[94.863,25.801],[93.13,21.867],[89.106,23.606],[82.739,39.526],[84.704,43.918],[107.642,52.656],[112.136,50.735],[118.549,34.585]],"c":true},"ix":2},"nm":"Path 15","mn":"ADBE Vector Shape - Group","hd":false},{"ind":13,"ty":"sh","ix":14,"ks":{"a":0,"k":{"i":[[1.359,1.052],[1.124,-1.327],[0,0],[0,0],[0,0],[1.358,1.098],[1.124,-1.326],[0,0],[-1.498,-1.191],[0,0],[-1.217,1.464],[1.498,1.19],[0,0],[0,0]],"o":[[-1.357,-1.098],[0,0],[0,0],[0,0],[1.124,-1.327],[-1.357,-1.098],[0,0],[-1.218,1.464],[0,0],[1.498,1.19],[1.216,-1.464],[0,0],[0,0],[1.123,-1.327]],"v":[[91.164,57.506],[86.717,57.963],[79.04,67.204],[73.798,63.087],[82.739,52.29],[82.27,47.944],[77.824,48.401],[66.777,61.806],[67.291,66.565],[86.624,81.844],[91.493,81.341],[90.977,76.583],[83.956,71.047],[91.633,61.806]],"c":true},"ix":2},"nm":"Path 16","mn":"ADBE Vector Shape - Group","hd":false},{"ind":14,"ty":"sh","ix":15,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[65.607,86.373],[56.151,80.38],[58.398,91.223]],"c":true},"ix":2},"nm":"Path 17","mn":"ADBE Vector Shape - Group","hd":false},{"ind":15,"ty":"sh","ix":16,"ks":{"a":0,"k":{"i":[[-0.233,-0.366],[1.545,-1.007],[1.358,0.824],[0,0],[0,0],[0,0],[1.216,-0.823],[1.031,1.464],[0.093,0.503],[0,0],[-1.593,1.007],[0,0],[-1.592,-0.961],[0,0]],"o":[[1.03,1.51],[-1.357,0.869],[0,0],[0,0],[0,0],[0.28,1.419],[-1.499,1.007],[-0.281,-0.412],[0,0],[-0.329,-1.784],[0,0],[1.546,-1.053],[0,0],[0.516,0.275]],"v":[[80.632,88.34],[79.789,92.824],[75.67,92.732],[71.129,89.897],[59.662,97.536],[60.738,102.934],[59.287,106.457],[54.792,105.588],[54.278,104.17],[49.41,78.047],[51.236,73.609],[51.562,73.381],[56.431,73.381],[79.508,87.425]],"c":true},"ix":2},"nm":"Path 18","mn":"ADBE Vector Shape - Group","hd":false},{"ind":16,"ty":"sh","ix":17,"ks":{"a":0,"k":{"i":[[0,0],[6.6,-0.321],[0.608,1.922],[0,0],[-2.527,0.778],[-2.621,-0.595],[-0.657,0.183],[0.516,1.693],[0.891,0.183],[3.652,-1.144],[-1.686,-5.353],[0,0],[-6.554,0.366],[-0.516,-1.601],[0,0],[2.714,-0.823],[2.81,0.961],[0.841,-0.275],[-0.514,-1.692],[-0.889,-0.275],[-3.978,1.19],[1.872,5.856]],"o":[[-1.639,-5.124],[-5.617,0.274],[0,0],[-0.468,-1.418],[2.06,-0.64],[0.61,0.137],[1.731,-0.503],[-0.421,-1.281],[-3.229,-0.87],[-6.131,1.875],[0,0],[1.873,5.856],[5.429,-0.274],[0,0],[0.561,1.738],[-2.95,0.869],[-0.514,-0.183],[-1.733,0.503],[0.328,1.007],[4.12,1.327],[6.507,-1.967],[0,0]],"v":[[45.899,102.34],[33.54,96.439],[25.817,94.562],[25.769,94.471],[28.812,90.719],[35.787,90.765],[37.614,90.719],[39.766,86.785],[37.472,84.681],[27.08,85.001],[19.357,97.079],[19.404,97.17],[32.277,103.117],[39.533,104.993],[39.579,105.085],[36.116,109.202],[27.548,108.882],[25.49,108.882],[23.335,112.816],[25.348,114.875],[37.8,114.966],[45.946,102.477]],"c":true},"ix":2},"nm":"Path 19","mn":"ADBE Vector Shape - Group","hd":false},{"ind":17,"ty":"sh","ix":18,"ks":{"a":0,"k":{"i":[[7.959,0.183],[0,0],[0,0],[0,0],[-0.187,6.817],[0,0]],"o":[[0,0],[0,0],[0,0],[7.958,0.183],[0,0],[0.141,-6.817]],"v":[[-4.143,92.412],[-11.539,92.229],[-12.147,116.613],[-4.751,116.796],[8.637,104.993],[8.637,104.902]],"c":true},"ix":2},"nm":"Path 20","mn":"ADBE Vector Shape - Group","hd":false},{"ind":18,"ty":"sh","ix":19,"ks":{"a":0,"k":{"i":[[0.235,-8.647],[0,0],[9.925,0.229],[0,0],[0,0.961],[0,0],[-0.983,0],[0,0]],"o":[[0,0],[-0.234,8.692],[0,0],[-0.936,-0.046],[0,0],[0.048,-0.961],[0,0],[9.924,0.275]],"v":[[12.288,104.902],[12.288,104.993],[-4.892,119.999],[-14.067,119.77],[-15.798,117.986],[-15.097,90.674],[-13.271,88.935],[-4.096,89.164]],"c":true},"ix":2},"nm":"Path 21","mn":"ADBE Vector Shape - Group","hd":false},{"ind":19,"ty":"sh","ix":20,"ks":{"a":0,"k":{"i":[[0.936,0.275],[0.28,-0.961],[0,0],[-0.936,-0.229],[-0.281,0.961],[0,0]],"o":[[-0.89,-0.274],[0,0],[-0.281,0.915],[0.936,0.275],[0,0],[0.233,-0.869]],"v":[[-24.458,85.641],[-26.658,86.877],[-34.429,113.594],[-33.212,115.744],[-30.965,114.509],[-23.194,87.791]],"c":true},"ix":2},"nm":"Path 22","mn":"ADBE Vector Shape - Group","hd":false},{"ind":20,"ty":"sh","ix":21,"ks":{"a":0,"k":{"i":[[-0.141,0.229],[0.843,0.457],[0.608,-0.412],[0,0],[0,0],[0.796,0.457],[0.421,-0.732],[0,-0.274],[0,0],[-0.936,-0.503],[0,0],[-0.796,0.549],[0,0]],"o":[[0.467,-0.777],[-0.702,-0.412],[0,0],[0,0],[0.093,-0.823],[-0.842,-0.504],[-0.14,0.275],[0,0],[-0.14,0.961],[0,0],[0.889,0.503],[0,0],[0.14,-0.275]],"v":[[-33.071,85.824],[-33.727,83.537],[-35.88,83.766],[-58.443,100.693],[-54.885,73.243],[-55.822,71.185],[-58.256,71.779],[-58.49,72.557],[-61.907,102.294],[-60.831,104.581],[-60.69,104.673],[-58.162,104.444],[-33.493,86.465]],"c":true},"ix":2},"nm":"Path 23","mn":"ADBE Vector Shape - Group","hd":false},{"ind":21,"ty":"sh","ix":22,"ks":{"a":0,"k":{"i":[[4.775,5.307],[5.383,-4.62],[0,0],[-4.775,-5.261],[-5.384,4.621],[0,0]],"o":[[-4.775,-5.307],[0,0],[-5.384,4.621],[4.775,5.307],[0,0],[5.384,-4.621]],"v":[[-70.146,60.754],[-88.028,60.067],[-88.074,60.113],[-89.619,77.635],[-71.737,78.321],[-71.691,78.276]],"c":true},"ix":2},"nm":"Path 24","mn":"ADBE Vector Shape - Group","hd":false},{"ind":22,"ty":"sh","ix":23,"ks":{"a":0,"k":{"i":[[-6.366,-7.045],[6.507,-5.581],[0,0],[6.366,7.045],[-6.507,5.627],[0,0]],"o":[[6.366,7.045],[0,0],[-6.506,5.627],[-6.366,-7.045],[0,0],[6.553,-5.581]],"v":[[-67.618,58.695],[-69.163,80.929],[-69.21,80.975],[-92.147,79.694],[-90.602,57.414],[-90.555,57.368]],"c":true},"ix":2},"nm":"Path 25","mn":"ADBE Vector Shape - Group","hd":false},{"ind":23,"ty":"sh","ix":24,"ks":{"a":0,"k":{"i":[[-2.528,-6.314],[-3.324,-1.555],[-0.187,-0.412],[0.843,-0.32],[0.374,0.182],[1.873,4.712],[-8.426,3.249],[0,0],[-3.418,-8.463],[1.17,-3.524],[0.608,-0.228],[0.327,0.869],[-0.094,0.274],[1.451,3.523],[6.741,-2.608],[0,0]],"o":[[1.451,3.568],[0.327,0.138],[0.327,0.823],[-0.515,0.183],[-3.792,-1.831],[-3.37,-8.281],[0,0],[8.285,-3.203],[1.872,4.621],[-0.14,0.456],[-0.89,0.321],[-0.187,-0.458],[1.123,-2.974],[-2.575,-6.359],[0,0],[-6.788,2.562]],"v":[[-110.45,45.291],[-103.428,52.427],[-102.539,53.297],[-103.522,55.447],[-104.926,55.356],[-113.54,46.389],[-104.271,26.167],[-104.177,26.122],[-83.112,34.768],[-82.785,46.481],[-83.861,47.669],[-86.155,46.663],[-86.202,45.474],[-86.202,35.958],[-102.726,29.507],[-102.82,29.553]],"c":true},"ix":2},"nm":"Path 26","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[120,120],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":28,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Logo_Inside Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[130,130,0],"ix":2},"a":{"a":0,"k":[120,120,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.685,0],[0,0],[1.17,1.236],[0,0],[-2.574,2.333],[-2.387,-2.516],[0,0],[0,0],[-2.481,-2.379],[2.434,-2.425],[0,0]],"o":[[0,0],[-1.732,-0.046],[0,0],[-2.387,-2.469],[2.528,-2.333],[0,0],[0,0],[2.434,-2.425],[2.481,2.379],[0,0],[-1.217,1.143]],"v":[[-21.112,38.727],[-21.205,38.727],[-25.699,36.759],[-48.918,12.466],[-48.59,3.774],[-39.696,4.095],[-20.924,23.721],[39.93,-36.302],[48.824,-36.348],[48.871,-27.655],[-16.618,36.943]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[138.055,111.631],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.732,2.974],[0,0],[3.511,0],[0,0],[-1.733,-2.928],[0,0]],"o":[[0,0],[1.731,-2.974],[0,0],[-3.463,0],[0,0],[1.779,2.974]],"v":[[42.528,58.17],[45.945,52.36],[42.012,45.726],[35.178,45.726],[31.247,52.36],[34.664,58.17]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-4.072,3.705],[-3.792,-3.934],[0,0],[0,0],[0,0],[0.654,-2.607],[0,0],[1.639,0.412],[0,0],[-0.421,1.601],[0,0],[1.966,0],[0,0],[0.562,-0.457],[0,0],[-1.03,-2.333],[0,0],[-4.728,2.837],[0,0]],"o":[[-3.791,-3.98],[4.073,-3.706],[0,0],[0,0],[0,0],[-1.31,-2.379],[0,0],[-0.421,1.602],[0,0],[-1.638,-0.412],[0,0],[0.469,-1.876],[0,0],[-0.701,0],[0,0],[-2.012,1.693],[0,0],[2.153,4.986],[0,0],[0,0]],"v":[[-33.4,6.656],[-32.885,-7.251],[-18.654,-6.794],[-2.598,10.042],[47.021,-38.91],[35.974,-58.765],[30.404,-58.079],[25.957,-40.557],[22.305,-38.407],[6.764,-42.158],[4.563,-45.726],[6.061,-51.674],[3.113,-55.334],[-12.242,-55.334],[-14.207,-54.602],[-66.402,-10.866],[-68.04,-4.095],[-54.371,27.381],[-41.311,31.452],[-21.229,19.42]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0.844,-1.693],[0,0],[2.294,0],[0,0],[1.124,1.19],[0,0],[0,0]],"o":[[0.935,1.647],[0,0],[-0.982,2.058],[0,0],[-1.686,0],[0,0],[0,0],[0,0]],"v":[[68.087,-0.984],[68.226,4.369],[51.515,38.681],[46.086,42.066],[30.545,42.066],[26.143,40.19],[11.165,24.498],[57.086,-20.793]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[119.776,119.958],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":5,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Logo_Outside Outlines","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":599,"s":[360]}],"ix":10},"p":{"a":0,"k":[130,130,0],"ix":2},"a":{"a":0,"k":[120,120,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":240,"h":240,"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..35da219
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/au/gov/health/covidsafe/HasBlockingState.kt b/app/src/main/java/au/gov/health/covidsafe/HasBlockingState.kt
new file mode 100644
index 0000000..21c5fef
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/HasBlockingState.kt
@@ -0,0 +1,5 @@
+package au.gov.health.covidsafe
+
+interface HasBlockingState {
+ var isUiBlocked: Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt b/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt
new file mode 100644
index 0000000..04e0f08
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt
@@ -0,0 +1,15 @@
+package au.gov.health.covidsafe
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+
+class HomeActivity : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_home)
+
+ Utils.startBluetoothMonitoringService(this)
+
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/PeekActivity.kt b/app/src/main/java/au/gov/health/covidsafe/PeekActivity.kt
new file mode 100644
index 0000000..bf2bf8d
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/PeekActivity.kt
@@ -0,0 +1,147 @@
+package au.gov.health.covidsafe
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
+import au.gov.health.covidsafe.streetpass.view.RecordViewModel
+
+
+class PeekActivity : AppCompatActivity() {
+
+ private lateinit var viewModel: RecordViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ newPeek()
+ }
+
+ private fun newPeek() {
+ setContentView(R.layout.database_peek)
+ val recyclerView = findViewById(R.id.recyclerview)
+ val adapter = RecordListAdapter(this)
+ recyclerView.adapter = adapter
+ val layoutManager = LinearLayoutManager(this)
+ recyclerView.layoutManager = layoutManager
+
+ val dividerItemDecoration = DividerItemDecoration(
+ recyclerView.context,
+ layoutManager.orientation
+ )
+ recyclerView.addItemDecoration(dividerItemDecoration)
+
+ viewModel = ViewModelProvider(this).get(RecordViewModel::class.java)
+ viewModel.allRecords.observe(this, Observer { records ->
+ adapter.setSourceData(records)
+ })
+
+ findViewById(R.id.expand)
+ .setOnClickListener {
+ viewModel.allRecords.value?.let {
+ adapter.setMode(RecordListAdapter.MODE.ALL)
+ }
+ }
+
+ findViewById(R.id.collapse)
+ .setOnClickListener {
+ viewModel.allRecords.value?.let {
+ adapter.setMode(RecordListAdapter.MODE.COLLAPSE)
+ }
+ }
+
+
+ val start = findViewById(R.id.start)
+ start.setOnClickListener {
+ startService()
+ }
+
+ val stop = findViewById(R.id.stop)
+ stop.setOnClickListener {
+ stopService()
+ }
+
+ val delete = findViewById(R.id.delete)
+ delete.setOnClickListener { view ->
+ view.isEnabled = false
+
+ val builder = AlertDialog.Builder(this)
+ builder
+ .setTitle("Are you sure?")
+ .setCancelable(false)
+ .setMessage("Deleting the DB records is irreversible")
+ .setPositiveButton("DELETE") { dialog, which ->
+ Observable.create {
+ StreetPassRecordStorage(this).nukeDb()
+ it.onNext(true)
+ }
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribeOn(Schedulers.io())
+ .subscribe { result ->
+ Toast.makeText(this, "Database nuked: $result", Toast.LENGTH_SHORT)
+ .show()
+ view.isEnabled = true
+ dialog.cancel()
+ }
+ }
+
+ .setNegativeButton("DON'T DELETE") { dialog, which ->
+ view.isEnabled = true
+ dialog.cancel()
+ }
+
+ val dialog: AlertDialog = builder.create()
+ dialog.show()
+
+ }
+
+ val plot = findViewById(R.id.plot)
+ plot.setOnClickListener { view ->
+ val intent = Intent(this, PlotActivity::class.java)
+ intent.putExtra("time_period", nextTimePeriod())
+ startActivity(intent)
+ }
+
+ if(!BuildConfig.DEBUG) {
+ start.visibility = View.GONE
+ stop.visibility = View.GONE
+ delete.visibility = View.GONE
+ }
+
+ }
+
+ private var timePeriod: Int = 0
+
+ private fun nextTimePeriod(): Int {
+ timePeriod = when (timePeriod) {
+ 1 -> 3
+ 3 -> 6
+ 6 -> 12
+ 12 -> 24
+ else -> 1
+ }
+
+ return timePeriod
+ }
+
+
+ private fun startService() {
+ Utils.startBluetoothMonitoringService(this)
+ }
+
+ private fun stopService() {
+ Utils.stopBluetoothMonitoringService(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/PlotActivity.kt b/app/src/main/java/au/gov/health/covidsafe/PlotActivity.kt
new file mode 100644
index 0000000..a8febf1
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/PlotActivity.kt
@@ -0,0 +1,231 @@
+package au.gov.health.covidsafe
+
+import android.os.Build
+import android.os.Bundle
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import io.reactivex.Observable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.functions.BiFunction
+import io.reactivex.schedulers.Schedulers
+import au.gov.health.covidsafe.status.persistence.StatusRecord
+import au.gov.health.covidsafe.status.persistence.StatusRecordStorage
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
+import au.gov.health.covidsafe.ui.upload.model.DebugData
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.Comparator
+
+class PlotActivity : AppCompatActivity() {
+ private var TAG = "PlotActivity"
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_plot)
+
+ val webView = findViewById(R.id.webView)
+ webView.webViewClient = WebViewClient()
+ webView.settings.javaScriptEnabled = true
+
+ val displayTimePeriod = intent.getIntExtra("time_period", 1) // in hours
+
+ val observableStreetRecords = Observable.create> {
+ val result = StreetPassRecordStorage(this).getAllRecords()
+ it.onNext(result)
+ }
+ val observableStatusRecords = Observable.create> {
+ val result = StatusRecordStorage(this).getAllRecords()
+ it.onNext(result)
+ }
+
+ val zipResult = Observable.zip(observableStreetRecords, observableStatusRecords,
+ BiFunction, List, DebugData> { records, _ -> DebugData(records) }
+ )
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribeOn(Schedulers.io())
+ .subscribe { exportedData ->
+
+ if(exportedData.records.isEmpty()){
+ return@subscribe
+ }
+
+ val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+
+ // Use the date of the last record as the end time (Epoch time in seconds)
+ val endTime =
+ exportedData.records.sortedByDescending { it.timestamp }[0].timestamp / 1000 + 1 * 60
+ val endTimeString = dateFormatter.format(Date(endTime * 1000))
+
+ val startTime =
+ endTime - displayTimePeriod * 3600 // ignore records older than X hour(s)
+ val startTimeString = dateFormatter.format(Date(startTime * 1000))
+
+ val filteredRecords = exportedData.records.filter {
+ it.timestamp / 1000 in startTime..endTime
+ }
+
+ if (filteredRecords.isNotEmpty()) {
+ val dataByModelC = filteredRecords.groupBy { it.modelC }
+ val dataByModelP = filteredRecords.groupBy { it.modelP }
+
+ // get all models
+ val allModelList = dataByModelC.keys union dataByModelP.keys.toList()
+
+ // sort the list by the models that appear the most frequently
+ val sortedModelList =
+ allModelList.sortedWith(Comparator { a: String, b: String ->
+ val aSize = (dataByModelC[a]?.size ?: 0) + (dataByModelP[a]?.size ?: 0)
+ val bSize = (dataByModelC[b]?.size ?: 0) + (dataByModelP[b]?.size ?: 0)
+
+ bSize - aSize
+ })
+
+ val individualData = sortedModelList.joinToString(separator = "\n") { model ->
+ val index = sortedModelList.indexOf(model) + 1
+
+ val hasC = dataByModelC.containsKey(model)
+ val hasP = dataByModelP.containsKey(model)
+
+ val x1 = dataByModelC[model]?.joinToString(separator = "\", \"", prefix = "[\"", postfix = "\"]") {
+ dateFormatter.format(Date(it.timestamp))
+ }
+
+ val y1 = dataByModelC[model]?.map { it.rssi }
+ ?.joinToString(separator = ", ", prefix = "[", postfix = "]")
+
+ val x2 = dataByModelP[model]?.joinToString(separator = "\", \"", prefix = "[\"", postfix = "\"]") {
+ dateFormatter.format(Date(it.timestamp))
+ }
+
+ val y2 = dataByModelP[model]?.map { it.rssi }
+ ?.joinToString(separator = ", ", prefix = "[", postfix = "]")
+
+ val dataHead = "var data${index} = [];"
+
+ val dataA = if (!hasC) "" else """
+ var data${index}a = {
+ name: 'central',
+ x: ${x1},
+ y: ${y1},
+ xaxis: 'x${index}',
+ yaxis: 'y${index}',
+ mode: 'markers',
+ type: 'scatter',
+ line: {color: 'blue'}
+ };
+ data${index} = data${index}.concat(data${index}a);
+ """.trimIndent()
+
+ val dataB = if (!hasP) "" else """
+ var data${index}b = {
+ name: 'peripheral',
+ x: ${x2},
+ y: ${y2},
+ xaxis: 'x${index}',
+ yaxis: 'y${index}',
+ mode: 'markers',
+ type: 'scatter',
+ line: {color: 'red'}
+ };
+ data${index} = data${index}.concat(data${index}b);
+ """.trimIndent()
+
+ val data = dataHead + dataA + dataB
+
+ data
+
+ }
+
+ val top = 20
+
+ val combinedData = sortedModelList.joinToString(separator = "\n") { model ->
+ val index = sortedModelList.indexOf(model) + 1
+ if (index < top) """
+ data = data.concat(data${index});
+ """.trimIndent() else ""
+ }
+
+ val xAxis = sortedModelList.joinToString(separator = ",\n") { model ->
+ val index = sortedModelList.indexOf(model) + 1
+ if (index < top) """
+ xaxis${index}: {
+ type: 'date',
+ tickformat: '%H:%M:%S',
+ range: ['${startTimeString}', '${endTimeString}'],
+ dtick: ${displayTimePeriod * 5} * 60 * 1000
+ }
+ """.trimIndent() else ""
+ }
+
+ val yAxis = sortedModelList.joinToString(separator = ",\n") { model ->
+ val index = sortedModelList.indexOf(model) + 1
+ if (index < top) """
+ yaxis${index}: {
+ range: [-100, -30],
+ ticks: 'outside',
+ dtick: 10,
+ title: {
+ text: "$model"
+ }
+ }
+ """.trimIndent() else ""
+ }
+
+ // Form the complete HTML
+ val customHtml = """
+
+
+
+
+
+
+
+ """.trimIndent()
+
+ webView.loadData(customHtml, "text/html", "UTF-8")
+ } else {
+ webView.loadData(
+ "No data received in the last $displayTimePeriod hour(s) or more.",
+ "text/html",
+ "UTF-8"
+ )
+ }
+ }
+ webView.loadData("Loading...", "text/html", "UTF-8")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/Preference.kt b/app/src/main/java/au/gov/health/covidsafe/Preference.kt
new file mode 100644
index 0000000..b5e4587
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/Preference.kt
@@ -0,0 +1,162 @@
+package au.gov.health.covidsafe
+
+import android.content.Context
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKeys
+
+object Preference {
+ private const val PREF_ID = "Tracer_pref"
+ private const val IS_ONBOARDED = "IS_ONBOARDED"
+ private const val PHONE_NUMBER = "PHONE_NUMBER"
+ private const val HANDSHAKE_PIN = "HANDSHAKE_PIN"
+ private const val DEVICE_ID = "DEVICE_ID"
+ private const val JWT_TOKEN = "JWT_TOKEN"
+ private const val IS_DATA_UPLOADED = "IS_DATA_UPLOADED"
+ private const val DATA_UPLOADED_DATE_MS = "DATA_UPLOADED_DATE_MS"
+ private const val UPLOADED_MORE_THAN_24_HRS = "UPLOADED_MORE_THAN_24_HRS"
+
+ private const val NEXT_FETCH_TIME = "NEXT_FETCH_TIME"
+ private const val EXPIRY_TIME = "EXPIRY_TIME"
+ private const val NAME = "NAME"
+ private const val IS_MINOR = "IS_MINOR"
+ private const val POST_CODE = "POST_CODE"
+ private const val AGE = "AGE"
+
+ fun putDeviceID(context: Context, value: String) {
+ context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putString(DEVICE_ID, value)?.apply()
+ }
+
+ fun getDeviceID(context: Context?): String {
+ return context?.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ ?.getString(DEVICE_ID, "") ?: ""
+ }
+
+ fun putEncrypterJWTToken(context: Context?, jwtToken: String?) {
+ context?.let {
+ val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
+ EncryptedSharedPreferences.create(
+ PREF_ID,
+ masterKeyAlias,
+ context,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ ).edit()?.putString(JWT_TOKEN, jwtToken)?.apply()
+ }
+ }
+
+ fun getEncrypterJWTToken(context: Context?): String? {
+ return context?.let {
+ val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
+ EncryptedSharedPreferences.create(
+ PREF_ID,
+ masterKeyAlias,
+ context,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ ).getString(JWT_TOKEN, null)
+ }
+ }
+
+ fun putHandShakePin(context: Context?, value: String?) {
+ context?.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ ?.edit()?.putString(HANDSHAKE_PIN, value)?.apply()
+ }
+
+ fun putIsOnBoarded(context: Context, value: Boolean) {
+ context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putBoolean(IS_ONBOARDED, value).apply()
+ }
+
+ fun isOnBoarded(context: Context): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .getBoolean(IS_ONBOARDED, false)
+ }
+
+ fun putPhoneNumber(context: Context, value: String) {
+ context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putString(PHONE_NUMBER, value).apply()
+ }
+
+ fun putNextFetchTimeInMillis(context: Context, time: Long) {
+ context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putLong(NEXT_FETCH_TIME, time).apply()
+ }
+
+ fun getNextFetchTimeInMillis(context: Context): Long {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .getLong(
+ NEXT_FETCH_TIME, 0
+ )
+ }
+
+ fun putExpiryTimeInMillis(context: Context, time: Long) {
+ context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putLong(EXPIRY_TIME, time).apply()
+ }
+
+ fun getExpiryTimeInMillis(context: Context): Long {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .getLong(
+ EXPIRY_TIME, 0
+ )
+ }
+
+ fun isDataUploaded(context: Context): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE).getBoolean(IS_DATA_UPLOADED, false)
+ }
+
+ fun setDataIsUploaded(context: Context, value: Boolean) {
+ context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE).edit().also { editor ->
+ editor.putBoolean(IS_DATA_UPLOADED, value)
+ if (value) {
+ editor.putLong(DATA_UPLOADED_DATE_MS, System.currentTimeMillis())
+ } else {
+ editor.remove(DATA_UPLOADED_DATE_MS)
+ }
+ }.apply()
+ }
+
+ fun getDataUploadedDateMs(context: Context): Long {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE).getLong(DATA_UPLOADED_DATE_MS, System.currentTimeMillis())
+ }
+
+ fun putName(context: Context, name: String): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putString(NAME, name).commit()
+ }
+
+ fun getName(context: Context): String? {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE).getString(NAME, null)
+ }
+
+ fun putIsMinor(context: Context, minor: Boolean): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putBoolean(IS_MINOR, minor).commit()
+ }
+
+ fun isMinor(context: Context): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE).getBoolean(IS_MINOR, false)
+ }
+
+ fun putPostCode(context: Context, state: String): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putString(POST_CODE, state).commit()
+ }
+
+ fun getPostCode(context: Context): String? {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .getString(POST_CODE, null)
+ }
+
+ fun putAge(context: Context, age: String): Boolean {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .edit().putString(AGE, age).commit()
+ }
+
+ fun getAge(context: Context): String? {
+ return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE)
+ .getString(AGE, null)
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt b/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt
new file mode 100644
index 0000000..616c331
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt
@@ -0,0 +1,170 @@
+package au.gov.health.covidsafe
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+import au.gov.health.covidsafe.streetpass.view.StreetPassRecordViewModel
+
+
+class RecordListAdapter internal constructor(context: Context) :
+ RecyclerView.Adapter() {
+ private val inflater: LayoutInflater = LayoutInflater.from(context)
+ private var records = emptyList() // Cached copy of records
+ private var sourceData = emptyList()
+
+ enum class MODE {
+ ALL, COLLAPSE, MODEL_P, MODEL_C
+ }
+
+ private var mode = MODE.ALL
+
+ inner class RecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val modelCView: TextView = itemView.findViewById(R.id.modelc)
+ val modelPView: TextView = itemView.findViewById(R.id.modelp)
+ val timestampView: TextView = itemView.findViewById(R.id.timestamp)
+ val findsView: TextView = itemView.findViewById(R.id.finds)
+ val txpowerView: TextView = itemView.findViewById(R.id.txpower)
+ val signalStrengthView: TextView = itemView.findViewById(R.id.signal_strength)
+ val filterModelP: View = itemView.findViewById(R.id.filter_by_modelp)
+ val filterModelC: View = itemView.findViewById(R.id.filter_by_modelc)
+ val msgView: TextView = itemView.findViewById(R.id.msg)
+ val version: TextView = itemView.findViewById(R.id.version)
+ val org: TextView = itemView.findViewById(R.id.org)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecordViewHolder {
+ val itemView = inflater.inflate(R.layout.recycler_view_item, parent, false)
+ return RecordViewHolder(itemView)
+ }
+
+ override fun onBindViewHolder(holder: RecordViewHolder, position: Int) {
+ val current = records[position]
+ holder.msgView.text = current.msg
+ holder.modelCView.text = current.modelC
+ holder.modelPView.text = current.modelP
+ holder.findsView.text = "Detections: ${current.number}"
+ val readableDate = Utils.getDate(current.timeStamp)
+ holder.timestampView.text = readableDate
+ holder.version.text = "v: ${current.version}"
+ holder.org.text = "ORG: ${current.org}"
+
+ holder.filterModelP.tag = current
+ holder.filterModelC.tag = current
+
+ holder.signalStrengthView.text = "Signal Strength: ${current.rssi}"
+
+ holder.txpowerView.text = "Tx Power: ${current.transmissionPower}"
+
+ holder.filterModelP.setOnClickListener {
+ val model = it.tag as StreetPassRecordViewModel
+ setMode(MODE.MODEL_P, model)
+ }
+
+ holder.filterModelC.setOnClickListener {
+ val model = it.tag as StreetPassRecordViewModel
+ setMode(MODE.MODEL_C, model)
+ }
+ }
+
+ private fun filter(sample: StreetPassRecordViewModel?): List {
+ return when (mode) {
+ MODE.COLLAPSE -> prepareCollapsedData(sourceData)
+
+ MODE.ALL -> prepareViewData(sourceData)
+
+ MODE.MODEL_P -> filterByModelP(sample, sourceData)
+
+ MODE.MODEL_C -> filterByModelC(sample, sourceData)
+
+ else -> {
+ prepareViewData(sourceData)
+ }
+ }
+ }
+
+ private fun filterByModelC(
+ model: StreetPassRecordViewModel?,
+ words: List
+ ): List {
+ if (model != null) {
+ return prepareViewData(words.filter { it.modelC == model.modelC })
+ }
+ return prepareViewData(words)
+ }
+
+ private fun filterByModelP(
+ model: StreetPassRecordViewModel?,
+ words: List
+ ): List {
+
+ if (model != null) {
+ return prepareViewData(words.filter { it.modelP == model.modelP })
+ }
+ return prepareViewData(words)
+ }
+
+
+ private fun prepareCollapsedData(words: List): List {
+ //we'll need to count the number of unique device IDs
+ val countMap = words.groupBy {
+ it.modelC
+ }
+
+ val distinctAddresses = words.distinctBy { it.modelC }
+
+ return distinctAddresses.map { record ->
+ val count = countMap[record.modelC]?.size
+
+ count?.let { count ->
+ val mostRecentRecord = countMap[record.modelC]?.maxBy { it.timestamp }
+
+ if (mostRecentRecord != null) {
+ return@map StreetPassRecordViewModel(mostRecentRecord, count)
+ }
+
+ return@map StreetPassRecordViewModel(record, count)
+ }
+ //fallback - unintended
+ return@map StreetPassRecordViewModel(record)
+ }
+ }
+
+ private fun prepareViewData(words: List): List {
+
+ words.let {
+
+ val reversed = it.reversed()
+ return reversed.map { streetPassRecord ->
+ return@map StreetPassRecordViewModel(streetPassRecord)
+ }
+ }
+ }
+
+ fun setMode(mode: MODE) {
+ setMode(mode, null)
+ }
+
+ private fun setMode(mode: MODE, model: StreetPassRecordViewModel?) {
+ this.mode = mode
+
+ val list = filter(model)
+ setRecords(list)
+ }
+
+ private fun setRecords(records: List) {
+ this.records = records
+ notifyDataSetChanged()
+ }
+
+ internal fun setSourceData(records: List) {
+ this.sourceData = records
+ setMode(mode)
+ }
+
+ override fun getItemCount() = records.size
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/SelfIsolationDoneActivity.kt b/app/src/main/java/au/gov/health/covidsafe/SelfIsolationDoneActivity.kt
new file mode 100644
index 0000000..d3cf99e
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/SelfIsolationDoneActivity.kt
@@ -0,0 +1,35 @@
+package au.gov.health.covidsafe
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import kotlinx.android.synthetic.main.activity_self_isolation.*
+
+class SelfIsolationDoneActivity : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_self_isolation)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ activity_self_isolation_next.setOnClickListener {
+ Preference.setDataIsUploaded(this, false)
+ val intent = Intent(this, HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ startActivity(intent)
+ finish()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ activity_self_isolation_next.setOnClickListener(null)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt b/app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt
new file mode 100644
index 0000000..db781bc
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt
@@ -0,0 +1,82 @@
+package au.gov.health.covidsafe
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.provider.Settings
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import au.gov.health.covidsafe.ui.onboarding.OnboardingActivity
+import java.util.*
+
+class SplashActivity : AppCompatActivity() {
+
+ private val SPLASH_TIME: Long = 2000
+
+ private var retryProviderInstall: Boolean = false
+ private val ERROR_DIALOG_REQUEST_CODE = 1
+
+ private var updateFlag = false
+
+ private lateinit var mHandler: Handler
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_splash)
+ hideSystemUI()
+ mHandler = Handler()
+
+ Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver,
+ Settings.Secure.ANDROID_ID))
+
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mHandler.removeCallbacksAndMessages(null)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!updateFlag) {
+ mHandler.postDelayed({
+ goToNextScreen()
+ finish()
+ }, SPLASH_TIME)
+ }
+ }
+
+ private fun goToNextScreen() {
+ val dateUploaded = Calendar.getInstance().also {
+ it.timeInMillis = Preference.getDataUploadedDateMs(this)
+ }
+ val fourteenDaysAgo = Calendar.getInstance().also {
+ it.add(Calendar.DATE, -14)
+ }
+ startActivity(Intent(this, if (!Preference.isOnBoarded(this)) {
+ OnboardingActivity::class.java
+ } else if (dateUploaded.before(fourteenDaysAgo)) {
+ SelfIsolationDoneActivity::class.java
+ } else {
+ HomeActivity::class.java
+ }))
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
+ retryProviderInstall = true
+ }
+ }
+
+ // This snippet hides the system bars.
+ private fun hideSystemUI() {
+ window.decorView.systemUiVisibility = (
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_FULLSCREEN)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/TracerApp.kt b/app/src/main/java/au/gov/health/covidsafe/TracerApp.kt
new file mode 100644
index 0000000..e7e1cb1
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/TracerApp.kt
@@ -0,0 +1,48 @@
+package au.gov.health.covidsafe
+
+import android.app.Application
+import android.content.Context
+import android.os.Build
+import com.atlassian.mobilekit.module.feedback.FeedbackModule
+
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.services.BluetoothMonitoringService
+import au.gov.health.covidsafe.streetpass.CentralDevice
+import au.gov.health.covidsafe.streetpass.PeripheralDevice
+
+class TracerApp : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ AppContext = applicationContext
+ FeedbackModule.init(this)
+ }
+
+ companion object {
+
+ private const val TAG = "TracerApp"
+ const val ORG = BuildConfig.ORG
+ const val protocolVersion = BuildConfig.PROTOCOL_VERSION
+
+ lateinit var AppContext: Context
+
+ fun thisDeviceMsg(): String {
+ BluetoothMonitoringService.broadcastMessage?.let {
+ CentralLog.i(TAG, "Retrieved BM for storage: $it")
+ return it
+ }
+
+ CentralLog.e(TAG, "No local Broadcast Message")
+ return BluetoothMonitoringService.broadcastMessage!!
+ }
+
+ fun asPeripheralDevice(): PeripheralDevice {
+ return PeripheralDevice(Build.MODEL, "SELF")
+ }
+
+ fun asCentralDevice(): CentralDevice {
+ return CentralDevice(Build.MODEL, "SELF")
+ }
+
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/Utils.kt b/app/src/main/java/au/gov/health/covidsafe/Utils.kt
new file mode 100644
index 0000000..62939c1
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/Utils.kt
@@ -0,0 +1,247 @@
+package au.gov.health.covidsafe
+
+import android.Manifest
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import au.gov.health.covidsafe.bluetooth.gatt.*
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.scheduler.Scheduler
+import au.gov.health.covidsafe.services.BluetoothMonitoringService
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_ADVERTISE_REQ_CODE
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_BM_UPDATE
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_HEALTH_CHECK_CODE
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_SCAN_REQ_CODE
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_START
+import au.gov.health.covidsafe.status.Status
+import au.gov.health.covidsafe.streetpass.ACTION_DEVICE_SCANNED
+import au.gov.health.covidsafe.streetpass.ConnectablePeripheral
+import au.gov.health.covidsafe.streetpass.ConnectionRecord
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+
+object Utils {
+
+ private const val TAG = "Utils"
+
+ fun getRequiredPermissions(): Array {
+ return arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+
+ fun getBatteryOptimizerExemptionIntent(packageName: String): Intent {
+ val intent = Intent()
+ intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+ intent.data = Uri.parse("package:$packageName")
+ return intent
+ }
+
+ fun canHandleIntent(batteryExemptionIntent: Intent, packageManager: PackageManager?): Boolean {
+ packageManager?.let {
+ return batteryExemptionIntent.resolveActivity(packageManager) != null
+ }
+ return false
+ }
+
+ fun getDate(milliSeconds: Long): String {
+ val dateFormat = "dd/MM/yyyy HH:mm:ss.SSS"
+ // Create a DateFormatter object for displaying date in specified format.
+ val formatter = SimpleDateFormat(dateFormat)
+
+ // Create a calendar object that will convert the date and time value in milliseconds to date.
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = milliSeconds
+ return formatter.format(calendar.time)
+ }
+
+ fun startBluetoothMonitoringService(context: Context) {
+ val intent = Intent(context, BluetoothMonitoringService::class.java)
+ intent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_START.index
+ )
+
+ context.startService(intent)
+ }
+
+ fun scheduleStartMonitoringService(context: Context, timeInMillis: Long) {
+ val intent = Intent(context, BluetoothMonitoringService::class.java)
+ intent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_START.index
+ )
+
+ Scheduler.scheduleServiceIntent(
+ PENDING_START,
+ context,
+ intent,
+ timeInMillis
+ )
+ }
+
+ fun scheduleBMUpdateCheck(context: Context, bmCheckInterval: Long) {
+
+ cancelBMUpdateCheck(context)
+
+ val intent = Intent(context, BluetoothMonitoringService::class.java)
+ intent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_UPDATE_BM.index
+ )
+
+ Scheduler.scheduleServiceIntent(
+ PENDING_BM_UPDATE,
+ context,
+ intent,
+ bmCheckInterval
+ )
+ }
+
+ fun cancelBMUpdateCheck(context: Context) {
+ val intent = Intent(context, BluetoothMonitoringService::class.java)
+ intent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_UPDATE_BM.index
+ )
+
+ Scheduler.cancelServiceIntent(PENDING_BM_UPDATE, context, intent)
+ }
+
+ fun stopBluetoothMonitoringService(context: Context) {
+ val intent = Intent(context, BluetoothMonitoringService::class.java)
+ intent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_STOP.index
+ )
+ cancelNextScan(context)
+ cancelNextHealthCheck(context)
+ context.stopService(intent)
+ }
+
+ fun cancelNextScan(context: Context) {
+ val nextIntent = Intent(context, BluetoothMonitoringService::class.java)
+ nextIntent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_SCAN.index
+ )
+ Scheduler.cancelServiceIntent(PENDING_SCAN_REQ_CODE, context, nextIntent)
+ }
+
+ fun cancelNextAdvertise(context: Context) {
+ val nextIntent = Intent(context, BluetoothMonitoringService::class.java)
+ nextIntent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_ADVERTISE.index
+ )
+ Scheduler.cancelServiceIntent(PENDING_ADVERTISE_REQ_CODE, context, nextIntent)
+ }
+
+ fun scheduleNextHealthCheck(context: Context, timeInMillis: Long) {
+ //cancels any outstanding check schedules.
+ cancelNextHealthCheck(context)
+
+ val nextIntent = Intent(context, BluetoothMonitoringService::class.java)
+ nextIntent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_SELF_CHECK.index
+ )
+ Scheduler.scheduleServiceIntent(
+ PENDING_HEALTH_CHECK_CODE,
+ context,
+ nextIntent,
+ timeInMillis
+ )
+ }
+
+ private fun cancelNextHealthCheck(context: Context) {
+ val nextIntent = Intent(context, BluetoothMonitoringService::class.java)
+ nextIntent.putExtra(
+ BluetoothMonitoringService.COMMAND_KEY,
+ BluetoothMonitoringService.Command.ACTION_SELF_CHECK.index
+ )
+ Scheduler.cancelServiceIntent(PENDING_HEALTH_CHECK_CODE, context, nextIntent)
+ }
+
+ fun broadcastDeviceScanned(
+ context: Context,
+ device: BluetoothDevice,
+ connectableBleDevice: ConnectablePeripheral
+ ) {
+ val intent = Intent(ACTION_DEVICE_SCANNED)
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device)
+ intent.putExtra(CONNECTION_DATA, connectableBleDevice)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
+ }
+
+ fun broadcastDeviceProcessed(context: Context, deviceAddress: String) {
+ val intent = Intent(ACTION_DEVICE_PROCESSED)
+ intent.putExtra(DEVICE_ADDRESS, deviceAddress)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
+ }
+
+
+ fun broadcastStreetPassReceived(context: Context, streetpass: ConnectionRecord) {
+ val intent = Intent(ACTION_RECEIVED_STREETPASS)
+ intent.putExtra(STREET_PASS, streetpass)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
+ }
+
+ fun broadcastStatusReceived(context: Context, statusRecord: Status) {
+ val intent = Intent(ACTION_RECEIVED_STATUS)
+ intent.putExtra(STATUS, statusRecord)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
+ }
+
+ fun broadcastDeviceDisconnected(context: Context, device: BluetoothDevice) {
+ val intent = Intent(ACTION_GATT_DISCONNECTED)
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
+ }
+
+ fun isBluetoothAvailable(): Boolean {
+ val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
+ return bluetoothAdapter != null &&
+ bluetoothAdapter.isEnabled && bluetoothAdapter.state == BluetoothAdapter.STATE_ON
+ }
+
+ fun storeBroadcastMessage(context: Context?, packet: String) {
+ CentralLog.d(TAG, "Storing packet into internal storage...")
+ val file = File(context?.filesDir, "packet")
+ file.writeText(packet)
+ }
+
+ fun retrieveBroadcastMessage(context: Context): String? {
+ val file = File(context.filesDir, "packet")
+ if (file.exists()) {
+ val readback = file.readText()
+ CentralLog.d(TAG, "fetched broadcastmessage from file: $readback")
+ return readback
+ }
+ return null
+ }
+
+ fun needToUpdate(context: Context): Boolean {
+ val nextFetchTime = Preference.getNextFetchTimeInMillis(context)
+
+ val currentTime = System.currentTimeMillis()
+
+ val update = currentTime >= nextFetchTime
+ CentralLog.i(TAG, "Need to update BM? $nextFetchTime vs $currentTime: $update")
+ return update
+ }
+
+ fun bmValid(context: Context): Boolean {
+ val expiryTime = Preference.getExpiryTimeInMillis(context)
+ val currentTime = System.currentTimeMillis()
+
+ val update = currentTime < expiryTime
+ CentralLog.i(TAG, "Is BM Valid? $expiryTime vs $currentTime: $update")
+
+ return true
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/WebViewActivity.kt b/app/src/main/java/au/gov/health/covidsafe/WebViewActivity.kt
new file mode 100644
index 0000000..1ae927d
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/WebViewActivity.kt
@@ -0,0 +1,35 @@
+package au.gov.health.covidsafe
+
+import android.os.Bundle
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.fragment.app.FragmentActivity
+import au.gov.health.covidsafe.logging.CentralLog
+
+class WebViewActivity : FragmentActivity() {
+
+ companion object {
+ val URL_ARG = "URL_ARG"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.webview)
+ val webView = findViewById(R.id.webview)
+ webView.webViewClient = WebViewClient()
+ if (intent.getStringExtra(URL_ARG).isNullOrBlank()) {
+ webView.loadUrl("https://www.australia.gov.au")
+ } else {
+ webView.loadUrl(intent.getStringExtra(URL_ARG))
+ }
+
+ val wbc: WebChromeClient = object : WebChromeClient() {
+ override fun onCloseWindow(w: WebView) {
+ CentralLog.d("WebViewActivity", "Window trying to close")
+ }
+ }
+
+ webView.webChromeClient = wbc
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEAdvertiser.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEAdvertiser.kt
new file mode 100644
index 0000000..5a39480
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEAdvertiser.kt
@@ -0,0 +1,128 @@
+package au.gov.health.covidsafe.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.le.AdvertiseCallback
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertiseSettings
+import android.bluetooth.le.BluetoothLeAdvertiser
+import android.os.Handler
+import android.os.ParcelUuid
+import au.gov.health.covidsafe.logging.CentralLog
+import java.util.*
+
+
+class BLEAdvertiser constructor(serviceUUID: String) {
+
+ private var advertiser: BluetoothLeAdvertiser? =
+ BluetoothAdapter.getDefaultAdapter().bluetoothLeAdvertiser
+ private val TAG = "BLEAdvertiser"
+ private var charLength = 3
+ private var callback: AdvertiseCallback = object : AdvertiseCallback() {
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
+ super.onStartSuccess(settingsInEffect)
+ CentralLog.i(TAG, "Advertising onStartSuccess")
+ CentralLog.i(TAG, settingsInEffect.toString())
+ isAdvertising = true
+ }
+
+ override fun onStartFailure(errorCode: Int) {
+ super.onStartFailure(errorCode)
+
+ val reason: String
+
+ when (errorCode) {
+ ADVERTISE_FAILED_ALREADY_STARTED -> {
+ reason = "ADVERTISE_FAILED_ALREADY_STARTED"
+ isAdvertising = true
+ }
+ ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> {
+ reason = "ADVERTISE_FAILED_FEATURE_UNSUPPORTED"
+ isAdvertising = false
+ }
+ ADVERTISE_FAILED_INTERNAL_ERROR -> {
+ reason = "ADVERTISE_FAILED_INTERNAL_ERROR"
+ isAdvertising = false
+ }
+ ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
+ reason = "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS"
+ isAdvertising = false
+ }
+ ADVERTISE_FAILED_DATA_TOO_LARGE -> {
+ reason = "ADVERTISE_FAILED_DATA_TOO_LARGE"
+ isAdvertising = false
+ charLength--
+ }
+
+ else -> {
+ reason = "UNDOCUMENTED"
+ }
+ }
+
+ CentralLog.d(TAG, "Advertising onStartFailure: $errorCode - $reason")
+ }
+ }
+ private val pUuid = ParcelUuid(UUID.fromString(serviceUUID))
+
+ private val settings: AdvertiseSettings? = AdvertiseSettings.Builder()
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
+ .setConnectable(true)
+ .setTimeout(0)
+ .build()
+
+ var data: AdvertiseData? = null
+
+ private var handler = Handler()
+
+ private var stopRunnable: Runnable = Runnable {
+ CentralLog.i(TAG, "Advertising stopping as scheduled.")
+ stopAdvertising()
+ }
+
+ var isAdvertising = false
+ var shouldBeAdvertising = false
+
+ private fun startAdvertisingLegacy(timeoutInMillis: Long) {
+
+ val randomUUID = UUID.randomUUID().toString()
+ val finalString = randomUUID.substring(randomUUID.length - charLength, randomUUID.length)
+ CentralLog.d(TAG, "Unique string: $finalString")
+ val serviceDataByteArray = finalString.toByteArray()
+
+ if (data == null) {
+ data = AdvertiseData.Builder()
+ .setIncludeDeviceName(false)
+ .setIncludeTxPowerLevel(true)
+ .addServiceUuid(pUuid)
+ .addManufacturerData(1023, serviceDataByteArray)
+ .build()
+ }
+
+ try {
+ CentralLog.d(TAG, "Start advertising")
+ advertiser = advertiser ?: BluetoothAdapter.getDefaultAdapter().bluetoothLeAdvertiser
+ advertiser?.startAdvertising(settings, data, callback)
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "Failed to start advertising legacy: ${e.message}")
+ }
+
+ handler.removeCallbacksAndMessages(stopRunnable)
+ handler.postDelayed(stopRunnable, timeoutInMillis)
+ }
+
+ fun startAdvertising(timeoutInMillis: Long) {
+ startAdvertisingLegacy(timeoutInMillis)
+ shouldBeAdvertising = true
+ }
+
+ private fun stopAdvertising() {
+ try {
+ CentralLog.d(TAG, "stop advertising")
+ advertiser?.stopAdvertising(callback)
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "Failed to stop advertising: ${e.message}")
+ }
+ shouldBeAdvertising = false
+ handler.removeCallbacksAndMessages(null)
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt
new file mode 100644
index 0000000..03061af
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt
@@ -0,0 +1,67 @@
+package au.gov.health.covidsafe.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.le.BluetoothLeScanner
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.bluetooth.le.ScanSettings
+import android.content.Context
+import android.os.ParcelUuid
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.logging.CentralLog
+import java.util.*
+import kotlin.collections.ArrayList
+import kotlin.properties.Delegates
+
+class BLEScanner constructor(context: Context, uuid: String, reportDelay: Long) {
+
+ private var serviceUUID: String by Delegates.notNull()
+ private var context: Context by Delegates.notNull()
+ private var scanCallback: ScanCallback? = null
+ private var reportDelay: Long by Delegates.notNull()
+
+ private var scanner: BluetoothLeScanner? =
+ BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner
+
+ private val TAG = "BLEScanner"
+
+ init {
+ this.serviceUUID = uuid
+ this.context = context
+ this.reportDelay = reportDelay
+ }
+
+ fun startScan(scanCallback: ScanCallback) {
+ val filter = ScanFilter.Builder()
+ .setServiceUuid(ParcelUuid(UUID.fromString(serviceUUID)))
+ .build()
+
+ val filters: ArrayList = ArrayList()
+ filters.add(filter)
+
+ val settings = ScanSettings.Builder()
+ .setReportDelay(reportDelay)
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build()
+
+ this.scanCallback = scanCallback
+ scanner = scanner ?: BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner
+ scanner?.startScan(filters, settings, scanCallback)
+ }
+
+ fun stopScan() {
+
+ try {
+ if (scanCallback != null && Utils.isBluetoothAvailable()) { //fixed crash if BT if turned off, stop scan will crash.
+ scanner?.stopScan(scanCallback)
+ CentralLog.d(TAG, "scanning stopped")
+ }
+ } catch (e: Throwable) {
+ CentralLog.e(
+ TAG,
+ "unable to stop scanning - callback null or bluetooth off? : ${e.localizedMessage}"
+ )
+ }
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GATT.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GATT.kt
new file mode 100644
index 0000000..e6f8aa8
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GATT.kt
@@ -0,0 +1,65 @@
+package au.gov.health.covidsafe.bluetooth.gatt
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.streetpass.PeripheralDevice
+
+const val ACTION_RECEIVED_STREETPASS =
+ "${BuildConfig.APPLICATION_ID}.ACTION_RECEIVED_STREETPASS"
+const val ACTION_RECEIVED_STATUS =
+ "${BuildConfig.APPLICATION_ID}.ACTION_RECEIVED_STATUS"
+
+const val DEVICE_ADDRESS = "${BuildConfig.APPLICATION_ID}.DEVICE_ADDRESS"
+const val CONNECTION_DATA = "${BuildConfig.APPLICATION_ID}.CONNECTION_DATA"
+
+const val STREET_PASS = "${BuildConfig.APPLICATION_ID}.STREET_PASS"
+const val STATUS = "${BuildConfig.APPLICATION_ID}.STATUS"
+
+const val ACTION_DEVICE_PROCESSED = "${BuildConfig.APPLICATION_ID}.ACTION_DEVICE_PROCESSED"
+const val ACTION_GATT_DISCONNECTED = "${BuildConfig.APPLICATION_ID}.ACTION_GATT_DISCONNECTED"
+
+class ReadRequestPayload(
+ val v: Int,
+ val msg: String,
+ val org: String,
+ peripheral: PeripheralDevice
+) {
+ val modelP = peripheral.modelP
+
+ fun getPayload(): ByteArray {
+ return gson.toJson(this).toByteArray(Charsets.UTF_8)
+ }
+
+ companion object {
+ val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
+
+ fun createReadRequestPayload(dataBytes: ByteArray) : ReadRequestPayload {
+ val dataString = String(dataBytes, Charsets.UTF_8)
+ return gson.fromJson(dataString, ReadRequestPayload::class.java)
+ }
+ }
+}
+
+class WriteRequestPayload(
+ val v: Int,
+ val msg: String,
+ val org: String,
+ val modelC: String,
+ val rssi: Int,
+ val txPower: Int?
+) {
+
+ fun getPayload(): ByteArray {
+ return gson.toJson(this).toByteArray(Charsets.UTF_8)
+ }
+
+ companion object {
+ val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
+
+ fun createReadRequestPayload(dataBytes: ByteArray) : WriteRequestPayload {
+ val dataString = String(dataBytes, Charsets.UTF_8)
+ return gson.fromJson(dataString, WriteRequestPayload::class.java)
+ }
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt
new file mode 100644
index 0000000..057cf08
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattServer.kt
@@ -0,0 +1,286 @@
+package au.gov.health.covidsafe.bluetooth.gatt
+
+import android.bluetooth.*
+import android.bluetooth.BluetoothGatt.GATT_FAILURE
+import android.bluetooth.BluetoothGatt.GATT_SUCCESS
+import android.content.Context
+import au.gov.health.covidsafe.TracerApp
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.streetpass.CentralDevice
+import au.gov.health.covidsafe.streetpass.ConnectionRecord
+import java.util.*
+import kotlin.properties.Delegates
+
+class GattServer constructor(val context: Context, serviceUUIDString: String) {
+
+ private val TAG = "GattServer"
+ private var bluetoothManager: BluetoothManager by Delegates.notNull()
+
+ private var serviceUUID: UUID by Delegates.notNull()
+ var bluetoothGattServer: BluetoothGattServer? = null
+
+ init {
+ bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ this.serviceUUID = UUID.fromString(serviceUUIDString)
+ }
+
+ private val gattServerCallback = object : BluetoothGattServerCallback() {
+
+ val writeDataPayload: MutableMap = HashMap()
+ val readPayloadMap: MutableMap = HashMap()
+
+ override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) {
+ when (newState) {
+ BluetoothProfile.STATE_CONNECTED -> {
+ CentralLog.i(TAG, "${device?.address} Connected to local GATT server")
+ device?.let {
+ val b = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
+ .contains(device)
+ }
+ }
+
+ BluetoothProfile.STATE_DISCONNECTED -> {
+ CentralLog.i(TAG, "${device?.address} Disconnected from local GATT server.")
+ device?.let {
+ Utils.broadcastDeviceDisconnected(context, device)
+ }
+
+ }
+
+ else -> {
+ CentralLog.i(TAG, "Connection status: $newState - ${device?.address}")
+ }
+ }
+ }
+
+ override fun onCharacteristicReadRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ offset: Int,
+ characteristic: BluetoothGattCharacteristic?
+ ) {
+
+ device?.let {
+
+ CentralLog.i(TAG, "onCharacteristicReadRequest from ${device.address}")
+
+ if (serviceUUID == characteristic?.uuid) {
+
+ if (Utils.bmValid(context)) {
+ val base = readPayloadMap.getOrPut(device.address, {
+ ReadRequestPayload(
+ v = TracerApp.protocolVersion,
+ msg = TracerApp.thisDeviceMsg(),
+ org = TracerApp.ORG,
+ peripheral = TracerApp.asPeripheralDevice()
+ ).getPayload()
+ })
+
+ val value = base.copyOfRange(offset, base.size)
+
+ CentralLog.i(
+ TAG,
+ "onCharacteristicReadRequest from ${device.address} - $requestId- $offset - ${String(
+ value,
+ Charsets.UTF_8
+ )}"
+ )
+
+ bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, value)
+ } else {
+ CentralLog.i(
+ TAG,
+ "onCharacteristicReadRequest from ${device.address} - $requestId- $offset - BM Expired"
+ )
+ bluetoothGattServer?.sendResponse(
+ device,
+ requestId,
+ GATT_FAILURE,
+ 0,
+ ByteArray(0)
+ )
+ }
+ } else {
+ CentralLog.i(TAG, "incorrect serviceUUID from ${device.address}")
+ bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
+ }
+ }
+
+ if (device == null) {
+ CentralLog.i(TAG, "No device")
+ }
+
+ }
+
+
+ override fun onCharacteristicWriteRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ characteristic: BluetoothGattCharacteristic,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray?
+ ) {
+
+
+ device?.let {
+ CentralLog.i(
+ TAG,
+ "onCharacteristicWriteRequest - ${device.address} - preparedWrite: $preparedWrite"
+ )
+
+ CentralLog.i(
+ TAG,
+ "onCharacteristicWriteRequest from ${device.address} - $requestId - $offset"
+ )
+
+ if (serviceUUID == characteristic.uuid) {
+ var valuePassed = ""
+ value?.let {
+ valuePassed = String(value, Charsets.UTF_8)
+ }
+ CentralLog.i(
+ TAG,
+ "onCharacteristicWriteRequest from ${device.address} - $valuePassed"
+ )
+ if (value != null) {
+ var dataBuffer = writeDataPayload[device.address]
+
+ if (dataBuffer == null) {
+ dataBuffer = ByteArray(0)
+ }
+
+ dataBuffer = dataBuffer.plus(value)
+ writeDataPayload[device.address] = dataBuffer
+
+ CentralLog.i(
+ TAG,
+ "Accumulated characteristic: ${String(
+ dataBuffer,
+ Charsets.UTF_8
+ )}"
+ )
+
+ if (responseNeeded) {
+ CentralLog.i(TAG, "Sending response offset: ${dataBuffer.size}")
+ bluetoothGattServer?.sendResponse(
+ device,
+ requestId,
+ GATT_SUCCESS,
+ dataBuffer.size,
+ value
+ )
+ }
+ }
+ } else {
+ CentralLog.i(TAG, "no data from ${device.address}")
+ bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
+ }
+
+ if (!preparedWrite) {
+ CentralLog.i(
+ TAG,
+ "onCharacteristicWriteRequest - ${device.address} - preparedWrite: $preparedWrite"
+ )
+
+ saveDataSaved(device)
+ bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
+ }
+ }
+
+ if (device == null) {
+ CentralLog.e(TAG, "Write stopped - no device")
+ }
+ }
+
+ override fun onExecuteWrite(device: BluetoothDevice, requestId: Int, execute: Boolean) {
+ super.onExecuteWrite(device, requestId, execute)
+ val data = writeDataPayload[device.address]
+
+ data.let { dataBuffer ->
+
+ if (dataBuffer != null) {
+ CentralLog.i(
+ TAG,
+ "onExecuteWrite - $requestId- ${device.address} - ${String(
+ dataBuffer,
+ Charsets.UTF_8
+ )}"
+ )
+ saveDataSaved(device)
+ bluetoothGattServer?.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
+
+ } else {
+ bluetoothGattServer?.sendResponse(device, requestId, GATT_FAILURE, 0, null)
+ }
+ }
+ }
+
+ fun saveDataSaved(device: BluetoothDevice) {
+ val data = writeDataPayload[device.address]
+
+ data?.let {
+ try {
+ val dataWritten = WriteRequestPayload.createReadRequestPayload(data)
+ device.let {
+ val centralDevice: CentralDevice?
+
+ try {
+ centralDevice = CentralDevice(dataWritten.modelC, device.address)
+ val connectionRecord = ConnectionRecord(
+ version = dataWritten.v,
+ msg = dataWritten.msg,
+ org = dataWritten.org,
+ peripheral = TracerApp.asPeripheralDevice(),
+ central = centralDevice,
+ rssi = dataWritten.rssi,
+ txPower = dataWritten.txPower
+ )
+
+ Utils.broadcastStreetPassReceived(
+ context,
+ connectionRecord
+ )
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "caught error here ${e.message}")
+ }
+ }
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "Failed to save write payload - ${e.message}")
+ }
+
+ Utils.broadcastDeviceProcessed(context, device.address)
+ writeDataPayload.remove(device.address)
+ readPayloadMap.remove(device.address)
+
+ }
+ }
+ }
+
+ fun startServer(): Boolean {
+
+ bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback)
+
+ bluetoothGattServer?.let {
+ it.clearServices()
+ return true
+ }
+ return false
+ }
+
+ fun addService(service: GattService) {
+ bluetoothGattServer?.addService(service.gattService)
+ }
+
+ fun stop() {
+ try {
+ bluetoothGattServer?.clearServices()
+ bluetoothGattServer?.close()
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "GATT server can't be closed elegantly ${e.localizedMessage}")
+ }
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattService.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattService.kt
new file mode 100644
index 0000000..5787b94
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GattService.kt
@@ -0,0 +1,34 @@
+package au.gov.health.covidsafe.bluetooth.gatt
+
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattService
+import android.content.Context
+import java.util.*
+import kotlin.properties.Delegates
+
+class GattService constructor(val context: Context, serviceUUIDString: String) {
+
+ private var serviceUUID = UUID.fromString(serviceUUIDString)
+
+ var gattService: BluetoothGattService by Delegates.notNull()
+
+ private var devicePropertyCharacteristic: BluetoothGattCharacteristic by Delegates.notNull()
+
+ init {
+ gattService = BluetoothGattService(serviceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
+ devicePropertyCharacteristic = BluetoothGattCharacteristic(
+ serviceUUID,
+ BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
+ )
+ gattService.addCharacteristic(devicePropertyCharacteristic)
+ }
+
+ fun setValue(value: String) {
+ setValue(value.toByteArray(Charsets.UTF_8))
+ }
+
+ fun setValue(value: ByteArray) {
+ devicePropertyCharacteristic.value = value
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt
new file mode 100644
index 0000000..d99bd0d
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt
@@ -0,0 +1,25 @@
+package au.gov.health.covidsafe.boot
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.logging.CentralLog
+
+class StartOnBootReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+
+ if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
+ CentralLog.d("StartOnBootReceiver", "boot completed received")
+
+ try {
+ CentralLog.d("StartOnBootReceiver", "Attempting to start service")
+ Utils.scheduleStartMonitoringService(context, 500)
+ } catch (e: Throwable) {
+ CentralLog.e("StartOnBootReceiver", e.localizedMessage)
+ e.printStackTrace()
+ }
+
+ }
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/NavigationExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/NavigationExtensions.kt
new file mode 100644
index 0000000..7dccc56
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/extensions/NavigationExtensions.kt
@@ -0,0 +1,8 @@
+package au.gov.health.covidsafe.extensions
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.navigation.Navigator
+import androidx.navigation.fragment.NavHostFragment
+
+fun Fragment.navigateTo(actionId: Int, bundle: Bundle? = null, navigatorExtras: Navigator.Extras? = null) = NavHostFragment.findNavController(this).navigate(actionId, bundle, null, navigatorExtras)
diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/NetworkExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/NetworkExtensions.kt
new file mode 100644
index 0000000..647f78c
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/extensions/NetworkExtensions.kt
@@ -0,0 +1,13 @@
+package au.gov.health.covidsafe.extensions
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+
+fun Context.isInternetAvailable(): Boolean {
+ val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
+ val capabilities = connectivityManager?.getNetworkCapabilities(connectivityManager.activeNetwork)
+ return capabilities != null && (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt
new file mode 100644
index 0000000..00173a2
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt
@@ -0,0 +1,137 @@
+package au.gov.health.covidsafe.extensions
+
+import android.Manifest.permission.ACCESS_FINE_LOCATION
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.PowerManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.NotificationManagerCompat
+import androidx.fragment.app.Fragment
+import pub.devrel.easypermissions.AppSettingsDialog
+import pub.devrel.easypermissions.EasyPermissions
+import pub.devrel.easypermissions.PermissionRequest
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.Utils
+
+const val REQUEST_ENABLE_BT = 123
+const val LOCATION = 345
+const val BATTERY_OPTIMISER = 789
+
+fun Fragment.requestAllPermissions(onEndCallback: () -> Unit) {
+ if (isBlueToothEnabled() ?: true) {
+ requestFineLocationAndCheckBleSupportThenNextPermission(onEndCallback)
+ } else {
+ requestBlueToothPermissionThenNextPermission()
+ }
+}
+
+fun Fragment.requestBlueToothPermissionThenNextPermission() {
+ val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
+ startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
+}
+
+fun Fragment.checkBLESupport() {
+ if (BluetoothAdapter.getDefaultAdapter()?.isMultipleAdvertisementSupported?.not() ?: false) {
+ activity?.let {
+ Utils.stopBluetoothMonitoringService(it)
+ }
+ }
+}
+
+private fun Fragment.requestFineLocationAndCheckBleSupportThenNextPermission(onEndCallback: () -> Unit) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ activity?.let {
+ when {
+ EasyPermissions.hasPermissions(it, ACCESS_FINE_LOCATION) -> {
+ checkBLESupport()
+ excludeFromBatteryOptimization(onEndCallback)
+ }
+ else -> {
+ EasyPermissions.requestPermissions(
+ PermissionRequest.Builder(this, LOCATION, ACCESS_FINE_LOCATION)
+ .setRationale(R.string.permission_location_rationale)
+ .build())
+ }
+ }
+ }
+ } else {
+ checkBLESupport()
+ }
+}
+
+fun Fragment.excludeFromBatteryOptimization(onEndCallback: (() -> Unit)? = null) {
+ activity?.let {
+ val powerManager =
+ it.getSystemService(AppCompatActivity.POWER_SERVICE) as PowerManager
+ val packageName = it.packageName
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val intent = Utils.getBatteryOptimizerExemptionIntent(packageName)
+ if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
+ //check if there's any activity that can handle this
+ if (Utils.canHandleIntent(intent, it.packageManager)) {
+ this.startActivityForResult(intent, BATTERY_OPTIMISER)
+ } else {
+ //no way of handling battery optimizer
+ onEndCallback?.invoke()
+ }
+ } else {
+ onEndCallback?.invoke()
+ }
+ }
+ }
+
+}
+
+fun Fragment.isBlueToothEnabled(): Boolean? {
+ val bluetoothManager = activity?.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
+ return bluetoothManager?.adapter?.isEnabled
+}
+
+fun Fragment.isPushNotificationEnabled(): Boolean? {
+ return activity?.let { activity ->
+ NotificationManagerCompat.from(activity).areNotificationsEnabled()
+ }
+}
+
+fun Fragment.isFineLocationEnabled(): Boolean? {
+ return activity?.let { activity ->
+ EasyPermissions.hasPermissions(activity, ACCESS_FINE_LOCATION)
+ }
+}
+
+fun Fragment.isNonBatteryOptimizationAllowed(): Boolean? {
+ return activity?.let { activity ->
+ val powerManager = activity.getSystemService(AppCompatActivity.POWER_SERVICE) as PowerManager?
+ val packageName = activity.packageName
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ powerManager?.isIgnoringBatteryOptimizations(packageName) ?: true
+ } else {
+ null
+ }
+ } ?: run {
+ null
+ }
+}
+
+fun Fragment.askForLocationPermission() {
+ activity?.let {
+ when {
+ EasyPermissions.hasPermissions(it, ACCESS_FINE_LOCATION) -> {
+
+ }
+ EasyPermissions.somePermissionPermanentlyDenied(this, listOf(ACCESS_FINE_LOCATION)) -> {
+ AppSettingsDialog.Builder(this).build().show()
+ }
+ else -> {
+ EasyPermissions.requestPermissions(
+ PermissionRequest.Builder(this, LOCATION, ACCESS_FINE_LOCATION)
+ .setRationale(R.string.permission_location_rationale)
+ .build())
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt
new file mode 100644
index 0000000..6c67c4e
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt
@@ -0,0 +1,30 @@
+package au.gov.health.covidsafe.extensions
+
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.URLSpan
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import au.gov.health.covidsafe.R
+
+fun TextView.toHyperlink(textToHyperLink: String? = null, onClick: () -> Unit) {
+ val text = this.text
+ val spannableString = SpannableString(text)
+ val startIndex = if (textToHyperLink.isNullOrEmpty()) {
+ 0
+ } else {
+ text.indexOf(textToHyperLink)
+ }
+ val endIndex = if (textToHyperLink.isNullOrEmpty()) {
+ spannableString.length
+ } else {
+ text.indexOf(textToHyperLink) + textToHyperLink.length
+ }
+ spannableString.setSpan(URLSpan(""), startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ this.setText(spannableString, TextView.BufferType.SPANNABLE)
+ this.setLinkTextColor(ContextCompat.getColor(context, R.color.dark_green))
+ this.setOnClickListener {
+ onClick.invoke()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt b/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt
new file mode 100644
index 0000000..17423b2
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt
@@ -0,0 +1,43 @@
+package au.gov.health.covidsafe.factory
+
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.networking.service.AwsClient
+
+interface NetworkFactory {
+ companion object {
+ private val logging = HttpLoggingInterceptor()
+ .setLevel(HttpLoggingInterceptor.Level.BODY)
+
+ val awsClient: AwsClient by lazy {
+ RetrofitServiceGenerator.createService(AwsClient::class.java)
+ }
+
+ val okHttpClient: OkHttpClient by lazy {
+ val builder = OkHttpClient.Builder()
+ if (!builder.interceptors().contains(logging) && BuildConfig.DEBUG) {
+ builder.addInterceptor(logging)
+ }
+ builder.build()
+ }
+ }
+}
+
+object RetrofitServiceGenerator {
+ private val builder = Retrofit.Builder()
+ .baseUrl(BuildConfig.BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+
+ private var retrofit = builder.build()
+
+ fun createService(
+ serviceClass: Class): S {
+ builder.client(NetworkFactory.okHttpClient)
+ retrofit = builder.build()
+
+ return retrofit.create(serviceClass)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/Either.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/Either.kt
new file mode 100644
index 0000000..56c602e
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/interactor/Either.kt
@@ -0,0 +1,14 @@
+package au.gov.health.covidsafe.interactor
+
+sealed class Either {
+
+ inline fun fold(failed: (F) -> T, succeeded: (S) -> T): T =
+ when (this) {
+ is Failure -> failed(failure)
+ is Success -> succeeded(success)
+ }
+}
+
+data class Failure(val failure: F) : Either()
+
+data class Success(val success: S) : Either()
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/UseCase.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/UseCase.kt
new file mode 100644
index 0000000..4cefddb
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/interactor/UseCase.kt
@@ -0,0 +1,76 @@
+package au.gov.health.covidsafe.interactor
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import kotlinx.coroutines.*
+import retrofit2.Response
+import kotlin.math.pow
+
+private val RETRIES_LIMIT = 3
+
+abstract class UseCase(lifecycle: Lifecycle) : CoroutineScope by MainScope(), LifecycleObserver where Type : Any? {
+
+ private var job: Job = Job()
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ abstract suspend fun run(params: Params): Either
+
+ operator fun invoke(params: Params, onSuccess: (Type) -> Unit, onFailure: (Exception) -> Unit) {
+ job.cancel()
+ job = launch(context = coroutineContext) {
+ val result = async(context = Dispatchers.IO) {
+ run(params)
+ }
+ result.await().fold(
+ failed = { onFailure(it) },
+ succeeded = { onSuccess(it) }
+ )
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ fun onStop() {
+ job.cancel()
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ fun onDestroy() {
+ cancel()
+ }
+
+ protected suspend fun retryRetrofitCall(call: () -> Response?): Response? {
+ var response = call.invoke()
+ var retryCount = 0
+ while ((response == null || (!response.isSuccessful && response.code() != 403) || response.body() == null) && retryCount < RETRIES_LIMIT) {
+ val interval = 2.toDouble().pow(retryCount.toDouble()).toLong() * 1000
+ delay(interval)
+ response = call.invoke()
+ retryCount++
+ }
+ return response
+ }
+
+ protected suspend fun retryOkhttpCall(call: () -> okhttp3.Response?): okhttp3.Response? {
+ var response = call.invoke()
+ var retryCount = 0
+ while ((response == null || !response.isSuccessful || response.body == null) && retryCount < RETRIES_LIMIT) {
+ val interval = 2.toDouble().pow(retryCount.toDouble()).toLong() * 1000
+ delay(interval)
+ response = call.invoke()
+ retryCount++
+ }
+ return if (response != null && response.isSuccessful) {
+ response
+ } else {
+ null
+
+ }
+ }
+
+ object None
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt
new file mode 100644
index 0000000..911c169
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt
@@ -0,0 +1,60 @@
+package au.gov.health.covidsafe.interactor.usecase
+
+import androidx.lifecycle.Lifecycle
+import au.gov.health.covidsafe.interactor.Either
+import au.gov.health.covidsafe.interactor.Failure
+import au.gov.health.covidsafe.interactor.Success
+import au.gov.health.covidsafe.interactor.UseCase
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.networking.request.OTPChallengeRequest
+import au.gov.health.covidsafe.networking.response.OTPChallengeResponse
+import au.gov.health.covidsafe.networking.service.AwsClient
+
+class GetOnboardingOtp(private val awsClient: AwsClient, lifecycle: Lifecycle) : UseCase(lifecycle) {
+
+ private val TAG = this.javaClass.simpleName
+
+ override suspend fun run(params: GetOtpParams): Either {
+ return try {
+ val response = awsClient.initiateAuth(
+ OTPChallengeRequest(params.phoneNumber,
+ params.deviceId,
+ params.postCode,
+ params.age,
+ params.name)).execute()
+ when {
+ response.code() == 200 -> {
+ response.body()?.let { body ->
+ CentralLog.d(TAG, "onCodeSent: ${response.body()?.challengeName}")
+ Success(body)
+ } ?: run {
+ CentralLog.d(TAG, "AWSAuthInvalidBody")
+ Failure(GetOnboardingOtpException.GetOtpServiceException(response.code()))
+ }
+ }
+ response.code() == 400 -> {
+ CentralLog.d(TAG, "AWSAuthInvalidNumber")
+ Failure(GetOnboardingOtpException.GetOtpInvalidNumberException)
+ }
+ else -> {
+ CentralLog.d(TAG, "AWSAuthServiceError")
+ Failure(GetOnboardingOtpException.GetOtpServiceException(response.code()))
+ }
+ }
+ } catch (e: Exception) {
+ CentralLog.d(TAG, "AWSAuthInvalidChallengeRequest", e)
+ Failure(GetOnboardingOtpException.GetOtpServiceException())
+ }
+ }
+}
+
+data class GetOtpParams(internal val phoneNumber: String,
+ internal val deviceId: String,
+ internal val postCode: String?,
+ internal val age: String?,
+ internal val name: String?)
+
+sealed class GetOnboardingOtpException : Exception() {
+ class GetOtpServiceException(val code: Int? = null) : GetOnboardingOtpException()
+ object GetOtpInvalidNumberException : GetOnboardingOtpException()
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetUploadOtp.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetUploadOtp.kt
new file mode 100644
index 0000000..fab7186
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetUploadOtp.kt
@@ -0,0 +1,34 @@
+package au.gov.health.covidsafe.interactor.usecase
+
+import androidx.lifecycle.Lifecycle
+import au.gov.health.covidsafe.interactor.Either
+import au.gov.health.covidsafe.interactor.Failure
+import au.gov.health.covidsafe.interactor.Success
+import au.gov.health.covidsafe.interactor.UseCase
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.networking.response.UploadOTPResponse
+import au.gov.health.covidsafe.networking.service.AwsClient
+
+class GetUploadOtp(private val awsClient: AwsClient, lifecycle: Lifecycle)
+ : UseCase(lifecycle) {
+
+ private val TAG = this.javaClass.simpleName
+
+ override suspend fun run(params: String): Either {
+ return try {
+ val response = awsClient.requestUploadOtp("Bearer $params").execute()
+ return if (response.code() == 200) {
+ CentralLog.d(TAG, "onCodeUpload")
+ Success(response.body())
+ } else {
+ Failure(GetUploadOtpException.GetUploadOtpServiceException(response.code()))
+ }
+ } catch (e: Exception) {
+ Failure(e)
+ }
+ }
+}
+
+sealed class GetUploadOtpException : Exception() {
+ class GetUploadOtpServiceException(val code: Int?) : GetUploadOtpException()
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt
new file mode 100644
index 0000000..7fba051
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt
@@ -0,0 +1,69 @@
+package au.gov.health.covidsafe.interactor.usecase
+
+import android.content.Context
+import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.delay
+import retrofit2.Response
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.interactor.Either
+import au.gov.health.covidsafe.interactor.Failure
+import au.gov.health.covidsafe.interactor.Success
+import au.gov.health.covidsafe.interactor.UseCase
+import au.gov.health.covidsafe.networking.response.BroadcastMessageResponse
+import au.gov.health.covidsafe.networking.service.AwsClient
+import kotlin.math.pow
+
+class UpdateBroadcastMessageAndPerformScanWithExponentialBackOff(private val awsClient: AwsClient,
+ private val context: Context,
+ lifecycle: Lifecycle) : UseCase(lifecycle) {
+
+ private val TAG = this.javaClass.simpleName
+ private val RETRIES_LIMIT = 3
+
+ override suspend fun run(params: Void?): Either {
+ val jwtToken = Preference.getEncrypterJWTToken(context)
+ return jwtToken?.let { jwtToken ->
+ var response = call(jwtToken)
+ var retryCount = 0
+ while ((response == null || !response.isSuccessful || response.body() == null) && retryCount < RETRIES_LIMIT) {
+ val interval = 2.toDouble().pow(retryCount.toDouble()).toLong() * 1000
+ delay(interval)
+ response = call(jwtToken)
+ retryCount++
+ }
+
+ if (response != null && response.isSuccessful) {
+ response.body()?.let { broadcastMessageResponse ->
+ if (broadcastMessageResponse.tempId.isNullOrEmpty()) {
+ Failure(Exception())
+ } else {
+ val expiryTime = broadcastMessageResponse.expiryTime
+ val expiry = expiryTime?.toLongOrNull() ?: 0
+ Preference.putExpiryTimeInMillis(context, expiry * 1000)
+ val refreshTime = broadcastMessageResponse.refreshTime
+ val refresh = refreshTime?.toLongOrNull() ?: 0
+ Preference.putNextFetchTimeInMillis(context, refresh * 1000)
+ Utils.storeBroadcastMessage(context, broadcastMessageResponse.tempId)
+ Success(broadcastMessageResponse)
+ }
+ } ?: run {
+ Failure(Exception())
+ }
+ } else {
+ Failure(Exception())
+ }
+ } ?: run {
+ return Failure(Exception())
+ }
+ }
+
+ private fun call(jwtToken: String): Response? {
+ return try {
+ awsClient.getTempId("Bearer $jwtToken").execute()
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt
new file mode 100644
index 0000000..2f766b2
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt
@@ -0,0 +1,86 @@
+package au.gov.health.covidsafe.interactor.usecase
+
+import android.content.Context
+import androidx.lifecycle.Lifecycle
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.TracerApp
+import au.gov.health.covidsafe.interactor.Either
+import au.gov.health.covidsafe.interactor.Failure
+import au.gov.health.covidsafe.interactor.Success
+import au.gov.health.covidsafe.interactor.UseCase
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.networking.service.AwsClient
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
+import au.gov.health.covidsafe.ui.upload.model.ExportData
+import com.google.gson.Gson
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+
+class UploadData(private val awsClient: AwsClient,
+ private val okHttpClient: OkHttpClient,
+ private val context: Context?,
+ lifecycle: Lifecycle)
+ : UseCase(lifecycle) {
+
+ private val TAG = this.javaClass.simpleName
+
+ override suspend fun run(params: String): Either {
+ val jwtToken = Preference.getEncrypterJWTToken(context)
+ return jwtToken?.let { jwtToken ->
+ try {
+ val initialUploadResponse = retryRetrofitCall {
+ awsClient.initiateUpload("Bearer $jwtToken", params).execute()
+ }
+ if (initialUploadResponse == null) {
+ Failure(UploadDataException.UploadDataIncorrectPinException)
+ } else if (initialUploadResponse.isSuccessful) {
+ val uploadLink = initialUploadResponse.body()?.uploadLink
+ if (uploadLink.isNullOrEmpty()) {
+ Failure(Exception())
+ } else {
+ zipAndUploadData(uploadLink)
+ }
+ } else if (initialUploadResponse.code() == 400) {
+ Failure(UploadDataException.UploadDataIncorrectPinException)
+ } else if (initialUploadResponse.code() == 403) {
+ Failure(UploadDataException.UploadDataJwtExpiredException)
+ } else {
+ Failure(Exception())
+ }
+ } catch (e: Exception) {
+ Failure(e)
+ }
+ } ?: run {
+ return Failure(Exception())
+ }
+ }
+
+ private suspend fun zipAndUploadData(uploadLink: String): Either {
+ val exportedData = ExportData(StreetPassRecordStorage(TracerApp.AppContext).getAllRecords())
+ CentralLog.d(TAG, "records: ${exportedData.records}")
+
+ val jsonData = Gson().toJson(exportedData)
+
+ val request = Request.Builder()
+ .url(uploadLink)
+ .put(jsonData.toRequestBody(null))
+ .build()
+ return try {
+ val response = retryOkhttpCall { okHttpClient.newCall(request).execute() }
+ return if (response == null) {
+ Failure(Exception())
+ } else {
+ Success(None)
+ }
+ } catch (e: Exception) {
+ Failure(Exception())
+ }
+ }
+
+}
+
+sealed class UploadDataException : Exception() {
+ object UploadDataIncorrectPinException : UploadDataException()
+ object UploadDataJwtExpiredException : UploadDataException()
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/logging/CentralLog.kt b/app/src/main/java/au/gov/health/covidsafe/logging/CentralLog.kt
new file mode 100644
index 0000000..be27312
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/logging/CentralLog.kt
@@ -0,0 +1,85 @@
+package au.gov.health.covidsafe.logging
+
+import android.os.Build
+import android.os.PowerManager
+import android.util.Log
+import au.gov.health.covidsafe.BuildConfig
+
+class CentralLog {
+
+ companion object {
+
+ private var pm: PowerManager? = null
+
+ fun setPowerManager(powerManager: PowerManager) {
+ pm = powerManager
+ }
+
+ private fun shouldLog(): Boolean {
+ return BuildConfig.DEBUG
+ }
+
+ private fun getIdleStatus(): String {
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return if (true == pm?.isDeviceIdleMode) {
+ " IDLE "
+ } else {
+ " NOT-IDLE "
+ }
+ }
+ return " NO-DOZE-FEATURE "
+ }
+
+ fun d(tag: String, message: String) {
+ if (!shouldLog()) {
+ return
+ }
+
+ Log.d(tag, getIdleStatus() + message)
+ }
+
+ fun d(tag: String, message: String, e: Throwable?) {
+ if (!shouldLog()) {
+ return
+ }
+
+ Log.d(tag, getIdleStatus() + message, e)
+ }
+
+
+ fun w(tag: String, message: String) {
+ if (!shouldLog()) {
+ return
+ }
+
+ Log.w(tag, getIdleStatus() + message)
+ }
+
+ fun i(tag: String, message: String) {
+ if (!shouldLog()) {
+ return
+ }
+
+ Log.i(tag, getIdleStatus() + message)
+ }
+
+ fun e(tag: String, message: String) {
+ if (!shouldLog()) {
+ return
+ }
+
+ Log.e(tag, getIdleStatus() + message)
+ }
+
+ fun e(tag: String, message: String, exception: Exception) {
+ if (!shouldLog()) {
+ return
+ }
+
+ Log.e(tag, getIdleStatus() + message, exception)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/request/AuthChallengeRequest.kt b/app/src/main/java/au/gov/health/covidsafe/networking/request/AuthChallengeRequest.kt
new file mode 100644
index 0000000..ab0b0e0
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/request/AuthChallengeRequest.kt
@@ -0,0 +1,6 @@
+package au.gov.health.covidsafe.networking.request
+
+import androidx.annotation.Keep
+
+@Keep
+data class AuthChallengeRequest(val session: String?, val code: String?)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt b/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt
new file mode 100644
index 0000000..81c4c1b
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt
@@ -0,0 +1,10 @@
+package au.gov.health.covidsafe.networking.request
+
+import androidx.annotation.Keep
+
+@Keep
+data class OTPChallengeRequest(val phone_number: String,
+ val device_id: String,
+ val postcode: String?,
+ val age: String?,
+ val name: String?)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/AuthChallengeResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/AuthChallengeResponse.kt
new file mode 100644
index 0000000..c09d56c
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/AuthChallengeResponse.kt
@@ -0,0 +1,6 @@
+package au.gov.health.covidsafe.networking.response
+
+import androidx.annotation.Keep
+
+@Keep
+data class AuthChallengeResponse(val token: String, val uuid: String, val token_expiry: String, val pin: String)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/BroadcastMessageResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/BroadcastMessageResponse.kt
new file mode 100644
index 0000000..62772f5
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/BroadcastMessageResponse.kt
@@ -0,0 +1,6 @@
+package au.gov.health.covidsafe.networking.response
+
+import androidx.annotation.Keep
+
+@Keep
+data class BroadcastMessageResponse(val tempId: String?, val expiryTime: String?, val refreshTime: String?)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/InitiateUploadResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/InitiateUploadResponse.kt
new file mode 100644
index 0000000..0fcf7e8
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/InitiateUploadResponse.kt
@@ -0,0 +1,9 @@
+package au.gov.health.covidsafe.networking.response
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+
+@Keep
+data class InitiateUploadResponse(@SerializedName("UploadLink") val uploadLink: String,
+ @SerializedName("ExpiresIn") val expiresIn: String,
+ @SerializedName("UploadPrefix") val uploadPrefix: String)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/OTPChallengeResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/OTPChallengeResponse.kt
new file mode 100644
index 0000000..48ea669
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/OTPChallengeResponse.kt
@@ -0,0 +1,6 @@
+package au.gov.health.covidsafe.networking.response
+
+import androidx.annotation.Keep
+
+@Keep
+data class OTPChallengeResponse(val session: String, val challengeName: String)
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/UploadOTPResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/UploadOTPResponse.kt
new file mode 100644
index 0000000..d9c6bba
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/UploadOTPResponse.kt
@@ -0,0 +1,6 @@
+package au.gov.health.covidsafe.networking.response
+
+import androidx.annotation.Keep
+
+@Keep
+class UploadOTPResponse
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt b/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt
new file mode 100644
index 0000000..1514347
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt
@@ -0,0 +1,33 @@
+package au.gov.health.covidsafe.networking.service
+
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.networking.request.AuthChallengeRequest
+import au.gov.health.covidsafe.networking.request.OTPChallengeRequest
+import au.gov.health.covidsafe.networking.response.*
+import retrofit2.Call
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.POST
+
+interface AwsClient {
+
+ @POST(BuildConfig.END_POINT_PREFIX + "/initiateAuth")
+ fun initiateAuth(@Body body : OTPChallengeRequest) : Call
+
+ @POST(BuildConfig.END_POINT_PREFIX + "/respondToAuthChallenge")
+ fun respondToAuthChallenge(@Body body : AuthChallengeRequest) : Call
+
+ @GET(BuildConfig.END_POINT_PREFIX + "/getTempId")
+ fun getTempId(@Header("Authorization") jwtToken: String?) : Call
+
+ @GET(BuildConfig.END_POINT_PREFIX + "/initiateDataUpload")
+ fun initiateUpload(@Header("Authorization") jwtToken: String?,@Header("pin") pin : String) : Call
+
+ @GET(BuildConfig.END_POINT_PREFIX + "/initiateDataUpload")
+ fun initiateReUpload(@Header("Authorization") jwtToken: String?): Call
+
+ @GET(BuildConfig.END_POINT_PREFIX + "/requestUploadOtp")
+ fun requestUploadOtp(@Header("Authorization") jwtToken : String?) : Call
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt
new file mode 100644
index 0000000..e3596d1
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt
@@ -0,0 +1,114 @@
+package au.gov.health.covidsafe.notifications
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import au.gov.health.covidsafe.HomeActivity
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.DAILY_UPLOAD_NOTIFICATION_CODE
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_ACTIVITY
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_WIZARD_REQ_CODE
+
+class NotificationTemplates {
+
+ companion object {
+
+ fun getRunningNotification(context: Context, channel: String): Notification {
+
+ val intent = Intent(context, HomeActivity::class.java)
+
+ val activityPendingIntent = PendingIntent.getActivity(
+ context, PENDING_ACTIVITY,
+ intent, 0
+ )
+
+ val builder = NotificationCompat.Builder(context, channel)
+ .setContentTitle(context.getText(R.string.service_ok_title))
+ .setContentText(context.getText(R.string.service_ok_body))
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setSmallIcon(R.drawable.ic_notification_icon)
+ .setContentIntent(activityPendingIntent)
+ .setTicker(context.getText(R.string.service_ok_body))
+ .setStyle(NotificationCompat.BigTextStyle().bigText(context.getText(R.string.service_ok_body)))
+ .setWhen(System.currentTimeMillis())
+ .setSound(null)
+ .setVibrate(null)
+ .setColor(ContextCompat.getColor(context, R.color.notification_tint))
+
+ return builder.build()
+ }
+
+ fun lackingThingsNotification(context: Context, channel: String): Notification {
+
+
+ val intent = Intent(context, HomeActivity::class.java)
+ intent.putExtra("page", 3)
+
+ val activityPendingIntent = PendingIntent.getActivity(
+ context, PENDING_WIZARD_REQ_CODE,
+ intent, 0
+ )
+
+ val builder = NotificationCompat.Builder(context, channel)
+ .setContentTitle(context.getText(R.string.service_not_ok_title))
+ .setContentText(context.getText(R.string.service_not_ok_body))
+ .setStyle(NotificationCompat.BigTextStyle().bigText(context.getText(R.string.service_not_ok_body)))
+
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setSmallIcon(R.drawable.ic_notification_warning)
+ .setTicker(context.getText(R.string.service_not_ok_body))
+ .addAction(
+ R.drawable.ic_notification_setting,
+ context.getText(R.string.service_not_ok_action),
+ activityPendingIntent
+ )
+ .setContentIntent(activityPendingIntent)
+ .setWhen(System.currentTimeMillis())
+ .setSound(null)
+ .setVibrate(null)
+ .setColor(ContextCompat.getColor(context, R.color.notification_tint))
+
+ return builder.build()
+ }
+
+ fun getUploadReminder(context: Context, channel: String): Notification {
+ val intent = Intent(context, HomeActivity::class.java)
+ intent.putExtra("uploadNotification", true)
+
+ val activityPendingIntent = PendingIntent.getActivity(
+ context, DAILY_UPLOAD_NOTIFICATION_CODE,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ val builder = NotificationCompat.Builder(context, channel)
+ .setContentTitle(context.getText(R.string.upload_your_data_title))
+ .setContentText(context.getText(R.string.upload_your_data_description))
+ .setOngoing(false)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setSmallIcon(R.drawable.ic_notification_icon)
+ .setTicker(context.getText(R.string.upload_your_data_description))
+ .setStyle(NotificationCompat.BigTextStyle().bigText(context.getText(R.string.upload_your_data_description)))
+ .setAutoCancel(true)
+
+ .addAction(
+ R.drawable.ic_notification_setting,
+ context.getText(R.string.upload_data_action),
+ activityPendingIntent
+ )
+ .setContentIntent(activityPendingIntent)
+ .setWhen(System.currentTimeMillis())
+ .setSound(null)
+ .setVibrate(null)
+ .setColor(ContextCompat.getColor(context, R.color.notification_tint))
+
+ return builder.build()
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt
new file mode 100644
index 0000000..4cbdfff
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt
@@ -0,0 +1,64 @@
+package au.gov.health.covidsafe.receivers
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_PRIVACY_CLEANER_CODE
+import au.gov.health.covidsafe.services.SensorMonitoringService.Companion.TAG
+import au.gov.health.covidsafe.status.persistence.StatusRecordStorage
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import java.util.*
+import kotlin.coroutines.CoroutineContext
+
+class PrivacyCleanerReceiver : BroadcastReceiver(), CoroutineScope {
+
+ private val TAG = this.javaClass.simpleName
+
+ private var job: Job = Job()
+
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Default + job
+
+ companion object {
+
+ private fun getIntent(context: Context, requestCode: Int): PendingIntent? {
+ val intent = Intent(context, PrivacyCleanerReceiver::class.java)
+ return PendingIntent.getBroadcast(context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT)
+ }
+
+ fun startAlarm(context: Context) {
+ val pendingIntent = getIntent(context, PENDING_PRIVACY_CLEANER_CODE)
+ val alarm = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ alarm.setRepeating(AlarmManager.RTC, System.currentTimeMillis(), AlarmManager.INTERVAL_DAY, pendingIntent)
+ }
+
+ suspend fun cleanDb(context: Context) {
+ val twentyOneDaysAgo = Calendar.getInstance()
+ twentyOneDaysAgo.set(Calendar.HOUR_OF_DAY, 23)
+ twentyOneDaysAgo.set(Calendar.MINUTE, 59)
+ twentyOneDaysAgo.set(Calendar.SECOND, 59)
+ twentyOneDaysAgo.add(Calendar.DATE, -21)
+
+ val countStreetDeleted = StreetPassRecordStorage(context).deleteDataOlderThan(twentyOneDaysAgo.timeInMillis)
+ val countStatusDeleted = StatusRecordStorage(context).deleteDataOlderThan(twentyOneDaysAgo.timeInMillis)
+
+ CentralLog.i(TAG, "Street info deleted count : $countStreetDeleted")
+ CentralLog.i(TAG, "Status info deleted count : $countStatusDeleted")
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ launch {
+ cleanDb(context)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt
new file mode 100644
index 0000000..3502ff6
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt
@@ -0,0 +1,23 @@
+package au.gov.health.covidsafe.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.logging.CentralLog
+
+class UpgradeReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+
+ try {
+ if (Intent.ACTION_MY_PACKAGE_REPLACED != intent!!.action) return
+ context?.let {
+ CentralLog.i("UpgradeReceiver", "Starting service from upgrade receiver")
+ Utils.startBluetoothMonitoringService(context)
+ }
+ } catch (e: Exception) {
+ CentralLog.e("UpgradeReceiver", "Unable to handle upgrade: ${e.localizedMessage}")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/scheduler/Scheduler.kt b/app/src/main/java/au/gov/health/covidsafe/scheduler/Scheduler.kt
new file mode 100644
index 0000000..381163d
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/scheduler/Scheduler.kt
@@ -0,0 +1,53 @@
+package au.gov.health.covidsafe.scheduler
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.SystemClock
+
+object Scheduler {
+
+ fun scheduleServiceIntent(
+ requestCode: Int,
+ context: Context,
+ intent: Intent,
+ timeFromNowInMillis: Long
+ ) {
+ val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val alarmIntent = PendingIntent.getService(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmMgr.setExactAndAllowWhileIdle(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + timeFromNowInMillis, alarmIntent
+ )
+
+ } else {
+ alarmMgr.set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + timeFromNowInMillis, alarmIntent
+ )
+ }
+
+ }
+
+ fun cancelServiceIntent(requestCode: Int, context: Context, intent: Intent) {
+ val alarmIntent =
+ PendingIntent.getService(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ alarmIntent.cancel()
+ }
+
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt b/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt
new file mode 100644
index 0000000..8779923
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt
@@ -0,0 +1,668 @@
+package au.gov.health.covidsafe.services
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.content.*
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+import androidx.annotation.Keep
+import androidx.lifecycle.LifecycleService
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.bluetooth.BLEAdvertiser
+import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STATUS
+import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STREETPASS
+import au.gov.health.covidsafe.bluetooth.gatt.STATUS
+import au.gov.health.covidsafe.bluetooth.gatt.STREET_PASS
+import au.gov.health.covidsafe.factory.NetworkFactory
+import au.gov.health.covidsafe.interactor.usecase.UpdateBroadcastMessageAndPerformScanWithExponentialBackOff
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.notifications.NotificationTemplates
+import au.gov.health.covidsafe.receivers.PrivacyCleanerReceiver
+import au.gov.health.covidsafe.status.Status
+import au.gov.health.covidsafe.status.persistence.StatusRecord
+import au.gov.health.covidsafe.status.persistence.StatusRecordStorage
+import au.gov.health.covidsafe.streetpass.ConnectionRecord
+import au.gov.health.covidsafe.streetpass.StreetPassScanner
+import au.gov.health.covidsafe.streetpass.StreetPassServer
+import au.gov.health.covidsafe.streetpass.StreetPassWorker
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import pub.devrel.easypermissions.EasyPermissions
+import java.lang.ref.WeakReference
+import kotlin.coroutines.CoroutineContext
+@Keep
+class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
+
+ private var mNotificationManager: NotificationManager? = null
+
+ @Keep
+ private lateinit var serviceUUID: String
+
+ private var streetPassServer: StreetPassServer? = null
+ private var streetPassScanner: StreetPassScanner? = null
+ private var advertiser: BLEAdvertiser? = null
+
+ private var worker: StreetPassWorker? = null
+
+ private val streetPassReceiver = StreetPassReceiver()
+ private val statusReceiver = StatusReceiver()
+ private val bluetoothStatusReceiver = BluetoothStatusReceiver()
+
+ private lateinit var streetPassRecordStorage: StreetPassRecordStorage
+ private lateinit var statusRecordStorage: StatusRecordStorage
+
+ private var job: Job = Job()
+
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ private lateinit var commandHandler: CommandHandler
+
+ private lateinit var mService: SensorMonitoringService
+ private var mBound: Boolean = false
+
+ private lateinit var localBroadcastManager: LocalBroadcastManager
+
+ private val awsClient = NetworkFactory.awsClient
+
+ /** Defines callbacks for service binding, passed to bindService() */
+ private val connection = object : ServiceConnection {
+
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ // We've bound to LocalService, cast the IBinder and get LocalService instance
+ val binder = service as SensorMonitoringService.LocalBinder
+ mService = binder.getService()
+ mBound = true
+ }
+
+ override fun onServiceDisconnected(arg0: ComponentName) {
+ mBound = false
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ localBroadcastManager = LocalBroadcastManager.getInstance(this)
+ setup()
+ }
+
+ private fun setup() {
+ val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
+
+ CentralLog.setPowerManager(pm)
+
+ commandHandler = CommandHandler(WeakReference(this))
+
+ CentralLog.d(TAG, "Creating service - BluetoothMonitoringService")
+ serviceUUID = BuildConfig.BLE_SSID
+
+ worker = StreetPassWorker(this.applicationContext)
+
+ unregisterReceivers()
+ registerReceivers()
+
+ streetPassRecordStorage = StreetPassRecordStorage(this.applicationContext)
+ statusRecordStorage = StatusRecordStorage(this.applicationContext)
+ PrivacyCleanerReceiver.startAlarm(this.applicationContext)
+ setupNotifications()
+ broadcastMessage = Utils.retrieveBroadcastMessage(this.applicationContext)
+ }
+
+ fun teardown() {
+ streetPassServer?.tearDown()
+ streetPassServer = null
+
+ streetPassScanner?.stopScan()
+ streetPassScanner = null
+
+ commandHandler.removeCallbacksAndMessages(null)
+
+ Utils.cancelBMUpdateCheck(this.applicationContext)
+ Utils.cancelNextScan(this.applicationContext)
+ Utils.cancelNextAdvertise(this.applicationContext)
+ }
+
+ private fun setupNotifications() {
+
+ val mNotificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ // Android O requires a Notification Channel.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val name = CHANNEL_SERVICE
+ // Create the channel for the notification
+ val mChannel =
+ NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW)
+ mChannel.enableLights(false)
+ mChannel.enableVibration(true)
+ mChannel.vibrationPattern = longArrayOf(0L)
+ mChannel.setSound(null, null)
+ mChannel.setShowBadge(false)
+
+ // Set the Notification Channel for the Notification Manager.
+ mNotificationManager.createNotificationChannel(mChannel)
+ }
+ }
+
+ private fun hasLocationPermissions(): Boolean {
+ val perms = Utils.getRequiredPermissions()
+ return EasyPermissions.hasPermissions(this.applicationContext, *perms)
+ }
+
+ private fun isBluetoothEnabled(): Boolean {
+ var btOn = false
+ val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
+ val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ bluetoothManager.adapter
+ }
+
+ bluetoothAdapter?.let {
+ btOn = it.isEnabled
+ }
+ return btOn
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ CentralLog.i(TAG, "Service onStartCommand")
+
+ // Bind to LocalService
+ Intent(this.applicationContext, SensorMonitoringService::class.java).also { intent ->
+ bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ }
+
+ //check for permissions
+ if (!hasLocationPermissions() || !isBluetoothEnabled()) {
+ CentralLog.i(
+ TAG,
+ "location permission: ${hasLocationPermissions()} bluetooth: ${isBluetoothEnabled()}"
+ )
+ val notif =
+ NotificationTemplates.lackingThingsNotification(this.applicationContext, CHANNEL_ID)
+ startForeground(NOTIFICATION_ID, notif)
+ return START_STICKY
+ }
+
+ intent?.let {
+ val cmd = intent.getIntExtra(COMMAND_KEY, Command.INVALID.index)
+ runService(Command.findByValue(cmd))
+
+ return START_STICKY
+ }
+
+ if (intent == null) {
+ CentralLog.e(TAG, "Nothing in intent @ onStartCommand")
+ commandHandler.startBluetoothMonitoringService()
+ }
+
+ // Tells the system to not try to recreate the service after it has been killed.
+ return START_STICKY
+ }
+
+ fun runService(cmd: Command?) {
+
+ CentralLog.i(TAG, "Command is:${cmd?.string}")
+
+ //check for permissions
+ if (!hasLocationPermissions() || !isBluetoothEnabled()) {
+ CentralLog.i(
+ TAG,
+ "location permission: ${hasLocationPermissions()} bluetooth: ${isBluetoothEnabled()}"
+ )
+ val notif =
+ NotificationTemplates.lackingThingsNotification(this.applicationContext, CHANNEL_ID)
+ startForeground(NOTIFICATION_ID, notif)
+ return
+ }
+
+ when (cmd) {
+ Command.ACTION_START -> {
+ setupService()
+ actionStart()
+ Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval)
+ Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval)
+ }
+
+ Command.ACTION_SCAN -> {
+ actionScan()
+ }
+
+ Command.ACTION_ADVERTISE -> {
+ actionAdvertise()
+ }
+
+ Command.ACTION_UPDATE_BM -> {
+ actionUpdateBm()
+ }
+
+ Command.ACTION_STOP -> {
+ actionStop()
+ }
+
+ Command.ACTION_SELF_CHECK -> {
+ actionHealthCheck()
+ }
+
+ else -> CentralLog.i(TAG, "Invalid command: $cmd. Nothing to do")
+ }
+ }
+
+ private fun actionStop() {
+ stopForeground(true)
+ stopSelf()
+ CentralLog.w(TAG, "Service Stopping")
+ }
+
+ private fun actionHealthCheck() {
+ Utils.scheduleNextHealthCheck(this.applicationContext, healthCheckInterval)
+ performHealthCheck()
+ }
+
+ private fun actionStart() {
+ if (Preference.isOnBoarded(this)) {
+ CentralLog.d(TAG, "Service Starting ")
+
+ startForeground(
+ NOTIFICATION_ID,
+ NotificationTemplates.getRunningNotification(
+ this.applicationContext,
+ CHANNEL_ID
+ )
+ )
+ //ensure BM is ready here
+ if (Preference.isOnBoarded(this) && Utils.needToUpdate(this.applicationContext) || broadcastMessage == null) {
+ //need to pull new BM
+
+ UpdateBroadcastMessageAndPerformScanWithExponentialBackOff(awsClient, applicationContext, lifecycle).invoke(
+ params = null,
+ onSuccess = {
+ broadcastMessage = it.tempId
+ setupCycles()
+ },
+ onFailure = {
+ }
+ )
+ } else if (Preference.isOnBoarded(this)) {
+ setupCycles()
+ }
+ }
+ }
+
+
+ private fun actionUpdateBm() {
+ Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval)
+
+ CentralLog.i(TAG, "checking need to update BM")
+ if (Preference.isOnBoarded(this) && Utils.needToUpdate(this.applicationContext) || broadcastMessage == null) {
+ //need to pull new BM
+
+ UpdateBroadcastMessageAndPerformScanWithExponentialBackOff(awsClient, applicationContext, lifecycle).invoke(
+ params = null,
+ onSuccess = {
+ broadcastMessage = it.tempId
+ },
+ onFailure = {
+ }
+ )
+ } else {
+ CentralLog.i(TAG, "Don't need to update bm")
+ }
+
+ }
+
+ private fun calcPhaseShift(min: Long, max: Long): Long {
+ return (min + (Math.random() * (max - min))).toLong()
+ }
+
+ private fun actionScan() {
+ if (Preference.isOnBoarded(this) && Utils.needToUpdate(this.applicationContext) || broadcastMessage == null) {
+ //need to pull new BM
+ UpdateBroadcastMessageAndPerformScanWithExponentialBackOff(awsClient, applicationContext, lifecycle).invoke(
+ params = null,
+ onSuccess = {
+ broadcastMessage = it.tempId
+ performScanAndScheduleNextScan()
+ },
+ onFailure = {
+ }
+ )
+ } else if (Preference.isOnBoarded(this)) {
+ performScanAndScheduleNextScan()
+ }
+ }
+
+ private fun actionAdvertise() {
+ setupAdvertiser()
+
+ if (isBluetoothEnabled()) {
+ advertiser?.startAdvertising(advertisingDuration)
+ } else {
+ CentralLog.w(TAG, "Unable to start advertising, bluetooth is off")
+ }
+
+ commandHandler.scheduleNextAdvertise(advertisingDuration + advertisingGap)
+ }
+
+ private fun setupService() {
+ streetPassServer =
+ streetPassServer ?: StreetPassServer(this.applicationContext, serviceUUID)
+ setupScanner()
+ setupAdvertiser()
+ }
+
+ private fun setupScanner() {
+ streetPassScanner = streetPassScanner ?: StreetPassScanner(
+ this,
+ serviceUUID,
+ scanDuration
+ )
+ }
+
+ private fun setupAdvertiser() {
+ advertiser = advertiser ?: BLEAdvertiser(serviceUUID)
+ }
+
+ private fun setupCycles() {
+ setupScanCycles()
+ setupAdvertisingCycles()
+ }
+
+ private fun setupScanCycles() {
+ actionScan()
+ }
+
+ private fun setupAdvertisingCycles() {
+ actionAdvertise()
+ }
+
+ private fun performScanAndScheduleNextScan() {
+
+ setupScanner()
+
+ commandHandler.scheduleNextScan(
+ scanDuration + calcPhaseShift(
+ minScanInterval,
+ maxScanInterval
+ )
+ )
+
+ startScan()
+
+ }
+
+ private fun startScan() {
+
+ if (isBluetoothEnabled()) {
+
+ streetPassScanner?.let { scanner ->
+ if (!scanner.isScanning()) {
+ scanner.startScan()
+ } else {
+ CentralLog.e(TAG, "Already scanning!")
+ }
+ }
+ } else {
+ CentralLog.w(TAG, "Unable to start scan - bluetooth is off")
+ }
+ }
+
+ private fun performHealthCheck() {
+
+ CentralLog.i(TAG, "Performing self diagnosis")
+
+ if (!hasLocationPermissions() || !isBluetoothEnabled()) {
+ CentralLog.i(TAG, "no location permission")
+ val notif =
+ NotificationTemplates.lackingThingsNotification(this.applicationContext, CHANNEL_ID)
+ startForeground(NOTIFICATION_ID, notif)
+ return
+ }
+
+ startForeground(
+ NOTIFICATION_ID,
+ NotificationTemplates.getRunningNotification(
+ this.applicationContext,
+ CHANNEL_ID
+ )
+ )
+
+ //ensure our service is there
+ setupService()
+
+ if (!commandHandler.hasScanScheduled()) {
+ CentralLog.w(TAG, "Missing Scan Schedule - rectifying")
+ setupScanCycles()
+ } else {
+ CentralLog.w(TAG, "Scan Schedule present")
+ }
+
+ if (!commandHandler.hasAdvertiseScheduled()) {
+ CentralLog.w(TAG, "Missing Advertise Schedule - rectifying")
+ setupAdvertisingCycles()
+ } else {
+ CentralLog.w(
+ TAG,
+ "Advertise Schedule present. Should be advertising?: ${advertiser?.shouldBeAdvertising
+ ?: false}. Is Advertising?: ${advertiser?.isAdvertising ?: false}"
+ )
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ CentralLog.i(TAG, "BluetoothMonitoringService destroyed - tearing down")
+
+ teardown()
+ unregisterReceivers()
+
+ worker?.terminateConnections()
+ worker?.unregisterReceivers()
+
+ job.cancel()
+
+ if (mBound) {
+ unbindService(connection)
+ mBound = false
+ }
+
+ CentralLog.i(TAG, "BluetoothMonitoringService destroyed")
+ }
+
+ private fun registerReceivers() {
+ val recordAvailableFilter = IntentFilter(ACTION_RECEIVED_STREETPASS)
+ localBroadcastManager.registerReceiver(streetPassReceiver, recordAvailableFilter)
+
+ val statusReceivedFilter = IntentFilter(ACTION_RECEIVED_STATUS)
+ localBroadcastManager.registerReceiver(statusReceiver, statusReceivedFilter)
+
+ val bluetoothStatusReceivedFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
+ registerReceiver(bluetoothStatusReceiver, bluetoothStatusReceivedFilter)
+
+ CentralLog.i(TAG, "Receivers registered")
+ }
+
+ private fun unregisterReceivers() {
+ try {
+ localBroadcastManager.unregisterReceiver(streetPassReceiver)
+ } catch (e: Throwable) {
+ CentralLog.w(TAG, "streetPassReceiver is not registered?")
+ }
+
+ try {
+ localBroadcastManager.unregisterReceiver(statusReceiver)
+ } catch (e: Throwable) {
+ CentralLog.w(TAG, "statusReceiver is not registered?")
+ }
+
+ try {
+ unregisterReceiver(bluetoothStatusReceiver)
+ } catch (e: Throwable) {
+ CentralLog.w(TAG, "bluetoothStatusReceiver is not registered?")
+ }
+ }
+
+ inner class BluetoothStatusReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent?.let {
+ val action = intent.action
+ if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
+
+ when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
+ BluetoothAdapter.STATE_TURNING_OFF -> {
+ CentralLog.d(TAG, "BluetoothAdapter.STATE_TURNING_OFF")
+ val notif = NotificationTemplates.lackingThingsNotification(
+ this@BluetoothMonitoringService.applicationContext,
+ CHANNEL_ID
+ )
+ startForeground(NOTIFICATION_ID, notif)
+ teardown()
+ }
+ BluetoothAdapter.STATE_OFF -> {
+ CentralLog.d(TAG, "BluetoothAdapter.STATE_OFF")
+ }
+ BluetoothAdapter.STATE_TURNING_ON -> {
+ CentralLog.d(TAG, "BluetoothAdapter.STATE_TURNING_ON")
+ }
+ BluetoothAdapter.STATE_ON -> {
+ CentralLog.d(TAG, "BluetoothAdapter.STATE_ON")
+ Utils.startBluetoothMonitoringService(this@BluetoothMonitoringService.applicationContext)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ inner class StreetPassReceiver : BroadcastReceiver() {
+
+ private val TAG = "StreetPassReceiver"
+
+ override fun onReceive(context: Context, intent: Intent) {
+
+ if (ACTION_RECEIVED_STREETPASS == intent.action) {
+ val connRecord: ConnectionRecord = intent.getParcelableExtra(STREET_PASS)
+ CentralLog.d(
+ TAG,
+ "StreetPass received: $connRecord"
+ )
+
+ if (connRecord.msg.isNotEmpty()) {
+
+ if (mBound) {
+ val proximity = mService.proximity
+ val light = mService.light
+ CentralLog.d(
+ TAG,
+ "Sensor values just before saving StreetPassRecord: proximity=$proximity light=$light"
+ )
+ }
+
+ val record = StreetPassRecord(
+ v = connRecord.version,
+ msg = connRecord.msg,
+ org = connRecord.org,
+ modelP = connRecord.peripheral.modelP,
+ modelC = connRecord.central.modelC,
+ rssi = connRecord.rssi,
+ txPower = connRecord.txPower
+ )
+
+
+ launch{
+ CentralLog.d(
+ TAG,
+ "Coroutine - Saving StreetPassRecord: ${Utils.getDate(record.timestamp)} $record")
+
+ streetPassRecordStorage.saveRecord(record)
+ }
+ }
+ }
+ }
+ }
+
+ inner class StatusReceiver : BroadcastReceiver() {
+ private val TAG = "StatusReceiver"
+
+ override fun onReceive(context: Context, intent: Intent) {
+
+ if (ACTION_RECEIVED_STATUS == intent.action) {
+ val statusRecord: Status = intent.getParcelableExtra(STATUS)
+ CentralLog.d(TAG, "Status received: ${statusRecord.msg}")
+
+ if (statusRecord.msg.isNotEmpty()) {
+ val statusRecord = StatusRecord(statusRecord.msg)
+ launch {
+ statusRecordStorage.saveRecord(statusRecord)
+ }
+ }
+ }
+ }
+ }
+
+ enum class Command(val index: Int, val string: String) {
+ INVALID(-1, "INVALID"),
+ ACTION_START(0, "START"),
+ ACTION_SCAN(1, "SCAN"),
+ ACTION_STOP(2, "STOP"),
+ ACTION_ADVERTISE(3, "ADVERTISE"),
+ ACTION_SELF_CHECK(4, "SELF_CHECK"),
+ ACTION_UPDATE_BM(5, "UPDATE_BM");
+
+ companion object {
+ private val types = values().associate { it.index to it }
+ fun findByValue(value: Int) = types[value]
+ }
+ }
+
+ companion object {
+
+ private const val TAG = "BTMService"
+
+ private const val NOTIFICATION_ID = BuildConfig.SERVICE_FOREGROUND_NOTIFICATION_ID
+ private const val CHANNEL_ID = BuildConfig.SERVICE_FOREGROUND_CHANNEL_ID
+ const val CHANNEL_SERVICE = BuildConfig.SERVICE_FOREGROUND_CHANNEL_NAME
+
+ const val COMMAND_KEY = "${BuildConfig.APPLICATION_ID}_CMD"
+
+ const val PENDING_ACTIVITY = 5
+ const val PENDING_START = 6
+ const val PENDING_SCAN_REQ_CODE = 7
+ const val PENDING_ADVERTISE_REQ_CODE = 8
+ const val PENDING_HEALTH_CHECK_CODE = 9
+ const val PENDING_WIZARD_REQ_CODE = 10
+ const val PENDING_BM_UPDATE = 11
+ const val PENDING_PRIVACY_CLEANER_CODE = 12
+ const val DAILY_UPLOAD_NOTIFICATION_CODE = 13
+
+
+ var broadcastMessage: String? = null
+
+ const val scanDuration: Long = BuildConfig.SCAN_DURATION
+ const val minScanInterval: Long = BuildConfig.MIN_SCAN_INTERVAL
+ const val maxScanInterval: Long = BuildConfig.MAX_SCAN_INTERVAL
+
+ const val advertisingDuration: Long = BuildConfig.ADVERTISING_DURATION
+ const val advertisingGap: Long = BuildConfig.ADVERTISING_INTERVAL
+
+ const val maxQueueTime: Long = BuildConfig.MAX_QUEUE_TIME
+ const val bmCheckInterval: Long = BuildConfig.BM_CHECK_INTERVAL
+ const val healthCheckInterval: Long = BuildConfig.HEALTH_CHECK_INTERVAL
+
+ const val connectionTimeout: Long = BuildConfig.CONNECTION_TIMEOUT
+
+ const val blacklistDuration: Long = BuildConfig.BLACKLIST_DURATION
+
+ }
+
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/services/CommandHandler.kt b/app/src/main/java/au/gov/health/covidsafe/services/CommandHandler.kt
new file mode 100644
index 0000000..391b0b4
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/services/CommandHandler.kt
@@ -0,0 +1,55 @@
+package au.gov.health.covidsafe.services
+
+import android.os.Handler
+import android.os.Message
+import java.lang.ref.WeakReference
+
+class CommandHandler(val service: WeakReference) : Handler() {
+ override fun handleMessage(msg: Message?) {
+ msg?.let {
+ val cmd = msg.what
+ service.get()?.runService(BluetoothMonitoringService.Command.findByValue(cmd))
+ }
+ }
+
+ private fun sendCommandMsg(cmd: BluetoothMonitoringService.Command, delay: Long) {
+ val msg = Message.obtain(this, cmd.index)
+ sendMessageDelayed(msg, delay)
+ }
+
+ private fun sendCommandMsg(cmd: BluetoothMonitoringService.Command) {
+ val msg = obtainMessage(cmd.index)
+ msg.arg1 = cmd.index
+ sendMessage(msg)
+ }
+
+ fun startBluetoothMonitoringService() {
+ sendCommandMsg(BluetoothMonitoringService.Command.ACTION_START)
+ }
+
+ fun scheduleNextScan(timeInMillis: Long) {
+ cancelNextScan()
+ sendCommandMsg(BluetoothMonitoringService.Command.ACTION_SCAN, timeInMillis)
+ }
+
+ private fun cancelNextScan() {
+ removeMessages(BluetoothMonitoringService.Command.ACTION_SCAN.index)
+ }
+
+ fun hasScanScheduled(): Boolean {
+ return hasMessages(BluetoothMonitoringService.Command.ACTION_SCAN.index)
+ }
+
+ fun scheduleNextAdvertise(timeInMillis: Long) {
+ cancelNextAdvertise()
+ sendCommandMsg(BluetoothMonitoringService.Command.ACTION_ADVERTISE, timeInMillis)
+ }
+
+ private fun cancelNextAdvertise() {
+ removeMessages(BluetoothMonitoringService.Command.ACTION_ADVERTISE.index)
+ }
+
+ fun hasAdvertiseScheduled(): Boolean {
+ return hasMessages(BluetoothMonitoringService.Command.ACTION_ADVERTISE.index)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt b/app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt
new file mode 100644
index 0000000..6c4abbf
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt
@@ -0,0 +1,96 @@
+package au.gov.health.covidsafe.services
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.os.Binder
+import android.os.IBinder
+import au.gov.health.covidsafe.logging.CentralLog
+import kotlin.math.sqrt
+
+class SensorMonitoringService : Service(), SensorEventListener {
+ private lateinit var sensorManager: SensorManager
+ private var _light: FloatArray? = null
+ private var _proximity: FloatArray? = null
+ private val binder = LocalBinder()
+
+ override fun onCreate() {
+ super.onCreate()
+ sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
+
+ val proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
+ val lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)
+
+ if (proximitySensor != null) {
+ CentralLog.d(TAG, "Proximity sensor: $proximitySensor")
+ sensorManager.registerListener(this, proximitySensor, SENSOR_DELAY_SUPER_SLOW)
+
+ } else {
+ CentralLog.d(TAG, "Proximity sensor not available")
+ }
+
+ if (lightSensor != null) {
+ CentralLog.d(TAG, "Light sensor: $lightSensor")
+ sensorManager.registerListener(this, lightSensor, SENSOR_DELAY_SUPER_SLOW)
+ } else {
+ CentralLog.d(TAG, "Light sensor not available")
+ }
+
+ CentralLog.d(TAG, "SensorMonitoringService started")
+ }
+
+ override fun onDestroy() {
+ sensorManager.unregisterListener(this)
+ CentralLog.d(TAG, "SensorMonitoringService destroyed")
+ super.onDestroy()
+ }
+
+ inner class LocalBinder : Binder() {
+ fun getService(): SensorMonitoringService = this@SensorMonitoringService
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ return binder
+ }
+
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
+ CentralLog.d(TAG, "Sensor accuracy changed! $sensor")
+ }
+
+ override fun onSensorChanged(event: SensorEvent) {
+ when (event.sensor.type) {
+ Sensor.TYPE_PROXIMITY -> {
+ _proximity = event.values
+ }
+ Sensor.TYPE_LIGHT -> {
+ _light = event.values
+ }
+ else -> {
+ CentralLog.w(TAG, "Unexpected sensor type changed: ${event.sensor.type}")
+ }
+ }
+ }
+
+ val proximity
+ get() = if (_proximity != null) {
+ sqrt((_proximity as FloatArray).reduce { acc: Float, n: Float -> acc + n * n })
+ } else {
+ -1.0f
+ }
+
+ val light
+ get() = if (_light != null) {
+ sqrt((_light as FloatArray).reduce { acc: Float, n: Float -> acc + n * n })
+ } else {
+ -1.0f
+ }
+
+ companion object {
+ const val TAG = "SensorMonitoringService"
+ const val SENSOR_DELAY_SUPER_SLOW = 3_000_000
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/status/Status.kt b/app/src/main/java/au/gov/health/covidsafe/status/Status.kt
new file mode 100644
index 0000000..eac69ac
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/status/Status.kt
@@ -0,0 +1,9 @@
+package au.gov.health.covidsafe.status
+
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+data class Status(
+ val msg: String
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecord.kt b/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecord.kt
new file mode 100644
index 0000000..6739728
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecord.kt
@@ -0,0 +1,20 @@
+package au.gov.health.covidsafe.status.persistence
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "status_table")
+class StatusRecord constructor(
+
+ @ColumnInfo(name = "msg")
+ var msg: String
+) {
+
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Int = 0
+
+ @ColumnInfo(name = "timestamp")
+ var timestamp: Long = System.currentTimeMillis()
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecordDao.kt b/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecordDao.kt
new file mode 100644
index 0000000..4465d59
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecordDao.kt
@@ -0,0 +1,32 @@
+package au.gov.health.covidsafe.status.persistence
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import androidx.sqlite.db.SupportSQLiteQuery
+
+@Dao
+interface StatusRecordDao {
+
+ @Query("SELECT * from status_table ORDER BY timestamp ASC")
+ fun getRecords(): LiveData>
+
+ @Query("SELECT * from status_table ORDER BY timestamp ASC")
+ fun getCurrentRecords(): List
+
+ @Query("SELECT * from status_table where msg = :msg ORDER BY timestamp DESC LIMIT 1")
+ fun getMostRecentRecord(msg: String): LiveData
+
+ @Query("DELETE FROM status_table WHERE timestamp <= :timeInMs")
+ fun deleteDataOlder(timeInMs: Long): Int
+
+ @Query("DELETE FROM status_table")
+ fun nukeDb()
+
+ @RawQuery
+ fun getRecordsViaQuery(query: SupportSQLiteQuery): List
+
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insert(record: StatusRecord)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecordStorage.kt b/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecordStorage.kt
new file mode 100644
index 0000000..762c0db
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/status/persistence/StatusRecordStorage.kt
@@ -0,0 +1,22 @@
+package au.gov.health.covidsafe.status.persistence
+
+import android.content.Context
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase
+
+class StatusRecordStorage(val context: Context) {
+
+ private val statusDao = StreetPassRecordDatabase.getDatabase(context).statusDao()
+
+ suspend fun saveRecord(record: StatusRecord) {
+ statusDao.insert(record)
+ }
+
+ fun getAllRecords(): List {
+ return statusDao.getCurrentRecords()
+ }
+
+ fun deleteDataOlderThan(timeInMs: Long): Int {
+ return statusDao.deleteDataOlder(timeInMs)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/BlacklistEntry.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/BlacklistEntry.kt
new file mode 100644
index 0000000..51f177d
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/BlacklistEntry.kt
@@ -0,0 +1,3 @@
+package au.gov.health.covidsafe.streetpass
+
+class BlacklistEntry(val uniqueIdentifier: String?)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/ConnectablePeripheral.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/ConnectablePeripheral.kt
new file mode 100644
index 0000000..d715424
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/ConnectablePeripheral.kt
@@ -0,0 +1,40 @@
+package au.gov.health.covidsafe.streetpass
+
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+class ConnectablePeripheral(
+ var transmissionPower: Int?,
+ var rssi: Int
+) : Parcelable
+
+@Parcelize
+data class PeripheralDevice(
+ val modelP: String,
+ val address: String?
+) : Parcelable
+
+@Parcelize
+data class CentralDevice(
+ val modelC: String,
+ val address: String?
+) : Parcelable
+
+@Parcelize
+data class ConnectionRecord(
+ val version: Int,
+
+ val msg: String,
+ val org: String,
+
+ val peripheral: PeripheralDevice,
+ val central: CentralDevice,
+
+ var rssi: Int,
+ var txPower: Int?
+) : Parcelable {
+ override fun toString(): String {
+ return "Central ${central.modelC} - ${central.address} ---> Peripheral ${peripheral.modelP} - ${peripheral.address}"
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPass.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPass.kt
new file mode 100644
index 0000000..a596460
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPass.kt
@@ -0,0 +1,5 @@
+package au.gov.health.covidsafe.streetpass
+
+import au.gov.health.covidsafe.BuildConfig
+
+const val ACTION_DEVICE_SCANNED = "${BuildConfig.APPLICATION_ID}.ACTION_DEVICE_SCANNED"
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt
new file mode 100644
index 0000000..607e2a1
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt
@@ -0,0 +1,122 @@
+package au.gov.health.covidsafe.streetpass
+
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanResult
+import android.content.Context
+import android.os.Build
+import android.os.Handler
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.bluetooth.BLEScanner
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.status.Status
+import kotlin.properties.Delegates
+
+class StreetPassScanner constructor(
+ context: Context,
+ serviceUUIDString: String,
+ private val scanDurationInMillis: Long
+) {
+
+ private var scanner: BLEScanner by Delegates.notNull()
+
+ private var context: Context by Delegates.notNull()
+ private val TAG = "StreetPassScanner"
+
+ private var handler: Handler = Handler()
+
+ var scannerCount = 0
+
+ private val scanCallback = BleScanCallback()
+
+ init {
+ scanner = BLEScanner(context, serviceUUIDString, 0)
+ this.context = context
+ }
+
+ fun startScan() {
+
+ val statusRecord = Status("Scanning Started")
+ Utils.broadcastStatusReceived(context, statusRecord)
+
+ scanner.startScan(scanCallback)
+ scannerCount++
+
+ handler.postDelayed(
+ { stopScan() }
+ , scanDurationInMillis)
+
+
+ CentralLog.d(TAG, "scanning started")
+ }
+
+ fun stopScan() {
+ if (scannerCount > 0) {
+ val statusRecord = Status("Scanning Stopped")
+ Utils.broadcastStatusReceived(context, statusRecord)
+ scannerCount--
+ scanner.stopScan()
+ }
+ }
+
+ fun isScanning(): Boolean {
+ return scannerCount > 0
+ }
+
+ inner class BleScanCallback : ScanCallback() {
+
+ private val TAG = "BleScanCallback"
+
+ private fun processScanResult(scanResult: ScanResult?) {
+
+ scanResult?.let { result ->
+ val device = result.device
+ val rssi = result.rssi // get RSSI value
+
+ var txPower: Int? = null
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ txPower = result.txPower
+ if (txPower == 127) {
+ txPower = null
+ }
+ }
+
+ val manuData: ByteArray = scanResult.scanRecord?.getManufacturerSpecificData(1023)
+ ?: "N.A".toByteArray()
+ val manuString = String(manuData, Charsets.UTF_8)
+
+ val connectable = ConnectablePeripheral(txPower, rssi)
+
+ CentralLog.i(TAG, "Scanned: $manuString - ${device.address}")
+
+ Utils.broadcastDeviceScanned(context, device, connectable)
+ }
+ }
+
+ override fun onScanResult(callbackType: Int, result: ScanResult?) {
+ super.onScanResult(callbackType, result)
+ processScanResult(result)
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ super.onScanFailed(errorCode)
+
+ val reason = when (errorCode) {
+ SCAN_FAILED_ALREADY_STARTED -> "$errorCode - SCAN_FAILED_ALREADY_STARTED"
+ SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "$errorCode - SCAN_FAILED_APPLICATION_REGISTRATION_FAILED"
+ SCAN_FAILED_FEATURE_UNSUPPORTED -> "$errorCode - SCAN_FAILED_FEATURE_UNSUPPORTED"
+ SCAN_FAILED_INTERNAL_ERROR -> "$errorCode - SCAN_FAILED_INTERNAL_ERROR"
+ else -> {
+ "$errorCode - UNDOCUMENTED"
+ }
+ }
+ CentralLog.e(TAG, "BT Scan failed: $reason")
+ if (scannerCount > 0) {
+ scannerCount--
+ }
+ }
+ }
+
+
+}
+
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt
new file mode 100644
index 0000000..80e5401
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassServer.kt
@@ -0,0 +1,32 @@
+package au.gov.health.covidsafe.streetpass
+
+import android.content.Context
+import au.gov.health.covidsafe.bluetooth.gatt.GattServer
+import au.gov.health.covidsafe.bluetooth.gatt.GattService
+
+class StreetPassServer constructor(val context: Context, serviceUUIDString: String) {
+
+ private val TAG = "StreetPassServer"
+ private var gattServer: GattServer? = null
+
+ init {
+ gattServer = setupGattServer(context, serviceUUIDString)
+ }
+
+ private fun setupGattServer(context: Context, serviceUUIDString: String): GattServer? {
+ val gattServer = GattServer(context, serviceUUIDString)
+ val started = gattServer.startServer()
+
+ if (started) {
+ val readService = GattService(context, serviceUUIDString)
+ gattServer.addService(readService)
+ return gattServer
+ }
+ return null
+ }
+
+ fun tearDown() {
+ gattServer?.stop()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt
new file mode 100644
index 0000000..ac3b9c7
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt
@@ -0,0 +1,721 @@
+package au.gov.health.covidsafe.streetpass
+
+import android.bluetooth.*
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import androidx.annotation.Keep
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.TracerApp
+import au.gov.health.covidsafe.Utils
+import au.gov.health.covidsafe.bluetooth.gatt.*
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.services.BluetoothMonitoringService
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.blacklistDuration
+import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.maxQueueTime
+import java.util.*
+import java.util.concurrent.PriorityBlockingQueue
+@Keep
+class StreetPassWorker(val context: Context) {
+
+ private val workQueue: PriorityBlockingQueue = PriorityBlockingQueue()
+ private val blacklist: MutableList = Collections.synchronizedList(ArrayList())
+
+ private val workReceiver = StreetPassWorkReceiver()
+ private val deviceProcessedReceiver = DeviceProcessedReceiver()
+ private val serviceUUID: UUID = UUID.fromString(BuildConfig.BLE_SSID)
+ private val TAG = "StreetPassWorker"
+
+ private val bluetoothManager =
+ context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+
+ private lateinit var timeoutHandler: Handler
+ private lateinit var queueHandler: Handler
+ private lateinit var blacklistHandler: Handler
+
+ private var currentPendingConnection: Work? = null
+ private var localBroadcastManager: LocalBroadcastManager = LocalBroadcastManager.getInstance(context)
+
+ val onWorkTimeoutListener = object : Work.OnWorkTimeoutListener {
+ override fun onWorkTimeout(work: Work) {
+
+ if (!isCurrentlyWorkedOn(work.device.address)) {
+ CentralLog.i(TAG, "Work already removed. Timeout ineffective??.")
+ }
+
+ CentralLog.e(
+ TAG,
+ "Work timed out for ${work.device.address} @ ${work.connectable.rssi} queued for ${work.checklist.started.timePerformed - work.timeStamp}ms"
+ )
+ CentralLog.e(
+ TAG,
+ "${work.device.address} work status: ${work.checklist}."
+ )
+
+ //connection never formed - don't need to disconnect
+ if (!work.checklist.connected.status) {
+ CentralLog.e(TAG, "No connection formed for ${work.device.address}")
+ if (work.device.address == currentPendingConnection?.device?.address) {
+ currentPendingConnection = null
+ }
+
+ try {
+ work.gatt?.close()
+ } catch (e: Exception) {
+ CentralLog.e(
+ TAG,
+ "Unexpected error while attempting to close clientIf to ${work.device.address}: ${e.localizedMessage}"
+ )
+ }
+
+ finishWork(work)
+ }
+ //the connection is still there - might be stuck / work in progress
+ else if (work.checklist.connected.status && !work.checklist.disconnected.status) {
+
+ if (work.checklist.readCharacteristic.status || work.checklist.writeCharacteristic.status || work.checklist.skipped.status) {
+ CentralLog.e(
+ TAG,
+ "Connected but did not disconnect in time for ${work.device.address}"
+ )
+
+ try {
+ work.gatt?.disconnect()
+ //disconnect callback won't get invoked
+ if (work.gatt == null) {
+ currentPendingConnection = null
+ finishWork(work)
+ }
+ } catch (e: Throwable) {
+ CentralLog.e(
+ TAG,
+ "Failed to clean up work, bluetooth state likely changed or other device's advertiser stopped: ${e.localizedMessage}"
+ )
+ }
+
+ } else {
+ CentralLog.e(
+ TAG,
+ "Connected but did nothing for ${work.device.address}"
+ )
+
+ try {
+ work.gatt?.disconnect()
+ //disconnect callback won't get invoked
+ if (work.gatt == null) {
+ currentPendingConnection = null
+ finishWork(work)
+ }
+ } catch (e: Throwable) {
+ CentralLog.e(
+ TAG,
+ "Failed to clean up work, bluetooth state likely changed or other device's advertiser stopped: ${e.localizedMessage}"
+ )
+ }
+ }
+ }
+
+ //all other edge cases? - disconnected
+ else {
+ CentralLog.e(
+ TAG,
+ "Disconnected but callback not invoked in time. Waiting.: ${work.device.address}: ${work.checklist}"
+ )
+ }
+ }
+ }
+
+ init {
+ prepare()
+ }
+
+ private fun prepare() {
+ val deviceAvailableFilter = IntentFilter(ACTION_DEVICE_SCANNED)
+ localBroadcastManager.registerReceiver(workReceiver, deviceAvailableFilter)
+
+ val deviceProcessedFilter = IntentFilter(ACTION_DEVICE_PROCESSED)
+ localBroadcastManager.registerReceiver(deviceProcessedReceiver, deviceProcessedFilter)
+
+ timeoutHandler = Handler()
+ queueHandler = Handler()
+ blacklistHandler = Handler()
+ }
+
+ fun isCurrentlyWorkedOn(address: String?): Boolean {
+ return currentPendingConnection?.let {
+ it.device.address == address
+ } ?: false
+ }
+
+ fun addWork(work: Work): Boolean {
+ //if it's our current work. ignore
+ if (isCurrentlyWorkedOn(work.device.address)) {
+ CentralLog.i(TAG, "${work.device.address} is being worked on, not adding to queue")
+ return false
+ }
+
+ //if its in blacklist - check for both mac address and manu data
+
+ if (blacklist.any { it.uniqueIdentifier == work.device.address }) {
+ CentralLog.i(TAG, "${work.device.address} is in blacklist, not adding to queue")
+ return false
+ }
+
+ //if we haven't seen this device yet
+ if (workQueue.none { it.device.address == work.device.address }) {
+ workQueue.offer(work)
+ queueHandler.postDelayed({
+ if (workQueue.contains(work))
+ CentralLog.i(
+ TAG,
+ "Work for ${work.device.address} removed from queue? : ${workQueue.remove(
+ work
+ )}"
+ )
+ }, maxQueueTime)
+ CentralLog.i(TAG, "Added to work queue: ${work.device.address}")
+ return true
+ }
+ //this gadget is already in the queue, we can use the latest rssi and txpower? replace the entry
+ else {
+
+ //ignore it
+ CentralLog.i(TAG, "${work.device.address} is already in work queue")
+
+ val prevWork = workQueue.find { it.device.address == work.device.address }
+ val removed = workQueue.remove(prevWork)
+ val added = workQueue.offer(work)
+
+ CentralLog.i(TAG, "Queue entry updated - removed: ${removed}, added: $added")
+
+ return false
+ }
+ }
+
+ fun doWork() {
+
+ if (currentPendingConnection != null) {
+ CentralLog.i(
+ TAG,
+ "Already trying to connect to: ${currentPendingConnection?.device?.address}"
+ )
+ //devices may reset their bluetooth before the disconnection happens properly and disconnect is never called.
+ //handle that situation here
+
+ //if the job was finished but not removed
+ //or if the job was timed out but not removed
+ val timedout = System.currentTimeMillis() > currentPendingConnection?.timeout ?: 0
+ if (currentPendingConnection?.finished ?: false || timedout) {
+
+ CentralLog.w(
+ TAG,
+ "Handling erroneous current work for ${currentPendingConnection?.device?.address} : - finished: ${currentPendingConnection?.finished
+ ?: false}, timedout: $timedout"
+ )
+ //check if there is, for some reason, an existing connection
+ if (currentPendingConnection != null) {
+ if (bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(
+ currentPendingConnection?.device
+ )
+ ) {
+ CentralLog.w(
+ TAG,
+ "Disconnecting dangling connection to ${currentPendingConnection?.device?.address}"
+ )
+ currentPendingConnection?.gatt?.disconnect()
+ }
+ } else {
+ doWork()
+ }
+ }
+
+ return
+ }
+
+ if (workQueue.isEmpty()) {
+ CentralLog.i(TAG, "Queue empty. Nothing to do.")
+ return
+ }
+
+ CentralLog.i(TAG, "Queue size: ${workQueue.size}")
+
+ var workToDo: Work? = null
+ val now = System.currentTimeMillis()
+
+ while (workToDo == null && workQueue.isNotEmpty()) {
+ workToDo = workQueue.poll()
+ workToDo?.let { work ->
+ if (now - work.timeStamp > maxQueueTime) {
+ CentralLog.w(
+ TAG,
+ "Work request for ${work.device.address} too old. Not doing"
+ )
+ workToDo = null
+ }
+ }
+ }
+
+ workToDo?.let {
+
+ val device = it.device
+
+ if (blacklist.filter { it.uniqueIdentifier == device.address }.isNotEmpty()) {
+ CentralLog.w(TAG, "Already worked on ${device.address}. Skip.")
+ doWork()
+ return
+ }
+
+ var currentWorkOrder = it
+
+ val alreadyConnected = getConnectionStatus(device)
+ CentralLog.i(TAG, "Already connected to ${device.address} : $alreadyConnected")
+
+ if (alreadyConnected) {
+ //this might mean that the other device is currently connected to this device's local gatt server
+ //skip. we'll rely on the other party to do a write
+ currentWorkOrder.checklist.skipped.status = true
+ currentWorkOrder.checklist.skipped.timePerformed = System.currentTimeMillis()
+ currentWorkOrder.let {
+ finishWork(it)
+ }
+
+ } else {
+
+ currentWorkOrder.let {
+
+ if (it != null) {
+
+ val gattCallback = StreetPassGattCallback(it)
+ CentralLog.i(
+ TAG,
+ "Starting work - connecting to device: ${device.address} @ ${it.connectable.rssi} ${System.currentTimeMillis() - it.timeStamp}ms ago"
+ )
+ currentPendingConnection = it
+
+ try {
+ it.checklist.started.status = true
+ it.checklist.started.timePerformed = System.currentTimeMillis()
+
+ it.startWork(context, gattCallback)
+
+ var connecting = it.gatt?.connect() ?: false
+
+ if (!connecting) {
+ CentralLog.e(
+ TAG,
+ "not connecting to ${it.device.address}??"
+ )
+
+ //bail and do the next job
+ CentralLog.e(TAG, "Moving on to next task")
+ currentPendingConnection = null
+ doWork()
+ return
+
+ } else {
+ CentralLog.i(
+ TAG,
+ "Connection to ${it.device.address} attempt in progress"
+ )
+ }
+
+ timeoutHandler.postDelayed(
+ it.timeoutRunnable,
+ BluetoothMonitoringService.connectionTimeout
+ )
+ it.timeout =
+ System.currentTimeMillis() + BluetoothMonitoringService.connectionTimeout
+
+ CentralLog.i(TAG, "Timeout scheduled for ${it.device.address}")
+ } catch (e: Throwable) {
+ CentralLog.e(
+ TAG,
+ "Unexpected error while attempting to connect to ${device.address}: ${e.localizedMessage}"
+ )
+ CentralLog.e(TAG, "Moving on to next task")
+ currentPendingConnection = null
+ doWork()
+ return
+ }
+
+ } else {
+ CentralLog.e(TAG, "Work not started - missing Work Object")
+ }
+ }
+ }
+ }
+
+ if (workToDo == null) {
+ CentralLog.i(TAG, "No outstanding work")
+ }
+
+ }
+
+ private fun getConnectionStatus(device: BluetoothDevice): Boolean {
+
+ val connectedDevices = bluetoothManager.getDevicesMatchingConnectionStates(
+ BluetoothProfile.GATT,
+ intArrayOf(BluetoothProfile.STATE_CONNECTED)
+ )
+ return connectedDevices.contains(device)
+ }
+
+ fun finishWork(work: Work) {
+
+ if (work.finished) {
+ CentralLog.i(
+ TAG,
+ "Work on ${work.device.address} already finished and closed"
+ )
+ return
+ }
+
+ if (work.isCriticalsCompleted()) {
+ Utils.broadcastDeviceProcessed(context, work.device.address)
+ }
+
+ CentralLog.i(
+ TAG,
+ "Work on ${work.device.address} stopped in: ${work.checklist.disconnected.timePerformed - work.checklist.started.timePerformed}"
+ )
+
+ CentralLog.i(
+ TAG,
+ "Work on ${work.device.address} completed?: ${work.isCriticalsCompleted()}. Connected in: ${work.checklist.connected.timePerformed - work.checklist.started.timePerformed}. connection lasted for: ${work.checklist.disconnected.timePerformed - work.checklist.connected.timePerformed}. Status: ${work.checklist}"
+ )
+
+ timeoutHandler.removeCallbacks(work.timeoutRunnable)
+ CentralLog.i(TAG, "Timeout removed for ${work.device.address}")
+
+ work.finished = true
+ doWork()
+ }
+
+ inner class StreetPassGattCallback(private val work: Work) : BluetoothGattCallback() {
+
+ private fun endWorkConnection(gatt: BluetoothGatt) {
+ CentralLog.i(TAG, "Ending connection with: ${gatt.device.address}")
+ gatt.disconnect()
+ }
+
+ override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
+
+ gatt?.let {
+
+ when (newState) {
+ BluetoothProfile.STATE_CONNECTED -> {
+ CentralLog.i(TAG, "Connected to other GATT server - ${gatt.device.address}")
+
+ gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_BALANCED)
+ gatt.requestMtu(512)
+
+ work.checklist.connected.status = true
+ work.checklist.connected.timePerformed = System.currentTimeMillis()
+
+ }
+
+ BluetoothProfile.STATE_DISCONNECTED -> {
+ CentralLog.i(
+ TAG,
+ "Disconnected from other GATT server - ${gatt.device.address}"
+ )
+ work.checklist.disconnected.status = true
+ work.checklist.disconnected.timePerformed = System.currentTimeMillis()
+
+ //remove timeout runnable if its still there
+ timeoutHandler.removeCallbacks(work.timeoutRunnable)
+ CentralLog.i(TAG, "Timeout removed for ${work.device.address}")
+
+ //remove job from list of current work - if it is the current work
+ if (work.device.address == currentPendingConnection?.device?.address) {
+ currentPendingConnection = null
+ }
+ gatt.close()
+ finishWork(work)
+ }
+
+ else -> {
+ CentralLog.i(TAG, "Connection status for ${gatt.device.address}: $newState")
+ endWorkConnection(gatt)
+ }
+ }
+ }
+ }
+
+ override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
+
+ if (!work.checklist.mtuChanged.status) {
+
+ work.checklist.mtuChanged.status = true
+ work.checklist.mtuChanged.timePerformed = System.currentTimeMillis()
+
+ CentralLog.i(
+ TAG,
+ "${gatt?.device?.address} MTU is $mtu. Was change successful? : ${status == BluetoothGatt.GATT_SUCCESS}"
+ )
+
+ gatt?.let {
+ val discoveryOn = gatt.discoverServices()
+ CentralLog.i(
+ TAG,
+ "Attempting to start service discovery on ${gatt.device.address}: $discoveryOn"
+ )
+ }
+ }
+ }
+
+ // New services discovered
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
+ when (status) {
+ BluetoothGatt.GATT_SUCCESS -> {
+ CentralLog.i(
+ TAG,
+ "onServicesDiscovered received: BluetoothGatt.GATT_SUCCESS - $status"
+ )
+ CentralLog.i(
+ TAG,
+ "Discovered ${gatt.services.size} services on ${gatt.device.address}"
+ )
+
+ val service = gatt.getService(serviceUUID)
+
+ service?.let {
+ val characteristic = service.getCharacteristic(serviceUUID)
+ if (characteristic != null) {
+ val readSuccess = gatt.readCharacteristic(characteristic)
+ CentralLog.i(
+ TAG,
+ "Attempt to read characteristic of our service on ${gatt.device.address}: $readSuccess"
+ )
+ } else {
+ CentralLog.e(
+ TAG,
+ "${gatt.device.address} does not have our characteristic"
+ )
+ endWorkConnection(gatt)
+ }
+ }
+
+ if (service == null) {
+ CentralLog.e(
+ TAG,
+ "${gatt.device.address} does not have our service"
+ )
+ endWorkConnection(gatt)
+ }
+ }
+ else -> {
+ CentralLog.w(TAG, "No services discovered on ${gatt.device.address}")
+ endWorkConnection(gatt)
+ }
+ }
+ }
+
+ // data read from a perhipheral
+ //I am a central
+ override fun onCharacteristicRead(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ status: Int
+ ) {
+
+ CentralLog.i(TAG, "Read Status: $status")
+ when (status) {
+ BluetoothGatt.GATT_SUCCESS -> {
+ CentralLog.i(
+ TAG,
+ "Characteristic read from ${gatt.device.address}: ${characteristic.getStringValue(
+ 0
+ )}"
+ )
+
+ when (characteristic.uuid) {
+
+ serviceUUID -> {
+ //need to populate in the rssi here?
+ CentralLog.i(
+ TAG,
+ "onCharacteristicRead: ${work.device.address} - [${work.connectable.rssi}]"
+ )
+
+ val dataBytes = characteristic.value
+
+ try {
+ val readData = ReadRequestPayload.createReadRequestPayload(dataBytes)
+ val peripheral =
+ PeripheralDevice(readData.modelP, work.device.address)
+
+ val connectionRecord = ConnectionRecord(
+ version = readData.v,
+ msg = readData.msg,
+ org = readData.org,
+ peripheral = peripheral,
+ central = TracerApp.asCentralDevice(),
+ rssi = work.connectable.rssi,
+ txPower = work.connectable.transmissionPower
+ )
+
+ Utils.broadcastStreetPassReceived(
+ context,
+ connectionRecord
+ )
+
+ } catch (e: Throwable) {
+ CentralLog.e(
+ TAG,
+ "Failed to de-serialize request payload object - ${e.message}"
+ )
+ }
+ }
+ }
+ work.checklist.readCharacteristic.status = true
+ work.checklist.readCharacteristic.timePerformed = System.currentTimeMillis()
+ }
+
+ else -> {
+ CentralLog.w(
+ TAG,
+ "Failed to read characteristics from ${gatt.device.address}: $status"
+ )
+ }
+ }
+
+ // Only attempt to write BM back to peripheral if it is still valid
+ if (Utils.bmValid(context)) {
+ //may have failed to read, can try to write
+ //we are writing as the central device
+ val thisCentralDevice = TracerApp.asCentralDevice()
+
+ val writedata = WriteRequestPayload(
+ v = TracerApp.protocolVersion,
+ msg = TracerApp.thisDeviceMsg(),
+ org = TracerApp.ORG,
+ modelC = thisCentralDevice.modelC,
+ rssi = work.connectable.rssi,
+ txPower = work.connectable.transmissionPower
+ )
+
+ characteristic.value = writedata.getPayload()
+ val writeSuccess = gatt.writeCharacteristic(characteristic)
+ CentralLog.i(
+ TAG,
+ "Attempt to write characteristic to our service on ${gatt.device.address}: $writeSuccess"
+ )
+ } else {
+ CentralLog.i(
+ TAG,
+ "Expired BM. Skipping attempt to write characteristic to our service on ${gatt.device.address}"
+ )
+
+ endWorkConnection(gatt)
+ }
+ }
+
+ override fun onCharacteristicWrite(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ status: Int
+ ) {
+
+ when (status) {
+ BluetoothGatt.GATT_SUCCESS -> {
+ CentralLog.i(TAG, "Characteristic wrote successfully")
+ work.checklist.writeCharacteristic.status = true
+ work.checklist.writeCharacteristic.timePerformed = System.currentTimeMillis()
+ }
+ else -> {
+ CentralLog.i(TAG, "Failed to write characteristics: $status")
+ }
+ }
+
+ endWorkConnection(gatt)
+ }
+
+ }
+
+ fun terminateConnections() {
+ CentralLog.d(TAG, "Cleaning up worker.")
+
+ currentPendingConnection?.gatt?.disconnect()
+ currentPendingConnection = null
+
+ timeoutHandler.removeCallbacksAndMessages(null)
+ queueHandler.removeCallbacksAndMessages(null)
+ blacklistHandler.removeCallbacksAndMessages(null)
+
+ //concurrent modifications?
+ workQueue.clear()
+ blacklist.clear()
+ }
+
+ fun unregisterReceivers() {
+ try {
+ localBroadcastManager.unregisterReceiver(deviceProcessedReceiver)
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "Unable to close receivers: ${e.localizedMessage}")
+ }
+
+ try {
+ localBroadcastManager.unregisterReceiver(workReceiver)
+ } catch (e: Throwable) {
+ CentralLog.e(TAG, "Unable to close receivers: ${e.localizedMessage}")
+ }
+ }
+
+ inner class DeviceProcessedReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (ACTION_DEVICE_PROCESSED == intent.action) {
+ val deviceAddress = intent.getStringExtra(DEVICE_ADDRESS)
+ CentralLog.d(TAG, "Adding to blacklist: $deviceAddress")
+ val entry = BlacklistEntry(deviceAddress)
+ blacklist.add(entry)
+ blacklistHandler.postDelayed({
+ CentralLog.i(
+ TAG,
+ "blacklist for ${entry.uniqueIdentifier} removed? : ${blacklist.remove(entry)}"
+ )
+ }, blacklistDuration)
+ }
+ }
+ }
+
+ inner class StreetPassWorkReceiver : BroadcastReceiver() {
+
+ private val TAG = "StreetPassWorkReceiver"
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+
+ intent?.let {
+ if (ACTION_DEVICE_SCANNED == intent.action) {
+ //get data from extras
+ val device: BluetoothDevice? =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ val connectable: ConnectablePeripheral? =
+ intent.getParcelableExtra(CONNECTION_DATA)
+
+ val devicePresent = device != null
+ val connectablePresent = connectable != null
+
+ CentralLog.i(
+ TAG,
+ "Device received: ${device?.address}. Device present: $devicePresent, Connectable Present: $connectablePresent"
+ )
+
+ device?.let {
+ connectable?.let {
+ val work = Work(device, connectable, onWorkTimeoutListener)
+ if (addWork(work)) {
+ doWork()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt
new file mode 100644
index 0000000..a7c47d2
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt
@@ -0,0 +1,71 @@
+package au.gov.health.covidsafe.streetpass
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.content.Context
+import com.google.gson.Gson
+import au.gov.health.covidsafe.logging.CentralLog
+import kotlin.properties.Delegates
+
+class Work constructor(
+ var device: BluetoothDevice,
+ var connectable: ConnectablePeripheral,
+ private val onWorkTimeoutListener: OnWorkTimeoutListener
+) : Comparable {
+ var timeStamp: Long by Delegates.notNull()
+ var checklist = WorkCheckList()
+ var gatt: BluetoothGatt? = null
+ var finished = false
+ var timeout : Long = 0
+
+ private val TAG = "Work"
+
+ val timeoutRunnable: Runnable = Runnable {
+ onWorkTimeoutListener.onWorkTimeout(this)
+ }
+
+ init {
+ timeStamp = System.currentTimeMillis()
+ }
+
+ fun isCriticalsCompleted(): Boolean {
+ return (checklist.connected.status && checklist.readCharacteristic.status && checklist.writeCharacteristic.status) || checklist.skipped.status
+ }
+
+ fun startWork(
+ context: Context,
+ gattCallback: StreetPassWorker.StreetPassGattCallback
+ ) {
+ gatt = device.connectGatt(context, false, gattCallback)
+ if (gatt == null) {
+ CentralLog.e(TAG, "Unable to connect to ${device.address}")
+ }
+ }
+
+ override fun compareTo(other: Work): Int {
+ return -(timeStamp - other.timeStamp).toInt()
+ }
+
+ inner class WorkCheckList {
+ var started = Check()
+ var connected = Check()
+ var mtuChanged = Check()
+ var readCharacteristic = Check()
+ var writeCharacteristic = Check()
+ var disconnected = Check()
+ var skipped = Check()
+
+ override fun toString(): String {
+ return Gson().toJson(this)
+ }
+ }
+
+ inner class Check {
+ var status = false
+ var timePerformed: Long = 0
+ }
+
+ interface OnWorkTimeoutListener {
+ fun onWorkTimeout(work: Work)
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecord.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecord.kt
new file mode 100644
index 0000000..eb5feea
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecord.kt
@@ -0,0 +1,45 @@
+package au.gov.health.covidsafe.streetpass.persistence
+
+import androidx.annotation.Keep
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Keep
+@Entity(tableName = "record_table")
+class StreetPassRecord(
+ @ColumnInfo(name = "v")
+ var v: Int,
+
+ @ColumnInfo(name = "msg")
+ var msg: String,
+
+ @ColumnInfo(name = "org")
+ var org: String,
+
+ @ColumnInfo(name = "modelP")
+ val modelP: String,
+
+ @ColumnInfo(name = "modelC")
+ val modelC: String,
+
+ @ColumnInfo(name = "rssi")
+ val rssi: Int,
+
+ @ColumnInfo(name = "txPower")
+ val txPower: Int?
+
+) {
+
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Int = 0
+
+ @ColumnInfo(name = "timestamp")
+ var timestamp: Long = System.currentTimeMillis()
+
+ override fun toString(): String {
+ return "StreetPassRecord(v=$v, msg='$msg', org='$org', modelP='$modelP', modelC='$modelC', rssi=$rssi, txPower=$txPower, id=$id, timestamp=$timestamp)"
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt
new file mode 100644
index 0000000..9ea4678
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt
@@ -0,0 +1,31 @@
+package au.gov.health.covidsafe.streetpass.persistence
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import androidx.sqlite.db.SupportSQLiteQuery
+
+@Dao
+interface StreetPassRecordDao {
+
+ @Query("SELECT * from record_table ORDER BY timestamp ASC")
+ fun getRecords(): LiveData>
+
+ @Query("SELECT * from record_table ORDER BY timestamp DESC LIMIT 1")
+ fun getMostRecentRecord(): LiveData
+
+ @Query("SELECT * from record_table ORDER BY timestamp ASC")
+ fun getCurrentRecords(): List
+
+ @Query("DELETE FROM record_table WHERE timestamp <= :timeInMs")
+ fun deleteDataOlder(timeInMs: Long): Int
+
+ @Query("DELETE FROM record_table")
+ fun nukeDb()
+
+ @RawQuery
+ fun getRecordsViaQuery(query: SupportSQLiteQuery): List
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insert(record: StreetPassRecord)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDatabase.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDatabase.kt
new file mode 100644
index 0000000..b990256
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDatabase.kt
@@ -0,0 +1,76 @@
+package au.gov.health.covidsafe.streetpass.persistence
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import au.gov.health.covidsafe.status.persistence.StatusRecord
+import au.gov.health.covidsafe.status.persistence.StatusRecordDao
+
+
+@Database(
+ entities = [StreetPassRecord::class, StatusRecord::class],
+ version = 2,
+ exportSchema = true
+)
+abstract class StreetPassRecordDatabase : RoomDatabase() {
+
+ abstract fun recordDao(): StreetPassRecordDao
+ abstract fun statusDao(): StatusRecordDao
+
+ companion object {
+ @Volatile
+ private var INSTANCE: StreetPassRecordDatabase? = null
+
+ fun getDatabase(context: Context): StreetPassRecordDatabase {
+ val tempInstance = INSTANCE
+ if (tempInstance != null) {
+ return tempInstance
+ }
+ synchronized(this) {
+ val instance = Room.databaseBuilder(
+ context,
+ StreetPassRecordDatabase::class.java,
+ "record_database"
+ )
+ .addMigrations(MIGRATION_1_2)
+ .build()
+ INSTANCE = instance
+ return instance
+ }
+ }
+
+ private val MIGRATION_1_2 = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ //adding of status table
+ database.execSQL("CREATE TABLE IF NOT EXISTS `status_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `msg` TEXT NOT NULL)")
+
+ if (!isFieldExist(database, "record_table", "v")) {
+ database.execSQL("ALTER TABLE `record_table` ADD COLUMN `v` INTEGER NOT NULL DEFAULT 1")
+ }
+
+ if (!isFieldExist(database, "record_table", "org")) {
+ database.execSQL("ALTER TABLE `record_table` ADD COLUMN `org` TEXT NOT NULL DEFAULT 'AU_DTA'")
+ }
+
+ }
+ }
+
+ fun isFieldExist(db: SupportSQLiteDatabase, tableName: String, fieldName: String): Boolean {
+ var isExist = false
+ val res =
+ db.query("PRAGMA table_info($tableName)", null)
+ res.moveToFirst()
+ do {
+ val currentColumn = res.getString(1)
+ if (currentColumn == fieldName) {
+ isExist = true
+ }
+ } while (res.moveToNext())
+ return isExist
+ }
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordRepository.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordRepository.kt
new file mode 100644
index 0000000..2eb7280
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordRepository.kt
@@ -0,0 +1,8 @@
+package au.gov.health.covidsafe.streetpass.persistence
+
+import androidx.lifecycle.LiveData
+
+class StreetPassRecordRepository(recordDao: StreetPassRecordDao) {
+ val allRecords: LiveData> = recordDao.getRecords()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt
new file mode 100644
index 0000000..53c8536
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt
@@ -0,0 +1,29 @@
+package au.gov.health.covidsafe.streetpass.persistence
+
+import android.content.Context
+
+class StreetPassRecordStorage(val context: Context) {
+
+ private val recordDao = StreetPassRecordDatabase.getDatabase(context).recordDao()
+
+ suspend fun saveRecord(record: StreetPassRecord) {
+ recordDao.insert(record)
+ }
+
+ fun deleteDataOlderThan(timeInMs: Long): Int {
+ return recordDao.deleteDataOlder(timeInMs)
+ }
+
+ fun nukeDb() {
+ recordDao.nukeDb()
+ }
+
+ suspend fun nukeDbAsync() {
+ recordDao.nukeDb()
+ }
+
+ fun getAllRecords(): List {
+ return recordDao.getCurrentRecords()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/view/RecordViewModel.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/view/RecordViewModel.kt
new file mode 100644
index 0000000..29ef022
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/view/RecordViewModel.kt
@@ -0,0 +1,23 @@
+package au.gov.health.covidsafe.streetpass.view
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordRepository
+
+class RecordViewModel(app: Application) : AndroidViewModel(app) {
+
+ private var repo: StreetPassRecordRepository
+
+ var allRecords: LiveData>
+
+ init {
+ val recordDao = StreetPassRecordDatabase.getDatabase(app).recordDao()
+ repo = StreetPassRecordRepository(recordDao)
+ allRecords = repo.allRecords
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/view/StreetPassRecordViewModel.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/view/StreetPassRecordViewModel.kt
new file mode 100644
index 0000000..a41417e
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/view/StreetPassRecordViewModel.kt
@@ -0,0 +1,16 @@
+package au.gov.health.covidsafe.streetpass.view
+
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+
+class StreetPassRecordViewModel(record: StreetPassRecord, val number: Int) {
+ val version = record.v
+ val modelC = record.modelC
+ val modelP = record.modelP
+ val msg = record.msg
+ val timeStamp = record.timestamp
+ val rssi = record.rssi
+ val transmissionPower = record.txPower
+ val org = record.org
+
+ constructor(record: StreetPassRecord) : this(record, 1)
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/BaseFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/BaseFragment.kt
new file mode 100644
index 0000000..b9b45f6
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/BaseFragment.kt
@@ -0,0 +1,34 @@
+package au.gov.health.covidsafe.ui
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.navigation.Navigator
+import androidx.navigation.fragment.NavHostFragment
+import au.gov.health.covidsafe.HasBlockingState
+
+open class BaseFragment : Fragment() {
+
+ override fun onResume() {
+ super.onResume()
+ val activity = this.activity
+ if (activity is HasBlockingState) {
+ activity.isUiBlocked = false
+ }
+ }
+
+ protected fun navigateTo(actionId: Int, bundle: Bundle? = null, navigatorExtras: Navigator.Extras? = null) {
+ val activity = this.activity
+ if (activity is HasBlockingState) {
+ activity.isUiBlocked = true
+ }
+ NavHostFragment.findNavController(this).navigate(actionId, bundle, null, navigatorExtras)
+ }
+
+ protected fun popBackStack() {
+ val activity = this.activity
+ if (activity is HasBlockingState) {
+ activity.isUiBlocked = true
+ }
+ NavHostFragment.findNavController(this).popBackStack()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt
new file mode 100644
index 0000000..ab462e9
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt
@@ -0,0 +1,63 @@
+package au.gov.health.covidsafe.ui
+
+import androidx.annotation.StringRes
+
+abstract class PagerChildFragment : BaseFragment() {
+ override fun onResume() {
+ super.onResume()
+ updateToolBar()
+ updateButton()
+ updateProgressBar()
+ updateButtonState()
+ }
+
+ private fun updateProgressBar() {
+ (parentFragment?.parentFragment as? PagerContainer)?.updateProgressBar(stepProgress)
+ (activity as? PagerContainer)?.updateProgressBar(stepProgress)
+ }
+
+ private fun updateToolBar() {
+ (parentFragment?.parentFragment as? PagerContainer)?.setNavigationIcon(navigationIcon)
+ (activity as? PagerContainer)?.setNavigationIcon(navigationIcon)
+ }
+
+ private fun updateButton() {
+ val updateButtonLayout = getUploadButtonLayout()
+ if (updateButtonLayout is UploadButtonLayout.ContinueLayout) {
+ updateButtonState()
+ }
+ (parentFragment?.parentFragment as? PagerContainer)?.refreshButton(updateButtonLayout)
+ (activity as? PagerContainer)?.refreshButton(updateButtonLayout)
+ }
+
+ fun enableContinueButton() {
+ (parentFragment?.parentFragment as? PagerContainer)?.enableNextButton()
+ (activity as? PagerContainer)?.enableNextButton()
+ }
+
+ fun disableContinueButton() {
+ (parentFragment?.parentFragment as? PagerContainer)?.disableNextButton()
+ (activity as? PagerContainer)?.disableNextButton()
+ }
+
+ fun showLoading() {
+ (parentFragment?.parentFragment as? PagerContainer)?.showLoading()
+ (activity as? PagerContainer)?.showLoading()
+ }
+
+ fun hideLoading() {
+ (parentFragment?.parentFragment as? PagerContainer)?.hideLoading((getUploadButtonLayout() as? UploadButtonLayout.ContinueLayout)?.buttonText)
+ (activity as? PagerContainer)?.hideLoading((getUploadButtonLayout() as? UploadButtonLayout.ContinueLayout)?.buttonText)
+ }
+
+ abstract val navigationIcon: Int?
+ abstract var stepProgress: Int?
+ abstract fun getUploadButtonLayout(): UploadButtonLayout
+ abstract fun updateButtonState()
+}
+
+sealed class UploadButtonLayout {
+ class ContinueLayout(@StringRes val buttonText: Int, val buttonListener: (() -> Unit)?) : UploadButtonLayout()
+ class QuestionLayout(val buttonYesListener: () -> Unit, val buttonNoListener: () -> Unit) : UploadButtonLayout()
+}
+
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/PagerContainer.kt b/app/src/main/java/au/gov/health/covidsafe/ui/PagerContainer.kt
new file mode 100644
index 0000000..30d4504
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/PagerContainer.kt
@@ -0,0 +1,13 @@
+package au.gov.health.covidsafe.ui
+
+import androidx.annotation.StringRes
+
+interface PagerContainer {
+ fun enableNextButton()
+ fun disableNextButton()
+ fun showLoading()
+ fun hideLoading(@StringRes stringRes: Int?)
+ fun updateProgressBar(stepProgress: Int?)
+ fun setNavigationIcon(navigationIcon: Int?)
+ fun refreshButton(updateButtonLayout: UploadButtonLayout)
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt
new file mode 100644
index 0000000..89bcd9d
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt
@@ -0,0 +1,79 @@
+package au.gov.health.covidsafe.ui.home
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.core.view.isVisible
+import androidx.navigation.fragment.findNavController
+import com.atlassian.mobilekit.module.feedback.FeedbackModule
+import kotlinx.android.synthetic.main.fragment_help.*
+import kotlinx.android.synthetic.main.fragment_help.view.*
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.BaseFragment
+
+class HelpFragment : BaseFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? = inflater.inflate(R.layout.fragment_help, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val webView = view.helpWebView
+ webView.settings.javaScriptEnabled = true
+ webView.webViewClient = createWebVieClient(view)
+ webView.loadUrl(HELP_URL)
+ reportAnIssue.setOnClickListener {
+ FeedbackModule.showFeedbackScreen()
+ }
+ toolbar.setNavigationOnClickListener { findNavController().popBackStack() }
+ }
+
+ private fun createWebVieClient(view: View): WebViewClient =
+ object : WebViewClient() {
+ private var isRedirecting = false
+ private var loadFinished = false
+
+ override fun shouldOverrideUrlLoading(webView: WebView, request: WebResourceRequest): Boolean {
+ if (!loadFinished) isRedirecting = true
+ loadFinished = false
+ val urlString = request.url.toString()
+ if (urlString == HELP_URL) {
+ webView.loadUrl(request.url.toString())
+ } else {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(urlString))
+ webView.context.startActivity(intent)
+ }
+ return true
+ }
+
+ override fun onPageStarted(webView: WebView, url: String?, favicon: Bitmap?) {
+ super.onPageStarted(webView, url, favicon)
+ loadFinished = false
+ view.progress.isVisible = true
+ }
+
+ override fun onPageFinished(webView: WebView, url: String?) {
+ super.onPageFinished(webView, url)
+
+ if (!isRedirecting) loadFinished = true
+
+ if (loadFinished && !isRedirecting) {
+ view.progress.isVisible = false
+ } else {
+ isRedirecting = false
+ }
+ }
+ }
+}
+
+private const val HELP_URL = "https://www.covidsafe.gov.au/help-topics.html"
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt
new file mode 100644
index 0000000..3141017
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragment.kt
@@ -0,0 +1,332 @@
+package au.gov.health.covidsafe.ui.home
+
+import android.Manifest
+import android.bluetooth.BluetoothAdapter
+import android.content.*
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.navigation.fragment.findNavController
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.WebViewActivity
+import au.gov.health.covidsafe.extensions.*
+import au.gov.health.covidsafe.ui.BaseFragment
+import kotlinx.android.synthetic.main.fragment_home.*
+import kotlinx.android.synthetic.main.fragment_home.view.*
+import kotlinx.android.synthetic.main.fragment_home_external_links.*
+import kotlinx.android.synthetic.main.fragment_home_setup_complete_header.*
+import kotlinx.android.synthetic.main.fragment_home_setup_incomplete_content.*
+import pub.devrel.easypermissions.AppSettingsDialog
+import pub.devrel.easypermissions.EasyPermissions
+
+class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
+
+ private lateinit var presenter: HomePresenter
+
+ private var mIsBroadcastListenerRegistered = false
+
+ private var counter: Int = 0
+
+ private val mBroadcastListener: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action
+ if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
+ when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
+ BluetoothAdapter.STATE_OFF -> {
+ bluetooth_card_view.render(formatBlueToothTitle(false), false)
+ refreshSetupCompleteOrIncompleteUi()
+ }
+ BluetoothAdapter.STATE_TURNING_OFF -> {
+ bluetooth_card_view.render(formatBlueToothTitle(false), false)
+ refreshSetupCompleteOrIncompleteUi()
+ }
+ BluetoothAdapter.STATE_ON -> {
+ bluetooth_card_view.render(formatBlueToothTitle(true), true)
+ refreshSetupCompleteOrIncompleteUi()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ presenter = HomePresenter(this)
+ return inflater.inflate(R.layout.fragment_home, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.home_header_help.setOnClickListener {
+ findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment())
+ }
+ if (BuildConfig.ENABLE_DEBUG_SCREEN) {
+ view.header_background.setOnClickListener {
+ counter++
+ if (counter >= 2) {
+ counter = 0
+ findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToPeekActivity())
+ }
+ }
+ }
+ home_version_number.text = getString(R.string.home_version_number, BuildConfig.VERSION_NAME)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ bluetooth_card_view.setOnClickListener { requestBlueToothPermissionThenNextPermission() }
+ location_card_view.setOnClickListener { askForLocationPermission() }
+ battery_card_view.setOnClickListener { excludeFromBatteryOptimization() }
+ home_been_tested_button.setOnClickListener {
+ navigateTo(R.id.action_home_to_selfIsolate)
+ }
+ home_setup_complete_share.setOnClickListener {
+ shareThisApp()
+ }
+ home_setup_complete_news.setOnClickListener {
+ goToNewsWebsite()
+ }
+ home_setup_complete_app.setOnClickListener {
+ goToCovidApp()
+ }
+
+ if (!mIsBroadcastListenerRegistered) {
+ registerBroadcast()
+ }
+ refreshSetupCompleteOrIncompleteUi()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ bluetooth_card_view.setOnClickListener(null)
+ location_card_view.setOnClickListener(null)
+ battery_card_view.setOnClickListener(null)
+ home_been_tested_button.setOnClickListener(null)
+ home_setup_complete_share.setOnClickListener(null)
+ home_setup_complete_news.setOnClickListener(null)
+ home_setup_complete_app.setOnClickListener(null)
+ activity?.let { activity ->
+ if (mIsBroadcastListenerRegistered) {
+ activity.unregisterReceiver(mBroadcastListener)
+ mIsBroadcastListenerRegistered = false
+ }
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ home_root.removeAllViews()
+ }
+
+ private fun refreshSetupCompleteOrIncompleteUi() {
+ val isUploaded = context?.let {
+ Preference.isDataUploaded(it)
+ } ?: run {
+ false
+ }
+ home_been_tested_button.visibility = if (isUploaded) GONE else VISIBLE
+ when {
+ !allPermissionsEnabled() -> {
+ home_header_setup_complete_header_uploaded.visibility = GONE
+ home_header_setup_complete_header_divider.visibility = GONE
+ home_header_setup_complete_header.setText(R.string.home_header_inactive_title)
+ home_header_picture_setup_complete.setImageResource(R.drawable.ic_logo_home_inactive)
+ home_header_help.setImageResource(R.drawable.ic_help_outline_black)
+ context?.let { context ->
+ val backGroundColor = ContextCompat.getColor(context, R.color.grey)
+ header_background.setBackgroundColor(backGroundColor)
+ header_background_overlap.setBackgroundColor(backGroundColor)
+
+ val textColor = ContextCompat.getColor(context, R.color.slack_black)
+ home_header_setup_complete_header_uploaded.setTextColor(textColor)
+ home_header_setup_complete_header.setTextColor(textColor)
+ }
+ content_setup_incomplete_group.visibility = VISIBLE
+ updateBlueToothStatus()
+ updatePushNotificationStatus()
+ updateBatteryOptimizationStatus()
+ updateLocationStatus()
+ }
+ isUploaded -> {
+ home_header_setup_complete_header_uploaded.visibility = VISIBLE
+ home_header_setup_complete_header_divider.visibility = VISIBLE
+ home_header_setup_complete_header.setText(R.string.home_header_active_title)
+ home_header_picture_setup_complete.setImageResource(R.drawable.ic_logo_home_uploaded)
+ home_header_picture_setup_complete.setAnimation("spinner_home_upload_complete.json")
+ home_header_help.setImageResource(R.drawable.ic_help_outline_white)
+ content_setup_incomplete_group.visibility = GONE
+ context?.let { context ->
+ val backGroundColor = ContextCompat.getColor(context, R.color.dark_green)
+ header_background.setBackgroundColor(backGroundColor)
+ header_background_overlap.setBackgroundColor(backGroundColor)
+
+ val textColor = ContextCompat.getColor(context, R.color.white)
+ home_header_setup_complete_header_uploaded.setTextColor(textColor)
+ home_header_setup_complete_header.setTextColor(textColor)
+ }
+ }
+
+ else -> {
+ home_header_setup_complete_header_uploaded.visibility = GONE
+ home_header_setup_complete_header_divider.visibility = GONE
+ home_header_setup_complete_header.setText(R.string.home_header_active_title)
+ home_header_help.setImageResource(R.drawable.ic_help_outline_black)
+ home_header_picture_setup_complete.setAnimation("spinner_home.json")
+ content_setup_incomplete_group.visibility = GONE
+ context?.let { context ->
+ val backGroundColor = ContextCompat.getColor(context, R.color.lighter_green)
+ header_background.setBackgroundColor(backGroundColor)
+ header_background_overlap.setBackgroundColor(backGroundColor)
+
+ val textColor = ContextCompat.getColor(context, R.color.slack_black)
+ home_header_setup_complete_header_uploaded.setTextColor(textColor)
+ home_header_setup_complete_header.setTextColor(textColor)
+ }
+ }
+ }
+ }
+
+ private fun allPermissionsEnabled(): Boolean {
+ val bluetoothEnabled = isBlueToothEnabled() ?: true
+ val pushNotificationEnabled = isPushNotificationEnabled() ?: true
+ val nonBatteryOptimizationAllowed = isNonBatteryOptimizationAllowed() ?: true
+ val locationStatusAllowed = isFineLocationEnabled() ?: true
+
+ return bluetoothEnabled &&
+ pushNotificationEnabled &&
+ nonBatteryOptimizationAllowed &&
+ locationStatusAllowed
+ }
+
+ private fun registerBroadcast() {
+ activity?.let { activity ->
+ var f = IntentFilter()
+ activity.registerReceiver(mBroadcastListener, f)
+ // bluetooth on/off
+ f = IntentFilter()
+ f.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
+ activity.registerReceiver(mBroadcastListener, f)
+ mIsBroadcastListenerRegistered = true
+ }
+ }
+
+ private fun shareThisApp() {
+ val newIntent = Intent(Intent.ACTION_SEND)
+ newIntent.type = "text/plain"
+ newIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_this_app_content))
+ newIntent.putExtra(Intent.EXTRA_HTML_TEXT, getString(R.string.share_this_app_content_html))
+ startActivity(Intent.createChooser(newIntent, null))
+ }
+
+ private fun updateBlueToothStatus() {
+ isBlueToothEnabled()?.let {
+ bluetooth_card_view.visibility = VISIBLE
+ bluetooth_card_view.render(formatBlueToothTitle(it), it)
+ } ?: run {
+ bluetooth_card_view.visibility = GONE
+ }
+ }
+
+ private fun updatePushNotificationStatus() {
+ isPushNotificationEnabled()?.let {
+ push_card_view.visibility = VISIBLE
+ push_card_view.render(formatPushNotificationTitle(it), it)
+ } ?: run {
+ push_card_view.visibility = GONE
+ }
+ }
+
+ private fun updateBatteryOptimizationStatus() {
+ isNonBatteryOptimizationAllowed()?.let {
+ battery_card_view.visibility = VISIBLE
+ battery_card_view.render(formatNonBatteryOptimizationTitle(!it), it)
+ } ?: run {
+ battery_card_view.visibility = GONE
+ }
+ }
+
+ private fun updateLocationStatus() {
+ isFineLocationEnabled()?.let {
+ location_card_view.visibility = VISIBLE
+ location_card_view.render(formatLocationTitle(it), it)
+ } ?: run {
+ location_card_view.visibility = VISIBLE
+ }
+ }
+
+ private fun formatBlueToothTitle(on: Boolean): String {
+ return resources.getString(R.string.home_bluetooth_permission, getPermissionEnabledTitle(on))
+ }
+
+ private fun formatLocationTitle(on: Boolean): String {
+ return resources.getString(R.string.home_location_permission, getPermissionEnabledTitle(on))
+ }
+
+ private fun formatNonBatteryOptimizationTitle(on: Boolean): String {
+ return resources.getString(R.string.home_non_battery_optimization_permission, getPermissionEnabledTitle(on))
+ }
+
+ private fun formatPushNotificationTitle(on: Boolean): String {
+ return resources.getString(R.string.home_push_notification_permission, getPermissionEnabledTitle(on))
+ }
+
+ private fun getPermissionEnabledTitle(on: Boolean): String {
+ return resources.getString(if (on) R.string.home_permission_on else R.string.home_permission_off)
+ }
+
+ private fun goToNewsWebsite() {
+ val url = getString(R.string.home_set_complete_external_link_news_url)
+ try {
+ Intent(Intent.ACTION_VIEW).run {
+ data = Uri.parse(url)
+ startActivity(this)
+ }
+ } catch (e: ActivityNotFoundException) {
+ val intent = Intent(activity, WebViewActivity::class.java)
+ intent.putExtra(WebViewActivity.URL_ARG, url)
+ startActivity(intent)
+ }
+ }
+
+ private fun goToCovidApp() {
+ val url = getString(R.string.home_set_complete_external_link_app_url)
+ try {
+ Intent(Intent.ACTION_VIEW).run {
+ data = Uri.parse(url)
+ startActivity(this)
+ }
+ } catch (e: ActivityNotFoundException) {
+ val intent = Intent(activity, WebViewActivity::class.java)
+ intent.putExtra(WebViewActivity.URL_ARG, url)
+ startActivity(intent)
+ }
+ }
+
+ override fun onPermissionsDenied(requestCode: Int, perms: MutableList) {
+ if (requestCode == LOCATION && EasyPermissions.somePermissionPermanentlyDenied(this, listOf(Manifest.permission.ACCESS_FINE_LOCATION))) {
+ AppSettingsDialog.Builder(this).build().show()
+ }
+ }
+
+ override fun onPermissionsGranted(requestCode: Int, perms: MutableList) {
+ if (requestCode == LOCATION) {
+ checkBLESupport()
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt
new file mode 100644
index 0000000..e5a5aa4
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt
@@ -0,0 +1,10 @@
+package au.gov.health.covidsafe.ui.home
+
+import androidx.lifecycle.LifecycleObserver
+
+class HomePresenter(fragment: HomeFragment) : LifecycleObserver {
+
+ init {
+ fragment.lifecycle.addObserver(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt
new file mode 100644
index 0000000..f5f1efe
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt
@@ -0,0 +1,34 @@
+package au.gov.health.covidsafe.ui.home.view
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import au.gov.health.covidsafe.R
+import kotlinx.android.synthetic.main.view_card_external_link_card.view.*
+
+class ExternalLinkCard @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.view_card_external_link_card, this, true)
+
+ val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ExternalLinkCard)
+ val icon = a.getDrawable(R.styleable.ExternalLinkCard_external_linkCard_icon)
+ val title = a.getString(R.styleable.ExternalLinkCard_external_linkCard_title)
+ val content = a.getString(R.styleable.ExternalLinkCard_external_linkCard_content)
+ val padding = a.getDimension(R.styleable.ExternalLinkCard_external_linkCard_icon_padding, 0f).toInt()
+ val iconBackground = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_icon_background, R.color.transparent)
+
+ external_link_round_image.setImageDrawable(icon)
+ external_link_round_image.setBackgroundResource(iconBackground)
+ external_link_round_image.setPadding(padding, padding, padding, padding)
+ external_link_headline.text = title
+ external_link_content.text = content
+ a.recycle()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt
new file mode 100644
index 0000000..c1869df
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt
@@ -0,0 +1,37 @@
+package au.gov.health.covidsafe.ui.home.view
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import au.gov.health.covidsafe.R
+import kotlinx.android.synthetic.main.view_card_permission_card.view.*
+
+class PermissionStatusCard @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.view_card_permission_card, this, true)
+
+ val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.PermissionStatusCard)
+ val title = a.getString(R.styleable.PermissionStatusCard_permissionStatusCard_title)
+ a.recycle()
+
+ permission_title.text = title
+
+ val height = context.resources.getDimensionPixelSize(R.dimen.permission_height)
+ layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
+ }
+
+ fun render(text: String, correct: Boolean) {
+ permission_icon.isSelected = correct
+ permission_title.text = text
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/OnboardingActivity.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/OnboardingActivity.kt
new file mode 100644
index 0000000..01b5dbb
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/OnboardingActivity.kt
@@ -0,0 +1,104 @@
+package au.gov.health.covidsafe.ui.onboarding
+
+import android.os.Bundle
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import au.gov.health.covidsafe.HasBlockingState
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerContainer
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import com.github.razir.progressbutton.bindProgressButton
+import com.github.razir.progressbutton.hideProgress
+import com.github.razir.progressbutton.showProgress
+import kotlinx.android.synthetic.main.activity_onboarding.*
+
+class OnboardingActivity : FragmentActivity(), HasBlockingState, PagerContainer {
+
+ override var isUiBlocked: Boolean = false
+ set(value) {
+ loadingProgressBarFrame?.isVisible = value
+ field = value
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_onboarding)
+ bindProgressButton(onboarding_next)
+ if (isUiBlocked) {
+ loadingProgressBarFrame?.isVisible = true
+ }
+ }
+
+ override fun onAttachFragment(fragment: Fragment) {
+ super.onAttachFragment(fragment)
+ isUiBlocked = false
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ toolbar.setNavigationOnClickListener {
+ super.onBackPressed()
+ }
+ }
+
+ override fun updateProgressBar(stepProgress: Int?) {
+ stepProgress?.let { progress ->
+ onboarding_progress_bar.visibility = VISIBLE
+ onboarding_progress_bar.progress = progress
+ } ?: run {
+ onboarding_progress_bar.visibility = GONE
+ }
+ }
+
+ override fun setNavigationIcon(navigationIcon: Int?) {
+ if (navigationIcon == null) {
+ toolbar.navigationIcon = null
+ } else {
+ toolbar.navigationIcon = ContextCompat.getDrawable(this, navigationIcon)
+ }
+ }
+
+ override fun refreshButton(updateButtonLayout: UploadButtonLayout) {
+ if (updateButtonLayout is UploadButtonLayout.ContinueLayout) {
+ onboarding_next.setText(updateButtonLayout.buttonText)
+ onboarding_next.setOnClickListener {
+ updateButtonLayout.buttonListener?.invoke()
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ onboarding_next.setOnClickListener(null)
+ toolbar.setNavigationOnClickListener(null)
+ }
+
+ override fun enableNextButton() {
+ onboarding_next.isEnabled = true
+ }
+
+ override fun disableNextButton() {
+ onboarding_next.isEnabled = false
+ }
+
+ override fun showLoading() {
+ onboarding_next.showProgress {
+ progressColorRes = R.color.slack_black_2
+ }
+ }
+
+ override fun hideLoading(@StringRes stringRes: Int?) {
+ if (stringRes == null) {
+ onboarding_next.hideProgress()
+ } else {
+ onboarding_next.hideProgress(newTextRes = stringRes)
+ }
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt
new file mode 100644
index 0000000..b12d786
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt
@@ -0,0 +1,39 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.dataprivacy
+
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_data_privacy.*
+import kotlinx.android.synthetic.main.fragment_data_privacy.view.*
+
+class DataPrivacyFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_data_privacy, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.data_privacy_content.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.data_privacy_button) {
+ navigateTo(DataPrivacyFragmentDirections.actionDataPrivacyToRegistrationConsentFragment().actionId)
+ }
+
+ override fun updateButtonState() {
+ enableContinueButton()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt
new file mode 100644
index 0000000..f84425e
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt
@@ -0,0 +1,144 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.enternumber
+
+import android.app.AlertDialog
+import android.os.Bundle
+import android.text.Editable
+import android.text.InputFilter
+import android.text.TextWatcher
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.annotation.NavigationRes
+import androidx.core.os.bundleOf
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.TracerApp
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_CHALLENGE_NAME
+import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_DESTINATION_ID
+import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_PHONE_NUMBER
+import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_PROGRESS
+import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_SESSION
+import kotlinx.android.synthetic.main.fragment_enter_number.*
+import kotlinx.android.synthetic.main.fragment_enter_number.view.*
+
+class EnterNumberFragment : PagerChildFragment() {
+
+ companion object {
+ const val ENTER_NUMBER_DESTINATION_ID = "destination_id"
+ const val ENTER_NUMBER_PROGRESS = "progress"
+ }
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = 2
+
+ private val enterNumberPresenter = EnterNumberPresenter(this)
+ private var alertDialog: AlertDialog? = null
+ @NavigationRes
+ private var destinationId: Int? = null
+
+ private val phoneNumberTextWatcher: TextWatcher = object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {
+ // change LengthFilter if user making a mistake of entering phone number starting with 0
+ val phoneNumberLength = TracerApp.AppContext.resources.getInteger(R.integer.australian_phone_number_length)
+ val filters = enter_number_phone_number.filters
+ val newFilterLength = if (s?.toString()?.startsWith("0") == true) {
+ phoneNumberLength + 1
+ } else {
+ phoneNumberLength
+ }
+ enter_number_phone_number.filters = filters.filterNot { it is InputFilter.LengthFilter }.toTypedArray() +
+ InputFilter.LengthFilter(newFilterLength)
+
+ updateButtonState()
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_enter_number, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.use_oz_phone_number.movementMethod = LinkMovementMethod.getInstance()
+ arguments?.let {
+ destinationId = it.getInt(ENTER_NUMBER_DESTINATION_ID)
+ stepProgress = if (it.containsKey(ENTER_NUMBER_PROGRESS)) it.getInt(ENTER_PIN_PROGRESS) else null
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ enter_number_phone_number.selectAll()
+ enter_number_phone_number.addTextChangedListener(phoneNumberTextWatcher)
+ updateButtonState()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ enter_number_phone_number.removeTextChangedListener(phoneNumberTextWatcher)
+ }
+
+ fun showInvalidPhoneNumber() {
+ invalid_phone_number.visibility = VISIBLE
+ enter_number_phone_number.background = context?.getDrawable(R.drawable.phone_number_invalid_background)
+ }
+
+ override fun updateButtonState() {
+ if (enterNumberPresenter.validateAuNumber(enter_number_phone_number?.text?.toString())) {
+ enableContinueButton()
+ } else {
+ disableContinueButton()
+ }
+ }
+
+ fun showGenericError() {
+ alertDialog?.dismiss()
+ alertDialog = AlertDialog.Builder(activity)
+ .setMessage(R.string.generic_error)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.yes, null).show()
+ }
+
+ fun navigateToOTPPage(
+ session: String?,
+ challengeName: String?,
+ phoneNumber: String) {
+ val bundle = bundleOf(
+ ENTER_PIN_SESSION to session,
+ ENTER_PIN_CHALLENGE_NAME to challengeName,
+ ENTER_PIN_PHONE_NUMBER to phoneNumber,
+ ENTER_PIN_DESTINATION_ID to destinationId).also { bundle ->
+ stepProgress?.let {
+ bundle.putInt(ENTER_PIN_PROGRESS, it + 1)
+ }
+ }
+ navigateTo(R.id.action_enterNumberFragment_to_otpFragment, bundle)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ alertDialog?.dismiss()
+ root.removeAllViews()
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.enter_number_button) {
+ enterNumberPresenter.requestOTP(enter_number_phone_number.text.toString().trim())
+ }
+
+ fun showCheckInternetError() {
+ alertDialog?.dismiss()
+ alertDialog = AlertDialog.Builder(activity)
+ .setMessage(R.string.generic_internet_error)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.yes, null).show()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt
new file mode 100644
index 0000000..9167abb
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt
@@ -0,0 +1,91 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.enternumber
+
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.TracerApp
+import au.gov.health.covidsafe.extensions.isInternetAvailable
+import au.gov.health.covidsafe.factory.NetworkFactory
+import au.gov.health.covidsafe.interactor.usecase.GetOnboardingOtp
+import au.gov.health.covidsafe.interactor.usecase.GetOnboardingOtpException
+import au.gov.health.covidsafe.interactor.usecase.GetOtpParams
+
+
+class EnterNumberPresenter(private val enterNumberFragment: EnterNumberFragment) : LifecycleObserver {
+
+ private val TAG = this.javaClass.simpleName
+
+ private lateinit var phoneNumber: String
+ private lateinit var getOnboardingOtp: GetOnboardingOtp
+
+ init {
+ enterNumberFragment.lifecycle.addObserver(this)
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ private fun onCreate() {
+ getOnboardingOtp = GetOnboardingOtp(NetworkFactory.awsClient, enterNumberFragment.lifecycle)
+ }
+
+ internal fun requestOTP(phoneNumber: String) {
+ when {
+ enterNumberFragment.activity?.isInternetAvailable() == false -> {
+ enterNumberFragment.showCheckInternetError()
+ }
+ validateAuNumber(phoneNumber) -> {
+ val cleansedNumber = if (phoneNumber.startsWith("0")) {
+ phoneNumber.takeLast(TracerApp.AppContext.resources.getInteger(R.integer.australian_phone_number_length))
+ } else phoneNumber
+ val fullNumber = "${enterNumberFragment.resources.getString(R.string.enter_number_prefix)}$cleansedNumber"
+ Preference.putPhoneNumber(TracerApp.AppContext, fullNumber)
+ this.phoneNumber = cleansedNumber
+ makeOTPCall(cleansedNumber)
+ }
+ else -> {
+ enterNumberFragment.showInvalidPhoneNumber()
+ }
+ }
+ }
+
+ /**
+ * @param phoneNumber cleansed phone number, 9 digits, doesn't start with 0
+ */
+ private fun makeOTPCall(phoneNumber: String) {
+ enterNumberFragment.activity?.let {
+ enterNumberFragment.disableContinueButton()
+ enterNumberFragment.showLoading()
+ getOnboardingOtp.invoke(GetOtpParams(phoneNumber,
+ Preference.getDeviceID(enterNumberFragment.requireContext()),
+ Preference.getPostCode(enterNumberFragment.requireContext()),
+ Preference.getAge(enterNumberFragment.requireContext()),
+ Preference.getName(enterNumberFragment.requireContext())),
+ onSuccess = {
+ enterNumberFragment.navigateToOTPPage(
+ it.session,
+ it.challengeName,
+ phoneNumber)
+ },
+ onFailure = {
+ if (it is GetOnboardingOtpException.GetOtpInvalidNumberException) {
+ enterNumberFragment.showInvalidPhoneNumber()
+ } else {
+ enterNumberFragment.showGenericError()
+ }
+ enterNumberFragment.hideLoading()
+ enterNumberFragment.enableContinueButton()
+ })
+ }
+ }
+
+ internal fun validateAuNumber(phoneNumber: String?): Boolean {
+ var australianPhoneNumberLength = enterNumberFragment.resources.getInteger(R.integer.australian_phone_number_length)
+ if (phoneNumber?.startsWith("0") == true) {
+ australianPhoneNumberLength++
+ }
+ return (phoneNumber?.length ?: 0) == australianPhoneNumberLength
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt
new file mode 100644
index 0000000..13e97b0
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt
@@ -0,0 +1,191 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.enterpin
+
+import android.app.AlertDialog
+import android.os.Bundle
+import android.os.CountDownTimer
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.NavigationRes
+import androidx.core.content.ContextCompat
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.extensions.toHyperlink
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import com.atlassian.mobilekit.module.core.utils.SystemUtils
+import kotlinx.android.synthetic.main.fragment_enter_pin.*
+import kotlinx.android.synthetic.main.fragment_enter_pin.view.*
+import kotlin.math.floor
+
+
+class EnterPinFragment : PagerChildFragment() {
+
+ companion object {
+ const val ENTER_PIN_SESSION = "session"
+ const val ENTER_PIN_CHALLENGE_NAME = "challenge_name"
+ const val ENTER_PIN_PHONE_NUMBER = "phone_number"
+ const val ENTER_PIN_DESTINATION_ID = "destination_id"
+ const val ENTER_PIN_PROGRESS = "progress"
+ }
+
+ override val navigationIcon = R.drawable.ic_up
+ override var stepProgress: Int? = 3
+
+ private val COUNTDOWN_DURATION = 5 * 60L // OTP Code expiry
+
+ private var alertDialog: AlertDialog? = null
+ private var stopWatch: CountDownTimer? = null
+ private lateinit var presenter: EnterPinPresenter
+ @NavigationRes
+ private var destinationId: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_enter_pin, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ arguments?.let {
+ val session = it.getString(ENTER_PIN_SESSION)
+ val challengeName = it.getString(ENTER_PIN_CHALLENGE_NAME)
+ val phoneNumber = it.getString(ENTER_PIN_PHONE_NUMBER)
+ destinationId = it.getInt(ENTER_PIN_DESTINATION_ID)
+ stepProgress = if (it.containsKey(ENTER_PIN_PROGRESS)) it.getInt(ENTER_PIN_PROGRESS) else null
+ enter_pin_headline.text = resources.getString(R.string.enter_pin_headline, resources.getString(R.string.enter_number_prefix), phoneNumber)
+ presenter = EnterPinPresenter(this@EnterPinFragment,
+ session,
+ challengeName,
+ phoneNumber)
+ }
+
+ enter_pin_wrong_number.toHyperlink {
+ popBackStack()
+ }
+
+ enter_pin_resend_pin.toHyperlink {
+ presenter.resendCode()
+ }
+
+ view.pin_issue.movementMethod = LinkMovementMethod.getInstance()
+
+ startTimer()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateButtonState()
+ pin.onPinChanged = {
+ updateButtonState()
+ hideInvalidOtp()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ pin.onPinChanged = null
+ }
+
+ private fun startTimer() {
+ stopWatch = object : CountDownTimer(COUNTDOWN_DURATION * 1000, 1000) {
+ override fun onTick(millisUntilFinished: Long) {
+ val numberOfMins = floor((millisUntilFinished * 1.0) / 60000)
+ val numberOfMinsInt = numberOfMins.toInt()
+ val numberOfSeconds = floor((millisUntilFinished / 1000.0) % 60)
+ val numberOfSecondsInt = numberOfSeconds.toInt()
+ val finalNumberOfSecondsString = if (numberOfSecondsInt < 10) {
+ "0$numberOfSecondsInt"
+ } else {
+ "$numberOfSecondsInt"
+ }
+
+ enter_pin_timer_value?.text = "$numberOfMinsInt:$finalNumberOfSecondsString"
+ }
+
+ override fun onFinish() {
+ enter_pin_timer_value?.text = "0:00"
+ enter_pin_resend_pin.isEnabled = true
+ activity?.let {
+ enter_pin_resend_pin.setLinkTextColor(ContextCompat.getColor(it, R.color.hyperlink_enabled))
+ }
+ }
+ }
+ stopWatch?.start()
+ enter_pin_resend_pin.isEnabled = false
+ activity?.let {
+ enter_pin_resend_pin.setLinkTextColor(ContextCompat.getColor(it, R.color.hyperlink_disabled))
+ }
+ }
+
+ fun resetTimer() {
+ stopWatch?.cancel()
+ startTimer()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ stopWatch?.cancel()
+ alertDialog?.dismiss()
+ enter_pin_resend_pin.setOnClickListener(null)
+ enter_pin_wrong_number.setOnClickListener(null)
+ root.removeAllViews()
+ }
+
+ fun hideKeyboard() {
+ activity?.currentFocus?.let { view ->
+ SystemUtils.hideSoftKeyboard(view)
+ }
+ }
+
+ fun showInvalidOtp() {
+ enter_pin_error_label.visibility = View.VISIBLE
+ }
+
+ private fun hideInvalidOtp() {
+ enter_pin_error_label.visibility = View.GONE
+ }
+
+ fun showGenericError() {
+ alertDialog?.dismiss()
+ alertDialog = AlertDialog.Builder(activity)
+ .setMessage(R.string.generic_error)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.yes, null).show()
+ }
+
+ private fun isIncorrectPinFormat(): Boolean {
+ return requireView().pin.isIncomplete
+ }
+
+ override fun updateButtonState() {
+ if (isIncorrectPinFormat()) {
+ disableContinueButton()
+ } else {
+ enableContinueButton()
+ }
+ }
+
+ private fun validateOtp() {
+ presenter.validateOTP(requireView().pin.value)
+ }
+
+ fun showErrorOtpMustBeSixDigits() {
+
+ }
+
+ fun navigateToNextPage() {
+ navigateTo(destinationId ?: R.id.action_otpFragment_to_permissionFragment)
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.enter_pin_button) {
+ validateOtp()
+ }
+
+ fun showCheckInternetError() {
+ alertDialog?.dismiss()
+ alertDialog = AlertDialog.Builder(activity)
+ .setMessage(R.string.generic_internet_error)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.yes, null).show()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt
new file mode 100644
index 0000000..339e64c
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt
@@ -0,0 +1,112 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.enterpin
+
+import android.text.TextUtils
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.extensions.isInternetAvailable
+import au.gov.health.covidsafe.factory.NetworkFactory
+import au.gov.health.covidsafe.interactor.usecase.GetOnboardingOtp
+import au.gov.health.covidsafe.interactor.usecase.GetOtpParams
+import au.gov.health.covidsafe.logging.CentralLog
+import au.gov.health.covidsafe.networking.request.AuthChallengeRequest
+import au.gov.health.covidsafe.networking.response.AuthChallengeResponse
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+
+class EnterPinPresenter(private val enterPinFragment: EnterPinFragment,
+ private var session: String?,
+ private var challengeName: String?,
+ private val phoneNumber: String?) : LifecycleObserver {
+
+ private val TAG = this.javaClass.simpleName
+
+ private var awsClient = NetworkFactory.awsClient
+ private lateinit var getOtp: GetOnboardingOtp
+
+ init {
+ enterPinFragment.lifecycle.addObserver(this)
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ private fun onCreate() {
+ getOtp = GetOnboardingOtp(awsClient, enterPinFragment.lifecycle)
+ }
+
+ internal fun resendCode() {
+ enterPinFragment.activity?.let {
+ when {
+ !it.isInternetAvailable() -> {
+ enterPinFragment.showCheckInternetError()
+ }
+ phoneNumber == null -> {
+ enterPinFragment.showGenericError()
+ }
+ else -> {
+ getOtp.invoke(GetOtpParams(phoneNumber,
+ Preference.getDeviceID(enterPinFragment.requireContext()),
+ Preference.getPostCode(enterPinFragment.requireContext()),
+ Preference.getAge(enterPinFragment.requireContext()),
+ Preference.getName(enterPinFragment.requireContext())),
+ onSuccess = {
+ session = it.session
+ challengeName = it.challengeName
+ enterPinFragment.resetTimer()
+ },
+ onFailure = {
+ enterPinFragment.showGenericError()
+ })
+ }
+ }
+ }
+ }
+
+ internal fun validateOTP(otp: String) {
+ if (TextUtils.isEmpty(otp) || otp.length != 6) {
+ enterPinFragment.showErrorOtpMustBeSixDigits()
+ return
+ }
+ if (enterPinFragment.activity?.isInternetAvailable() == false) {
+ enterPinFragment.showCheckInternetError()
+ return
+ }
+ enterPinFragment.disableContinueButton()
+ enterPinFragment.showLoading()
+ val authChallengeCall: Call = awsClient.respondToAuthChallenge(AuthChallengeRequest(session, otp))
+ authChallengeCall.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ if (response.code() == 200) {
+ CentralLog.d(TAG, "code received")
+
+ val authChallengeResponse = response.body()
+
+ val handShakePin = authChallengeResponse?.pin
+ handShakePin?.let {
+ Preference.putHandShakePin(enterPinFragment.context, handShakePin)
+ }
+ val jwtToken = authChallengeResponse?.token
+ jwtToken.let {
+ Preference.putEncrypterJWTToken(enterPinFragment.requireContext(), jwtToken)
+ }
+ enterPinFragment.hideKeyboard()
+ enterPinFragment.navigateToNextPage()
+ } else {
+ onError()
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ onError()
+ }
+ })
+ }
+
+ private fun onError() {
+ enterPinFragment.enableContinueButton()
+ enterPinFragment.hideLoading()
+ enterPinFragment.hideKeyboard()
+ enterPinFragment.showInvalidOtp()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt
new file mode 100644
index 0000000..14688d0
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt
@@ -0,0 +1,39 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.howitworks
+
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_how_it_works.*
+import kotlinx.android.synthetic.main.fragment_how_it_works.view.*
+
+class HowItWorksFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_how_it_works, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ view.how_it_works_content.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.how_it_works_button) {
+ navigateTo(R.id.action_howItWorksFragment_to_dataPrivacy)
+ }
+
+ override fun updateButtonState() {
+ enableContinueButton()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt
new file mode 100644
index 0000000..26bc4f6
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt
@@ -0,0 +1,32 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.introduction
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_intro.*
+
+class IntroductionFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = null
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_intro, container, false)
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.intro_button) {
+ navigateTo(R.id.action_introFragment_to_howItWorksFragment)
+ }
+
+ override fun updateButtonState() {
+ enableContinueButton()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt
new file mode 100644
index 0000000..7c58d15
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt
@@ -0,0 +1,121 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.permission
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.PowerManager
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import au.gov.health.covidsafe.HomeActivity
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.TracerApp
+import au.gov.health.covidsafe.extensions.*
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_permission.*
+import pub.devrel.easypermissions.EasyPermissions
+
+class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallbacks {
+
+ companion object {
+
+ val requiredPermissions = arrayOf(
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADMIN,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ )
+ }
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = 5
+
+ private var navigationStarted = false
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_permission, container, false)
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == REQUEST_ENABLE_BT) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ excludeFromBatteryOptimization { navigateToNextPage() }
+ return
+ } else {
+ requestAllPermissions { navigateToNextPage() }
+ }
+ } else if (requestCode == BATTERY_OPTIMISER) {
+ Handler().postDelayed({
+ navigateToNextPage()
+ }, 1000)
+ } else super.onActivityResult(requestCode, resultCode, data)
+ }
+
+ private fun navigateToNextPage() {
+ navigationStarted = false
+ if (hasAllPermissionsAndBluetoothOn()) {
+ navigateTo(R.id.action_permissionFragment_to_permissionSuccessFragment)
+ } else {
+ navigateToMainActivity()
+ }
+ }
+
+ private fun hasAllPermissionsAndBluetoothOn(): Boolean {
+ val context = TracerApp.AppContext
+ return isBlueToothEnabled() == true
+ && requiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
+ && ContextCompat.getSystemService(context, PowerManager::class.java)?.isIgnoringBatteryOptimizations(context.packageName) ?: true
+ }
+
+ private fun navigateToMainActivity() {
+ val intent = Intent(context, HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ activity?.startActivity(intent)
+ activity?.finish()
+ }
+
+ override fun onPermissionsDenied(requestCode: Int, perms: MutableList) {
+ if (requestCode == LOCATION) {
+ excludeFromBatteryOptimization { navigateToNextPage() }
+ } else {
+ requestAllPermissions { navigateToNextPage() }
+ }
+ }
+
+ override fun onPermissionsGranted(requestCode: Int, perms: MutableList) {
+ requestAllPermissions { navigateToNextPage() }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.permission_button) {
+ disableContinueButton()
+ navigationStarted = true
+ activity?.let {
+ Preference.putIsOnBoarded(it, true)
+ }
+ requestAllPermissions {
+ navigateToNextPage()
+ }
+ }
+
+ override fun updateButtonState() {
+ if (navigationStarted) {
+ disableContinueButton()
+ } else {
+ enableContinueButton()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permissionsuccess/PermissionSuccessFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permissionsuccess/PermissionSuccessFragment.kt
new file mode 100644
index 0000000..6b49e20
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permissionsuccess/PermissionSuccessFragment.kt
@@ -0,0 +1,41 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.permissionsuccess
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import kotlinx.android.synthetic.main.fragment_permission_success.*
+import au.gov.health.covidsafe.HomeActivity
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+
+class PermissionSuccessFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = 5
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_permission_success, container, false)
+
+ private fun navigateToNextPage() {
+ val intent = Intent(context, HomeActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ activity?.startActivity(intent)
+ activity?.finish()
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.permission_success_button) {
+ navigateToNextPage()
+ }
+
+ override fun updateButtonState() {
+ enableContinueButton()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt
new file mode 100644
index 0000000..362ce14
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt
@@ -0,0 +1,182 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.personal
+
+import android.app.Activity
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.NumberPicker
+import androidx.appcompat.app.AlertDialog
+import androidx.core.os.bundleOf
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment
+import kotlinx.android.synthetic.main.fragment_personal_details.*
+
+class PersonalDetailsFragment : PagerChildFragment() {
+
+ private var picker: NumberPicker? = null
+
+ private var alertDialog: AlertDialog? = null
+ override var stepProgress: Int? = 1
+ override val navigationIcon: Int = R.drawable.ic_up
+ private var ageSelected: Pair? = null
+
+ private val presenter = PersonalDetailsPresenter(this)
+
+ private val nameTextWatcher: TextWatcher = object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {
+ hideNameError()
+ updateButtonState()
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ }
+ }
+
+ private val postCodeTextWatcher: TextWatcher = object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {
+ presenter.validateInlinePostCode(s.toString())
+ updateButtonState()
+ }
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_personal_details, container, false)
+
+ override fun onResume() {
+ super.onResume()
+ personal_details_name.addTextChangedListener(nameTextWatcher)
+ personal_details_post_code.addTextChangedListener(postCodeTextWatcher)
+ personal_details_age.setOnClickListener {
+ showAgePicker()
+ }
+ personal_details_age.text = ageSelected?.second
+
+ }
+
+ override fun onPause() {
+ super.onPause()
+ personal_details_name.removeTextChangedListener(nameTextWatcher)
+ personal_details_post_code.removeTextChangedListener(postCodeTextWatcher)
+ personal_details_age.setOnClickListener(null)
+ alertDialog?.dismiss()
+ }
+
+ override fun getUploadButtonLayout(): UploadButtonLayout = UploadButtonLayout.ContinueLayout(R.string.personal_details_button) {
+ presenter.saveInfos(personal_details_name.text.toString(), personal_details_post_code.text.toString(), getMidAgeToSend())
+ }
+
+ override fun updateButtonState() {
+ if (presenter.validateInputsForButtonUpdate(personal_details_name.text.toString(), personal_details_post_code.text.toString(), getMidAgeToSend())) {
+ enableContinueButton()
+ } else {
+ disableContinueButton()
+ }
+ }
+
+ fun showGenericError() {
+ activity?.let { activity ->
+ alertDialog?.dismiss()
+ alertDialog = AlertDialog.Builder(activity)
+ .setMessage(R.string.generic_error)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.yes, null).show()
+ }
+ }
+
+ fun navigateToNextPage(minor: Boolean) {
+ if (minor) {
+ navigateTo(PersonalDetailsFragmentDirections.actionPersonalDetailsToUnderSixteenFragment().actionId)
+ } else {
+ val bundle = bundleOf(
+ EnterNumberFragment.ENTER_NUMBER_DESTINATION_ID to R.id.action_otpFragment_to_permissionFragment,
+ EnterNumberFragment.ENTER_NUMBER_PROGRESS to 2)
+ navigateTo(PersonalDetailsFragmentDirections.actionPersonalDetailsToEnterNumberFragment().actionId, bundle)
+ }
+ }
+
+ fun showPostcodeError() {
+ personal_details_post_code_error.visibility = VISIBLE
+ }
+
+ fun hidePostcodeError() {
+ personal_details_post_code_error.visibility = GONE
+ }
+
+ fun showNameError() {
+ personal_details_name_error.visibility = VISIBLE
+ }
+
+ fun hideNameError() {
+ personal_details_name_error.visibility = GONE
+ }
+
+ fun showAgeError() {
+ personal_details_age_error.visibility = VISIBLE
+ }
+
+ fun hideAgeError() {
+ personal_details_age_error.visibility = GONE
+ }
+
+ private fun showAgePicker() {
+ activity?.let { activity ->
+ val ages = resources.getStringArray(R.array.personal_details_age_array).map {
+ it.split(":").let { it[0] to it[1] }
+ }
+ var selected = ages.firstOrNull { it == ageSelected }?.let {
+ ages.indexOf(it)
+ } ?: 0
+
+ picker = NumberPicker(activity)
+ picker?.minValue = 0
+ picker?.maxValue = ages.size - 1
+ picker?.displayedValues = ages.map { it.second }.toTypedArray()
+ picker?.setOnValueChangedListener { _, _, newVal ->
+ selected = newVal
+ }
+ picker?.value = selected
+ alertDialog?.dismiss()
+ alertDialog = AlertDialog.Builder(activity)
+ .setTitle(R.string.personal_details_age_dialog_title)
+ .setView(picker)
+ .setPositiveButton(R.string.personal_details_dialog_ok) { _, _ ->
+ ageSelected = ages[selected]
+ personal_details_age.text = ages[selected].second
+ hideAgeError()
+ updateButtonState()
+ }
+ .setNegativeButton(android.R.string.no, null)
+ .show()
+ }
+ }
+
+ private fun getMidAgeToSend(): String? {
+ val ages = resources.getStringArray(R.array.personal_details_age_array).map {
+ it.split(":").let { it[0] to it[1] }
+ }
+ val selected = ages.firstOrNull { it == ageSelected }?.let {
+ ages.indexOf(it)
+ }
+ return selected?.let {
+ ages[selected].first
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt
new file mode 100644
index 0000000..7c60df8
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt
@@ -0,0 +1,96 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.personal
+
+import au.gov.health.covidsafe.Preference
+import java.util.regex.Pattern
+
+class PersonalDetailsPresenter(private val personalDetailsFragment: PersonalDetailsFragment) {
+
+ private val TAG = this.javaClass.simpleName
+
+ private val POST_CODE_REGEX = Pattern.compile("^(?:(?:[2-8]\\d|9[0-7]|0?[28]|0?9(?=09))(?:\\d{2}))$")
+
+ fun saveInfos(name: String?, postCode: String?, age: String?) {
+ personalDetailsFragment.showLoading()
+ personalDetailsFragment.context?.let { context ->
+ val ageInt = age?.toIntOrNull()
+ val nameValid = name.isNullOrBlank().not()
+ val postCodeValid = postCode.isNullOrBlank().not() && isPostCodeValid(postCode)
+ val ageValid = age.isNullOrBlank().not()
+
+ if (nameValid && postCodeValid && ageValid) {
+ val valid = (name?.let { name ->
+ Preference.putName(context, name)
+ } ?: false) &&
+ (age?.let { age ->
+ Preference.putAge(context, age)
+ } ?: false) &&
+ (postCode?.let { postCode ->
+ Preference.putPostCode(context, postCode)
+ } ?: false)
+
+ if (valid) {
+ personalDetailsFragment.hideLoading()
+ personalDetailsFragment.navigateToNextPage(ageInt?.let { it < 16 } ?: false)
+ } else {
+ personalDetailsFragment.hideLoading()
+ personalDetailsFragment.showGenericError()
+ }
+ } else {
+ showFieldsError(name, postCode, age)
+ personalDetailsFragment.hideLoading()
+ }
+ } ?: run {
+ personalDetailsFragment.hideLoading()
+ personalDetailsFragment.showGenericError()
+ }
+ }
+
+ private fun showFieldsError(name: String?, postCode: String?, age: String?) {
+ updateNameFieldError(name)
+ updateAgeFieldError(age)
+ updatePostcodeFieldError(postCode)
+ }
+
+ private fun updateAgeFieldError(age: String?) {
+ return if (age.isNullOrBlank()) {
+ personalDetailsFragment.showAgeError()
+ } else {
+ personalDetailsFragment.hideAgeError()
+ }
+ }
+
+ private fun updateNameFieldError(name: String?) {
+ return if (name.isNullOrBlank()) {
+ personalDetailsFragment.showNameError()
+ } else {
+ personalDetailsFragment.hideNameError()
+ }
+ }
+
+ private fun updatePostcodeFieldError(postCode: String?) {
+ return if (postCode.isNullOrBlank()) {
+ personalDetailsFragment.showPostcodeError()
+ } else {
+ personalDetailsFragment.hidePostcodeError()
+ }
+ }
+
+ fun validateInputsForButtonUpdate(name: String?, postCode: String?, age: String?): Boolean {
+ val nameValid = name.isNullOrBlank().not()
+ val postCodeValid = postCode.isNullOrBlank().not() && isPostCodeValid(postCode)
+ val ageValid = age.isNullOrBlank().not()
+
+ return nameValid && postCodeValid && ageValid
+ }
+
+ internal fun validateInlinePostCode(postCode: String?) {
+ if (!postCode.isNullOrEmpty() && postCode.length == 4 && !isPostCodeValid(postCode)) {
+ personalDetailsFragment.showPostcodeError()
+ } else {
+ personalDetailsFragment.hidePostcodeError()
+ }
+ }
+
+ private fun isPostCodeValid(postCode: String?) = POST_CODE_REGEX.matcher(postCode.toString()).matches()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationcontent/RegistrationContentFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationcontent/RegistrationContentFragment.kt
new file mode 100644
index 0000000..11654d3
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationcontent/RegistrationContentFragment.kt
@@ -0,0 +1,44 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.registrationcontent
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.PagerContainer
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_registration_consent.*
+
+class RegistrationContentFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_registration_consent, container, false)
+
+ override fun onResume() {
+ super.onResume()
+ registration_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
+ updateButtonState()
+ }
+ }
+
+ override fun updateButtonState() {
+ if (registration_consent_checkbox.isChecked) {
+ (activity as? PagerContainer)?.enableNextButton()
+ } else {
+ (activity as? PagerContainer)?.disableNextButton()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.registration_consent_button) {
+ navigateTo(RegistrationContentFragmentDirections.actionRegistrationConsentFragmentToPersonalDetailsFragment().actionId)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt
new file mode 100644
index 0000000..f163647
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt
@@ -0,0 +1,51 @@
+package au.gov.health.covidsafe.ui.onboarding.fragment.undersixteen
+
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment
+import kotlinx.android.synthetic.main.fragment_under_sixteen.*
+
+class UnderSixteenFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+
+ override var stepProgress: Int? = null
+
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
+ : View? = inflater.inflate(R.layout.fragment_under_sixteen, container, false)
+
+ override fun onResume() {
+ super.onResume()
+ under_sixteen_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
+ updateButtonState()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ under_sixteen_checkbox.setOnCheckedChangeListener(null)
+ }
+
+ override fun getUploadButtonLayout(): UploadButtonLayout = UploadButtonLayout.ContinueLayout(R.string.under_sixteen_button) {
+ val bundle = bundleOf(
+ EnterNumberFragment.ENTER_NUMBER_DESTINATION_ID to R.id.action_otpFragment_to_permissionFragment,
+ EnterNumberFragment.ENTER_NUMBER_PROGRESS to 2)
+ navigateTo(UnderSixteenFragmentDirections.actionUnderSixteenFragmentToEnterNumberFragment().actionId, bundle)
+ }
+
+ override fun updateButtonState() {
+ if (under_sixteen_checkbox.isChecked) {
+ enableContinueButton()
+ } else {
+ disableContinueButton()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt
new file mode 100644
index 0000000..489b42f
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt
@@ -0,0 +1,107 @@
+package au.gov.health.covidsafe.ui.upload
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.*
+import android.view.ViewGroup
+import androidx.annotation.StringRes
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerContainer
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import com.github.razir.progressbutton.hideProgress
+import com.github.razir.progressbutton.showProgress
+import kotlinx.android.synthetic.main.fragment_upload_master.*
+
+class UploadContainerFragment : Fragment(), PagerContainer {
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_upload_master, container, false)
+
+ override fun onResume() {
+ super.onResume()
+ toolbar.setNavigationOnClickListener {
+ activity?.onBackPressed()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ toolbar.setNavigationOnClickListener(null)
+ }
+
+ override fun updateProgressBar(stepProgress: Int?) {
+ if (stepProgress == null) {
+ upload_progress.visibility = INVISIBLE
+ } else {
+ upload_progress.visibility = VISIBLE
+ upload_progress.progress = stepProgress
+ }
+ }
+
+ override fun setNavigationIcon(navigationIcon: Int?) {
+ if (navigationIcon == null) {
+ toolbar.navigationIcon = null
+ } else {
+ activity?.let {
+ toolbar.navigationIcon = ContextCompat.getDrawable(it, navigationIcon)
+ }
+ }
+ }
+
+ override fun refreshButton(uploadButtonLayout: UploadButtonLayout) {
+ when (uploadButtonLayout) {
+ is UploadButtonLayout.ContinueLayout -> {
+ upload_continue.setOnClickListener {
+ uploadButtonLayout.buttonListener?.invoke()
+ }
+ upload_continue.setText(uploadButtonLayout.buttonText)
+ upload_continue.visibility = VISIBLE
+ upload_answerNo.setOnClickListener(null)
+ upload_answerYes.setOnClickListener(null)
+ upload_answerNo.visibility = GONE
+ upload_answerYes.visibility = GONE
+ }
+ is UploadButtonLayout.QuestionLayout -> {
+ upload_continue.setOnClickListener(null)
+ upload_continue.visibility = GONE
+ upload_answerNo.setOnClickListener {
+ uploadButtonLayout.buttonNoListener.invoke()
+ }
+ upload_answerYes.setOnClickListener {
+ uploadButtonLayout.buttonYesListener.invoke()
+ }
+ upload_answerNo.visibility = VISIBLE
+ upload_answerYes.visibility = VISIBLE
+ }
+ }
+ }
+
+ override fun enableNextButton() {
+ upload_continue.isEnabled = true
+ }
+
+ override fun disableNextButton() {
+ upload_continue.isEnabled = false
+ }
+
+ override fun showLoading() {
+ upload_continue.showProgress {
+ progressColorRes = R.color.slack_black_2
+ }
+ }
+
+ override fun hideLoading(@StringRes stringRes: Int?) {
+ if (stringRes == null) {
+ upload_continue.hideProgress()
+ } else {
+ upload_continue.hideProgress(newTextRes = stringRes)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/model/DebugData.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/model/DebugData.kt
new file mode 100644
index 0000000..39e97e8
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/model/DebugData.kt
@@ -0,0 +1,5 @@
+package au.gov.health.covidsafe.ui.upload.model
+
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+
+class DebugData constructor(var records: List)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/model/ExportData.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/model/ExportData.kt
new file mode 100644
index 0000000..946deaf
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/model/ExportData.kt
@@ -0,0 +1,6 @@
+package au.gov.health.covidsafe.ui.upload.model
+
+import androidx.annotation.Keep
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
+@Keep
+class ExportData constructor(var records: List)
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadFinishedFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadFinishedFragment.kt
new file mode 100644
index 0000000..4f590d4
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadFinishedFragment.kt
@@ -0,0 +1,32 @@
+package au.gov.health.covidsafe.ui.upload.presentation
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_upload_finished.*
+
+class UploadFinishedFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = null
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
+ inflater.inflate(R.layout.fragment_upload_finished, container, false)
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.action_upload_done) {
+ activity?.onBackPressed()
+ }
+
+ override fun updateButtonState() {
+ enableContinueButton()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadInitialFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadInitialFragment.kt
new file mode 100644
index 0000000..0f1a0f6
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadInitialFragment.kt
@@ -0,0 +1,39 @@
+package au.gov.health.covidsafe.ui.upload.presentation
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_upload_page_4.*
+
+class UploadInitialFragment : PagerChildFragment() {
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
+ inflater.inflate(R.layout.fragment_upload_initial, container, false)
+
+
+ override fun updateButtonState() {
+ enableContinueButton()
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.QuestionLayout(
+ buttonYesListener = {
+ navigateTo(R.id.action_uploadInitial_to_uploadStepFourFragment)
+ },
+ buttonNoListener = {
+ activity?.onBackPressed()
+ })
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadStepFourFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadStepFourFragment.kt
new file mode 100644
index 0000000..4aae617
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadStepFourFragment.kt
@@ -0,0 +1,59 @@
+package au.gov.health.covidsafe.ui.upload.presentation
+
+import android.app.AlertDialog
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import kotlinx.android.synthetic.main.fragment_upload_page_4.*
+
+class UploadStepFourFragment : PagerChildFragment() {
+
+ private var alertDialog: AlertDialog? = null
+ override var stepProgress: Int? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
+ inflater.inflate(R.layout.fragment_upload_page_4, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ subHeader.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+
+ override fun onResume() {
+ super.onResume()
+ upload_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
+ updateButtonState()
+ }
+ }
+ override fun updateButtonState() {
+ if (upload_consent_checkbox.isChecked) {
+ enableContinueButton()
+ } else {
+ disableContinueButton()
+ }
+ }
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(
+ R.string.action_agree) {
+ navigateToVerifyUploadPin()
+ }
+
+ private fun navigateToVerifyUploadPin() {
+ navigateTo(R.id.action_uploadStepFourFragment_to_verifyUploadPinFragment)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ alertDialog?.dismiss()
+ root.removeAllViews()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinFragment.kt
new file mode 100644
index 0000000..25c4301
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinFragment.kt
@@ -0,0 +1,136 @@
+package au.gov.health.covidsafe.ui.upload.presentation
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.PagerChildFragment
+import au.gov.health.covidsafe.ui.UploadButtonLayout
+import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment
+import au.gov.health.covidsafe.ui.view.UploadingDialog
+import au.gov.health.covidsafe.ui.view.UploadingErrorDialog
+import com.atlassian.mobilekit.module.core.utils.SystemUtils
+import kotlinx.android.synthetic.main.fragment_verify_upload_pin.*
+import kotlinx.android.synthetic.main.fragment_verify_upload_pin.view.*
+
+
+class VerifyUploadPinFragment : PagerChildFragment() {
+
+ interface OnUploadErrorInterface {
+ fun onPositiveClicked()
+ fun onNegativeClicked()
+ }
+
+ private var dialog: Dialog? = null
+
+ private lateinit var presenter : VerifyUploadPinPresenter
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
+ inflater.inflate(R.layout.fragment_verify_upload_pin, container, false)
+
+ override val navigationIcon: Int? = R.drawable.ic_up
+ override var stepProgress: Int? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ presenter = VerifyUploadPinPresenter(this)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ pin.onPinChanged = {
+ updateButtonState()
+ hideInvalidOtp()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ pin.onPinChanged = null
+ }
+
+ override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.action_verify_upload_pin) {
+ presenter.uploadData(requireView().pin.value)
+ }
+
+ override fun updateButtonState() {
+ if (isIncorrectPinFormat()) {
+ disableContinueButton()
+ } else {
+ enableContinueButton()
+ }
+ }
+
+ private fun isIncorrectPinFormat(): Boolean {
+ return requireView().pin.isIncomplete
+ }
+
+ fun hideKeyboard() {
+ activity?.currentFocus?.let { view ->
+ SystemUtils.hideSoftKeyboard(view)
+ }
+ }
+
+ fun showInvalidOtp() {
+ dialog?.dismiss()
+ enter_pin_error_label.visibility = View.VISIBLE
+ }
+
+ private fun hideInvalidOtp() {
+ enter_pin_error_label.visibility = View.GONE
+ }
+
+ fun showGenericError() {
+ dialog?.dismiss()
+ activity?.let {
+ dialog = UploadingErrorDialog(it, object : OnUploadErrorInterface {
+ override fun onPositiveClicked() {
+ presenter.uploadData(requireView().pin.value)
+ }
+
+ override fun onNegativeClicked() {
+ dialog?.dismiss()
+ }
+ })
+ dialog?.show()
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ dialog?.dismiss()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ root.removeAllViews()
+ }
+
+ fun navigateToRegister() {
+ val bundle = bundleOf(
+ EnterNumberFragment.ENTER_NUMBER_DESTINATION_ID to R.id.action_enterPinFragment_to_uploadStepFourFragment)
+ navigateTo(VerifyUploadPinFragmentDirections.actionVerifyUploadPinFragmentToEnterNumberFragment().actionId, bundle)
+ }
+
+ fun navigateToNextPage() {
+ navigateTo(R.id.action_verifyUploadPinFragment_to_uploadFinishedFragment)
+ }
+
+ fun showDialogLoading() {
+ dialog?.dismiss()
+ dialog = UploadingDialog(requireActivity())
+ dialog?.show()
+ }
+
+ fun showCheckInternetError() {
+ dialog?.dismiss()
+ dialog = AlertDialog.Builder(activity)
+ .setMessage(R.string.generic_internet_error)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.yes, null).show()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt
new file mode 100644
index 0000000..5a7a0e5
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt
@@ -0,0 +1,74 @@
+package au.gov.health.covidsafe.ui.upload.presentation
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import au.gov.health.covidsafe.BuildConfig
+import au.gov.health.covidsafe.Preference
+import au.gov.health.covidsafe.extensions.isInternetAvailable
+import au.gov.health.covidsafe.factory.NetworkFactory
+import au.gov.health.covidsafe.interactor.usecase.UploadData
+import au.gov.health.covidsafe.interactor.usecase.UploadDataException
+import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+
+class VerifyUploadPinPresenter(private val fragment: VerifyUploadPinFragment) : LifecycleObserver {
+
+ private val TAG = this.javaClass.simpleName
+
+ private var awsClient = NetworkFactory.awsClient
+ private lateinit var uploadData: UploadData
+
+ private lateinit var recordStorage: StreetPassRecordStorage
+
+ init {
+ fragment.lifecycle.addObserver(this)
+ fragment.context?.let { context ->
+ recordStorage = StreetPassRecordStorage(context)
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ private fun onCreate() {
+ uploadData = UploadData(awsClient, NetworkFactory.okHttpClient, fragment.context, fragment.lifecycle)
+ }
+
+ internal fun uploadData(otp: String) {
+ if (fragment.activity?.isInternetAvailable() == false) {
+ fragment.showCheckInternetError()
+ } else {
+ fragment.disableContinueButton()
+ fragment.showDialogLoading()
+ uploadData.invoke(otp,
+ onSuccess = {
+ if (!BuildConfig.DEBUG) {
+ GlobalScope.launch { recordStorage.nukeDbAsync() }
+ }
+ fragment.context?.let { context ->
+ Preference.setDataIsUploaded(context, true)
+ }
+ fragment.navigateToNextPage()
+ },
+ onFailure = {
+ when (it) {
+ is UploadDataException.UploadDataIncorrectPinException -> {
+ fragment.showInvalidOtp()
+ }
+ is UploadDataException.UploadDataJwtExpiredException -> {
+ fragment.navigateToRegister()
+ }
+ else -> {
+ fragment.showGenericError()
+ }
+ }
+ fragment.enableContinueButton()
+ fragment.hideKeyboard()
+ fragment.hideLoading()
+ }
+ )
+ }
+ }
+}
+
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/view/PinInputView.kt b/app/src/main/java/au/gov/health/covidsafe/ui/view/PinInputView.kt
new file mode 100644
index 0000000..59f9435
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/view/PinInputView.kt
@@ -0,0 +1,76 @@
+package au.gov.health.covidsafe.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.widget.EditText
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.widget.doAfterTextChanged
+import kotlinx.android.synthetic.main.view_pin.view.*
+import au.gov.health.covidsafe.R
+
+class PinInputView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = -1) : ConstraintLayout(context, attrs, defStyle) {
+
+ private val pinOne: EditText? by lazy { pin_1 }
+ private val pinTwo: EditText? by lazy { pin_2 }
+ private val pinThree: EditText? by lazy { pin_3 }
+ private val pinFour: EditText? by lazy { pin_4 }
+ private val pinFive: EditText? by lazy { pin_5 }
+ private val pinSix: EditText? by lazy { pin_6 }
+ var onPinChanged: (() -> Unit)? = null
+
+ private val allInputs by lazy {
+ listOf(pinOne, pinTwo, pinThree, pinFour, pinFive, pinSix)
+ }
+
+ val value: String
+ get() = allInputs.mapNotNull { it?.text }.joinToString("")
+
+ val isIncomplete: Boolean
+ get() = allInputs.any { it?.text.isNullOrEmpty() }
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.view_pin, this, true)
+ pinOne?.onDigitChanged(pinTwo)
+ pinOne?.onDeletePressed(null)
+
+ pinTwo?.onDigitChanged(pinThree)
+ pinTwo?.onDeletePressed(pinOne)
+
+ pinThree?.onDigitChanged(pinFour)
+ pinThree?.onDeletePressed(pinTwo)
+
+ pinFour?.onDigitChanged(pinFive)
+ pinFour?.onDeletePressed(pinThree)
+
+ pinFive?.onDigitChanged(pinSix)
+ pinFive?.onDeletePressed(pinFour)
+
+ pinSix?.onDigitChanged(null)
+ pinSix?.onDeletePressed(pinFive)
+ }
+
+ private fun EditText.onDigitChanged(next: EditText? = null) {
+ doAfterTextChanged {
+ if (it?.length == 1) {
+ next?.requestFocus()
+ onPinChanged?.invoke()
+ } else if (it.isNullOrBlank()) {
+ onPinChanged?.invoke()
+ }
+ }
+ }
+
+ private fun EditText.onDeletePressed(prev: EditText? = null) {
+ setOnKeyListener { view, keyCode, keyEvent ->
+ if (keyCode == KeyEvent.KEYCODE_DEL && text.isNullOrEmpty()) {
+ prev?.requestFocus()
+ onPinChanged?.invoke()
+ true
+ } else {
+ false
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/view/SegmentedProgressBar.kt b/app/src/main/java/au/gov/health/covidsafe/ui/view/SegmentedProgressBar.kt
new file mode 100644
index 0000000..97a2cf6
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/view/SegmentedProgressBar.kt
@@ -0,0 +1,53 @@
+package au.gov.health.covidsafe.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
+import au.gov.health.covidsafe.R
+
+class SegmentedProgressBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = -1) : LinearLayout(context, attrs, defStyle) {
+ private val maxValue: Int
+ var progress: Int = DEFAULT_PROGRESS
+ set(value) {
+ field = value
+ for (i in 0..childCount) {
+ val segment = getChildAt(i)
+ segment?.isSelected = i + 1 <= progress
+ }
+ }
+
+ private val segmentSpacing: Int
+
+ init {
+ orientation = HORIZONTAL
+ val values = context.obtainStyledAttributes(attrs, R.styleable.SegmentedProgressBar, defStyle, 0)
+ maxValue = values.getInt(R.styleable.SegmentedProgressBar_progress_max_value, DEFAULT_MAX_VALUE)
+ segmentSpacing = values.getDimensionPixelSize(R.styleable.SegmentedProgressBar_segment_spacing,
+ DEFAULT_SEGMENT_SPACING_DP * resources.displayMetrics.density.toInt())
+ progress = values.getInt(R.styleable.SegmentedProgressBar_progress_value, DEFAULT_PROGRESS)
+ drawProgress()
+ values.recycle()
+ }
+
+ private fun drawProgress() {
+ repeat(maxValue) { index ->
+ val lp = generateDefaultLayoutParams()
+ lp.height = LayoutParams.WRAP_CONTENT
+ lp.width = 0
+ lp.weight = 1.0f
+ lp.rightMargin = if (index in 1 until maxValue - 1) segmentSpacing else 0
+ lp.leftMargin = if (index > 0) segmentSpacing else 0
+
+ val view = View(context)
+ view.background = ContextCompat.getDrawable(context, R.drawable.progress_segment)
+ view.isSelected = index + 1 <= progress
+ addView(view, lp)
+ }
+ }
+}
+
+private const val DEFAULT_MAX_VALUE = 5
+private const val DEFAULT_PROGRESS = 0
+private const val DEFAULT_SEGMENT_SPACING_DP = 4
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/view/UlView.kt b/app/src/main/java/au/gov/health/covidsafe/ui/view/UlView.kt
new file mode 100644
index 0000000..648a737
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/view/UlView.kt
@@ -0,0 +1,30 @@
+package au.gov.health.covidsafe.ui.view
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import au.gov.health.covidsafe.R
+import kotlinx.android.synthetic.main.view_ul.view.*
+
+class UlView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.view_ul, this, true)
+
+ val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.UlView)
+ val title = a.getString(R.styleable.UlView_ul_view_text)
+
+ ul_content.text = title
+
+ layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+ a.recycle()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingDialog.kt b/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingDialog.kt
new file mode 100644
index 0000000..c524c47
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingDialog.kt
@@ -0,0 +1,24 @@
+package au.gov.health.covidsafe.ui.view
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Window
+import android.view.WindowManager
+import au.gov.health.covidsafe.R
+
+class UploadingDialog(context: Context) : Dialog(context) {
+
+ init {
+ setCancelable(false)
+ setCanceledOnTouchOutside(false)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ setContentView(R.layout.dialog_uploading)
+ val window: Window? = this.window
+ window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt b/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt
new file mode 100644
index 0000000..38f6b84
--- /dev/null
+++ b/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt
@@ -0,0 +1,32 @@
+package au.gov.health.covidsafe.ui.view
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Window
+import android.view.WindowManager
+import au.gov.health.covidsafe.R
+import au.gov.health.covidsafe.ui.upload.presentation.VerifyUploadPinFragment
+import kotlinx.android.synthetic.main.dialog_error_uploading.*
+
+class UploadingErrorDialog(context: Context, private val listener: VerifyUploadPinFragment.OnUploadErrorInterface) : Dialog(context) {
+
+ init {
+ setCancelable(false)
+ setCanceledOnTouchOutside(false)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ setContentView(R.layout.dialog_error_uploading)
+ val window: Window? = this.window
+ window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
+ dialog_error_positive.setOnClickListener {
+ listener.onPositiveClicked()
+ }
+ dialog_error_negative.setOnClickListener {
+ listener.onNegativeClicked()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml
new file mode 100644
index 0000000..b5eb2cd
--- /dev/null
+++ b/app/src/main/res/anim/slide_in_left.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml
new file mode 100644
index 0000000..95c1e58
--- /dev/null
+++ b/app/src/main/res/anim/slide_in_right.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 0000000..c764ede
--- /dev/null
+++ b/app/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml
new file mode 100644
index 0000000..20db00c
--- /dev/null
+++ b/app/src/main/res/anim/slide_out_right.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/default_button_text_color.xml b/app/src/main/res/color/default_button_text_color.xml
new file mode 100644
index 0000000..2e8bf89
--- /dev/null
+++ b/app/src/main/res/color/default_button_text_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/progress_segment.xml b/app/src/main/res/color/progress_segment.xml
new file mode 100644
index 0000000..f780887
--- /dev/null
+++ b/app/src/main/res/color/progress_segment.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v24/ic_notification_setting.xml b/app/src/main/res/drawable-anydpi-v24/ic_notification_setting.xml
new file mode 100644
index 0000000..3914c01
--- /dev/null
+++ b/app/src/main/res/drawable-anydpi-v24/ic_notification_setting.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable-hdpi/australian_government_stacked_white.png b/app/src/main/res/drawable-hdpi/australian_government_stacked_white.png
new file mode 100644
index 0000000..281714e
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/australian_government_stacked_white.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_home_news.png b/app/src/main/res/drawable-hdpi/ic_home_news.png
new file mode 100644
index 0000000..feb44a8
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_home_news.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_notification_icon.png b/app/src/main/res/drawable-hdpi/ic_notification_icon.png
new file mode 100755
index 0000000..31bb774
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_icon.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_notification_setting.png b/app/src/main/res/drawable-hdpi/ic_notification_setting.png
new file mode 100644
index 0000000..698a4f4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_setting.png differ
diff --git a/app/src/main/res/drawable-hdpi/ic_notification_warning.png b/app/src/main/res/drawable-hdpi/ic_notification_warning.png
new file mode 100755
index 0000000..deed9a0
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_warning.png differ
diff --git a/app/src/main/res/drawable-hdpi/small_green_tick.png b/app/src/main/res/drawable-hdpi/small_green_tick.png
new file mode 100644
index 0000000..a0121f8
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/small_green_tick.png differ
diff --git a/app/src/main/res/drawable-hdpi/small_red_cross.png b/app/src/main/res/drawable-hdpi/small_red_cross.png
new file mode 100644
index 0000000..71bcefd
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/small_red_cross.png differ
diff --git a/app/src/main/res/drawable-mdpi/australian_government_stacked_white.png b/app/src/main/res/drawable-mdpi/australian_government_stacked_white.png
new file mode 100644
index 0000000..8372b2c
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/australian_government_stacked_white.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_home_news.png b/app/src/main/res/drawable-mdpi/ic_home_news.png
new file mode 100644
index 0000000..4431cc6
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_home_news.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_notification_icon.png b/app/src/main/res/drawable-mdpi/ic_notification_icon.png
new file mode 100755
index 0000000..91c38ca
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_icon.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_notification_setting.png b/app/src/main/res/drawable-mdpi/ic_notification_setting.png
new file mode 100644
index 0000000..280259b
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_setting.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_notification_warning.png b/app/src/main/res/drawable-mdpi/ic_notification_warning.png
new file mode 100755
index 0000000..c3993f1
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_warning.png differ
diff --git a/app/src/main/res/drawable-mdpi/small_green_tick.png b/app/src/main/res/drawable-mdpi/small_green_tick.png
new file mode 100644
index 0000000..7ebb055
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/small_green_tick.png differ
diff --git a/app/src/main/res/drawable-mdpi/small_red_cross.png b/app/src/main/res/drawable-mdpi/small_red_cross.png
new file mode 100644
index 0000000..065929a
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/small_red_cross.png differ
diff --git a/app/src/main/res/drawable-xhdpi/australian_government_stacked_white.png b/app/src/main/res/drawable-xhdpi/australian_government_stacked_white.png
new file mode 100644
index 0000000..2390d68
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/australian_government_stacked_white.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_home_news.png b/app/src/main/res/drawable-xhdpi/ic_home_news.png
new file mode 100644
index 0000000..a3fb468
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_home_news.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_icon.png b/app/src/main/res/drawable-xhdpi/ic_notification_icon.png
new file mode 100755
index 0000000..484ec1c
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_icon.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_setting.png b/app/src/main/res/drawable-xhdpi/ic_notification_setting.png
new file mode 100644
index 0000000..99b6892
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_setting.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_warning.png b/app/src/main/res/drawable-xhdpi/ic_notification_warning.png
new file mode 100755
index 0000000..a063c58
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_warning.png differ
diff --git a/app/src/main/res/drawable-xhdpi/small_green_tick.png b/app/src/main/res/drawable-xhdpi/small_green_tick.png
new file mode 100644
index 0000000..c7d1069
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/small_green_tick.png differ
diff --git a/app/src/main/res/drawable-xhdpi/small_red_cross.png b/app/src/main/res/drawable-xhdpi/small_red_cross.png
new file mode 100644
index 0000000..8a8b000
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/small_red_cross.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/australian_government_stacked_white.png b/app/src/main/res/drawable-xxhdpi/australian_government_stacked_white.png
new file mode 100644
index 0000000..ca519eb
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/australian_government_stacked_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_home_news.png b/app/src/main/res/drawable-xxhdpi/ic_home_news.png
new file mode 100644
index 0000000..318b013
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_home_news.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png b/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png
new file mode 100755
index 0000000..95cc94c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_setting.png b/app/src/main/res/drawable-xxhdpi/ic_notification_setting.png
new file mode 100644
index 0000000..88f9121
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_setting.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_warning.png b/app/src/main/res/drawable-xxhdpi/ic_notification_warning.png
new file mode 100755
index 0000000..c955ea9
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_warning.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/small_green_tick.png b/app/src/main/res/drawable-xxhdpi/small_green_tick.png
new file mode 100644
index 0000000..bd76e1b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/small_green_tick.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/small_red_cross.png b/app/src/main/res/drawable-xxhdpi/small_red_cross.png
new file mode 100644
index 0000000..4db4b69
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/small_red_cross.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png
new file mode 100755
index 0000000..8b28e91
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_warning.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_warning.png
new file mode 100755
index 0000000..d75b38e
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_warning.png differ
diff --git a/app/src/main/res/drawable/background_circular_black.xml b/app/src/main/res/drawable/background_circular_black.xml
new file mode 100644
index 0000000..48b0b70
--- /dev/null
+++ b/app/src/main/res/drawable/background_circular_black.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/background_circular_dark_cerulean_1.xml b/app/src/main/res/drawable/background_circular_dark_cerulean_1.xml
new file mode 100644
index 0000000..a7cf7e9
--- /dev/null
+++ b/app/src/main/res/drawable/background_circular_dark_cerulean_1.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/background_circular_dark_cerulean_4.xml b/app/src/main/res/drawable/background_circular_dark_cerulean_4.xml
new file mode 100644
index 0000000..21848e7
--- /dev/null
+++ b/app/src/main/res/drawable/background_circular_dark_cerulean_4.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/background_circular_green.xml b/app/src/main/res/drawable/background_circular_green.xml
new file mode 100644
index 0000000..c287a3d
--- /dev/null
+++ b/app/src/main/res/drawable/background_circular_green.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/background_light_green.xml b/app/src/main/res/drawable/background_light_green.xml
new file mode 100644
index 0000000..c39b180
--- /dev/null
+++ b/app/src/main/res/drawable/background_light_green.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/default_button_selector.xml b/app/src/main/res/drawable/default_button_selector.xml
new file mode 100644
index 0000000..06ef114
--- /dev/null
+++ b/app/src/main/res/drawable/default_button_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/edit_text_black_background.xml b/app/src/main/res/drawable/edit_text_black_background.xml
new file mode 100644
index 0000000..f814a20
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_black_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/edit_text_green_background.xml b/app/src/main/res/drawable/edit_text_green_background.xml
new file mode 100644
index 0000000..9adef1a
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_green_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/edittext_modified_states.xml b/app/src/main/res/drawable/edittext_modified_states.xml
new file mode 100644
index 0000000..3a677e1
--- /dev/null
+++ b/app/src/main/res/drawable/edittext_modified_states.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/green_button_background.xml b/app/src/main/res/drawable/green_button_background.xml
new file mode 100644
index 0000000..03cd68d
--- /dev/null
+++ b/app/src/main/res/drawable/green_button_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/grey_button_background.xml b/app/src/main/res/drawable/grey_button_background.xml
new file mode 100644
index 0000000..297bba2
--- /dev/null
+++ b/app/src/main/res/drawable/grey_button_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_arrow_drop_down.xml b/app/src/main/res/drawable/ic_arrow_drop_down.xml
new file mode 100644
index 0000000..b571455
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_drop_down.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_forward_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_forward_black_24dp.xml
new file mode 100644
index 0000000..cf9e208
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_forward_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml
new file mode 100644
index 0000000..2505846
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml
new file mode 100644
index 0000000..125885a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chevron_right.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 0000000..55c4d27
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_delete_black_24dp.xml b/app/src/main/res/drawable/ic_delete_black_24dp.xml
new file mode 100644
index 0000000..39e64d6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_doctor_circle.xml b/app/src/main/res/drawable/ic_doctor_circle.xml
new file mode 100644
index 0000000..8b78a18
--- /dev/null
+++ b/app/src/main/res/drawable/ic_doctor_circle.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_ellipse.xml b/app/src/main/res/drawable/ic_ellipse.xml
new file mode 100644
index 0000000..44585f1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ellipse.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_help_outline_black.xml b/app/src/main/res/drawable/ic_help_outline_black.xml
new file mode 100644
index 0000000..ab6f7c8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help_outline_black.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_help_outline_white.xml b/app/src/main/res/drawable/ic_help_outline_white.xml
new file mode 100644
index 0000000..ec5ea13
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help_outline_white.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_help_stop_covid.xml b/app/src/main/res/drawable/ic_help_stop_covid.xml
new file mode 100644
index 0000000..edc824e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help_stop_covid.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_logo.xml b/app/src/main/res/drawable/ic_home_logo.xml
new file mode 100644
index 0000000..1fae9cb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_logo.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_share.xml b/app/src/main/res/drawable/ic_home_share.xml
new file mode 100644
index 0000000..a2dd419
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_share.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_unprotected.xml b/app/src/main/res/drawable/ic_home_unprotected.xml
new file mode 100644
index 0000000..25c0e0a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_unprotected.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_how_it_works.xml b/app/src/main/res/drawable/ic_how_it_works.xml
new file mode 100644
index 0000000..e946d81
--- /dev/null
+++ b/app/src/main/res/drawable/ic_how_it_works.xml
@@ -0,0 +1,341 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_illustration_upload_inital_state.xml b/app/src/main/res/drawable/ic_illustration_upload_inital_state.xml
new file mode 100644
index 0000000..6cf1d0e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_illustration_upload_inital_state.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_illustration_upload_step_4.xml b/app/src/main/res/drawable/ic_illustration_upload_step_4.xml
new file mode 100644
index 0000000..feeca9e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_illustration_upload_step_4.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 0000000..8e1ea6e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_intro_picture.xml b/app/src/main/res/drawable/ic_intro_picture.xml
new file mode 100644
index 0000000..c385fde
--- /dev/null
+++ b/app/src/main/res/drawable/ic_intro_picture.xml
@@ -0,0 +1,535 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..3700fd0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..d552f8c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_round.xml b/app/src/main/res/drawable/ic_launcher_round.xml
new file mode 100644
index 0000000..9aa50ee
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_round.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo_home_inactive.xml b/app/src/main/res/drawable/ic_logo_home_inactive.xml
new file mode 100644
index 0000000..54fc727
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_home_inactive.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_logo_home_uploaded.xml b/app/src/main/res/drawable/ic_logo_home_uploaded.xml
new file mode 100644
index 0000000..59a3c6c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_logo_home_uploaded.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_permission.xml b/app/src/main/res/drawable/ic_permission.xml
new file mode 100644
index 0000000..8a4ee92
--- /dev/null
+++ b/app/src/main/res/drawable/ic_permission.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_permission_success.xml b/app/src/main/res/drawable/ic_permission_success.xml
new file mode 100644
index 0000000..ac083f9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_permission_success.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_personal_details.xml b/app/src/main/res/drawable/ic_personal_details.xml
new file mode 100644
index 0000000..5b28ddf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_personal_details.xml
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_phone_checked_circle.xml b/app/src/main/res/drawable/ic_phone_checked_circle.xml
new file mode 100644
index 0000000..bb41493
--- /dev/null
+++ b/app/src/main/res/drawable/ic_phone_checked_circle.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml
new file mode 100644
index 0000000..bf9b895
--- /dev/null
+++ b/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_privacy.xml b/app/src/main/res/drawable/ic_privacy.xml
new file mode 100644
index 0000000..2def57b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_privacy.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_smartphone.xml b/app/src/main/res/drawable/ic_smartphone.xml
new file mode 100644
index 0000000..51ad6a1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_smartphone.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_smile.xml b/app/src/main/res/drawable/ic_smile.xml
new file mode 100644
index 0000000..f7c1170
--- /dev/null
+++ b/app/src/main/res/drawable/ic_smile.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_splash_screen_logo.xml b/app/src/main/res/drawable/ic_splash_screen_logo.xml
new file mode 100644
index 0000000..ec26a0d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_splash_screen_logo.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_splash_screen_title.xml b/app/src/main/res/drawable/ic_splash_screen_title.xml
new file mode 100644
index 0000000..e1ede1b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_splash_screen_title.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_stop_black_24dp.xml b/app/src/main/res/drawable/ic_stop_black_24dp.xml
new file mode 100644
index 0000000..c428d72
--- /dev/null
+++ b/app/src/main/res/drawable/ic_stop_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_under_sixteen.xml b/app/src/main/res/drawable/ic_under_sixteen.xml
new file mode 100644
index 0000000..695680a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_under_sixteen.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_unfold_less_black_24dp.xml b/app/src/main/res/drawable/ic_unfold_less_black_24dp.xml
new file mode 100644
index 0000000..7282a97
--- /dev/null
+++ b/app/src/main/res/drawable/ic_unfold_less_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_unfold_more_black_24dp.xml b/app/src/main/res/drawable/ic_unfold_more_black_24dp.xml
new file mode 100644
index 0000000..e9ba754
--- /dev/null
+++ b/app/src/main/res/drawable/ic_unfold_more_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_up.xml b/app/src/main/res/drawable/ic_up.xml
new file mode 100644
index 0000000..c1704dc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_up.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_upload_error.xml b/app/src/main/res/drawable/ic_upload_error.xml
new file mode 100644
index 0000000..5d6d3fd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_upload_error.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_upload_failed.xml b/app/src/main/res/drawable/ic_upload_failed.xml
new file mode 100644
index 0000000..8abcc84
--- /dev/null
+++ b/app/src/main/res/drawable/ic_upload_failed.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_upload_icon.xml b/app/src/main/res/drawable/ic_upload_icon.xml
new file mode 100644
index 0000000..2f808d9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_upload_icon.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/icon_checkbox.xml b/app/src/main/res/drawable/icon_checkbox.xml
new file mode 100644
index 0000000..cc6ef6a
--- /dev/null
+++ b/app/src/main/res/drawable/icon_checkbox.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/illustration_data_uploaded.xml b/app/src/main/res/drawable/illustration_data_uploaded.xml
new file mode 100644
index 0000000..f0e0ec5
--- /dev/null
+++ b/app/src/main/res/drawable/illustration_data_uploaded.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/illustration_upload_finished.xml b/app/src/main/res/drawable/illustration_upload_finished.xml
new file mode 100644
index 0000000..4c503f7
--- /dev/null
+++ b/app/src/main/res/drawable/illustration_upload_finished.xml
@@ -0,0 +1,530 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/outline_button_background.xml b/app/src/main/res/drawable/outline_button_background.xml
new file mode 100644
index 0000000..f156edc
--- /dev/null
+++ b/app/src/main/res/drawable/outline_button_background.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/phone_number_invalid_background.xml b/app/src/main/res/drawable/phone_number_invalid_background.xml
new file mode 100644
index 0000000..6a24df1
--- /dev/null
+++ b/app/src/main/res/drawable/phone_number_invalid_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/progress_segment.xml b/app/src/main/res/drawable/progress_segment.xml
new file mode 100644
index 0000000..4b52324
--- /dev/null
+++ b/app/src/main/res/drawable/progress_segment.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/splash_screen_white_wave.xml b/app/src/main/res/drawable/splash_screen_white_wave.xml
new file mode 100644
index 0000000..0b8abe5
--- /dev/null
+++ b/app/src/main/res/drawable/splash_screen_white_wave.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/font/font_roboto_regular.xml b/app/src/main/res/font/font_roboto_regular.xml
new file mode 100644
index 0000000..7ce3a40
--- /dev/null
+++ b/app/src/main/res/font/font_roboto_regular.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf
new file mode 100644
index 0000000..f714a51
Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ
diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml
new file mode 100644
index 0000000..215d022
--- /dev/null
+++ b/app/src/main/res/layout/activity_home.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml
new file mode 100644
index 0000000..4996935
--- /dev/null
+++ b/app/src/main/res/layout/activity_onboarding.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_plot.xml b/app/src/main/res/layout/activity_plot.xml
new file mode 100644
index 0000000..b414ffc
--- /dev/null
+++ b/app/src/main/res/layout/activity_plot.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_self_isolation.xml b/app/src/main/res/layout/activity_self_isolation.xml
new file mode 100644
index 0000000..4e16166
--- /dev/null
+++ b/app/src/main/res/layout/activity_self_isolation.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..716a2ba
--- /dev/null
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/database_peek.xml b/app/src/main/res/layout/database_peek.xml
new file mode 100644
index 0000000..2f7faee
--- /dev/null
+++ b/app/src/main/res/layout/database_peek.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_error_uploading.xml b/app/src/main/res/layout/dialog_error_uploading.xml
new file mode 100644
index 0000000..4f177f9
--- /dev/null
+++ b/app/src/main/res/layout/dialog_error_uploading.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_uploading.xml b/app/src/main/res/layout/dialog_uploading.xml
new file mode 100644
index 0000000..56d3c21
--- /dev/null
+++ b/app/src/main/res/layout/dialog_uploading.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_data_privacy.xml b/app/src/main/res/layout/fragment_data_privacy.xml
new file mode 100644
index 0000000..e071c89
--- /dev/null
+++ b/app/src/main/res/layout/fragment_data_privacy.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_enter_number.xml b/app/src/main/res/layout/fragment_enter_number.xml
new file mode 100644
index 0000000..f4506fa
--- /dev/null
+++ b/app/src/main/res/layout/fragment_enter_number.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_enter_pin.xml b/app/src/main/res/layout/fragment_enter_pin.xml
new file mode 100644
index 0000000..3c49c9b
--- /dev/null
+++ b/app/src/main/res/layout/fragment_enter_pin.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_help.xml b/app/src/main/res/layout/fragment_help.xml
new file mode 100644
index 0000000..1c8f85e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_help.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 0000000..5d3e6f3
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home_external_links.xml b/app/src/main/res/layout/fragment_home_external_links.xml
new file mode 100644
index 0000000..c2943ae
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home_external_links.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_home_setup_complete_header.xml b/app/src/main/res/layout/fragment_home_setup_complete_header.xml
new file mode 100644
index 0000000..5f4ed66
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home_setup_complete_header.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml b/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml
new file mode 100644
index 0000000..a34af95
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home_uploaded_card.xml b/app/src/main/res/layout/fragment_home_uploaded_card.xml
new file mode 100644
index 0000000..77d9ef6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home_uploaded_card.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_how_it_works.xml b/app/src/main/res/layout/fragment_how_it_works.xml
new file mode 100644
index 0000000..5b62caa
--- /dev/null
+++ b/app/src/main/res/layout/fragment_how_it_works.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_intro.xml b/app/src/main/res/layout/fragment_intro.xml
new file mode 100644
index 0000000..5f0000a
--- /dev/null
+++ b/app/src/main/res/layout/fragment_intro.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_permission.xml b/app/src/main/res/layout/fragment_permission.xml
new file mode 100644
index 0000000..f27d5a4
--- /dev/null
+++ b/app/src/main/res/layout/fragment_permission.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_permission_success.xml b/app/src/main/res/layout/fragment_permission_success.xml
new file mode 100644
index 0000000..16d588d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_permission_success.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_personal_details.xml b/app/src/main/res/layout/fragment_personal_details.xml
new file mode 100644
index 0000000..ddc2d1f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_personal_details.xml
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_registration_consent.xml b/app/src/main/res/layout/fragment_registration_consent.xml
new file mode 100644
index 0000000..2373c20
--- /dev/null
+++ b/app/src/main/res/layout/fragment_registration_consent.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_under_sixteen.xml b/app/src/main/res/layout/fragment_under_sixteen.xml
new file mode 100644
index 0000000..71e4376
--- /dev/null
+++ b/app/src/main/res/layout/fragment_under_sixteen.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_upload_finished.xml b/app/src/main/res/layout/fragment_upload_finished.xml
new file mode 100644
index 0000000..b1defc7
--- /dev/null
+++ b/app/src/main/res/layout/fragment_upload_finished.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_upload_initial.xml b/app/src/main/res/layout/fragment_upload_initial.xml
new file mode 100644
index 0000000..422f12a
--- /dev/null
+++ b/app/src/main/res/layout/fragment_upload_initial.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_upload_master.xml b/app/src/main/res/layout/fragment_upload_master.xml
new file mode 100644
index 0000000..566e204
--- /dev/null
+++ b/app/src/main/res/layout/fragment_upload_master.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_upload_page_4.xml b/app/src/main/res/layout/fragment_upload_page_4.xml
new file mode 100644
index 0000000..14d4e4f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_upload_page_4.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_verify_upload_pin.xml b/app/src/main/res/layout/fragment_verify_upload_pin.xml
new file mode 100644
index 0000000..268f0ea
--- /dev/null
+++ b/app/src/main/res/layout/fragment_verify_upload_pin.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/recycler_view_item.xml b/app/src/main/res/layout/recycler_view_item.xml
new file mode 100644
index 0000000..0c76a77
--- /dev/null
+++ b/app/src/main/res/layout/recycler_view_item.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_card_external_link_card.xml b/app/src/main/res/layout/view_card_external_link_card.xml
new file mode 100644
index 0000000..24a9fbd
--- /dev/null
+++ b/app/src/main/res/layout/view_card_external_link_card.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_card_permission_card.xml b/app/src/main/res/layout/view_card_permission_card.xml
new file mode 100644
index 0000000..97940f5
--- /dev/null
+++ b/app/src/main/res/layout/view_card_permission_card.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_pin.xml b/app/src/main/res/layout/view_pin.xml
new file mode 100644
index 0000000..40c7bf3
--- /dev/null
+++ b/app/src/main/res/layout/view_pin.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_ul.xml b/app/src/main/res/layout/view_ul.xml
new file mode 100644
index 0000000..b5633a9
--- /dev/null
+++ b/app/src/main/res/layout/view_ul.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/webview.xml b/app/src/main/res/layout/webview.xml
new file mode 100644
index 0000000..dbe28ff
--- /dev/null
+++ b/app/src/main/res/layout/webview.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..ac94b34
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..ac94b34
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 0000000..4ec2bb5
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100755
index 0000000..da092c4
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 0000000..ae4d17e
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100755
index 0000000..4ef2426
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..3a0d0ff
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100755
index 0000000..ac94c9f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..449587f
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100755
index 0000000..f1b7714
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 0000000..df72e4f
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100755
index 0000000..689081f
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/navigation/nav_home.xml b/app/src/main/res/navigation/nav_home.xml
new file mode 100644
index 0000000..afb6d51
--- /dev/null
+++ b/app/src/main/res/navigation/nav_home.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_onboarding.xml b/app/src/main/res/navigation/nav_onboarding.xml
new file mode 100644
index 0000000..efc55bf
--- /dev/null
+++ b/app/src/main/res/navigation/nav_onboarding.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_register.xml b/app/src/main/res/navigation/nav_register.xml
new file mode 100644
index 0000000..a7df664
--- /dev/null
+++ b/app/src/main/res/navigation/nav_register.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_upload.xml b/app/src/main/res/navigation/nav_upload.xml
new file mode 100644
index 0000000..22770ec
--- /dev/null
+++ b/app/src/main/res/navigation/nav_upload.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v24/themes.xml b/app/src/main/res/values-v24/themes.xml
new file mode 100644
index 0000000..24275fa
--- /dev/null
+++ b/app/src/main/res/values-v24/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..b5c7062
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f4d362a
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,41 @@
+
+
+ #FFFFFF
+ #024B7E
+ #356F98
+ #6793B2
+ #E7EDF5
+ #141515
+ #505151
+ #DBDDDD
+ #788586
+ #0C3155
+ #008A23
+ #F6FCF4
+ #E2E2E2
+ #C8FFB9
+
+
+ #E5E5E5
+ #C8FFB9
+
+ #00574B
+ #024B7E
+
+ #2A3756
+
+ #17363d
+ #3d5c63
+ #F5F2D0
+
+ #00000000
+ #000000
+
+ #A31919
+
+ @color/dark_green
+ @color/slack_black_2
+
+ @color/colorPrimary
+ #DBDDDD
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..b70559b
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4dp
+ 8dp
+ 12dp
+ 16dp
+ 24dp
+ 28dp
+ 32dp
+ 48dp
+ 60dp
+
+ 48sp
+ 32sp
+ 24sp
+ 20sp
+
+ 24sp
+ 18sp
+ 16sp
+ 18sp
+
+ 46dp
+
+ 24dp
+
+ 4dp
+ 2dp
+ 56dp
+ 104dp
+ 16sp
+
+ 4dp
+
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..e093bfd
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #C8FFB9
+
\ No newline at end of file
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
new file mode 100644
index 0000000..2f4fa11
--- /dev/null
+++ b/app/src/main/res/values/integers.xml
@@ -0,0 +1,4 @@
+
+
+ 9
+
\ No newline at end of file
diff --git a/app/src/main/res/values/mp_feedback_config.xml b/app/src/main/res/values/mp_feedback_config.xml
new file mode 100644
index 0000000..24247e1
--- /dev/null
+++ b/app/src/main/res/values/mp_feedback_config.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ - Android
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..971942a
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,196 @@
+
+
+
+
+
+ Please try again later
+ Please check your internet connection
+
+ Your COVIDSafe information is currently being uploaded.\n\nPlease do not close the app.
+ An error occurred while uploading your information, please try again.
+ Try again
+ Cancel
+
+
+ Help
+
+ Join me in stopping the spread of COVID-19! Download COVIDSafe, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives https://covidsafe.gov.au
+ Join me in stopping the spread of COVID-19! Download COVIDSafe>, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives covidsafe.gov.au
+
+ COVIDSafe is active
+ Keep COVIDSafe active when you leave home or are in public places.
+
+ Make sure COVIDSafe is active
+ COVIDSafe is currently inactive. Make sure it’s active before you leave home and when in public places.
+ Check app now
+
+
+ Together we can stop the spread of COVID-19
+ COVIDSafe has been developed by the Australian Government to help keep the community safe from the spread of coronavirus.\n\nCOVIDSafe will securely note contact that you have with other users of the app. This will allow state and territory health officials to contact you, if you have been in close contact with someone who has tested positive to the virus.\n\nTogether we can help stop the spread and stay healthy.
+ Please head to aus.gov.au for the latest Coronavirus news
+ I want to help
+
+
+ How COVIDSafe works
+ Bluetooth® signals are used to determine when you\'re near another COVIDSafe user.\n\nEvery instance of close contact between you and other COVIDSafe users is noted to create close contact information. The information is encrypted and only stored in your phone.\n\nIf you test positive to COVID-19 as a COVIDSafe user, a state ot territory health official will contact you. They will assist with voluntary upload of your close contact information to a highly secure information storage system\n\nState or territory health officials can also contact you if you came in close contact with another COVIDSafe user who tested positive.\n\nFor more information please refer to the Help Topics page
+ Your consent will always be requested if health tracing is required.
+ Read our Privacy Notice
+ Read our Terms and conditions
+ Next
+
+
+ Registration and privacy
+ It is important that you read the COVIDSafe privacy policy before you register for COVIDSafe.\n\nIf you are under 16 years of age, your parent/guardian must also read the privacy policy.\n\nUse of COVIDSafe is completely voluntary. You can install or delete the application at any time. If you delete COVIDSafe, you may also ask for your information to be deleted from the secure server.\n\nTo register for COVIDSafe, you will need to enter a name, mobile number, age range and postcode.\n\nInformation you submit when you register, and information about your use of COVIDSafe will be collected and stored on a highly secure server.\n\nCOVIDSafe will not collect your location information.\n\nCOVIDSafe will note the time of contact and an anonymous ID code of other COVIDSafe users you come into contact with.\n\nOther COVIDSafe users you come into contact with will record an anonymous ID code and the time of contact with your device.\n\nIf another user tests positive to COVID-19, they may upload their contact information and a state or territory health official may contact you for tracing purposes.\n\nYour registration details will only be used or disclosed for contact tracing and for the proper and lawful functioning of COVIDSafe.\n\nMore information is available at the Australian Government Department of Health website.\n\nSee the COVIDSafe privacy policy for further details about your rights about your information and how it will be handled and shared.
+ Next
+
+
+ Registration consent
+ I consent to the Australian Department of Health collecting:
+ My registration information to allow contact tracing by state and territory health officials.
+ My contact information from other COVIDSafe users after they test positive for COVID-19.
+ I consent.
+ I agree
+
+
+ Enter your details
+ Full name
+ Firstname Lastname
+ Age
+ Age range
+ Postcode
+ e.g. 2000
+ privacy policy
+ Postcode
+ Invalid postcode
+ Age cannot be empty
+ Invalid name
+ Select your age
+ Select
+
+ Continue
+
+ - 8:0–15
+ - 22:16–29
+ - 35:30–39
+ - 45:40–49
+ - 55:50–59
+ - 65:60–69
+ - 75:70–79
+ - 85:80–89
+ - 95:90+
+
+
+
+
+ You need the consent of your parent/guardian to proceed
+ I confirm my parent or guardian consents to the Australian Department of Health collecting:
+ My registration information to allow contact tracing by state and territory health officials.
+ My contact information from other COVIDSafe users after they test positive for COVID-19.
+ I confirm.
+ Continue
+
+
+
+ Enter your mobile number
+ Invalid phone number.
+ +61
+ Use an Australian phone number.
+ We’ll send you a six-digit PIN to verify your mobile number.
+ Trying to register on behalf of a friend or relative?\n\nThey will need to register using their own device and phone number so that COVIDSafe can work for them.
+ Get PIN
+
+
+ Enter the PIN sent to %s %s
+ Is this mobile number wrong?
+ Your PIN will expire in
+ Wrong PIN entered
+ Resend PIN
+ Issues receiving your PIN?
+ Verify
+
+
+ App permissions
+ COVIDSafe needs Bluetooth® and notifications enabled to work.\n\nSelect ‘Proceed’ to enable:\n\n1. Bluetooth®\n\n2. Location Permissions\n\n3. Battery Optimiser\n\n\nAndroid needs Location Permissions for Bluetooth® to work.
+ Proceed
+ Android requires location access to enable Bluetooth® functions for COVIDSafe. COVIDSafe cannot work properly without it
+
+
+ You\'ve successfully registered
+ 1. Keep your phone with you when you leave home.\n\n2. Keep the app running.\n\n3. Keep Bluetooth® on.
+ Keep push notifications on for COVIDSafe so we can notify you quickly if the app isn\'t working properly.
+ Continue
+
+
+ COVIDSafe is active.\nNo further action is required.
+ COVIDSafe is not active.\nCheck your permissions.
+ Thank you for helping stop the spread of COVID-19. Your information has been uploaded.
+ Bluetooth®: %s
+ Battery optimization: %s
+ Location: %s
+ Push Notification: %s
+ On
+ Off
+ Check\npermissions
+ COVIDSafe needs permission to access these features.
+ Help
+ Check your permissions
+ COVIDSafe won\'t work without these permissions.
+ Has a health official asked you to upload your data?
+ Register for self isolation
+ Help stop the spread of COVID-19 and track your symptoms.
+ Register
+ Your data has been uploaded
+ You\’re helping stop the spread of COVID-19 by uploading your data daily while in self-isolation.
+
+ Let\'s stop the spread of COVID-19.
+ For more help, see our FAQ.
+ Share COVIDSafe
+
+ Invite others to join. Together, we’re stronger.
+ Latest news and updates
+ https://www.australia.gov.au
+ https://covid-form.service.gov.au
+ Head to aus.gov.au for the latest Coronavirus news.
+ Get the Coronavirus app
+ Download the government app for the latest news and advice.
+ Has a health official contacted you?
+ You can only upload your information if you have tested positive.
+ https://www.health.gov.au/resources/apps-and-tools/coronavirus-australia-app
+ Version Number:%s
+ Report an issue
+
+
+ Yes
+ No
+
+ Is a health official asking you to upload your information?
+ Only if you test positive to COVID-19 will a state or territory health official contact you to assist with voluntary upload of your information.\n\nOnce you press ‘Yes’ you’ll need to provide consent to upload your information.
+
+ Upload consent
+ Unless you consent, your close contact information will not be uploaded.\n\nIf you consent, your close contact information will be uploaded and shared with state or territory health officials for contact tracing purposes.\n\nRead the COVIDSafe privacy policy for further details.
+ I consent to upload my information
+ Upload your information
+ A state or territory health official will send a PIN to your device via text message. Enter it below to upload your information.
+ Upload my information
+ Invalid PIN, please ask the health official to send you another PIN.
+
+ Thank you for helping to stop the spread of COVID-19!
+ You have successfully uploaded your information to the COVIDSafe highly secure storage system.\n\nState or territory health officials will notify other COVIDSafe users that have recorded instances of close contact with you. Your identity will remain anonymous to other users.
+
+ Continue
+ I agree
+ Got it
+
+ Upload your data
+ Continue
+
+ Upload failed
+ Upload your data
+ Please upload your data today to help stop the spread of COVID-19
+ Upload data now
+
+
+ Thank you! You have helped to stop the spread of COVID-19!
+ You’ve kept others safe while helping to stop the spread of COVID-19 during self-isolation.
+ Continue
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..f51b8c2
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..d58e75c
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/type.xml b/app/src/main/res/values/type.xml
new file mode 100755
index 0000000..9ab72eb
--- /dev/null
+++ b/app/src/main/res/values/type.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..1525983
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+ aws.covidsafe.gov.au
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 0000000..56213de
--- /dev/null
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..c298c73
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,33 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.72'
+ ext.retrofit_version = "2.6.2"
+ ext.okhttp_version = "4.4.0"
+ repositories {
+ google()
+ jcenter()
+
+ maven { url 'https://maven.fabric.io/public' }
+ maven { url "https://jitpack.io" }
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.6.3'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.2.1'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/feedback-android/build.gradle b/feedback-android/build.gradle
new file mode 100644
index 0000000..86cd43d
--- /dev/null
+++ b/feedback-android/build.gradle
@@ -0,0 +1,60 @@
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+
+project.ext {
+ pomName = "MobileKit Feedback Module"
+ pomDescription = "A library for collecting in-app feedback, sent directly to your JIRA project"
+}
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.2"
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables.useSupportLibrary = true
+ multiDexEnabled = true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ //Dependencies are listed in alphabetical order.
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+
+ implementation "androidx.core:core-ktx:1.2.0"
+ implementation "androidx.cardview:cardview:1.0.0"
+ implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
+ implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
+ implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
+ implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
+ implementation "com.google.android.material:material:1.1.0"
+ implementation "io.reactivex:rxjava:1.3.8"
+ implementation "io.reactivex:rxjava-reactive-streams:1.2.1"
+}
diff --git a/feedback-android/lint.xml b/feedback-android/lint.xml
new file mode 100644
index 0000000..b5cc8be
--- /dev/null
+++ b/feedback-android/lint.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feedback-android/proguard-rules.pro b/feedback-android/proguard-rules.pro
new file mode 100644
index 0000000..5604a3b
--- /dev/null
+++ b/feedback-android/proguard-rules.pro
@@ -0,0 +1,35 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/yzhou/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+#rxJava 1.3.x rules
+-dontwarn sun.misc.**
+
+-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
+ long producerIndex;
+ long consumerIndex;
+}
+
+-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
+ rx.internal.util.atomic.LinkedQueueNode producerNode;
+}
+
+-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
+ rx.internal.util.atomic.LinkedQueueNode consumerNode;
+}
+
+-dontnote rx.internal.util.PlatformDependent
\ No newline at end of file
diff --git a/feedback-android/src/main/AndroidManifest.xml b/feedback-android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2a5d04f
--- /dev/null
+++ b/feedback-android/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/ActivityTracker.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/ActivityTracker.java
new file mode 100644
index 0000000..a492ffc
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/ActivityTracker.java
@@ -0,0 +1,108 @@
+package com.atlassian.mobilekit.module.core;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+public class ActivityTracker implements Application.ActivityLifecycleCallbacks, UiInfo {
+
+ private WeakReference activityRef = new WeakReference<>(null);
+ private boolean isAppVisible = false;
+ private final CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>();
+
+ public ActivityTracker(Application application) {
+ application.registerActivityLifecycleCallbacks(this);
+ }
+
+ @Override
+ public Activity getCurrentActivity() {
+ Activity currentActivity = activityRef.get();
+
+ if (currentActivity != null
+ && (currentActivity.isFinishing()
+ || currentActivity.isChangingConfigurations())) {
+ currentActivity = null;
+ activityRef = new WeakReference<>(null);
+ }
+
+ return currentActivity;
+ }
+
+ @Override
+ public boolean isAppVisible() {
+ return isAppVisible;
+ }
+
+ @Override
+ public void registerListener(UiInfoListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void unregisterListener(UiInfoListener listener) {
+ listeners.remove(listener);
+ }
+
+ private void notifyAppVisible() {
+ isAppVisible = true;
+ for (UiInfoListener listener : listeners) {
+ listener.onAppVisible();
+ }
+ }
+
+ private void notifyAppNotVisible() {
+ isAppVisible = false;
+ for (UiInfoListener listener : listeners) {
+ listener.onAppNotVisible();
+ }
+ }
+
+ @Override
+ public void onActivityCreated(@NotNull Activity activity, Bundle bundle) {
+ }
+
+ @Override
+ public void onActivityStarted(@NotNull Activity activity) {
+
+ }
+
+ @Override
+ public void onActivityResumed(@NotNull Activity activity) {
+ final boolean wasEmpty = (activityRef.get() == null);
+ activityRef = new WeakReference<>(activity);
+
+ if (wasEmpty) {
+ notifyAppVisible();
+ }
+ }
+
+ @Override
+ public void onActivityPaused(@NotNull Activity activity) {
+
+ }
+
+ @Override
+ public void onActivityStopped(@NotNull Activity activity) {
+ if (activityRef.get() == activity) {
+ activityRef = new WeakReference<>(null);
+ notifyAppNotVisible();
+ }
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(@NotNull Activity activity, @NotNull Bundle bundle) {
+
+ }
+
+ @Override
+ public void onActivityDestroyed(@NotNull Activity activity) {
+ if (activityRef.get() == activity) {
+ activityRef = new WeakReference<>(null);
+ }
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/AndroidUiNotifier.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/AndroidUiNotifier.java
new file mode 100644
index 0000000..f760bcc
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/AndroidUiNotifier.java
@@ -0,0 +1,24 @@
+package com.atlassian.mobilekit.module.core;
+
+
+import android.os.Handler;
+import android.os.Looper;
+
+public class AndroidUiNotifier implements UiNotifier {
+
+ private final Handler uiHandler;
+
+ public AndroidUiNotifier() {
+ uiHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void post(Runnable runnable) {
+ uiHandler.post(runnable);
+ }
+
+ @Override
+ public void postDelayed(Runnable runnable, int delay) {
+ uiHandler.postDelayed(runnable, delay);
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/Command.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/Command.java
new file mode 100644
index 0000000..8d38005
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/Command.java
@@ -0,0 +1,7 @@
+package com.atlassian.mobilekit.module.core;
+
+/**
+ * Synonym interface, only to emphasize its usage.
+ */
+public interface Command extends Runnable {
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/DeviceInfo.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/DeviceInfo.java
new file mode 100644
index 0000000..1d6030f
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/DeviceInfo.java
@@ -0,0 +1,144 @@
+package com.atlassian.mobilekit.module.core;
+
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.provider.Settings;
+
+
+import java.util.Locale;
+import java.util.UUID;
+
+public final class DeviceInfo {
+
+ private static final String NAMESPACE = "com.atlassian.mobilekit.module.core";
+ private static final String STORE_NAME = NAMESPACE + ".preferences";
+ private static final String UUID_KEY = NAMESPACE + ".UUID";
+ private static final String ANDROID_OS = "Android OS";
+
+ private final Context ctx;
+ private final SharedPreferences store;
+
+ // These use lazy initialization
+ private String uuid;
+ private String udid;
+ private String appVersionName;
+ private int appVersionCode = -1;
+
+ public DeviceInfo(Context ctx) {
+ this.ctx = ctx.getApplicationContext();
+ store = ctx.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE);
+ }
+
+ private synchronized String initUdid() {
+ String androidId = Settings.Secure.getString(ctx.getContentResolver(), Settings.Secure.ANDROID_ID);
+ if (androidId == null) {
+ throw new AssertionError("ANDROID_ID setting was null");
+ }
+ return androidId;
+ }
+
+ private synchronized String initUuid() {
+ String uuidStr = store.getString(UUID_KEY, null);
+ if (uuidStr == null) {
+ uuidStr = UUID.randomUUID().toString();
+ SharedPreferences.Editor edit = store.edit();
+ edit.putString(UUID_KEY, uuidStr);
+ edit.apply();
+ }
+ return uuidStr;
+ }
+
+ public String getUuid() {
+ if (uuid == null) {
+ uuid = initUuid();
+ }
+
+ return uuid;
+ }
+
+ public String getUdid() {
+ if (udid == null) {
+ udid = initUdid();
+ }
+
+ return udid;
+ }
+
+ public String getAppPkgName() {
+ return ctx.getPackageName();
+ }
+
+ public String getAppName() {
+ return ctx.getPackageManager()
+ .getApplicationLabel(ctx.getApplicationInfo())
+ .toString();
+ }
+
+ public String getAppVersionName() {
+ if (appVersionName == null) {
+ PackageInfo pInfo = getPackageInfo();
+ appVersionName = pInfo.versionName;
+ }
+
+ return appVersionName;
+ }
+
+ public int getAppVersionCode() {
+ if (appVersionCode == -1) {
+ PackageInfo pInfo = getPackageInfo();
+ appVersionCode = pInfo.versionCode;
+ }
+
+ return appVersionCode;
+ }
+
+ public String getSystemVersion() {
+ return Build.VERSION.RELEASE;
+ }
+
+ public String getSystemName() {
+ return ANDROID_OS;
+ }
+
+ public String getDeviceName() {
+ return Build.DEVICE;
+ }
+
+ public String getModel() {
+ return Build.MODEL;
+ }
+
+ public String getLanguage() {
+ return Locale.getDefault().getDisplayLanguage();
+ }
+
+ public String getLocale() {
+ Locale locale = Locale.getDefault();
+ return locale.getLanguage() + "_" + locale.getCountry();
+ }
+
+ public boolean hasConnectivity() {
+ ConnectivityManager cMgr = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cMgr == null) {
+ return false;
+ }
+
+ NetworkInfo nwInfo = cMgr.getActiveNetworkInfo();
+ return nwInfo != null && nwInfo.isConnected();
+ }
+
+
+ private PackageInfo getPackageInfo() {
+ try {
+ return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/FeedbackBaseActivity.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/FeedbackBaseActivity.java
new file mode 100644
index 0000000..37be496
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/FeedbackBaseActivity.java
@@ -0,0 +1,32 @@
+package com.atlassian.mobilekit.module.core;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+
+public class FeedbackBaseActivity extends AppCompatActivity {
+
+ private boolean isPaused;
+ private long pausedAt;
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ isPaused = true;
+ pausedAt = System.currentTimeMillis();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ isPaused = false;
+ pausedAt = 0;
+ }
+
+ protected boolean isPaused() {
+ return isPaused;
+ }
+
+ protected long getPausedDuration() {
+ return System.currentTimeMillis() - pausedAt;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/JobQueue.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/JobQueue.java
new file mode 100644
index 0000000..020e6dd
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/JobQueue.java
@@ -0,0 +1,27 @@
+package com.atlassian.mobilekit.module.core;
+
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+public final class JobQueue {
+
+ private final ExecutorService executor;
+
+ public JobQueue() {
+ executor = Executors.newFixedThreadPool(5);
+ }
+
+ public final void enqueue(Runnable r) {
+ executor.execute(r);
+ }
+
+ @VisibleForTesting
+ public Executor getExecutor() {
+ return executor;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/MobileKitDialogViewBuilder.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/MobileKitDialogViewBuilder.java
new file mode 100644
index 0000000..297f979
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/MobileKitDialogViewBuilder.java
@@ -0,0 +1,97 @@
+package com.atlassian.mobilekit.module.core;
+
+
+import androidx.appcompat.widget.AppCompatTextView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+
+import com.atlassian.mobilekit.module.feedback.R;
+
+public class MobileKitDialogViewBuilder {
+
+ private final LayoutInflater inflater;
+ private final ViewGroup container;
+ private int titleResId;
+ private int msgResId;
+ private int posBtnResId;
+ private int negBtnResId;
+
+ private View.OnClickListener posClickListener;
+ private View.OnClickListener negClickListener;
+
+ public MobileKitDialogViewBuilder(LayoutInflater inflater, ViewGroup container) {
+ this.inflater = inflater;
+ this.container = container;
+ }
+
+ public MobileKitDialogViewBuilder title(int titleResId) {
+ this.titleResId = titleResId;
+ return this;
+ }
+
+ public MobileKitDialogViewBuilder message(int msgResId) {
+ this.msgResId = msgResId;
+ return this;
+ }
+
+ public MobileKitDialogViewBuilder positiveButton(
+ int posBtnResId, View.OnClickListener onClickListener) {
+
+ this.posBtnResId = posBtnResId;
+ posClickListener = onClickListener;
+ return this;
+ }
+
+ public MobileKitDialogViewBuilder negativeButton(
+ int negBtnResId, View.OnClickListener onClickListener) {
+
+ this.negBtnResId = negBtnResId;
+ negClickListener = onClickListener;
+ return this;
+ }
+
+
+ public View build() {
+
+ View dialogView = inflater.inflate(R.layout.mk_feedback_dialog_container, container, false);
+ FrameLayout frameLayout = (FrameLayout) dialogView.findViewById(R.id.dialog_container);
+ inflater.inflate(R.layout.mk_feedback_dialog_content, frameLayout);
+
+ final AppCompatTextView titleView = (AppCompatTextView) dialogView.findViewById(R.id.title);
+ if (titleResId == 0) {
+ titleView.setVisibility(View.GONE);
+ } else {
+ titleView.setText(titleResId);
+ }
+
+ final AppCompatTextView msgView = (AppCompatTextView) dialogView.findViewById(R.id.message);
+ if (msgResId == 0) {
+ msgView.setVisibility(View.GONE);
+ } else {
+ msgView.setText(msgResId);
+ }
+
+ final Button posBtn = (Button) dialogView.findViewById(R.id.positive_btn);
+ if (posBtnResId == 0) {
+ posBtn.setVisibility(View.GONE);
+ } else {
+ posBtn.setText(posBtnResId);
+ posBtn.setOnClickListener(posClickListener);
+ }
+
+ final Button negBtn = (Button) dialogView.findViewById(R.id.negative_btn);
+ if (negBtnResId == 0) {
+ negBtn.setVisibility(View.GONE);
+ } else {
+ negBtn.setText(negBtnResId);
+ negBtn.setOnClickListener(negClickListener);
+ }
+
+ return dialogView;
+ }
+
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/Receiver.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/Receiver.java
new file mode 100644
index 0000000..0759c12
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/Receiver.java
@@ -0,0 +1,6 @@
+package com.atlassian.mobilekit.module.core;
+
+public interface Receiver {
+
+ void receive(T data);
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiInfo.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiInfo.java
new file mode 100644
index 0000000..517f7fc
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiInfo.java
@@ -0,0 +1,16 @@
+package com.atlassian.mobilekit.module.core;
+
+
+import android.app.Activity;
+
+public interface UiInfo {
+
+ Activity getCurrentActivity();
+
+ boolean isAppVisible();
+
+ void registerListener(UiInfoListener listener);
+
+ void unregisterListener(UiInfoListener listener);
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiInfoListener.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiInfoListener.java
new file mode 100644
index 0000000..be1e06f
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiInfoListener.java
@@ -0,0 +1,10 @@
+package com.atlassian.mobilekit.module.core;
+
+
+public interface UiInfoListener {
+
+ void onAppVisible();
+
+ void onAppNotVisible();
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiNotifier.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiNotifier.java
new file mode 100644
index 0000000..7d87590
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiNotifier.java
@@ -0,0 +1,9 @@
+package com.atlassian.mobilekit.module.core;
+
+
+public interface UiNotifier {
+
+ void post(Runnable runnable);
+
+ void postDelayed(Runnable runnable, int delay);
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiReceiver.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiReceiver.java
new file mode 100644
index 0000000..7007b17
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/UiReceiver.java
@@ -0,0 +1,10 @@
+package com.atlassian.mobilekit.module.core;
+
+/**
+ * Notifications to this receiver will be delivered on Ui/Main Thread
+ *
+ * @param
+ */
+public interface UiReceiver extends Receiver {
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/utils/StringUtils.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/utils/StringUtils.java
new file mode 100644
index 0000000..5730c28
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/utils/StringUtils.java
@@ -0,0 +1,23 @@
+package com.atlassian.mobilekit.module.core.utils;
+
+import android.text.TextUtils;
+
+/**
+ * This class exists as a workaround for Unit Test issues where TextUtils cannot be mocked
+ * Refer: http://tools.android.com/tech-docs/unit-testing-support
+ */
+public final class StringUtils {
+
+ public static final String EOL = "\n";
+ private static final String ELLIPSIS = "\u2026";
+ private static final int ELLIPSIS_LEN = ELLIPSIS.length();
+
+ private StringUtils() {
+ }
+
+ public static String ellipsize(String input, int maxLen) {
+ return (TextUtils.isEmpty(input) || input.length() <= (maxLen + ELLIPSIS_LEN))
+ ? input
+ : input.substring(0, maxLen) + ELLIPSIS;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/utils/SystemUtils.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/utils/SystemUtils.java
new file mode 100644
index 0000000..0319def
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/core/utils/SystemUtils.java
@@ -0,0 +1,22 @@
+package com.atlassian.mobilekit.module.core.utils;
+
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import static android.content.Context.INPUT_METHOD_SERVICE;
+
+/**
+ * Utils to interact with Android system APIs.
+ */
+public class SystemUtils {
+
+ /**
+ * Hides soft keyboard
+ */
+ public static void hideSoftKeyboard(View target) {
+ InputMethodManager inputMethodManager = (InputMethodManager) target.getContext().getSystemService(INPUT_METHOD_SERVICE);
+ if (inputMethodManager != null) {
+ inputMethodManager.hideSoftInputFromWindow(target.getWindowToken(), 0);
+ }
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackActivity.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackActivity.java
new file mode 100644
index 0000000..acb90b0
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackActivity.java
@@ -0,0 +1,239 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Patterns;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+
+import com.atlassian.mobilekit.module.core.DeviceInfo;
+import com.atlassian.mobilekit.module.core.utils.SystemUtils;
+import com.atlassian.mobilekit.module.feedback.commands.Result;
+
+public class FeedbackActivity extends AppCompatActivity
+ implements ProgressDialogActions, FinishAction, SendFeedbackListener {
+
+ private EditText feedbackEt;
+ private EditText feedbackEmailEt;
+ private MenuItem sendMenuItem = null;
+ private DeviceInfo deviceInfo = null;
+
+ public static Intent getIntent(Context src) {
+ Intent intent = new Intent(src, FeedbackActivity.class);
+ if (!(src instanceof Activity)) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // TO make sure scroll works with editTexts
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ setContentView(R.layout.activity_feedback);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+
+ }
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finish();
+ }
+ });
+
+ feedbackEt = (EditText) findViewById(R.id.feedbackIssueDescriptionEditText);
+ feedbackEt.addTextChangedListener(new TextWatcherAdapter() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateSendButtonState();
+ }
+ });
+ feedbackEmailEt = (EditText) findViewById(R.id.feedbackIssueEmailEditText);
+ feedbackEmailEt.addTextChangedListener(new TextWatcherAdapter() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateSendButtonState();
+ }
+ });
+ feedbackEmailEt.setOnEditorActionListener((v, actionId, event) -> {
+ onOptionsItemSelected(sendMenuItem);
+ return true;
+ });
+
+ View immediateParentView = findViewById(R.id.feedback_content_parent);
+ if (immediateParentView != null) {
+ immediateParentView.setOnClickListener(view -> {
+ // Show keyboard when user clicks on
+ // Large white area on the screen beside the screenshot
+ focusOnFeedbackEditText();
+ });
+ }
+
+ View rootView = findViewById(android.R.id.content);
+ if (rootView != null) {
+ rootView.setOnClickListener(view -> {
+ // Show keyboard when user clicks on
+ // Large white area on the screen below the screenshot
+ focusOnFeedbackEditText();
+ });
+ }
+
+ if (null == savedInstanceState) {
+ focusOnFeedbackEditText();
+ }
+
+ deviceInfo = new DeviceInfo(getApplicationContext());
+
+ FeedbackModule.registerSendFeedbackListener(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ FeedbackModule.unregisterSendFeedbackListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_feedback, menu);
+ sendMenuItem = menu.findItem(R.id.action_send);
+ sendMenuItem.setEnabled(false);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.action_send) {
+ String msg = feedbackEt.getText().toString().trim();
+ if (TextUtils.isEmpty(msg)) {
+ Toast.makeText(this, R.string.mk_fb_feedback_empty, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ String email = feedbackEmailEt.getText().toString().trim();
+ if (TextUtils.isEmpty(email) || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
+ Toast.makeText(this, R.string.mk_fb_invalid_email_address, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ if (!deviceInfo.hasConnectivity()) {
+ Toast.makeText(this, R.string.mk_fb_device_offline, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ sendMenuItem = item;
+ SystemUtils.hideSoftKeyboard(feedbackEt);
+ SystemUtils.hideSoftKeyboard(feedbackEmailEt);
+ sendFeedback(msg, email);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void updateSendButtonState() {
+ String feedback = feedbackEt.getText().toString();
+ String email = feedbackEmailEt.getText().toString();
+ if (!TextUtils.isEmpty(feedback) && Patterns.EMAIL_ADDRESS.matcher(email).matches() && sendMenuItem != null) {
+ sendMenuItem.setEnabled(true);
+ } else if (sendMenuItem != null) {
+ sendMenuItem.setEnabled(false);
+ }
+ }
+
+ public void showProgressDialog() {
+ final ProgressDialogFragment progressDialog = new ProgressDialogFragment();
+ progressDialog.show(getSupportFragmentManager(), ProgressDialogFragment.class.getSimpleName());
+ }
+
+ public void dismissProgressDialog() {
+ Fragment dialogFragment = getSupportFragmentManager().findFragmentByTag(ProgressDialogFragment.class.getSimpleName());
+ if (dialogFragment != null
+ && dialogFragment instanceof ProgressDialogFragment) {
+ ((ProgressDialogFragment) dialogFragment).dismiss();
+ }
+ }
+
+ @Override
+ public void doFinish() {
+ finish();
+ }
+
+ private void focusOnFeedbackEditText() {
+ feedbackEt.requestFocus();
+ showKeyboard();
+ }
+
+ private void sendFeedback(final String msg, final String email) {
+ FeedbackModule.sendFeedback(msg, email);
+ showProgressDialog();
+ }
+
+ @Override
+ public void onSendCompleted(Result result) {
+ if (Result.SUCCESS == result) {
+
+ // Don't allow any more changes
+ if (sendMenuItem != null) {
+ sendMenuItem.setEnabled(false);
+ }
+ feedbackEt.setEnabled(false);
+ }
+
+ boolean isPaused = !getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED);
+
+ if (isPaused) {
+ finish(); // Cannot show any notification to user. So just finish.
+ }
+ }
+
+ private void showKeyboard() {
+ feedbackEt.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(feedbackEt, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+ }, 300);
+ }
+
+ private abstract class TextWatcherAdapter implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // unused
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // unused
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // unused
+ }
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackClient.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackClient.java
new file mode 100644
index 0000000..90ce3be
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackClient.java
@@ -0,0 +1,214 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.atlassian.mobilekit.module.core.Command;
+import com.atlassian.mobilekit.module.core.DeviceInfo;
+import com.atlassian.mobilekit.module.core.JobQueue;
+import com.atlassian.mobilekit.module.core.Receiver;
+import com.atlassian.mobilekit.module.core.UiInfo;
+import com.atlassian.mobilekit.module.core.UiInfoListener;
+import com.atlassian.mobilekit.module.core.UiNotifier;
+import com.atlassian.mobilekit.module.feedback.commands.Result;
+import com.atlassian.mobilekit.module.feedback.commands.SendFeedbackCommand;
+import com.atlassian.mobilekit.module.feedback.model.CreateIssueRequest;
+import com.atlassian.mobilekit.module.feedback.model.FeedbackConfig;
+import com.atlassian.mobilekit.module.feedback.network.BaseApiParams;
+import com.atlassian.mobilekit.module.feedback.network.JmcRestClient;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Commands supported by this client
+ * Show Feedback
+ * Send Feedback
+ */
+class FeedbackClient implements Receiver, UiInfoListener {
+
+ private static final String LOG_TAG = FeedbackClient.class.getSimpleName();
+ private static final String PROTOCOL_HTTPS = "https://";
+
+ private static final Set> IGNORE_ACTIVITIES = new HashSet<>();
+
+ static {
+ IGNORE_ACTIVITIES.add(FeedbackActivity.class);
+ }
+
+ private final JmcRestClient restClient;
+ private final JobQueue jobQueue;
+ private final DeviceInfo deviceInfo;
+ private final Map baseQueryMap = new HashMap<>();
+ private final UiNotifier uiNotifier;
+ private final UiInfo uiInfo;
+ private final FeedbackSettings settings;
+ private final AtomicInteger notificationViewId = new AtomicInteger(0);
+ private final CopyOnWriteArraySet notificationListeners = new CopyOnWriteArraySet<>();
+ private final CopyOnWriteArraySet sendFeedbackListeners = new CopyOnWriteArraySet<>();
+ private FeedbackDataProvider feedbackDataProvider;
+
+ private FeedbackConfig feedbackConfig;
+
+ FeedbackClient(@NonNull JmcRestClient restClient,
+ @NonNull DeviceInfo deviceInfo,
+ @NonNull JobQueue jobQueue,
+ @NonNull UiNotifier uiNotifier,
+ @NonNull UiInfo uiInfo,
+ @NonNull FeedbackSettings settings) {
+
+ this.restClient = restClient;
+ this.jobQueue = jobQueue;
+ this.deviceInfo = deviceInfo;
+ this.uiNotifier = uiNotifier;
+ this.uiInfo = uiInfo;
+ this.settings = settings;
+
+ init();
+ }
+
+ private void init() {
+ uiInfo.registerListener(this);
+ }
+
+ @Override
+ public void receive(FeedbackConfig data) {
+ feedbackConfig = data;
+ restClient.init(PROTOCOL_HTTPS, data.getHost());
+
+ baseQueryMap.put(BaseApiParams.API_KEY, data.getApiKey());
+ baseQueryMap.put(BaseApiParams.PROJECT, data.getProjectKey());
+ }
+
+ final void sendFeedback(String message, String email) {
+
+ final CreateIssueRequest.Builder requestBuilder =
+ new CreateIssueRequest.Builder()
+ .summary(message)
+ .description(message)
+ .isCrash(false)
+ .udid(deviceInfo.getUdid())
+ .uuid(deviceInfo.getUuid())
+ .appName(deviceInfo.getAppName())
+ .appId(deviceInfo.getAppPkgName())
+ .systemName(deviceInfo.getSystemName())
+ .deviceName(deviceInfo.getDeviceName())
+ .language(deviceInfo.getLanguage())
+ .components(Arrays.asList(feedbackConfig.getComponents()));
+
+ setFeedbackDataProvider(new FeedbackDataProvider() {
+ @Override
+ public String getAdditionalDescription() {
+ return null;
+ }
+
+ @Override
+ public JiraIssueType getIssueType() {
+ return JiraIssueType.SUPPORT;
+ }
+
+ @Override
+ public Map getCustomFieldsData() {
+ HashMap map = new HashMap<>();
+ map.put("E-mail", email);
+ map.put("OS version", deviceInfo.getSystemVersion());
+ map.put("App version", deviceInfo.getAppVersionName());
+ map.put("Phone model", deviceInfo.getModel());
+ return map;
+ }
+ });
+
+ final Command cmd = new SendFeedbackCommand(
+ baseQueryMap, requestBuilder, feedbackDataProvider,
+ restClient,
+ new SnackbarReceiver(uiInfo, uiNotifier, message, email),
+ uiNotifier);
+
+ jobQueue.enqueue(cmd);
+ }
+
+ @Override
+ public void onAppVisible() {
+
+ }
+
+ @Override
+ public void onAppNotVisible() {
+ }
+
+ final void showFeedback() {
+
+ final Activity curActivity = uiInfo.getCurrentActivity();
+ if (curActivity == null) {
+ Log.e(LOG_TAG, "No usable current activity. Abort Feedback.");
+ return;
+ } else if (IGNORE_ACTIVITIES.contains(curActivity.getClass())) {
+ Log.e(LOG_TAG, "User is already in Feedback flow. Abort.");
+ return;
+ }
+
+ final Context appCtx = curActivity.getApplicationContext();
+
+ uiNotifier.post(new Runnable() {
+ @Override
+ public void run() {
+ launchFeedbackScreen(curActivity, appCtx);
+ }
+ });
+ }
+
+ private void launchFeedbackScreen(Activity activity, Context appCtx) {
+ final Context useCtx = activity.isFinishing() || activity.isChangingConfigurations()
+ ? appCtx : activity;
+
+ Intent intent = FeedbackActivity.getIntent(useCtx);
+ useCtx.startActivity(intent);
+ }
+
+ private void setFeedbackDataProvider(FeedbackDataProvider feedbackDataProvider) {
+ this.feedbackDataProvider = feedbackDataProvider;
+ }
+
+ final int getNotificationViewId() {
+ return notificationViewId.get();
+ }
+
+ final void registerSendFeedbackListener(SendFeedbackListener listener) {
+ sendFeedbackListeners.add(listener);
+ }
+
+ final void unregisterSendFeedbackListener(SendFeedbackListener listener) {
+ sendFeedbackListeners.remove(listener);
+ }
+
+ final void notifySendCompleted(Result result) {
+ for (SendFeedbackListener listener : sendFeedbackListeners) {
+ listener.onSendCompleted(result);
+ }
+ }
+
+ final void notificationStarted() {
+ for (FeedbackNotificationListener fnl : notificationListeners) {
+ fnl.onNotificationStarted();
+ }
+ }
+
+ final void notificationDismissed() {
+ for (FeedbackNotificationListener fnl : notificationListeners) {
+ fnl.onNotificationDismissed();
+ }
+ }
+
+ final void setEnableDialogDisplayed() {
+ settings.setEnableDialogDisplayed();
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackDataProvider.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackDataProvider.java
new file mode 100644
index 0000000..3f921b6
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackDataProvider.java
@@ -0,0 +1,34 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import java.util.Map;
+import androidx.annotation.Nullable;
+
+/**
+ * This is used to provide more information which will be used when creating the feedback JIRA issue.
+ */
+public interface FeedbackDataProvider {
+
+ /**
+ * This string will be appended to the description of Feedback JIRA issue.
+ * It may contain standard wiki markup that is accepted by the JIRA Instance in description field.
+ * Encoding supported: UTF-8
+ * @return
+ */
+ String getAdditionalDescription();
+
+ /**
+ * See {@link JiraIssueType} for available options.
+ * If this returns null, then the library will default to {@link JiraIssueType#TASK}
+ * An admin must pre-configure the JIRA Project to accept this type.
+ * If not, resultant issue will be of default type as per the project.
+ * @return
+ */
+ JiraIssueType getIssueType();
+
+ /**
+ * This data will be passed to the feedback client to create custom fields in the Jira issue
+ * @return map of Jira field name and respective values
+ */
+ @Nullable
+ Map getCustomFieldsData();
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackModule.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackModule.java
new file mode 100644
index 0000000..05e85fb
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackModule.java
@@ -0,0 +1,103 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+
+import com.atlassian.mobilekit.module.core.ActivityTracker;
+import com.atlassian.mobilekit.module.core.AndroidUiNotifier;
+import com.atlassian.mobilekit.module.core.Command;
+import com.atlassian.mobilekit.module.core.DeviceInfo;
+import com.atlassian.mobilekit.module.core.JobQueue;
+import com.atlassian.mobilekit.module.core.UiInfo;
+import com.atlassian.mobilekit.module.core.UiNotifier;
+import com.atlassian.mobilekit.module.feedback.commands.LoadFeedbackConfigCommand;
+import com.atlassian.mobilekit.module.feedback.commands.Result;
+import com.atlassian.mobilekit.module.feedback.network.JmcRestClient;
+
+public final class FeedbackModule {
+
+ private static final String NAMESPACE = "com.atlassian.mobilekit.module.feedback";
+ private static final String STORE_NAME = NAMESPACE + ".preferences";
+
+ private static FeedbackClient feedbackClient = null;
+ private static JobQueue jobQueue = null;
+ private static UiInfo activityTracker = null;
+
+ private static UiNotifier androidUiNotifier = new AndroidUiNotifier();
+
+ private FeedbackModule() {
+ throw new AssertionError("Instances of this class are not allowed.");
+ }
+
+ /**
+ * Initializes using Application object
+ *
+ * @param application
+ */
+ public static void init(@NonNull Application application) {
+ // Build a Feedback Client here and _start_ its initialization.
+ // Initialization will happen asynchronously in a background thread.
+ if (feedbackClient == null) {
+ jobQueue = new JobQueue();
+ activityTracker = new ActivityTracker(application);
+
+ final SharedPreferences store = application.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE);
+
+ feedbackClient = new FeedbackClient(
+ new JmcRestClient(),
+ new DeviceInfo(application.getApplicationContext()),
+ jobQueue,
+ androidUiNotifier,
+ activityTracker,
+ new FeedbackSettings(store));
+ }
+
+ Command loadConfigCommand = new LoadFeedbackConfigCommand(
+ application.getApplicationContext(),
+ feedbackClient, androidUiNotifier);
+
+ jobQueue.enqueue(loadConfigCommand);
+ }
+
+ /**
+ * Displays a screen to prompt user for feedback
+ */
+ public static void showFeedbackScreen() {
+ feedbackClient.showFeedback();
+ }
+
+ static void notificationStarted() {
+ feedbackClient.notificationStarted();
+ }
+
+ static void notificationDismissed() {
+ feedbackClient.notificationDismissed();
+ }
+
+ static int getNotificationViewId() {
+ return feedbackClient.getNotificationViewId();
+ }
+
+ static void sendFeedback(@NonNull String message, @NonNull String email) {
+ feedbackClient.sendFeedback(message, email);
+ }
+
+ static void setEnableDialogDisplayed() {
+ feedbackClient.setEnableDialogDisplayed();
+ }
+
+ static void registerSendFeedbackListener(SendFeedbackListener listener) {
+ feedbackClient.registerSendFeedbackListener(listener);
+ }
+
+ static void unregisterSendFeedbackListener(SendFeedbackListener listener) {
+ feedbackClient.unregisterSendFeedbackListener(listener);
+ }
+
+ static void notifySendCompleted(Result result) {
+ feedbackClient.notifySendCompleted(result);
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackNotificationListener.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackNotificationListener.java
new file mode 100644
index 0000000..f2417c6
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackNotificationListener.java
@@ -0,0 +1,12 @@
+package com.atlassian.mobilekit.module.feedback;
+
+/**
+ * This listener is notified when Feedback success/error prompts are displayed to the user.
+ * These api are guaranteed to be invoked on the Main thread.
+ */
+public interface FeedbackNotificationListener {
+
+ void onNotificationStarted();
+
+ void onNotificationDismissed();
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackSettings.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackSettings.java
new file mode 100644
index 0000000..9c9e61d
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FeedbackSettings.java
@@ -0,0 +1,20 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import android.content.SharedPreferences;
+
+class FeedbackSettings {
+
+ private static final String KEY_ENABLE_DIALOG_SHOWN = "enable_dialog_shown";
+
+ private final SharedPreferences store;
+
+ FeedbackSettings(SharedPreferences store) {
+ this.store = store;
+ }
+
+ final void setEnableDialogDisplayed() {
+ SharedPreferences.Editor editor = store.edit();
+ editor.putBoolean(KEY_ENABLE_DIALOG_SHOWN, true);
+ editor.apply();
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FinishAction.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FinishAction.java
new file mode 100644
index 0000000..acedace
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/FinishAction.java
@@ -0,0 +1,6 @@
+package com.atlassian.mobilekit.module.feedback;
+
+public interface FinishAction {
+
+ void doFinish();
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/JiraIssueType.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/JiraIssueType.java
new file mode 100644
index 0000000..747da1d
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/JiraIssueType.java
@@ -0,0 +1,22 @@
+package com.atlassian.mobilekit.module.feedback;
+
+public enum JiraIssueType {
+
+ BUG("Bug"),
+ EPIC("Epic"),
+ IMPROVEMENT("Improvement"),
+ STORY("Story"),
+ SUPPORT("Support"),
+ TASK("Task");
+
+ private String type;
+
+ JiraIssueType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return type;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/ProgressDialogActions.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/ProgressDialogActions.java
new file mode 100644
index 0000000..004716b
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/ProgressDialogActions.java
@@ -0,0 +1,8 @@
+package com.atlassian.mobilekit.module.feedback;
+
+public interface ProgressDialogActions {
+
+ void showProgressDialog();
+
+ void dismissProgressDialog();
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/ProgressDialogFragment.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/ProgressDialogFragment.java
new file mode 100644
index 0000000..d8380fa
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/ProgressDialogFragment.java
@@ -0,0 +1,27 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatDialogFragment;
+
+public class ProgressDialogFragment extends AppCompatDialogFragment {
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setCancelable(false);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ ProgressDialog dialog = new ProgressDialog(getActivity(), getTheme());
+ dialog.setTitle(null);
+ dialog.setMessage(getString(R.string.mk_fb_sending));
+ dialog.setIndeterminate(true);
+ dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ return dialog;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SendFeedbackListener.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SendFeedbackListener.java
new file mode 100644
index 0000000..a23d3a1
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SendFeedbackListener.java
@@ -0,0 +1,11 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import com.atlassian.mobilekit.module.feedback.commands.Result;
+
+/**
+ * This listener is notified when Feedback sending completes successfully or with an error
+ */
+public interface SendFeedbackListener {
+
+ void onSendCompleted(Result result);
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarBuilder.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarBuilder.java
new file mode 100644
index 0000000..f7b741d
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarBuilder.java
@@ -0,0 +1,46 @@
+package com.atlassian.mobilekit.module.feedback;
+
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.view.View;
+
+import com.google.android.material.snackbar.Snackbar;
+
+public class SnackbarBuilder {
+
+ private SnackbarBuilder() {
+ // intentionally empty
+ }
+
+ public static Snackbar build(Activity activity, int resId) {
+ return Snackbar.make(getNotificationView(activity),
+ applyColorSpan(activity.getString(resId)),
+ Snackbar.LENGTH_LONG);
+ }
+
+ private static SpannableStringBuilder applyColorSpan(String txt) {
+ // Force text color, otherwise it may show up using odd color in the app.
+ final ForegroundColorSpan whiteSpan = new ForegroundColorSpan(Color.WHITE);
+ final SpannableStringBuilder spanText = new SpannableStringBuilder(txt);
+ spanText.setSpan(whiteSpan, 0, spanText.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ return spanText;
+ }
+
+ private static View getNotificationView(Activity activity) {
+ int id = FeedbackModule.getNotificationViewId();
+ if (id == 0) {
+ id = android.R.id.content;
+ }
+
+ View v = activity.findViewById(id);
+ if (v == null) {
+ v = activity.findViewById(android.R.id.content);
+ }
+
+ return v;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarCallback.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarCallback.java
new file mode 100644
index 0000000..33f21cb
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarCallback.java
@@ -0,0 +1,21 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import com.google.android.material.snackbar.Snackbar;
+
+
+public class SnackbarCallback extends Snackbar.Callback {
+
+ // Handle multiple onDismissed calls
+ // Refer: https://code.google.com/p/android/issues/detail?id=214547
+ private boolean isDismissed = false;
+
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+ if (isDismissed) {
+ return;
+ }
+ isDismissed = true;
+ FeedbackModule.notificationDismissed();
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarReceiver.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarReceiver.java
new file mode 100644
index 0000000..70a374e
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/SnackbarReceiver.java
@@ -0,0 +1,150 @@
+package com.atlassian.mobilekit.module.feedback;
+
+import android.app.Activity;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.atlassian.mobilekit.module.core.UiInfo;
+import com.atlassian.mobilekit.module.core.UiNotifier;
+import com.atlassian.mobilekit.module.core.UiReceiver;
+import com.atlassian.mobilekit.module.feedback.commands.Result;
+import com.google.android.material.snackbar.Snackbar;
+
+public class SnackbarReceiver implements UiReceiver {
+
+ private final UiInfo uiInfo;
+ private final UiNotifier uiNotifier;
+ private final String message;
+ private final String email;
+
+ SnackbarReceiver(@NonNull UiInfo uiInfo, @NonNull UiNotifier uiNotifier,
+ @NonNull String message, @NonNull String email) {
+ this.uiInfo = uiInfo;
+ this.uiNotifier = uiNotifier;
+ this.message = message;
+ this.email = email;
+ }
+
+ @Override
+ public void receive(Result data) {
+ switch (data) {
+ case SUCCESS:
+ showSuccessRunnable.run();
+ break;
+
+ case FAIL:
+ showFailureRunnable.run();
+ break;
+ }
+ }
+
+ private static void showProgressBar(Activity activity) {
+ if (activity instanceof ProgressDialogActions) {
+ ((ProgressDialogActions) activity).showProgressDialog();
+ }
+ }
+
+ private static void dismissProgressBar(Activity activity) {
+ if (activity instanceof ProgressDialogActions) {
+ ((ProgressDialogActions) activity).dismissProgressDialog();
+ }
+ }
+
+ private static void doFinish(Activity activity) {
+ if (activity instanceof FinishAction) {
+ ((FinishAction) activity).doFinish();
+ }
+ }
+
+ private final Runnable showFailureRunnable = new Runnable() {
+
+ private int numOfRetries;
+
+ @Override
+ public void run() {
+ final Activity activity = uiInfo.getCurrentActivity();
+
+ if (!uiInfo.isAppVisible()) {
+ FeedbackModule.notifySendCompleted(Result.FAIL);
+ return;
+ } else if (null == activity) {
+ if (numOfRetries < 3) {
+ numOfRetries++;
+ uiNotifier.postDelayed(this, 200);
+ } else {
+ FeedbackModule.notifySendCompleted(Result.FAIL);
+ }
+ return;
+ }
+
+ // If the code has reached here, then the activity is visible.
+ FeedbackModule.notifySendCompleted(Result.FAIL);
+
+ final Snackbar snackbar = SnackbarBuilder.build(activity, R.string.mk_fb_feedback_failed);
+ final SnackbarCallback callback = new SnackbarCallback();
+ snackbar.addCallback(callback);
+ snackbar.setAction(R.string.mk_fb_retry, new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ FeedbackModule.sendFeedback(message, email);
+ // We remove the callback here so we don't release the screenshot
+ snackbar.removeCallback(callback);
+
+ showProgressBar(activity);
+
+ // We have to handle any notifications that the callback would have.
+ FeedbackModule.notificationDismissed();
+ }
+ });
+
+ // Notify Listeners Early of intent
+ FeedbackModule.notificationStarted();
+ dismissProgressBar(activity);
+
+ snackbar.show();
+ }
+ };
+
+ private final Runnable showSuccessRunnable = new Runnable() {
+
+ private int numOfRetries;
+
+ @Override
+ public void run() {
+ final Activity activity = uiInfo.getCurrentActivity();
+
+ if (!uiInfo.isAppVisible()) {
+ FeedbackModule.notifySendCompleted(Result.SUCCESS);
+ return;
+ } else if (null == activity) {
+ if (numOfRetries < 3) {
+ numOfRetries++;
+ uiNotifier.postDelayed(this, 200);
+ } else {
+ FeedbackModule.notifySendCompleted(Result.SUCCESS);
+ }
+ return;
+ }
+
+ // If the code has reached here, then the activity is visible.
+ FeedbackModule.notifySendCompleted(Result.SUCCESS);
+
+ final Snackbar snackbar = SnackbarBuilder.build(activity, R.string.mk_fb_feedback_sent);
+ snackbar.addCallback(new SnackbarCallback() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+ doFinish(activity);
+ }
+ });
+
+ // Notify Listeners Early of intent
+ FeedbackModule.notificationStarted();
+ dismissProgressBar(activity);
+
+ snackbar.show();
+ }
+ };
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/AbstractCommand.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/AbstractCommand.java
new file mode 100644
index 0000000..b97801f
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/AbstractCommand.java
@@ -0,0 +1,45 @@
+package com.atlassian.mobilekit.module.feedback.commands;
+
+
+import android.os.Looper;
+
+import com.atlassian.mobilekit.module.core.Command;
+import com.atlassian.mobilekit.module.core.Receiver;
+import com.atlassian.mobilekit.module.core.UiNotifier;
+import com.atlassian.mobilekit.module.core.UiReceiver;
+
+abstract class AbstractCommand implements Command {
+
+ private final Receiver receiver;
+ private final UiNotifier uiNotifier;
+
+ AbstractCommand(Receiver receiver, UiNotifier uiNotifier) {
+ this.receiver = receiver;
+ this.uiNotifier = uiNotifier;
+ }
+
+ void updateReceiver(final T data) {
+
+ if (receiver == null) {
+ return;
+ }
+
+ if (!(receiver instanceof UiReceiver) || isMainThread()) {
+ receiver.receive(data);
+ } else {
+ // Post runnable
+ uiNotifier.post(new Runnable() {
+ @Override
+ public void run() {
+ receiver.receive(data);
+ }
+ });
+ }
+ }
+
+ private boolean isMainThread() {
+ return Looper.getMainLooper().getThread() == Thread.currentThread();
+ }
+
+}
+
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/LoadFeedbackConfigCommand.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/LoadFeedbackConfigCommand.java
new file mode 100644
index 0000000..bcbdd0b
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/LoadFeedbackConfigCommand.java
@@ -0,0 +1,118 @@
+package com.atlassian.mobilekit.module.feedback.commands;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.atlassian.mobilekit.module.core.Receiver;
+import com.atlassian.mobilekit.module.core.UiNotifier;
+import com.atlassian.mobilekit.module.feedback.model.FeedbackConfig;
+import com.atlassian.mobilekit.module.feedback.R;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public final class LoadFeedbackConfigCommand extends AbstractCommand {
+
+ private static final String LOG_TAG = LoadFeedbackConfigCommand.class.getSimpleName();
+ private final Context context;
+
+ public LoadFeedbackConfigCommand(Context ctx,
+ Receiver receiver,
+ UiNotifier uiNotifier) {
+ super(receiver, uiNotifier);
+ context = ctx;
+ }
+
+
+ @Override
+ public void run() {
+
+ FeedbackConfig config = new FeedbackConfig(
+ context.getString(R.string.mp_feedback_host),
+ context.getString(R.string.mp_feedback_apikey),
+ context.getString(R.string.mp_feedback_projectkey),
+ context.getResources().getStringArray(R.array.mp_feedback_components)
+ );
+
+ String errMsg = errorCheck(config);
+ if (errMsg != null) {
+ // This will crash the app, so that developers can correct their code.
+ throw new IllegalStateException(errMsg);
+ }
+
+ updateReceiver(config);
+ }
+
+ private String errorCheck(FeedbackConfig config) {
+
+ StringBuilder errMsg = new StringBuilder();
+
+ if (TextUtils.isEmpty(config.getHost())) {
+ errMsg.append(getConfigEmptyErrMsg(R.string.mp_feedback_host));
+ } else if (!isValidHost(config.getHost())) {
+ errMsg.append(getConfigInvalidErrMsg(R.string.mp_feedback_host));
+ }
+
+ if (TextUtils.isEmpty(config.getApiKey())) {
+ errMsg.append(getConfigEmptyErrMsg(R.string.mp_feedback_apikey));
+ }
+
+ if (TextUtils.isEmpty(config.getProjectKey())) {
+ errMsg.append(getConfigEmptyErrMsg(R.string.mp_feedback_projectkey));
+ }
+
+ if (errMsg.length() > 0) {
+ errMsg.append(context.getString(R.string.mk_fb_config_err_help));
+ return errMsg.toString();
+ }
+
+ return null;
+ }
+
+ private String getConfigEmptyErrMsg(int resId) {
+ return context.getString(R.string.mk_fb_no_config_property,
+ context.getResources().getResourceEntryName(resId));
+ }
+
+ private String getConfigInvalidErrMsg(int resId) {
+ return context.getString(R.string.mk_fb_invalid_config_property,
+ context.getResources().getResourceEntryName(resId));
+ }
+
+ private boolean isValidHost(String input) {
+
+ if (TextUtils.isEmpty(input)) {
+ return false;
+ }
+
+ if (input.indexOf("/") >= 0) {
+ return false;
+ }
+
+ try {
+ URI uri = new URI("scheme://" + input);
+ String host = uri.getHost();
+ int port = uri.getPort();
+
+ if (TextUtils.isEmpty(host)) {
+ return false;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(host);
+
+ if (port != -1) {
+ sb.append(":").append(port);
+ }
+
+ return input.equals(sb.toString());
+
+ } catch (URISyntaxException use) {
+ Log.e(LOG_TAG, "URI Validation Failed.", use);
+ }
+
+ return false;
+ }
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/Result.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/Result.java
new file mode 100644
index 0000000..d5207c4
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/Result.java
@@ -0,0 +1,6 @@
+package com.atlassian.mobilekit.module.feedback.commands;
+
+
+public enum Result {
+ SUCCESS, FAIL
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/SendFeedbackCommand.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/SendFeedbackCommand.java
new file mode 100644
index 0000000..b83b5ec
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/commands/SendFeedbackCommand.java
@@ -0,0 +1,109 @@
+package com.atlassian.mobilekit.module.feedback.commands;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.atlassian.mobilekit.module.core.Receiver;
+import com.atlassian.mobilekit.module.core.UiNotifier;
+import com.atlassian.mobilekit.module.feedback.FeedbackDataProvider;
+import com.atlassian.mobilekit.module.feedback.JiraIssueType;
+import com.atlassian.mobilekit.module.feedback.model.CreateIssueRequest;
+import com.atlassian.mobilekit.module.feedback.model.CreateIssueResponse;
+import com.atlassian.mobilekit.module.feedback.network.JmcRestClient;
+import com.google.gson.Gson;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.RequestBody;
+import retrofit2.Call;
+import retrofit2.Response;
+
+public final class SendFeedbackCommand extends AbstractCommand {
+
+
+ private static final String LOG_TAG = SendFeedbackCommand.class.getSimpleName();
+
+ private final Map queryMap;
+ private final CreateIssueRequest.Builder requestBuilder;
+
+ private final JmcRestClient restClient;
+ private final FeedbackDataProvider feedbackDataProvider;
+
+ public SendFeedbackCommand(Map queryMap,
+ CreateIssueRequest.Builder requestBuilder,
+ FeedbackDataProvider feedbackDataProvider,
+ JmcRestClient restClient,
+ Receiver receiver,
+ UiNotifier uiNotifier) {
+
+ super(receiver, uiNotifier);
+ this.queryMap = queryMap;
+ this.requestBuilder = requestBuilder;
+ this.feedbackDataProvider = feedbackDataProvider;
+ this.restClient = restClient;
+ }
+
+ @Override
+ public void run() {
+
+ JiraIssueType issueType = JiraIssueType.TASK;
+
+ List customFieldsPart = new ArrayList<>();
+ if (feedbackDataProvider != null) {
+ final String appendDesc = feedbackDataProvider.getAdditionalDescription();
+ if (!TextUtils.isEmpty(appendDesc)) {
+ requestBuilder.appendToDescription(appendDesc);
+ }
+
+ final JiraIssueType typeFromProvider = feedbackDataProvider.getIssueType();
+ if (typeFromProvider != null) {
+ issueType = typeFromProvider;
+ }
+
+ final Map customFieldsData = feedbackDataProvider.getCustomFieldsData();
+ if(customFieldsData != null) {
+ RequestBody customFieldRequestBody =
+ RequestBody.create(MediaType.parse("application/json"), new Gson().toJson(customFieldsData));
+
+ MultipartBody.Part customFieldPart =
+ MultipartBody.Part.createFormData("customfields", "customfields.json", customFieldRequestBody);
+ customFieldsPart.add(customFieldPart);
+ }
+
+ }
+ requestBuilder.issueType(issueType.toString());
+
+ final CreateIssueRequest request = requestBuilder.build();
+
+ Call call = restClient.getJmcApi().createIssue(queryMap, request, Collections.emptyList(), customFieldsPart);
+ try {
+ Response response = call.execute();
+ Log.d(LOG_TAG, String.format("Response code %1$d\nmessage %2$s\nbody %3$s",
+ response.code(), response.message(), response.body()));
+
+ if (response.isSuccessful()) {
+ CreateIssueResponse body = response.body();
+ if (body == null) {
+ Log.e(LOG_TAG, "Bad api response. Empty body.");
+ } else if (TextUtils.isEmpty(body.getKey())) {
+ Log.e(LOG_TAG, "Bad api response. Missing Issue Key.");
+ } else {
+ Log.d(LOG_TAG, String.format("New Issue Created %s", body.getKey()));
+ updateReceiver(Result.SUCCESS);
+ return;
+ }
+ }
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG,"Failed to create new issue.", ioe);
+ }
+
+ updateReceiver(Result.FAIL);
+ }
+
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/CreateIssueRequest.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/CreateIssueRequest.java
new file mode 100644
index 0000000..cd56fac
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/CreateIssueRequest.java
@@ -0,0 +1,233 @@
+package com.atlassian.mobilekit.module.feedback.model;
+
+
+import android.text.TextUtils;
+
+import com.atlassian.mobilekit.module.core.utils.StringUtils;
+
+import java.util.List;
+
+import androidx.annotation.Keep;
+
+@Keep
+public final class CreateIssueRequest {
+
+ @Keep
+ public static class Builder {
+
+ private String type;
+ private String summary;
+ private String description;
+ private boolean isCrash;
+ private String udid;
+ private String uuid;
+
+ private String appName;
+ private String appId;
+ private String appVersion;
+
+ private String systemVersion;
+ private String systemName;
+ private String deviceName;
+ private String model;
+
+ private String language;
+ private List components;
+
+ public Builder() {
+
+ }
+
+ public Builder issueType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder summary(String summary) {
+ this.summary = summary;
+ return this;
+ }
+
+ public Builder description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder appendToDescription(String moreInfo) {
+ if (!TextUtils.isEmpty(description)) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(StringUtils.EOL).append(StringUtils.EOL).append(moreInfo);
+ description += sb.toString();
+ }
+ return this;
+ }
+
+ public Builder isCrash(boolean crash) {
+ isCrash = crash;
+ return this;
+ }
+
+ public Builder udid(String udid) {
+ this.udid = udid;
+ return this;
+ }
+
+ public Builder uuid(String uuid) {
+ this.uuid = uuid;
+ return this;
+ }
+
+ public Builder appName(String appName) {
+ this.appName = appName;
+ return this;
+ }
+
+ public Builder appId(String appId) {
+ this.appId = appId;
+ return this;
+ }
+
+ public Builder appVersion(String appVersion) {
+ this.appVersion = appVersion;
+ return this;
+ }
+
+ public Builder systemVersion(String systemVersion) {
+ this.systemVersion = systemVersion;
+ return this;
+ }
+
+ public Builder systemName(String systemName) {
+ this.systemName = systemName;
+ return this;
+ }
+
+ public Builder deviceName(String devName) {
+ this.deviceName = devName;
+ return this;
+ }
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder language(String language) {
+ this.language = language;
+ return this;
+ }
+
+ public Builder components(List components) {
+ this.components = components;
+ return this;
+ }
+
+ public CreateIssueRequest build() {
+ return new CreateIssueRequest(this);
+ }
+ }
+
+
+ private static final int MAX_SUMMARY_LENGTH = 240;
+ private final String type;
+ private final String summary;
+ private final String description;
+ private final boolean isCrash;
+ private final String udid;
+ private final String uuid;
+
+ private final String appName;
+ private final String appId;
+ private final String appVersion;
+
+ private final String systemVersion;
+ private final String systemName;
+
+ // This is actually DeviceName.
+ // *** But this declaration cannot be changed since the Server API expects it to be 'devName'
+ private final String devName;
+
+ private final String model;
+
+ private final String language;
+
+ private final List components;
+
+ public CreateIssueRequest(Builder builder) {
+ this.type = builder.type;
+ this.summary = StringUtils.ellipsize(builder.summary, MAX_SUMMARY_LENGTH);
+ this.description = builder.description;
+ this.isCrash = builder.isCrash;
+ this.udid = builder.udid;
+ this.uuid = builder.uuid;
+ this.appName = builder.appName;
+ this.appId = builder.appId;
+ this.appVersion = builder.appVersion;
+ this.systemVersion = builder.systemVersion;
+ this.systemName = builder.systemName;
+ this.devName = builder.deviceName;
+ this.model = builder.model;
+ this.language = builder.language;
+ this.components = builder.components;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public boolean isCrash() {
+ return isCrash;
+ }
+
+ public String getUdid() {
+ return udid;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public String getAppName() {
+ return appName;
+ }
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public String getAppVersion() {
+ return appVersion;
+ }
+
+ public String getSystemVersion() {
+ return systemVersion;
+ }
+
+ public String getSystemName() {
+ return systemName;
+ }
+
+ public String getDeviceName() {
+ return devName;
+ }
+
+ public String getModel() {
+ return model;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public List getComponents() {
+ return components;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/CreateIssueResponse.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/CreateIssueResponse.java
new file mode 100644
index 0000000..5ee5143
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/CreateIssueResponse.java
@@ -0,0 +1,52 @@
+package com.atlassian.mobilekit.module.feedback.model;
+
+
+import java.util.List;
+
+import androidx.annotation.Keep;
+
+@Keep
+public final class CreateIssueResponse {
+
+ private String key;
+ private String status;
+ private String summary;
+ private String description;
+ private long dateUpdated;
+ private long dateCreated;
+ private boolean hasUpdates;
+ private List comments;
+
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public long getDateUpdated() {
+ return dateUpdated;
+ }
+
+ public long getDateCreated() {
+ return dateCreated;
+ }
+
+ public boolean hasUpdates() {
+ return hasUpdates;
+ }
+
+ public List getComments() {
+ return comments;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/FeedbackConfig.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/FeedbackConfig.java
new file mode 100644
index 0000000..f044cba
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/model/FeedbackConfig.java
@@ -0,0 +1,33 @@
+package com.atlassian.mobilekit.module.feedback.model;
+
+
+public final class FeedbackConfig {
+
+ private final String host;
+ private final String apiKey;
+ private final String projectKey;
+ private final String[] components;
+
+ public FeedbackConfig(String host, String apiKey, String projectKey, String[] components) {
+ this.host = host;
+ this.apiKey = apiKey;
+ this.projectKey = projectKey;
+ this.components = components;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public String getProjectKey() {
+ return projectKey;
+ }
+
+ public String[] getComponents() {
+ return components;
+ }
+}
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/BaseApiParams.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/BaseApiParams.java
new file mode 100644
index 0000000..fbe784d
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/BaseApiParams.java
@@ -0,0 +1,8 @@
+package com.atlassian.mobilekit.module.feedback.network;
+
+
+public interface BaseApiParams {
+
+ String API_KEY = "apikey";
+ String PROJECT = "project";
+}
\ No newline at end of file
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/JmcApi.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/JmcApi.java
new file mode 100644
index 0000000..18c775a
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/JmcApi.java
@@ -0,0 +1,28 @@
+package com.atlassian.mobilekit.module.feedback.network;
+
+
+import com.atlassian.mobilekit.module.feedback.model.CreateIssueRequest;
+import com.atlassian.mobilekit.module.feedback.model.CreateIssueResponse;
+
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.Keep;
+import okhttp3.MultipartBody;
+import retrofit2.Call;
+import retrofit2.http.Multipart;
+import retrofit2.http.POST;
+import retrofit2.http.Part;
+import retrofit2.http.QueryMap;
+
+public interface JmcApi {
+
+ @Multipart
+ @POST("rest/jconnect/latest/issue/create")
+ @Keep
+ Call createIssue(
+ @QueryMap Map params,
+ @Part("issue") CreateIssueRequest request,
+ @Part List screenshotPart,
+ @Part List customFields);
+}
\ No newline at end of file
diff --git a/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/JmcRestClient.java b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/JmcRestClient.java
new file mode 100644
index 0000000..2e36606
--- /dev/null
+++ b/feedback-android/src/main/java/com/atlassian/mobilekit/module/feedback/network/JmcRestClient.java
@@ -0,0 +1,34 @@
+package com.atlassian.mobilekit.module.feedback.network;
+
+
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+
+public final class JmcRestClient {
+
+ private JmcApi jmcApi = null;
+
+ public JmcRestClient() {
+
+ }
+
+ public void init(String protocol, String host) {
+
+ final String baseUrl = new StringBuilder()
+ .append(protocol)
+ .append(host)
+ .append("/")
+ .toString();
+
+ final Retrofit retrofit = new Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build();
+
+ jmcApi = retrofit.create(JmcApi.class);
+ }
+
+ public JmcApi getJmcApi() {
+ return jmcApi;
+ }
+}
diff --git a/feedback-android/src/main/res/drawable/feedback_button_text_state.xml b/feedback-android/src/main/res/drawable/feedback_button_text_state.xml
new file mode 100644
index 0000000..8d4eac8
--- /dev/null
+++ b/feedback-android/src/main/res/drawable/feedback_button_text_state.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feedback-android/src/main/res/drawable/ic_send.xml b/feedback-android/src/main/res/drawable/ic_send.xml
new file mode 100644
index 0000000..5ffa7f2
--- /dev/null
+++ b/feedback-android/src/main/res/drawable/ic_send.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/feedback-android/src/main/res/drawable/ic_send_disabled.xml b/feedback-android/src/main/res/drawable/ic_send_disabled.xml
new file mode 100644
index 0000000..fd52c41
--- /dev/null
+++ b/feedback-android/src/main/res/drawable/ic_send_disabled.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/feedback-android/src/main/res/drawable/send_selector.xml b/feedback-android/src/main/res/drawable/send_selector.xml
new file mode 100644
index 0000000..189e6e0
--- /dev/null
+++ b/feedback-android/src/main/res/drawable/send_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feedback-android/src/main/res/layout/activity_feedback.xml b/feedback-android/src/main/res/layout/activity_feedback.xml
new file mode 100644
index 0000000..f3f3889
--- /dev/null
+++ b/feedback-android/src/main/res/layout/activity_feedback.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feedback-android/src/main/res/layout/content_feedback.xml b/feedback-android/src/main/res/layout/content_feedback.xml
new file mode 100644
index 0000000..dd456a5
--- /dev/null
+++ b/feedback-android/src/main/res/layout/content_feedback.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feedback-android/src/main/res/layout/mk_feedback_dialog_container.xml b/feedback-android/src/main/res/layout/mk_feedback_dialog_container.xml
new file mode 100644
index 0000000..7efe30c
--- /dev/null
+++ b/feedback-android/src/main/res/layout/mk_feedback_dialog_container.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feedback-android/src/main/res/layout/mk_feedback_dialog_content.xml b/feedback-android/src/main/res/layout/mk_feedback_dialog_content.xml
new file mode 100644
index 0000000..ce19a1f
--- /dev/null
+++ b/feedback-android/src/main/res/layout/mk_feedback_dialog_content.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/feedback-android/src/main/res/layout/mk_feedback_dialog_title.xml b/feedback-android/src/main/res/layout/mk_feedback_dialog_title.xml
new file mode 100644
index 0000000..f32d259
--- /dev/null
+++ b/feedback-android/src/main/res/layout/mk_feedback_dialog_title.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/feedback-android/src/main/res/menu/menu_feedback.xml b/feedback-android/src/main/res/menu/menu_feedback.xml
new file mode 100644
index 0000000..febfc2b
--- /dev/null
+++ b/feedback-android/src/main/res/menu/menu_feedback.xml
@@ -0,0 +1,10 @@
+
diff --git a/feedback-android/src/main/res/values-w820dp/dimens.xml b/feedback-android/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/feedback-android/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/feedback-android/src/main/res/values/colors.xml b/feedback-android/src/main/res/values/colors.xml
new file mode 100644
index 0000000..90957de
--- /dev/null
+++ b/feedback-android/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #5a697c
+ #A5ADBA
+ #0065FF
+
\ No newline at end of file
diff --git a/feedback-android/src/main/res/values/dimens.xml b/feedback-android/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..5e6f372
--- /dev/null
+++ b/feedback-android/src/main/res/values/dimens.xml
@@ -0,0 +1,13 @@
+
+
+ 16dp
+ 16dp
+
+ 24dp
+ 24dp
+ 20dp
+ 24dp
+
+ 8dp
+
+
diff --git a/feedback-android/src/main/res/values/fonts.xml b/feedback-android/src/main/res/values/fonts.xml
new file mode 100644
index 0000000..e062ecd
--- /dev/null
+++ b/feedback-android/src/main/res/values/fonts.xml
@@ -0,0 +1,4 @@
+
+
+ sans-serif
+
\ No newline at end of file
diff --git a/feedback-android/src/main/res/values/mp_feedback_config.xml b/feedback-android/src/main/res/values/mp_feedback_config.xml
new file mode 100644
index 0000000..24247e1
--- /dev/null
+++ b/feedback-android/src/main/res/values/mp_feedback_config.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+ - Android
+
+
diff --git a/feedback-android/src/main/res/values/strings.xml b/feedback-android/src/main/res/values/strings.xml
new file mode 100644
index 0000000..19ce774
--- /dev/null
+++ b/feedback-android/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+
+ \'%s\' configuration property value is empty.
+ Cannot create feedback. Please check mp_feedback_config.xml in your application resources.
+ \'%s\' configuration property value is invalid.
+
+
+ Report an issue
+ Send
+ Tell us something before sending.
+
+ Feedback sent
+ Sending feedback failed
+ Retry
+
+ Your device is offline.
+ Invalid email address
+ Sending…
+ Found an issue in the COVIDSafe app?
+ Please describe an issue
+ Email address (Required)
+ We may reach out to you for further details about your feedback.\nYour email address won`t be used for any other purpose.
+
+
diff --git a/feedback-android/src/main/res/values/styles.xml b/feedback-android/src/main/res/values/styles.xml
new file mode 100644
index 0000000..8f0e556
--- /dev/null
+++ b/feedback-android/src/main/res/values/styles.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..b54d1db
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,45 @@
+## For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+#Mon Apr 06 10:10:18 AEST 2020
+android.useAndroidX=true
+PUSH_NOTIFICATION_ID=771578
+MAX_SCAN_INTERVAL=43000
+ORG="AU_DTA"
+ADVERTISING_DURATION=180000
+PROTOCOL_VERSION=1
+BLACKLIST_DURATION=100000
+BM_CHECK_INTERVAL=540000
+MAX_QUEUE_TIME=7000
+org.gradle.jvmargs=-Xmx2048M
+PUSH_NOTIFICATION_CHANNEL_NAME="COVIDSafe Notifications"
+MIN_SCAN_INTERVAL=36000
+PRODUCTION_SERVICE_UUID="B82AB3FC-1595-4F6A-80F0-FE094CC218F9"
+SCAN_DURATION=8000
+SERVICE_FOREGROUND_CHANNEL_NAME="Foreground Service"
+SERVICE_FOREGROUND_CHANNEL_ID="COVIDSafe Updates"
+SERVICE_FOREGROUND_NOTIFICATION_ID=771579
+
+STAGING_SERVICE_UUID="CC0AC8B7-03B5-4252-8D84-44D199E16065"
+CONNECTION_TIMEOUT=6000
+HEALTH_CHECK_INTERVAL=900000
+android.enableJetifier=true
+ADVERTISING_INTERVAL=5000
+
+TEST_BASE_URL="https://device-api.uat.unp.aws.covidsafe.gov.au"
+STAGING_BASE_URL="https://device-api.uat.unp.aws.covidsafe.gov.au"
+PROD_BASE_URL="https://device-api.prod.lp.aws.covidsafe.gov.au"
+
+
+TEST_END_POINT_PREFIX="/uat"
+STAGING_END_POINT_PREFIX="/uat"
+PRODUCTION_END_POINT_PREFIX="/prod"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..642df7f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Jan 27 05:51:54 SGT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# 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
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# 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
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+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" -a "$nonstop" = "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
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@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
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@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=
+
+@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 Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_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=%*
+
+: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/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..7930890
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':app',':feedback-android'
+rootProject.name='Tracer'