From c16533add66e043f5f6e841df8921afff8593d04 Mon Sep 17 00:00:00 2001 From: COVIDSafe Support <64945427+covidsafe-support@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:41:15 +1000 Subject: [PATCH] COVIDSafe code from version 1.0.28 (#4) --- app/build.gradle | 8 +- app/src/main/AndroidManifest.xml | 21 +- .../au/gov/health/covidsafe/Preference.kt | 45 ++- .../extensions/PermissionExtensions.kt | 21 +- .../interactor/usecase/GetOnboardingOtp.kt | 17 +- .../networking/request/OTPChallengeRequest.kt | 13 +- .../gov/health/covidsafe/streetpass/Work.kt | 22 +- .../streetpass/persistence/Encryption.kt | 2 +- .../health/covidsafe/ui/home/HomeFragment.kt | 8 +- .../ui/home/view/PermissionStatusCard.kt | 21 +- .../CountryCodeSelectionActivity.kt | 193 ++++++++++++ .../CountryInitialLetterRecyclerView.kt | 48 +++ .../covidsafe/ui/onboarding/CountryList.kt | 253 ++++++++++++++++ .../ui/onboarding/CountryListRecyclerView.kt | 127 ++++++++ .../enternumber/EnterNumberFragment.kt | 116 +++++--- .../enternumber/EnterNumberPresenter.kt | 98 +++++-- .../fragment/enterpin/EnterPinFragment.kt | 8 +- .../fragment/enterpin/EnterPinPresenter.kt | 17 +- .../personal/PersonalDetailsFragment.kt | 255 +++++++++------- .../personal/PersonalDetailsPresenter.kt | 96 ------ app/src/main/res/drawable/ic_microphone.xml | 9 + app/src/main/res/drawable/ic_right.xml | 13 + app/src/main/res/drawable/ic_search.xml | 9 + .../activity_country_code_selection.xml | 78 +++++ .../main/res/layout/fragment_enter_number.xml | 133 +++++---- .../res/layout/fragment_personal_details.xml | 20 +- .../res/layout/view_card_permission_card.xml | 32 +- .../res/layout/view_list_item_country.xml | 44 +++ .../view_list_item_country_initial_letter.xml | 6 + .../res/layout/view_list_item_group_title.xml | 20 ++ app/src/main/res/values/integers.xml | 4 - app/src/main/res/values/strings.xml | 277 +++++++++++++++++- app/src/main/res/values/styles.xml | 8 + build.gradle | 3 +- security.txt | 21 +- 35 files changed, 1653 insertions(+), 413 deletions(-) create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryCodeSelectionActivity.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryInitialLetterRecyclerView.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryList.kt create mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryListRecyclerView.kt delete mode 100644 app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt create mode 100644 app/src/main/res/drawable/ic_microphone.xml create mode 100644 app/src/main/res/drawable/ic_right.xml create mode 100644 app/src/main/res/drawable/ic_search.xml create mode 100644 app/src/main/res/layout/activity_country_code_selection.xml create mode 100644 app/src/main/res/layout/view_list_item_country.xml create mode 100644 app/src/main/res/layout/view_list_item_country_initial_letter.xml create mode 100644 app/src/main/res/layout/view_list_item_group_title.xml delete mode 100644 app/src/main/res/values/integers.xml diff --git a/app/build.gradle b/app/build.gradle index 3693b7e..8d3d148 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,8 +40,8 @@ android { Before you increase the targetSdkVersion make sure that all its usage are still working */ targetSdkVersion 28 - versionCode 21 - versionName "1.0.21" + versionCode 28 + versionName "1.0.28" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -243,6 +243,10 @@ dependencies { implementation "androidx.lifecycle:lifecycle-service:2.2.0" implementation 'com.github.razir.progressbutton:progressbutton:2.0.1' + // flags + implementation 'com.michaelfotiadis:android-country-flags:1.0.3' + androidTestImplementation "androidx.room:room-testing:2.2.5" + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac27238..61be7a7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,12 +1,13 @@ - + @@ -19,13 +20,14 @@ + android:theme="@style/MyTheme.DayNight"> + android:screenOrientation="portrait" + android:windowSoftInputMode="adjustPan" /> + + + android:screenOrientation="portrait" + android:windowSoftInputMode="adjustPan" /> diff --git a/app/src/main/java/au/gov/health/covidsafe/Preference.kt b/app/src/main/java/au/gov/health/covidsafe/Preference.kt index d23294c..4060271 100644 --- a/app/src/main/java/au/gov/health/covidsafe/Preference.kt +++ b/app/src/main/java/au/gov/health/covidsafe/Preference.kt @@ -9,6 +9,12 @@ import au.gov.health.covidsafe.security.crypto.AESEncryptionForPreAndroidM object Preference { private const val PREF_ID = "Tracer_pref" private const val IS_ONBOARDED = "IS_ONBOARDED" + private const val CALLING_CODE = "CALLING_CODE" + private const val AUSTRALIA_CALLING_CODE = 61 + private const val COUNTRY_NAME_RES_ID = "COUNTRY_NAME" + private const val AUSTRALIA_COUNTRY_NAME_RES_ID = R.string.country_au + private const val NATIONAL_FLAG_RES_ID = "NATIONAL_FLAG_RES_ID" + private const val AUSTRALIA_NATIONAL_FLAG_RES_ID = R.drawable.ic_list_country_au private const val PHONE_NUMBER = "PHONE_NUMBER" private const val HANDSHAKE_PIN = "HANDSHAKE_PIN" private const val DEVICE_ID = "DEVICE_ID" @@ -41,7 +47,7 @@ object Preference { .edit().putString(AES_IV, value)?.apply() } - fun getEncodedAESInitialisationVector(context: Context) : String? { + fun getEncodedAESInitialisationVector(context: Context): String? { return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) .getString(AES_IV, null) } @@ -119,6 +125,43 @@ object Preference { .edit().putString(PHONE_NUMBER, value).apply() } + fun getPhoneNumber(context: Context): String { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + ?.getString(PHONE_NUMBER, "") ?: "" + } + + fun putCallingCode(context: Context, value: Int) { + context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .edit().putInt(CALLING_CODE, value).apply() + } + + fun getCallingCode(context: Context): Int { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + ?.getInt(CALLING_CODE, AUSTRALIA_CALLING_CODE) ?: AUSTRALIA_CALLING_CODE + } + + fun putCountryNameResID(context: Context, value: Int) { + context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .edit().putInt(COUNTRY_NAME_RES_ID, value).apply() + } + + fun getCountryNameResID(context: Context): Int { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + ?.getInt(COUNTRY_NAME_RES_ID, AUSTRALIA_COUNTRY_NAME_RES_ID) + ?: AUSTRALIA_COUNTRY_NAME_RES_ID + } + + fun putNationalFlagResID(context: Context, value: Int) { + context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + .edit().putInt(NATIONAL_FLAG_RES_ID, value).apply() + } + + fun getNationalFlagResID(context: Context): Int { + return context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) + ?.getInt(NATIONAL_FLAG_RES_ID, AUSTRALIA_NATIONAL_FLAG_RES_ID) + ?: AUSTRALIA_NATIONAL_FLAG_RES_ID + } + fun putNextFetchTimeInMillis(context: Context, time: Long) { context.getSharedPreferences(PREF_ID, Context.MODE_PRIVATE) .edit().putLong(NEXT_FETCH_TIME, time).apply() 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 00173a2..de8f6c0 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 @@ -7,14 +7,15 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.PowerManager +import android.provider.Settings 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 pub.devrel.easypermissions.AppSettingsDialog import pub.devrel.easypermissions.EasyPermissions import pub.devrel.easypermissions.PermissionRequest -import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.Utils const val REQUEST_ENABLE_BT = 123 const val LOCATION = 345 @@ -85,6 +86,22 @@ fun Fragment.excludeFromBatteryOptimization(onEndCallback: (() -> Unit)? = null) } +fun Fragment.gotoPushNotificationSettings() { + val context = requireContext() + val intent = Intent() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" + intent.putExtra("app_package", context.packageName) + intent.putExtra("app_uid", context.applicationInfo.uid) + } + + context.startActivity(intent) +} + fun Fragment.isBlueToothEnabled(): Boolean? { val bluetoothManager = activity?.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager? return bluetoothManager?.adapter?.isEnabled diff --git a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt index 911c169..90960c2 100644 --- a/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt +++ b/app/src/main/java/au/gov/health/covidsafe/interactor/usecase/GetOnboardingOtp.kt @@ -17,7 +17,9 @@ class GetOnboardingOtp(private val awsClient: AwsClient, lifecycle: Lifecycle) : override suspend fun run(params: GetOtpParams): Either { return try { val response = awsClient.initiateAuth( - OTPChallengeRequest(params.phoneNumber, + OTPChallengeRequest( + params.countryCode, + params.phoneNumber, params.deviceId, params.postCode, params.age, @@ -48,11 +50,14 @@ class GetOnboardingOtp(private val awsClient: AwsClient, lifecycle: Lifecycle) : } } -data class GetOtpParams(internal val phoneNumber: String, - internal val deviceId: String, - internal val postCode: String?, - internal val age: String?, - internal val name: String?) +data class GetOtpParams( + internal val countryCode: String, + internal val phoneNumber: String, + internal val deviceId: String, + internal val postCode: String?, + internal val age: String?, + internal val name: String? +) sealed class GetOnboardingOtpException : Exception() { class GetOtpServiceException(val code: Int? = null) : GetOnboardingOtpException() diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt b/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt index 81c4c1b..c6da828 100644 --- a/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt +++ b/app/src/main/java/au/gov/health/covidsafe/networking/request/OTPChallengeRequest.kt @@ -3,8 +3,11 @@ package au.gov.health.covidsafe.networking.request import androidx.annotation.Keep @Keep -data class OTPChallengeRequest(val phone_number: String, - val device_id: String, - val postcode: String?, - val age: String?, - val name: String?) \ No newline at end of file +data class OTPChallengeRequest( + val country_code: String, + val phone_number: String, + val device_id: String, + val postcode: String?, + val age: String?, + val name: String? +) \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt index a7c47d2..709c75e 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/Work.kt @@ -3,20 +3,21 @@ package au.gov.health.covidsafe.streetpass import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.content.Context -import com.google.gson.Gson +import android.os.Build import au.gov.health.covidsafe.logging.CentralLog +import com.google.gson.Gson import kotlin.properties.Delegates class Work constructor( - var device: BluetoothDevice, - var connectable: ConnectablePeripheral, - private val onWorkTimeoutListener: OnWorkTimeoutListener + var device: BluetoothDevice, + var connectable: ConnectablePeripheral, + private val onWorkTimeoutListener: OnWorkTimeoutListener ) : Comparable { var timeStamp: Long by Delegates.notNull() var checklist = WorkCheckList() var gatt: BluetoothGatt? = null var finished = false - var timeout : Long = 0 + var timeout: Long = 0 private val TAG = "Work" @@ -33,10 +34,15 @@ class Work constructor( } fun startWork( - context: Context, - gattCallback: StreetPassWorker.StreetPassGattCallback + context: Context, + gattCallback: StreetPassWorker.StreetPassGattCallback ) { - gatt = device.connectGatt(context, false, gattCallback) + gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + } else { + device.connectGatt(context, false, gattCallback) + } + if (gatt == null) { CentralLog.e(TAG, "Unable to connect to ${device.address}") } diff --git a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt index 5628f7d..ac41801 100644 --- a/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt +++ b/app/src/main/java/au/gov/health/covidsafe/streetpass/persistence/Encryption.kt @@ -98,7 +98,6 @@ object Encryption { private val NONCE_PADDING = ByteArray(14) { 0x0E.toByte() } private val serverPubKey: PublicKey = readKey() - private val symCipher: Cipher = makeSymCipher() private var cachedEphPubKey: ByteArray? = null private var cachedAesKey: SecretKey? = null @@ -131,6 +130,7 @@ object Encryption { // IV = AES(ctr, iv=null), AES(plaintext, iv=IV) === AES(ctr_with_padding || plaintext, iv=null) // Using the latter construction to reduce key expansions val ivParams = IvParameterSpec(ByteArray(16)) // null IV + val symCipher: Cipher = makeSymCipher() symCipher.init(Cipher.ENCRYPT_MODE, keys.aesKey, ivParams) val ciphertextWithIV: ByteArray = symCipher.doFinal(keys.nonce.plus(NONCE_PADDING).plus(data)) 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 70222cb..2ef234f 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 @@ -94,6 +94,8 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { bluetooth_card_view.setOnClickListener { requestBlueToothPermissionThenNextPermission() } location_card_view.setOnClickListener { askForLocationPermission() } battery_card_view.setOnClickListener { excludeFromBatteryOptimization() } + push_card_view.setOnClickListener { gotoPushNotificationSettings() } + home_been_tested_button.setOnClickListener { navigateTo(R.id.action_home_to_selfIsolate) } @@ -264,7 +266,11 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { private fun updatePushNotificationStatus() { isPushNotificationEnabled()?.let { push_card_view.visibility = VISIBLE - push_card_view.render(formatPushNotificationTitle(it), it) + push_card_view.render( + formatPushNotificationTitle(it), + it, + getString(R.string.home_app_permission_push_notification_prompt) + ) } ?: run { push_card_view.visibility = GONE } 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 c1869df..c5300db 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 @@ -4,9 +4,12 @@ import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import android.view.LayoutInflater +import android.view.View 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 kotlinx.android.synthetic.main.view_card_permission_card.view.* class PermissionStatusCard @JvmOverloads constructor( @@ -28,10 +31,20 @@ class PermissionStatusCard @JvmOverloads constructor( layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height) } - fun render(text: String, correct: Boolean) { + fun render(title: String, correct: Boolean, body: String? = null) { + val errorTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.error) + val normalTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.slack_black) + permission_icon.isSelected = correct - permission_title.text = text + permission_title.text = title + permission_title.setTextColor(if (correct) normalTextColor else errorTextColor) + + if (correct || body == null) { + permission_body.visibility = View.GONE + } else { + permission_body.visibility = View.VISIBLE + permission_body.text = body + permission_body.setTextColor(if (correct) normalTextColor else errorTextColor) + } } - - } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryCodeSelectionActivity.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryCodeSelectionActivity.kt new file mode 100644 index 0000000..6acd06f --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryCodeSelectionActivity.kt @@ -0,0 +1,193 @@ +package au.gov.health.covidsafe.ui.onboarding + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.speech.RecognizerIntent +import android.speech.RecognizerIntent.EXTRA_RESULTS +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import au.gov.health.covidsafe.R +import kotlinx.android.synthetic.main.activity_country_code_selection.* + +fun RecyclerView.smoothSnapToPosition( + position: Int, + snapMode: Int = LinearSmoothScroller.SNAP_TO_START +) { + val smoothScroller = object : LinearSmoothScroller(this.context) { + override fun getVerticalSnapPreference(): Int = snapMode + override fun getHorizontalSnapPreference(): Int = snapMode + } + + smoothScroller.targetPosition = position + layoutManager?.startSmoothScroll(smoothScroller) +} + +const val VOICE_TO_TEXT_REQUEST_CODE = 2020 + +class CountryCodeSelectionActivity : Activity() { + val countryListItem = CountryList.getCountryList() + + private fun setupToolbar() { + countrySelectionToolbar.setNavigationOnClickListener { + super.onBackPressed() + } + + countrySelectionToolbar.title = getString(R.string.select_country_or_region) + } + + private fun setupCountryListRecyclerView() { + val linearLayoutManager = LinearLayoutManager(this) + countryListRecyclerView.layoutManager = linearLayoutManager + + val dividerItemDecoration = DividerItemDecoration( + this, + linearLayoutManager.orientation + ) + countryListRecyclerView.addItemDecoration(dividerItemDecoration) + + countryListRecyclerView.adapter = CountryListRecyclerViewAdapter( + this, + countryListItem + ) { + super.onBackPressed() + } + } + + private fun countryListScrollToPosition(positionOfLetter: Int) { + countryListRecyclerView.scrollToPosition(positionOfLetter) + + Thread { + Thread.sleep(100) + runOnUiThread { + countryListRecyclerView.smoothSnapToPosition(positionOfLetter) + } + }.start() + } + + private fun setupSearchFunctions() { + // text based search + countryRegionNameEditText.setOnFocusChangeListener { _, hasFocus -> + countrySearchImageView.visibility = if (hasFocus) View.GONE else View.VISIBLE + } + + countryRegionNameEditText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + fun getPositionOfCountryName(searchText: String): Int { + countryListItem.forEachIndexed { index, countryListItemInterface -> + if (countryListItemInterface is CountryGroupTitle) { + val groupTitle = getString(countryListItemInterface.titleResId) + if (groupTitle.startsWith(searchText, ignoreCase = true) + ) { + return index + } + } else if (countryListItemInterface is CountryListItem) { + val countryName = getString(countryListItemInterface.countryNameResId) + if (countryName.contains(searchText, ignoreCase = true) + ) { + return index + } + + val callingCode = countryListItemInterface.callingCode + if ("$callingCode".startsWith(searchText, ignoreCase = true) + ) { + return index + } + } + } + + return -1 + } + + s?.toString()?.let { enteredText -> + val positionOfCountryName = getPositionOfCountryName(enteredText) + if (positionOfCountryName != -1) { + countryListScrollToPosition(positionOfCountryName) + } + } + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // do nothing + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + // do nothing + } + + }) + + // voice to text search + microphoneImageView.setOnClickListener { + startActivityForResult( + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), + VOICE_TO_TEXT_REQUEST_CODE + ) + } + } + + private fun setupInitialLetterRecyclerView() { + val alphabet = ArrayList() + var letter = 'A' + while (letter <= 'Z') { + if (letter != 'W' && letter != 'X') { + alphabet.add(letter.toString()) + } + ++letter + } + + countryInitialLetterRecyclerView.layoutManager = LinearLayoutManager(this) + countryInitialLetterRecyclerView.adapter = CountryInitialLetterRecyclerViewAdapter( + this, + alphabet + ) { letterClicked -> + fun getPositionOfLetter(letter: String): Int { + countryListItem.forEachIndexed { index, countryListItemInterface -> + if (countryListItemInterface is CountryGroupTitle) { + val groupTitle = getString(countryListItemInterface.titleResId) + if (groupTitle.startsWith(letter, ignoreCase = true)) { + return index + } + } + } + + return 0 + } + + val positionOfLetter = getPositionOfLetter(letterClicked) + + countryListScrollToPosition(positionOfLetter) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == VOICE_TO_TEXT_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + data?.getStringArrayListExtra(EXTRA_RESULTS)?.let { + countryRegionNameEditText.setText(it.first()) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_country_code_selection) + + setupToolbar() + setupCountryListRecyclerView() + setupInitialLetterRecyclerView() + + // set up the search functions + setupSearchFunctions() + + } + + +} + diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryInitialLetterRecyclerView.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryInitialLetterRecyclerView.kt new file mode 100644 index 0000000..f31a320 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryInitialLetterRecyclerView.kt @@ -0,0 +1,48 @@ +package au.gov.health.covidsafe.ui.onboarding + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import au.gov.health.covidsafe.R + +class CountryInitialLetterHolder( + itemView: View, + private val onLetterClicked: (letter: String) -> Unit +) : RecyclerView.ViewHolder(itemView) { + fun setLetter(letter: String) { + val letterTextView = itemView.findViewById(R.id.country_initial_letter) + letterTextView.text = letter + letterTextView.setOnClickListener { + onLetterClicked(letter) + } + } +} + +class CountryInitialLetterRecyclerViewAdapter( + private val context: Context, + private val initialLetters: List, + private val onLetterClicked: (letter: String) -> Unit +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CountryInitialLetterHolder { + return CountryInitialLetterHolder( + LayoutInflater.from(context).inflate( + R.layout.view_list_item_country_initial_letter, + parent, + false + ), + onLetterClicked + ) + } + + override fun getItemCount(): Int { + return initialLetters.size + } + + override fun onBindViewHolder(holder: CountryInitialLetterHolder, position: Int) { + holder.setLetter(initialLetters[position]) + } + +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryList.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryList.kt new file mode 100644 index 0000000..8063129 --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryList.kt @@ -0,0 +1,253 @@ +package au.gov.health.covidsafe.ui.onboarding + +import au.gov.health.covidsafe.R + +object CountryList { + + fun getCountryList() : List{ + // for now, it returns the grouping for English only, in the future, it should return + // different groupings according to different language + return groupingForEnglish + } + + private val groupingForEnglish = listOf( + CountryGroupTitle(R.string.options_for_australia), + CountryListItem(R.string.country_au, 61, R.drawable.ic_list_country_au), + CountryListItem(R.string.country_nf, 672, R.drawable.ic_list_country_nf), + CountryGroupTitle(R.string.group_title_a), +// CountryListItem(R.string.country_af, 93, R.drawable.ic_list_country_af), + CountryListItem(R.string.country_al, 355, R.drawable.ic_list_country_al), + CountryListItem(R.string.country_dz, 213, R.drawable.ic_list_country_dz), +// CountryListItem(R.string.country_ad, 376, R.drawable.ic_list_country_ad), + CountryListItem(R.string.country_ao, 244, R.drawable.ic_list_country_ao), + CountryListItem(R.string.country_ai, 1, R.drawable.ic_list_country_ai), + CountryListItem(R.string.country_ag, 1, R.drawable.ic_list_country_ag), + CountryListItem(R.string.country_ar, 54, R.drawable.ic_list_country_ar), + CountryListItem(R.string.country_am, 374, R.drawable.ic_list_country_am), + CountryListItem(R.string.country_aw, 297, R.drawable.ic_list_country_aw), + CountryListItem(R.string.country_au, 61, R.drawable.ic_list_country_au), + CountryListItem(R.string.country_at, 43, R.drawable.ic_list_country_at), + CountryListItem(R.string.country_az, 994, R.drawable.ic_list_country_az), + CountryGroupTitle(R.string.group_title_b), + CountryListItem(R.string.country_bs, 1, R.drawable.ic_list_country_bs), + CountryListItem(R.string.country_bh, 973, R.drawable.ic_list_country_bh), + CountryListItem(R.string.country_bd, 880, R.drawable.ic_list_country_bd), + CountryListItem(R.string.country_bb, 1, R.drawable.ic_list_country_bb), + CountryListItem(R.string.country_by, 375, R.drawable.ic_list_country_by), + CountryListItem(R.string.country_be, 32, R.drawable.ic_list_country_be), + CountryListItem(R.string.country_bz, 501, R.drawable.ic_list_country_bz), + CountryListItem(R.string.country_bj, 229, R.drawable.ic_list_country_bj), + CountryListItem(R.string.country_bm, 1, R.drawable.ic_list_country_bm), +// CountryListItem(R.string.country_bt, 975, R.drawable.ic_list_country_bt), + CountryListItem(R.string.country_bo, 591, R.drawable.ic_list_country_bo), + CountryListItem(R.string.country_ba, 387, R.drawable.ic_list_country_ba), + CountryListItem(R.string.country_bw, 267, R.drawable.ic_list_country_bw), + CountryListItem(R.string.country_br, 55, R.drawable.ic_list_country_br), +// CountryListItem(R.string.country_bn, 673, R.drawable.ic_list_country_bn), + CountryListItem(R.string.country_vg, 1, R.drawable.ic_list_country_vg), + CountryListItem(R.string.country_bg, 359, R.drawable.ic_list_country_bg), + CountryListItem(R.string.country_bf, 226, R.drawable.ic_list_country_bf), +// CountryListItem(R.string.country_bi, 257, R.drawable.ic_list_country_bi), + CountryGroupTitle(R.string.group_title_c), + CountryListItem(R.string.country_kh, 855, R.drawable.ic_list_country_kh), + CountryListItem(R.string.country_cm, 237, R.drawable.ic_list_country_cm), + CountryListItem(R.string.country_ca, 1, R.drawable.ic_list_country_ca), + CountryListItem(R.string.country_cv, 238, R.drawable.ic_list_country_cv), + CountryListItem(R.string.country_ky, 1, R.drawable.ic_list_country_ky), +// CountryListItem(R.string.country_cf, 236, R.drawable.ic_list_country_cf), +// CountryListItem(R.string.country_td, 235, R.drawable.ic_list_country_td), + CountryListItem(R.string.country_cl, 56, R.drawable.ic_list_country_cl), + CountryListItem(R.string.country_cn, 86, R.drawable.ic_list_country_cn), + CountryListItem(R.string.country_co, 57, R.drawable.ic_list_country_co), +// CountryListItem(R.string.country_km, 269, R.drawable.ic_list_country_km), +// CountryListItem(R.string.country_ck, 682, R.drawable.ic_list_country_ck), + CountryListItem(R.string.country_cr, 506, R.drawable.ic_list_country_cr), + CountryListItem(R.string.country_hr, 385, R.drawable.ic_list_country_hr), + CountryListItem(R.string.country_cu, 53, R.drawable.ic_list_country_cu), + CountryListItem(R.string.country_cw, 599, R.drawable.ic_list_country_cw), + CountryListItem(R.string.country_cy, 357, R.drawable.ic_list_country_cy), + CountryListItem(R.string.country_cz, 420, R.drawable.ic_list_country_cz), +// CountryListItem(R.string.country_cd, 243, R.drawable.ic_list_country_cd), + CountryGroupTitle(R.string.group_title_d), + CountryListItem(R.string.country_dk, 45, R.drawable.ic_list_country_dk), +// CountryListItem(R.string.country_dj, 253, R.drawable.ic_list_country_dj), + CountryListItem(R.string.country_dm, 1, R.drawable.ic_list_country_dm), + CountryListItem(R.string.country_do, 1, R.drawable.ic_list_country_do), + CountryGroupTitle(R.string.group_title_e), + CountryListItem(R.string.country_ec, 593, R.drawable.ic_list_country_ec), +// CountryListItem(R.string.country_eg, 20, R.drawable.ic_list_country_eg), + CountryListItem(R.string.country_sv, 503, R.drawable.ic_list_country_sv), +// CountryListItem(R.string.country_gq, 240, R.drawable.ic_list_country_gq), + CountryListItem(R.string.country_ee, 372, R.drawable.ic_list_country_ee), +// CountryListItem(R.string.country_et, 251, R.drawable.ic_list_country_et), + CountryGroupTitle(R.string.group_title_f), +// CountryListItem(R.string.country_fo, 298, R.drawable.ic_list_country_fo), + CountryListItem(R.string.country_fj, 679, R.drawable.ic_list_country_fj), + CountryListItem(R.string.country_fi, 358, R.drawable.ic_list_country_fi), + CountryListItem(R.string.country_fr, 33, R.drawable.ic_list_country_fr), + CountryGroupTitle(R.string.group_title_g), +// CountryListItem(R.string.country_gf, 995, R.drawable.ic_list_country_fr), + CountryListItem(R.string.country_ga, 241, R.drawable.ic_list_country_ga), +// CountryListItem(R.string.country_gm, 220, R.drawable.ic_list_country_gm), + CountryListItem(R.string.country_ge, 995, R.drawable.ic_list_country_ge), + CountryListItem(R.string.country_de, 49, R.drawable.ic_list_country_de), + CountryListItem(R.string.country_gh, 233, R.drawable.ic_list_country_gh), +// CountryListItem(R.string.country_gi, 350, R.drawable.ic_list_country_gi), + CountryListItem(R.string.country_gr, 30, R.drawable.ic_list_country_gr), +// CountryListItem(R.string.country_gl, 299, R.drawable.ic_list_country_gl), + CountryListItem(R.string.country_gd, 1, R.drawable.ic_list_country_gd), +// CountryListItem(R.string.country_gp, 224, R.drawable.ic_list_country_fr), + CountryListItem(R.string.country_gu, 1, R.drawable.ic_list_country_gu), + CountryListItem(R.string.country_gt, 502, R.drawable.ic_list_country_gt), +// CountryListItem(R.string.country_gn, 224, R.drawable.ic_list_country_gn), + CountryListItem(R.string.country_gw, 245, R.drawable.ic_list_country_gw), +// CountryListItem(R.string.country_gy, 592, R.drawable.ic_list_country_gy), + CountryGroupTitle(R.string.group_title_h), + CountryListItem(R.string.country_ht, 509, R.drawable.ic_list_country_ht), + CountryListItem(R.string.country_hn, 504, R.drawable.ic_list_country_hn), + CountryListItem(R.string.country_hk, 852, R.drawable.ic_list_country_hk), + CountryListItem(R.string.country_hu, 36, R.drawable.ic_list_country_hu), + CountryGroupTitle(R.string.group_title_i), + CountryListItem(R.string.country_is, 354, R.drawable.ic_list_country_is), + CountryListItem(R.string.country_in, 91, R.drawable.ic_list_country_in), + CountryListItem(R.string.country_id, 62, R.drawable.ic_list_country_id), + CountryListItem(R.string.country_ir, 964, R.drawable.ic_list_country_ir), + CountryListItem(R.string.country_iq, 964, R.drawable.ic_list_country_iq), + CountryListItem(R.string.country_ie, 353, R.drawable.ic_list_country_ie), + CountryListItem(R.string.country_il, 972, R.drawable.ic_list_country_il), +// CountryListItem(R.string.country_it, 39, R.drawable.ic_list_country_it), + CountryListItem(R.string.country_ci, 225, R.drawable.ic_list_country_ci), + CountryGroupTitle(R.string.group_title_j), + CountryListItem(R.string.country_jm, 1, R.drawable.ic_list_country_jm), + CountryListItem(R.string.country_jp, 81, R.drawable.ic_list_country_jp), + CountryListItem(R.string.country_jo, 962, R.drawable.ic_list_country_jo), + CountryGroupTitle(R.string.group_title_k), + CountryListItem(R.string.country_kz, 7, R.drawable.ic_list_country_kz), + CountryListItem(R.string.country_ke, 254, R.drawable.ic_list_country_ke), +// CountryListItem(R.string.country_ki, 686, R.drawable.ic_list_country_ki), + CountryListItem(R.string.country_kw, 965, R.drawable.ic_list_country_kw), + CountryListItem(R.string.country_kg, 996, R.drawable.ic_list_country_kg), + CountryGroupTitle(R.string.group_title_l), + CountryListItem(R.string.country_la, 856, R.drawable.ic_list_country_la), + CountryListItem(R.string.country_lv, 371, R.drawable.ic_list_country_lv), + CountryListItem(R.string.country_lb, 961, R.drawable.ic_list_country_lb), +// CountryListItem(R.string.country_ls, 266, R.drawable.ic_list_country_ls), +// CountryListItem(R.string.country_lr, 231, R.drawable.ic_list_country_lr), +// CountryListItem(R.string.country_ly, 218, R.drawable.ic_list_country_ly), + CountryListItem(R.string.country_li, 423, R.drawable.ic_list_country_li), + CountryListItem(R.string.country_lt, 370, R.drawable.ic_list_country_lt), + CountryListItem(R.string.country_lu, 352, R.drawable.ic_list_country_lu), + CountryGroupTitle(R.string.group_title_m), + CountryListItem(R.string.country_mo, 853, R.drawable.ic_list_country_mo), +// CountryListItem(R.string.country_mg, 261, R.drawable.ic_list_country_mg), +// CountryListItem(R.string.country_mw, 265, R.drawable.ic_list_country_mw), + CountryListItem(R.string.country_my, 60, R.drawable.ic_list_country_my), +// CountryListItem(R.string.country_mv, 960, R.drawable.ic_list_country_mv), + CountryListItem(R.string.country_ml, 223, R.drawable.ic_list_country_ml), + CountryListItem(R.string.country_mt, 356, R.drawable.ic_list_country_mt), + CountryListItem(R.string.country_mq, 1, R.drawable.ic_list_country_mq), +// CountryListItem(R.string.country_mr, 222, R.drawable.ic_list_country_mr), + CountryListItem(R.string.country_mu, 230, R.drawable.ic_list_country_mu), + CountryListItem(R.string.country_mx, 52, R.drawable.ic_list_country_mx), + CountryListItem(R.string.country_md, 373, R.drawable.ic_list_country_md), +// CountryListItem(R.string.country_mc, 377, R.drawable.ic_list_country_mc), +// CountryListItem(R.string.country_mn, 976, R.drawable.ic_list_country_mn), +// CountryListItem(R.string.country_me, 382, R.drawable.ic_list_country_me), + CountryListItem(R.string.country_ms, 1, R.drawable.ic_list_country_ms), + CountryListItem(R.string.country_ma, 212, R.drawable.ic_list_country_ma), + CountryListItem(R.string.country_mz, 258, R.drawable.ic_list_country_mz), + CountryListItem(R.string.country_mm, 95, R.drawable.ic_list_country_mm), + CountryGroupTitle(R.string.group_title_n), + CountryListItem(R.string.country_na, 264, R.drawable.ic_list_country_na), + CountryListItem(R.string.country_np, 977, R.drawable.ic_list_country_np), + CountryListItem(R.string.country_nl, 31, R.drawable.ic_list_country_nl), +// CountryListItem(R.string.country_an, 599, R.drawable.ic_list_country_an), +// CountryListItem(R.string.country_nc, 687, R.drawable.ic_list_country_nc), + CountryListItem(R.string.country_nz, 64, R.drawable.ic_list_country_nz), + CountryListItem(R.string.country_ni, 505, R.drawable.ic_list_country_ni), + CountryListItem(R.string.country_ne, 227, R.drawable.ic_list_country_ne), + CountryListItem(R.string.country_ng, 234, R.drawable.ic_list_country_ng), + CountryListItem(R.string.country_nf, 672, R.drawable.ic_list_country_nf), + CountryListItem(R.string.country_mk, 389, R.drawable.ic_list_country_mk), + CountryListItem(R.string.country_no, 47, R.drawable.ic_list_country_no), + CountryGroupTitle(R.string.group_title_o), + CountryListItem(R.string.country_om, 968, R.drawable.ic_list_country_om), + CountryGroupTitle(R.string.group_title_p), + CountryListItem(R.string.country_pk, 92, R.drawable.ic_list_country_pk), +// CountryListItem(R.string.country_pw, 680, R.drawable.ic_list_country_pw), +// CountryListItem(R.string.country_ps, 970, R.drawable.ic_list_country_ps), + CountryListItem(R.string.country_pa, 507, R.drawable.ic_list_country_pa), + CountryListItem(R.string.country_pg, 675, R.drawable.ic_list_country_pg), + CountryListItem(R.string.country_py, 595, R.drawable.ic_list_country_py), + CountryListItem(R.string.country_pe, 51, R.drawable.ic_list_country_pe), + CountryListItem(R.string.country_ph, 63, R.drawable.ic_list_country_ph), + CountryListItem(R.string.country_pl, 48, R.drawable.ic_list_country_pl), + CountryListItem(R.string.country_pt, 351, R.drawable.ic_list_country_pt), + CountryListItem(R.string.country_pr, 1, R.drawable.ic_list_country_pr), + CountryGroupTitle(R.string.group_title_q), + CountryListItem(R.string.country_qa, 974, R.drawable.ic_list_country_qa), + CountryGroupTitle(R.string.group_title_r), +// CountryListItem(R.string.country_cg, 242, R.drawable.ic_list_country_cg), +// CountryListItem(R.string.country_re, 262, R.drawable.ic_list_country_fr), +// CountryListItem(R.string.country_ro, 40, R.drawable.ic_list_country_ro), + CountryListItem(R.string.country_ru, 7, R.drawable.ic_list_country_ru), + CountryListItem(R.string.country_rw, 250, R.drawable.ic_list_country_rw), + CountryGroupTitle(R.string.group_title_s), + CountryListItem(R.string.country_kn, 1, R.drawable.ic_list_country_kn), + CountryListItem(R.string.country_lc, 1, R.drawable.ic_list_country_lc), + CountryListItem(R.string.country_vc, 1, R.drawable.ic_list_country_vc), +// CountryListItem(R.string.country_ws, 685, R.drawable.ic_list_country_ws), +// CountryListItem(R.string.country_st, 239, R.drawable.ic_list_country_st), +// CountryListItem(R.string.country_sa, 966, R.drawable.ic_list_country_sa), + CountryListItem(R.string.country_sn, 221, R.drawable.ic_list_country_sn), + CountryListItem(R.string.country_rs, 381, R.drawable.ic_list_country_rs), +// CountryListItem(R.string.country_sc, 248, R.drawable.ic_list_country_sc), +// CountryListItem(R.string.country_sl, 232, R.drawable.ic_list_country_sl), + CountryListItem(R.string.country_sg, 65, R.drawable.ic_list_country_sg), + CountryListItem(R.string.country_sk, 421, R.drawable.ic_list_country_sk), + CountryListItem(R.string.country_si, 386, R.drawable.ic_list_country_si), + CountryListItem(R.string.country_sb, 677, R.drawable.ic_list_country_sb), +// CountryListItem(R.string.country_so, 252, R.drawable.ic_list_country_so), + CountryListItem(R.string.country_za, 27, R.drawable.ic_list_country_za), + CountryListItem(R.string.country_kr, 82, R.drawable.ic_list_country_kr), +// CountryListItem(R.string.country_ss, 211, R.drawable.ic_list_country_ss), + CountryListItem(R.string.country_es, 34, R.drawable.ic_list_country_es), + CountryListItem(R.string.country_lk, 94, R.drawable.ic_list_country_lk), + CountryListItem(R.string.country_sd, 249, R.drawable.ic_list_country_sd), +// CountryListItem(R.string.country_sr, 597, R.drawable.ic_list_country_sr), +// CountryListItem(R.string.country_sz, 268, R.drawable.ic_list_country_sz), + CountryListItem(R.string.country_se, 46, R.drawable.ic_list_country_se), + CountryListItem(R.string.country_ch, 41, R.drawable.ic_list_country_ch), + CountryGroupTitle(R.string.group_title_t), + CountryListItem(R.string.country_tw, 886, R.drawable.ic_list_country_tw), + CountryListItem(R.string.country_tj, 992, R.drawable.ic_list_country_tj), + CountryListItem(R.string.country_tz, 255, R.drawable.ic_list_country_tz), + CountryListItem(R.string.country_th, 66, R.drawable.ic_list_country_th), +// CountryListItem(R.string.country_tl, 670, R.drawable.ic_list_country_tl), + CountryListItem(R.string.country_tg, 228, R.drawable.ic_list_country_tg), +// CountryListItem(R.string.country_to, 676, R.drawable.ic_list_country_to), + CountryListItem(R.string.country_tt, 1, R.drawable.ic_list_country_tt), + CountryListItem(R.string.country_tn, 216, R.drawable.ic_list_country_tn), + CountryListItem(R.string.country_tr, 90, R.drawable.ic_list_country_tr), + CountryListItem(R.string.country_tm, 993, R.drawable.ic_list_country_tm), + CountryListItem(R.string.country_tc, 1, R.drawable.ic_list_country_tc), + CountryGroupTitle(R.string.group_title_u), + CountryListItem(R.string.country_ug, 256, R.drawable.ic_list_country_ug), + CountryListItem(R.string.country_ua, 380, R.drawable.ic_list_country_ua), + CountryListItem(R.string.country_ae, 971, R.drawable.ic_list_country_ae), + CountryListItem(R.string.country_gb, 44, R.drawable.ic_list_country_gb), + CountryListItem(R.string.country_us, 1, R.drawable.ic_list_country_us), + CountryListItem(R.string.country_uy, 598, R.drawable.ic_list_country_uy), + CountryListItem(R.string.country_uz, 998, R.drawable.ic_list_country_uz), + CountryGroupTitle(R.string.group_title_v), +// CountryListItem(R.string.country_vu, 678, R.drawable.ic_list_country_vu), + CountryListItem(R.string.country_ve, 58, R.drawable.ic_list_country_ve), + CountryListItem(R.string.country_vn, 84, R.drawable.ic_list_country_vn), + CountryListItem(R.string.country_vi, 1, R.drawable.ic_list_country_vi), + CountryGroupTitle(R.string.group_title_y), + CountryListItem(R.string.country_ye, 967, R.drawable.ic_list_country_ye), + CountryGroupTitle(R.string.group_title_z), + CountryListItem(R.string.country_zm, 260, R.drawable.ic_list_country_zm), + CountryListItem(R.string.country_zw, 263, R.drawable.ic_list_country_zw) + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000..7c1325d --- /dev/null +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/CountryListRecyclerView.kt @@ -0,0 +1,127 @@ +package au.gov.health.covidsafe.ui.onboarding + +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.recyclerview.widget.RecyclerView +import au.gov.health.covidsafe.Preference +import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.TracerApp + +const val VIEW_TYPE_GROUP_TITLE = 1 +const val VIEW_TYPE_COUNTRY = 2 + +interface CountryListItemInterface + +class CountryListItem( + val countryNameResId: Int, + val callingCode: Int, + val flagResID: Int +) : CountryListItemInterface + +class CountryGroupTitle( + val titleResId: Int +) : CountryListItemInterface + +class CountryGroupTitleHolder( + itemView: View +) : RecyclerView.ViewHolder(itemView) { + fun setCountryGroupTitle(title: String) { + itemView.findViewById(R.id.country_group_title).text = title + } +} + +class CountryListItemHolder( + itemView: View, + private val onCountryClicked: () -> Unit +) : RecyclerView.ViewHolder(itemView) { + private var countryNameResId: Int = 0 + private var callingCode: Int = 0 + private var flagResID: Int = 0 + + fun setCountryListItem(countryNameResId: Int, + countryName: String, + callingCode: Int, + flagResID: Int) { + this.countryNameResId = countryNameResId + this.callingCode = callingCode + this.flagResID = flagResID + + itemView.findViewById(R.id.country_list_name).text = countryName + itemView.findViewById(R.id.country_list_calling_code).text = "+$callingCode" + itemView.findViewById(R.id.country_list_flag).setImageResource(flagResID) + + itemView.findViewById(R.id.country_list_item).setOnClickListener { + Preference.putCountryNameResID(TracerApp.AppContext, countryNameResId) + Preference.putCallingCode(TracerApp.AppContext, callingCode) + Preference.putNationalFlagResID(TracerApp.AppContext, flagResID) + + onCountryClicked() + } + } +} + +class CountryListRecyclerViewAdapter( + private val context: Context, + private val countryListItem: List, + private val onCountryClicked: () -> Unit +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_GROUP_TITLE -> CountryGroupTitleHolder( + LayoutInflater.from(context).inflate( + R.layout.view_list_item_group_title, + parent, + false + ) + ) + else -> CountryListItemHolder( + LayoutInflater.from(context).inflate( + R.layout.view_list_item_country, + parent, + false + ), + onCountryClicked + ) + } + } + + override fun getItemViewType(position: Int): Int { + return when (countryListItem[position]) { + is CountryGroupTitle -> VIEW_TYPE_GROUP_TITLE + else -> VIEW_TYPE_COUNTRY + } + } + + override fun getItemCount(): Int { + return countryListItem.size + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is CountryGroupTitleHolder -> { + val title = context.getString((countryListItem[position] as CountryGroupTitle).titleResId) + holder.setCountryGroupTitle(title) + } + + is CountryListItemHolder -> { + val countryListItem = (countryListItem[position] as CountryListItem) + val countryName = context.getString(countryListItem.countryNameResId) + + holder.setCountryListItem( + countryListItem.countryNameResId, + countryName, + countryListItem.callingCode, + countryListItem.flagResID + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enternumber/EnterNumberFragment.kt index f84425e..4c59b8c 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 @@ -1,28 +1,35 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.enternumber +import android.annotation.SuppressLint import android.app.AlertDialog +import android.content.Intent import android.os.Bundle -import android.text.Editable -import android.text.InputFilter -import android.text.TextWatcher -import android.text.method.LinkMovementMethod +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View +import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener 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.R import au.gov.health.covidsafe.TracerApp import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.UploadButtonLayout +import au.gov.health.covidsafe.ui.onboarding.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 import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_DESTINATION_ID import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_PHONE_NUMBER import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_PROGRESS import au.gov.health.covidsafe.ui.onboarding.fragment.enterpin.EnterPinFragment.Companion.ENTER_PIN_SESSION import kotlinx.android.synthetic.main.fragment_enter_number.* -import kotlinx.android.synthetic.main.fragment_enter_number.view.* class EnterNumberFragment : PagerChildFragment() { @@ -36,30 +43,21 @@ class EnterNumberFragment : PagerChildFragment() { private val enterNumberPresenter = EnterNumberPresenter(this) private var alertDialog: AlertDialog? = null + @NavigationRes private var destinationId: Int? = null - private val phoneNumberTextWatcher: TextWatcher = object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - // change LengthFilter if user making a mistake of entering phone number starting with 0 - val phoneNumberLength = TracerApp.AppContext.resources.getInteger(R.integer.australian_phone_number_length) - val filters = enter_number_phone_number.filters - val newFilterLength = if (s?.toString()?.startsWith("0") == true) { - phoneNumberLength + 1 - } else { - phoneNumberLength - } - enter_number_phone_number.filters = filters.filterNot { it is InputFilter.LengthFilter }.toTypedArray() + - InputFilter.LengthFilter(newFilterLength) + private lateinit var countryName: String + private var callingCode: Int = 0 + private var nationalFlagResID: Int = 0 - updateButtonState() - } + private val errorTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.error) + private val normalTextColor = ContextCompat.getColor(TracerApp.AppContext, R.color.slack_black) - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - } + private fun updateSelectedCountry() { + countryName = getString(Preference.getCountryNameResID(this.requireContext())) + callingCode = Preference.getCallingCode(this.requireContext()) + nationalFlagResID = Preference.getNationalFlagResID(this.requireContext()) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) @@ -67,7 +65,7 @@ class EnterNumberFragment : PagerChildFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - view.use_oz_phone_number.movementMethod = LinkMovementMethod.getInstance() + arguments?.let { destinationId = it.getInt(ENTER_NUMBER_DESTINATION_ID) stepProgress = if (it.containsKey(ENTER_NUMBER_PROGRESS)) it.getInt(ENTER_PIN_PROGRESS) else null @@ -76,26 +74,74 @@ class EnterNumberFragment : PagerChildFragment() { override fun onResume() { super.onResume() - enter_number_phone_number.selectAll() - enter_number_phone_number.addTextChangedListener(phoneNumberTextWatcher) + + updateSelectedCountry() + + enter_number_phone_number.addTextChangedListener { + enter_number_phone_number.setTextColor(normalTextColor) + } + + enter_number_phone_number.setOnEditorActionListener { _, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH || + actionId == EditorInfo.IME_ACTION_DONE || + event != null && + event.action == KeyEvent.ACTION_DOWN && + event.keyCode == KeyEvent.KEYCODE_ENTER) { + + if (event == null || !event.isShiftPressed) { + // user has done typing. + updateButtonState() + } + } + + false // pass on to other listeners. + } + updateButtonState() + displaySelectedCountryOrRegion() } - override fun onPause() { - super.onPause() - enter_number_phone_number.removeTextChangedListener(phoneNumberTextWatcher) + @SuppressLint("SetTextI18n") + private fun displaySelectedCountryOrRegion() { + country_name_code.text = "$countryName(+$callingCode)" + national_flag.setImageResource(nationalFlagResID) + + country_selection_box.setOnClickListener { + startActivity(Intent(this.requireContext(), CountryCodeSelectionActivity::class.java)) + } } - fun showInvalidPhoneNumber() { - invalid_phone_number.visibility = VISIBLE + private fun hideInvalidPhoneNumberPrompt() { + enter_number_headline.setTextColor(normalTextColor) + enter_number_phone_number.background = context?.getDrawable(R.drawable.edittext_modified_states) + enter_number_phone_number.setTextColor(normalTextColor) + invalid_phone_number.visibility = GONE + } + + fun showInvalidPhoneNumberPrompt(errorMessageResID: Int) { + enter_number_headline.setTextColor(errorTextColor) enter_number_phone_number.background = context?.getDrawable(R.drawable.phone_number_invalid_background) + enter_number_phone_number.setTextColor(errorTextColor) + invalid_phone_number.visibility = VISIBLE + invalid_phone_number.setText(errorMessageResID) } override fun updateButtonState() { - if (enterNumberPresenter.validateAuNumber(enter_number_phone_number?.text?.toString())) { + val phoneNumberValidity = enterNumberPresenter.validatePhoneNumber( + callingCode, + enter_number_phone_number.text.toString().trim() + ) + + if (phoneNumberValidity.first) { enableContinueButton() + + hideInvalidPhoneNumberPrompt() } else { disableContinueButton() + + if (enter_number_phone_number.text.toString().isNotEmpty()) { + showInvalidPhoneNumberPrompt(phoneNumberValidity.second) + } } } @@ -110,10 +156,12 @@ class EnterNumberFragment : PagerChildFragment() { fun navigateToOTPPage( session: String?, challengeName: String?, + callingCode: Int, phoneNumber: String) { val bundle = bundleOf( ENTER_PIN_SESSION to session, ENTER_PIN_CHALLENGE_NAME to challengeName, + ENTER_PIN_CALLING_CODE to callingCode, ENTER_PIN_PHONE_NUMBER to phoneNumber, ENTER_PIN_DESTINATION_ID to destinationId).also { bundle -> stepProgress?.let { @@ -130,7 +178,7 @@ class EnterNumberFragment : PagerChildFragment() { } override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.enter_number_button) { - enterNumberPresenter.requestOTP(enter_number_phone_number.text.toString().trim()) + enterNumberPresenter.requestOTP(callingCode, enter_number_phone_number.text.toString().trim()) } fun showCheckInternetError() { 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 9167abb..53ae16e 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 @@ -6,19 +6,21 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import au.gov.health.covidsafe.Preference import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.TracerApp import au.gov.health.covidsafe.extensions.isInternetAvailable import au.gov.health.covidsafe.factory.NetworkFactory import au.gov.health.covidsafe.interactor.usecase.GetOnboardingOtp import au.gov.health.covidsafe.interactor.usecase.GetOnboardingOtpException import au.gov.health.covidsafe.interactor.usecase.GetOtpParams +const val AUSTRALIA_CALLING_CODE = 61 +const val AUSTRALIA_MOBILE_NUMBER_LENGTH = 9 +const val AUSTRALIA_MOBILE_NUMBER_PREFIX_DIGIT = "0" +const val NORFOLK_ISLAND_CALLING_CODE = 672 +const val NORFOLK_ISLAND_MOBILE_NUMBER_LENGTH = 5 +const val NORFOLK_ISLAND_MOBILE_PREFIX_DIGIT = "3" class EnterNumberPresenter(private val enterNumberFragment: EnterNumberFragment) : LifecycleObserver { - private val TAG = this.javaClass.simpleName - - private lateinit var phoneNumber: String private lateinit var getOnboardingOtp: GetOnboardingOtp init { @@ -30,47 +32,49 @@ class EnterNumberPresenter(private val enterNumberFragment: EnterNumberFragment) getOnboardingOtp = GetOnboardingOtp(NetworkFactory.awsClient, enterNumberFragment.lifecycle) } - internal fun requestOTP(phoneNumber: String) { + fun requestOTP(callingCode: Int, phoneNumber: String) { + val prefixZeroRemovedPhoneNumber = + adjustPrefixForAustralianAndNorfolkPhoneNumber(callingCode, phoneNumber) + when { enterNumberFragment.activity?.isInternetAvailable() == false -> { enterNumberFragment.showCheckInternetError() } - validateAuNumber(phoneNumber) -> { - val cleansedNumber = if (phoneNumber.startsWith("0")) { - phoneNumber.takeLast(TracerApp.AppContext.resources.getInteger(R.integer.australian_phone_number_length)) - } else phoneNumber - val fullNumber = "${enterNumberFragment.resources.getString(R.string.enter_number_prefix)}$cleansedNumber" - Preference.putPhoneNumber(TracerApp.AppContext, fullNumber) - this.phoneNumber = cleansedNumber - makeOTPCall(cleansedNumber) - } - else -> { - enterNumberFragment.showInvalidPhoneNumber() - } + else -> makeOTPCall(callingCode, prefixZeroRemovedPhoneNumber) } } /** - * @param phoneNumber cleansed phone number, 9 digits, doesn't start with 0 + * if [callingCode] is 61 for Australia, then [phoneNumber] should be a prefix removed + * Australian phone number: 9 digits, doesn't start with 0. + * Otherwise [phoneNumber] can be any number and the validation should be done in the backend */ - private fun makeOTPCall(phoneNumber: String) { + private fun makeOTPCall(callingCode: Int, phoneNumber: String) { enterNumberFragment.activity?.let { enterNumberFragment.disableContinueButton() enterNumberFragment.showLoading() - getOnboardingOtp.invoke(GetOtpParams(phoneNumber, - Preference.getDeviceID(enterNumberFragment.requireContext()), - Preference.getPostCode(enterNumberFragment.requireContext()), - Preference.getAge(enterNumberFragment.requireContext()), - Preference.getName(enterNumberFragment.requireContext())), + + val context = enterNumberFragment.requireContext() + + getOnboardingOtp.invoke( + GetOtpParams( + countryCode = "+$callingCode", + phoneNumber = phoneNumber, + deviceId = Preference.getDeviceID(context), + postCode = Preference.getPostCode(context), + age = Preference.getAge(context), + name = Preference.getName(context) + ), onSuccess = { enterNumberFragment.navigateToOTPPage( it.session, it.challengeName, + callingCode, phoneNumber) }, onFailure = { if (it is GetOnboardingOtpException.GetOtpInvalidNumberException) { - enterNumberFragment.showInvalidPhoneNumber() + enterNumberFragment.showInvalidPhoneNumberPrompt(R.string.invalid_phone_number) } else { enterNumberFragment.showGenericError() } @@ -80,12 +84,46 @@ class EnterNumberPresenter(private val enterNumberFragment: EnterNumberFragment) } } - internal fun validateAuNumber(phoneNumber: String?): Boolean { - var australianPhoneNumberLength = enterNumberFragment.resources.getInteger(R.integer.australian_phone_number_length) - if (phoneNumber?.startsWith("0") == true) { - australianPhoneNumberLength++ + fun validatePhoneNumber(callingCode: Int, phoneNumber: String): Pair { + val isNumberValid = when (callingCode) { + AUSTRALIA_CALLING_CODE -> { + if (phoneNumber.startsWith(AUSTRALIA_MOBILE_NUMBER_PREFIX_DIGIT)) { + phoneNumber.length == AUSTRALIA_MOBILE_NUMBER_LENGTH + 1 + } else { + phoneNumber.length == AUSTRALIA_MOBILE_NUMBER_LENGTH + } + } + NORFOLK_ISLAND_CALLING_CODE -> { + if (phoneNumber.startsWith(NORFOLK_ISLAND_MOBILE_PREFIX_DIGIT)) { + phoneNumber.length == NORFOLK_ISLAND_MOBILE_NUMBER_LENGTH + 1 + } else { + phoneNumber.length == NORFOLK_ISLAND_MOBILE_NUMBER_LENGTH + } + } + else -> true + } + + val errorMessageResID = when (callingCode) { + AUSTRALIA_CALLING_CODE -> R.string.invalid_australian_phone_number_error_prompt + NORFOLK_ISLAND_CALLING_CODE -> R.string.invalid_norfolk_island_phone_number_error_prompt + else -> 0 + } + + return Pair(isNumberValid, errorMessageResID) + } + + private fun adjustPrefixForAustralianAndNorfolkPhoneNumber(callingCode: Int, phoneNumber: String): String { + return when (callingCode) { + AUSTRALIA_CALLING_CODE -> phoneNumber.removePrefix(AUSTRALIA_MOBILE_NUMBER_PREFIX_DIGIT) + NORFOLK_ISLAND_CALLING_CODE -> { + if (phoneNumber.length == NORFOLK_ISLAND_MOBILE_NUMBER_LENGTH) { + "$NORFOLK_ISLAND_MOBILE_PREFIX_DIGIT$phoneNumber" + } else { + phoneNumber + } + } + else -> phoneNumber } - return (phoneNumber?.length ?: 0) == australianPhoneNumberLength } } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/enterpin/EnterPinFragment.kt index 13e97b0..e3bb502 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 @@ -24,6 +24,7 @@ class EnterPinFragment : PagerChildFragment() { companion object { const val ENTER_PIN_SESSION = "session" const val ENTER_PIN_CHALLENGE_NAME = "challenge_name" + const val ENTER_PIN_CALLING_CODE = "calling_code" const val ENTER_PIN_PHONE_NUMBER = "phone_number" const val ENTER_PIN_DESTINATION_ID = "destination_id" const val ENTER_PIN_PROGRESS = "progress" @@ -48,13 +49,18 @@ class EnterPinFragment : PagerChildFragment() { arguments?.let { val session = it.getString(ENTER_PIN_SESSION) val challengeName = it.getString(ENTER_PIN_CHALLENGE_NAME) + val callingCode = it.getInt(ENTER_PIN_CALLING_CODE) val phoneNumber = it.getString(ENTER_PIN_PHONE_NUMBER) destinationId = it.getInt(ENTER_PIN_DESTINATION_ID) + stepProgress = if (it.containsKey(ENTER_PIN_PROGRESS)) it.getInt(ENTER_PIN_PROGRESS) else null - enter_pin_headline.text = resources.getString(R.string.enter_pin_headline, resources.getString(R.string.enter_number_prefix), phoneNumber) + + enter_pin_headline.text = resources.getString(R.string.enter_pin_headline, "+$callingCode", phoneNumber) + presenter = EnterPinPresenter(this@EnterPinFragment, session, challengeName, + callingCode, phoneNumber) } 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 339e64c..17e1b9c 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 @@ -19,6 +19,7 @@ import retrofit2.Response class EnterPinPresenter(private val enterPinFragment: EnterPinFragment, private var session: String?, private var challengeName: String?, + private val callingCode: Int, private val phoneNumber: String?) : LifecycleObserver { private val TAG = this.javaClass.simpleName @@ -45,11 +46,17 @@ class EnterPinPresenter(private val enterPinFragment: EnterPinFragment, enterPinFragment.showGenericError() } else -> { - getOtp.invoke(GetOtpParams(phoneNumber, - Preference.getDeviceID(enterPinFragment.requireContext()), - Preference.getPostCode(enterPinFragment.requireContext()), - Preference.getAge(enterPinFragment.requireContext()), - Preference.getName(enterPinFragment.requireContext())), + val context = enterPinFragment.requireContext() + + getOtp.invoke( + GetOtpParams( + countryCode = "+$callingCode", + phoneNumber = phoneNumber, + deviceId = Preference.getDeviceID(context), + postCode = Preference.getPostCode(context), + age = Preference.getAge(context), + name = Preference.getName(context) + ), onSuccess = { session = it.session challengeName = it.challengeName 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 a58393a..e366f77 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 @@ -3,20 +3,27 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.personal import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent +import android.view.inputmethod.EditorInfo import android.widget.NumberPicker import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf +import androidx.core.widget.addTextChangedListener +import au.gov.health.covidsafe.Preference import au.gov.health.covidsafe.R import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.UploadButtonLayout import au.gov.health.covidsafe.ui.onboarding.fragment.enternumber.EnterNumberFragment +import kotlinx.android.synthetic.main.fragment_enter_number.* import kotlinx.android.synthetic.main.fragment_personal_details.* +import java.util.regex.Pattern + + +private val POST_CODE_REGEX = Pattern.compile("^(?:(?:[2-8]\\d|9[0-7]|0?[28]|0?9(?=09))(?:\\d{2}))$") class PersonalDetailsFragment : PagerChildFragment() { @@ -25,47 +32,141 @@ class PersonalDetailsFragment : PagerChildFragment() { private var alertDialog: AlertDialog? = null override var stepProgress: Int? = 1 override val navigationIcon: Int = R.drawable.ic_up - private var ageSelected: Pair? = null - private val presenter = PersonalDetailsPresenter(this) + private var ageSelected: Pair = Pair(-1, "") - private val nameTextWatcher: TextWatcher = object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - hideNameError() - updateButtonState() - } + private lateinit var name: String + private lateinit var postcode: String + private var age: Int = -1 - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - } + private fun updatePersonalDetailsDataField() { + name = personal_details_name.text.toString() + postcode = personal_details_post_code.text.toString() + age = ageSelected.first } - private val postCodeTextWatcher: TextWatcher = object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - presenter.validateInlinePostCode(s.toString()) - updateButtonState() - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - } - } + private fun isFullName() = name.trim().length > 1 + private fun isValidAge() = age >= 0 + private fun isValidPostcode() = postcode.length == 4 && POST_CODE_REGEX.matcher(postcode).matches() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) : View? = inflater.inflate(R.layout.fragment_personal_details, container, false) override fun onResume() { super.onResume() - personal_details_name.addTextChangedListener(nameTextWatcher) - personal_details_post_code.addTextChangedListener(postCodeTextWatcher) + + personal_details_age.setText(ageSelected.second) + + fun showAgePicker() { + activity?.let { activity -> + val ages = resources.getStringArray(R.array.personal_details_age_array).map { + it.split(":").let { split -> + (split[0]).toInt() to split[1] + } + } + + var selected = ages.firstOrNull { it == ageSelected }?.let { + ages.indexOf(it) + } ?: 0 + + picker = NumberPicker(activity) + picker?.minValue = 0 + picker?.maxValue = ages.size - 1 + picker?.displayedValues = ages.map { it.second }.toTypedArray() + picker?.setOnValueChangedListener { _, _, newVal -> + selected = newVal + } + picker?.value = selected + alertDialog?.dismiss() + alertDialog = AlertDialog.Builder(activity) + .setTitle(R.string.personal_details_age_dialog_title) + .setView(picker) + .setPositiveButton(R.string.personal_details_dialog_ok) { _, _ -> + ageSelected = ages[selected] + + personal_details_age.setText(ageSelected.second) + + updatePersonalDetailsDataField() + updateButtonState() + + personal_details_age_error.visibility = + if (isValidAge()) { + View.GONE + } else { + View.VISIBLE + } + + personal_details_post_code.requestFocus() + } + .setNegativeButton(android.R.string.no, null) + .show() + } + } + + personal_details_name.setOnFocusChangeListener { _, hasFocus -> + updatePersonalDetailsDataField() + updateButtonState() + + personal_details_name_error.visibility = if (hasFocus || isFullName()) { + View.GONE + } else { + View.VISIBLE + } + } + + personal_details_post_code.setOnFocusChangeListener { _, hasFocus -> + if(hasFocus) { + updatePersonalDetailsDataField() + updateButtonState() + + personal_details_age_error.visibility = + if (isValidAge()) { + View.GONE + } else { + View.VISIBLE + } + } + } + + personal_details_post_code.setOnEditorActionListener { _, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH || + actionId == EditorInfo.IME_ACTION_DONE || + event != null && + event.action == KeyEvent.ACTION_DOWN && + event.keyCode == KeyEvent.KEYCODE_ENTER) { + + if (event == null || !event.isShiftPressed) { + // user has done typing. + updatePersonalDetailsDataField() + updateButtonState() + + personal_details_age_error.visibility = + if (isValidAge()) { + View.GONE + } else { + View.VISIBLE + } + + personal_details_post_code_error.visibility = if (isValidPostcode()) { + View.GONE + } else { + View.VISIBLE + } + } + } + + false // pass on to other listeners. + } + + personal_details_age.setOnFocusChangeListener { _, hasFocus -> + if(hasFocus){ + showAgePicker() + } + } + personal_details_age.setOnClickListener { showAgePicker() } - personal_details_age.text = ageSelected?.second // set accessibility focus to the title "Enter your details" personal_details_headline.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) @@ -73,36 +174,30 @@ class PersonalDetailsFragment : PagerChildFragment() { override fun onPause() { super.onPause() - personal_details_name.removeTextChangedListener(nameTextWatcher) - personal_details_post_code.removeTextChangedListener(postCodeTextWatcher) - personal_details_age.setOnClickListener(null) alertDialog?.dismiss() } override fun getUploadButtonLayout(): UploadButtonLayout = UploadButtonLayout.ContinueLayout(R.string.personal_details_button) { - presenter.saveInfos(personal_details_name.text.toString(), personal_details_post_code.text.toString(), getMidAgeToSend()) + val context = this.requireContext() + Preference.putName(context, name) + Preference.putAge(context, "$age") + Preference.putPostCode(context, postcode) + + navigateToNextPage(age < 16) } override fun updateButtonState() { - if (presenter.validateInputsForButtonUpdate(personal_details_name.text.toString(), personal_details_post_code.text.toString(), getMidAgeToSend())) { + updatePersonalDetailsDataField() + + if (isFullName() && isValidAge() && isValidPostcode()) { enableContinueButton() } else { disableContinueButton() } } - fun showGenericError() { - activity?.let { activity -> - alertDialog?.dismiss() - alertDialog = AlertDialog.Builder(activity) - .setMessage(R.string.generic_error) - .setIcon(android.R.drawable.ic_dialog_alert) - .setPositiveButton(android.R.string.yes, null).show() - } - } - - fun navigateToNextPage(minor: Boolean) { - if (minor) { + private fun navigateToNextPage(isUnder16: Boolean) { + if (isUnder16) { navigateTo(PersonalDetailsFragmentDirections.actionPersonalDetailsToUnderSixteenFragment().actionId) } else { val bundle = bundleOf( @@ -111,72 +206,4 @@ class PersonalDetailsFragment : PagerChildFragment() { navigateTo(PersonalDetailsFragmentDirections.actionPersonalDetailsToEnterNumberFragment().actionId, bundle) } } - - fun showPostcodeError() { - personal_details_post_code_error.visibility = VISIBLE - } - - fun hidePostcodeError() { - personal_details_post_code_error.visibility = GONE - } - - fun showNameError() { - personal_details_name_error.visibility = VISIBLE - } - - fun hideNameError() { - personal_details_name_error.visibility = GONE - } - - fun showAgeError() { - personal_details_age_error.visibility = VISIBLE - } - - fun hideAgeError() { - personal_details_age_error.visibility = GONE - } - - private fun showAgePicker() { - activity?.let { activity -> - val ages = resources.getStringArray(R.array.personal_details_age_array).map { - it.split(":").let { it[0] to it[1] } - } - var selected = ages.firstOrNull { it == ageSelected }?.let { - ages.indexOf(it) - } ?: 0 - - picker = NumberPicker(activity) - picker?.minValue = 0 - picker?.maxValue = ages.size - 1 - picker?.displayedValues = ages.map { it.second }.toTypedArray() - picker?.setOnValueChangedListener { _, _, newVal -> - selected = newVal - } - picker?.value = selected - alertDialog?.dismiss() - alertDialog = AlertDialog.Builder(activity) - .setTitle(R.string.personal_details_age_dialog_title) - .setView(picker) - .setPositiveButton(R.string.personal_details_dialog_ok) { _, _ -> - ageSelected = ages[selected] - personal_details_age.text = ages[selected].second - hideAgeError() - updateButtonState() - } - .setNegativeButton(android.R.string.no, null) - .show() - } - } - - private fun getMidAgeToSend(): String? { - val ages = resources.getStringArray(R.array.personal_details_age_array).map { - it.split(":").let { it[0] to it[1] } - } - val selected = ages.firstOrNull { it == ageSelected }?.let { - ages.indexOf(it) - } - return selected?.let { - ages[selected].first - } - } } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt deleted file mode 100644 index 7c60df8..0000000 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/personal/PersonalDetailsPresenter.kt +++ /dev/null @@ -1,96 +0,0 @@ -package au.gov.health.covidsafe.ui.onboarding.fragment.personal - -import au.gov.health.covidsafe.Preference -import java.util.regex.Pattern - -class PersonalDetailsPresenter(private val personalDetailsFragment: PersonalDetailsFragment) { - - private val TAG = this.javaClass.simpleName - - private val POST_CODE_REGEX = Pattern.compile("^(?:(?:[2-8]\\d|9[0-7]|0?[28]|0?9(?=09))(?:\\d{2}))$") - - fun saveInfos(name: String?, postCode: String?, age: String?) { - personalDetailsFragment.showLoading() - personalDetailsFragment.context?.let { context -> - val ageInt = age?.toIntOrNull() - val nameValid = name.isNullOrBlank().not() - val postCodeValid = postCode.isNullOrBlank().not() && isPostCodeValid(postCode) - val ageValid = age.isNullOrBlank().not() - - if (nameValid && postCodeValid && ageValid) { - val valid = (name?.let { name -> - Preference.putName(context, name) - } ?: false) && - (age?.let { age -> - Preference.putAge(context, age) - } ?: false) && - (postCode?.let { postCode -> - Preference.putPostCode(context, postCode) - } ?: false) - - if (valid) { - personalDetailsFragment.hideLoading() - personalDetailsFragment.navigateToNextPage(ageInt?.let { it < 16 } ?: false) - } else { - personalDetailsFragment.hideLoading() - personalDetailsFragment.showGenericError() - } - } else { - showFieldsError(name, postCode, age) - personalDetailsFragment.hideLoading() - } - } ?: run { - personalDetailsFragment.hideLoading() - personalDetailsFragment.showGenericError() - } - } - - private fun showFieldsError(name: String?, postCode: String?, age: String?) { - updateNameFieldError(name) - updateAgeFieldError(age) - updatePostcodeFieldError(postCode) - } - - private fun updateAgeFieldError(age: String?) { - return if (age.isNullOrBlank()) { - personalDetailsFragment.showAgeError() - } else { - personalDetailsFragment.hideAgeError() - } - } - - private fun updateNameFieldError(name: String?) { - return if (name.isNullOrBlank()) { - personalDetailsFragment.showNameError() - } else { - personalDetailsFragment.hideNameError() - } - } - - private fun updatePostcodeFieldError(postCode: String?) { - return if (postCode.isNullOrBlank()) { - personalDetailsFragment.showPostcodeError() - } else { - personalDetailsFragment.hidePostcodeError() - } - } - - fun validateInputsForButtonUpdate(name: String?, postCode: String?, age: String?): Boolean { - val nameValid = name.isNullOrBlank().not() - val postCodeValid = postCode.isNullOrBlank().not() && isPostCodeValid(postCode) - val ageValid = age.isNullOrBlank().not() - - return nameValid && postCodeValid && ageValid - } - - internal fun validateInlinePostCode(postCode: String?) { - if (!postCode.isNullOrEmpty() && postCode.length == 4 && !isPostCodeValid(postCode)) { - personalDetailsFragment.showPostcodeError() - } else { - personalDetailsFragment.hidePostcodeError() - } - } - - private fun isPostCodeValid(postCode: String?) = POST_CODE_REGEX.matcher(postCode.toString()).matches() - -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_microphone.xml b/app/src/main/res/drawable/ic_microphone.xml new file mode 100644 index 0000000..a61895b --- /dev/null +++ b/app/src/main/res/drawable/ic_microphone.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_right.xml b/app/src/main/res/drawable/ic_right.xml new file mode 100644 index 0000000..b5290b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_right.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..e74075f --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_country_code_selection.xml b/app/src/main/res/layout/activity_country_code_selection.xml new file mode 100644 index 0000000..4aa960b --- /dev/null +++ b/app/src/main/res/layout/activity_country_code_selection.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_enter_number.xml b/app/src/main/res/layout/fragment_enter_number.xml index f4506fa..a18e215 100644 --- a/app/src/main/res/layout/fragment_enter_number.xml +++ b/app/src/main/res/layout/fragment_enter_number.xml @@ -12,41 +12,97 @@ android:layout_height="wrap_content"> + app:layout_constraintTop_toBottomOf="@+id/enter_number_page_headline" /> + + + + + + + + + + + + - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/enter_number_headline" /> - - + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@+id/invalid_phone_number" + tools:text="@string/enter_number_content" /> diff --git a/app/src/main/res/layout/fragment_personal_details.xml b/app/src/main/res/layout/fragment_personal_details.xml index d0271ec..8ba854c 100644 --- a/app/src/main/res/layout/fragment_personal_details.xml +++ b/app/src/main/res/layout/fragment_personal_details.xml @@ -11,13 +11,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingStart="@dimen/keyline_5" - android:paddingEnd="@dimen/keyline_5"> + android:paddingEnd="@dimen/keyline_5" + android:paddingBottom="@dimen/keyline_9"> - + + - + android:gravity="start" + android:paddingTop="@dimen/keyline_0" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/permission_title" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/permission_icon" + tools:visibility="visible" + tools:text="Allow push notification" /> diff --git a/app/src/main/res/layout/view_list_item_country.xml b/app/src/main/res/layout/view_list_item_country.xml new file mode 100644 index 0000000..67325cf --- /dev/null +++ b/app/src/main/res/layout/view_list_item_country.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_list_item_country_initial_letter.xml b/app/src/main/res/layout/view_list_item_country_initial_letter.xml new file mode 100644 index 0000000..5103569 --- /dev/null +++ b/app/src/main/res/layout/view_list_item_country_initial_letter.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_list_item_group_title.xml b/app/src/main/res/layout/view_list_item_group_title.xml new file mode 100644 index 0000000..6642c14 --- /dev/null +++ b/app/src/main/res/layout/view_list_item_group_title.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml deleted file mode 100644 index 2f4fa11..0000000 --- a/app/src/main/res/values/integers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 9 - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef4c638..9d6370a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,16 +61,16 @@ Heading, Enter your details Full name Enter full name + Please enter your full name. Age range (select) Select age range - Postcode + Please select your age range. + Postcode in Australia e.g. 2000 Enter postcode + Your Australian postcode number must contain 4 digits. privacy policy Postcode - Invalid postcode - Age cannot be empty - Invalid name Select your age Select @@ -98,13 +98,258 @@ + Select country or region Enter your mobile number + For example: Invalid phone number. - +61 + Australian mobile numbers contain a maximum of 10 digits. + Mobile numbers in Norfolk Island contain 5 to 6 digits. + Mobile numbers in %1s contain %2s digits. Use an Australian phone number. We’ll send you a six-digit PIN to verify your mobile number. Trying to register on behalf of a friend or relative?\n\nThey will need to register using their own device and phone number so that COVIDSafe can work for them. Get PIN + Search + Options for Australia + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z + + + Afghanistan + Albania + Algeria + Andorra + Angola + Anguilla + Antigua and Barbuda + Argentina + Armenia + Aruba + Australia + Austria + Azerbaijan + Bahamas + Bahrain + Bangladesh + Barbados + Belarus + Belgium + Belize + Benin + Bermuda + Bhutan + Bolivia + Bosnia and Herzegovina + Botswana + Brazil + Brunei + Bulgaria + Burkina Faso + Burundi + Cambodia + Cameroon + Canada + Cape Verde + Cayman Islands + Central African Republic + Chad + Chile + China + Colombia + Comoros + Cook Islands + Costa Rica + Croatia + Cuba + Curaçao + Cyprus + Czech Republic + Democratic Republic of the Congo + Denmark + Djibouti + Dominica + Dominican Republic + Ecuador + Egypt + El Salvador + Equatorial Guinea + Estonia + Ethiopia + Faroe Islands + Fiji + Finland + France + French Guiana + Gabon + Gambia + Georgia + Germany + Ghana + Gibraltar + Greece + Greenland + Grenada + Guadeloupe + Guam + Guatemala + Guinea + Guinea-Bissau + Guyana + Haiti + Honduras + Hong Kong + Hungary + Iceland + India + Indonesia + Iran + Iraq + Ireland + Israel + Italy + Ivory Coast + Jamaica + Japan + Jordan + Kazakhstan + Kenya + Kiribati + Kuwait + Kyrgyzstan + Laos + Latvia + Lebanon + Lesotho + Liberia + Libya + Liechtenstein + Lithuania + Luxembourg + Macau + North Macedonia + Madagascar + Malawi + Malaysia + Maldives + Mali + Malta + Martinique + Mauritania + Mauritius + Mexico + Moldova + Monaco + Mongolia + Montenegro + Montserrat + Morocco + Mozambique + Myanmar + Namibia + Nepal + Netherlands + Netherlands Antilles + New Caledonia + New Zealand + Nicaragua + Niger + Norfolk Island + Nigeria + Norway + Oman + Pakistan + Palau + Palestinian Territories + Panama + Papua New Guinea + Paraguay + Peru + Philippines + Poland + Portugal + Puerto Rico + Qatar + Republic of the Congo + Reunion Island + Romania + Russia + Rwanda + Saint Kitts and Nevis + Saint Lucia + Saint Vincent and the Grenadines + Samoa + Sao Tome and Principe + Saudi Arabia + Senegal + Serbia + Seychelles + Sierra Leone + Singapore + Slovakia + Slovenia + Solomon Islands + Somalia + South Africa + South Korea + South Sudan + Spain + Sri Lanka + Sudan + Suriname + Swaziland + Sweden + Switzerland + Taiwan + Tajikistan + Tanzania + Thailand + Timor-Leste + Togo + Tonga + Trinidad and Tobago + Tunisia + Turkey + Turkmenistan + Turks and Caicos Islands + Uganda + Ukraine + United Arab Emirates + United Kingdom + United States + Uruguay + Uzbekistan + Vanuatu + Venezuela + Vietnam + British Virgin Islands + Virgin Islands, US + Yemen + Zambia + Zimbabwe Enter the PIN sent to %s %s @@ -116,8 +361,8 @@ Verify - App permissions - COVIDSafe needs Bluetooth® and notifications enabled to work.\n\nSelect ‘Proceed’ to:\n\n1. Enable Bluetooth®\n\n2. Allow Location permissions\n\n3. Disable Battery optimisation\n\n\nAndroid needs Location Permissions for Bluetooth® to work.\n\nCOVIDSafe does not send pairing requests. + App settings + COVIDSafe needs Bluetooth® and notifications enabled to work.\n\nSelect ‘Proceed’ to:\n\n1. Enable Bluetooth®\n\n2. Allow Location permissions\n\n3. Disable Battery optimisation\n\n\nAndroid needs Location permissions for Bluetooth® to work.\n\nCOVIDSafe does not send pairing requests. Proceed Android requires location access to enable Bluetooth® functions for COVIDSafe. COVIDSafe cannot work properly without it @@ -143,25 +388,32 @@ Keep push notifications on for COVIDSafe so we can notify you quickly if the app isn\'t working properly. Continue + + COVIDSafe is active + COVIDSafe is not active + Keep COVIDSafe active when you leave home or are in public places. + Make sure COVIDSafe is active before you leave home or when in public places. + COVIDSafe is active. No further action required. COVIDSafe is not active. - Check your permissions. + Check your settings. Your information was uploaded on %s. COVIDSafe does not send pairing requests. Bluetooth®: %s Battery optimization: %s Location: %s - Push Notification: %s + Push notification: %s On Off Check\npermissions COVIDSafe needs permission to access these features. Help - Check your permissions - COVIDSafe won\'t work without these permissions. + Check your settings + COVIDSafe won\'t work without the right settings. + Allow COVIDSafe to push notifications. Has a health official asked you to upload your data? Register for self isolation Help stop the spread of COVID-19 and track your symptoms. @@ -197,7 +449,7 @@ Heading, Is a health official asking you to upload your information? Only if you test positive to COVID-19 will a state or territory health official contact you to assist with voluntary upload of your information.\n\nOnce you press ‘Yes’ you’ll need to provide consent to upload your information. - Upload consent + Upload consent Heading, Upload consent Unless you consent, your close contact information will not be uploaded.\n\nIf you consent, your close contact information will be uploaded and shared with state or territory health officials for contact tracing purposes.\n\nRead the COVIDSafe privacy policy for further details. Upload your information @@ -224,4 +476,5 @@ Thank you! You have helped to stop the spread of COVID-19! You’ve kept others safe while helping to stop the spread of COVID-19 during self-isolation. Continue + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f51b8c2..2262fd5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -32,4 +32,12 @@ @font/font_roboto_regular + + diff --git a/build.gradle b/build.gradle index c298c73..bd0e2f0 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { maven { url "https://jitpack.io" } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:4.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -25,6 +25,7 @@ allprojects { google() jcenter() maven { url 'https://jitpack.io' } + maven { url "https://dl.bintray.com/mikefot/maven/" } } } diff --git a/security.txt b/security.txt index 8056377..10e754e 100644 --- a/security.txt +++ b/security.txt @@ -1,17 +1,20 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 -Contact: mailto:support@covidsafe.gov.au +Contact: mailto:security@covidsafe.gov.au Canonical: https://www.covidsafe.gov.au/.well-known/security.txt Encryption: https://www.covidsafe.gov.au/.well-known/pgp-key.txt -----BEGIN PGP SIGNATURE----- -iQFNBAEBCAA3FiEEbUgetBuPAas8w7zHDyQUNNekxBkFAl6xF6AZHHN1cHBvcnRA -Y292aWRzYWZlLmdvdi5hdQAKCRAPJBQ016TEGd+bCACLrYjCbKRsTsQQyZVVtGxj -wYKW2AWclnKZWX/sxnTexg6D1tlGbZbB0OJpw0gJ0NpMoOLFd0kRZXOzv8RocIdx -xd90Nwwl3NQ2ygGCDXR0Y7uRKX/P/Y1xO7XkyiYXAqVq3YWvI9M04pY/TCRvRZ/1 -qBs/WDHv/6eRh2qNy/WGXD66CmTLHBcXilTeihcTZ/27Mny5SPthdfy8odQnhUja -NfFxDm+8gQuFKUUQmr9rd8FEMPSl6BWf/kQtn0YmTeZRzD01uT1ydeHkyPSgn+nq -k9us35AlkI7aZNfNkFVWJ2v5ZVAdTHDh3pgBRZETwVg1of5DEXhc5XJV6mLsu9bM -=tik2 +iQHOBAEBCAA4FiEEUWgkqhSOHRHwGue2IZjqt5flXH4FAl7Hkw0aHHNlY3VyaXR5 +QGNvdmlkc2FmZS5nb3YuYXUACgkQIZjqt5flXH4KfQv/ZagXzgn9HspSTupO7zQQ +t7lpZsNWKJsXddUD3+JQQ8b1uB9kmHhiydIXervEc1yGti6YYZxKybehLdvv7HjV +Cps9kn3cns9ex3s+KEMMFrU7MykvZ2x+fvCoGfgzWm1UJElxn/cSbOnt38VxmSjC +wkmh+DGa1z9OB6ZMuivO2XBx+Y0GV5tsoxWKRtXMruQ41bTHZvIJ9lJctYTv2xEK +hYlHmrzkZjLM/sTwGKWj+ARCn1IA0Zp8uxkoBi3/++NonKAlQpSZ4Rk6B780H8GM +5lCZVLIRfH1VJzPr7+eSSBF8p0coEapyO7bk/ioBIQB5v4xfvpJRBEc+eI7IZeDa +TcO/TVZw7Vut89flR+34g9PCSFDCJgv5zR8vE5mB5m/vlfJ7XI99XMwvFNWgpcyL +72tFv74uH6tUPZOlbHGZTnBsQLkuj4fwUYaUPcR9TzeMlBHoe21bQ6OeDO02rIy4 +09bWSTMGe33LvTz2UMuEkZcsNT8vc7J+597GSxgDWoR6 +=6MOG -----END PGP SIGNATURE-----