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 @@ + + + + + + + + + + + + + + +