From ae18438d17b66a6e4e3dac765883170e52ebec09 Mon Sep 17 00:00:00 2001 From: COVIDSafe Support <64945427+covidsafe-support@users.noreply.github.com> Date: Wed, 23 Sep 2020 08:46:31 +1000 Subject: [PATCH] COVIDSafe code from version 1.11.0 (#26) --- app/build.gradle | 75 +- app/src/debug/AndroidManifest.xml | 2 +- .../au/gov/health/covidsafe/PeekActivity.kt | 70 +- .../gov/health/covidsafe/RecordListAdapter.kt | 1 + app/src/main/AndroidManifest.xml | 26 +- .../au/gov/health/covidsafe/HomeActivity.kt | 160 +-- .../health/covidsafe/{ => app}/TracerApp.kt | 5 +- .../health/covidsafe/bluetooth/BLEScanner.kt | 2 +- .../covidsafe/bluetooth/gatt/GattServer.kt | 7 +- .../covidsafe/boot/StartOnBootReceiver.kt | 16 +- .../extensions/PermissionExtensions.kt | 4 +- .../covidsafe/extensions/ViewExtensions.kt | 36 + .../covidsafe/factory/NetworkFactory.kt | 15 + .../usecase/GetCaseStatisticsUseCase.kt | 53 + ...ageAndPerformScanWithExponentialBackOff.kt | 8 +- .../interactor/usecase/UploadData.kt | 8 +- .../gov/health/covidsafe/links/LinkBuilder.kt | 51 +- .../response/CaseStatisticResponse.kt | 25 + .../covidsafe/networking/service/AwsClient.kt | 5 + .../CovidFirebaseMessagingService.kt | 110 +++ .../notifications/NotificationBuilder.kt | 35 + .../notifications/NotificationTemplates.kt | 12 +- .../covidsafe/{ => preference}/Preference.kt | 14 +- .../receivers/PrivacyCleanerReceiver.kt | 7 +- .../covidsafe/receivers/UpgradeReceiver.kt | 2 +- .../scheduler/GetMessagesScheduler.kt | 30 +- .../crypto/AESEncryptionForPreAndroidM.kt | 4 +- .../services/BluetoothMonitoringService.kt | 208 ++-- .../services/CovidFirebaseMessagingService.kt | 53 - .../services/SensorMonitoringService.kt | 96 -- .../covidsafe/streetpass/StreetPassScanner.kt | 2 +- .../covidsafe/streetpass/StreetPassWorker.kt | 4 +- .../persistence/StreetPassRecordDatabase.kt | 18 +- .../gov/health/covidsafe/talkback/TalkBack.kt | 2 +- .../covidsafe/ui/{ => base}/BaseFragment.kt | 4 +- .../covidsafe/ui/base/BindingAdapter.kt | 56 ++ .../{ => ui/base}/HasBlockingState.kt | 2 +- .../ui/{ => base}/PagerChildFragment.kt | 2 +- .../covidsafe/ui/{ => base}/PagerContainer.kt | 2 +- .../InternetConnectionIssuesActivity.kt | 3 +- .../DeviceNameChangePromptActivity.kt | 8 +- .../covidsafe/ui/home/CaseNumbersState.kt | 8 + .../health/covidsafe/ui/home/HelpFragment.kt | 2 +- .../health/covidsafe/ui/home/HomeFragment.kt | 653 ++++++------- .../ui/home/HomeFragmentViewModel.kt | 88 ++ .../health/covidsafe/ui/home/HomePresenter.kt | 10 - .../ui/home/view/ExternalLinkCard.kt | 55 +- .../ui/home/view/PermissionStatusCard.kt | 4 +- .../ui/onboarding/CountryListRecyclerView.kt | 4 +- .../ui/onboarding/OnboardingActivity.kt | 8 +- .../dataprivacy/DataPrivacyFragment.kt | 4 +- .../enternumber/EnterNumberFragment.kt | 10 +- .../enternumber/EnterNumberPresenter.kt | 2 +- .../fragment/enterpin/EnterPinFragment.kt | 26 +- .../fragment/enterpin/EnterPinPresenter.kt | 2 +- .../fragment/howitworks/HowItWorksFragment.kt | 4 +- .../introduction/IntroductionFragment.kt | 4 +- .../PermissionDeviceNameFragment.kt | 6 +- .../fragment/permission/PermissionFragment.kt | 33 +- .../PermissionSuccessFragment.kt | 4 +- .../personal/PersonalDetailsFragment.kt | 10 +- .../RegistrationConsentFragment.kt | 6 +- .../undersixteen/UnderSixteenFragment.kt | 4 +- .../covidsafe/ui/settings/SettingsFragment.kt | 226 +++++ .../{ => ui/splash}/SplashActivity.kt | 9 +- .../ui/upload/UploadContainerFragment.kt | 21 +- .../presentation/UploadFinishedFragment.kt | 4 +- .../presentation/UploadInitialFragment.kt | 5 +- .../presentation/UploadStepFourFragment.kt | 4 +- .../presentation/VerifyUploadPinFragment.kt | 4 +- .../presentation/VerifyUploadPinPresenter.kt | 2 +- .../covidsafe/{ => ui/utils}/LocalBlobV2.kt | 2 +- .../health/covidsafe/{ => ui/utils}/Utils.kt | 21 +- .../{ => ui/webview}/WebViewActivity.kt | 3 +- .../covidsafe/utils/NetworkConnectionCheck.kt | 93 ++ .../res/drawable/background_circular_grey.xml | 5 + .../drawable/background_circular_white.xml | 5 + ...n_right.xml => ic_chevron_right_black.xml} | 0 .../res/drawable/ic_chevron_right_white.xml | 9 + .../main/res/drawable/ic_confirmed_cases.xml | 18 + app/src/main/res/drawable/ic_heart.xml | 13 + .../main/res/drawable/ic_help_question.xml | 14 + app/src/main/res/drawable/ic_mail_support.xml | 20 + .../main/res/drawable/ic_question_circle.xml | 18 - .../res/drawable/ic_red_cross_no_circle.xml | 20 - app/src/main/res/drawable/ic_red_error.xml | 23 + .../drawable/ic_settings_outline_black.xml | 20 + app/src/main/res/drawable/ic_trending_up.xml | 20 + ....xml => ic_white_background_red_error.xml} | 8 +- app/src/main/res/drawable/icon_checkbox.xml | 27 - .../phone_number_invalid_background.xml | 2 +- .../activity_device_name_change_prompt.xml | 2 +- app/src/main/res/layout/activity_splash.xml | 2 +- app/src/main/res/layout/database_peek.xml | 40 +- .../main/res/layout/fragment_enter_number.xml | 3 +- .../main/res/layout/fragment_enter_pin.xml | 2 +- app/src/main/res/layout/fragment_help.xml | 2 +- app/src/main/res/layout/fragment_home.xml | 210 ++-- .../layout/fragment_home_case_statistics.xml | 166 ++++ .../layout/fragment_home_external_links.xml | 271 ++---- .../main/res/layout/fragment_home_header.xml | 63 ++ .../fragment_home_setup_complete_header.xml | 66 -- ...fragment_home_setup_incomplete_content.xml | 101 -- .../fragment_home_setup_status_header.xml | 24 + .../res/layout/fragment_personal_details.xml | 6 +- app/src/main/res/layout/fragment_settings.xml | 82 ++ .../fragment_settings_external_link.xml | 96 ++ .../res/layout/fragment_verify_upload_pin.xml | 2 +- .../layout/view_card_external_link_card.xml | 21 +- .../res/layout/view_card_permission_card.xml | 20 +- .../main/res/layout/view_covid_share_tile.xml | 19 + .../main/res/layout/view_help_topics_tile.xml | 21 + .../res/layout/view_home_setup_complete.xml | 86 ++ .../res/layout/view_home_setup_incomplete.xml | 101 ++ .../layout/view_national_case_statistics.xml | 88 ++ ..._settings_improve_app_performance_tile.xml | 66 ++ .../res/layout/view_state_case_statistics.xml | 355 +++++++ app/src/main/res/navigation/nav_home.xml | 19 + app/src/main/res/values-ar/strings.xml | 911 ++++++++--------- app/src/main/res/values-el-rGR/strings.xml | 910 ++++++++--------- app/src/main/res/values-it-rIT/strings.xml | 911 ++++++++--------- app/src/main/res/values-ko/strings.xml | 912 ++++++++--------- app/src/main/res/values-pa-rIN/strings.xml | 917 +++++++++--------- app/src/main/res/values-tr/strings.xml | 917 +++++++++--------- app/src/main/res/values-vi/strings.xml | 911 ++++++++--------- app/src/main/res/values-zh-rCN/strings.xml | 909 ++++++++--------- app/src/main/res/values-zh-rTW/strings.xml | 912 ++++++++--------- app/src/main/res/values/attrs.xml | 10 +- app/src/main/res/values/colors.xml | 10 +- app/src/main/res/values/dimens.xml | 36 +- app/src/main/res/values/strings.xml | 917 +++++++++--------- .../res/values/strings_no_translation.xml | 10 + app/src/main/res/values/styles.xml | 13 +- app/src/main/res/values/type.xml | 27 +- .../main/res/xml/network_security_config.xml | 5 + build.gradle | 22 +- feedback-android/build.gradle | 28 +- .../src/main/res/values/strings.xml | 2 +- scripts/lokalise.sh | 19 + 139 files changed, 8039 insertions(+), 6010 deletions(-) rename app/src/{main => debug}/java/au/gov/health/covidsafe/RecordListAdapter.kt (98%) rename app/src/main/java/au/gov/health/covidsafe/{ => app}/TracerApp.kt (93%) create mode 100644 app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetCaseStatisticsUseCase.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/networking/response/CaseStatisticResponse.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/notifications/CovidFirebaseMessagingService.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/notifications/NotificationBuilder.kt rename app/src/main/java/au/gov/health/covidsafe/{ => preference}/Preference.kt (95%) delete mode 100644 app/src/main/java/au/gov/health/covidsafe/services/CovidFirebaseMessagingService.kt delete mode 100644 app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt rename app/src/main/java/au/gov/health/covidsafe/ui/{ => base}/BaseFragment.kt (90%) create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/base/BindingAdapter.kt rename app/src/main/java/au/gov/health/covidsafe/{ => ui/base}/HasBlockingState.kt (60%) rename app/src/main/java/au/gov/health/covidsafe/ui/{ => base}/PagerChildFragment.kt (98%) rename app/src/main/java/au/gov/health/covidsafe/ui/{ => base}/PagerContainer.kt (89%) rename app/src/main/java/au/gov/health/covidsafe/{ => ui/connection}/InternetConnectionIssuesActivity.kt (91%) rename app/src/main/java/au/gov/health/covidsafe/{ => ui/devicename}/DeviceNameChangePromptActivity.kt (74%) create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/home/CaseNumbersState.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt delete mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsFragment.kt rename app/src/main/java/au/gov/health/covidsafe/{ => ui/splash}/SplashActivity.kt (92%) rename app/src/main/java/au/gov/health/covidsafe/{ => ui/utils}/LocalBlobV2.kt (70%) rename app/src/main/java/au/gov/health/covidsafe/{ => ui/utils}/Utils.kt (95%) rename app/src/main/java/au/gov/health/covidsafe/{ => ui/webview}/WebViewActivity.kt (93%) create mode 100644 app/src/main/java/au/gov/health/covidsafe/utils/NetworkConnectionCheck.kt create mode 100644 app/src/main/res/drawable/background_circular_grey.xml create mode 100644 app/src/main/res/drawable/background_circular_white.xml rename app/src/main/res/drawable/{ic_chevron_right.xml => ic_chevron_right_black.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_chevron_right_white.xml create mode 100644 app/src/main/res/drawable/ic_confirmed_cases.xml create mode 100644 app/src/main/res/drawable/ic_heart.xml create mode 100644 app/src/main/res/drawable/ic_help_question.xml create mode 100644 app/src/main/res/drawable/ic_mail_support.xml delete mode 100644 app/src/main/res/drawable/ic_question_circle.xml delete mode 100644 app/src/main/res/drawable/ic_red_cross_no_circle.xml create mode 100644 app/src/main/res/drawable/ic_red_error.xml create mode 100644 app/src/main/res/drawable/ic_settings_outline_black.xml create mode 100644 app/src/main/res/drawable/ic_trending_up.xml rename app/src/main/res/drawable/{ic_red_cross.xml => ic_white_background_red_error.xml} (80%) delete mode 100644 app/src/main/res/drawable/icon_checkbox.xml create mode 100644 app/src/main/res/layout/fragment_home_case_statistics.xml create mode 100644 app/src/main/res/layout/fragment_home_header.xml delete mode 100644 app/src/main/res/layout/fragment_home_setup_complete_header.xml delete mode 100644 app/src/main/res/layout/fragment_home_setup_incomplete_content.xml create mode 100644 app/src/main/res/layout/fragment_home_setup_status_header.xml create mode 100644 app/src/main/res/layout/fragment_settings.xml create mode 100644 app/src/main/res/layout/fragment_settings_external_link.xml create mode 100644 app/src/main/res/layout/view_covid_share_tile.xml create mode 100644 app/src/main/res/layout/view_help_topics_tile.xml create mode 100644 app/src/main/res/layout/view_home_setup_complete.xml create mode 100644 app/src/main/res/layout/view_home_setup_incomplete.xml create mode 100644 app/src/main/res/layout/view_national_case_statistics.xml create mode 100644 app/src/main/res/layout/view_settings_improve_app_performance_tile.xml create mode 100644 app/src/main/res/layout/view_state_case_statistics.xml create mode 100755 scripts/lokalise.sh diff --git a/app/build.gradle b/app/build.gradle index 343a905..535e35b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,19 +1,14 @@ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - apply plugin: 'kotlin-android-extensions' - apply plugin: 'kotlin-kapt' - apply plugin: "androidx.navigation.safeargs.kotlin" - apply plugin: 'com.google.gms.google-services' buildscript { repositories { google() - + jcenter() } } @@ -27,26 +22,22 @@ def getGitHash = { -> } android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + defaultConfig { applicationId "au.gov.health.covidsafe" - minSdkVersion 21 + minSdkVersion rootProject.ext.minSdkVersion /* TargetSdk is currently set to 28 because we are using a greylisted api in SDK 29, in order to fix a BLE vulnerability Before you increase the targetSdkVersion make sure that all its usage are still working */ - targetSdkVersion 28 - versionCode 54 - versionName "1.0.54" + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 72 + versionName "1.11.0" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] @@ -105,9 +96,9 @@ android { String ssid = STAGING_SERVICE_UUID - versionNameSuffix "-debug-${getGitHash()}-${ssid.substring(ssid.length() - 5,ssid.length() - 1 )}" + versionNameSuffix "-debug-${getGitHash()}-${ssid.substring(ssid.length() - 5, ssid.length() - 1)}" resValue "string", "app_name", "COVIDSafe Debug" - applicationIdSuffix "debug" + applicationIdSuffix ".debug" signingConfig signingConfigs.debug } @@ -119,15 +110,12 @@ android { buildConfigField "String", "IOS_BACKGROUND_UUID", STAGING_BACKGROUND_IOS_SERVICE_UUID buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", STAGING_ENCRYPTION_PUBLIC_KEY - - - // Retrieve bluetooth ssid from staging's strings.xml String ssid = STAGING_SERVICE_UUID - versionNameSuffix "-beta-${getGitHash()}-${ssid.substring(ssid.length() - 5,ssid.length() - 1 )}" + versionNameSuffix "-beta-${getGitHash()}-${ssid.substring(ssid.length() - 5, ssid.length() - 1)}" debuggable false - applicationIdSuffix "beta" + applicationIdSuffix ".beta" resValue "string", "app_name", "COVIDSafe beta" lintOptions { @@ -137,6 +125,7 @@ android { matchingFallbacks = ['release'] signingConfig signingConfigs.staging } + release { buildConfigField "String", "BLE_SSID", PRODUCTION_SERVICE_UUID @@ -145,8 +134,6 @@ android { buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", PRODUCTION_ENCRYPTION_PUBLIC_KEY - - debuggable false jniDebuggable false renderscriptDebuggable false @@ -161,10 +148,13 @@ android { abortOnError false } signingConfig signingConfigs.release - } } + buildFeatures { + dataBinding true + } + sourceSets { staging { java.srcDirs = ['src/debug/java'] @@ -185,45 +175,31 @@ android { packagingOptions { exclude 'META-INF/atomicfu.kotlin_module' } - } -repositories { - jcenter() -} - - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation project(":feedback-android") // kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - def kotlin_coroutines_version = "1.3.5" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" //androidx - implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'pub.devrel:easypermissions:3.0.0' implementation 'com.google.code.gson:gson:2.8.6' - testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // room - def room_version = "2.2.5" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" - // http - implementation "com.squareup.retrofit2:retrofit:$retrofit_version" - implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" - implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" // rx @@ -233,11 +209,10 @@ dependencies { implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0' implementation 'androidx.navigation:navigation-ui-ktx:2.3.0' - //cardview - implementation 'androidx.cardview:cardview:1.0.0' + implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version" //Lottie - implementation 'com.airbnb.android:lottie:3.4.0' + implementation 'com.airbnb.android:lottie:3.4.1' implementation 'com.google.guava:guava:28.2-android' implementation "androidx.collection:collection:1.1.0" @@ -251,7 +226,9 @@ dependencies { // Firebase Cloud Messaging implementation 'com.google.firebase:firebase-messaging:20.2.4' + //Unit testing + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation "androidx.room:room-testing:2.2.5" - - } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 8d43b5f..bdeed4b 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -3,7 +3,7 @@ package="au.gov.health.covidsafe"> - Observable.create { - StreetPassRecordStorage(this).nukeDb() - it.onNext(true) - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe { result -> - Toast.makeText(this, "Database nuked: $result", Toast.LENGTH_SHORT) - .show() - view.isEnabled = true - dialog.cancel() + .setTitle("Are you sure?") + .setCancelable(false) + .setMessage("Deleting the DB records is irreversible") + .setPositiveButton("DELETE") { dialog, _ -> + Observable.create { + StreetPassRecordStorage(this).nukeDb() + it.onNext(true) } - } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe { result -> + Toast.makeText(this, "Database nuked: $result", Toast.LENGTH_SHORT) + .show() + view.isEnabled = true + dialog.cancel() + } + } - .setNegativeButton("DON'T DELETE") { dialog, which -> - view.isEnabled = true - dialog.cancel() - } + .setNegativeButton("DON'T DELETE") { dialog, _ -> + view.isEnabled = true + dialog.cancel() + } val dialog: AlertDialog = builder.create() dialog.show() @@ -100,12 +104,12 @@ class PeekActivity : AppCompatActivity() { shareDatabase.setOnClickListener { val authority = "${BuildConfig.APPLICATION_ID}.fileprovider" - val databaseFilePath= getDatabasePath("record_database").absolutePath + val databaseFilePath = getDatabasePath("record_database").absolutePath val databaseFile = File(databaseFilePath) CentralLog.d(TAG, "authority = $authority, databaseFilePath = $databaseFilePath") - if(databaseFile.exists()) { + if (databaseFile.exists()) { CentralLog.d(TAG, "databaseFile.length = ${databaseFile.length()}") FileProvider.getUriForFile( @@ -123,7 +127,9 @@ class PeekActivity : AppCompatActivity() { } } - if(!BuildConfig.DEBUG) { + showPushTokenOnDebugBuild() + + if (!BuildConfig.DEBUG) { start.visibility = View.GONE stop.visibility = View.GONE delete.visibility = View.GONE @@ -131,6 +137,20 @@ class PeekActivity : AppCompatActivity() { } + private fun showPushTokenOnDebugBuild() { + if (BuildConfig.DEBUG) { + val token = Preference.getFirebaseInstanceID(this) + home_push_notification_token.run { + visibility = View.VISIBLE + val tokenText = "Firebase Push Token: $token" + text = fromHtml(tokenText) + } + + } else { + home_push_notification_token.visibility = View.GONE + } + } + private var timePeriod: Int = 0 private fun nextTimePeriod(): Int { diff --git a/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt b/app/src/debug/java/au/gov/health/covidsafe/RecordListAdapter.kt similarity index 98% rename from app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt rename to app/src/debug/java/au/gov/health/covidsafe/RecordListAdapter.kt index aa47076..141bf76 100644 --- a/app/src/main/java/au/gov/health/covidsafe/RecordListAdapter.kt +++ b/app/src/debug/java/au/gov/health/covidsafe/RecordListAdapter.kt @@ -8,6 +8,7 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord import au.gov.health.covidsafe.streetpass.view.StreetPassRecordViewModel +import au.gov.health.covidsafe.ui.utils.Utils class RecordListAdapter internal constructor(context: Context) : diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 724713b..ed7b59f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ @@ -47,17 +47,21 @@ android:windowSoftInputMode="adjustPan" /> - - - + + + + android:windowSoftInputMode="adjustPan"> - + + + + + @@ -72,8 +76,6 @@ android:name="au.gov.health.covidsafe.services.BluetoothMonitoringService" android:foregroundServiceType="location" /> - - @@ -83,7 +85,7 @@ @@ -92,8 +94,8 @@ + android:exported="true" + android:permission="android.permission.BIND_JOB_SERVICE" /> diff --git a/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt b/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt index 7bdd909..027eae5 100644 --- a/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt +++ b/app/src/main/java/au/gov/health/covidsafe/HomeActivity.kt @@ -3,24 +3,96 @@ package au.gov.health.covidsafe import android.content.Intent import android.os.Bundle import androidx.fragment.app.FragmentActivity -import au.gov.health.covidsafe.Utils.checkInternetConnectionToGoogle +import androidx.lifecycle.MutableLiveData +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.networking.response.Message import au.gov.health.covidsafe.networking.response.MessagesResponse +import au.gov.health.covidsafe.notifications.NotificationBuilder +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.scheduler.GetMessagesScheduler -import au.gov.health.covidsafe.ui.home.HomeFragment +import au.gov.health.covidsafe.ui.devicename.DeviceNameChangePromptActivity +import au.gov.health.covidsafe.ui.utils.Utils +import au.gov.health.covidsafe.utils.NetworkConnectionCheck import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.iid.FirebaseInstanceId private const val TAG = "HomeActivity" -class HomeActivity : FragmentActivity() { - private fun checkInternetConnection() { - checkInternetConnectionToGoogle { - HomeFragment.instanceWeakRef?.get()?.updateConnectionTile(it) +class HomeActivity : FragmentActivity(), NetworkConnectionCheck.NetworkConnectionListener { + + var isAppUpdateAvailableLiveData = MutableLiveData() + var appUpdateAvailableMessageResponseLiveData = MutableLiveData() + var isWindowFocusChangeLiveData = MutableLiveData() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + CentralLog.d(TAG, "onCreate() intent.action = ${intent.action}") + NotificationBuilder.clearPossibleIssueNotificationCheck() + NotificationBuilder.handlePushNotification(this, intent.action) + + setContentView(R.layout.activity_home) + + Utils.startBluetoothMonitoringService(this) + + //Get Firebase Token + getInstanceID() + + NetworkConnectionCheck.addNetworkChangedListener(this, this) + } + + override fun onResume() { + super.onResume() + + checkAndShowDeviceNameChangePrompt() + checkAndUpdateHealthStatus() + } + + private fun checkAndShowDeviceNameChangePrompt() { + if (!Preference.isDeviceNameChangePromptDisplayed(this)) { + Intent(this, DeviceNameChangePromptActivity::class.java).also { + startActivity(it) + } + Preference.setDeviceNameChangePromptDisplayed(this) } } + private fun checkAndUpdateHealthStatus() { + GetMessagesScheduler.scheduleGetMessagesJob { + val isAppWithLatestVersion = it.messages.isNullOrEmpty() + isAppUpdateAvailableLiveData.postValue(isAppWithLatestVersion) + CentralLog.d(TAG, "isAppWithLatestVersion: $it") + + if (!isAppWithLatestVersion) { + val title = getString(R.string.update_available_title) + val body = getString(R.string.update_available_message_android) + + appUpdateAvailableMessageResponseLiveData.postValue(MessagesResponse( + listOf( + Message( + title, + body, + "https://play.google.com/store/apps/details?id=au.gov.health.covidsafe") + ) + )) + } + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + CentralLog.d(TAG, "onNewIntent = ${intent?.action})") + NotificationBuilder.handlePushNotification(this, intent?.action) + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + CentralLog.d(TAG, "onWindowFocusChanged(hasFocus = $hasFocus)") + isWindowFocusChangeLiveData.postValue(hasFocus) + + super.onWindowFocusChanged(hasFocus) + } + /** * Provides notification support to inform users to update to the latest version of the app. * This feature will also allow for troubleshooting of the app in the future and allow for @@ -46,76 +118,12 @@ class HomeActivity : FragmentActivity() { }) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - CentralLog.d(TAG, "onCreate() intent.action = ${intent.action}") - - setContentView(R.layout.activity_home) - - Utils.startBluetoothMonitoringService(this) - - // messages API related - getInstanceID() + private var previousInternetConnection = true + override fun onNetworkStatusChanged(isAvailable: Boolean) { + if (!previousInternetConnection && isAvailable) { + checkAndUpdateHealthStatus() + } + previousInternetConnection = isAvailable } - override fun onResume() { - super.onResume() - - CentralLog.d(TAG, "onResume() intent.action = ${intent.action}") - - if (intent.action == "au.gov.health.covidsafe.UPGRADE_APP") { - Utils.gotoPlayStore(this) - } - - if (!Preference.isDeviceNameChangePromptDisplayed(this)) { - Intent(this, DeviceNameChangePromptActivity::class.java).also { - startActivity(it) - } - - Preference.setDeviceNameChangePromptDisplayed(this) - } - - checkInternetConnection() - - GetMessagesScheduler.scheduleGetMessagesJob { -// HomeFragment.instanceWeakRef?.get()?.updateMessageTiles(it) - -// HomeFragment.instanceWeakRef?.get()?.updateMessageTiles(MessagesResponse( -// listOf( -// Message( -// "Update available", -// "We’ve been making improvements to COVIDSafe. Update via Google Play Store.", -// "market://details?id=au.gov.health.covidsafe"), -// Message( -// "Update available", -// "We’ve been making improvements to COVIDSafe. Update via Google Play Store.", -// "https://play.google.com/store/apps/details?id=au.gov.health.covidsafe") -// ) -// )) - - if (!it.messages.isNullOrEmpty()) { - val title = getString(R.string.update_available_title) - val body = getString(R.string.update_available_message_android) - - HomeFragment.instanceWeakRef?.get()?.updateMessageTiles(MessagesResponse( - listOf( - Message( - title, - body, - "https://play.google.com/store/apps/details?id=au.gov.health.covidsafe") - ) - )) - } - } - } - - override fun onWindowFocusChanged(hasFocus: Boolean) { - CentralLog.d(TAG, "onWindowFocusChanged(hasFocus = $hasFocus)") - - HomeFragment.instanceWeakRef?.get()?.refreshSetupCompleteOrIncompleteUi() - checkInternetConnection() - - super.onWindowFocusChanged(hasFocus) - } } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/TracerApp.kt b/app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt similarity index 93% rename from app/src/main/java/au/gov/health/covidsafe/TracerApp.kt rename to app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt index fb6a71e..71f357e 100644 --- a/app/src/main/java/au/gov/health/covidsafe/TracerApp.kt +++ b/app/src/main/java/au/gov/health/covidsafe/app/TracerApp.kt @@ -1,12 +1,12 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.app import android.app.Application import android.content.Context import android.os.Build +import au.gov.health.covidsafe.BuildConfig import com.atlassian.mobilekit.module.feedback.FeedbackModule import au.gov.health.covidsafe.logging.CentralLog -import au.gov.health.covidsafe.scheduler.GetMessagesScheduler import au.gov.health.covidsafe.services.BluetoothMonitoringService import au.gov.health.covidsafe.streetpass.CentralDevice import au.gov.health.covidsafe.streetpass.PeripheralDevice @@ -23,6 +23,7 @@ class TracerApp : Application() { companion object { + private const val TAG = "TracerApp" const val ORG = BuildConfig.ORG const val protocolVersion = BuildConfig.PROTOCOL_VERSION diff --git a/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt b/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt index b2de214..c0808c1 100644 --- a/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt +++ b/app/src/main/java/au/gov/health/covidsafe/bluetooth/BLEScanner.kt @@ -10,7 +10,7 @@ import android.os.ParcelUuid import android.util.Base64 import android.util.Base64.decode import au.gov.health.covidsafe.BuildConfig -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.bluetooth.BLEScanner.FilterConstant.APPLE_MANUFACTURER_ID import au.gov.health.covidsafe.bluetooth.BLEScanner.FilterConstant.BACKGROUND_IOS_SERVICE_UUID import au.gov.health.covidsafe.logging.CentralLog 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 19ec4eb..d963b49 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 @@ -4,8 +4,8 @@ import android.bluetooth.* import android.bluetooth.BluetoothGatt.GATT_FAILURE import android.bluetooth.BluetoothGatt.GATT_SUCCESS import android.content.Context -import au.gov.health.covidsafe.TracerApp -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.app.TracerApp +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.streetpass.CentralDevice import au.gov.health.covidsafe.streetpass.ConnectionRecord @@ -41,8 +41,7 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) { BluetoothProfile.STATE_CONNECTED -> { CentralLog.i(TAG, "${device?.address} Connected to local GATT server") device?.let { - val b = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT) - .contains(device) + bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(device) } } diff --git a/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt index d99bd0d..2bb72c0 100644 --- a/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt +++ b/app/src/main/java/au/gov/health/covidsafe/boot/StartOnBootReceiver.kt @@ -3,23 +3,33 @@ package au.gov.health.covidsafe.boot import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.scheduler.GetMessagesScheduler + +private const val TAG = "StartOnBootReceiver" class StartOnBootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_BOOT_COMPLETED == intent.action) { - CentralLog.d("StartOnBootReceiver", "boot completed received") + CentralLog.d(TAG, "boot completed received") try { CentralLog.d("StartOnBootReceiver", "Attempting to start service") Utils.scheduleStartMonitoringService(context, 500) + checkAndUpdateHealthStatus() } catch (e: Throwable) { - CentralLog.e("StartOnBootReceiver", e.localizedMessage) + CentralLog.e(TAG, e.localizedMessage ?: "Error message is empty:") e.printStackTrace() } } } + + private fun checkAndUpdateHealthStatus() { + GetMessagesScheduler.scheduleGetMessagesJob { + CentralLog.d(TAG, "checkAndUpdateHealthStatus on app reboot completed ") + } + } } diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt index 297d0ce..08e8101 100644 --- a/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt +++ b/app/src/main/java/au/gov/health/covidsafe/extensions/PermissionExtensions.kt @@ -11,10 +11,9 @@ import android.os.PowerManager import android.provider.Settings import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.ui.utils.Utils import pub.devrel.easypermissions.EasyPermissions import pub.devrel.easypermissions.PermissionRequest @@ -62,6 +61,7 @@ private fun Fragment.requestFineLocationAndCheckBleSupportThenNextPermission(onE } } else { checkBLESupport() + onEndCallback.invoke() } } diff --git a/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt b/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt index 6c67c4e..e3d78e4 100644 --- a/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt +++ b/app/src/main/java/au/gov/health/covidsafe/extensions/ViewExtensions.kt @@ -1,10 +1,15 @@ package au.gov.health.covidsafe.extensions +import android.content.Context +import android.content.Intent +import android.os.Build +import android.text.Html import android.text.SpannableString import android.text.Spanned import android.text.style.URLSpan import android.widget.TextView import androidx.core.content.ContextCompat +import au.gov.health.covidsafe.BuildConfig import au.gov.health.covidsafe.R fun TextView.toHyperlink(textToHyperLink: String? = null, onClick: () -> Unit) { @@ -27,4 +32,35 @@ fun TextView.toHyperlink(textToHyperLink: String? = null, onClick: () -> Unit) { onClick.invoke() } +} + +fun Context.shareThisApp() { + val newIntent = Intent(Intent.ACTION_SEND) + newIntent.type = "text/plain" + newIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_this_app_content)) + startActivity(Intent.createChooser(newIntent, null)) +} + +fun Context.getAppVersionNumberDetails(): String { + return getString(R.string.home_version_number, BuildConfig.VERSION_NAME) +} + +fun fromHtml(stringData: String): Spanned { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(stringData, Html.FROM_HTML_MODE_COMPACT) + } else { + Html.fromHtml(stringData) + } +} + +fun Context.allPermissionsEnabled(): Boolean { + + val bluetoothEnabled = isBlueToothEnabled() ?: false + val nonBatteryOptimizationAllowed = isBatteryOptimizationDisabled() ?: true + val locationStatusAllowed = isLocationPermissionAllowed() ?: true + + return bluetoothEnabled && + nonBatteryOptimizationAllowed && + locationStatusAllowed && + isLocationEnabledOnDevice() } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt b/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt index 684dd22..be6df91 100644 --- a/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt +++ b/app/src/main/java/au/gov/health/covidsafe/factory/NetworkFactory.kt @@ -19,6 +19,8 @@ interface NetworkFactory { RetrofitServiceGenerator.createService(AwsClient::class.java) } + private const val USER_AGENT_TAG = "User-Agent" + val okHttpClient: OkHttpClient by lazy { val okHttpClientBuilder = OkHttpClient.Builder() @@ -26,6 +28,15 @@ interface NetworkFactory { okHttpClientBuilder.addInterceptor(logging) } + okHttpClientBuilder.addInterceptor { chain -> + val request = chain.request() + val newRequest = request.newBuilder() + .removeHeader(USER_AGENT_TAG) + .addHeader(USER_AGENT_TAG, getCustomUserAgent()) + .build() + chain.proceed(newRequest) + } + // This certificate pinning mechanism is only needed on Android 23 and lower. // For Android 24 and above, the pinning is set up in AndroidManifest.xml if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { @@ -52,6 +63,10 @@ interface NetworkFactory { okHttpClientBuilder.build() } + + private fun getCustomUserAgent(): String = "COVIDSafe/${BuildConfig.VERSION_NAME}" + + " (build:${BuildConfig.VERSION_CODE}; android-${Build.VERSION.SDK_INT}) " + okhttp3.internal.userAgent + } } diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetCaseStatisticsUseCase.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetCaseStatisticsUseCase.kt new file mode 100644 index 0000000..19de7f7 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetCaseStatisticsUseCase.kt @@ -0,0 +1,53 @@ +package au.gov.health.covidsafe.interactor.usecase + +import android.content.Context +import androidx.lifecycle.Lifecycle +import au.gov.health.covidsafe.interactor.Either +import au.gov.health.covidsafe.interactor.Failure +import au.gov.health.covidsafe.interactor.Success +import au.gov.health.covidsafe.interactor.UseCase +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.networking.response.CaseStatisticResponse +import au.gov.health.covidsafe.networking.service.AwsClient +import au.gov.health.covidsafe.preference.Preference + +private const val TAG = "GetCaseStatisticsUseCase" + +class GetCaseStatisticsUseCase(private val awsClient: AwsClient, lifecycle: Lifecycle, private val context: Context?) : UseCase(lifecycle) { + + override suspend fun run(params: String): Either { + val token = Preference.getEncrypterJWTToken(context) + return token?.let { jwtToken -> + try { + CentralLog.d(TAG, "GetCaseStatisticsUseCase run request") + val response = retryRetrofitCall { awsClient.getCaseStatistics("Bearer $jwtToken").execute() } + + when { + response?.code() == 200 -> { + response.body()?.let { body -> + CentralLog.d(TAG, "GetCaseStatistics Success: ${body.national}") + Success(body) + } ?: run { + CentralLog.d(TAG, "GetCaseStatistics Invalid response") + Failure(GetCaseStatisticsException.GetGetCaseStatisticsServiceException(response.code())) + } + } + else -> { + CentralLog.d(TAG, "GetCaseStatistics AWSAuthServiceError") + Failure(GetCaseStatisticsException.GetGetCaseStatisticsServiceException(response?.code())) + } + } + } catch (e: Exception) { + Failure(e) + } + } ?: run { + return Failure(Exception()) + } + } +} + +sealed class GetCaseStatisticsException : Exception() { + class GetGetCaseStatisticsServiceException(val code: Int? = null) : GetCaseStatisticsException() +} + + diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt index aceaeab..e9b554c 100644 --- a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt +++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UpdateBroadcastMessageAndPerformScanWithExponentialBackOff.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.lifecycle.Lifecycle import kotlinx.coroutines.delay import retrofit2.Response -import au.gov.health.covidsafe.Preference -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.interactor.Either import au.gov.health.covidsafe.interactor.Failure import au.gov.health.covidsafe.interactor.Success @@ -24,8 +24,8 @@ class UpdateBroadcastMessageAndPerformScanWithExponentialBackOff(private val aws lifecycle: Lifecycle) : UseCase(lifecycle) { override suspend fun run(params: Void?): Either { - val jwtToken = Preference.getEncrypterJWTToken(context) - return jwtToken?.let { jwtToken -> + val token = Preference.getEncrypterJWTToken(context) + return token?.let { jwtToken -> var response = call(jwtToken) var retryCount = 0 while ((response == null || !response.isSuccessful || response.body() == null) && retryCount < RETRIES_LIMIT) { diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt index 2f766b2..4da73d0 100644 --- a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt +++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/UploadData.kt @@ -2,8 +2,8 @@ package au.gov.health.covidsafe.interactor.usecase import android.content.Context import androidx.lifecycle.Lifecycle -import au.gov.health.covidsafe.Preference -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.interactor.Either import au.gov.health.covidsafe.interactor.Failure import au.gov.health.covidsafe.interactor.Success @@ -26,8 +26,8 @@ class UploadData(private val awsClient: AwsClient, private val TAG = this.javaClass.simpleName override suspend fun run(params: String): Either { - val jwtToken = Preference.getEncrypterJWTToken(context) - return jwtToken?.let { jwtToken -> + val token = Preference.getEncrypterJWTToken(context) + return token?.let { jwtToken -> try { val initialUploadResponse = retryRetrofitCall { awsClient.initiateUpload("Bearer $jwtToken", params).execute() diff --git a/app/src/main/java/au/gov/health/covidsafe/links/LinkBuilder.kt b/app/src/main/java/au/gov/health/covidsafe/links/LinkBuilder.kt index e6b4a76..e6e08ba 100644 --- a/app/src/main/java/au/gov/health/covidsafe/links/LinkBuilder.kt +++ b/app/src/main/java/au/gov/health/covidsafe/links/LinkBuilder.kt @@ -10,7 +10,7 @@ import android.text.TextPaint import android.text.style.ClickableSpan import android.view.View import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.logging.CentralLog import java.lang.StringBuilder import java.util.* @@ -18,11 +18,13 @@ import java.util.* const val TAG = "LinkBuilder" -private const val PRIVACY_URL = "https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app" private const val DEPARTMENT_OF_HEALTH_URL = "https://www.health.gov.au/" +private const val HOST_URL = "https://www.covidsafe.gov.au" -private const val HELP_TOPICS_BASE = "https://www.covidsafe.gov.au/help-topics" -private const val HELP_TOPICS_SUFFIX = ".html" +private const val HELP_TOPICS_BASE = "/help-topics" +private const val PRIVACY_TOPICS_BASE = "/privacy-policy" + +private const val TOPICS_EXT_SUFFIX = ".html" private const val HELP_TOPICS_ENGLISH_PAGE = "" private const val HELP_TOPICS_S_CHINESE_PAGE = "/zh-hans" private const val HELP_TOPICS_T_CHINESE_PAGE = "/zh-hant" @@ -36,6 +38,7 @@ private const val HELP_TOPICS_TURKISH_PAGE = "/tr" private const val HELP_TOPICS_ANCHOR_VERIFY_MOBILE_NUMBER_PIN = "#verify-mobile-number-pin" private const val HELP_TOPICS_ANCHOR_BLUETOOTH_PAIRING_REQUEST = "#bluetooth-pairing-request" +private const val HELP_TOPICS_ANCHOR_LOCATION_PERMISSION_ANDROID = "#location-permission-android" object LinkBuilder { @@ -47,9 +50,23 @@ object LinkBuilder { } fun getHelpTopicsUrl(): String { + val url = buildLocalisedURL(HELP_TOPICS_BASE) + CentralLog.d(TAG, "getHelpTopicsUrl() $url") + return url + } + + private fun getPrivacyTopicsUrl(): String { + val url = buildLocalisedURL(PRIVACY_TOPICS_BASE) + CentralLog.d(TAG, "getPrivacyTopicsUrl() $url") + return url + } + + private fun buildLocalisedURL(path: String): String { val localeLanguageTag = Locale.getDefault().toLanguageTag() - val url = HELP_TOPICS_BASE + when { + CentralLog.d(TAG, "Locale Language: $localeLanguageTag") + + return HOST_URL + path + when { localeLanguageTag.startsWith("zh-Hans") -> HELP_TOPICS_S_CHINESE_PAGE localeLanguageTag.startsWith("zh-Hant") -> HELP_TOPICS_T_CHINESE_PAGE localeLanguageTag.startsWith("ar") -> HELP_TOPICS_ARABIC_PAGE @@ -60,22 +77,19 @@ object LinkBuilder { localeLanguageTag.startsWith("pa") -> HELP_TOPICS_PUNJABI_PAGE localeLanguageTag.startsWith("tr") -> HELP_TOPICS_TURKISH_PAGE else -> HELP_TOPICS_ENGLISH_PAGE - } + HELP_TOPICS_SUFFIX + } + TOPICS_EXT_SUFFIX - - CentralLog.d(TAG, "getHelpTopicsUrl() " + - "localeLanguageTag = $localeLanguageTag " + - "url = $url") - - return url } - fun getHelpTopicsUrlWithAnchor(anchor : String) = + fun getHelpTopicsUrlWithAnchor(anchor: String) = getHelpTopicsUrl() + anchor private fun getBluetoothPairingRequestUrl() = getHelpTopicsUrl() + HELP_TOPICS_ANCHOR_BLUETOOTH_PAIRING_REQUEST + private fun getLocationPairingRequestUrl() = + getHelpTopicsUrl() + HELP_TOPICS_ANCHOR_LOCATION_PERMISSION_ANDROID + private fun getVerifyMobileNumberPinLink(linkText: String) = buildHtmlText( "$linkText") @@ -126,15 +140,16 @@ object LinkBuilder { } fun getRegistrationAndPrivacyContent(context: Context): SpannableString { + val privacyUrl = getPrivacyTopicsUrl() return buildSpannableStringContent( context, TracerApp.AppContext.getString(R.string.data_privacy_content), listOf( - PRIVACY_URL, - PRIVACY_URL, + privacyUrl, + privacyUrl, getHelpTopicsUrl(), DEPARTMENT_OF_HEALTH_URL, - PRIVACY_URL + privacyUrl ) ) } @@ -143,7 +158,7 @@ object LinkBuilder { return buildSpannableStringContent( context, TracerApp.AppContext.getString(R.string.permission_success_content), - listOf(getBluetoothPairingRequestUrl()) + listOf(getBluetoothPairingRequestUrl(), getLocationPairingRequestUrl()) ) } @@ -166,7 +181,7 @@ object LinkBuilder { return buildSpannableStringContent( context, TracerApp.AppContext.getString(R.string.upload_step_4_sub_header), - listOf(PRIVACY_URL) + listOf(getPrivacyTopicsUrl()) ) } } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/CaseStatisticResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/CaseStatisticResponse.kt new file mode 100644 index 0000000..006d2b0 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/CaseStatisticResponse.kt @@ -0,0 +1,25 @@ +package au.gov.health.covidsafe.networking.response + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class CaseStatisticResponse(@SerializedName("updated_date") val updatedDate: String?, + @SerializedName("stats_published_status") val statsPublishedStatus: String?, + val national: CaseDetailsData?, + val act: CaseDetailsData?, + val nsw: CaseDetailsData?, + val nt: CaseDetailsData?, + val qld: CaseDetailsData?, + val sa: CaseDetailsData?, + val tas: CaseDetailsData?, + val vic: CaseDetailsData?, + val wa: CaseDetailsData?) + +@Keep +data class CaseDetailsData( + @SerializedName("total_cases") var totalCases: Int?, + @SerializedName("active_cases") var activeCases: Int?, + @SerializedName("new_cases") var newCases: Int?, + @SerializedName("recovered_cases") var recoveredCases: Int?, + var deaths: Int?) \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt b/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt index 8a5fefe..82d43c3 100644 --- a/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt +++ b/app/src/main/java/au/gov/health/covidsafe/networking/service/AwsClient.kt @@ -1,6 +1,7 @@ package au.gov.health.covidsafe.networking.service import au.gov.health.covidsafe.BuildConfig +import au.gov.health.covidsafe.networking.response.CaseStatisticResponse import au.gov.health.covidsafe.networking.request.AuthChallengeRequest import au.gov.health.covidsafe.networking.request.OTPChallengeRequest import au.gov.health.covidsafe.networking.response.* @@ -40,7 +41,11 @@ interface AwsClient { @Query("appversion") appversion: String, @Query("token") token: String, @Query("healthcheck") healthcheck: String, + @Query("encountershealth") encountershealth: String, @Query("preferredlanguages") preferredLanguages: String ): Call + @GET(BuildConfig.END_POINT_PREFIX + "/statistics") + fun getCaseStatistics(@Header("Authorization") jwtToken: String?): Call + } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/notifications/CovidFirebaseMessagingService.kt b/app/src/main/java/au/gov/health/covidsafe/notifications/CovidFirebaseMessagingService.kt new file mode 100644 index 0000000..80d89fa --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/notifications/CovidFirebaseMessagingService.kt @@ -0,0 +1,110 @@ +package au.gov.health.covidsafe.notifications + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import au.gov.health.covidsafe.HomeActivity +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.app.TracerApp +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.ui.utils.Utils.gotoPlayStore +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +private const val TAG = "CovidFirebaseMessagingService" + + +class CovidFirebaseMessagingService : FirebaseMessagingService(), CoroutineScope { + + /** + * Called when message is received. + * + * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. + */ + override fun onMessageReceived(remoteMessage: RemoteMessage) { + // There are two types of messages data messages and notification messages. Data messages are handled + // here in onMessageReceived whether the app is in the foreground or background. Data messages are the type + // traditionally used with GCM. Notification messages are only received here in onMessageReceived when the app + // is in the foreground. When the app is in the background an automatically generated notification is displayed. + // When the user taps on the notification they are returned to the app. Messages containing both notification + // and data payloads are treated as notification messages. The Firebase console always sends notification + // messages. For more see: https://firebase.google.com/docs/cloud-messaging/concept-options + + // Not getting messages here? See why this may be: https://goo.gl/39bRNJ + CentralLog.d(TAG, "onMessageReceived() received message from ${remoteMessage.from}") + + // log notification payload. + remoteMessage.notification?.let { + CentralLog.d(TAG, "onMessageReceived() notification = ${it.title} ${it.body} ${it.clickAction}") + + if (it.clickAction == getString(R.string.notification_click_action_upgrade_app)) { + gotoPlayStore(applicationContext) + } else { + createPushNotification(it) + } + } + + // log data payload. + remoteMessage.data.isNotEmpty().let { + CentralLog.d(TAG, "onMessageReceived() data = " + remoteMessage.data) + } + } + + private fun createPushNotification(notification: RemoteMessage.Notification) { + launch(Dispatchers.Main) { + val intent = Intent(this@CovidFirebaseMessagingService, HomeActivity::class.java) + intent.action = notification.clickAction + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + + val pendingIntent = PendingIntent.getActivity(this@CovidFirebaseMessagingService, 0, intent, PendingIntent.FLAG_ONE_SHOT) + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + val channelId = getString(R.string.default_notification_channel_id) + + val notificationBuilder = NotificationCompat.Builder(this@CovidFirebaseMessagingService, channelId) + .setContentTitle(notification.title) + .setContentText(notification.body) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setSmallIcon(R.drawable.ic_notification_icon) + .setContentIntent(pendingIntent) + .setSound(defaultSoundUri) + .setColor(ContextCompat.getColor(this@CovidFirebaseMessagingService, R.color.notification_tint)) + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + getString(R.string.default_notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(channel) + } + + notificationManager.notify(0, notificationBuilder.build()) + } + } + + /** + * Called when InstanceID token is updated. + */ + override fun onNewToken(token: String) { + CentralLog.d(TAG, "onNewToken() InstanceID = $token") + Preference.putFirebaseInstanceID(TracerApp.AppContext, token) + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.IO + Job() +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationBuilder.kt b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationBuilder.kt new file mode 100644 index 0000000..d3b9852 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationBuilder.kt @@ -0,0 +1,35 @@ +package au.gov.health.covidsafe.notifications + +import android.content.Context +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.ui.utils.Utils + +class NotificationBuilder { + + companion object { + + private var isPushNotificationErrorCheck = false + + fun handlePushNotification(context: Context, action: String?) { + action?.let { + when (it) { + context.getString(R.string.notification_click_action_upgrade_app) -> { + Utils.gotoPlayStore(context) + } + context.getString(R.string.notification_click_action_no_check_in), + context.getString(R.string.notification_click_action_check_possible_encountered_error), + context.getString(R.string.notification_click_action_check_possible_issue) -> { + isPushNotificationErrorCheck = true + } + } + } + } + + fun isShowPossibleIssueNotification() = isPushNotificationErrorCheck + + fun clearPossibleIssueNotificationCheck() { + isPushNotificationErrorCheck = false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt index 7df8b20..4e283f9 100644 --- a/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt +++ b/app/src/main/java/au/gov/health/covidsafe/notifications/NotificationTemplates.kt @@ -45,7 +45,11 @@ class NotificationTemplates { return builder.build() } - fun lackingThingsNotification(context: Context, channel: String): Notification { + fun lackingThingsNotification(context: Context, contentTextResId: Int, channel: String): Notification { + return lackingThingsNotification(context, context.getString(contentTextResId), channel) + } + + fun lackingThingsNotification(context: Context, contentText: String, channel: String): Notification { val intent = Intent(context, HomeActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.putExtra("page", 3) @@ -57,13 +61,13 @@ class NotificationTemplates { val builder = NotificationCompat.Builder(context, channel) .setContentTitle(context.getText(R.string.service_not_ok_title)) - .setContentText(context.getText(R.string.service_not_ok_body)) - .setStyle(NotificationCompat.BigTextStyle().bigText(context.getText(R.string.service_not_ok_body))) + .setContentText(contentText) + .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .setOngoing(true) .setPriority(NotificationCompat.PRIORITY_LOW) .setSmallIcon(R.drawable.ic_notification_warning) - .setTicker(context.getText(R.string.service_not_ok_body)) + .setTicker(contentText) .addAction( R.drawable.ic_notification_setting, context.getText(R.string.service_not_ok_action), diff --git a/app/src/main/java/au/gov/health/covidsafe/Preference.kt b/app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt similarity index 95% rename from app/src/main/java/au/gov/health/covidsafe/Preference.kt rename to app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt index acbd32d..cb32d2e 100644 --- a/app/src/main/java/au/gov/health/covidsafe/Preference.kt +++ b/app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt @@ -1,7 +1,8 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.preference import android.content.Context import android.os.Build +import au.gov.health.covidsafe.R import au.gov.health.covidsafe.security.crypto.EncryptedSharedPreferences import au.gov.health.covidsafe.security.crypto.MasterKeys import au.gov.health.covidsafe.security.crypto.AESEncryptionForPreAndroidM @@ -32,6 +33,7 @@ object Preference { private const val IS_MINOR = "IS_MINOR" private const val POST_CODE = "POST_CODE" private const val AGE = "AGE" + private const val CASE_STATISTIC = "CASESTATISTIC" private const val IS_DEVICE_NAME_CHANGE_PROMPT_DISPLAYED = "IS_DEVICE_NAME_CHANGE_DISPLAYED" fun putDeviceID(context: Context, value: String) { @@ -275,4 +277,14 @@ object Preference { .getBoolean(IS_DEVICE_NAME_CHANGE_PROMPT_DISPLAYED, false) } + fun putCaseStatisticData(context: Context, caseStaticData: String): Boolean { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .edit().putString(CASE_STATISTIC, caseStaticData).commit() + } + + fun getCaseStatisticData(context: Context): String? { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .getString(CASE_STATISTIC, null) + } + } diff --git a/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt index 4cbdfff..7cbda6e 100644 --- a/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt +++ b/app/src/main/java/au/gov/health/covidsafe/receivers/PrivacyCleanerReceiver.kt @@ -7,7 +7,6 @@ import android.content.Context import android.content.Intent import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.PENDING_PRIVACY_CLEANER_CODE -import au.gov.health.covidsafe.services.SensorMonitoringService.Companion.TAG import au.gov.health.covidsafe.status.persistence.StatusRecordStorage import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage import kotlinx.coroutines.CoroutineScope @@ -19,8 +18,6 @@ import kotlin.coroutines.CoroutineContext class PrivacyCleanerReceiver : BroadcastReceiver(), CoroutineScope { - private val TAG = this.javaClass.simpleName - private var job: Job = Job() override val coroutineContext: CoroutineContext @@ -40,7 +37,7 @@ class PrivacyCleanerReceiver : BroadcastReceiver(), CoroutineScope { alarm.setRepeating(AlarmManager.RTC, System.currentTimeMillis(), AlarmManager.INTERVAL_DAY, pendingIntent) } - suspend fun cleanDb(context: Context) { + fun cleanDb(context: Context) { val twentyOneDaysAgo = Calendar.getInstance() twentyOneDaysAgo.set(Calendar.HOUR_OF_DAY, 23) twentyOneDaysAgo.set(Calendar.MINUTE, 59) @@ -53,6 +50,8 @@ class PrivacyCleanerReceiver : BroadcastReceiver(), CoroutineScope { CentralLog.i(TAG, "Street info deleted count : $countStreetDeleted") CentralLog.i(TAG, "Status info deleted count : $countStatusDeleted") } + + const val TAG = "PrivacyCleanerReceiver" } override fun onReceive(context: Context, intent: Intent) { diff --git a/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt b/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt index 3502ff6..bda9314 100644 --- a/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt +++ b/app/src/main/java/au/gov/health/covidsafe/receivers/UpgradeReceiver.kt @@ -3,7 +3,7 @@ package au.gov.health.covidsafe.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.logging.CentralLog class UpgradeReceiver : BroadcastReceiver() { diff --git a/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt b/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt index 3727758..d8225ef 100644 --- a/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt +++ b/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt @@ -7,8 +7,8 @@ import android.app.job.JobService import android.content.ComponentName import android.content.Context import au.gov.health.covidsafe.BuildConfig -import au.gov.health.covidsafe.Preference -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.extensions.isBatteryOptimizationDisabled import au.gov.health.covidsafe.extensions.isBlueToothEnabled import au.gov.health.covidsafe.extensions.isLocationEnabledOnDevice @@ -31,14 +31,16 @@ import java.util.* private const val TAG = "GetMessagesScheduler" private const val GET_MESSAGES_JOB_ID = 1 -private const val TWENTY_FOUR_HOURS_IN_MILLIS = 24 * 60 * 60 * 1000L +private const val ONE_HOURS_IN_MILLIS = 60 * 60 * 1000L +private const val FOUR_HOURS_IN_MILLIS = 4 * ONE_HOURS_IN_MILLIS +private const val TWENTY_FOUR_HOURS_IN_MILLIS = 24 * ONE_HOURS_IN_MILLIS private const val SEVEN_DAYS_IN_MILLIS = 7 * TWENTY_FOUR_HOURS_IN_MILLIS private const val HEALTH_CHECK_RESULT_OK = "OK" private const val HEALTH_CHECK_RESULT_POSSIBLE_ERROR = "POSSIBLE_ERROR" // for testing only -//private const val TWENTY_FOUR_HOURS_IN_MILLIS = 16 * 60 * 1000L +//private const val FOUR_HOURS_IN_MILLIS = 16 * 60 * 1000L class GetMessagesJobSchedulerService : JobService() { private val mostRecentRecordLiveData = StreetPassRecordDatabase.getDatabase(TracerApp.AppContext).recordDao().getMostRecentRecord() @@ -89,19 +91,27 @@ object GetMessagesScheduler { (context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler?) ?.let { CentralLog.d(TAG, "JobScheduler available") + val schedulerInMillis = getRandomMinsInMillis() + FOUR_HOURS_IN_MILLIS + CentralLog.d(TAG, "Next scheduled Jon run in ${(schedulerInMillis / 1000) / 60} mins") it.schedule( JobInfo.Builder(GET_MESSAGES_JOB_ID, ComponentName(context, GetMessagesJobSchedulerService::class.java)) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .setPersisted(true) - .setPeriodic(TWENTY_FOUR_HOURS_IN_MILLIS) + .setPeriodic(schedulerInMillis) .build() ) } } } + private fun getRandomMinsInMillis(): Long { + //We don't want everyone calling at the same time eg 9 am, 1 pm, etc we want to add an offset between 0-30 minutes based upon some random number. + val randomSecs = Random().nextInt(30 * 60) + return randomSecs * 1000L + } + fun getMessages(jobService: JobService? = null, params: JobParameters? = null) { val context = TracerApp.AppContext @@ -127,14 +137,19 @@ object GetMessagesScheduler { isBlueToothEnabled && isBatteryOptimizationDisabled && isLocationPermissionAllowed && - isLocationEnabledOnDevice && - isLastRecordWithinSevenDays + isLocationEnabledOnDevice ) { HEALTH_CHECK_RESULT_OK } else { HEALTH_CHECK_RESULT_POSSIBLE_ERROR } + val encountersHealth = if (isLastRecordWithinSevenDays) { + HEALTH_CHECK_RESULT_OK + } else { + HEALTH_CHECK_RESULT_POSSIBLE_ERROR + } + CentralLog.d(TAG, "healthCheck = $healthCheck") val preferredLanguages = Locale.getDefault().language @@ -145,6 +160,7 @@ object GetMessagesScheduler { appVersion, token, healthCheck, + encountersHealth, preferredLanguages ) diff --git a/app/src/main/java/au/gov/health/covidsafe/security/crypto/AESEncryptionForPreAndroidM.kt b/app/src/main/java/au/gov/health/covidsafe/security/crypto/AESEncryptionForPreAndroidM.kt index 159b68d..f59cfcb 100644 --- a/app/src/main/java/au/gov/health/covidsafe/security/crypto/AESEncryptionForPreAndroidM.kt +++ b/app/src/main/java/au/gov/health/covidsafe/security/crypto/AESEncryptionForPreAndroidM.kt @@ -3,8 +3,8 @@ package au.gov.health.covidsafe.security.crypto import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyProperties import android.util.Base64 -import au.gov.health.covidsafe.Preference -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.app.TracerApp import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.math.BigInteger 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 97868dd..ab78fba 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 @@ -4,24 +4,31 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.location.LocationManager import android.os.Build -import android.os.IBinder import android.os.PowerManager import androidx.annotation.Keep import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LifecycleService import androidx.localbroadcastmanager.content.LocalBroadcastManager -import au.gov.health.covidsafe.* +import au.gov.health.covidsafe.BuildConfig +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.bluetooth.BLEAdvertiser import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STATUS import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STREETPASS import au.gov.health.covidsafe.bluetooth.gatt.STATUS import au.gov.health.covidsafe.bluetooth.gatt.STREET_PASS +import au.gov.health.covidsafe.extensions.isLocationEnabledOnDevice import au.gov.health.covidsafe.factory.NetworkFactory import au.gov.health.covidsafe.interactor.usecase.UpdateBroadcastMessageAndPerformScanWithExponentialBackOff import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.notifications.NotificationTemplates +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.receivers.PrivacyCleanerReceiver import au.gov.health.covidsafe.status.Status import au.gov.health.covidsafe.status.persistence.StatusRecord @@ -39,6 +46,8 @@ import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.C 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 au.gov.health.covidsafe.ui.utils.LocalBlobV2 +import au.gov.health.covidsafe.ui.utils.Utils import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.CoroutineScope @@ -46,15 +55,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import pub.devrel.easypermissions.EasyPermissions -import java.lang.Exception import java.lang.ref.WeakReference import kotlin.coroutines.CoroutineContext +private const val POWER_SAVE_WHITELIST_CHANGED = "android.os.action.POWER_SAVE_WHITELIST_CHANGED" + @Keep class BluetoothMonitoringService : LifecycleService(), CoroutineScope { - private var mNotificationManager: NotificationManager? = null - @Keep private lateinit var serviceUUID: String @@ -78,31 +86,12 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { private lateinit var commandHandler: CommandHandler - private lateinit var mService: SensorMonitoringService - private var mBound: Boolean = false - private lateinit var localBroadcastManager: LocalBroadcastManager private val awsClient = NetworkFactory.awsClient private val gson: Gson = GsonBuilder().disableHtmlEscaping().create() - - /** Defines callbacks for service binding, passed to bindService() */ - private val connection = object : ServiceConnection { - - override fun onServiceConnected(className: ComponentName, service: IBinder) { - // We've bound to LocalService, cast the IBinder and get LocalService instance - val binder = service as SensorMonitoringService.LocalBinder - mService = binder.getService() - mBound = true - } - - override fun onServiceDisconnected(arg0: ComponentName) { - mBound = false - } - } - override fun onCreate() { super.onCreate() localBroadcastManager = LocalBroadcastManager.getInstance(this) @@ -171,6 +160,10 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { return EasyPermissions.hasPermissions(this.applicationContext, *perms) } + private fun isLocationPermissionEnabled(): Boolean { + return hasLocationPermissions() && this.isLocationEnabledOnDevice() + } + private fun isBluetoothEnabled(): Boolean { var btOn = false val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { @@ -204,20 +197,13 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { super.onStartCommand(intent, flags, startId) CentralLog.i(TAG, "Service onStartCommand") - // Bind to LocalService - Intent(this.applicationContext, SensorMonitoringService::class.java).also { intent -> - bindService(intent, connection, Context.BIND_AUTO_CREATE) - } - //check for permissions - if (!hasLocationPermissions() || !isBluetoothEnabled() || !isBatteryOptimizationDisabled()) { + if (!isLocationPermissionEnabled() || !isBluetoothEnabled() || !isBatteryOptimizationDisabled()) { CentralLog.i( TAG, - "location permission: ${hasLocationPermissions()} bluetooth: ${isBluetoothEnabled()}" + "location permission: ${isLocationPermissionEnabled()} bluetooth: ${isBluetoothEnabled()}" ) - val notif = - NotificationTemplates.lackingThingsNotification(this.applicationContext, CHANNEL_ID) - startForeground(NOTIFICATION_ID, notif) + showForegroundNotification() return START_STICKY } @@ -242,14 +228,12 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { CentralLog.i(TAG, "Command is:${cmd?.string}") //check for permissions - if (!hasLocationPermissions() || !isBluetoothEnabled()) { + if (!isLocationPermissionEnabled() || !isBluetoothEnabled()) { CentralLog.i( TAG, - "location permission: ${hasLocationPermissions()} bluetooth: ${isBluetoothEnabled()}" + "location permission: ${isLocationPermissionEnabled()} bluetooth: ${isBluetoothEnabled()}" ) - val notif = - NotificationTemplates.lackingThingsNotification(this.applicationContext, CHANNEL_ID) - startForeground(NOTIFICATION_ID, notif) + showForegroundNotification() return } @@ -326,7 +310,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } } - private fun actionUpdateBm() { Utils.scheduleBMUpdateCheck(this.applicationContext, bmCheckInterval) @@ -345,7 +328,6 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } else { CentralLog.i(TAG, "Don't need to update bm") } - } private fun calcPhaseShift(min: Long, max: Long): Long { @@ -448,11 +430,9 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { CentralLog.i(TAG, "Performing self diagnosis") - if (!hasLocationPermissions() || !isBluetoothEnabled() || !isBatteryOptimizationDisabled()) { + if (!isLocationPermissionEnabled() || !isBluetoothEnabled() || !isBatteryOptimizationDisabled()) { CentralLog.i(TAG, "no location permission") - val notif = - NotificationTemplates.lackingThingsNotification(this.applicationContext, CHANNEL_ID) - startForeground(NOTIFICATION_ID, notif) + showForegroundNotification() return } @@ -480,8 +460,10 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { } else { CentralLog.w( TAG, - "Advertise Schedule present. Should be advertising?: ${advertiser?.shouldBeAdvertising - ?: false}. Is Advertising?: ${advertiser?.isAdvertising ?: false}" + "Advertise Schedule present. Should be advertising?: ${ + advertiser?.shouldBeAdvertising + ?: false + }. Is Advertising?: ${advertiser?.isAdvertising ?: false}" ) } } @@ -498,14 +480,74 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { job.cancel() - if (mBound) { - unbindService(connection) - mBound = false - } - CentralLog.i(TAG, "BluetoothMonitoringService destroyed") } + private fun showForegroundNotification() { + + launch(Dispatchers.Main) { + + val notificationContentText: Int = if (!isLocationPermissionEnabled() && isBluetoothEnabled() && isBatteryOptimizationDisabled()) { + //Location Disabled + R.string.notification_location + } else if (!isBluetoothEnabled() && isLocationPermissionEnabled() && isBatteryOptimizationDisabled()) { + //Bluetooth Disabled + R.string.notification_bluetooth + } else if (!isBatteryOptimizationDisabled() && isLocationPermissionEnabled() && isBluetoothEnabled()) { + //Battery optimization Disabled + R.string.notification_battery + } else if (!isBatteryOptimizationDisabled() || !isLocationPermissionEnabled() || !isBluetoothEnabled()) { + //Multiple permission Disabled + R.string.notification_settings + } else { + //All permission are enabled, so we should show Active message. + -1 + } + + val notificationMessage = if (notificationContentText > 0) { + NotificationTemplates.lackingThingsNotification( + this@BluetoothMonitoringService.applicationContext, + notificationContentText, + CHANNEL_ID) + } else { + //All permissions are enabled + NotificationTemplates.getRunningNotification(this@BluetoothMonitoringService.applicationContext, CHANNEL_ID) + } + + startForeground(NOTIFICATION_ID, notificationMessage) + } + } + + private val gpsSwitchStateReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + intent.action?.let { + if (it == LocationManager.PROVIDERS_CHANGED_ACTION) { + CentralLog.i(TAG, "Location ON/OFF status changed") + showForegroundNotification() + } + } + } + } + + private val powerStateChangeReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + intent.action?.let { + if (it == POWER_SAVE_WHITELIST_CHANGED) { + CentralLog.i(TAG, "Save mode status changed") + showForegroundNotification() + } + } + } + } + + private fun registerLocationChangeReceiver() { + registerReceiver(gpsSwitchStateReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)) + } + + private fun registerPowerModeChangeReceiver() { + registerReceiver(powerStateChangeReceiver, IntentFilter(POWER_SAVE_WHITELIST_CHANGED)) + } + private fun registerReceivers() { val recordAvailableFilter = IntentFilter(ACTION_RECEIVED_STREETPASS) localBroadcastManager.registerReceiver(streetPassReceiver, recordAvailableFilter) @@ -516,6 +558,9 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { val bluetoothStatusReceivedFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) registerReceiver(bluetoothStatusReceiver, bluetoothStatusReceivedFilter) + registerLocationChangeReceiver() + registerPowerModeChangeReceiver() + CentralLog.i(TAG, "Receivers registered") } @@ -534,9 +579,29 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { try { unregisterReceiver(bluetoothStatusReceiver) + } catch (e: Throwable) { CentralLog.w(TAG, "bluetoothStatusReceiver is not registered?") } + + unregisterLocationReceiver() + unregisterPowerStateChangeReceiver() + } + + private fun unregisterLocationReceiver() { + try { + unregisterReceiver(gpsSwitchStateReceiver) + } catch (e: Throwable) { + CentralLog.w(TAG, "Location Receiver is not registered?") + } + } + + private fun unregisterPowerStateChangeReceiver() { + try { + unregisterReceiver(powerStateChangeReceiver) + } catch (e: Throwable) { + CentralLog.w(TAG, "Power State Receiver is not registered?") + } } inner class BluetoothStatusReceiver : BroadcastReceiver() { @@ -549,11 +614,7 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) { BluetoothAdapter.STATE_TURNING_OFF -> { CentralLog.d(TAG, "BluetoothAdapter.STATE_TURNING_OFF") - val notif = NotificationTemplates.lackingThingsNotification( - this@BluetoothMonitoringService.applicationContext, - CHANNEL_ID - ) - startForeground(NOTIFICATION_ID, notif) + showForegroundNotification() teardown() } BluetoothAdapter.STATE_OFF -> { @@ -565,6 +626,7 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { BluetoothAdapter.STATE_ON -> { CentralLog.d(TAG, "BluetoothAdapter.STATE_ON") Utils.startBluetoothMonitoringService(this@BluetoothMonitoringService.applicationContext) + showForegroundNotification() } } } @@ -580,22 +642,10 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { if (ACTION_RECEIVED_STREETPASS == intent.action) { val connRecord: ConnectionRecord? = intent.getParcelableExtra(STREET_PASS) - CentralLog.d( - TAG, - "StreetPass received: $connRecord" - ) + CentralLog.d(TAG, "StreetPass received: $connRecord") if (connRecord != null && connRecord.msg.isNotEmpty()) { - if (mBound) { - val proximity = mService.proximity - val light = mService.light - CentralLog.d( - TAG, - "Sensor values just before saving StreetPassRecord: proximity=$proximity light=$light" - ) - } - val remoteBlob: String = if (connRecord.version == VERSION_ONE) { with(receiver = connRecord) { val plainRecordByteArray = gson.toJson(StreetPassRecordDatabase.Companion.EncryptedRecord( @@ -646,13 +696,15 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { override fun onReceive(context: Context, intent: Intent) { if (ACTION_RECEIVED_STATUS == intent.action) { - val statusRecord: Status = intent.getParcelableExtra(STATUS) - CentralLog.d(TAG, "Status received: ${statusRecord.msg}") + val status: Status? = intent.getParcelableExtra(STATUS) + status?.let { + CentralLog.d(TAG, "Status received: ${it.msg}") - if (statusRecord.msg.isNotEmpty()) { - val statusRecord = StatusRecord(statusRecord.msg) - launch { - statusRecordStorage.saveRecord(statusRecord) + if (it.msg.isNotEmpty()) { + val statusRecord = StatusRecord(it.msg) + launch { + statusRecordStorage.saveRecord(statusRecord) + } } } } @@ -713,6 +765,4 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope { const val blacklistDuration: Long = BuildConfig.BLACKLIST_DURATION } - - } diff --git a/app/src/main/java/au/gov/health/covidsafe/services/CovidFirebaseMessagingService.kt b/app/src/main/java/au/gov/health/covidsafe/services/CovidFirebaseMessagingService.kt deleted file mode 100644 index 39178c5..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/services/CovidFirebaseMessagingService.kt +++ /dev/null @@ -1,53 +0,0 @@ -package au.gov.health.covidsafe.services - -import au.gov.health.covidsafe.* -import au.gov.health.covidsafe.Utils.gotoPlayStore -import au.gov.health.covidsafe.logging.CentralLog -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage - -private const val TAG = "CovidFirebaseMessagingService" - -class CovidFirebaseMessagingService : FirebaseMessagingService() { - - /** - * Called when message is received. - * - * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. - */ - override fun onMessageReceived(remoteMessage: RemoteMessage) { - // There are two types of messages data messages and notification messages. Data messages are handled - // here in onMessageReceived whether the app is in the foreground or background. Data messages are the type - // traditionally used with GCM. Notification messages are only received here in onMessageReceived when the app - // is in the foreground. When the app is in the background an automatically generated notification is displayed. - // When the user taps on the notification they are returned to the app. Messages containing both notification - // and data payloads are treated as notification messages. The Firebase console always sends notification - // messages. For more see: https://firebase.google.com/docs/cloud-messaging/concept-options - - // Not getting messages here? See why this may be: https://goo.gl/39bRNJ - CentralLog.d(TAG, "onMessageReceived() received message from ${remoteMessage.from}") - - // log notification payload. - remoteMessage.notification?.let { - CentralLog.d(TAG, "onMessageReceived() notification = ${it.title} ${it.body} ${it.clickAction}") - - if (it.clickAction == "au.gov.health.covidsafe.UPGRADE_APP"){ - gotoPlayStore(applicationContext) - } - } - - // log data payload. - remoteMessage.data.isNotEmpty().let { - CentralLog.d(TAG, "onMessageReceived() data = " + remoteMessage.data) - } - } - - /** - * Called when InstanceID token is updated. - */ - override fun onNewToken(token: String) { - CentralLog.d(TAG, "onNewToken() InstanceID = $token") - - Preference.putFirebaseInstanceID(TracerApp.AppContext, token) - } -} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt b/app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt deleted file mode 100644 index 6c4abbf..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/services/SensorMonitoringService.kt +++ /dev/null @@ -1,96 +0,0 @@ -package au.gov.health.covidsafe.services - -import android.app.Service -import android.content.Context -import android.content.Intent -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import android.os.Binder -import android.os.IBinder -import au.gov.health.covidsafe.logging.CentralLog -import kotlin.math.sqrt - -class SensorMonitoringService : Service(), SensorEventListener { - private lateinit var sensorManager: SensorManager - private var _light: FloatArray? = null - private var _proximity: FloatArray? = null - private val binder = LocalBinder() - - override fun onCreate() { - super.onCreate() - sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager - - val proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) - val lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) - - if (proximitySensor != null) { - CentralLog.d(TAG, "Proximity sensor: $proximitySensor") - sensorManager.registerListener(this, proximitySensor, SENSOR_DELAY_SUPER_SLOW) - - } else { - CentralLog.d(TAG, "Proximity sensor not available") - } - - if (lightSensor != null) { - CentralLog.d(TAG, "Light sensor: $lightSensor") - sensorManager.registerListener(this, lightSensor, SENSOR_DELAY_SUPER_SLOW) - } else { - CentralLog.d(TAG, "Light sensor not available") - } - - CentralLog.d(TAG, "SensorMonitoringService started") - } - - override fun onDestroy() { - sensorManager.unregisterListener(this) - CentralLog.d(TAG, "SensorMonitoringService destroyed") - super.onDestroy() - } - - inner class LocalBinder : Binder() { - fun getService(): SensorMonitoringService = this@SensorMonitoringService - } - - override fun onBind(intent: Intent): IBinder { - return binder - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { - CentralLog.d(TAG, "Sensor accuracy changed! $sensor") - } - - override fun onSensorChanged(event: SensorEvent) { - when (event.sensor.type) { - Sensor.TYPE_PROXIMITY -> { - _proximity = event.values - } - Sensor.TYPE_LIGHT -> { - _light = event.values - } - else -> { - CentralLog.w(TAG, "Unexpected sensor type changed: ${event.sensor.type}") - } - } - } - - val proximity - get() = if (_proximity != null) { - sqrt((_proximity as FloatArray).reduce { acc: Float, n: Float -> acc + n * n }) - } else { - -1.0f - } - - val light - get() = if (_light != null) { - sqrt((_light as FloatArray).reduce { acc: Float, n: Float -> acc + n * n }) - } else { - -1.0f - } - - companion object { - const val TAG = "SensorMonitoringService" - const val SENSOR_DELAY_SUPER_SLOW = 3_000_000 - } -} diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt index 607e2a1..a07ab45 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/StreetPassScanner.kt @@ -5,7 +5,7 @@ import android.bluetooth.le.ScanResult import android.content.Context import android.os.Build import android.os.Handler -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.bluetooth.BLEScanner import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.status.Status 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 2000c43..4b14549 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 @@ -9,8 +9,8 @@ import android.os.Handler import androidx.annotation.Keep import androidx.localbroadcastmanager.content.LocalBroadcastManager import au.gov.health.covidsafe.BuildConfig -import au.gov.health.covidsafe.TracerApp -import au.gov.health.covidsafe.Utils +import au.gov.health.covidsafe.app.TracerApp +import au.gov.health.covidsafe.ui.utils.Utils import au.gov.health.covidsafe.bluetooth.gatt.* import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.services.BluetoothMonitoringService 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 0281c8e..7de6e06 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 @@ -8,14 +8,12 @@ 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.ui.utils.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 @@ -31,7 +29,7 @@ abstract class StreetPassRecordDatabase : RoomDatabase() { companion object { - private val TAG = this.javaClass.simpleName + private val TAG = this::class.java.simpleName private const val ID_COLUMN_INDEX = 0 private const val TIMESTAMP_COLUMN_INDEX = 1 @@ -147,12 +145,12 @@ abstract class StreetPassRecordDatabase : RoomDatabase() { 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) + val colModelP = if (DUMMY_DEVICE == modelP) null else modelP + val colModelC = if (DUMMY_DEVICE == modelC) null else modelC + val colRssi = if (DUMMY_RSSI == rssi) null else rssi + val colTxPower = if (DUMMY_TXPOWER == txPower) null else txPower + val updatedPlainRecord = gson.toJson(LocalBlobV2(colModelP, colModelC, colRssi, colTxPower)).toByteArray(Charsets.UTF_8) + Encryption.encryptPayload(updatedPlainRecord) } contentValues.put("v", VERSION_TWO) contentValues.put("org", org) diff --git a/app/src/main/java/au/gov/health/covidsafe/talkback/TalkBack.kt b/app/src/main/java/au/gov/health/covidsafe/talkback/TalkBack.kt index e6ab614..4accfdd 100644 --- a/app/src/main/java/au/gov/health/covidsafe/talkback/TalkBack.kt +++ b/app/src/main/java/au/gov/health/covidsafe/talkback/TalkBack.kt @@ -3,7 +3,7 @@ package au.gov.health.covidsafe.talkback import android.view.View import android.widget.TextView import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.logging.CentralLog import java.lang.StringBuilder diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/BaseFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/base/BaseFragment.kt similarity index 90% rename from app/src/main/java/au/gov/health/covidsafe/ui/BaseFragment.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/base/BaseFragment.kt index 6686fb7..97550a8 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/BaseFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/base/BaseFragment.kt @@ -1,4 +1,4 @@ -package au.gov.health.covidsafe.ui +package au.gov.health.covidsafe.ui.base import android.content.res.Configuration import android.os.Bundle @@ -6,8 +6,6 @@ import android.view.View import androidx.fragment.app.Fragment import androidx.navigation.Navigator import androidx.navigation.fragment.NavHostFragment -import au.gov.health.covidsafe.HasBlockingState -import kotlinx.android.synthetic.main.fragment_intro.* open class BaseFragment : Fragment() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/base/BindingAdapter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/base/BindingAdapter.kt new file mode 100644 index 0000000..8ce365a --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/base/BindingAdapter.kt @@ -0,0 +1,56 @@ +package au.gov.health.covidsafe.ui.base + +import android.graphics.Typeface +import android.view.View +import android.widget.TextView +import androidx.databinding.BindingAdapter +import au.gov.health.covidsafe.extensions.fromHtml +import au.gov.health.covidsafe.ui.home.view.ExternalLinkCard +import java.text.SimpleDateFormat +import java.util.* + +@BindingAdapter("visibility") +fun setVisibility(view: View, isVisible: Boolean?) { + isVisible?.let { + view.visibility = if (isVisible) View.VISIBLE else View.GONE + } +} + +@BindingAdapter("addUnderline") +fun setUnderlineText(textView: TextView, text: String?) { + text?.let { + textView.text = fromHtml("$text") + } +} + +@BindingAdapter("externalCardTitle") +fun setExternalCardTitle(view: ExternalLinkCard, text: Int?) { + text?.let { + view.setTitleTextTypeFace(Typeface.DEFAULT_BOLD) + view.setTitleText(String.format("%,d", it)) + } +} + +@BindingAdapter("stateCaseNumberFormat") +fun setStateCaseNumberFormat(view: TextView, text: Int?) { + text?.let { + view.text = String.format("%,d", it) + } +} + +@BindingAdapter("dateFormat") +fun setDateFormat(textView: TextView, dateString: String?) { + dateString?.let { + val cal = Calendar.getInstance() + val timeZoneID = "Australia/Sydney" + val tz = TimeZone.getTimeZone(timeZoneID) + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault()) + dateFormat.timeZone = tz + cal.time = dateFormat.parse(it) + val convertedDateString = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(cal.time) + val convertedTimeString = SimpleDateFormat("h a", Locale.getDefault()).format(cal.time) + + val finalDisplayDateFormat = "$convertedDateString at $convertedTimeString AEST" + textView.text = finalDisplayDateFormat + } +} diff --git a/app/src/main/java/au/gov/health/covidsafe/HasBlockingState.kt b/app/src/main/java/au/gov/health/covidsafe/ui/base/HasBlockingState.kt similarity index 60% rename from app/src/main/java/au/gov/health/covidsafe/HasBlockingState.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/base/HasBlockingState.kt index 21c5fef..dff6068 100644 --- a/app/src/main/java/au/gov/health/covidsafe/HasBlockingState.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/base/HasBlockingState.kt @@ -1,4 +1,4 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.base interface HasBlockingState { var isUiBlocked: Boolean diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/base/PagerChildFragment.kt similarity index 98% rename from app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/base/PagerChildFragment.kt index 0955226..92044dc 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/PagerChildFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/base/PagerChildFragment.kt @@ -1,4 +1,4 @@ -package au.gov.health.covidsafe.ui +package au.gov.health.covidsafe.ui.base import android.view.View import androidx.annotation.StringRes diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/PagerContainer.kt b/app/src/main/java/au/gov/health/covidsafe/ui/base/PagerContainer.kt similarity index 89% rename from app/src/main/java/au/gov/health/covidsafe/ui/PagerContainer.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/base/PagerContainer.kt index 35683a1..c85c3bc 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/PagerContainer.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/base/PagerContainer.kt @@ -1,4 +1,4 @@ -package au.gov.health.covidsafe.ui +package au.gov.health.covidsafe.ui.base import androidx.annotation.StringRes diff --git a/app/src/main/java/au/gov/health/covidsafe/InternetConnectionIssuesActivity.kt b/app/src/main/java/au/gov/health/covidsafe/ui/connection/InternetConnectionIssuesActivity.kt similarity index 91% rename from app/src/main/java/au/gov/health/covidsafe/InternetConnectionIssuesActivity.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/connection/InternetConnectionIssuesActivity.kt index 658f53a..68ce02b 100644 --- a/app/src/main/java/au/gov/health/covidsafe/InternetConnectionIssuesActivity.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/connection/InternetConnectionIssuesActivity.kt @@ -1,8 +1,9 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.connection import android.annotation.SuppressLint import android.os.Bundle import androidx.fragment.app.FragmentActivity +import au.gov.health.covidsafe.R import kotlinx.android.synthetic.main.activity_internet_connection_issues.* import kotlinx.android.synthetic.main.activity_onboarding.toolbar diff --git a/app/src/main/java/au/gov/health/covidsafe/DeviceNameChangePromptActivity.kt b/app/src/main/java/au/gov/health/covidsafe/ui/devicename/DeviceNameChangePromptActivity.kt similarity index 74% rename from app/src/main/java/au/gov/health/covidsafe/DeviceNameChangePromptActivity.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/devicename/DeviceNameChangePromptActivity.kt index 7790b92..20282a4 100644 --- a/app/src/main/java/au/gov/health/covidsafe/DeviceNameChangePromptActivity.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/devicename/DeviceNameChangePromptActivity.kt @@ -1,14 +1,10 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.devicename import android.bluetooth.BluetoothAdapter import android.os.Bundle import android.provider.Settings import androidx.fragment.app.FragmentActivity -import au.gov.health.covidsafe.logging.CentralLog -import au.gov.health.covidsafe.scheduler.GetMessagesScheduler -import au.gov.health.covidsafe.ui.home.HomeFragment -import com.google.android.gms.tasks.OnCompleteListener -import com.google.firebase.iid.FirebaseInstanceId +import au.gov.health.covidsafe.R import kotlinx.android.synthetic.main.activity_device_name_change_prompt.* import kotlinx.android.synthetic.main.fragment_permission_device_name.* diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/CaseNumbersState.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/CaseNumbersState.kt new file mode 100644 index 0000000..f66d80a --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/CaseNumbersState.kt @@ -0,0 +1,8 @@ +package au.gov.health.covidsafe.ui.home + +enum class CaseNumbersState(state: Int) { + LOADING(0), + SUCCESS(1), + ERROR_NO_NETWORK(2), + ERROR_UNKNOWN(3), +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt index d89b10f..2a41691 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HelpFragment.kt @@ -14,7 +14,7 @@ import androidx.core.view.isVisible import androidx.navigation.fragment.findNavController import au.gov.health.covidsafe.R import au.gov.health.covidsafe.links.LinkBuilder -import au.gov.health.covidsafe.ui.BaseFragment +import au.gov.health.covidsafe.ui.base.BaseFragment import com.atlassian.mobilekit.module.feedback.FeedbackModule import kotlinx.android.synthetic.main.fragment_help.* import kotlinx.android.synthetic.main.fragment_help.view.* 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 9adf415..16e1e77 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 @@ -4,11 +4,8 @@ import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.content.* -import android.net.Uri -import android.os.Build +import android.graphics.Typeface import android.os.Bundle -import android.provider.Settings -import android.text.Html import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View @@ -16,92 +13,207 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent -import android.widget.LinearLayout -import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import androidx.core.view.children +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import au.gov.health.covidsafe.* +import au.gov.health.covidsafe.app.TracerApp +import au.gov.health.covidsafe.databinding.FragmentHomeBinding import au.gov.health.covidsafe.extensions.* import au.gov.health.covidsafe.links.LinkBuilder import au.gov.health.covidsafe.logging.CentralLog -import au.gov.health.covidsafe.networking.response.MessagesResponse +import au.gov.health.covidsafe.notifications.NotificationBuilder +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.BaseFragment -import au.gov.health.covidsafe.ui.home.view.ExternalLinkCard +import au.gov.health.covidsafe.ui.base.BaseFragment +import au.gov.health.covidsafe.utils.NetworkConnectionCheck +import kotlinx.android.synthetic.main.fragment_home_case_statistics.* import kotlinx.android.synthetic.main.fragment_home.* -import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home_external_links.* -import kotlinx.android.synthetic.main.fragment_home_setup_complete_header.* -import kotlinx.android.synthetic.main.fragment_home_setup_incomplete_content.* +import kotlinx.android.synthetic.main.fragment_home_header.* +import kotlinx.android.synthetic.main.view_covid_share_tile.* +import kotlinx.android.synthetic.main.view_help_topics_tile.* +import kotlinx.android.synthetic.main.view_home_setup_complete.* +import kotlinx.android.synthetic.main.view_home_setup_incomplete.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import pub.devrel.easypermissions.AppSettingsDialog import pub.devrel.easypermissions.EasyPermissions -import java.lang.ref.WeakReference import java.text.SimpleDateFormat import java.util.* - private const val TAG = "HomeFragment" private const val ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000L private const val FOURTEEN_DAYS_IN_MILLIS = 14 * ONE_DAY_IN_MILLIS -class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { +class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, NetworkConnectionCheck.NetworkConnectionListener { - companion object { - var instanceWeakRef: WeakReference? = null - } - - private lateinit var presenter: HomePresenter + private val homeFragmentViewModel: HomeFragmentViewModel by viewModels() private var mIsBroadcastListenerRegistered = false private var counter: Int = 0 - private val mBroadcastListener: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - if (action == BluetoothAdapter.ACTION_STATE_CHANGED) { - when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) { - BluetoothAdapter.STATE_OFF -> { - bluetooth_card_view.render(formatBlueToothTitle(false), false) - refreshSetupCompleteOrIncompleteUi() - } - BluetoothAdapter.STATE_TURNING_OFF -> { - bluetooth_card_view.render(formatBlueToothTitle(false), false) - refreshSetupCompleteOrIncompleteUi() - } - BluetoothAdapter.STATE_ON -> { - bluetooth_card_view.render(formatBlueToothTitle(true), true) - refreshSetupCompleteOrIncompleteUi() - } - } - } - } + private var checkIsInternetConnected = false + private var isAppWithLatestVersion = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initializeObservers() } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - presenter = HomePresenter(this) - return inflater.inflate(R.layout.fragment_home, container, false) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return FragmentHomeBinding.inflate(layoutInflater).apply { + lifecycleOwner = viewLifecycleOwner + viewModel = homeFragmentViewModel + }.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - view.home_header_help.setOnClickListener { + initializeSettingsNavigation() + setAppVersionNumber() + initializeDebugTestActivity() + initializeNoNetworkError() + initializeRefreshButton() + + initializePullToRefresh() + + NetworkConnectionCheck.addNetworkChangedListener(requireContext(), this) + } + + private fun initializeNoNetworkError() { + no_network_error_text_view.setOnClickListener { + startActivity(Intent(android.provider.Settings.ACTION_WIRELESS_SETTINGS)) + } + } + + private fun initializeRefreshButton() { + refresh_button.setOnClickListener { + initiateFetchingCaseNumbers() + } + } + + private fun initializePullToRefresh() { + swipeRefreshLayout.setOnRefreshListener { + initiateFetchingCaseNumbers() + } + } + + private fun initiateFetchingCaseNumbers() { + lifecycleScope.launch { + homeFragmentViewModel.fetchGetCaseStatistics(lifecycle) + } + } + + private fun initializeObservers() { + (activity as HomeActivity?)?.run { + isAppUpdateAvailableLiveData.observe(this@HomeFragment, latestAppAvailable) + isWindowFocusChangeLiveData.observe(this@HomeFragment, refreshUiObserver) + } + } + + private val latestAppAvailable = Observer { + isAppWithLatestVersion = it + refreshSetupCompleteOrIncompleteUi() + showCovidThanksMessage() + } + + private val refreshUiObserver = Observer { + refreshSetupCompleteOrIncompleteUi() + + if (it) { + initiateFetchingCaseNumbers() + } + } + + override fun onResume() { + super.onResume() + + // disable the app update reminder for now + app_update_reminder.visibility = GONE + + initializePermissionViewButtonClickListeners() + + initializeUploadTestDataNavigation() + + initializeAppShareNavigation() + initializeHelpTopicsNavigation() + initializeChangeLanguageNavigation() + + registerBroadcastListener() + + refreshSetupCompleteOrIncompleteUi() + + updateHealthOfficialTile() + initializeBluetoothPairingInfo() + } + + private fun initializeSettingsNavigation() { + home_header_settings.setOnClickListener { + navigateToSettingsFragment() + } + } + + private fun navigateToSettingsFragment() { + findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToSettingsFragment()) + } + + private fun setAppVersionNumber() { + home_version_number.text = context?.getAppVersionNumberDetails() + } + + private fun initializePermissionViewButtonClickListeners() { + bluetooth_card_view.setOnClickListener { requestBlueToothPermissionThenNextPermission() } + location_card_view.setOnClickListener { askForLocationPermission() } + battery_card_view.setOnClickListener { excludeFromBatteryOptimization() } + } + + private fun initializeUploadTestDataNavigation() { + home_been_tested_button.setOnClickListener { + navigateTo(R.id.action_home_to_selfIsolate) + } + } + + private fun registerBroadcastListener() { + if (!mIsBroadcastListenerRegistered) { + registerBroadcast() + } + } + + private fun initializeAppShareNavigation() { + app_share.setOnClickListener { + context?.shareThisApp() + } + } + + private fun initializeHelpTopicsNavigation() { + help_topics_link.setOnClickListener { HelpFragment.anchor = null findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment()) - } + } + + private fun initializeChangeLanguageNavigation() { + change_language_link.setOnClickListener { + HelpFragment.anchor = "#other-languages" + findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment()) + } + } + + private fun initializeBluetoothPairingInfo() { + home_header_no_bluetooth_pairing.text = LinkBuilder.getNoBluetoothPairingContent(requireContext()) + home_header_no_bluetooth_pairing.movementMethod = LinkMovementMethod.getInstance() + } + + private fun initializeDebugTestActivity() { if (BuildConfig.ENABLE_DEBUG_SCREEN) { - view.header_background.setOnClickListener { + header_background.setOnClickListener { counter++ if (counter >= 2) { counter = 0 @@ -109,69 +221,25 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { } } } - home_version_number.text = getString(R.string.home_version_number, BuildConfig.VERSION_NAME) - } - - override fun onResume() { - super.onResume() - - instanceWeakRef = WeakReference(this) - - // disable the app update reminder for now - app_update_reminder.visibility = GONE - - bluetooth_card_view.setOnClickListener { requestBlueToothPermissionThenNextPermission() } - location_card_view.setOnClickListener { askForLocationPermission() } - battery_card_view.setOnClickListener { excludeFromBatteryOptimization() } - - home_been_tested_button.setOnClickListener { - navigateTo(R.id.action_home_to_selfIsolate) - } - home_setup_complete_share.setOnClickListener { - shareThisApp() - } - home_setup_complete_news.setOnClickListener { - goToNewsWebsite() - } - home_setup_complete_app.setOnClickListener { - goToCovidApp() - } - - help_topics_link.setOnClickListener { - HelpFragment.anchor = null - findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment()) - } - - change_language_link.setOnClickListener { - HelpFragment.anchor = "#other-languages" - findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment()) - } - - if (!mIsBroadcastListenerRegistered) { - registerBroadcast() - } - refreshSetupCompleteOrIncompleteUi() - - home_header_no_bluetooth_pairing.text = LinkBuilder.getNoBluetoothPairingContent(requireContext()) - home_header_no_bluetooth_pairing.movementMethod = LinkMovementMethod.getInstance() - - updateNotificationStatusTile() } override fun onPause() { super.onPause() - instanceWeakRef = null + unregisterAllClickListener() + unregisterBroadcastListener() + } + private fun unregisterAllClickListener() { bluetooth_card_view.setOnClickListener(null) location_card_view.setOnClickListener(null) battery_card_view.setOnClickListener(null) home_been_tested_button.setOnClickListener(null) - home_setup_complete_share.setOnClickListener(null) - home_setup_complete_news.setOnClickListener(null) - home_setup_complete_app.setOnClickListener(null) + app_share.setOnClickListener(null) help_topics_link.setOnClickListener(null) + } + private fun unregisterBroadcastListener() { activity?.let { activity -> if (mIsBroadcastListenerRegistered) { activity.unregisterReceiver(mBroadcastListener) @@ -182,12 +250,26 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { override fun onDestroyView() { super.onDestroyView() + + NetworkConnectionCheck.removeNetworkChangedListener(this) home_root.removeAllViews() } + override fun onDestroy() { + super.onDestroy() + unregisterObservers() + } + + private fun unregisterObservers() { + (activity as HomeActivity?)?.run { + isAppUpdateAvailableLiveData.removeObserver(latestAppAvailable) + isWindowFocusChangeLiveData.removeObserver(refreshUiObserver) + } + } + private fun isDataUploadedInPast14Days(context: Context): Boolean { val isUploaded = Preference.isDataUploaded(context) - + CentralLog.d(TAG, "isDataUploadedInPast14Days : $isUploaded") if (!isUploaded) { return false } @@ -205,95 +287,86 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { @SuppressLint("SetTextI18n") fun refreshSetupCompleteOrIncompleteUi() { - context?.let { - val isAllPermissionsEnabled = allPermissionsEnabled() - val isDataUploadedInPast14Days = isDataUploadedInPast14Days(it) + lifecycleScope.launch(Dispatchers.Main) { + CentralLog.d(TAG, "refreshSetupCompleteOrIncompleteUi") + context?.let { + val isAllPermissionsEnabled = it.allPermissionsEnabled() + if (!isAllPermissionsEnabled) { + NotificationBuilder.clearPossibleIssueNotificationCheck() + } + 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 { - "" + updateSetupCompleteStatus(isAllPermissionsEnabled) + updateSetupCompleteHeaderTitle1(it, isAllPermissionsEnabled, isDataUploadedInPast14Days) + updateSetupCompleteHeaderTitle2(isAllPermissionsEnabled) + updateHealthOfficialTile(isDataUploadedInPast14Days) } - - val line3 = it.getString( - if (isAllPermissionsEnabled) { - R.string.home_header_active_no_action_required - } else { - R.string.home_header_inactive_check_your_permissions - } - ) - - home_header_setup_complete_header_line_1.text = line1 - - home_header_setup_complete_header_line_1.setHeading() - home_header_setup_complete_header_line_1.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) - - val line2and3 = "$line2$line3" - - home_header_setup_complete_header_line_2.text = - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - Html.fromHtml(line2and3, Html.FROM_HTML_MODE_COMPACT) - } else { - Html.fromHtml(line2and3) - } - - if (isAllPermissionsEnabled) { - home_header_top_left_icon.visibility = GONE - home_header_picture_setup_complete.layoutParams.let { layoutParams -> - val size = resources.getDimensionPixelSize(R.dimen.covidsafe_lottie_size) - layoutParams.height = size - layoutParams.width = size - } - home_header_picture_setup_complete.setAnimation("spinner_home.json") - home_header_picture_setup_complete.resumeAnimation() - - 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_top_left_icon.visibility = VISIBLE - home_header_picture_setup_complete.layoutParams.let { layoutParams -> - val size = resources.getDimensionPixelSize(R.dimen.keyline_8) - layoutParams.height = size - layoutParams.width = size - } - home_header_picture_setup_complete.setImageResource(R.drawable.ic_red_cross_no_circle) - - content_setup_incomplete_group.visibility = VISIBLE - updateBlueToothStatus() - updateBatteryOptimizationStatus() - updateLocationStatus() - ContextCompat.getColor(it, R.color.grey).let { bgColor -> - header_background.setBackgroundColor(bgColor) - header_background_overlap.setBackgroundColor(bgColor) - } - } - - home_been_tested_button.visibility = if (isDataUploadedInPast14Days) GONE else VISIBLE } } - private fun allPermissionsEnabled(): Boolean { - val context = requireContext() + private fun updateSetupCompleteHeaderTitle1(context: Context, isAllPermissionsEnabled: Boolean, isDataUploadedInPast14Days: Boolean) { + showLastDataUploadedInfo(context, isDataUploadedInPast14Days) - val bluetoothEnabled = context.isBlueToothEnabled() ?: false - val nonBatteryOptimizationAllowed = context.isBatteryOptimizationDisabled() ?: true - val locationStatusAllowed = context.isLocationPermissionAllowed() ?: true + home_header_setup_complete_header_line_1.run { + text = getString(getCovidActiveStatusMessage(isAllPermissionsEnabled)) + setHeading() + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + } + } - return bluetoothEnabled && - nonBatteryOptimizationAllowed && - locationStatusAllowed && - context.isLocationEnabledOnDevice() + private fun updateSetupCompleteHeaderTitle2(isAllPermissionsEnabled: Boolean) { + if (isAllPermissionsEnabled) { + val isAppPerformanceRequired = !checkIsInternetConnected || !isAppWithLatestVersion + + home_header_setup_complete_header_line_2.run { + setTextColor(ContextCompat.getColor(TracerApp.AppContext, if (isAppPerformanceRequired) R.color.error_red else R.color.slate_black_2)) + typeface = if (isAppPerformanceRequired) Typeface.DEFAULT_BOLD else Typeface.DEFAULT + text = fromHtml(getString(if (isAppPerformanceRequired) R.string.improve else R.string.home_header_active_no_action_required)) + } + + active_status_layout.setOnClickListener { + if (isAppPerformanceRequired) { + navigateToSettingsFragment() + } + } + } + } + + private fun showLastDataUploadedInfo(context: Context, isDataUploadedInPast14Days: Boolean) { + if (isDataUploadedInPast14Days) { + data_last_uploaded_layout.visibility = VISIBLE + val appendUploadInfo = getString(R.string.home_header_uploaded_on_date, getDataUploadDateHtmlString(context)) + home_header_no_last_updated_text_view.text = fromHtml(appendUploadInfo) + } else { + data_last_uploaded_layout.visibility = GONE + } + } + + private fun updateHealthOfficialTile(isDataUploadedInPast14Days: Boolean) { + home_been_tested_button.visibility = if (isDataUploadedInPast14Days) GONE else VISIBLE + } + + private fun updateSetupCompleteStatus(isAllPermissionsEnabled: Boolean) { + if (isAllPermissionsEnabled) { + home_setup_incomplete_permissions_layout.visibility = GONE + home_setup_complete_layout.visibility = VISIBLE + } else { + home_setup_incomplete_permissions_layout.visibility = VISIBLE + home_setup_complete_layout.visibility = GONE + updateBlueToothStatus() + updateBatteryOptimizationStatus() + updateLocationStatus() + } + } + + private fun getCovidActiveStatusMessage(isAllPermissionsEnabled: Boolean): Int { + return if (isAllPermissionsEnabled) { + if (isShowThanksCovidMsg(isAllPermissionsEnabled)) + R.string.home_header_active_title_thanks + else R.string.home_header_active_title + } else { + R.string.home_header_inactive_title + } } private fun registerBroadcast() { @@ -308,32 +381,33 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { } } - private fun shareThisApp() { - val newIntent = Intent(Intent.ACTION_SEND) - newIntent.type = "text/plain" - newIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_this_app_content)) - startActivity(Intent.createChooser(newIntent, null)) - } - private fun updateBlueToothStatus() { requireContext().isBlueToothEnabled()?.let { - bluetooth_card_view.visibility = VISIBLE - bluetooth_card_view.render(formatBlueToothTitle(it), it) + if (!it) { + bluetooth_card_view_layout.visibility = VISIBLE + bluetooth_card_view.render(formatBlueToothTitle(it), it) + } else { + bluetooth_card_view_layout.visibility = GONE + } } ?: run { - bluetooth_card_view.visibility = GONE + bluetooth_card_view_layout.visibility = GONE } } private fun updateBatteryOptimizationStatus() { requireContext().isBatteryOptimizationDisabled()?.let { - battery_card_view.visibility = VISIBLE - battery_card_view.render( - formatNonBatteryOptimizationTitle(!it), - it, - getString(R.string.battery_optimisation_prompt) - ) + if (!it) { + battery_card_view_layout.visibility = VISIBLE + battery_card_view.render( + formatNonBatteryOptimizationTitle(!it), + it, + getString(R.string.battery_optimisation_prompt) + ) + } else { + battery_card_view_layout.visibility = GONE + } } ?: run { - battery_card_view.visibility = GONE + battery_card_view_layout.visibility = GONE } } @@ -341,11 +415,21 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { requireContext().isLocationPermissionAllowed()?.let { val locationWorking = it && requireContext().isLocationEnabledOnDevice() val locationOffPrompts = getString(R.string.home_set_location_why) - - location_card_view.visibility = VISIBLE - location_card_view.render(formatLocationTitle(locationWorking), locationWorking, locationOffPrompts) + if (!locationWorking) { + location_card_view_layout.visibility = VISIBLE + location_card_view.render(formatLocationTitle(locationWorking), locationWorking, locationOffPrompts) + } else { + location_card_view_layout.visibility = GONE + } } ?: run { - location_card_view.visibility = GONE + location_card_view_layout.visibility = GONE + } + } + + private fun updateHealthOfficialTile() { + home_been_tested_button.run { + setTitleTextTypeFace(Typeface.DEFAULT_BOLD) + setContentTextTypeFace(Typeface.DEFAULT_BOLD) } } @@ -361,10 +445,6 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { return resources.getString(R.string.home_non_battery_optimization_permission, getEnabledOrDisabledString(on)) } - private fun formatPushNotificationTitle(on: Boolean): String { - return resources.getString(R.string.home_push_notification_permission, getPermissionEnabledTitle(on)) - } - private fun getPermissionEnabledTitle(on: Boolean): String { return resources.getString(if (on) R.string.home_permission_on else R.string.home_permission_off) } @@ -373,34 +453,6 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { return resources.getString(if (isEnabled) R.string.enabled else R.string.disabled) } - private fun goToNewsWebsite() { - val url = getString(R.string.home_set_complete_external_link_news_url) - try { - Intent(Intent.ACTION_VIEW).run { - data = Uri.parse(url) - startActivity(this) - } - } catch (e: ActivityNotFoundException) { - val intent = Intent(activity, WebViewActivity::class.java) - intent.putExtra(WebViewActivity.URL_ARG, url) - startActivity(intent) - } - } - - private fun goToCovidApp() { - val url = getString(R.string.home_set_complete_external_link_app_url) - try { - Intent(Intent.ACTION_VIEW).run { - data = Uri.parse(url) - startActivity(this) - } - } catch (e: ActivityNotFoundException) { - val intent = Intent(activity, WebViewActivity::class.java) - intent.putExtra(WebViewActivity.URL_ARG, url) - startActivity(intent) - } - } - override fun onPermissionsDenied(requestCode: Int, perms: MutableList) { if (requestCode == LOCATION && EasyPermissions.somePermissionPermanentlyDenied(this, listOf(Manifest.permission.ACCESS_COARSE_LOCATION))) { AppSettingsDialog.Builder(this).build().show() @@ -418,111 +470,44 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) } - fun updateConnectionTile(isInternetConnected: Boolean) { - // called on IO thread; run the UI logic on UI thread - GlobalScope.launch(Dispatchers.Main) { - CentralLog.d(TAG, "updateConnectionTile() isInternetConnected = $isInternetConnected") + private fun isShowThanksCovidMsg(isAllPermissionsEnabled: Boolean): Boolean { + return isAllPermissionsEnabled && checkIsInternetConnected && isAppWithLatestVersion && NotificationBuilder.isShowPossibleIssueNotification() + } - var visibility = if (isInternetConnected) GONE else VISIBLE - - // don't display the tile when there's permission not enabled - if (!allPermissionsEnabled()) { - visibility = GONE - } - - improve_performance_card.visibility = visibility - internet_connection_tile.visibility = visibility - - if (visibility == VISIBLE) { - internet_connection_tile.setOnClickListener { - // startActivity(Intent(ACTION_DATA_ROAMING_SETTINGS)) - // startActivity(Intent(ACTION_WIFI_SETTINGS)) - - startActivity(Intent(requireContext(), InternetConnectionIssuesActivity::class.java)) - } + private fun showCovidThanksMessage() { + activity?.runOnUiThread { + context?.let { + home_header_setup_complete_header_line_1.text = it.getString(getCovidActiveStatusMessage(it.allPermissionsEnabled())) } } } - fun updateMessageTiles(messagesResponse: MessagesResponse) { - GlobalScope.launch(Dispatchers.Main) { - improve_performance_card_linear_layout.children.forEach { - if (it != internet_connection_tile && it != improve_performance_title) { - improve_performance_card_linear_layout.removeView(it) - } - } + override fun onNetworkStatusChanged(isAvailable: Boolean) { + CentralLog.d(TAG, "onNetworkStatusChanged: $checkIsInternetConnected $isAvailable") + checkIsInternetConnected = isAvailable + refreshSetupCompleteOrIncompleteUi() - if (!messagesResponse.messages.isNullOrEmpty()) { - improve_performance_card.visibility = VISIBLE + initiateFetchingCaseNumbers() + } - messagesResponse.messages.forEach { message -> - ExternalLinkCard(requireContext(), null, 0).also { - it.layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ).also { layoutParams -> - layoutParams.setMargins(0, resources.getDimensionPixelSize(R.dimen.divider_height), 0, 0) - } - - it.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white)) - - it.setMessage(message) - it.setErrorTextColor() - - improve_performance_card_linear_layout.addView(it) + private val mBroadcastListener: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (action == BluetoothAdapter.ACTION_STATE_CHANGED) { + when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) { + BluetoothAdapter.STATE_OFF -> { + bluetooth_card_view.render(formatBlueToothTitle(false), false) + refreshSetupCompleteOrIncompleteUi() + } + BluetoothAdapter.STATE_TURNING_OFF -> { + bluetooth_card_view.render(formatBlueToothTitle(false), false) + refreshSetupCompleteOrIncompleteUi() + } + BluetoothAdapter.STATE_ON -> { + refreshSetupCompleteOrIncompleteUi() } } - } else { - improve_performance_card.visibility = GONE } } } - - private fun updateNotificationStatusTile() { - var title = "" - var body = "" - - if (NotificationManagerCompat.from(requireContext()).areNotificationsEnabled() - ) { - notification_status_link.setTopRightIcon(R.drawable.ic_green_tick) - title = getString(R.string.home_set_complete_external_link_notifications_title_iOS) - body = getString(R.string.NotificationsEnabledBlurb) - } else { - notification_status_link.setTopRightIcon(R.drawable.ic_red_cross) - title = getString(R.string.home_set_complete_external_link_notifications_title_iOS_off) - body = getString(R.string.NotificationsEnabledBlurb) - } - - notification_status_link.setTitleBodyAndClickCallback(title, body) { - openAppNotificationSettings() - } - - notification_status_link.setColorForContentWithAction() - } - - private fun openAppNotificationSettings() { - val context = requireContext() - - val intent = Intent().apply { - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { - action = Settings.ACTION_APP_NOTIFICATION_SETTINGS - putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { - action = "android.settings.APP_NOTIFICATION_SETTINGS" - putExtra("app_package", context.packageName) - putExtra("app_uid", context.applicationInfo.uid) - } - else -> { - action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - addCategory(Intent.CATEGORY_DEFAULT) - data = Uri.parse("package:" + context.packageName) - } - } - } - - context.startActivity(intent) - } - } diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt new file mode 100644 index 0000000..8c5e464 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomeFragmentViewModel.kt @@ -0,0 +1,88 @@ +package au.gov.health.covidsafe.ui.home + +import android.app.Application +import android.content.Context +import androidx.lifecycle.* +import au.gov.health.covidsafe.factory.RetrofitServiceGenerator +import au.gov.health.covidsafe.interactor.usecase.GetCaseStatisticsUseCase +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.networking.response.CaseStatisticResponse +import au.gov.health.covidsafe.extensions.isInternetAvailable +import au.gov.health.covidsafe.networking.service.AwsClient +import au.gov.health.covidsafe.preference.Preference +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +private const val TAG = "HomeFragmentViewModel" + +class HomeFragmentViewModel(application: Application) : AndroidViewModel(application) { + + val caseNumberDataState = MutableLiveData() + val caseStatisticsLiveData = MutableLiveData() + val isRefreshing = MutableLiveData() + + val awsClient: AwsClient by lazy { + RetrofitServiceGenerator.createService(AwsClient::class.java) + } + + fun fetchGetCaseStatistics(lifecycle: Lifecycle) { + if(caseNumberDataState.value != CaseNumbersState.LOADING) { + caseNumberDataState.value = CaseNumbersState.LOADING + + viewModelScope.launch(Dispatchers.IO) { + + GetCaseStatisticsUseCase(awsClient, lifecycle, getApplication()).invoke("", + onSuccess = { + updateOnSuccess(it) + }, + onFailure = { + CentralLog.e(TAG, "On Failure: ${it.message}") + showErrorMessage() + } + ) + } + } else { + isRefreshing.value = false + } + } + + private fun updateOnSuccess(caseStatisticResponse: CaseStatisticResponse) { + viewModelScope.launch { + isRefreshing.value = false + + CentralLog.d(TAG, "On Success: ${caseStatisticResponse.vic?.totalCases}") + caseNumberDataState.value = CaseNumbersState.SUCCESS + cacheCaseStatisticDataInPersistent(caseStatisticResponse) + caseStatisticsLiveData.value = caseStatisticResponse + } + } + + private fun showErrorMessage() { + viewModelScope.launch { + + isRefreshing.value = false + + val context = getApplication() as Context + caseStatisticsLiveData.value = getCachedCaseStatisticDataFromPersistent(context) + if (context.isInternetAvailable()) { + caseNumberDataState.value = CaseNumbersState.ERROR_UNKNOWN + } else { + caseNumberDataState.value = CaseNumbersState.ERROR_NO_NETWORK + } + } + } + + private fun cacheCaseStatisticDataInPersistent(caseStatisticResponse: CaseStatisticResponse) { + Preference.putCaseStatisticData(getApplication(), Gson().toJson(caseStatisticResponse)) + } + + private fun getCachedCaseStatisticDataFromPersistent(context: Context): CaseStatisticResponse? { + val caseStatisticString = Preference.getCaseStatisticData(context) + return caseStatisticString?.let { + val caseStatisticData = Gson().fromJson(it, CaseStatisticResponse::class.java) + CentralLog.d(TAG, "On Preference: ${caseStatisticData.vic?.totalCases}") + caseStatisticData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt deleted file mode 100644 index e5a5aa4..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/HomePresenter.kt +++ /dev/null @@ -1,10 +0,0 @@ -package au.gov.health.covidsafe.ui.home - -import androidx.lifecycle.LifecycleObserver - -class HomePresenter(fragment: HomeFragment) : LifecycleObserver { - - init { - fragment.lifecycle.addObserver(this) - } -} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt index 5bda36f..a474d55 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt @@ -3,6 +3,7 @@ package au.gov.health.covidsafe.ui.home.view import android.content.Context import android.content.Intent import android.content.res.TypedArray +import android.graphics.Typeface import android.net.Uri import android.text.Html import android.util.AttributeSet @@ -26,24 +27,32 @@ class ExternalLinkCard @JvmOverloads constructor( attrs?.let { val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ExternalLinkCard) - val icon = a.getDrawable(R.styleable.ExternalLinkCard_external_linkCard_icon) - val iconVisible = a.getBoolean(R.styleable.ExternalLinkCard_external_linkCard_icon_visible, true) + val startIcon = a.getDrawable(R.styleable.ExternalLinkCard_external_linkCard_start_icon) + val startIconVisible = a.getBoolean(R.styleable.ExternalLinkCard_external_linkCard_start_icon_visible, true) val title = a.getString(R.styleable.ExternalLinkCard_external_linkCard_title) val content = a.getString(R.styleable.ExternalLinkCard_external_linkCard_content) - val padding = a.getDimension(R.styleable.ExternalLinkCard_external_linkCard_icon_padding, 0f).toInt() - val iconBackground = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_icon_background, R.color.transparent) + val padding = a.getDimension(R.styleable.ExternalLinkCard_external_linkCard_start_icon_padding, 0f).toInt() + val contentPadding = a.getDimension(R.styleable.ExternalLinkCard_external_linkCard_content_padding, 0f).toInt() + val iconBackground = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_start_icon_background, R.color.transparent) val textColorResId = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_text_color, R.color.slack_black) + val endIconVisible = a.getBoolean(R.styleable.ExternalLinkCard_external_linkCard_end_icon_visible, true) val textColor = ContextCompat.getColor(context, textColorResId) - external_link_round_image.setImageDrawable(icon) - external_link_round_image.visibility = if (iconVisible) View.VISIBLE else View.GONE + external_link_round_image.setImageDrawable(startIcon) + external_link_round_image.visibility = if (startIconVisible) View.VISIBLE else View.GONE external_link_round_image.setBackgroundResource(iconBackground) external_link_round_image.setPadding(padding, padding, padding, padding) + external_link_headline.text = title + external_link_content.text = content + if (contentPadding > 0) { + external_link_content.setPadding(0, contentPadding, 0, 0) + } + + external_link_end_image_view.visibility = if (endIconVisible) View.VISIBLE else View.GONE setTextColor(textColor) - a.recycle() } } @@ -53,19 +62,33 @@ class ExternalLinkCard @JvmOverloads constructor( external_link_content.setTextColor(textColor) val icChevron = - if (textColor == ContextCompat.getColor(context, R.color.error)) { - R.drawable.ic_chevron_right_red - } else { - R.drawable.ic_chevron_right + when (textColor) { + ContextCompat.getColor(context, R.color.error_red) -> { + R.drawable.ic_chevron_right_red + } + ContextCompat.getColor(context, R.color.white) -> { + R.drawable.ic_chevron_right_white + } + else -> { + R.drawable.ic_chevron_right_black + } } - next_arrow.setImageDrawable( + external_link_end_image_view.setImageDrawable( ContextCompat.getDrawable(context, icChevron).also { it?.isAutoMirrored = true } ) } + fun setTitleTextTypeFace(typeface: Typeface) { + external_link_headline.typeface = typeface + } + + fun setContentTextTypeFace(typeface: Typeface) { + external_link_content.typeface = typeface + } + fun setMessage(message: Message) { external_link_round_image.visibility = View.GONE @@ -79,6 +102,10 @@ class ExternalLinkCard @JvmOverloads constructor( } } + fun setTitleText(title: String) { + external_link_headline.text = title + } + fun setTitleBodyAndClickCallback(title: String, body: String, clickCallBack: () -> Unit) { external_link_headline.text = title external_link_content.text = body @@ -89,11 +116,11 @@ class ExternalLinkCard @JvmOverloads constructor( } fun setErrorTextColor() { - setTextColor(ContextCompat.getColor(context, R.color.error)) + setTextColor(ContextCompat.getColor(context, R.color.error_red)) } fun setTopRightIcon(iconResID: Int) { - next_arrow.setImageResource(iconResID) + external_link_end_image_view.setImageResource(iconResID) } fun setColorForContentWithAction() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt index c5300db..1a2da83 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/PermissionStatusCard.kt @@ -9,7 +9,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.content.ContextCompat import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.app.TracerApp import kotlinx.android.synthetic.main.view_card_permission_card.view.* class PermissionStatusCard @JvmOverloads constructor( @@ -32,7 +32,7 @@ class PermissionStatusCard @JvmOverloads constructor( } fun render(title: String, correct: Boolean, body: String? = null) { - val errorTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.error) + val errorTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.error_red) val normalTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.slack_black) permission_icon.isSelected = correct diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryListRecyclerView.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryListRecyclerView.kt index 0cf0b59..088ddba 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryListRecyclerView.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryListRecyclerView.kt @@ -7,9 +7,9 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.app.TracerApp const val VIEW_TYPE_GROUP_TITLE = 1 const val VIEW_TYPE_COUNTRY = 2 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 9de88d5..c80536d 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 @@ -8,11 +8,11 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import au.gov.health.covidsafe.HasBlockingState -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.ui.base.HasBlockingState +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.ui.PagerContainer -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerContainer +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import com.github.razir.progressbutton.bindProgressButton import com.github.razir.progressbutton.hideProgress import com.github.razir.progressbutton.showProgress diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt index 208bc53..b3b9f76 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/dataprivacy/DataPrivacyFragment.kt @@ -9,8 +9,8 @@ import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.links.LinkBuilder import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_data_privacy.* import kotlinx.android.synthetic.main.fragment_data_privacy.root import kotlinx.android.synthetic.main.fragment_data_privacy.view.* diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt index 7c672b1..bbcd66d 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt @@ -16,12 +16,12 @@ import androidx.annotation.NavigationRes import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.widget.addTextChangedListener -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import au.gov.health.covidsafe.ui.onboarding.CountryCodeSelectionActivity import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_CALLING_CODE import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_CHALLENGE_NAME @@ -50,7 +50,7 @@ class EnterNumberFragment : PagerChildFragment() { private var callingCode: Int = 0 private var nationalFlagResID: Int = 0 - private val errorTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.error) + private val errorTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.error_red) private val normalTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.slack_black) private fun updateSelectedCountry() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt index 4a12968..e74b047 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberPresenter.kt @@ -4,7 +4,7 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.enternumber import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R import au.gov.health.covidsafe.extensions.isInternetAvailable import au.gov.health.covidsafe.factory.NetworkFactory diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt index 1d82d49..0ba0262 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt @@ -1,6 +1,7 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.enterpin import android.app.AlertDialog +import android.graphics.Color import android.os.Bundle import android.os.CountDownTimer import android.text.method.LinkMovementMethod @@ -9,15 +10,14 @@ import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.annotation.NavigationRes -import androidx.core.content.ContextCompat import androidx.core.widget.doOnTextChanged import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.Utils.announceForAccessibility +import au.gov.health.covidsafe.ui.utils.Utils.announceForAccessibility import au.gov.health.covidsafe.extensions.toHyperlink import au.gov.health.covidsafe.links.LinkBuilder import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import com.atlassian.mobilekit.module.core.utils.SystemUtils import kotlinx.android.synthetic.main.fragment_enter_pin.* import kotlinx.android.synthetic.main.fragment_enter_pin.view.* @@ -107,23 +107,15 @@ class EnterPinFragment : PagerChildFragment() { } else { "$numberOfSecondsInt" } - enter_pin_timer_value?.text = "$numberOfMinsInt:$finalNumberOfSecondsString" } - override fun onFinish() { enter_pin_timer_value?.text = "0:00" - enter_pin_resend_pin.isEnabled = true - activity?.let { - enter_pin_resend_pin.setLinkTextColor(ContextCompat.getColor(it, R.color.hyperlink_enabled)) - } + setupTimer(false) } } stopWatch?.start() - enter_pin_resend_pin.isEnabled = false - activity?.let { - enter_pin_resend_pin.setLinkTextColor(ContextCompat.getColor(it, R.color.hyperlink_disabled)) - } + setupTimer(true) } fun resetTimer() { @@ -200,4 +192,10 @@ class EnterPinFragment : PagerChildFragment() { .setPositiveButton(android.R.string.yes, null).show() } + private fun setupTimer(isTimerOn: Boolean) { + enter_pin_resend_pin.visibility = if (isTimerOn) View.GONE else View.VISIBLE + enter_pin_timer_label.text = if (isTimerOn) context?.getString(R.string.enter_pin_timer_expire) else context?.getString(R.string.CodeHasExpired) + enter_pin_timer_label.setTextColor(if (isTimerOn) Color.BLACK else Color.RED) + enter_pin_timer_value.visibility = if (isTimerOn) View.VISIBLE else View.INVISIBLE + } } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt index 6ac28d9..6b23a39 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinPresenter.kt @@ -4,7 +4,7 @@ import android.text.TextUtils import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.extensions.isInternetAvailable import au.gov.health.covidsafe.factory.NetworkFactory import au.gov.health.covidsafe.interactor.usecase.GetOnboardingOtp diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt index ba1ee11..db74d29 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/howitworks/HowItWorksFragment.kt @@ -9,8 +9,8 @@ import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.links.LinkBuilder import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_how_it_works.* import kotlinx.android.synthetic.main.fragment_how_it_works.view.* diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt index d99a2c1..a904a41 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/introduction/IntroductionFragment.kt @@ -7,8 +7,8 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_intro.* class IntroductionFragment : PagerChildFragment() { 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 index 63c1478..d546997 100644 --- 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 @@ -7,11 +7,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_permission.root import kotlinx.android.synthetic.main.fragment_permission_device_name.* 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 795395d..5278a9d 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 @@ -4,6 +4,7 @@ import android.Manifest import android.app.Activity import android.content.Intent import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.PowerManager @@ -13,13 +14,13 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import androidx.core.content.ContextCompat import au.gov.health.covidsafe.HomeActivity -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.extensions.* import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_permission.* import pub.devrel.easypermissions.EasyPermissions @@ -74,7 +75,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb private fun navigateToNextPage() { navigationStarted = false if (hasAllPermissionsAndBluetoothOn()) { - navigateTo(R.id.action_permissionFragment_to_permissionDeviceNameFragment ) + navigateTo(R.id.action_permissionFragment_to_permissionDeviceNameFragment) } else { navigateToMainActivity() } @@ -82,9 +83,25 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb private fun hasAllPermissionsAndBluetoothOn(): Boolean { val context = TracerApp.AppContext - return context.isBlueToothEnabled() == true - && requiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } - && ContextCompat.getSystemService(context, PowerManager::class.java)?.isIgnoringBatteryOptimizations(context.packageName) ?: true + return context.isBlueToothEnabled() == true && checkAllRequiredPermissions() && checkIgnoreBatteryOptimization() + } + + private fun checkAllRequiredPermissions(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val context = TracerApp.AppContext + requiredPermissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } + } else { + true + } + } + + private fun checkIgnoreBatteryOptimization(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val context = TracerApp.AppContext + ContextCompat.getSystemService(context, PowerManager::class.java)?.isIgnoringBatteryOptimizations(context.packageName) ?: true + } else { + true + } } private fun 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 118663f..ed0b2ea 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 @@ -11,8 +11,8 @@ import au.gov.health.covidsafe.HomeActivity import au.gov.health.covidsafe.R import au.gov.health.covidsafe.links.LinkBuilder import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_permission_success.* class PermissionSuccessFragment : PagerChildFragment() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt index 6712920..822d974 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsFragment.kt @@ -13,12 +13,12 @@ import android.widget.ArrayAdapter import android.widget.TextView.OnEditorActionListener import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.R import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment import kotlinx.android.synthetic.main.fragment_personal_details.* import java.util.regex.Pattern @@ -27,7 +27,7 @@ import java.util.regex.Pattern private const val TAG = "PersonalDetailsFragment" private val POST_CODE_REGEX = Pattern.compile("^(?:(?:[2-8]\\d|9[0-7]|0?[28]|0?9(?=09))(?:\\d{2}))$") -private val NAME_REGEX = Pattern.compile("^[A-Za-z0-9\\u00C0-\\u017F][A-Za-z'0-9\\-\\u00C0-\\u017F ]{0,80}\$") +private val NAME_REGEX = Pattern.compile("^[.A-Za-z0-9\\u00C0-\\u017F][.A-Za-z'0-9\\-\\u00C0-\\u017F ]{0,80}\$") class PersonalDetailsFragment : PagerChildFragment() { @@ -121,7 +121,7 @@ class PersonalDetailsFragment : PagerChildFragment() { } } - personal_details_name.setOnEditorActionListener(OnEditorActionListener { textView, actionId, event -> + personal_details_name.setOnEditorActionListener(OnEditorActionListener { textView, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_NEXT) { hideKeyboard() textView.clearFocus() diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationconsent/RegistrationConsentFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationconsent/RegistrationConsentFragment.kt index 0987b28..f042c21 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationconsent/RegistrationConsentFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/registrationconsent/RegistrationConsentFragment.kt @@ -7,9 +7,9 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.PagerContainer -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.PagerContainer +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_registration_consent.* class RegistrationConsentFragment : PagerChildFragment() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt index 8edc727..17ed26d 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/undersixteen/UnderSixteenFragment.kt @@ -8,8 +8,8 @@ import android.view.accessibility.AccessibilityEvent import androidx.core.os.bundleOf import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment import kotlinx.android.synthetic.main.fragment_under_sixteen.* diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsFragment.kt new file mode 100644 index 0000000..f163705 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsFragment.kt @@ -0,0 +1,226 @@ +package au.gov.health.covidsafe.ui.settings + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.view.children +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import au.gov.health.covidsafe.HomeActivity +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.extensions.allPermissionsEnabled +import au.gov.health.covidsafe.extensions.getAppVersionNumberDetails +import au.gov.health.covidsafe.extensions.shareThisApp +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.networking.response.MessagesResponse +import au.gov.health.covidsafe.ui.base.BaseFragment +import au.gov.health.covidsafe.ui.connection.InternetConnectionIssuesActivity +import au.gov.health.covidsafe.ui.home.HelpFragment +import au.gov.health.covidsafe.ui.home.view.ExternalLinkCard +import au.gov.health.covidsafe.utils.NetworkConnectionCheck +import com.atlassian.mobilekit.module.feedback.FeedbackModule +import kotlinx.android.synthetic.main.fragment_settings.* +import kotlinx.android.synthetic.main.fragment_settings_external_link.* +import kotlinx.android.synthetic.main.view_covid_share_tile.* +import kotlinx.android.synthetic.main.view_help_topics_tile.* +import kotlinx.android.synthetic.main.view_settings_improve_app_performance_tile.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +private const val TAG = "SettingsFragment" + +class SettingsFragment : BaseFragment(), NetworkConnectionCheck.NetworkConnectionListener { + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_settings, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initializeToolbarNavigation() + initializeObservers() + NetworkConnectionCheck.addNetworkChangedListener(requireContext(), this) + } + + private fun initializeToolbarNavigation() { + settings_toolbar.setNavigationOnClickListener { findNavController().popBackStack() } + } + + private fun initializeObservers() { + (activity as HomeActivity?)?.run { + appUpdateAvailableMessageResponseLiveData.observe(viewLifecycleOwner, latestAppAvailable) + } + } + + override fun onResume() { + super.onResume() + + updateNotificationStatusTile() + initializeHelpTopicsNavigation() + initializeSupportNavigation() + initializeAppShareNavigation() + setAppVersionNumber() + } + + private fun initializeHelpTopicsNavigation() { + help_topics_link.setOnClickListener { + HelpFragment.anchor = null + findNavController().navigate(SettingsFragmentDirections.actionSettingsFragmentToHelpFragment()) + } + } + + private fun initializeSupportNavigation() { + settings_support_view.setOnClickListener { + FeedbackModule.showFeedbackScreen() + } + } + + private fun initializeAppShareNavigation() { + app_share.setOnClickListener { + context?.shareThisApp() + } + } + + private fun setAppVersionNumber() { + settings_version_number.text = context?.getAppVersionNumberDetails() + } + + private val latestAppAvailable = Observer { + updateMessageTiles(it) + } + + override fun onDestroyView() { + super.onDestroyView() + + NetworkConnectionCheck.removeNetworkChangedListener(this) + removeObservers() + } + + private fun removeObservers() { + (activity as HomeActivity?)?.run { + appUpdateAvailableMessageResponseLiveData.removeObserver(latestAppAvailable) + } + } + + private fun updateNotificationStatusTile() { + val title: String + val body: String + + if (NotificationManagerCompat.from(requireContext()).areNotificationsEnabled()) { + notification_status_link.setTopRightIcon(R.drawable.ic_green_tick) + title = getString(R.string.home_set_complete_external_link_notifications_title_iOS) + body = getString(R.string.NotificationsEnabledBlurb) + } else { + notification_status_link.setTopRightIcon(R.drawable.ic_white_background_red_error) + title = getString(R.string.home_set_complete_external_link_notifications_title_iOS_off) + body = getString(R.string.NotificationsEnabledBlurb) + } + + notification_status_link.setTitleBodyAndClickCallback(title, body) { + openAppNotificationSettings() + } + + notification_status_link.setColorForContentWithAction() + } + + private fun updateInternetConnectionTile(isInternetConnected: Boolean) { + // called on IO thread; run the UI logic on UI thread + GlobalScope.launch(Dispatchers.Main) { + context?.let { + CentralLog.d(TAG, "updateConnectionTile() isInternetConnected = $isInternetConnected") + + var visibility = if (isInternetConnected) View.GONE else View.VISIBLE + + // don't display the tile when there's permission not enabled + val isAllPermissionsEnabled = it.allPermissionsEnabled() + if (!isAllPermissionsEnabled) { + visibility = View.GONE + } + + improve_performance_group.visibility = visibility + internet_connection_tile.visibility = visibility + + if (visibility == View.VISIBLE) { + internet_connection_tile.setOnClickListener { + startActivity(Intent(requireContext(), InternetConnectionIssuesActivity::class.java)) + } + } + } + } + } + + private fun updateMessageTiles(messagesResponse: MessagesResponse) { + GlobalScope.launch(Dispatchers.Main) { + improve_performance_card_linear_layout.children.forEach { + if (it != internet_connection_tile && it != improve_performance_title) { + improve_performance_card_linear_layout.removeView(it) + } + } + + if (!messagesResponse.messages.isNullOrEmpty()) { + improve_performance_group.visibility = View.VISIBLE + + messagesResponse.messages.forEach { message -> + ExternalLinkCard(requireContext(), null, 0).also { + it.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).also { layoutParams -> + layoutParams.setMargins(0, resources.getDimensionPixelSize(R.dimen.divider_height), 0, 0) + } + + it.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white)) + + it.setMessage(message) + it.setErrorTextColor() + + improve_performance_card_linear_layout.addView(it) + } + } + } else { + improve_performance_group.visibility = View.GONE + } + } + } + + private fun openAppNotificationSettings() { + val context = requireContext() + + val intent = Intent().apply { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { + action = "android.settings.APP_NOTIFICATION_SETTINGS" + putExtra("app_package", context.packageName) + putExtra("app_uid", context.applicationInfo.uid) + } + else -> { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addCategory(Intent.CATEGORY_DEFAULT) + data = Uri.parse("package:" + context.packageName) + } + } + } + + context.startActivity(intent) + } + + override fun onNetworkStatusChanged(isAvailable: Boolean) { + CentralLog.d(TAG, "Settings onNetworkStatusChanged: $isAvailable") + updateInternetConnectionTile(isAvailable) + } +} \ 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/ui/splash/SplashActivity.kt similarity index 92% rename from app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/splash/SplashActivity.kt index acb16e6..ca4dcb3 100644 --- a/app/src/main/java/au/gov/health/covidsafe/SplashActivity.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/splash/SplashActivity.kt @@ -1,4 +1,4 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.splash import android.content.Intent import android.os.Bundle @@ -10,12 +10,11 @@ import android.view.View.VISIBLE import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import au.gov.health.covidsafe.HomeActivity +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.R 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() { diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt index c1db2be..df9cf4a 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/UploadContainerFragment.kt @@ -9,8 +9,8 @@ import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.ui.PagerContainer -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerContainer +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import com.github.razir.progressbutton.hideProgress import com.github.razir.progressbutton.showProgress import kotlinx.android.synthetic.main.fragment_upload_master.* @@ -26,9 +26,8 @@ class UploadContainerFragment : Fragment(), PagerContainer { activity?.onBackPressed() } - if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - toolbar.navigationIcon = - requireContext().getDrawable(R.drawable.ic_up_rtl) + if (resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL) { + toolbar.navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up_rtl) } } @@ -51,13 +50,13 @@ class UploadContainerFragment : Fragment(), PagerContainer { } } - override fun refreshButton(uploadButtonLayout: UploadButtonLayout) { - when (uploadButtonLayout) { + override fun refreshButton(updateButtonLayout: UploadButtonLayout) { + when (updateButtonLayout) { is UploadButtonLayout.ContinueLayout -> { upload_continue.setOnClickListener { - uploadButtonLayout.buttonListener?.invoke() + updateButtonLayout.buttonListener?.invoke() } - upload_continue.setText(uploadButtonLayout.buttonText) + upload_continue.setText(updateButtonLayout.buttonText) upload_continue.visibility = VISIBLE upload_answerNo.setOnClickListener(null) upload_answerYes.setOnClickListener(null) @@ -68,10 +67,10 @@ class UploadContainerFragment : Fragment(), PagerContainer { upload_continue.setOnClickListener(null) upload_continue.visibility = GONE upload_answerNo.setOnClickListener { - uploadButtonLayout.buttonNoListener.invoke() + updateButtonLayout.buttonNoListener.invoke() } upload_answerYes.setOnClickListener { - uploadButtonLayout.buttonYesListener.invoke() + updateButtonLayout.buttonYesListener.invoke() } upload_answerNo.visibility = VISIBLE upload_answerYes.visibility = VISIBLE 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 0cd0d23..4132eaa 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 @@ -7,8 +7,8 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_upload_finished.* class UploadFinishedFragment : PagerChildFragment() { 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 481e2a6..698c588 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 @@ -7,8 +7,8 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_upload_initial.* import kotlinx.android.synthetic.main.fragment_upload_page_4.root @@ -45,5 +45,4 @@ class UploadInitialFragment : PagerChildFragment() { super.onDestroyView() root.removeAllViews() } - } diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadStepFourFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/UploadStepFourFragment.kt index 584e691..6972f96 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 @@ -10,8 +10,8 @@ import android.view.accessibility.AccessibilityEvent import au.gov.health.covidsafe.R import au.gov.health.covidsafe.links.LinkBuilder import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import kotlinx.android.synthetic.main.fragment_upload_page_4.* class UploadStepFourFragment : PagerChildFragment() { 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 205c42e..86ce4bb 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 @@ -11,8 +11,8 @@ import androidx.core.os.bundleOf import androidx.core.widget.doOnTextChanged import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading -import au.gov.health.covidsafe.ui.PagerChildFragment -import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.base.PagerChildFragment +import au.gov.health.covidsafe.ui.base.UploadButtonLayout import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment import au.gov.health.covidsafe.ui.view.UploadingDialog import au.gov.health.covidsafe.ui.view.UploadingErrorDialog diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt index 494bb1d..97b709a 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinPresenter.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import au.gov.health.covidsafe.BuildConfig -import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.preference.Preference import au.gov.health.covidsafe.extensions.isInternetAvailable import au.gov.health.covidsafe.factory.NetworkFactory import au.gov.health.covidsafe.interactor.usecase.UploadData diff --git a/app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt b/app/src/main/java/au/gov/health/covidsafe/ui/utils/LocalBlobV2.kt similarity index 70% rename from app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/utils/LocalBlobV2.kt index e6a6b71..3f9b0ab 100644 --- a/app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/utils/LocalBlobV2.kt @@ -1,3 +1,3 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.utils 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/Utils.kt b/app/src/main/java/au/gov/health/covidsafe/ui/utils/Utils.kt similarity index 95% rename from app/src/main/java/au/gov/health/covidsafe/Utils.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/utils/Utils.kt index b7dfafa..6fb9ceb 100644 --- a/app/src/main/java/au/gov/health/covidsafe/Utils.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/utils/Utils.kt @@ -1,4 +1,4 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.utils import android.Manifest import android.bluetooth.BluetoothAdapter @@ -12,6 +12,9 @@ import android.provider.Settings import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager import androidx.localbroadcastmanager.content.LocalBroadcastManager +import au.gov.health.covidsafe.BuildConfig +import au.gov.health.covidsafe.preference.Preference +import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.bluetooth.gatt.* import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.scheduler.Scheduler @@ -29,7 +32,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.io.File -import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketAddress import java.text.SimpleDateFormat import java.util.* @@ -295,17 +300,21 @@ object Utils { private fun checkInternetConnectionToURL(url: String, callback: (Boolean) -> Unit) { GlobalScope.launch(Dispatchers.IO) { - var reachable = false - reachable = try { - InetAddress.getByName(url).isReachable(2000) + val reachable = try { +// InetAddress.getByName(url).isReachable(2000) + val timeoutMs = 2000 + val sock = Socket() + val sockaddr: SocketAddress = InetSocketAddress(url, 53) + sock.connect(sockaddr, timeoutMs) + sock.close() + true } catch (e: Exception) { CentralLog.w(TAG, "checkInternetConnectionToURL() failed to reach the url $url") false } CentralLog.w(TAG, "checkInternetConnectionToURL() reachability to url $url is $reachable") - callback(reachable) } } diff --git a/app/src/main/java/au/gov/health/covidsafe/WebViewActivity.kt b/app/src/main/java/au/gov/health/covidsafe/ui/webview/WebViewActivity.kt similarity index 93% rename from app/src/main/java/au/gov/health/covidsafe/WebViewActivity.kt rename to app/src/main/java/au/gov/health/covidsafe/ui/webview/WebViewActivity.kt index 1ae927d..91f8f03 100644 --- a/app/src/main/java/au/gov/health/covidsafe/WebViewActivity.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/webview/WebViewActivity.kt @@ -1,10 +1,11 @@ -package au.gov.health.covidsafe +package au.gov.health.covidsafe.ui.webview import android.os.Bundle import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import androidx.fragment.app.FragmentActivity +import au.gov.health.covidsafe.R import au.gov.health.covidsafe.logging.CentralLog class WebViewActivity : FragmentActivity() { diff --git a/app/src/main/java/au/gov/health/covidsafe/utils/NetworkConnectionCheck.kt b/app/src/main/java/au/gov/health/covidsafe/utils/NetworkConnectionCheck.kt new file mode 100644 index 0000000..1be10c1 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/utils/NetworkConnectionCheck.kt @@ -0,0 +1,93 @@ +package au.gov.health.covidsafe.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import android.os.Build +import au.gov.health.covidsafe.ui.utils.Utils +import kotlin.collections.ArrayList + + +object NetworkConnectionCheck { + + private var networkConnectionListener: ArrayList? = null + + private var isInternetConnected:Boolean? = null + + fun addNetworkChangedListener(context: Context, listener: NetworkConnectionListener) { + if (networkConnectionListener == null) { + networkConnectionListener = ArrayList() + registerNetworkChange(context) + } + + isInternetConnected?.let { + listener.onNetworkStatusChanged(it) + } + + networkConnectionListener?.add(listener) + } + + fun removeNetworkChangedListener(listener: NetworkConnectionListener) { + networkConnectionListener?.let { + val i: Int = it.indexOf(listener) + if (i >= 0) { + it.removeAt(i) + } + } + } + + private fun registerNetworkChange(context: Context) { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivityManager.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + it.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + networkStatusUpdate(true) + } + + override fun onLost(network: Network?) { + networkStatusUpdate(false) + } + }) + } else { + it.registerNetworkCallback(NetworkRequest.Builder().build(), + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network?) { + networkStatusUpdate(true) + } + + override fun onLost(network: Network?) { + networkStatusUpdate(false) + } + }) + } + } + } + + private fun networkStatusUpdate(isAvailable: Boolean) { + if (!isAvailable) { + isInternetConnected = isAvailable + sendNetworkStatusToListeners(isAvailable) + } else { + checkInternetConnection() + } + } + + private fun checkInternetConnection() { + Utils.checkInternetConnectionToGoogle { + isInternetConnected = it + sendNetworkStatusToListeners(it) + } + } + + private fun sendNetworkStatusToListeners(isAvailable: Boolean) { + networkConnectionListener?.forEach { + it.onNetworkStatusChanged(isAvailable) + } + } + + interface NetworkConnectionListener { + fun onNetworkStatusChanged(isAvailable: Boolean) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/background_circular_grey.xml b/app/src/main/res/drawable/background_circular_grey.xml new file mode 100644 index 0000000..cfdf8b8 --- /dev/null +++ b/app/src/main/res/drawable/background_circular_grey.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_circular_white.xml b/app/src/main/res/drawable/background_circular_white.xml new file mode 100644 index 0000000..522eb16 --- /dev/null +++ b/app/src/main/res/drawable/background_circular_white.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right_black.xml similarity index 100% rename from app/src/main/res/drawable/ic_chevron_right.xml rename to app/src/main/res/drawable/ic_chevron_right_black.xml diff --git a/app/src/main/res/drawable/ic_chevron_right_white.xml b/app/src/main/res/drawable/ic_chevron_right_white.xml new file mode 100644 index 0000000..8dbbbab --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_confirmed_cases.xml b/app/src/main/res/drawable/ic_confirmed_cases.xml new file mode 100644 index 0000000..81e8976 --- /dev/null +++ b/app/src/main/res/drawable/ic_confirmed_cases.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 0000000..25e12d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_help_question.xml b/app/src/main/res/drawable/ic_help_question.xml new file mode 100644 index 0000000..3c05fc4 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_question.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mail_support.xml b/app/src/main/res/drawable/ic_mail_support.xml new file mode 100644 index 0000000..556fedf --- /dev/null +++ b/app/src/main/res/drawable/ic_mail_support.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_question_circle.xml b/app/src/main/res/drawable/ic_question_circle.xml deleted file mode 100644 index 8ecb6d4..0000000 --- a/app/src/main/res/drawable/ic_question_circle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_red_cross_no_circle.xml b/app/src/main/res/drawable/ic_red_cross_no_circle.xml deleted file mode 100644 index c80fa06..0000000 --- a/app/src/main/res/drawable/ic_red_cross_no_circle.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_red_error.xml b/app/src/main/res/drawable/ic_red_error.xml new file mode 100644 index 0000000..6193793 --- /dev/null +++ b/app/src/main/res/drawable/ic_red_error.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_settings_outline_black.xml b/app/src/main/res/drawable/ic_settings_outline_black.xml new file mode 100644 index 0000000..57fc02c --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_outline_black.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_trending_up.xml b/app/src/main/res/drawable/ic_trending_up.xml new file mode 100644 index 0000000..a51dbf3 --- /dev/null +++ b/app/src/main/res/drawable/ic_trending_up.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_red_cross.xml b/app/src/main/res/drawable/ic_white_background_red_error.xml similarity index 80% rename from app/src/main/res/drawable/ic_red_cross.xml rename to app/src/main/res/drawable/ic_white_background_red_error.xml index ad1318f..83da97b 100644 --- a/app/src/main/res/drawable/ic_red_cross.xml +++ b/app/src/main/res/drawable/ic_white_background_red_error.xml @@ -7,16 +7,16 @@ android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0" android:fillColor="#A31919"/> diff --git a/app/src/main/res/drawable/icon_checkbox.xml b/app/src/main/res/drawable/icon_checkbox.xml deleted file mode 100644 index fa2c9eb..0000000 --- a/app/src/main/res/drawable/icon_checkbox.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/phone_number_invalid_background.xml b/app/src/main/res/drawable/phone_number_invalid_background.xml index 6a24df1..39c7107 100644 --- a/app/src/main/res/drawable/phone_number_invalid_background.xml +++ b/app/src/main/res/drawable/phone_number_invalid_background.xml @@ -3,6 +3,6 @@ android:shape="rectangle"> + android:color="@color/error_red" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_device_name_change_prompt.xml b/app/src/main/res/layout/activity_device_name_change_prompt.xml index 0246abf..1273bd2 100644 --- a/app/src/main/res/layout/activity_device_name_change_prompt.xml +++ b/app/src/main/res/layout/activity_device_name_change_prompt.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" xmlns:tools="http://schemas.android.com/tools" - tools:context=".DeviceNameChangePromptActivity"> + tools:context=".ui.devicename.DeviceNameChangePromptActivity"> + + + + - - - - - - - - + app:layout_constraintTop_toBottomOf="@+id/home_push_notification_token" /> - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + + + android:layout_height="match_parent" + android:background="@color/screen_background"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:background="@color/white" + android:orientation="vertical" + android:visibility="gone" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" + tools:visibility="visible"> + android:layout_height="@dimen/space_12" + android:background="@color/error_red" /> - + - + -