From 0388f9885a8f8949c4579061e5c8ab9288ddad22 Mon Sep 17 00:00:00 2001 From: COVIDSafe Support <64945427+covidsafe-support@users.noreply.github.com> Date: Tue, 2 Feb 2021 11:04:40 +1100 Subject: [PATCH] COVIDSafe code from version 2.1 (#44) --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 2 + .../au/gov/health/covidsafe/HomeActivity.kt | 1 + .../usecase/GetCaseStatisticsUseCase.kt | 4 +- .../interactor/usecase/UploadData.kt | 22 +- .../gov/health/covidsafe/links/LinkBuilder.kt | 63 +++++- .../request/ChangePostcodeRequest.kt | 6 + .../response/CaseStatisticResponse.kt | 8 +- .../response/UploadPostcodeResponse.kt | 6 + .../covidsafe/networking/service/AwsClient.kt | 7 + .../health/covidsafe/preference/Preference.kt | 11 + .../covidsafe/sensor/ble/BLEDevice.java | 3 +- .../sensor/ble/ConcreteBLESensor.java | 9 +- .../covidsafe/sensor/datatype/Base64.java | 3 +- .../sensor/datatype/PseudoDeviceAddress.java | 73 +------ .../services/BluetoothMonitoringService.kt | 2 +- .../persistence/StreetPassRecordDao.kt | 5 + .../persistence/StreetPassRecordStorage.kt | 8 + .../health/covidsafe/ui/home/HomeFragment.kt | 91 +++++++- .../ui/home/HomeFragmentViewModel.kt | 115 +++++++++- .../health/covidsafe/ui/home/StateAdapter.kt | 67 ++++++ .../personal/PersonalDetailsFragment.kt | 4 +- .../ui/settings/ChangePostCodeActivity.kt | 61 ++++++ .../covidsafe/ui/settings/SettingsFragment.kt | 7 + .../ui/settings/SettingsViewModel.kt | 59 +++++ .../presentation/VerifyUploadPinFragment.kt | 4 +- .../presentation/VerifyUploadPinPresenter.kt | 6 +- .../covidsafe/ui/view/UploadingErrorDialog.kt | 8 +- app/src/main/res/drawable/circle.xml | 10 + app/src/main/res/drawable/ic_activity.xml | 13 ++ .../main/res/drawable/ic_alert_triangle.xml | 27 +++ app/src/main/res/drawable/ic_check.xml | 13 ++ .../main/res/drawable/ic_external_link.xml | 27 +++ app/src/main/res/drawable/ic_illustration.xml | 67 ++++++ app/src/main/res/drawable/ic_map_pin.xml | 20 ++ app/src/main/res/drawable/ic_postcode.xml | 20 ++ .../res/layout/activity_change_postcode.xml | 203 ++++++++++++++++++ .../res/layout/dialog_error_uploading.xml | 18 +- app/src/main/res/layout/fragment_home.xml | 75 ++++++- .../layout/fragment_home_case_statistics.xml | 33 ++- .../main/res/layout/fragment_home_header.xml | 52 ++--- .../fragment_settings_external_link.xml | 63 +++++- .../main/res/layout/view_list_item_state.xml | 57 +++++ .../layout/view_national_case_statistics.xml | 1 - .../res/layout/view_state_case_statistics.xml | 199 ++++++++++++++++- app/src/main/res/values-ar/strings.xml | 81 ++++++- app/src/main/res/values-el-rGR/strings.xml | 116 ++++++++-- app/src/main/res/values-it-rIT/strings.xml | 125 ++++++++--- app/src/main/res/values-ko/strings.xml | 99 +++++++-- app/src/main/res/values-pa-rIN/strings.xml | 80 ++++++- app/src/main/res/values-tr/strings.xml | 81 ++++++- app/src/main/res/values-vi/strings.xml | 81 ++++++- app/src/main/res/values-zh-rCN/strings.xml | 81 ++++++- app/src/main/res/values-zh-rTW/strings.xml | 81 ++++++- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 84 +++++++- app/src/main/res/values/type.xml | 8 + 58 files changed, 2241 insertions(+), 235 deletions(-) create mode 100644 app/src/main/java/au/gov/health/covidsafe/networking/request/ChangePostcodeRequest.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/networking/response/UploadPostcodeResponse.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/home/StateAdapter.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/settings/ChangePostCodeActivity.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsViewModel.kt create mode 100644 app/src/main/res/drawable/circle.xml create mode 100644 app/src/main/res/drawable/ic_activity.xml create mode 100644 app/src/main/res/drawable/ic_alert_triangle.xml create mode 100644 app/src/main/res/drawable/ic_check.xml create mode 100644 app/src/main/res/drawable/ic_external_link.xml create mode 100644 app/src/main/res/drawable/ic_illustration.xml create mode 100644 app/src/main/res/drawable/ic_map_pin.xml create mode 100644 app/src/main/res/drawable/ic_postcode.xml create mode 100644 app/src/main/res/layout/activity_change_postcode.xml create mode 100644 app/src/main/res/layout/view_list_item_state.xml diff --git a/app/build.gradle b/app/build.gradle index bc01b32..585ea4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "au.gov.health.covidsafe" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 94 - versionName "2.0" + versionCode 106 + versionName "2.1" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2934ee6..4737aa9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,8 @@ + + try { CentralLog.d(TAG, "GetCaseStatisticsUseCase run request") - val response = retryRetrofitCall { awsClient.getCaseStatistics("Bearer $jwtToken").execute() } + val response = retryRetrofitCall { + awsClient.getCaseStatisticsVersion2("Bearer $jwtToken").execute() + } when { response?.code() == 200 -> { 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 4da73d0..2227b95 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 @@ -33,11 +33,13 @@ class UploadData(private val awsClient: AwsClient, awsClient.initiateUpload("Bearer $jwtToken", params).execute() } if (initialUploadResponse == null) { - Failure(UploadDataException.UploadDataIncorrectPinException) + // No data in response + Failure(Exception("100")) } else if (initialUploadResponse.isSuccessful) { val uploadLink = initialUploadResponse.body()?.uploadLink if (uploadLink.isNullOrEmpty()) { - Failure(Exception()) + // Upload link is null or empty + Failure(Exception("104")) } else { zipAndUploadData(uploadLink) } @@ -46,13 +48,16 @@ class UploadData(private val awsClient: AwsClient, } else if (initialUploadResponse.code() == 403) { Failure(UploadDataException.UploadDataJwtExpiredException) } else { - Failure(Exception()) + // any other status code + Failure(Exception(initialUploadResponse.code().toString())) } } catch (e: Exception) { - Failure(e) + // unable to parse success response + Failure(Exception("101")) } } ?: run { - return Failure(Exception()) + // Error getting db context + return Failure(Exception("001")) } } @@ -69,15 +74,16 @@ class UploadData(private val awsClient: AwsClient, return try { val response = retryOkhttpCall { okHttpClient.newCall(request).execute() } return if (response == null) { - Failure(Exception()) + // Error creating url obj + Failure(Exception("102")) } else { Success(None) } } catch (e: Exception) { - Failure(Exception()) + // Error encoding data to json + Failure(Exception("003")) } } - } sealed class UploadDataException : Exception() { 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 4328df6..27f1944 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 @@ -12,10 +12,10 @@ import android.view.View import au.gov.health.covidsafe.R import au.gov.health.covidsafe.app.TracerApp import au.gov.health.covidsafe.logging.CentralLog +import java.lang.Exception import java.lang.StringBuilder import java.util.* - const val TAG = "LinkBuilder" private const val DEPARTMENT_OF_HEALTH_URL = "https://www.health.gov.au/" @@ -23,6 +23,7 @@ private const val HOST_URL = "https://www.covidsafe.gov.au" private const val HELP_TOPICS_BASE = "/help-topics" private const val PRIVACY_TOPICS_BASE = "/privacy-policy" +private const val COLLECTION_NOTICE_BASE = "/collection-notice" private const val TOPICS_EXT_SUFFIX = ".html" private const val HELP_TOPICS_ENGLISH_PAGE = "" @@ -39,6 +40,8 @@ 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" +private const val HELP_TOPICS_ANCHOR_READ_MORE = "#covidsafe-working-correctly" +private const val HELP_TOPICS_DELETE_INFORMATION = "#delete-information" object LinkBuilder { @@ -61,6 +64,12 @@ object LinkBuilder { return url } + private fun getCollectionNoticeUrl(): String { + val url = buildLocalisedURL(COLLECTION_NOTICE_BASE) + CentralLog.d(TAG, "getCollectionNoticeUrl() $url") + return url + } + fun getCollectionMassageLearnMore(context: Context): SpannableString { return buildSpannableStringContent( context, @@ -109,6 +118,12 @@ object LinkBuilder { private fun getVerifyMobileNumberPinLink(linkText: String) = buildHtmlText( "$linkText") + private fun getReadMoreLink(linkText: String) = buildHtmlText( + "$linkText") + + private fun getHotspotLink(linkText: String?, link: String) = buildHtmlText( + "$linkText") + class LinkSpan(private val context: Context, private val linkURL: String) : ClickableSpan() { override fun onClick(widget: View) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(linkURL)) @@ -141,9 +156,12 @@ object LinkBuilder { val retVal = SpannableString(stringBuilder.toString().trim()) spanStartEndIndex.forEachIndexed { index, pair -> - retVal.setSpan(LinkSpan(context, links[index]), pair.first, pair.second, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + try { + retVal.setSpan(LinkSpan(context, links[index]), pair.first, pair.second, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } catch (e: Exception) { + return retVal + } } - return retVal } @@ -163,13 +181,37 @@ object LinkBuilder { listOf( privacyUrl, privacyUrl, - getHelpTopicsUrl(), + getHelpTopicsUrl() + HELP_TOPICS_DELETE_INFORMATION, HOST_URL, privacyUrl ) ) } + fun getPostcodeContent(context: Context): SpannableString { + val privacyUrl = getPrivacyTopicsUrl() + return buildSpannableStringContent( + context, + TracerApp.AppContext.getString(R.string.change_postcode_intro), + listOf( + privacyUrl, + getCollectionNoticeUrl() + ) + ) + } + + fun getPostcodeUpdatedSuccessfullyContent(context: Context): SpannableString { + val privacyUrl = getPrivacyTopicsUrl() + return buildSpannableStringContent( + context, + TracerApp.AppContext.getString(R.string.permission_success_content), + listOf( + privacyUrl, + getLocationPairingRequestUrl() + ) + ) + } + fun getHowPermissionSuccessContent(context: Context): SpannableString { return buildSpannableStringContent( context, @@ -185,6 +227,19 @@ object LinkBuilder { } } + fun getReadMore(): Spanned { + val linkText = TracerApp.AppContext.getString(R.string.low_handshakes_link) + return getReadMoreLink(linkText).also { + CentralLog.d(TAG, "getReadMore returns $it") + } + } + + fun getHotSpot(title: String?, link: String): Spanned { + return getHotspotLink(title,link).also { + CentralLog.d(TAG, "getReadMore returns $it") + } + } + fun getNoBluetoothPairingContent(context: Context): SpannableString { return buildSpannableStringContent( context, diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/request/ChangePostcodeRequest.kt b/app/src/main/java/au/gov/health/covidsafe/networking/request/ChangePostcodeRequest.kt new file mode 100644 index 0000000..ee1eca5 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/networking/request/ChangePostcodeRequest.kt @@ -0,0 +1,6 @@ +package au.gov.health.covidsafe.networking.request + +import androidx.annotation.Keep + +@Keep +data class ChangePostcodeRequest(val postcode: String?) \ 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 index 006d2b0..32962fb 100644 --- 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 @@ -14,7 +14,8 @@ data class CaseStatisticResponse(@SerializedName("updated_date") val updatedDate val sa: CaseDetailsData?, val tas: CaseDetailsData?, val vic: CaseDetailsData?, - val wa: CaseDetailsData?) + val wa: CaseDetailsData?, + val version: Int? ) @Keep data class CaseDetailsData( @@ -22,4 +23,9 @@ data class CaseDetailsData( @SerializedName("active_cases") var activeCases: Int?, @SerializedName("new_cases") var newCases: Int?, @SerializedName("recovered_cases") var recoveredCases: Int?, + @SerializedName("overseas_acquired") var overseasAcquired: Int?, + @SerializedName("recent_tests") var recentTests: Int?, + @SerializedName("locally_acquired") var locallyAcquired: Int?, + @SerializedName("new_locally_acquired") var newLocallyAcquired: Int?, + @SerializedName("new_overseas_acquired") var newOverseasAcquired: Int?, var deaths: Int?) \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/UploadPostcodeResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/UploadPostcodeResponse.kt new file mode 100644 index 0000000..4810599 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/UploadPostcodeResponse.kt @@ -0,0 +1,6 @@ +package au.gov.health.covidsafe.networking.response + +import androidx.annotation.Keep + +@Keep +class UploadPostcodeResponse \ 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 82d43c3..f0cf404 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 @@ -3,6 +3,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.ChangePostcodeRequest import au.gov.health.covidsafe.networking.request.OTPChallengeRequest import au.gov.health.covidsafe.networking.response.* import retrofit2.Call @@ -48,4 +49,10 @@ interface AwsClient { @GET(BuildConfig.END_POINT_PREFIX + "/statistics") fun getCaseStatistics(@Header("Authorization") jwtToken: String?): Call + @GET(BuildConfig.END_POINT_PREFIX + "/v2/statistics") + fun getCaseStatisticsVersion2(@Header("Authorization") jwtToken: String?): Call + + @POST(BuildConfig.END_POINT_PREFIX + "/device") + fun changePostcode(@Header("Authorization") jwtToken: String?, + @Body body: ChangePostcodeRequest): Call } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt b/app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt index cadbdfa..4846dce 100644 --- a/app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt +++ b/app/src/main/java/au/gov/health/covidsafe/preference/Preference.kt @@ -38,6 +38,7 @@ object Preference { private const val BUILD_NUMBER_FOR_POP_UP_NOTIFICATION = "BUILD_NUMBER_FOR_POP_UP_NOTIFICATION" private const val TURN_CASE_NUMBER = "TURN_CASE_NUMBER" private const val IS_REREGISTER = "IS_REREGISTER" + private const val SELECTED_STATE = "SELECTED_STATE" fun putDeviceID(context: Context, value: String) { context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) @@ -320,4 +321,14 @@ object Preference { return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) .getBoolean(TURN_CASE_NUMBER, true) } + + fun putSelectedState(context: Context, selectState: String): Boolean { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .edit().putString(SELECTED_STATE, selectState).commit() + } + + fun getSelectedState(context: Context): String? { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .getString(SELECTED_STATE, null) + } } diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java index cb42e96..8732ecf 100644 --- a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/BLEDevice.java @@ -38,7 +38,8 @@ public class BLEDevice { /// Payload data acquired from the device via payloadCharacteristic read, e.g. C19X beacon code or Sonar encrypted identifier private PayloadData payloadData; /// Most recent RSSI measurement taken by readRSSI or didDiscover. - private RSSI rssi; + int DUMMY_RSSI = 999; + private RSSI rssi = new RSSI(DUMMY_RSSI); /// Transmit power data where available (only provided by Android devices) private BLE_TxPower txPower; /// Is device receive only? diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java index ad28356..52cdaff 100644 --- a/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/ble/ConcreteBLESensor.java @@ -100,6 +100,7 @@ public class ConcreteBLESensor implements BLESensor, BLEDatabaseDelegate, Blueto if (rssi == null) { return; } + final Proximity proximity = new Proximity(ProximityMeasurementUnit.RSSI, (double) rssi.value); Log.d(LOG_TAG, "device.payloadData():" + device.payloadData()); Log.d(LOG_TAG, "device:" + device); @@ -150,17 +151,11 @@ public class ConcreteBLESensor implements BLESensor, BLEDatabaseDelegate, Blueto } // Notify delegates logger.debug("didRead (device={},payloadData={},payloadData={})", device, device.payloadData(), payloadData.shortName()); - final RSSI rssi = device.rssi(); - final Proximity proximity = new Proximity(ProximityMeasurementUnit.RSSI, (double) rssi.value); operationQueue.execute(new Runnable() { @Override public void run() { for (SensorDelegate delegate : delegates) { - if (device.txPower() != null) { - delegate.sensor(SensorType.BLE, payloadData, device.identifier, proximity, device.txPower().value, device); - } else { - delegate.sensor(SensorType.BLE, payloadData, device.identifier, proximity, 999, device); - } + delegate.sensor(SensorType.BLE, payloadData, device.identifier); } } }); diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java index 293d1f1..3fb4b43 100644 --- a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/Base64.java @@ -29,7 +29,8 @@ public class Base64 { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 48, 49, 50, 51}; public static String encode(byte[] data) { final StringBuilder buffer = new StringBuilder(); diff --git a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java index 3ddb053..9bad9c8 100644 --- a/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java +++ b/app/src/main/java/au/gov/health/covidsafe/sensor/datatype/PseudoDeviceAddress.java @@ -5,14 +5,12 @@ package au.gov.health.covidsafe.sensor.datatype; import android.util.Base64; - import au.gov.health.covidsafe.sensor.data.ConcreteSensorLogger; import au.gov.health.covidsafe.sensor.data.SensorLogger; - import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.SecureRandom; import java.util.Objects; +import java.security.SecureRandom; import java.util.Random; /// Pseudo device address to enable caching of device payload without relying on device mac address @@ -21,21 +19,10 @@ public class PseudoDeviceAddress { public final long address; public final byte[] data; - /// Using secure random can cause blocking on app initialisation due to lack of entropy - /// on some devices. Worst case scenario is app blocking upon initialisation, bluetooth - /// power cycle, or advert refresh that occurs once every 15 minutes, leading to zero - /// detection until sufficient entropy has been collected, which may take time given - /// the device is likely to be idle. Not using secure random is acceptable and recommended - /// in this instance because it is non-blocking and the sequence has sufficient uncertainty - /// introduced programmatically to make an attack impractical from limited obeservations. public PseudoDeviceAddress() { // Bluetooth device address is 48-bit (6 bytes), using // the same length to offer the same collision avoidance - // Choose between random, secure random, and NIST compliant secure random as random source - // - Random is non-blocking and sufficiently secure for this purpose, recommended - // - SecureRandom is potentially blocking and unnecessary in this instance, not recommended - // - NISTSecureRandom is most likely to block and unnecessary in this instance, not recommended - this.data = encode(getSecureRandomLong()); + this.data = encode(getSecureRandomSingletonLong()); this.address = decode(this.data); } @@ -44,23 +31,11 @@ public class PseudoDeviceAddress { this.address = decode(data); } - /// Non-blocking random number generator with appropriate strength for this purpose - protected final static long getRandomLong() { - // Use a different instance with random seed from another sequence each time - final Random random = new Random(Math.round(Math.random() * Long.MAX_VALUE)); - // Skip a random number of bytes from another sequence - random.nextBytes(new byte[256 + (int) Math.round(Math.random() * 1024)]); - return random.nextLong(); - } - - /// Secure random number generator that is potentially blocking. Experiments have - /// shown blocking can occur, especially on idle device, due to lack of entropy. protected final static long getSecureRandomLong() { return new SecureRandom().nextLong(); } private static SecureRandom secureRandomSingleton = null; - /// Secure random number generator that is potentially blocking. protected final static long getSecureRandomSingletonLong() { // On-demand initialisation in the hope that sufficient // entropy has been gathered during app initialisation @@ -70,49 +45,6 @@ public class PseudoDeviceAddress { return secureRandomSingleton.nextLong(); } - /// Get secure random instance seed according to NIST SP800-90A recommendations - /// - SHA1PRNG algorithm - /// - Algorithm seeded with 440 bits of secure random data - /// - Skips first random number of bytes to mitigate against poor implementations - /// Compliance to NIST SP800-90A offers quality assurance against an accepted - /// standard. The aim here is not to offer the most perfect random source, but - /// a source with well defined and understood characteristics, thus enabling - /// selection of the most appropropriate method, given the intented purpose. - /// This implementation supports security strength for NIST SP800-57 - /// Part 1 Revision 5 (informally, generation of cryptographic keys for - /// encryption of sensitive data). - public final static long getNISTSecureRandomLong() { - try { - // Obtain SHA1PRNG specifically where possible for NIST SP800-90A compliance. - // Ignoring Android recommendation to use "new SecureRandom()" because that - // decision was taken based on a single peer reviewed statistical test that - // showed SHA1PRNG has bias. The test has not been adopted by NIST yet which - // already uses 15 other statistical tests for quality assurance. This does - // not mean the new test is invalid, but it is more appropriate for this work - // to adopt and comply with an accepted standard for security assurance. - final SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); - // Obtain the most secure PRNG claimed by the platform for generating the seed - // according to Android recommendation. - final SecureRandom secureRandomForSeed = new SecureRandom(); - // NIST SP800-90A (see section 10.1) recommends 440 bit seed for SHA1PRNG - // to support security strength defined in NIST SP800-57 Part 1 Revision 5. - final byte[] seed = secureRandomForSeed.generateSeed(55); - // Seed secure random with 440 bit seed according to NIST SP800-90A recommendation. - secureRandom.setSeed(seed); // seed with random number - // Skip the first 256 - 1280 bytes as mitigation against poor implementations - // of SecureRandom where the initial values are predictable given the seed - secureRandom.nextBytes(new byte[256 + secureRandom.nextInt(1024)]); - return secureRandom.nextLong(); - } catch (Throwable e) { - // Android OS may mandate the use of "new SecureRandom()" and forbid the use - // of a specific provider in the future. Fallback to Android mandated option - // and log the fact that it is no longer NIST SP800-90A compliant. - final SensorLogger logger = new ConcreteSensorLogger("Sensor", "Datatype.PseudoDeviceAddress"); - logger.fault("NIST SP800-90A compliant SecureRandom initialisation failed, reverting back to SecureRandom", e); - return getSecureRandomLong(); - } - } - protected final static byte[] encode(final long value) { final ByteBuffer byteBuffer = ByteBuffer.allocate(8); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); @@ -121,6 +53,7 @@ public class PseudoDeviceAddress { System.arraycopy(byteBuffer.array(), 0, data, 0, data.length); return data; } + protected final static long decode(final byte[] data) { final ByteBuffer byteBuffer = ByteBuffer.allocate(8); byteBuffer.put(data); 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 0719385..149b981 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 @@ -193,7 +193,7 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope, SensorDel fun runService(cmd: Command?) { CentralLog.i(TAG, "Command is:${cmd?.string}") - + when (cmd) { Command.ACTION_START -> { actionStart() diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt index 9ea4678..ba46f98 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordDao.kt @@ -28,4 +28,9 @@ interface StreetPassRecordDao { @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(record: StreetPassRecord) + @Query("SELECT * from record_table WHERE timestamp >= :timeInMs") + fun getCurrentRecordsFilterTime(timeInMs: Long): List + + @Query("SELECT * from record_table WHERE timestamp <= :endTime and timestamp >= :startTime") + fun getCurrentRecordsBetweenTime(startTime: Long, endTime: Long): List } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt index ccf9591..d6a348a 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/StreetPassRecordStorage.kt @@ -25,4 +25,12 @@ class StreetPassRecordStorage(val context: Context) { fun getAllRecords(): List { return recordDao.getCurrentRecords() } + + fun getAllRecodsFilterTime(timeInMs: Long): List { + return recordDao.getCurrentRecordsFilterTime(timeInMs) + } + + fun getAllRecodsBetweenTime(startTime: Long, endTime: Long): List { + return recordDao.getCurrentRecordsBetweenTime(startTime, endTime) + } } \ No newline at end of file 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 e54b0c1..f189c12 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 @@ -22,6 +22,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager import au.gov.health.covidsafe.BuildConfig import au.gov.health.covidsafe.HomeActivity import au.gov.health.covidsafe.R @@ -47,14 +48,15 @@ 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.android.synthetic.main.view_national_case_statistics.* +import kotlinx.android.synthetic.main.view_national_case_statistics.national_case_layout +import kotlinx.android.synthetic.main.view_state_case_statistics.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import pub.devrel.easypermissions.AppSettingsDialog import pub.devrel.easypermissions.EasyPermissions import java.text.SimpleDateFormat import java.util.* - +import kotlin.collections.ArrayList private const val TAG = "HomeFragment" @@ -73,6 +75,7 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ private var checkIsInternetConnected = false private var isAppWithLatestVersion = false lateinit var staticsLayout: ConstraintLayout + private lateinit var stateListAdapter: StateAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -100,10 +103,17 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ initializePullToRefresh() initialiseReRegistration() setupHyperlink() + removeRegistrationData() NetworkConnectionCheck.addNetworkChangedListener(requireContext(), this) } + private fun removeRegistrationData() { + Preference.putName(requireContext(), "") + Preference.putAge(requireContext(), "") + Preference.putPostCode(requireContext(), "") + } + private fun setupHyperlink() { txt_update_description.movementMethod = LinkMovementMethod.getInstance() } @@ -166,6 +176,20 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ } } }) + + homeFragmentViewModel.visibleSelectStateLayout.observe(this, Observer { visible -> + visible?.let { + select_state_layout.visibility = if (visible) VISIBLE else GONE + } + }) + + homeFragmentViewModel.hotSpotLink.observe(this, Observer { link -> + link?.let { + val title = homeFragmentViewModel.hotSpotTitle.value + txt_hotspot.text = LinkBuilder.getHotSpot(title, link) + txt_hotspot.movementMethod = LinkMovementMethod.getInstance() + } + }) } private val latestAppAvailable = Observer { @@ -206,6 +230,22 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ updateHealthOfficialTile() initializeBluetoothPairingInfo() + loadStateAndListener() + } + + private fun loadStateAndListener() { + stateListAdapter = StateAdapter(this.requireContext()) + select_state.adapter = stateListAdapter + val layoutManager = LinearLayoutManager(this.requireContext()) + select_state.layoutManager = layoutManager + + stateListAdapter.setRecords(createStateList(), 0) + + stateListAdapter.setOnStateListClickListener(object : StateAdapter.OnStateListClickListener{ + override fun onStateClick(state: String) { + homeFragmentViewModel.setSelectedState(state) + } + }) } private fun initialisePrivacyPolicyMessageAfterUpdate() { @@ -275,6 +315,14 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ HelpFragment.anchor = "#other-languages" findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment()) } + + home_header_picture_setup_complete.setOnClickListener { + counter++ + if (counter >= 2) { + counter = 0 + findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToPeekActivity()) + } + } } private fun initializeBluetoothPairingInfo() { @@ -413,6 +461,7 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ } } + @SuppressLint("StringFormatInvalid") private fun showLastDataUploadedInfo(context: Context, isDataUploadedInPast14Days: Boolean) { if (isDataUploadedInPast14Days) { data_last_uploaded_layout.visibility = VISIBLE @@ -564,17 +613,35 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ } private fun showAndHideCaseNumber(turnOn: Boolean) { + val isV2Available = homeFragmentViewModel.isV2Available.value //When we open the app, should show the previous setting (hide/show) and doesn't need animation if (homeFragmentViewModel.getTurningCaseAfterOpenPage()) { if (turnOn) { - national_case_layout.visibility = VISIBLE + if (isV2Available != null && isV2Available) { + state_layout.visibility = VISIBLE + } else { + national_case_layout.visibility = VISIBLE + } } else { - national_case_layout.visibility = GONE + if (isV2Available != null && isV2Available) { + state_layout.visibility = GONE + } else { + national_case_layout.visibility = GONE + } } } else { if (turnOn) { - national_case_layout.slideAnimation(SlideDirection.DOWN, SlideType.SHOW) + if (isV2Available != null && isV2Available) { + state_layout.slideAnimation(SlideDirection.DOWN, SlideType.SHOW) + } else { + national_case_layout.slideAnimation(SlideDirection.DOWN, SlideType.SHOW) + } } else { + if (isV2Available != null && isV2Available) { + state_layout.slideAnimation(SlideDirection.UP, SlideType.HIDE) + } else { + national_case_layout.slideAnimation(SlideDirection.UP, SlideType.HIDE) + } national_case_layout.slideAnimation(SlideDirection.UP, SlideType.HIDE) } } @@ -616,4 +683,18 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks, Networ location_card_view.isEnabled = enableParent battery_card_view.isEnabled = enableParent } + + private fun createStateList(): ArrayList{ + val list = ArrayList() + list.add("Australia") + list.add("Australian Capital Territory") + list.add("New South Wales") + list.add("Northern Territory") + list.add("Queensland") + list.add("South Australia") + list.add("Tasmania") + list.add("Victoria") + list.add("Western Australia") + return list + } } 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 index 39914e8..1888bf7 100644 --- 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 @@ -2,17 +2,24 @@ package au.gov.health.covidsafe.ui.home import android.app.Application import android.content.Context -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import au.gov.health.covidsafe.BuildConfig +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.extensions.isInternetAvailable 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.CaseDetailsData 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.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch private const val TAG = "HomeFragmentViewModel" @@ -21,6 +28,7 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica val caseNumberDataState = MutableLiveData() val caseStatisticsLiveData = MutableLiveData() + val caseStateStatisticsLiveData = MutableLiveData() val isRefreshing = MutableLiveData() val collectionMessageVisible = MutableLiveData() val heraldUpgradeMessage = MutableLiveData() @@ -28,13 +36,31 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica val turnCaseNumber = MutableLiveData() lateinit var context: Context var turnCaseAfterOpenPage = true + val visibleSelectStateLayout = MutableLiveData() + val newCase = MutableLiveData() + val localCase = MutableLiveData() + val overseaCase = MutableLiveData() + val activeCase = MutableLiveData() + val totalDeaths = MutableLiveData() + val titleOfNumber = MutableLiveData() + val hotSpotTitle = MutableLiveData() + val hotSpotLink = MutableLiveData() + val locallyAquired = MutableLiveData() + val aquiredOversea = MutableLiveData() + val totalyDeathe = MutableLiveData() + val isV2Available = MutableLiveData() + + private val viewModelJob = SupervisorJob() + private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) val awsClient: AwsClient by lazy { RetrofitServiceGenerator.createService(AwsClient::class.java) } fun fetchGetCaseStatistics(lifecycle: Lifecycle) { + isV2Available.value = false context = getApplication() as Context + titleOfNumber.value = context.getString(R.string.national_numbers) turnCaseNumber.value = Preference.getTurnCaseNumber(context) if(caseNumberDataState.value != CaseNumbersState.LOADING) { caseNumberDataState.value = CaseNumbersState.LOADING @@ -64,6 +90,12 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica caseNumberDataState.value = CaseNumbersState.SUCCESS cacheCaseStatisticDataInPersistent(caseStatisticResponse) caseStatisticsLiveData.value = caseStatisticResponse + + if ( caseStatisticResponse.version != null) { + isV2Available.value = true + } + caseStateStatisticsLiveData.value = caseStatisticResponse + selectState() } } @@ -120,5 +152,84 @@ class HomeFragmentViewModel(application: Application) : AndroidViewModel(applica turnCaseNumber.value = turnoff } + fun showSelectSate(visibleLayout: Boolean) { + visibleSelectStateLayout.value = visibleLayout + } + fun getTurningCaseAfterOpenPage() = turnCaseAfterOpenPage + fun setSelectedState(state: String) { + Preference.putSelectedState(context, state) + } + + fun selectState() { + visibleSelectStateLayout.value = false + val stateResponse: CaseDetailsData? + locallyAquired.value = context.getString(R.string.locally_acquired).replace("%@", "") + aquiredOversea.value = context.getString(R.string.overseas_acquired).replace("%@", "") + totalyDeathe.value = context.getString(R.string.total_deaths).replace("%@", "") + + when (Preference.getSelectedState(context).toString()) { + "Australia" -> { + stateResponse = caseStateStatisticsLiveData.value?.national + recordStateData(stateResponse, "National") + } + "Australian Capital Territory" -> { + stateResponse = caseStateStatisticsLiveData.value?.act + recordStateData(stateResponse, "ACT") + hotSpotLink.value = context.getString(R.string.hotspot_link_ACT) + } + "New South Wales" -> { + stateResponse = caseStateStatisticsLiveData.value?.nsw + recordStateData(stateResponse, "NSW") + hotSpotLink.value = context.getString(R.string.hotspot_link_NSW) + } + "Northern Territory" -> { + stateResponse = caseStateStatisticsLiveData.value?.nt + recordStateData(stateResponse, "NT") + hotSpotLink.value = context.getString(R.string.hotspot_link_NT) + } + "Queensland" -> { + stateResponse = caseStateStatisticsLiveData.value?.qld + recordStateData(stateResponse, "QLD") + hotSpotLink.value = context.getString(R.string.hotspot_link_QLD) + } + "South Australia" -> { + stateResponse = caseStateStatisticsLiveData.value?.sa + recordStateData(stateResponse, "SA") + hotSpotLink.value = context.getString(R.string.hotspot_link_SA) + } + "Tasmania" -> { + stateResponse = caseStateStatisticsLiveData.value?.tas + recordStateData(stateResponse, "TAS") + hotSpotLink.value = context.getString(R.string.hotspot_link_TAS) + } + "Victoria" -> { + stateResponse = caseStateStatisticsLiveData.value?.vic + recordStateData(stateResponse, "VIC") + hotSpotLink.value = context.getString(R.string.hotspot_link_VIC) + } + "Western Australia" -> { + stateResponse = caseStateStatisticsLiveData.value?.wa + recordStateData(stateResponse, "WA") + hotSpotLink.value = context.getString(R.string.hotspot_link_WA) + } + else -> { + stateResponse = caseStateStatisticsLiveData.value?.national + recordStateData(stateResponse, "National") + } + } + } + + private fun recordStateData(stateResponse: CaseDetailsData?, stateName: String?) { + newCase.value = if (stateResponse?.newCases != null) { stateResponse.newCases} else { 0 } + localCase.value = if (stateResponse?.locallyAcquired != null) { stateResponse.newLocallyAcquired.toString()} else { "0" } + overseaCase.value = if (stateResponse?.overseasAcquired != null) { stateResponse.newOverseasAcquired.toString()} else { "0" } + activeCase.value = if (stateResponse?.activeCases != null) { stateResponse.activeCases} else { 0 } + totalDeaths.value = if (stateResponse?.deaths != null) { stateResponse.deaths.toString()} else { "0" } + titleOfNumber.value = "$stateName numbers" + stateName?.let { + val hotSpot = context.getString(R.string.hotspots_state_territory) + hotSpotTitle.value = hotSpot.replace("%@", it, false) + } + } } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/StateAdapter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/StateAdapter.kt new file mode 100644 index 0000000..4567eaa --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/StateAdapter.kt @@ -0,0 +1,67 @@ +package au.gov.health.covidsafe.ui.home + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView +import au.gov.health.covidsafe.R + + +class StateAdapter internal constructor(context: Context) : + RecyclerView.Adapter() { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + private var stateList = emptyList() + private var selectedState: Int = 0 + private var mListener: OnStateListClickListener? = null + + interface OnStateListClickListener { + fun onStateClick(state: String) + } + + inner class StateViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val txtStateName: TextView = itemView.findViewById(R.id.state_name) + val imageStateSelect: ImageView = itemView.findViewById(R.id.img_state_select) + val endLine: View = itemView.findViewById(R.id.end_line) + val countryListLayout: ConstraintLayout = itemView.findViewById(R.id.country_list_item) + } + + fun setOnStateListClickListener(actionListener: OnStateListClickListener) { + mListener = actionListener + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StateAdapter.StateViewHolder { + val itemView = inflater.inflate(R.layout.view_list_item_state, parent, false) + return StateViewHolder(itemView) + } + + override fun onBindViewHolder(holder: StateAdapter.StateViewHolder, position: Int) { + holder.txtStateName.text = stateList[position] + holder.imageStateSelect.visibility = View.GONE + holder.endLine.visibility = View.VISIBLE + + if (position == selectedState) { + holder.imageStateSelect.visibility = View.VISIBLE + } + if (position == (this.stateList.size - 1)) { + holder.endLine.visibility = View.GONE + } + holder.countryListLayout.setOnClickListener { + setRecords(stateList, position) + mListener?.onStateClick(stateList[position]) + } + } + + fun setRecords(stateList: List, selectedState: Int) { + this.stateList = stateList + this.selectedState = selectedState + notifyDataSetChanged() + } + + override fun getItemCount() = stateList.size + +} 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 ae27496..b741ff6 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 @@ -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}))$") +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}\$") class PersonalDetailsFragment : PagerChildFragment() { @@ -75,7 +75,7 @@ class PersonalDetailsFragment : PagerChildFragment() { R.array.age_range_array, android.R.layout.simple_spinner_item ).also { adapter -> - // Specify the layout to use when the list of choices appears + // Specify the activity_change_postcode to use when the list of choices appears adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) // Apply the adapter to the spinner diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/settings/ChangePostCodeActivity.kt b/app/src/main/java/au/gov/health/covidsafe/ui/settings/ChangePostCodeActivity.kt new file mode 100644 index 0000000..5ae9f23 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/settings/ChangePostCodeActivity.kt @@ -0,0 +1,61 @@ +package au.gov.health.covidsafe.ui.settings + +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.databinding.ActivityChangePostcodeBinding +import au.gov.health.covidsafe.links.LinkBuilder +import au.gov.health.covidsafe.ui.onboarding.fragment.personal.POST_CODE_REGEX +import kotlinx.android.synthetic.main.activity_change_postcode.* + +class ChanePostCodeActivity: AppCompatActivity() { + + var viewModel: SettingsViewModel? = null + var postcode: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // TO make sure scroll works with editTexts + + val binding = DataBindingUtil.setContentView(this, R.layout.activity_change_postcode) + viewModel = ViewModelProvider(this).get(SettingsViewModel::class.java) + binding.lifecycleOwner = this + binding.viewModel = viewModel + + val toolbar = findViewById(com.atlassian.mobilekit.module.feedback.R.id.toolbar) as Toolbar + setSupportActionBar(toolbar) + val actionBar = supportActionBar + actionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationOnClickListener { finish() } + + setupListener() + + viewModel?.getCurrentPostCode() + postcode_content.text = LinkBuilder.getPostcodeContent(this) + postcode_content.movementMethod = LinkMovementMethod.getInstance() + + postcode_updated.text = LinkBuilder.getPostcodeUpdatedSuccessfullyContent(this) + postcode_updated.movementMethod = LinkMovementMethod.getInstance() + } + + private fun setupListener() { + btn_done.setOnClickListener { + this.finish() + } + btn_continue.setOnClickListener { + postcode = post_code.text.toString() + if (isValidPostcode() && postcode != null) { + viewModel?.changePostcode(postcode!!) + post_code_error.visibility = View.GONE + } else { + post_code_error.visibility = View.VISIBLE + } + } + } + private fun isValidPostcode() = postcode?.length == 4 && POST_CODE_REGEX.matcher(postcode).matches() +} \ No newline at end of file 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 index f163705..e7362e7 100644 --- 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 @@ -71,6 +71,7 @@ class SettingsFragment : BaseFragment(), NetworkConnectionCheck.NetworkConnectio initializeSupportNavigation() initializeAppShareNavigation() setAppVersionNumber() + initializeChangePostCodeNavigation() } private fun initializeHelpTopicsNavigation() { @@ -92,6 +93,12 @@ class SettingsFragment : BaseFragment(), NetworkConnectionCheck.NetworkConnectio } } + private fun initializeChangePostCodeNavigation() { + postcode_card_view.setOnClickListener { + startActivity(Intent(requireContext(), ChanePostCodeActivity::class.java)) + } + } + private fun setAppVersionNumber() { settings_version_number.text = context?.getAppVersionNumberDetails() } diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsViewModel.kt b/app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..3912343 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/settings/SettingsViewModel.kt @@ -0,0 +1,59 @@ +package au.gov.health.covidsafe.ui.settings + +import android.app.Application +import android.content.Context +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import au.gov.health.covidsafe.factory.RetrofitServiceGenerator +import au.gov.health.covidsafe.networking.request.ChangePostcodeRequest +import au.gov.health.covidsafe.networking.response.UploadPostcodeResponse +import au.gov.health.covidsafe.networking.service.AwsClient +import au.gov.health.covidsafe.preference.Preference +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class SettingsViewModel(application: Application): AndroidViewModel(application) { + + val postcodeUpdated = MutableLiveData() + val showSpinner = MutableLiveData() + lateinit var context: Context + + val awsClient: AwsClient by lazy { + RetrofitServiceGenerator.createService(AwsClient::class.java) + } + + fun getCurrentPostCode() { + context = getApplication() as Context + } + + init { + postcodeUpdated.value = false + showSpinner.value = false + } + + fun changePostcode(postcode: String) { + showSpinner.value = true + + val token = Preference.getEncrypterJWTToken(getApplication() as Context) + val changePstcode: Call = awsClient.changePostcode("Bearer $token", ChangePostcodeRequest(postcode)) + changePstcode.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + onError() + showSpinner.value = false + } + + override fun onResponse(call: Call, response: Response) { + if (response.code() == 200) { + postcodeUpdated.value = true + showSpinner.value = false + Preference.putPostCode(context, postcode) + } + } + }) + } + + private fun onError() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/upload/presentation/VerifyUploadPinFragment.kt index 86ce4bb..98dfcba 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 @@ -88,10 +88,10 @@ class VerifyUploadPinFragment : PagerChildFragment() { enter_pin_error_label.visibility = View.GONE } - fun showGenericError() { + fun showGenericError(errorCode: String?) { dialog?.dismiss() activity?.let { - dialog = UploadingErrorDialog(it, object : OnUploadErrorInterface { + dialog = UploadingErrorDialog(it, errorCode, object : OnUploadErrorInterface { override fun onPositiveClicked() { presenter.uploadData(requireView().pin.text.toString()) } 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 97b709a..cedb8db 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 @@ -49,15 +49,17 @@ class VerifyUploadPinPresenter(private val fragment: VerifyUploadPinFragment) : fragment.navigateToNextPage() }, onFailure = { + + val errorCode = it.message.toString() when { it is UploadDataException.UploadDataIncorrectPinException -> { fragment.showInvalidOtp() } it is UploadDataException.UploadDataJwtExpiredException -> { - fragment.navigateToRegister() + fragment.navigateToRegister() } fragment.activity?.isInternetAvailable() == true -> { - fragment.showGenericError() + fragment.showGenericError(errorCode) } else -> { fragment.showCheckInternetError() diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt b/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt index 38f6b84..33b0759 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/view/UploadingErrorDialog.kt @@ -7,9 +7,10 @@ import android.view.Window import android.view.WindowManager import au.gov.health.covidsafe.R import au.gov.health.covidsafe.ui.upload.presentation.VerifyUploadPinFragment +import kotlinx.android.synthetic.main.activity_onboarding.view.* import kotlinx.android.synthetic.main.dialog_error_uploading.* -class UploadingErrorDialog(context: Context, private val listener: VerifyUploadPinFragment.OnUploadErrorInterface) : Dialog(context) { +class UploadingErrorDialog(context: Context, val errorCode: String?, private val listener: VerifyUploadPinFragment.OnUploadErrorInterface) : Dialog(context) { init { setCancelable(false) @@ -20,6 +21,9 @@ class UploadingErrorDialog(context: Context, private val listener: VerifyUploadP super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE) setContentView(R.layout.dialog_error_uploading) + + val errorMessage = context.getString(R.string.dialog_error_uploading_message).replace("%@", errorCode.toString(), false) + home_data_uploaded_error_message.text = errorMessage val window: Window? = this.window window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT) dialog_error_positive.setOnClickListener { @@ -29,4 +33,4 @@ class UploadingErrorDialog(context: Context, private val listener: VerifyUploadP listener.onNegativeClicked() } } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 0000000..190bb7e --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activity.xml b/app/src/main/res/drawable/ic_activity.xml new file mode 100644 index 0000000..cf4d5b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_activity.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_alert_triangle.xml b/app/src/main/res/drawable/ic_alert_triangle.xml new file mode 100644 index 0000000..f8c6ba3 --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_triangle.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..94eecf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_external_link.xml b/app/src/main/res/drawable/ic_external_link.xml new file mode 100644 index 0000000..24826e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_external_link.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_illustration.xml b/app/src/main/res/drawable/ic_illustration.xml new file mode 100644 index 0000000..5f05bbf --- /dev/null +++ b/app/src/main/res/drawable/ic_illustration.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_map_pin.xml b/app/src/main/res/drawable/ic_map_pin.xml new file mode 100644 index 0000000..ab71d70 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_pin.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_postcode.xml b/app/src/main/res/drawable/ic_postcode.xml new file mode 100644 index 0000000..5f69679 --- /dev/null +++ b/app/src/main/res/drawable/ic_postcode.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/layout/activity_change_postcode.xml b/app/src/main/res/layout/activity_change_postcode.xml new file mode 100644 index 0000000..db0cdb1 --- /dev/null +++ b/app/src/main/res/layout/activity_change_postcode.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +