From 3b77cc31e58b9db2514bb06cc27af67aa1a8da6b Mon Sep 17 00:00:00 2001 From: COVIDSafe Support <64945427+covidsafe-support@users.noreply.github.com> Date: Tue, 26 May 2020 17:19:36 +1000 Subject: [PATCH] COVIDSafe code from version 1.0.18 (#2) --- README.md | 2 +- app/.gitignore | 1 + app/build.gradle | 35 +- .../streetpass/persistence/DBUtilityTest.kt | 47 + app/src/debug/AndroidManifest.xml | 16 + .../au/gov/health/covidsafe/PeekActivity.kt | 52 +- app/src/main/AndroidManifest.xml | 23 - app/src/main/assets/spinner_migrating_db.json | 5514 +++++++++++++++++ .../au/gov/health/covidsafe/LocalBlobV2.kt | 3 + .../au/gov/health/covidsafe/PlotActivity.kt | 231 - .../gov/health/covidsafe/RecordListAdapter.kt | 101 +- .../covidsafe/SelfIsolationDoneActivity.kt | 35 - .../au/gov/health/covidsafe/SplashActivity.kt | 57 +- .../health/covidsafe/bluetooth/gatt/GATT.kt | 13 +- .../covidsafe/bluetooth/gatt/GattServer.kt | 22 +- .../services/BluetoothMonitoringService.kt | 66 +- .../streetpass/ConnectablePeripheral.kt | 14 +- .../streetpass/StreetPassPairingFix.kt | 177 + .../covidsafe/streetpass/StreetPassWorker.kt | 37 +- .../streetpass/persistence/Encryption.kt | 158 + .../persistence/StreetPassRecord.kt | 19 +- .../persistence/StreetPassRecordDatabase.kt | 135 +- .../view/StreetPassRecordViewModel.kt | 10 +- .../health/covidsafe/ui/PagerChildFragment.kt | 8 + .../health/covidsafe/ui/home/HomeFragment.kt | 130 +- .../ui/onboarding/OnboardingActivity.kt | 26 +- .../PermissionDeviceNameFragment.kt | 68 + .../fragment/permission/PermissionFragment.kt | 4 +- .../PermissionSuccessFragment.kt | 11 +- .../covidsafe/ui/splash/SplashViewModel.kt | 85 + .../presentation/UploadFinishedFragment.kt | 8 + .../presentation/UploadInitialFragment.kt | 11 +- .../presentation/UploadStepFourFragment.kt | 7 +- .../presentation/VerifyUploadPinFragment.kt | 4 + .../main/res/layout/activity_onboarding.xml | 14 +- app/src/main/res/layout/activity_splash.xml | 28 +- app/src/main/res/layout/database_peek.xml | 44 +- app/src/main/res/layout/fragment_help.xml | 1 + .../fragment_home_setup_complete_header.xml | 56 +- ...fragment_home_setup_incomplete_content.xml | 2 +- .../fragment_permission_device_name.xml | 79 + .../layout/fragment_permission_success.xml | 1 + .../res/layout/fragment_upload_finished.xml | 1 + .../res/layout/fragment_upload_initial.xml | 1 + .../res/layout/fragment_upload_master.xml | 3 +- .../res/layout/fragment_upload_page_4.xml | 4 +- .../res/layout/fragment_verify_upload_pin.xml | 1 + .../main/res/navigation/nav_onboarding.xml | 16 +- app/src/main/res/values/strings.xml | 53 +- app/src/main/res/xml/provider_paths.xml | 1 + feedback-android/build.gradle | 2 +- gradle.properties | 10 +- 52 files changed, 6784 insertions(+), 663 deletions(-) create mode 100644 app/src/androidTest/java/au/gov/health/covidsafe/streetpass/persistence/DBUtilityTest.kt rename app/src/{main => debug}/java/au/gov/health/covidsafe/PeekActivity.kt (76%) create mode 100644 app/src/main/assets/spinner_migrating_db.json create mode 100644 app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt delete mode 100644 app/src/main/java/au/gov/health/covidsafe/PlotActivity.kt delete mode 100644 app/src/main/java/au/gov/health/covidsafe/SelfIsolationDoneActivity.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/splash/SplashViewModel.kt create mode 100644 app/src/main/res/layout/fragment_permission_device_name.xml diff --git a/README.md b/README.md index 12aa027..2cf13c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # COVIDSafe app -# Please report any security vulnerabilities using the details from [https://covidsafe.gov.au/.well-known/security.txt](https://covidsafe.gov.au/.well-known/security.txt) +# Please report any security vulnerabilities using the details from [https://covidsafe.gov.au/.well-known/security.txt](https://covidsafe.gov.au/.well-known/security.txt) # [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: diff --git a/app/.gitignore b/app/.gitignore index 796b96d..3f2a4d9 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ /build +/schemas diff --git a/app/build.gradle b/app/build.gradle index 941333b..3c12914 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,16 +35,25 @@ android { applicationId "au.gov.health.covidsafe" resValue "string", "build_config_package", "au.gov.health.covidsafe" minSdkVersion 23 - targetSdkVersion 29 - versionCode 17 - versionName "1.0.17" + /* + TargetSdk is currently set to 28 because we are using a greylisted api in SDK 29 + Before you increase the targetSdkVersion make sure that all its usage are still working + */ + targetSdkVersion 28 + versionCode 18 + versionName "1.0.18" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + javaCompileOptions { annotationProcessorOptions { - arguments = ["room.schemaLocation": - "$projectDir/schemas".toString()] + arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + sourceSets { + androidTest { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } } @@ -91,6 +100,7 @@ android { buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX buildConfigField "String", "BASE_URL", TEST_BASE_URL buildConfigField "String", "IOS_BACKGROUND_UUID", DEBUG_BACKGROUND_IOS_SERVICE_UUID + buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", DEBUG_ENCRYPTION_PUBLIC_KEY String ssid = STAGING_SERVICE_UUID @@ -106,6 +116,8 @@ android { buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX buildConfigField "String", "BASE_URL", STAGING_BASE_URL buildConfigField "String", "IOS_BACKGROUND_UUID", STAGING_BACKGROUND_IOS_SERVICE_UUID + buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", STAGING_ENCRYPTION_PUBLIC_KEY + @@ -130,6 +142,8 @@ android { buildConfigField "String", "END_POINT_PREFIX", PRODUCTION_END_POINT_PREFIX buildConfigField "String", "BASE_URL", PROD_BASE_URL buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID + buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", PRODUCTION_ENCRYPTION_PUBLIC_KEY + debuggable false @@ -150,6 +164,14 @@ android { } } + sourceSets { + staging { + java.srcDirs = ['src/debug/java'] + res.srcDirs = ['src/debug/res'] + manifest.srcFile 'src/debug/AndroidManifest.xml' + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -219,4 +241,7 @@ dependencies { 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' + + androidTestImplementation "androidx.room:room-testing:2.2.5" + } diff --git a/app/src/androidTest/java/au/gov/health/covidsafe/streetpass/persistence/DBUtilityTest.kt b/app/src/androidTest/java/au/gov/health/covidsafe/streetpass/persistence/DBUtilityTest.kt new file mode 100644 index 0000000..bb236a3 --- /dev/null +++ b/app/src/androidTest/java/au/gov/health/covidsafe/streetpass/persistence/DBUtilityTest.kt @@ -0,0 +1,47 @@ +package au.gov.health.covidsafe.streetpass.persistence + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Rule +import org.junit.Test +import java.io.IOException + +/** + * This test class is used as a util to revert the actual db version and to populate it with version one record in order to test the migrations + */ +class DBUtilityTest { + private val ACTUAL_DB = "record_database" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + StreetPassRecordDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + @Throws(IOException::class) + fun revertDbToVersion1() { + helper.createDatabase(ACTUAL_DB, 1).apply { + close() + } + } + + @Test + @Throws(IOException::class) + fun populateVersion1Db() { + var db = helper.createDatabase(ACTUAL_DB, 1).apply { + // db has schema version 1. insert some data using SQL queries. + // We cannot use DAO classes because they expect the latest schema. + for (i in 1..1000) { + val insertSql = """INSERT INTO record_table values (?,?,?,?,?,?,?,?,?)""".trimIndent() + + execSQL(insertSql, arrayOf(i, System.currentTimeMillis(), 1, "testMessage", "AU_DTA", "modelP", "modelC", i, i)) + + } + close() + } + + } +} \ No newline at end of file diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 91bf3ec..8d43b5f 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -10,6 +10,22 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/MyTheme.DayNight"> + + + + + + + diff --git a/app/src/main/java/au/gov/health/covidsafe/PeekActivity.kt b/app/src/debug/java/au/gov/health/covidsafe/PeekActivity.kt similarity index 76% rename from app/src/main/java/au/gov/health/covidsafe/PeekActivity.kt rename to app/src/debug/java/au/gov/health/covidsafe/PeekActivity.kt index bf2bf8d..a4d9320 100644 --- a/app/src/main/java/au/gov/health/covidsafe/PeekActivity.kt +++ b/app/src/debug/java/au/gov/health/covidsafe/PeekActivity.kt @@ -6,18 +6,23 @@ import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage +import au.gov.health.covidsafe.streetpass.view.RecordViewModel 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 +import kotlinx.android.synthetic.main.database_peek.* +import java.io.File +private const val TAG = "PeekActivity" class PeekActivity : AppCompatActivity() { @@ -47,20 +52,6 @@ class PeekActivity : AppCompatActivity() { 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 { @@ -106,11 +97,30 @@ class PeekActivity : AppCompatActivity() { } - val plot = findViewById(R.id.plot) - plot.setOnClickListener { view -> - val intent = Intent(this, PlotActivity::class.java) - intent.putExtra("time_period", nextTimePeriod()) - startActivity(intent) + + shareDatabase.setOnClickListener { + val authority = "${BuildConfig.APPLICATION_ID}.fileprovider" + val databaseFilePath= getDatabasePath("record_database").absolutePath + val databaseFile = File(databaseFilePath) + + CentralLog.d(TAG, "authority = $authority, databaseFilePath = $databaseFilePath") + + if(databaseFile.exists()) { + CentralLog.d(TAG, "databaseFile.length = ${databaseFile.length()}") + + FileProvider.getUriForFile( + this, + authority, + databaseFile + )?.let { databaseFileUri -> + CentralLog.d(TAG, "databaseFileUri = $databaseFileUri") + + val intent = Intent(Intent.ACTION_SEND) + intent.type = "application/octet-stream" + intent.putExtra(Intent.EXTRA_STREAM, databaseFileUri) + startActivity(Intent.createChooser(intent, "Sharing database")) + } + } } if(!BuildConfig.DEBUG) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 32b7112..ac27238 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,16 +27,6 @@ android:theme="@style/MyTheme.DayNight" android:networkSecurityConfig="@xml/network_security_config"> - - - - - @@ -75,16 +62,6 @@ - - - - diff --git a/app/src/main/assets/spinner_migrating_db.json b/app/src/main/assets/spinner_migrating_db.json new file mode 100644 index 0000000..c58ba42 --- /dev/null +++ b/app/src/main/assets/spinner_migrating_db.json @@ -0,0 +1,5514 @@ +{ + "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": [ + 228.5, + 120, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 228.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": [ + [ + 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": [ + 0.3137254901960784, + 0.3176470588235294, + 0.3176470588235294, + 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": 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": [ + 11.75, + 120, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 11.75, + 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": [ + 0.3137254901960784, + 0.3176470588235294, + 0.3176470588235294, + 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": 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": 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.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": [ + 0.3137254901960784, + 0.3176470588235294, + 0.3176470588235294, + 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": [ + 0.3137254901960784, + 0.3176470588235294, + 0.3176470588235294, + 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": [ + 0.3137254901960784, + 0.3176470588235294, + 0.3176470588235294, + 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": "Pre-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/java/au/gov/health/covidsafe/LocalBlobV2.kt b/app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt new file mode 100644 index 0000000..e6a6b71 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt @@ -0,0 +1,3 @@ +package au.gov.health.covidsafe + +class LocalBlobV2(val modelP : String?, val modelC : String?, val txPower : Int?, val rssi : Int?) \ 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 deleted file mode 100644 index a8febf1..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/PlotActivity.kt +++ /dev/null @@ -1,231 +0,0 @@ -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/RecordListAdapter.kt b/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt index 616c331..aa47076 100644 --- a/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt +++ b/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt @@ -16,12 +16,6 @@ class RecordListAdapter internal constructor(context: 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) @@ -59,78 +53,16 @@ class RecordListAdapter internal constructor(context: Context) : 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 setRecords(records: List) { + this.records = records + notifyDataSetChanged() } - 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) - } + internal fun setSourceData(records: List) { + this.sourceData = records + setRecords(prepareViewData(this.sourceData)) } private fun prepareViewData(words: List): List { @@ -144,27 +76,6 @@ class RecordListAdapter internal constructor(context: Context) : } } - 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 deleted file mode 100644 index d3cf99e..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/SelfIsolationDoneActivity.kt +++ /dev/null @@ -1,35 +0,0 @@ -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 index db781bc..acb16e6 100644 --- a/app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt +++ b/app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt @@ -1,24 +1,29 @@ 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 android.view.View.GONE +import android.view.View.VISIBLE import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import au.gov.health.covidsafe.ui.onboarding.OnboardingActivity +import au.gov.health.covidsafe.ui.splash.SplashNavigationEvent +import au.gov.health.covidsafe.ui.splash.SplashViewModel +import au.gov.health.covidsafe.ui.splash.SplashViewModelFactory +import kotlinx.android.synthetic.main.activity_splash.* import java.util.* class SplashActivity : AppCompatActivity() { - private val SPLASH_TIME: Long = 2000 + private lateinit var viewModel: SplashViewModel 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?) { @@ -26,10 +31,21 @@ class SplashActivity : AppCompatActivity() { setContentView(R.layout.activity_splash) hideSystemUI() mHandler = Handler() + viewModel = ViewModelProvider(this, SplashViewModelFactory(this)).get(SplashViewModel::class.java) - Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver, - Settings.Secure.ANDROID_ID)) + Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID)) + viewModel.splashNavigationLiveData.observe(this, Observer { + when (it) { + is SplashNavigationEvent.GoToNextScreen -> goToNextScreen() + is SplashNavigationEvent.ShowMigrationScreen -> migrationScreen() + } + }) + } + + override fun onStart() { + super.onStart() + viewModel.setupUI() } override fun onPause() { @@ -37,30 +53,23 @@ class SplashActivity : AppCompatActivity() { mHandler.removeCallbacksAndMessages(null) } - override fun onResume() { - super.onResume() - if (!updateFlag) { - mHandler.postDelayed({ - goToNextScreen() - finish() - }, SPLASH_TIME) - } + private fun migrationScreen() { + splash_screen_logo.setImageResource(R.drawable.ic_logo_home_inactive) + splash_screen_logo.setAnimation("spinner_migrating_db.json") + splash_screen_logo.playAnimation() + splash_migration_text.visibility = VISIBLE + help_stop_covid.visibility = GONE } 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 })) + viewModel.splashNavigationLiveData.removeObservers(this) + viewModel.release() + finish() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -79,4 +88,6 @@ class SplashActivity : AppCompatActivity() { 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/bluetooth/gatt/GATT.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/gatt/GATT.kt index e6f8aa8..7f78497 100644 --- 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 @@ -1,9 +1,8 @@ package au.gov.health.covidsafe.bluetooth.gatt +import au.gov.health.covidsafe.BuildConfig 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" @@ -20,12 +19,12 @@ const val ACTION_DEVICE_PROCESSED = "${BuildConfig.APPLICATION_ID}.ACTION_DEVICE 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 v: Int, + val msg: String, + val org: String, + modelP: String? ) { - val modelP = peripheral.modelP + val modelP = modelP ?: "" fun getPayload(): ByteArray { return gson.toJson(this).toByteArray(Charsets.UTF_8) 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 index 86ac409..34106d6 100644 --- 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 @@ -9,6 +9,9 @@ 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 au.gov.health.covidsafe.streetpass.persistence.Encryption +import com.google.gson.Gson +import com.google.gson.GsonBuilder import java.util.* import kotlin.properties.Delegates @@ -20,6 +23,9 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) { private var serviceUUID: UUID by Delegates.notNull() var bluetoothGattServer: BluetoothGattServer? = null + val gson: Gson = GsonBuilder().disableHtmlEscaping().create() + + init { bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager this.serviceUUID = UUID.fromString(serviceUUIDString) @@ -69,12 +75,18 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) { if (serviceUUID == characteristic?.uuid) { if (Utils.bmValid(context)) { + val peripheral = TracerApp.asPeripheralDevice() + val readRequest = ReadRequestEncryptedPayload(peripheral.modelP, + TracerApp.thisDeviceMsg()) + val plainRecord = gson.toJson(readRequest) + val plainRecordByteArray = plainRecord.toByteArray(Charsets.UTF_8) + val remoteBlob = Encryption.encryptPayload(plainRecordByteArray) val base = readPayloadMap.getOrPut(device.address, { ReadRequestPayload( - v = TracerApp.protocolVersion, - msg = TracerApp.thisDeviceMsg(), - org = TracerApp.ORG, - peripheral = TracerApp.asPeripheralDevice() + v = TracerApp.protocolVersion, + msg = remoteBlob, + org = TracerApp.ORG, + modelP = null //This is going to be stored as empty in the db as DUMMY value ).getPayload() }) @@ -114,6 +126,8 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) { } + inner class ReadRequestEncryptedPayload (val modelP : String, val msg: String) + override fun onCharacteristicWriteRequest( device: BluetoothDevice?, 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 index 8779923..f734465 100644 --- a/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt +++ b/app/src/main/java/au/gov/health/covidsafe/services/BluetoothMonitoringService.kt @@ -11,9 +11,7 @@ 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.* 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 @@ -31,8 +29,17 @@ 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.Encryption import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_DEVICE +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_RSSI +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_TXPOWER +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.ENCRYPTED_EMPTY_DICT +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.VERSION_ONE import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage +import com.google.gson.Gson +import com.google.gson.GsonBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -40,6 +47,7 @@ import kotlinx.coroutines.launch import pub.devrel.easypermissions.EasyPermissions import java.lang.ref.WeakReference import kotlin.coroutines.CoroutineContext + @Keep class BluetoothMonitoringService : LifecycleService(), CoroutineScope { @@ -75,6 +83,9 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { private val awsClient = NetworkFactory.awsClient + private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() + + /** Defines callbacks for service binding, passed to bindService() */ private val connection = object : ServiceConnection { @@ -550,13 +561,13 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { override fun onReceive(context: Context, intent: Intent) { if (ACTION_RECEIVED_STREETPASS == intent.action) { - val connRecord: ConnectionRecord = intent.getParcelableExtra(STREET_PASS) + val connRecord: ConnectionRecord? = intent.getParcelableExtra(STREET_PASS) CentralLog.d( TAG, "StreetPass received: $connRecord" ) - if (connRecord.msg.isNotEmpty()) { + if (connRecord != null && connRecord.msg.isNotEmpty()) { if (mBound) { val proximity = mService.proximity @@ -567,23 +578,44 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { ) } + val remoteBlob: String = if (connRecord.version == VERSION_ONE) { + with(receiver = connRecord) { + val plainRecordByteArray = gson.toJson(StreetPassRecordDatabase.Companion.EncryptedRecord( + peripheral.modelP, central.modelC, rssi, txPower, msg = msg)) + .toByteArray(Charsets.UTF_8) + Encryption.encryptPayload(plainRecordByteArray) + } + } else { + //For version after version 1, the message is already encrypted in msg and we can store it as remote BLOB + connRecord.msg + } + val localBlob : String = if (connRecord.version == VERSION_ONE) { + ENCRYPTED_EMPTY_DICT + } else { + with (receiver = connRecord) { + val modelP = if (DUMMY_DEVICE == peripheral.modelP) null else peripheral.modelP + val modelC = if (DUMMY_DEVICE == central.modelC) null else central.modelC + val rssi = if (rssi == DUMMY_RSSI) null else rssi + val txPower = if (txPower == DUMMY_TXPOWER) null else txPower + val plainLocalBlob = gson.toJson(LocalBlobV2(modelP, modelC, rssi, txPower)) + .toByteArray(Charsets.UTF_8) + Encryption.encryptPayload(plainLocalBlob) + } + } + val record = StreetPassRecord( - v = connRecord.version, - msg = connRecord.msg, + v = if (connRecord.version == 1) TracerApp.protocolVersion else (connRecord.version), org = connRecord.org, - modelP = connRecord.peripheral.modelP, - modelC = connRecord.central.modelC, - rssi = connRecord.rssi, - txPower = connRecord.txPower + localBlob = localBlob, + remoteBlob = remoteBlob ) + launch { + CentralLog.d( + TAG, + "Coroutine - Saving StreetPassRecord: ${Utils.getDate(record.timestamp)} $record") - launch{ - CentralLog.d( - TAG, - "Coroutine - Saving StreetPassRecord: ${Utils.getDate(record.timestamp)} $record") - - streetPassRecordStorage.saveRecord(record) + streetPassRecordStorage.saveRecord(record) } } } 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 index d715424..6f5bb0d 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/ConnectablePeripheral.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/ConnectablePeripheral.kt @@ -23,16 +23,16 @@ data class CentralDevice( @Parcelize data class ConnectionRecord( - val version: Int, + val version: Int, - val msg: String, - val org: String, + val msg: String, + val org: String, - val peripheral: PeripheralDevice, - val central: CentralDevice, + val peripheral: PeripheralDevice, + val central: CentralDevice, - var rssi: Int, - var txPower: Int? + 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/StreetPassPairingFix.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt new file mode 100644 index 0000000..4217163 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassPairingFix.kt @@ -0,0 +1,177 @@ +package au.gov.health.covidsafe.streetpass + +import android.bluetooth.BluetoothGatt +import android.content.pm.ApplicationInfo +import android.os.Build +import au.gov.health.covidsafe.logging.CentralLog +import java.lang.NullPointerException +import java.lang.RuntimeException +import java.lang.reflect.Field + + +object StreetPassPairingFix { + private const val TAG = "StreetPassPairingFix" + private var initFailed = false + private var initComplete = false + + private var bluetoothGattClass = BluetoothGatt::class.java + + private var mAuthRetryStateField: Field? = null + private var mAuthRetryField: Field? = null + + /** + * Initialises all the reflection references used by bypassAuthenticationRetry + * + * This has been checked against the source of Android 10_r36 + * + * Returns true if object is in valid state + */ + @Synchronized + private fun tryInit(): Boolean { + // Check if function has already run and failed + if (initFailed || initComplete) { + return !initFailed + } + + // This technique works only up to Android P/API 28. This is due to mAuthRetryState being + // a greylisted non-SDK interface. + // See + // https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces + // https://android.googlesource.com/platform/frameworks/base/+/45d2c252b19c08bbd20acaaa2f52ae8518150169%5E%21/core/java/android/bluetooth/BluetoothGatt.java + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P && ApplicationInfo().targetSdkVersion > Build.VERSION_CODES.P) { + CentralLog.i(TAG, + "Failed to initialise: mAuthRetryState is in restricted grey-list post API 28") + initFailed = true + initComplete = true + return !initFailed + } + + CentralLog.i(TAG, "Initialising StreetPassParingFix fields") + try { + try { + // Get a reference to the mAuthRetryState + // This will throw NoSuchFieldException on older android, which is handled below + mAuthRetryStateField = bluetoothGattClass.getDeclaredField("mAuthRetryState") + CentralLog.i(TAG, "Found mAuthRetryState") + + } catch (e: NoSuchFieldException) { + // Prior to https://android.googlesource.com/platform/frameworks/base/+/3854e2267487ecd129bdd0711c6d9dfbf8f7ed0d%5E%21/#F0, + // And at least after Nougat (7), mAuthRetryField (a boolean) was used instead + // of mAuthRetryState + CentralLog.i(TAG, + "No mAuthRetryState on this device, trying for mAuthRetry") + + // This will throw NoSuchFieldException again on fail, which is handled below + mAuthRetryField = bluetoothGattClass.getDeclaredField("mAuthRetry") + CentralLog.i(TAG, "Found mAuthRetry") + + } + + // Should be good to go now + CentralLog.i(TAG, "Initialisation complete") + initComplete = true + initFailed = false + return !initFailed + + } catch (e: NoSuchFieldException) { + // One of the fields was missing - likely an API version issue + CentralLog.i(TAG, "Unable to find field while initialising: "+ e.message) + } catch (e: SecurityException) { + // Sandbox didn't like reflection + CentralLog.i(TAG, + "Encountered sandbox exception while initialising: " + e.message) + } catch (e: NullPointerException) { + // Probably accessed an instance field as a static + CentralLog.i(TAG, "Encountered NPE while initialising: " + e.message) + } catch (e: RuntimeException) { + // For any other undocumented exception we just want to fail silentely + CentralLog.i(TAG, "Encountered Exception while initialising: " + e.message) + } + + // If this point is reached the initialisation has failed + CentralLog.i(TAG, + "Failed to initialise, bypassAuthenticationRetry will quietly fail") + initComplete = true + initFailed = true + + return !initFailed + } + + /** + * This function will attempt to bypass the conditionals in BluetoothGatt.mBluetoothGattCallback + * that cause bonding to occur. + * + * The function will fail silently if any errors occur during initialisation or patching. + * + * See + * https://android.googlesource.com/platform/frameworks/base/+/76c1d9d5e15f48e54fc810c3efb683a0c5fd14b0/core/java/android/bluetooth/BluetoothGatt.java#367 + * for an example of the conditional that is bypassed + */ + @Synchronized + fun bypassAuthenticationRetry(gatt: BluetoothGatt) { + if (!tryInit()) { + // Class failed to initialised correctly, return quietly + return + } + + try { + // Attempt the bypass for newer android + if (mAuthRetryStateField != null) { + CentralLog.i(TAG, "Attempting to bypass mAuthRetryState bonding conditional") + // Set the field accessible (if required) + val mAuthRetryStateAccessible = mAuthRetryStateField!!.isAccessible + if (!mAuthRetryStateAccessible) { + mAuthRetryStateField!!.isAccessible = true + } + + // The conditional branch that causes binding to occur in BluetoothGatt do not occur + // if mAuthRetryState == AUTH_RETRY_STATE_MITM (int 2), as this signifies that both + // steps of authenticated/encrypted reading have failed to establish. See + // https://android.googlesource.com/platform/frameworks/base/+/76c1d9d5e15f48e54fc810c3efb683a0c5fd14b0/core/java/android/bluetooth/BluetoothGatt.java#70 + // + // Previously this class reflectively read the value of AUTH_RETRY_STATE_MITM, + // instead of using a constant, but reportedly this doesn't work API 27+. + // + // Write mAuthRetryState to this value so it appears that bonding has already failed + mAuthRetryStateField!!.setInt(gatt, 2) // Unwrap is safe + + // Reset accessibility + mAuthRetryStateField!!.isAccessible = mAuthRetryStateAccessible + } else { + CentralLog.i(TAG, "Attempting to bypass mAuthRetry bonding conditional") + // Set the field accessible (if required) + val mAuthRetryAccessible = mAuthRetryField!!.isAccessible + if (!mAuthRetryAccessible) { + mAuthRetryField!!.isAccessible = true + } + + // The conditional branch that causes binding to occur in BluetoothGatt do not occur + // if mAuthRetry == true, as this signifies an attempt was made to bind + // + // See https://android.googlesource.com/platform/frameworks/base/+/63b4f6f5db4d5ea0114d195a0f33970e7070f21b/core/java/android/bluetooth/BluetoothGatt.java#263 + // + // Write mAuthRetry to true so it appears that bonding has already failed + mAuthRetryField!!.setBoolean(gatt, true) + + // Reset accessibility + mAuthRetryField!!.isAccessible = mAuthRetryAccessible + } + + } catch (e: SecurityException) { + // Sandbox didn't like reflection + CentralLog.i(TAG, + "Encountered sandbox exception in bypassAuthenticationRetry: " + e.message) + } catch (e: IllegalArgumentException) { + // Either a bad field access or wrong type was read + CentralLog.i(TAG, + "Encountered argument exception in bypassAuthenticationRetry: " + e.message) + } catch (e: NullPointerException) { + // Probably accessed an instance field as a static + CentralLog.i(TAG, + "Encountered NPE in bypassAuthenticationRetry: " + e.message) + } catch (e: ExceptionInInitializerError) { + CentralLog.i(TAG, + "Encountered reflection in bypassAuthenticationRetry: " + e.message) + } + } +} 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 index ac3b9c7..f0f62b9 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassWorker.kt @@ -16,8 +16,16 @@ 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 au.gov.health.covidsafe.streetpass.persistence.Encryption + +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_DEVICE +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_RSSI +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_TXPOWER + +import com.google.gson.GsonBuilder import java.util.* import java.util.concurrent.PriorityBlockingQueue + @Keep class StreetPassWorker(val context: Context) { @@ -39,6 +47,9 @@ class StreetPassWorker(val context: Context) { private var currentPendingConnection: Work? = null private var localBroadcastManager: LocalBroadcastManager = LocalBroadcastManager.getInstance(context) + private val gson = GsonBuilder().disableHtmlEscaping().create() + + val onWorkTimeoutListener = object : Work.OnWorkTimeoutListener { override fun onWorkTimeout(work: Work) { @@ -485,6 +496,10 @@ class StreetPassWorker(val context: Context) { service?.let { val characteristic = service.getCharacteristic(serviceUUID) if (characteristic != null) { + // Attempt to prevent bonding should the StreetPass characteristic + // require authentication or encryption + StreetPassPairingFix.bypassAuthenticationRetry(gatt) + val readSuccess = gatt.readCharacteristic(characteristic) CentralLog.i( TAG, @@ -587,18 +602,30 @@ class StreetPassWorker(val context: Context) { 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 plainRecord = gson.toJson(EncryptedWriteRequestPayload( + thisCentralDevice.modelC, + work.connectable.rssi, + work.connectable.transmissionPower, + TracerApp.thisDeviceMsg())).toByteArray(Charsets.UTF_8) + val remoteBlob = Encryption.encryptPayload(plainRecord) val writedata = WriteRequestPayload( v = TracerApp.protocolVersion, - msg = TracerApp.thisDeviceMsg(), + msg = remoteBlob, org = TracerApp.ORG, - modelC = thisCentralDevice.modelC, - rssi = work.connectable.rssi, - txPower = work.connectable.transmissionPower + modelC = DUMMY_DEVICE, + rssi = DUMMY_RSSI, + txPower = DUMMY_TXPOWER ) characteristic.value = writedata.getPayload() + + // Attempt to prevent bonding should the StreetPass characteristic + // require authentication or encryption + StreetPassPairingFix.bypassAuthenticationRetry(gatt) + val writeSuccess = gatt.writeCharacteristic(characteristic) CentralLog.i( TAG, @@ -614,6 +641,8 @@ class StreetPassWorker(val context: Context) { } } + inner class EncryptedWriteRequestPayload(val modelC: String, val rssi: Int, val txPower: Int?, val msg : String) + override fun onCharacteristicWrite( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt new file mode 100644 index 0000000..5628f7d --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt @@ -0,0 +1,158 @@ +package au.gov.health.covidsafe.streetpass.persistence + +import android.util.Base64 +import au.gov.health.covidsafe.BuildConfig +import java.math.BigInteger +import java.security.KeyFactory +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.PublicKey +import java.security.interfaces.ECPublicKey +import java.security.spec.X509EncodedKeySpec +import javax.crypto.Cipher +import javax.crypto.KeyAgreement +import javax.crypto.Mac +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +data class EncryptionKeys(val ephPubKey: ByteArray, val aesKey: SecretKey, val macKey: SecretKey, val nonce: ByteArray) + +object Encryption { + + const val KEY_GEN_TIME_DELTA = 450000 // 7.5 minutes + + private val TAG = this.javaClass.simpleName + + // Get the server's ECDH public key + private fun readKey(): PublicKey { + + val decodedKey: ByteArray = Base64.decode(BuildConfig.ENCRYPTION_PUBLIC_KEY, Base64.DEFAULT) + val keySpec = X509EncodedKeySpec(decodedKey) + return KeyFactory.getInstance("EC").generatePublic(keySpec) + } + + // Compute a SHA-256 hash + private fun hash(content: ByteArray): ByteArray { + val hash: MessageDigest = MessageDigest.getInstance("SHA-256") + hash.update(content) + return hash.digest() + } + + // Generate ECDH P256 key-pair + private fun makeECKeys(): KeyPair { + val kpg: KeyPairGenerator = KeyPairGenerator.getInstance("EC") + kpg.initialize(256) + return kpg.generateKeyPair() + } + + // Convert an ECDH public key coordinate to R + private fun getPublicKey(kp: KeyPair): ByteArray { + val key: PublicKey = kp.public + if (key is ECPublicKey) { + if (key.w.affineX == BigInteger.ZERO && key.w.affineY == BigInteger.ZERO) { + return ByteArray(1) + } + var x: ByteArray = key.w.affineX.toByteArray() + if (x.size == 33 && x[0] == 0.toByte()) { + x = x.sliceArray(1..32) + } else if (x.size >= 33) { + throw IllegalStateException("Unexpected x coordinate in ECDH public key") + } else if (x.size < 32) { + x = ByteArray(32 - x.size).plus(x) + } + // Using P256 so q = p, p (mod 2) = 1 + // Compression flag is 0x2 when y (mod 2) = 0 and 0x3 when y (mod 2) = 1 + val flag: Int = 2 or (key.w.affineY and 1.toBigInteger()).toInt() + val fba: ByteArray = byteArrayOf(flag.toByte()) + return fba.plus(x) + } + throw IllegalStateException("Key pair does not contain an ECDH public key") + } + + // Perform a key agreement against the server's long-term ECDH public key + private fun doKeyAgreement(kp: KeyPair): KeyAgreement { + val ka: KeyAgreement = KeyAgreement.getInstance("ECDH") + ka.init(kp.private) + ka.doPhase(serverPubKey, true) + return ka + } + + // Compute a message authentication code for the given data + private fun computeMAC(key: SecretKey, data: ByteArray): ByteArray { + val mac: Mac = Mac.getInstance("HmacSHA256") + mac.init(key) + return mac.doFinal(data).sliceArray(0..15) + } + + // Convert an int to a 2-byte big-endian ByteArray + private fun counterBytes(counter: Int): ByteArray { + return byteArrayOf(((counter and 0xFF00) shr 8).toByte(), (counter and 0x00FF).toByte()) + } + + // Create a new cipher instance for symmetric crypt + private fun makeSymCipher(): Cipher { + return Cipher.getInstance("AES/CBC/PKCS5Padding") + } + + private val NONCE_PADDING = ByteArray(14) { 0x0E.toByte() } + private val serverPubKey: PublicKey = readKey() + private val symCipher: Cipher = makeSymCipher() + + private var cachedEphPubKey: ByteArray? = null + private var cachedAesKey: SecretKey? = null + private var cachedMacKey: SecretKey? = null + private var keyGenTime: Long = Long.MIN_VALUE + private var counter: Int = 0 + + private fun generateKeys() { + + // ECDH + val kp: KeyPair = makeECKeys() + val ka: KeyAgreement = doKeyAgreement(kp) + val ephSecret: ByteArray = ka.generateSecret() + cachedEphPubKey = getPublicKey(kp) + + // KDF + val derivedKey: ByteArray = hash(ephSecret) + cachedAesKey = SecretKeySpec(derivedKey.sliceArray(0..15), "AES") + cachedMacKey = SecretKeySpec(derivedKey.sliceArray(16..31), "HmacSHA256") + + } + + fun encryptPayload(data: ByteArray): String { + + val keys = encryptionKeys() + + val prefix: ByteArray = keys.ephPubKey.plus(keys.nonce) + + // Encrypt + // IV = AES(ctr, iv=null), AES(plaintext, iv=IV) === AES(ctr_with_padding || plaintext, iv=null) + // Using the latter construction to reduce key expansions + val ivParams = IvParameterSpec(ByteArray(16)) // null IV + symCipher.init(Cipher.ENCRYPT_MODE, keys.aesKey, ivParams) + val ciphertextWithIV: ByteArray = symCipher.doFinal(keys.nonce.plus(NONCE_PADDING).plus(data)) + + // MAC + val size: Int = ciphertextWithIV.size - 1 + val blob: ByteArray = prefix.plus(ciphertextWithIV.sliceArray(16..size)) + val mac: ByteArray = computeMAC(keys.macKey, blob) + + return Base64.encodeToString(blob.plus(mac), Base64.DEFAULT) + } + + + + @Synchronized + private fun encryptionKeys(): EncryptionKeys { + if (keyGenTime <= System.currentTimeMillis() - KEY_GEN_TIME_DELTA || counter >= 65535) { + generateKeys() + keyGenTime = System.currentTimeMillis() + counter = 0 + } else { + counter++ + } + return EncryptionKeys(cachedEphPubKey!!, cachedAesKey!!, cachedMacKey!!, counterBytes(counter)) + } +} \ No newline at end of file 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 index eb5feea..89c50d6 100644 --- 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 @@ -11,23 +11,14 @@ 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 = "localBlob") + val localBlob: String, - @ColumnInfo(name = "modelC") - val modelC: String, - - @ColumnInfo(name = "rssi") - val rssi: Int, - - @ColumnInfo(name = "txPower") - val txPower: Int? + @ColumnInfo(name = "remoteBlob") + val remoteBlob: String ) { @@ -39,7 +30,7 @@ class StreetPassRecord( 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)" + return "StreetPassRecord(v=$v, , org='$org', id=$id, timestamp=$timestamp,localBlob=$localBlob, remoteBlob=$remoteBlob)" } } 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 index b990256..0281c8e 100644 --- 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 @@ -1,19 +1,28 @@ package au.gov.health.covidsafe.streetpass.persistence +import android.content.ContentValues import android.content.Context +import android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE 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.LocalBlobV2 +import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.status.persistence.StatusRecord import au.gov.health.covidsafe.status.persistence.StatusRecordDao +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import kotlin.concurrent.thread +const val CURRENT_DB_VERSION = 3 + @Database( entities = [StreetPassRecord::class, StatusRecord::class], - version = 2, - exportSchema = true + version = CURRENT_DB_VERSION, + exportSchema = true ) abstract class StreetPassRecordDatabase : RoomDatabase() { @@ -21,22 +30,60 @@ abstract class StreetPassRecordDatabase : RoomDatabase() { abstract fun statusDao(): StatusRecordDao companion object { + + private val TAG = this.javaClass.simpleName + + private const val ID_COLUMN_INDEX = 0 + private const val TIMESTAMP_COLUMN_INDEX = 1 + private const val VERSION_COLUMN_INDEX = 2 + private const val MESSAGE_COLUMN_INDEX = 3 + private const val ORG_COLUMN_INDEX = 4 + private const val MODELP_COLUMN_INDEX = 5 + private const val MODELC_COLUMN_INDEX = 6 + private const val RSSI_COLUMN_INDEX = 7 + private const val TX_POWER_COLUMN_INDEX = 8 + + private const val EMPTY_DICT = "{}" + private val EMPTY_DICT_BYTE_ARRAY = EMPTY_DICT.toByteArray(Charsets.UTF_8) + + val ENCRYPTED_EMPTY_DICT = Encryption.encryptPayload(EMPTY_DICT_BYTE_ARRAY) + + const val VERSION_ONE = 1 + const val VERSION_TWO = 2 + + const val DUMMY_DEVICE = "" + const val DUMMY_RSSI = 999 + const val DUMMY_TXPOWER = 999 + + var migrationCallback: MigrationCallBack? = null + + // Singleton prevents multiple instances of database opening at the + // same time. @Volatile private var INSTANCE: StreetPassRecordDatabase? = null - fun getDatabase(context: Context): StreetPassRecordDatabase { + private val CALLBACK = object : RoomDatabase.Callback() { + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + migrationCallback?.migrationFinished() + } + } + + fun getDatabase(context: Context, migrationCallBack: MigrationCallBack? = null): StreetPassRecordDatabase { val tempInstance = INSTANCE if (tempInstance != null) { return tempInstance } + this.migrationCallback = migrationCallBack synchronized(this) { val instance = Room.databaseBuilder( - context, - StreetPassRecordDatabase::class.java, - "record_database" + context, + StreetPassRecordDatabase::class.java, + "record_database" ) - .addMigrations(MIGRATION_1_2) - .build() + .addMigrations(MIGRATION_1_2, MIGRATION_2_3) + .addCallback(CALLBACK) + .build() INSTANCE = instance return instance } @@ -58,10 +105,74 @@ abstract class StreetPassRecordDatabase : RoomDatabase() { } } + private val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + migrationCallback?.migrationStarted() + //adding a temporary encrypted encounters table for the migration of old data + database.execSQL("CREATE TABLE IF NOT EXISTS `encrypted_record_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `org` TEXT NOT NULL, `localBlob` TEXT NOT NULL, `remoteBlob` TEXT NOT NULL)") + + encryptExistingRecords(database) + + database.execSQL("DROP TABLE `record_table`") + + database.execSQL("ALTER TABLE `encrypted_record_table` RENAME TO `record_table`") + } + } + + fun encryptExistingRecords(db: SupportSQLiteDatabase) { + + + val gson: Gson = GsonBuilder().disableHtmlEscaping().create() + + val allRecs = db.query("SELECT * FROM record_table") + CentralLog.d(TAG, "starting encryption of ${allRecs.count} records") + if (allRecs.moveToFirst()) { + do { + val contentValues = ContentValues() + val id = allRecs.getInt(ID_COLUMN_INDEX) + val version = allRecs.getInt(VERSION_COLUMN_INDEX) + val timestamp = allRecs.getLong(TIMESTAMP_COLUMN_INDEX) + val msg = allRecs.getString(MESSAGE_COLUMN_INDEX) + val org = allRecs.getString(ORG_COLUMN_INDEX) + val modelP = allRecs.getString(MODELP_COLUMN_INDEX) + val modelC = allRecs.getString(MODELC_COLUMN_INDEX) + val rssi = allRecs.getInt(RSSI_COLUMN_INDEX) + val txPower = allRecs.getInt(TX_POWER_COLUMN_INDEX) + val plainRecord = gson.toJson(EncryptedRecord(modelP, modelC, rssi, txPower, msg)).toByteArray(Charsets.UTF_8) + val remoteBlob: String = if (version == 1) { + Encryption.encryptPayload(plainRecord) + } else { + msg + } + val localBlob: String = if (version == 1) { + ENCRYPTED_EMPTY_DICT + } else { + val modelP = if (DUMMY_DEVICE == modelP) null else modelP + val modelC = if (DUMMY_DEVICE == modelC) null else modelC + val rssi = if (DUMMY_RSSI == rssi) null else rssi + val txPower = if (DUMMY_TXPOWER == txPower) null else txPower + val plainRecord = gson.toJson(LocalBlobV2(modelP, modelC, rssi, txPower)).toByteArray(Charsets.UTF_8) + Encryption.encryptPayload(plainRecord) + } + contentValues.put("v", VERSION_TWO) + contentValues.put("org", org) + contentValues.put("localBlob", localBlob) + contentValues.put("remoteBlob", remoteBlob) + contentValues.put("id", id) + contentValues.put("timestamp", timestamp) + db.insert("encrypted_record_table", CONFLICT_REPLACE, contentValues) + } while (allRecs.moveToNext()) + } + CentralLog.d(TAG, "encryption done") + } + + class EncryptedRecord(var modelP: String, var modelC: String, var rssi: Int, var txPower: Int?, var msg: String) + + // This method will check if column exists in your table fun isFieldExist(db: SupportSQLiteDatabase, tableName: String, fieldName: String): Boolean { var isExist = false val res = - db.query("PRAGMA table_info($tableName)", null) + db.query("PRAGMA table_info($tableName)", null) res.moveToFirst() do { val currentColumn = res.getString(1) @@ -72,5 +183,9 @@ abstract class StreetPassRecordDatabase : RoomDatabase() { return isExist } } - } + +interface MigrationCallBack { + fun migrationStarted() + fun migrationFinished() +} \ 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 index a41417e..bf2f3ca 100644 --- 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 @@ -4,12 +4,12 @@ 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 modelC = "Encrypted" + val modelP = "Encrypted" + val msg = record.remoteBlob val timeStamp = record.timestamp - val rssi = record.rssi - val transmissionPower = record.txPower + val rssi = 0 + val transmissionPower = 0 val org = record.org constructor(record: StreetPassRecord) : this(record, 1) 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 index ab462e9..b055a60 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt @@ -58,6 +58,14 @@ abstract class PagerChildFragment : BaseFragment() { sealed class UploadButtonLayout { class ContinueLayout(@StringRes val buttonText: Int, val buttonListener: (() -> Unit)?) : UploadButtonLayout() + + class TwoChoiceContinueLayout( + @StringRes val primaryButtonText: Int, + val primaryButtonListener: (() -> Unit)?, + @StringRes val secondaryButtonText: Int, + val secondaryButtonListener: (() -> Unit)? + ) : UploadButtonLayout() + class QuestionLayout(val buttonYesListener: () -> Unit, val buttonNoListener: () -> Unit) : UploadButtonLayout() } 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 index aa0b795..768bff2 100644 --- 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 @@ -1,10 +1,13 @@ package au.gov.health.covidsafe.ui.home import android.Manifest +import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.content.* import android.net.Uri import android.os.Bundle +import android.text.Html +import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.View.GONE @@ -25,6 +28,10 @@ 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 +import java.text.SimpleDateFormat +import java.util.* + +private const val FOURTEEN_DAYS_IN_MILLIS = 14 * 24 * 60 * 60 * 1000L class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { @@ -107,6 +114,8 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { registerBroadcast() } refreshSetupCompleteOrIncompleteUi() + + home_header_no_bluetooth_pairing.movementMethod = LinkMovementMethod.getInstance() } override fun onPause() { @@ -132,71 +141,82 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { 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) + private fun isDataUploadedInPast14Days(context: Context): Boolean { + val isUploaded = Preference.isDataUploaded(context) - val textColor = ContextCompat.getColor(context, R.color.slack_black) - home_header_setup_complete_header_uploaded.setTextColor(textColor) - home_header_setup_complete_header.setTextColor(textColor) + if (!isUploaded) { + return false + } + + val millisSinceDataUploaded = System.currentTimeMillis() - Preference.getDataUploadedDateMs(context) + return (millisSinceDataUploaded < FOURTEEN_DAYS_IN_MILLIS) + } + + private fun getDataUploadDateHtmlString(context: Context): String { + val dataUploadedDateMillis = Preference.getDataUploadedDateMs(context) + val format = SimpleDateFormat("d MMM yyyy", Locale.ENGLISH) + val dateString = format.format(Date(dataUploadedDateMillis)) + return "$dateString" + } + + @SuppressLint("SetTextI18n") + private fun refreshSetupCompleteOrIncompleteUi() { + context?.let { + val isAllPermissionsEnabled = allPermissionsEnabled() + val isDataUploadedInPast14Days = isDataUploadedInPast14Days(it) + + val line1 = it.getString( + if (isAllPermissionsEnabled) { + R.string.home_header_active_title + } else { + R.string.home_header_inactive_title + } + ) + + val line2 = if (isDataUploadedInPast14Days) { + "

" + it.getString(R.string.home_header_uploaded_on_date, getDataUploadDateHtmlString(it)) + } else { + "" + } + + val line3 = "
" + it.getString( + if (isAllPermissionsEnabled) { + R.string.home_header_active_no_action_required + } else { + R.string.home_header_inactive_check_your_permissions + } + ) + + val headerHtmlText = "$line1$line2$line3" + + home_header_setup_complete_header.text = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Html.fromHtml(headerHtmlText, Html.FROM_HTML_MODE_COMPACT) + } else { + Html.fromHtml(headerHtmlText) + } + + if (isAllPermissionsEnabled) { + home_header_picture_setup_complete.setAnimation("spinner_home.json") + content_setup_incomplete_group.visibility = GONE + ContextCompat.getColor(it, R.color.lighter_green).let { bgColor -> + header_background.setBackgroundColor(bgColor) + header_background_overlap.setBackgroundColor(bgColor) } + } else { + home_header_picture_setup_complete.setImageResource(R.drawable.ic_logo_home_inactive) 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) + ContextCompat.getColor(it, R.color.grey).let { bgColor -> + header_background.setBackgroundColor(bgColor) + header_background_overlap.setBackgroundColor(bgColor) } } - 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) - } - } + home_been_tested_button.visibility = if (isDataUploadedInPast14Days) GONE else VISIBLE } } 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 index 01b5dbb..122ed25 100644 --- 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 @@ -65,10 +65,28 @@ class OnboardingActivity : FragmentActivity(), HasBlockingState, PagerContainer } override fun refreshButton(updateButtonLayout: UploadButtonLayout) { - if (updateButtonLayout is UploadButtonLayout.ContinueLayout) { - onboarding_next.setText(updateButtonLayout.buttonText) - onboarding_next.setOnClickListener { - updateButtonLayout.buttonListener?.invoke() + when (updateButtonLayout) { + is UploadButtonLayout.ContinueLayout -> { + onboarding_next.setText(updateButtonLayout.buttonText) + onboarding_next.setOnClickListener { + updateButtonLayout.buttonListener?.invoke() + } + + onboarding_next_secondary.visibility = GONE + } + + is UploadButtonLayout.TwoChoiceContinueLayout -> { + onboarding_next.setText(updateButtonLayout.primaryButtonText) + onboarding_next.setOnClickListener { + updateButtonLayout.primaryButtonListener?.invoke() + } + + onboarding_next_secondary.setText(updateButtonLayout.secondaryButtonText) + onboarding_next_secondary.setOnClickListener { + updateButtonLayout.secondaryButtonListener?.invoke() + } + + onboarding_next_secondary.visibility = VISIBLE } } } diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt new file mode 100644 index 0000000..e81d4a9 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt @@ -0,0 +1,68 @@ +package au.gov.health.covidsafe.ui.onboarding.fragment.permission + +import android.bluetooth.BluetoothAdapter +import android.os.Bundle +import android.text.Html +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_permission.root +import kotlinx.android.synthetic.main.fragment_permission_device_name.* + +class PermissionDeviceNameFragment : 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_device_name, container, false) + + override fun onResume() { + super.onResume() + + context?.let { + val deviceName = "${BluetoothAdapter.getDefaultAdapter()?.name}" + + val paragraph1 = it.getString(R.string.change_device_name_content_line_1, deviceName) + val paragraph2 = "

" + it.getString(R.string.change_device_name_content_line_2) + + val paragraphs = "$paragraph1$paragraph2" + + change_device_name_content.text = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Html.fromHtml(paragraphs, Html.FROM_HTML_MODE_COMPACT) + } else { + Html.fromHtml(paragraphs) + } + } + + } + + private fun navigateToNextPage() { + navigateTo(R.id.action_permissionDeviceNameFragment_to_permissionSuccessFragment) + } + + override fun getUploadButtonLayout() = UploadButtonLayout.TwoChoiceContinueLayout( + R.string.change_device_name_primary_action, + { + BluetoothAdapter.getDefaultAdapter()?.name = change_device_name_text_box.text.toString() + navigateToNextPage() + }, + R.string.change_device_name_secondary_action, + { + 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/permission/PermissionFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionFragment.kt index 7c58d15..c1a47fd 100644 --- 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 @@ -33,7 +33,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb } override val navigationIcon: Int? = R.drawable.ic_up - override var stepProgress: Int? = 5 + override var stepProgress: Int? = 4 private var navigationStarted = false @@ -58,7 +58,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb private fun navigateToNextPage() { navigationStarted = false if (hasAllPermissionsAndBluetoothOn()) { - navigateTo(R.id.action_permissionFragment_to_permissionSuccessFragment) + navigateTo(R.id.action_permissionFragment_to_permissionDeviceNameFragment ) } else { navigateToMainActivity() } 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 index 6b49e20..2b6ace2 100644 --- 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 @@ -2,23 +2,30 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.permissionsuccess import android.content.Intent import android.os.Bundle +import android.text.method.LinkMovementMethod 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 +import kotlinx.android.synthetic.main.fragment_permission_success.* class PermissionSuccessFragment : PagerChildFragment() { override val navigationIcon: Int? = R.drawable.ic_up - override var stepProgress: Int? = 5 + override var stepProgress: Int? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? = inflater.inflate(R.layout.fragment_permission_success, container, false) + override fun onResume() { + super.onResume() + + permission_success_content.movementMethod = LinkMovementMethod.getInstance() + } + private fun navigateToNextPage() { val intent = Intent(context, HomeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/splash/SplashViewModel.kt b/app/src/main/java/au/gov/health/covidsafe/ui/splash/SplashViewModel.kt new file mode 100644 index 0000000..607eddf --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/splash/SplashViewModel.kt @@ -0,0 +1,85 @@ +package au.gov.health.covidsafe.ui.splash + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import au.gov.health.covidsafe.streetpass.persistence.CURRENT_DB_VERSION +import au.gov.health.covidsafe.streetpass.persistence.MigrationCallBack +import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase +import kotlinx.coroutines.* + +class SplashViewModel(context: Context) : ViewModel() { + + private val SPLASH_TIME: Long = 2000 + + val splashNavigationLiveData = MutableLiveData(SplashNavigationEvent.ShowSplashScreen) + + private var migrated = false + private var splashScreenPassed = false + + val db = StreetPassRecordDatabase.getDatabase(context, object : MigrationCallBack { + override fun migrationStarted() { + migrated = false + if (splashScreenPassed) { + viewModelScope.launch { + splashNavigationLiveData.value = SplashNavigationEvent.ShowMigrationScreen + } + } + } + + override fun migrationFinished() { + migrated = true + if (splashScreenPassed) { + viewModelScope.launch { + splashNavigationLiveData.value = SplashNavigationEvent.GoToNextScreen + } + } + } + }) + + fun setupUI() { + this.viewModelScope.launch { + val splashScreenCoroutine = async(context = Dispatchers.IO) { + delay(SPLASH_TIME) + viewModelScope.launch { + if (migrated) { + splashNavigationLiveData.value = SplashNavigationEvent.GoToNextScreen + } else { + splashNavigationLiveData.value = SplashNavigationEvent.ShowMigrationScreen + } + splashScreenPassed = true + } + } + val migratingCoroutine = async(context = Dispatchers.IO) { + val readableDatabase = db.openHelper.readableDatabase + migrated = !readableDatabase.needUpgrade(CURRENT_DB_VERSION) + viewModelScope.launch { + if (migrated && splashScreenPassed) { + splashNavigationLiveData.value = SplashNavigationEvent.GoToNextScreen + } else if (!migrated) { + splashNavigationLiveData.value = SplashNavigationEvent.ShowMigrationScreen + } + } + } + splashScreenCoroutine.join() + migratingCoroutine.join() + } + } + + fun release() { + StreetPassRecordDatabase.migrationCallback = null + this.viewModelScope.cancel() + } +} + +class SplashViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T = SplashViewModel(context) as T +} + +sealed class SplashNavigationEvent { + object ShowSplashScreen : SplashNavigationEvent() + object ShowMigrationScreen : SplashNavigationEvent() + object GoToNextScreen : SplashNavigationEvent() +} \ 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 index 4f590d4..119577b 100644 --- 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 @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.UploadButtonLayout @@ -17,6 +18,13 @@ class UploadFinishedFragment : PagerChildFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_upload_finished, container, false) + override fun onResume() { + super.onResume() + + // set accessibility focus to the title + header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.action_upload_done) { activity?.onBackPressed() } 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 index 0f1a0f6..9d706ec 100644 --- 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 @@ -4,10 +4,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent 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.* +import kotlinx.android.synthetic.main.fragment_upload_initial.* +import kotlinx.android.synthetic.main.fragment_upload_page_4.root class UploadInitialFragment : PagerChildFragment() { @@ -19,6 +21,13 @@ class UploadInitialFragment : PagerChildFragment() { inflater.inflate(R.layout.fragment_upload_initial, container, false) + override fun onResume() { + super.onResume() + + // set accessibility focus to the title + upload_initial_headline.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + override fun updateButtonState() { enableContinueButton() } 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 index 4aae617..5d23f6d 100644 --- 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 @@ -6,6 +6,7 @@ import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.UploadButtonLayout @@ -30,7 +31,11 @@ class UploadStepFourFragment : PagerChildFragment() { upload_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked -> updateButtonState() } + + // set accessibility focus to the title + header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } + override fun updateButtonState() { if (upload_consent_checkbox.isChecked) { enableContinueButton() @@ -42,7 +47,7 @@ class UploadStepFourFragment : PagerChildFragment() { override val navigationIcon: Int? = R.drawable.ic_up override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout( - R.string.action_agree) { + R.string.action_continue) { navigateToVerifyUploadPin() } 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 index 25c4301..705f156 100644 --- 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 @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent import androidx.core.os.bundleOf import au.gov.health.covidsafe.R import au.gov.health.covidsafe.ui.PagerChildFragment @@ -46,6 +47,9 @@ class VerifyUploadPinFragment : PagerChildFragment() { updateButtonState() hideInvalidOtp() } + + // set accessibility focus to the title + header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } override fun onPause() { diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml index e0bbe66..28ca64b 100644 --- a/app/src/main/res/layout/activity_onboarding.xml +++ b/app/src/main/res/layout/activity_onboarding.xml @@ -5,6 +5,7 @@ android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" + android:paddingBottom="@dimen/keyline_7" tools:context=".ui.onboarding.OnboardingActivity"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml index 716a2ba..0824fef 100644 --- a/app/src/main/res/layout/activity_splash.xml +++ b/app/src/main/res/layout/activity_splash.xml @@ -21,7 +21,26 @@ app:layout_constraintHeight_percent="0.33" /> - + + + app:layout_constraintTop_toBottomOf="@+id/splash_migration_text" + app:layout_constraintVertical_chainStyle="spread" + app:lottie_autoPlay="false" + app:lottie_loop="true" + app:lottie_speed="1"/> - - - - - - @@ -54,15 +38,6 @@ app:layout_constraintTop_toTopOf="parent" /> - - - + app:layout_constraintStart_toStartOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_help.xml b/app/src/main/res/layout/fragment_help.xml index 1c8f85e..a3bc226 100644 --- a/app/src/main/res/layout/fragment_help.xml +++ b/app/src/main/res/layout/fragment_help.xml @@ -17,6 +17,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/topInset" app:layout_constraintWidth_default="wrap" + app:navigationContentDescription="@string/navigation_back_button_content_description" app:navigationIcon="@drawable/ic_up" app:title="@string/title_help"> diff --git a/app/src/main/res/layout/fragment_home_setup_complete_header.xml b/app/src/main/res/layout/fragment_home_setup_complete_header.xml index fe3f580..8a900bd 100644 --- a/app/src/main/res/layout/fragment_home_setup_complete_header.xml +++ b/app/src/main/res/layout/fragment_home_setup_complete_header.xml @@ -26,39 +26,6 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/home_header_picture_setup_complete" /> - - - - - - + app:layout_constraintTop_toBottomOf="@+id/home_header_picture_setup_complete_space" /> + + - - + android:layout_height="@dimen/keyline_0" + app:layout_constraintTop_toBottomOf="@+id/home_header_no_bluetooth_pairing" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml b/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml index a34af95..acec37b 100644 --- a/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml +++ b/app/src/main/res/layout/fragment_home_setup_incomplete_content.xml @@ -17,6 +17,7 @@ android:layout_height="wrap_content" android:layout_marginLeft="@dimen/keyline_4" android:layout_marginRight="@dimen/keyline_4" + android:layout_marginTop="@dimen/keyline_7" app:layout_constraintTop_toBottomOf="@+id/header_barrier" card_view:cardBackgroundColor="@color/white" card_view:cardCornerRadius="6dp" @@ -28,7 +29,6 @@ android:id="@+id/home_setup_incomplete_permissions_group" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/keyline_4" android:orientation="vertical"> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_permission_success.xml b/app/src/main/res/layout/fragment_permission_success.xml index 16d588d..607af71 100644 --- a/app/src/main/res/layout/fragment_permission_success.xml +++ b/app/src/main/res/layout/fragment_permission_success.xml @@ -40,6 +40,7 @@ android:paddingStart="@dimen/keyline_5" android:paddingEnd="@dimen/keyline_5" android:text="@string/permission_success_content" + android:textColorLink="?attr/colorPrimary" android:textAppearance="?textAppearanceBody1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/fragment_upload_finished.xml b/app/src/main/res/layout/fragment_upload_finished.xml index b1defc7..340a758 100644 --- a/app/src/main/res/layout/fragment_upload_finished.xml +++ b/app/src/main/res/layout/fragment_upload_finished.xml @@ -33,6 +33,7 @@ android:layout_marginEnd="@dimen/keyline_5" android:textAppearance="?textAppearanceHeadline2" android:text="@string/upload_finished_header" + android:contentDescription="@string/upload_finished_header_content_description" app:layout_constraintBottom_toTopOf="@+id/subHeader" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/fragment_upload_initial.xml b/app/src/main/res/layout/fragment_upload_initial.xml index 422f12a..d052f19 100644 --- a/app/src/main/res/layout/fragment_upload_initial.xml +++ b/app/src/main/res/layout/fragment_upload_initial.xml @@ -26,6 +26,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyline_6" android:text="@string/upload_step_1_header" + android:contentDescription="@string/upload_step_1_header_content_description" android:textAppearance="?textAppearanceHeadline2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/fragment_upload_master.xml b/app/src/main/res/layout/fragment_upload_master.xml index 566e204..e287000 100644 --- a/app/src/main/res/layout/fragment_upload_master.xml +++ b/app/src/main/res/layout/fragment_upload_master.xml @@ -15,7 +15,8 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:navigationIcon="@drawable/ic_up" /> + app:navigationIcon="@drawable/ic_up" + app:navigationContentDescription="@string/navigation_back_button_content_description"/> + android:text="@string/upload_step_4_header" + android:contentDescription="@string/upload_step_4_header_content_description" + /> + + + + Join me in stopping the spread of COVID-19! Download COVIDSafe, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives https://covidsafe.gov.au Join me in stopping the spread of COVID-19! Download COVIDSafe>, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives covidsafe.gov.au - COVIDSafe is active - Keep COVIDSafe active when you leave home or are in public places. - COVIDSafe is not active Make sure COVIDSafe is active before you leave home or when in public places. Check app now + + COVIDSafe update in progress. \n\n Please make sure you phone is not switched off until the update is complete. + Together we can stop the spread of COVID-19 Heading, Together we can stop the spread of COVID-19 @@ -40,7 +40,7 @@ Read our Terms and conditions Next - + Registration and privacy Heading, Registration and privacy It is important that you read the COVIDSafe privacy policy before you register for COVIDSafe.\n\nIf you are under 16 years of age, your parent/guardian must also read the privacy policy.\n\nUse of COVIDSafe is completely voluntary. You can install or delete the application at any time. If you delete COVIDSafe, you may also ask for your information to be deleted from the secure server.\n\nTo register for COVIDSafe, you will need to enter a name, mobile number, age range and postcode.\n\nInformation you submit when you register, and information about your use of COVIDSafe will be collected and stored on a highly secure server.\n\nCOVIDSafe will not collect your location information.\n\nCOVIDSafe will note the time of contact and an anonymous ID code of other COVIDSafe users you come into contact with.\n\nOther COVIDSafe users you come into contact with will record an anonymous ID code and the time of contact with your device.\n\nIf another user tests positive to COVID-19, they may upload their contact information and a state or territory health official may contact you for tracing purposes.\n\nYour registration details will only be used or disclosed for contact tracing and for the proper and lawful functioning of COVIDSafe.\n\nMore information is available at the Australian Government Department of Health website.\n\nSee the COVIDSafe privacy policy for further details about your rights about your information and how it will be handled and shared. @@ -57,11 +57,9 @@ Enter your details Heading, Enter your details - Full name (First, Last) - Firstname Lastname - Enter full name (First, Last) - Age (select) - Age range + Full name + Enter full name + Age range (select) Select age range Postcode e.g. 2000 @@ -119,20 +117,40 @@ App permissions - COVIDSafe needs Bluetooth® and notifications enabled to work.\n\nSelect ‘Proceed’ to enable:\n\n1. Bluetooth®\n\n2. Location Permissions\n\n3. Battery Optimiser\n\n\nAndroid needs Location Permissions for Bluetooth® to work. + COVIDSafe needs Bluetooth® and notifications enabled to work.\n\nSelect ‘Proceed’ to:\n\n1. Enable Bluetooth®\n\n2. Allow Location permissions\n\n3. Disable Battery optimisation\n\n\nAndroid needs Location Permissions for Bluetooth® to work.\n\nCOVIDSafe does not send pairing requests. Proceed Android requires location access to enable Bluetooth® functions for COVIDSafe. COVIDSafe cannot work properly without it + + Your device name + Heading, Your device name + The current name of your device is %s. + Other Bluetooth® devices around you will be able to see this name. You may like to consider making the device name anonymous. + New device name + Android phone + Change and continue + Skip and keep as it is + You\'ve successfully registered - 1. Keep your phone with you when you leave home.\n\n2. Keep the app running.\n\n3. Keep Bluetooth® on. + + 1. When you leave home, keep your phone with you and make sure COVIDSafe is active.\n\n + 2. Bluetooth® should be kept ON.\n\n + 3. Battery optimisation should be OFF.\n\n + 4. COVIDSafe does not send pairing requests. Learn more. + + Keep push notifications on for COVIDSafe so we can notify you quickly if the app isn\'t working properly. Continue - COVIDSafe is active.\nNo further action is required. - COVIDSafe is not active.\nCheck your permissions. - Thank you for helping stop the spread of COVID-19. Your information has been uploaded. + COVIDSafe is active. + No further action required. + COVIDSafe is not active. + Check your permissions. + Your information was uploaded on %s. + COVIDSafe does not send pairing requests. + Bluetooth®: %s Battery optimization: %s Location: %s @@ -168,7 +186,6 @@ Help topics If you have issues or questions about the app. - Version Number:%s Report an issue @@ -177,22 +194,24 @@ No Is a health official asking you to upload your information? + Heading, Is a health official asking you to upload your information? Only if you test positive to COVID-19 will a state or territory health official contact you to assist with voluntary upload of your information.\n\nOnce you press ‘Yes’ you’ll need to provide consent to upload your information. Upload consent + Heading, Upload consent Unless you consent, your close contact information will not be uploaded.\n\nIf you consent, your close contact information will be uploaded and shared with state or territory health officials for contact tracing purposes.\n\nRead the COVIDSafe privacy policy for further details. I consent to upload my information Upload your information + Heading, Upload your information A state or territory health official will send a PIN to your device via text message. Enter it below to upload your information. Upload my information Invalid PIN, please ask the health official to send you another PIN. Thank you for helping to stop the spread of COVID-19! + Heading, Thank you for helping to stop the spread of COVID-19! You have successfully uploaded your information to the COVIDSafe highly secure storage system.\n\nState or territory health officials will notify other COVIDSafe users that have recorded instances of close contact with you. Your identity will remain anonymous to other users. Continue - I agree - Got it Upload your data Continue diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 56213de..90124d7 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,3 +1,4 @@ + \ No newline at end of file diff --git a/feedback-android/build.gradle b/feedback-android/build.gradle index 86cd43d..922c8fd 100644 --- a/feedback-android/build.gradle +++ b/feedback-android/build.gradle @@ -15,7 +15,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 28 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true multiDexEnabled = true diff --git a/gradle.properties b/gradle.properties index 6d1fd87..b723f35 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,11 +12,13 @@ # org.gradle.parallel=true #Mon Apr 06 10:10:18 AEST 2020 android.useAndroidX=true +android.enableJetifier=true + PUSH_NOTIFICATION_ID=771578 MAX_SCAN_INTERVAL=43000 ORG="AU_DTA" ADVERTISING_DURATION=180000 -PROTOCOL_VERSION=1 +PROTOCOL_VERSION=2 BLACKLIST_DURATION=100000 BM_CHECK_INTERVAL=540000 MAX_QUEUE_TIME=7000 @@ -32,7 +34,6 @@ SERVICE_FOREGROUND_NOTIFICATION_ID=771579 STAGING_SERVICE_UUID="CC0AC8B7-03B5-4252-8D84-44D199E16065" CONNECTION_TIMEOUT=6000 HEALTH_CHECK_INTERVAL=900000 -android.enableJetifier=true ADVERTISING_INTERVAL=5000 TEST_BASE_URL="https://device-api.uat.unp.aws.covidsafe.gov.au" @@ -47,3 +48,8 @@ PRODUCTION_END_POINT_PREFIX="/prod" DEBUG_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA=" STAGING_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA=" PRODUCTION_BACKGROUND_IOS_SERVICE_UUID="AQEAAAAAAAAAAAAAAAAAAAA=" + +DEBUG_ENCRYPTION_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2sBxH4LqeQKkhhL0pZi3RnlJuV6HtTJseYhPZP1jO5H1HNOHdhlwwGOvUrqyZ4Mlbuw8K8wUk1ZU+STd7GqORA==" +STAGING_ENCRYPTION_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2sBxH4LqeQKkhhL0pZi3RnlJuV6HtTJseYhPZP1jO5H1HNOHdhlwwGOvUrqyZ4Mlbuw8K8wUk1ZU+STd7GqORA==" +PRODUCTION_ENCRYPTION_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENBs4ziXF4rp531uvbqq9zCxiBpQr3DcKjMgc/WA6FHv6rBvu+uHSRJJRS2xrJ6Rqt30QcSUD1E2f/d0lb2Gvsg==" +