diff --git a/app/build.gradle b/app/build.gradle index fe90bb9..343a905 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,8 +41,8 @@ android { Before you increase the targetSdkVersion make sure that all its usage are still working */ targetSdkVersion 28 - versionCode 48 - versionName "1.0.48" + versionCode 54 + versionName "1.0.54" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -210,7 +210,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'pub.devrel:easypermissions:3.0.0' implementation 'com.google.code.gson:gson:2.8.6' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' @@ -241,7 +241,7 @@ dependencies { implementation 'com.google.guava:guava:28.2-android' implementation "androidx.collection:collection:1.1.0" - implementation "com.google.crypto.tink:tink-android:1.4.0-rc2" + implementation "com.google.crypto.tink:tink-android:1.4.0" implementation "androidx.lifecycle:lifecycle-service:2.2.0" implementation 'com.github.razir.progressbutton:progressbutton:2.0.1' @@ -249,7 +249,7 @@ dependencies { implementation 'com.michaelfotiadis:android-country-flags:1.0.3' // Firebase Cloud Messaging - implementation 'com.google.firebase:firebase-messaging:20.2.3' + implementation 'com.google.firebase:firebase-messaging:20.2.4' androidTestImplementation "androidx.room:room-testing:2.2.5" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d20850c..724713b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,6 +48,8 @@ android:windowSoftInputMode="adjustPan" /> + + Unit) { + GlobalScope.launch(Dispatchers.IO) { + var reachable = false + + reachable = try { + InetAddress.getByName(url).isReachable(2000) + } catch (e: Exception) { + CentralLog.w(TAG, "checkInternetConnectionToURL() failed to reach the url $url") + false + } + + CentralLog.w(TAG, "checkInternetConnectionToURL() reachability to url $url is $reachable") + + callback(reachable) + } + } + + fun checkInternetConnectionToCOVIDSafeBackend(callback: (Boolean) -> Unit) { + checkInternetConnectionToURL(BuildConfig.BASE_URL, callback) + } + + fun checkInternetConnectionToGoogle(callback: (Boolean) -> Unit) { + checkInternetConnectionToURL("8.8.8.8", callback) + } + } 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 4f529d6..297d0ce 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 @@ -109,10 +109,6 @@ fun Context.isBlueToothEnabled(): Boolean? { return bluetoothManager?.adapter?.isEnabled } -fun Context.isPushNotificationEnabled(): Boolean? { - return NotificationManagerCompat.from(this).areNotificationsEnabled() -} - fun Context.isLocationPermissionAllowed(): Boolean? { return EasyPermissions.hasPermissions(this, ACCESS_COARSE_LOCATION) } 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 10da2ba..e6b4a76 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 @@ -31,6 +31,8 @@ private const val HELP_TOPICS_VIETNAMESE_PAGE = "/vi" private const val HELP_TOPICS_KOREAN_PAGE = "/ko" private const val HELP_TOPICS_GREEK_PAGE = "/el" private const val HELP_TOPICS_ITALIAN_PAGE = "/it" +private const val HELP_TOPICS_PUNJABI_PAGE = "/pa-in" +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" @@ -55,6 +57,8 @@ object LinkBuilder { localeLanguageTag.startsWith("ko") -> HELP_TOPICS_KOREAN_PAGE localeLanguageTag.startsWith("el") -> HELP_TOPICS_GREEK_PAGE localeLanguageTag.startsWith("it") -> HELP_TOPICS_ITALIAN_PAGE + localeLanguageTag.startsWith("pa") -> HELP_TOPICS_PUNJABI_PAGE + localeLanguageTag.startsWith("tr") -> HELP_TOPICS_TURKISH_PAGE else -> HELP_TOPICS_ENGLISH_PAGE } + HELP_TOPICS_SUFFIX diff --git a/app/src/main/java/au/gov/health/covidsafe/networking/response/MessagesResponse.kt b/app/src/main/java/au/gov/health/covidsafe/networking/response/MessagesResponse.kt index 13416cf..b530394 100644 --- a/app/src/main/java/au/gov/health/covidsafe/networking/response/MessagesResponse.kt +++ b/app/src/main/java/au/gov/health/covidsafe/networking/response/MessagesResponse.kt @@ -3,4 +3,7 @@ package au.gov.health.covidsafe.networking.response import androidx.annotation.Keep @Keep -data class MessagesResponse(val message: String, val forceappupgrade: Boolean) \ No newline at end of file +data class MessagesResponse(val messages: List?) + +@Keep +data class Message(val title: String, val body: String, val destination: String) \ 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 46b74ef..8a5fefe 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 @@ -40,7 +40,7 @@ interface AwsClient { @Query("appversion") appversion: String, @Query("token") token: String, @Query("healthcheck") healthcheck: String, - @Query("preferredLanguages") preferredLanguages: String + @Query("preferredlanguages") preferredLanguages: String ): Call } \ No newline at end of file diff --git a/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt b/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt index b27b979..3727758 100644 --- a/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt +++ b/app/src/main/java/au/gov/health/covidsafe/scheduler/GetMessagesScheduler.kt @@ -76,24 +76,30 @@ class GetMessagesJobSchedulerService : JobService() { object GetMessagesScheduler { var mostRecentRecordTimestamp: Long = 0 - fun scheduleGetMessagesJob() { - CentralLog.d(TAG, "scheduleGetMessagesJob()") + var messagesResponseCallback: ((MessagesResponse) -> Unit)? = null - val context = TracerApp.AppContext + fun scheduleGetMessagesJob(callback: (MessagesResponse) -> Unit) { + GlobalScope.launch(Dispatchers.Default) { + CentralLog.d(TAG, "scheduleGetMessagesJob()") - (context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler?) - ?.let { - CentralLog.d(TAG, "JobScheduler available") + messagesResponseCallback = callback - it.schedule( - JobInfo.Builder(GET_MESSAGES_JOB_ID, - ComponentName(context, GetMessagesJobSchedulerService::class.java)) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) - .setPersisted(true) - .setPeriodic(TWENTY_FOUR_HOURS_IN_MILLIS) - .build() - ) - } + val context = TracerApp.AppContext + + (context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler?) + ?.let { + CentralLog.d(TAG, "JobScheduler available") + + it.schedule( + JobInfo.Builder(GET_MESSAGES_JOB_ID, + ComponentName(context, GetMessagesJobSchedulerService::class.java)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setPersisted(true) + .setPeriodic(TWENTY_FOUR_HOURS_IN_MILLIS) + .build() + ) + } + } } fun getMessages(jobService: JobService? = null, params: JobParameters? = null) { @@ -147,11 +153,14 @@ object GetMessagesScheduler { messagesCall.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val responseCode = response.code() + if (responseCode == 200) { CentralLog.d(TAG, "onResponse() got 200 response.") response.body()?.let { CentralLog.d(TAG, "onResponse() MessagesResponse = $it") + + messagesResponseCallback?.invoke(it) } } else { CentralLog.w(TAG, "onResponse() got error response code = $responseCode.") diff --git a/app/src/main/java/au/gov/health/covidsafe/security/crypto/EncryptedSharedPreferences.java b/app/src/main/java/au/gov/health/covidsafe/security/crypto/EncryptedSharedPreferences.java index 9afe84c..3106bd5 100644 --- a/app/src/main/java/au/gov/health/covidsafe/security/crypto/EncryptedSharedPreferences.java +++ b/app/src/main/java/au/gov/health/covidsafe/security/crypto/EncryptedSharedPreferences.java @@ -313,6 +313,7 @@ public final class EncryptedSharedPreferences implements SharedPreferences { clearKeysIfNeeded(); mEditor.apply(); notifyListeners(); + mKeysChanged.clear(); } private void clearKeysIfNeeded() { 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 ac41801..a34dd9f 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 @@ -1,13 +1,10 @@ package au.gov.health.covidsafe.streetpass.persistence +import android.os.Build import android.util.Base64 import au.gov.health.covidsafe.BuildConfig import java.math.BigInteger -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.MessageDigest -import java.security.PublicKey +import java.security.* import java.security.interfaces.ECPublicKey import java.security.spec.X509EncodedKeySpec import javax.crypto.Cipher @@ -98,6 +95,11 @@ object Encryption { private val NONCE_PADDING = ByteArray(14) { 0x0E.toByte() } private val serverPubKey: PublicKey = readKey() + private val random: SecureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + SecureRandom.getInstanceStrong() + } else { + SecureRandom() + } private var cachedEphPubKey: ByteArray? = null private var cachedAesKey: SecretKey? = null @@ -146,13 +148,21 @@ object Encryption { @Synchronized private fun encryptionKeys(): EncryptionKeys { - if (keyGenTime <= System.currentTimeMillis() - KEY_GEN_TIME_DELTA || counter >= 65535) { + // Allowing 2^8 encryption cycles with a 2B random IV gives a ~40% chance of IV collision. + // This threshold has been chosen as a balance between the competing needs of: + // - Reducing the number of key agreements, due to mobile platform limitations (battery, performance, background compute restrictions) + // - Ensuring semantic security + if (keyGenTime <= System.currentTimeMillis() - KEY_GEN_TIME_DELTA || counter >= 255) { generateKeys() keyGenTime = System.currentTimeMillis() counter = 0 } else { counter++ } - return EncryptionKeys(cachedEphPubKey!!, cachedAesKey!!, cachedMacKey!!, counterBytes(counter)) + + val nonce: ByteArray = ByteArray(2) + random.nextBytes(nonce) + + return EncryptionKeys(cachedEphPubKey!!, cachedAesKey!!, cachedMacKey!!, nonce) } } \ 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 8b33382..9adf415 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 @@ -5,7 +5,9 @@ import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.content.* import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings import android.text.Html import android.text.method.LinkMovementMethod import android.view.LayoutInflater @@ -14,33 +16,42 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent +import android.widget.LinearLayout +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import androidx.core.view.children import androidx.navigation.fragment.findNavController -import au.gov.health.covidsafe.BuildConfig -import au.gov.health.covidsafe.Preference -import au.gov.health.covidsafe.R -import au.gov.health.covidsafe.WebViewActivity +import au.gov.health.covidsafe.* import au.gov.health.covidsafe.extensions.* import au.gov.health.covidsafe.links.LinkBuilder +import au.gov.health.covidsafe.logging.CentralLog +import au.gov.health.covidsafe.networking.response.MessagesResponse import au.gov.health.covidsafe.talkback.setHeading import au.gov.health.covidsafe.ui.BaseFragment +import au.gov.health.covidsafe.ui.home.view.ExternalLinkCard import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home_external_links.* import kotlinx.android.synthetic.main.fragment_home_setup_complete_header.* import kotlinx.android.synthetic.main.fragment_home_setup_incomplete_content.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import pub.devrel.easypermissions.AppSettingsDialog import pub.devrel.easypermissions.EasyPermissions import java.lang.ref.WeakReference import java.text.SimpleDateFormat import java.util.* + +private const val TAG = "HomeFragment" + private const val ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000L private const val FOURTEEN_DAYS_IN_MILLIS = 14 * ONE_DAY_IN_MILLIS class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { - companion object{ + companion object { var instanceWeakRef: WeakReference? = null } @@ -106,22 +117,12 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { instanceWeakRef = WeakReference(this) - // display app update reminder -// if (System.currentTimeMillis() -// - -// Preference.getAppUpdateReminderDismissedTime(requireContext()) -// > ONE_DAY_IN_MILLIS -// ) { -// app_update_reminder.visibility = VISIBLE -// } - // disable the app update reminder for now app_update_reminder.visibility = GONE bluetooth_card_view.setOnClickListener { requestBlueToothPermissionThenNextPermission() } location_card_view.setOnClickListener { askForLocationPermission() } battery_card_view.setOnClickListener { excludeFromBatteryOptimization() } - push_card_view.setOnClickListener { gotoPushNotificationSettings() } home_been_tested_button.setOnClickListener { navigateTo(R.id.action_home_to_selfIsolate) @@ -146,15 +147,6 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToHelpFragment()) } -// go_to_play_store.setOnClickListener { -// app_update_reminder.visibility = GONE -// } -// -// remind_me_later.setOnClickListener { -// Preference.putAppUpdateReminderDismissedTime(requireContext()) -// app_update_reminder.visibility = GONE -// } - if (!mIsBroadcastListenerRegistered) { registerBroadcast() } @@ -162,6 +154,8 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { home_header_no_bluetooth_pairing.text = LinkBuilder.getNoBluetoothPairingContent(requireContext()) home_header_no_bluetooth_pairing.movementMethod = LinkMovementMethod.getInstance() + + updateNotificationStatusTile() } override fun onPause() { @@ -177,6 +171,7 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { home_setup_complete_news.setOnClickListener(null) home_setup_complete_app.setOnClickListener(null) help_topics_link.setOnClickListener(null) + activity?.let { activity -> if (mIsBroadcastListenerRegistered) { activity.unregisterReceiver(mBroadcastListener) @@ -272,11 +267,10 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { layoutParams.height = size layoutParams.width = size } - home_header_picture_setup_complete.setImageResource(R.drawable.ic_red_cross) + home_header_picture_setup_complete.setImageResource(R.drawable.ic_red_cross_no_circle) content_setup_incomplete_group.visibility = VISIBLE updateBlueToothStatus() - updatePushNotificationStatus() updateBatteryOptimizationStatus() updateLocationStatus() ContextCompat.getColor(it, R.color.grey).let { bgColor -> @@ -293,12 +287,10 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { val context = requireContext() val bluetoothEnabled = context.isBlueToothEnabled() ?: false - val pushNotificationEnabled = context.isPushNotificationEnabled() ?: true val nonBatteryOptimizationAllowed = context.isBatteryOptimizationDisabled() ?: true val locationStatusAllowed = context.isLocationPermissionAllowed() ?: true return bluetoothEnabled && - pushNotificationEnabled && nonBatteryOptimizationAllowed && locationStatusAllowed && context.isLocationEnabledOnDevice() @@ -332,19 +324,6 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { } } - private fun updatePushNotificationStatus() { - requireContext().isPushNotificationEnabled()?.let { - push_card_view.visibility = VISIBLE - push_card_view.render( - formatPushNotificationTitle(it), - it, - getString(R.string.home_app_permission_push_notification_prompt) - ) - } ?: run { - push_card_view.visibility = GONE - } - } - private fun updateBatteryOptimizationStatus() { requireContext().isBatteryOptimizationDisabled()?.let { battery_card_view.visibility = VISIBLE @@ -361,9 +340,10 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { private fun updateLocationStatus() { requireContext().isLocationPermissionAllowed()?.let { val locationWorking = it && requireContext().isLocationEnabledOnDevice() + val locationOffPrompts = getString(R.string.home_set_location_why) location_card_view.visibility = VISIBLE - location_card_view.render(formatLocationTitle(locationWorking), locationWorking) + location_card_view.render(formatLocationTitle(locationWorking), locationWorking, locationOffPrompts) } ?: run { location_card_view.visibility = GONE } @@ -438,4 +418,111 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) } + fun updateConnectionTile(isInternetConnected: Boolean) { + // called on IO thread; run the UI logic on UI thread + GlobalScope.launch(Dispatchers.Main) { + CentralLog.d(TAG, "updateConnectionTile() isInternetConnected = $isInternetConnected") + + var visibility = if (isInternetConnected) GONE else VISIBLE + + // don't display the tile when there's permission not enabled + if (!allPermissionsEnabled()) { + visibility = GONE + } + + improve_performance_card.visibility = visibility + internet_connection_tile.visibility = visibility + + if (visibility == VISIBLE) { + internet_connection_tile.setOnClickListener { + // startActivity(Intent(ACTION_DATA_ROAMING_SETTINGS)) + // startActivity(Intent(ACTION_WIFI_SETTINGS)) + + startActivity(Intent(requireContext(), InternetConnectionIssuesActivity::class.java)) + } + } + } + } + + fun updateMessageTiles(messagesResponse: MessagesResponse) { + GlobalScope.launch(Dispatchers.Main) { + improve_performance_card_linear_layout.children.forEach { + if (it != internet_connection_tile && it != improve_performance_title) { + improve_performance_card_linear_layout.removeView(it) + } + } + + if (!messagesResponse.messages.isNullOrEmpty()) { + improve_performance_card.visibility = VISIBLE + + messagesResponse.messages.forEach { message -> + ExternalLinkCard(requireContext(), null, 0).also { + it.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).also { layoutParams -> + layoutParams.setMargins(0, resources.getDimensionPixelSize(R.dimen.divider_height), 0, 0) + } + + it.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.white)) + + it.setMessage(message) + it.setErrorTextColor() + + improve_performance_card_linear_layout.addView(it) + } + } + } else { + improve_performance_card.visibility = GONE + } + } + } + + private fun updateNotificationStatusTile() { + var title = "" + var body = "" + + if (NotificationManagerCompat.from(requireContext()).areNotificationsEnabled() + ) { + notification_status_link.setTopRightIcon(R.drawable.ic_green_tick) + title = getString(R.string.home_set_complete_external_link_notifications_title_iOS) + body = getString(R.string.NotificationsEnabledBlurb) + } else { + notification_status_link.setTopRightIcon(R.drawable.ic_red_cross) + title = getString(R.string.home_set_complete_external_link_notifications_title_iOS_off) + body = getString(R.string.NotificationsEnabledBlurb) + } + + notification_status_link.setTitleBodyAndClickCallback(title, body) { + openAppNotificationSettings() + } + + notification_status_link.setColorForContentWithAction() + } + + private fun openAppNotificationSettings() { + val context = requireContext() + + val intent = Intent().apply { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { + action = "android.settings.APP_NOTIFICATION_SETTINGS" + putExtra("app_package", context.packageName) + putExtra("app_uid", context.applicationInfo.uid) + } + else -> { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addCategory(Intent.CATEGORY_DEFAULT) + data = Uri.parse("package:" + context.packageName) + } + } + } + + context.startActivity(intent) + } + } diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt index 1cf2f83..5bda36f 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/home/view/ExternalLinkCard.kt @@ -1,15 +1,20 @@ package au.gov.health.covidsafe.ui.home.view import android.content.Context -import android.content.res.Resources +import android.content.Intent import android.content.res.TypedArray +import android.net.Uri +import android.text.Html import android.util.AttributeSet import android.view.LayoutInflater +import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import au.gov.health.covidsafe.R +import au.gov.health.covidsafe.networking.response.Message import kotlinx.android.synthetic.main.view_card_external_link_card.view.* + class ExternalLinkCard @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -19,24 +24,99 @@ class ExternalLinkCard @JvmOverloads constructor( init { LayoutInflater.from(context).inflate(R.layout.view_card_external_link_card, this, true) - val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ExternalLinkCard) - val icon = a.getDrawable(R.styleable.ExternalLinkCard_external_linkCard_icon) - val title = a.getString(R.styleable.ExternalLinkCard_external_linkCard_title) - val content = a.getString(R.styleable.ExternalLinkCard_external_linkCard_content) - val padding = a.getDimension(R.styleable.ExternalLinkCard_external_linkCard_icon_padding, 0f).toInt() - val iconBackground = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_icon_background, R.color.transparent) + attrs?.let { + val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ExternalLinkCard) + val icon = a.getDrawable(R.styleable.ExternalLinkCard_external_linkCard_icon) + val iconVisible = a.getBoolean(R.styleable.ExternalLinkCard_external_linkCard_icon_visible, true) + val title = a.getString(R.styleable.ExternalLinkCard_external_linkCard_title) + val content = a.getString(R.styleable.ExternalLinkCard_external_linkCard_content) + val padding = a.getDimension(R.styleable.ExternalLinkCard_external_linkCard_icon_padding, 0f).toInt() + val iconBackground = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_icon_background, R.color.transparent) + val textColorResId = a.getResourceId(R.styleable.ExternalLinkCard_external_linkCard_text_color, R.color.slack_black) + val textColor = ContextCompat.getColor(context, textColorResId) - external_link_round_image.setImageDrawable(icon) - external_link_round_image.setBackgroundResource(iconBackground) - external_link_round_image.setPadding(padding, padding, padding, padding) - external_link_headline.text = title - external_link_content.text = content - a.recycle() + external_link_round_image.setImageDrawable(icon) + external_link_round_image.visibility = if (iconVisible) View.VISIBLE else View.GONE + external_link_round_image.setBackgroundResource(iconBackground) + external_link_round_image.setPadding(padding, padding, padding, padding) + external_link_headline.text = title + external_link_content.text = content + + setTextColor(textColor) + + a.recycle() + } + } + + private fun setTextColor(textColor: Int) { + external_link_headline.setTextColor(textColor) + external_link_content.setTextColor(textColor) + + val icChevron = + if (textColor == ContextCompat.getColor(context, R.color.error)) { + R.drawable.ic_chevron_right_red + } else { + R.drawable.ic_chevron_right + } next_arrow.setImageDrawable( - ContextCompat.getDrawable(context, R.drawable.ic_chevron_right).also { + ContextCompat.getDrawable(context, icChevron).also { it?.isAutoMirrored = true } ) } + + fun setMessage(message: Message) { + external_link_round_image.visibility = View.GONE + + external_link_headline.text = message.title + external_link_content.text = message.body + + this.setOnClickListener { + Intent(Intent.ACTION_VIEW, Uri.parse(message.destination)).also { + context.startActivity(it) + } + } + } + + fun setTitleBodyAndClickCallback(title: String, body: String, clickCallBack: () -> Unit) { + external_link_headline.text = title + external_link_content.text = body + + this.setOnClickListener { + clickCallBack() + } + } + + fun setErrorTextColor() { + setTextColor(ContextCompat.getColor(context, R.color.error)) + } + + fun setTopRightIcon(iconResID: Int) { + next_arrow.setImageResource(iconResID) + } + + fun setColorForContentWithAction() { + val actionColor = ContextCompat.getColor(context, R.color.dark_green) + val actionColorString = String.format("%X", actionColor).substring(2) + + val stringBuilder = StringBuilder() + + external_link_content.text.split("\n").forEachIndexed { index, s -> + if (index == 0) { + stringBuilder.append(s) + } else { + stringBuilder.append("
$s") + } + } + + val htmlText = stringBuilder.toString() + + external_link_content.text = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Html.fromHtml(htmlText, Html.FROM_HTML_MODE_COMPACT) + } else { + Html.fromHtml(htmlText) + } + } } \ 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 53c9472..7c672b1 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 @@ -78,6 +78,8 @@ class EnterNumberFragment : PagerChildFragment() { enter_number_phone_number.addTextChangedListener { enter_number_phone_number.setTextColor(normalTextColor) + + updateButtonState() } enter_number_phone_number.setOnEditorActionListener { _, actionId, event -> diff --git a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt index 3fd9a65..63c1478 100644 --- a/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt +++ b/app/src/main/java/au/gov/health/covidsafe/ui/onboarding/fragment/permission/PermissionDeviceNameFragment.kt @@ -7,6 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent +import au.gov.health.covidsafe.Preference import au.gov.health.covidsafe.R import au.gov.health.covidsafe.talkback.setHeading import au.gov.health.covidsafe.ui.PagerChildFragment @@ -27,6 +28,7 @@ class PermissionDeviceNameFragment : PagerChildFragment() { context?.let { change_device_name_content.setText(R.string.change_device_name_content_line_2) change_device_name_text_box.setText(Settings.Secure.getString(it.contentResolver, "bluetooth_name")) + Preference.setDeviceNameChangePromptDisplayed(it) } change_device_name_headline.setHeading() diff --git a/app/src/main/res/drawable-hdpi/small_green_tick.png b/app/src/main/res/drawable-hdpi/small_green_tick.png deleted file mode 100644 index a0121f8..0000000 Binary files a/app/src/main/res/drawable-hdpi/small_green_tick.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/small_red_cross.png b/app/src/main/res/drawable-hdpi/small_red_cross.png deleted file mode 100644 index 71bcefd..0000000 Binary files a/app/src/main/res/drawable-hdpi/small_red_cross.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/small_green_tick.png b/app/src/main/res/drawable-mdpi/small_green_tick.png deleted file mode 100644 index 7ebb055..0000000 Binary files a/app/src/main/res/drawable-mdpi/small_green_tick.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/small_red_cross.png b/app/src/main/res/drawable-mdpi/small_red_cross.png deleted file mode 100644 index 065929a..0000000 Binary files a/app/src/main/res/drawable-mdpi/small_red_cross.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/small_green_tick.png b/app/src/main/res/drawable-xhdpi/small_green_tick.png deleted file mode 100644 index c7d1069..0000000 Binary files a/app/src/main/res/drawable-xhdpi/small_green_tick.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/small_red_cross.png b/app/src/main/res/drawable-xhdpi/small_red_cross.png deleted file mode 100644 index 8a8b000..0000000 Binary files a/app/src/main/res/drawable-xhdpi/small_red_cross.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/small_green_tick.png b/app/src/main/res/drawable-xxhdpi/small_green_tick.png deleted file mode 100644 index bd76e1b..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/small_green_tick.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/small_red_cross.png b/app/src/main/res/drawable-xxhdpi/small_red_cross.png deleted file mode 100644 index 4db4b69..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/small_red_cross.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_bell.xml b/app/src/main/res/drawable/ic_bell.xml new file mode 100644 index 0000000..9c5460e --- /dev/null +++ b/app/src/main/res/drawable/ic_bell.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chevron_right_red.xml b/app/src/main/res/drawable/ic_chevron_right_red.xml new file mode 100644 index 0000000..a5c8d7d --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right_red.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_green_tick.xml b/app/src/main/res/drawable/ic_green_tick.xml new file mode 100644 index 0000000..3500f71 --- /dev/null +++ b/app/src/main/res/drawable/ic_green_tick.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_internet_issues.xml b/app/src/main/res/drawable/ic_internet_issues.xml new file mode 100644 index 0000000..01f441d --- /dev/null +++ b/app/src/main/res/drawable/ic_internet_issues.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_red_cross.xml b/app/src/main/res/drawable/ic_red_cross.xml index a189255..ad1318f 100644 --- a/app/src/main/res/drawable/ic_red_cross.xml +++ b/app/src/main/res/drawable/ic_red_cross.xml @@ -1,20 +1,23 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_red_cross_no_circle.xml b/app/src/main/res/drawable/ic_red_cross_no_circle.xml new file mode 100644 index 0000000..c80fa06 --- /dev/null +++ b/app/src/main/res/drawable/ic_red_cross_no_circle.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_checkbox.xml b/app/src/main/res/drawable/icon_checkbox.xml index cc6ef6a..fa2c9eb 100644 --- a/app/src/main/res/drawable/icon_checkbox.xml +++ b/app/src/main/res/drawable/icon_checkbox.xml @@ -3,25 +3,25 @@ android:exitFadeDuration="@android:integer/config_mediumAnimTime"> + android:drawable="@drawable/ic_red_cross" /> + android:drawable="@drawable/ic_green_tick" /> + android:drawable="@drawable/ic_green_tick" /> + android:drawable="@drawable/ic_red_cross" /> + android:drawable="@drawable/ic_green_tick" /> + android:drawable="@drawable/ic_green_tick" /> + android:drawable="@drawable/ic_green_tick" /> + android:drawable="@drawable/ic_green_tick" /> diff --git a/app/src/main/res/layout/activity_device_name_change_prompt.xml b/app/src/main/res/layout/activity_device_name_change_prompt.xml new file mode 100644 index 0000000..0246abf --- /dev/null +++ b/app/src/main/res/layout/activity_device_name_change_prompt.xml @@ -0,0 +1,29 @@ + + + + + + + + +