COVIDSafe code from version 1.0.18 (#2)

This commit is contained in:
COVIDSafe Support 2020-05-26 17:19:36 +10:00 committed by GitHub
parent 696e4ed498
commit 3b77cc31e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 6784 additions and 663 deletions

1
app/.gitignore vendored
View file

@ -1 +1,2 @@
/build /build
/schemas

View file

@ -35,16 +35,25 @@ android {
applicationId "au.gov.health.covidsafe" applicationId "au.gov.health.covidsafe"
resValue "string", "build_config_package", "au.gov.health.covidsafe" resValue "string", "build_config_package", "au.gov.health.covidsafe"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 29 /*
versionCode 17 TargetSdk is currently set to 28 because we are using a greylisted api in SDK 29
versionName "1.0.17" Before you increase the targetSdkVersion make sure that all its usage are still working
*/
targetSdkVersion 28
versionCode 18
versionName "1.0.18"
buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" buildConfigField "String", "GITHASH", "\"${getGitHash()}\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
arguments = ["room.schemaLocation": arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
"$projectDir/schemas".toString()] }
}
sourceSets {
androidTest {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
} }
@ -91,6 +100,7 @@ android {
buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX buildConfigField "String", "END_POINT_PREFIX", TEST_END_POINT_PREFIX
buildConfigField "String", "BASE_URL", TEST_BASE_URL buildConfigField "String", "BASE_URL", TEST_BASE_URL
buildConfigField "String", "IOS_BACKGROUND_UUID", DEBUG_BACKGROUND_IOS_SERVICE_UUID buildConfigField "String", "IOS_BACKGROUND_UUID", DEBUG_BACKGROUND_IOS_SERVICE_UUID
buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", DEBUG_ENCRYPTION_PUBLIC_KEY
String ssid = STAGING_SERVICE_UUID String ssid = STAGING_SERVICE_UUID
@ -106,6 +116,8 @@ android {
buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX
buildConfigField "String", "BASE_URL", STAGING_BASE_URL buildConfigField "String", "BASE_URL", STAGING_BASE_URL
buildConfigField "String", "IOS_BACKGROUND_UUID", STAGING_BACKGROUND_IOS_SERVICE_UUID buildConfigField "String", "IOS_BACKGROUND_UUID", STAGING_BACKGROUND_IOS_SERVICE_UUID
buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", STAGING_ENCRYPTION_PUBLIC_KEY
@ -130,6 +142,8 @@ android {
buildConfigField "String", "END_POINT_PREFIX", PRODUCTION_END_POINT_PREFIX buildConfigField "String", "END_POINT_PREFIX", PRODUCTION_END_POINT_PREFIX
buildConfigField "String", "BASE_URL", PROD_BASE_URL buildConfigField "String", "BASE_URL", PROD_BASE_URL
buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID
buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", PRODUCTION_ENCRYPTION_PUBLIC_KEY
debuggable false debuggable false
@ -150,6 +164,14 @@ android {
} }
} }
sourceSets {
staging {
java.srcDirs = ['src/debug/java']
res.srcDirs = ['src/debug/res']
manifest.srcFile 'src/debug/AndroidManifest.xml'
}
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
@ -219,4 +241,7 @@ dependencies {
implementation "androidx.security:security-crypto:1.0.0-beta01" implementation "androidx.security:security-crypto:1.0.0-beta01"
implementation "androidx.lifecycle:lifecycle-service:2.2.0" implementation "androidx.lifecycle:lifecycle-service:2.2.0"
implementation 'com.github.razir.progressbutton:progressbutton:2.0.1' implementation 'com.github.razir.progressbutton:progressbutton:2.0.1'
androidTestImplementation "androidx.room:room-testing:2.2.5"
} }

View file

@ -0,0 +1,47 @@
package au.gov.health.covidsafe.streetpass.persistence
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import java.io.IOException
/**
* This test class is used as a util to revert the actual db version and to populate it with version one record in order to test the migrations
*/
class DBUtilityTest {
private val ACTUAL_DB = "record_database"
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
StreetPassRecordDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
@Throws(IOException::class)
fun revertDbToVersion1() {
helper.createDatabase(ACTUAL_DB, 1).apply {
close()
}
}
@Test
@Throws(IOException::class)
fun populateVersion1Db() {
var db = helper.createDatabase(ACTUAL_DB, 1).apply {
// db has schema version 1. insert some data using SQL queries.
// We cannot use DAO classes because they expect the latest schema.
for (i in 1..1000) {
val insertSql = """INSERT INTO record_table values (?,?,?,?,?,?,?,?,?)""".trimIndent()
execSQL(insertSql, arrayOf(i, System.currentTimeMillis(), 1, "testMessage", "AU_DTA", "modelP", "modelC", i, i))
}
close()
}
}
}

View file

@ -10,6 +10,22 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/MyTheme.DayNight"> android:theme="@style/MyTheme.DayNight">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity
android:name="au.gov.health.covidsafe.PeekActivity"
android:screenOrientation="portrait"
android:theme="@style/MyTheme.DayNightDebug"/>
</application> </application>
</manifest> </manifest>

View file

@ -6,18 +6,23 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import au.gov.health.covidsafe.logging.CentralLog
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
import au.gov.health.covidsafe.streetpass.view.RecordViewModel
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage import kotlinx.android.synthetic.main.database_peek.*
import au.gov.health.covidsafe.streetpass.view.RecordViewModel import java.io.File
private const val TAG = "PeekActivity"
class PeekActivity : AppCompatActivity() { class PeekActivity : AppCompatActivity() {
@ -47,20 +52,6 @@ class PeekActivity : AppCompatActivity() {
adapter.setSourceData(records) adapter.setSourceData(records)
}) })
findViewById<FloatingActionButton>(R.id.expand)
.setOnClickListener {
viewModel.allRecords.value?.let {
adapter.setMode(RecordListAdapter.MODE.ALL)
}
}
findViewById<FloatingActionButton>(R.id.collapse)
.setOnClickListener {
viewModel.allRecords.value?.let {
adapter.setMode(RecordListAdapter.MODE.COLLAPSE)
}
}
val start = findViewById<FloatingActionButton>(R.id.start) val start = findViewById<FloatingActionButton>(R.id.start)
start.setOnClickListener { start.setOnClickListener {
@ -106,11 +97,30 @@ class PeekActivity : AppCompatActivity() {
} }
val plot = findViewById<FloatingActionButton>(R.id.plot)
plot.setOnClickListener { view -> shareDatabase.setOnClickListener {
val intent = Intent(this, PlotActivity::class.java) val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
intent.putExtra("time_period", nextTimePeriod()) val databaseFilePath= getDatabasePath("record_database").absolutePath
startActivity(intent) val databaseFile = File(databaseFilePath)
CentralLog.d(TAG, "authority = $authority, databaseFilePath = $databaseFilePath")
if(databaseFile.exists()) {
CentralLog.d(TAG, "databaseFile.length = ${databaseFile.length()}")
FileProvider.getUriForFile(
this,
authority,
databaseFile
)?.let { databaseFileUri ->
CentralLog.d(TAG, "databaseFileUri = $databaseFileUri")
val intent = Intent(Intent.ACTION_SEND)
intent.type = "application/octet-stream"
intent.putExtra(Intent.EXTRA_STREAM, databaseFileUri)
startActivity(Intent.createChooser(intent, "Sharing database"))
}
}
} }
if(!BuildConfig.DEBUG) { if(!BuildConfig.DEBUG) {

View file

@ -27,16 +27,6 @@
android:theme="@style/MyTheme.DayNight" android:theme="@style/MyTheme.DayNight"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity <activity
android:name="au.gov.health.covidsafe.SplashActivity" android:name="au.gov.health.covidsafe.SplashActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
@ -58,9 +48,6 @@
android:name="au.gov.health.covidsafe.HomeActivity" android:name="au.gov.health.covidsafe.HomeActivity"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustPan"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name="au.gov.health.covidsafe.SelfIsolationDoneActivity"
android:screenOrientation="portrait" />
<receiver android:name="au.gov.health.covidsafe.boot.StartOnBootReceiver"> <receiver android:name="au.gov.health.covidsafe.boot.StartOnBootReceiver">
<intent-filter> <intent-filter>
@ -75,16 +62,6 @@
<service android:name="au.gov.health.covidsafe.services.SensorMonitoringService" /> <service android:name="au.gov.health.covidsafe.services.SensorMonitoringService" />
<activity
android:name="au.gov.health.covidsafe.PeekActivity"
android:screenOrientation="portrait"
android:theme="@style/MyTheme.DayNightDebug"/>
<activity
android:name="au.gov.health.covidsafe.PlotActivity"
android:screenOrientation="landscape"
android:theme="@style/MyTheme.DayNightDebug" />
<receiver android:name="au.gov.health.covidsafe.receivers.UpgradeReceiver"> <receiver android:name="au.gov.health.covidsafe.receivers.UpgradeReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
package au.gov.health.covidsafe
class LocalBlobV2(val modelP : String?, val modelC : String?, val txPower : Int?, val rssi : Int?)

View file

@ -1,231 +0,0 @@
package au.gov.health.covidsafe
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import au.gov.health.covidsafe.status.persistence.StatusRecord
import au.gov.health.covidsafe.status.persistence.StatusRecordStorage
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
import au.gov.health.covidsafe.ui.upload.model.DebugData
import java.text.SimpleDateFormat
import java.util.*
import kotlin.Comparator
class PlotActivity : AppCompatActivity() {
private var TAG = "PlotActivity"
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_plot)
val webView = findViewById<WebView>(R.id.webView)
webView.webViewClient = WebViewClient()
webView.settings.javaScriptEnabled = true
val displayTimePeriod = intent.getIntExtra("time_period", 1) // in hours
val observableStreetRecords = Observable.create<List<StreetPassRecord>> {
val result = StreetPassRecordStorage(this).getAllRecords()
it.onNext(result)
}
val observableStatusRecords = Observable.create<List<StatusRecord>> {
val result = StatusRecordStorage(this).getAllRecords()
it.onNext(result)
}
val zipResult = Observable.zip(observableStreetRecords, observableStatusRecords,
BiFunction<List<StreetPassRecord>, List<StatusRecord>, DebugData> { records, _ -> DebugData(records) }
)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe { exportedData ->
if(exportedData.records.isEmpty()){
return@subscribe
}
val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
// Use the date of the last record as the end time (Epoch time in seconds)
val endTime =
exportedData.records.sortedByDescending { it.timestamp }[0].timestamp / 1000 + 1 * 60
val endTimeString = dateFormatter.format(Date(endTime * 1000))
val startTime =
endTime - displayTimePeriod * 3600 // ignore records older than X hour(s)
val startTimeString = dateFormatter.format(Date(startTime * 1000))
val filteredRecords = exportedData.records.filter {
it.timestamp / 1000 in startTime..endTime
}
if (filteredRecords.isNotEmpty()) {
val dataByModelC = filteredRecords.groupBy { it.modelC }
val dataByModelP = filteredRecords.groupBy { it.modelP }
// get all models
val allModelList = dataByModelC.keys union dataByModelP.keys.toList()
// sort the list by the models that appear the most frequently
val sortedModelList =
allModelList.sortedWith(Comparator { a: String, b: String ->
val aSize = (dataByModelC[a]?.size ?: 0) + (dataByModelP[a]?.size ?: 0)
val bSize = (dataByModelC[b]?.size ?: 0) + (dataByModelP[b]?.size ?: 0)
bSize - aSize
})
val individualData = sortedModelList.joinToString(separator = "\n") { model ->
val index = sortedModelList.indexOf(model) + 1
val hasC = dataByModelC.containsKey(model)
val hasP = dataByModelP.containsKey(model)
val x1 = dataByModelC[model]?.joinToString(separator = "\", \"", prefix = "[\"", postfix = "\"]") {
dateFormatter.format(Date(it.timestamp))
}
val y1 = dataByModelC[model]?.map { it.rssi }
?.joinToString(separator = ", ", prefix = "[", postfix = "]")
val x2 = dataByModelP[model]?.joinToString(separator = "\", \"", prefix = "[\"", postfix = "\"]") {
dateFormatter.format(Date(it.timestamp))
}
val y2 = dataByModelP[model]?.map { it.rssi }
?.joinToString(separator = ", ", prefix = "[", postfix = "]")
val dataHead = "var data${index} = [];"
val dataA = if (!hasC) "" else """
var data${index}a = {
name: 'central',
x: ${x1},
y: ${y1},
xaxis: 'x${index}',
yaxis: 'y${index}',
mode: 'markers',
type: 'scatter',
line: {color: 'blue'}
};
data${index} = data${index}.concat(data${index}a);
""".trimIndent()
val dataB = if (!hasP) "" else """
var data${index}b = {
name: 'peripheral',
x: ${x2},
y: ${y2},
xaxis: 'x${index}',
yaxis: 'y${index}',
mode: 'markers',
type: 'scatter',
line: {color: 'red'}
};
data${index} = data${index}.concat(data${index}b);
""".trimIndent()
val data = dataHead + dataA + dataB
data
}
val top = 20
val combinedData = sortedModelList.joinToString(separator = "\n") { model ->
val index = sortedModelList.indexOf(model) + 1
if (index < top) """
data = data.concat(data${index});
""".trimIndent() else ""
}
val xAxis = sortedModelList.joinToString(separator = ",\n") { model ->
val index = sortedModelList.indexOf(model) + 1
if (index < top) """
xaxis${index}: {
type: 'date',
tickformat: '%H:%M:%S',
range: ['${startTimeString}', '${endTimeString}'],
dtick: ${displayTimePeriod * 5} * 60 * 1000
}
""".trimIndent() else ""
}
val yAxis = sortedModelList.joinToString(separator = ",\n") { model ->
val index = sortedModelList.indexOf(model) + 1
if (index < top) """
yaxis${index}: {
range: [-100, -30],
ticks: 'outside',
dtick: 10,
title: {
text: "$model"
}
}
""".trimIndent() else ""
}
// Form the complete HTML
val customHtml = """
<head>
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
</head>
<body>
<div id='myDiv'></div>
<script>
$individualData
var data = [];
$combinedData
var layout = {
title: 'Activities from <b>${startTimeString.substring(11..15)}</b> to <b>${endTimeString.substring(11..15)}</b> <span style="color:blue">central</span> <span style="color:red">peripheral</span>',
height: 135 * ${allModelList.size},
showlegend: false,
grid: {rows: ${allModelList.size}, columns: 1, pattern: 'independent'},
margin: {
t: 30,
r: 30,
b: 20,
l: 50,
pad: 0
},
$xAxis,
$yAxis
};
var config = {
responsive: true,
displayModeBar: false,
displaylogo: false,
modeBarButtonsToRemove: ['toImage', 'sendDataToCloud', 'editInChartStudio', 'zoom2d', 'select2d', 'pan2d', 'lasso2d', 'autoScale2d', 'resetScale2d', 'zoomIn2d', 'zoomOut2d', 'hoverClosestCartesian', 'hoverCompareCartesian', 'toggleHover', 'toggleSpikelines']
}
Plotly.newPlot('myDiv', data, layout, config);
</script>
</body>
""".trimIndent()
webView.loadData(customHtml, "text/html", "UTF-8")
} else {
webView.loadData(
"No data received in the last $displayTimePeriod hour(s) or more.",
"text/html",
"UTF-8"
)
}
}
webView.loadData("Loading...", "text/html", "UTF-8")
}
}

View file

@ -16,12 +16,6 @@ class RecordListAdapter internal constructor(context: Context) :
private var records = emptyList<StreetPassRecordViewModel>() // Cached copy of records private var records = emptyList<StreetPassRecordViewModel>() // Cached copy of records
private var sourceData = emptyList<StreetPassRecord>() private var sourceData = emptyList<StreetPassRecord>()
enum class MODE {
ALL, COLLAPSE, MODEL_P, MODEL_C
}
private var mode = MODE.ALL
inner class RecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { inner class RecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val modelCView: TextView = itemView.findViewById(R.id.modelc) val modelCView: TextView = itemView.findViewById(R.id.modelc)
val modelPView: TextView = itemView.findViewById(R.id.modelp) val modelPView: TextView = itemView.findViewById(R.id.modelp)
@ -59,78 +53,16 @@ class RecordListAdapter internal constructor(context: Context) :
holder.txpowerView.text = "Tx Power: ${current.transmissionPower}" holder.txpowerView.text = "Tx Power: ${current.transmissionPower}"
holder.filterModelP.setOnClickListener {
val model = it.tag as StreetPassRecordViewModel
setMode(MODE.MODEL_P, model)
} }
holder.filterModelC.setOnClickListener { private fun setRecords(records: List<StreetPassRecordViewModel>) {
val model = it.tag as StreetPassRecordViewModel this.records = records
setMode(MODE.MODEL_C, model) notifyDataSetChanged()
}
} }
private fun filter(sample: StreetPassRecordViewModel?): List<StreetPassRecordViewModel> { internal fun setSourceData(records: List<StreetPassRecord>) {
return when (mode) { this.sourceData = records
MODE.COLLAPSE -> prepareCollapsedData(sourceData) setRecords(prepareViewData(this.sourceData))
MODE.ALL -> prepareViewData(sourceData)
MODE.MODEL_P -> filterByModelP(sample, sourceData)
MODE.MODEL_C -> filterByModelC(sample, sourceData)
else -> {
prepareViewData(sourceData)
}
}
}
private fun filterByModelC(
model: StreetPassRecordViewModel?,
words: List<StreetPassRecord>
): List<StreetPassRecordViewModel> {
if (model != null) {
return prepareViewData(words.filter { it.modelC == model.modelC })
}
return prepareViewData(words)
}
private fun filterByModelP(
model: StreetPassRecordViewModel?,
words: List<StreetPassRecord>
): List<StreetPassRecordViewModel> {
if (model != null) {
return prepareViewData(words.filter { it.modelP == model.modelP })
}
return prepareViewData(words)
}
private fun prepareCollapsedData(words: List<StreetPassRecord>): List<StreetPassRecordViewModel> {
//we'll need to count the number of unique device IDs
val countMap = words.groupBy {
it.modelC
}
val distinctAddresses = words.distinctBy { it.modelC }
return distinctAddresses.map { record ->
val count = countMap[record.modelC]?.size
count?.let { count ->
val mostRecentRecord = countMap[record.modelC]?.maxBy { it.timestamp }
if (mostRecentRecord != null) {
return@map StreetPassRecordViewModel(mostRecentRecord, count)
}
return@map StreetPassRecordViewModel(record, count)
}
//fallback - unintended
return@map StreetPassRecordViewModel(record)
}
} }
private fun prepareViewData(words: List<StreetPassRecord>): List<StreetPassRecordViewModel> { private fun prepareViewData(words: List<StreetPassRecord>): List<StreetPassRecordViewModel> {
@ -144,27 +76,6 @@ class RecordListAdapter internal constructor(context: Context) :
} }
} }
fun setMode(mode: MODE) {
setMode(mode, null)
}
private fun setMode(mode: MODE, model: StreetPassRecordViewModel?) {
this.mode = mode
val list = filter(model)
setRecords(list)
}
private fun setRecords(records: List<StreetPassRecordViewModel>) {
this.records = records
notifyDataSetChanged()
}
internal fun setSourceData(records: List<StreetPassRecord>) {
this.sourceData = records
setMode(mode)
}
override fun getItemCount() = records.size override fun getItemCount() = records.size
} }

View file

@ -1,35 +0,0 @@
package au.gov.health.covidsafe
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import kotlinx.android.synthetic.main.activity_self_isolation.*
class SelfIsolationDoneActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_self_isolation)
}
override fun onResume() {
super.onResume()
activity_self_isolation_next.setOnClickListener {
Preference.setDataIsUploaded(this, false)
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
}
override fun onPause() {
super.onPause()
activity_self_isolation_next.setOnClickListener(null)
}
override fun onDestroy() {
super.onDestroy()
root.removeAllViews()
}
}

View file

@ -1,24 +1,29 @@
package au.gov.health.covidsafe package au.gov.health.covidsafe
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import au.gov.health.covidsafe.ui.onboarding.OnboardingActivity import au.gov.health.covidsafe.ui.onboarding.OnboardingActivity
import au.gov.health.covidsafe.ui.splash.SplashNavigationEvent
import au.gov.health.covidsafe.ui.splash.SplashViewModel
import au.gov.health.covidsafe.ui.splash.SplashViewModelFactory
import kotlinx.android.synthetic.main.activity_splash.*
import java.util.* import java.util.*
class SplashActivity : AppCompatActivity() { class SplashActivity : AppCompatActivity() {
private val SPLASH_TIME: Long = 2000 private lateinit var viewModel: SplashViewModel
private var retryProviderInstall: Boolean = false private var retryProviderInstall: Boolean = false
private val ERROR_DIALOG_REQUEST_CODE = 1 private val ERROR_DIALOG_REQUEST_CODE = 1
private var updateFlag = false
private lateinit var mHandler: Handler private lateinit var mHandler: Handler
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -26,10 +31,21 @@ class SplashActivity : AppCompatActivity() {
setContentView(R.layout.activity_splash) setContentView(R.layout.activity_splash)
hideSystemUI() hideSystemUI()
mHandler = Handler() mHandler = Handler()
viewModel = ViewModelProvider(this, SplashViewModelFactory(this)).get(SplashViewModel::class.java)
Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver, Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver, Settings.Secure.ANDROID_ID))
Settings.Secure.ANDROID_ID))
viewModel.splashNavigationLiveData.observe(this, Observer {
when (it) {
is SplashNavigationEvent.GoToNextScreen -> goToNextScreen()
is SplashNavigationEvent.ShowMigrationScreen -> migrationScreen()
}
})
}
override fun onStart() {
super.onStart()
viewModel.setupUI()
} }
override fun onPause() { override fun onPause() {
@ -37,30 +53,23 @@ class SplashActivity : AppCompatActivity() {
mHandler.removeCallbacksAndMessages(null) mHandler.removeCallbacksAndMessages(null)
} }
override fun onResume() { private fun migrationScreen() {
super.onResume() splash_screen_logo.setImageResource(R.drawable.ic_logo_home_inactive)
if (!updateFlag) { splash_screen_logo.setAnimation("spinner_migrating_db.json")
mHandler.postDelayed({ splash_screen_logo.playAnimation()
goToNextScreen() splash_migration_text.visibility = VISIBLE
finish() help_stop_covid.visibility = GONE
}, SPLASH_TIME)
}
} }
private fun goToNextScreen() { private fun goToNextScreen() {
val dateUploaded = Calendar.getInstance().also {
it.timeInMillis = Preference.getDataUploadedDateMs(this)
}
val fourteenDaysAgo = Calendar.getInstance().also {
it.add(Calendar.DATE, -14)
}
startActivity(Intent(this, if (!Preference.isOnBoarded(this)) { startActivity(Intent(this, if (!Preference.isOnBoarded(this)) {
OnboardingActivity::class.java OnboardingActivity::class.java
} else if (dateUploaded.before(fourteenDaysAgo)) {
SelfIsolationDoneActivity::class.java
} else { } else {
HomeActivity::class.java HomeActivity::class.java
})) }))
viewModel.splashNavigationLiveData.removeObservers(this)
viewModel.release()
finish()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -79,4 +88,6 @@ class SplashActivity : AppCompatActivity() {
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN) or View.SYSTEM_UI_FLAG_FULLSCREEN)
} }
} }

View file

@ -1,9 +1,8 @@
package au.gov.health.covidsafe.bluetooth.gatt package au.gov.health.covidsafe.bluetooth.gatt
import au.gov.health.covidsafe.BuildConfig
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import au.gov.health.covidsafe.BuildConfig
import au.gov.health.covidsafe.streetpass.PeripheralDevice
const val ACTION_RECEIVED_STREETPASS = const val ACTION_RECEIVED_STREETPASS =
"${BuildConfig.APPLICATION_ID}.ACTION_RECEIVED_STREETPASS" "${BuildConfig.APPLICATION_ID}.ACTION_RECEIVED_STREETPASS"
@ -23,9 +22,9 @@ class ReadRequestPayload(
val v: Int, val v: Int,
val msg: String, val msg: String,
val org: String, val org: String,
peripheral: PeripheralDevice modelP: String?
) { ) {
val modelP = peripheral.modelP val modelP = modelP ?: ""
fun getPayload(): ByteArray { fun getPayload(): ByteArray {
return gson.toJson(this).toByteArray(Charsets.UTF_8) return gson.toJson(this).toByteArray(Charsets.UTF_8)

View file

@ -9,6 +9,9 @@ import au.gov.health.covidsafe.Utils
import au.gov.health.covidsafe.logging.CentralLog import au.gov.health.covidsafe.logging.CentralLog
import au.gov.health.covidsafe.streetpass.CentralDevice import au.gov.health.covidsafe.streetpass.CentralDevice
import au.gov.health.covidsafe.streetpass.ConnectionRecord import au.gov.health.covidsafe.streetpass.ConnectionRecord
import au.gov.health.covidsafe.streetpass.persistence.Encryption
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.util.* import java.util.*
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -20,6 +23,9 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) {
private var serviceUUID: UUID by Delegates.notNull() private var serviceUUID: UUID by Delegates.notNull()
var bluetoothGattServer: BluetoothGattServer? = null var bluetoothGattServer: BluetoothGattServer? = null
val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
init { init {
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
this.serviceUUID = UUID.fromString(serviceUUIDString) this.serviceUUID = UUID.fromString(serviceUUIDString)
@ -69,12 +75,18 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) {
if (serviceUUID == characteristic?.uuid) { if (serviceUUID == characteristic?.uuid) {
if (Utils.bmValid(context)) { if (Utils.bmValid(context)) {
val peripheral = TracerApp.asPeripheralDevice()
val readRequest = ReadRequestEncryptedPayload(peripheral.modelP,
TracerApp.thisDeviceMsg())
val plainRecord = gson.toJson(readRequest)
val plainRecordByteArray = plainRecord.toByteArray(Charsets.UTF_8)
val remoteBlob = Encryption.encryptPayload(plainRecordByteArray)
val base = readPayloadMap.getOrPut(device.address, { val base = readPayloadMap.getOrPut(device.address, {
ReadRequestPayload( ReadRequestPayload(
v = TracerApp.protocolVersion, v = TracerApp.protocolVersion,
msg = TracerApp.thisDeviceMsg(), msg = remoteBlob,
org = TracerApp.ORG, org = TracerApp.ORG,
peripheral = TracerApp.asPeripheralDevice() modelP = null //This is going to be stored as empty in the db as DUMMY value
).getPayload() ).getPayload()
}) })
@ -114,6 +126,8 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) {
} }
inner class ReadRequestEncryptedPayload (val modelP : String, val msg: String)
override fun onCharacteristicWriteRequest( override fun onCharacteristicWriteRequest(
device: BluetoothDevice?, device: BluetoothDevice?,

View file

@ -11,9 +11,7 @@ import android.os.PowerManager
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import au.gov.health.covidsafe.BuildConfig import au.gov.health.covidsafe.*
import au.gov.health.covidsafe.Preference
import au.gov.health.covidsafe.Utils
import au.gov.health.covidsafe.bluetooth.BLEAdvertiser import au.gov.health.covidsafe.bluetooth.BLEAdvertiser
import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STATUS import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STATUS
import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STREETPASS import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STREETPASS
@ -31,8 +29,17 @@ import au.gov.health.covidsafe.streetpass.ConnectionRecord
import au.gov.health.covidsafe.streetpass.StreetPassScanner import au.gov.health.covidsafe.streetpass.StreetPassScanner
import au.gov.health.covidsafe.streetpass.StreetPassServer import au.gov.health.covidsafe.streetpass.StreetPassServer
import au.gov.health.covidsafe.streetpass.StreetPassWorker import au.gov.health.covidsafe.streetpass.StreetPassWorker
import au.gov.health.covidsafe.streetpass.persistence.Encryption
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_DEVICE
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_RSSI
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_TXPOWER
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.ENCRYPTED_EMPTY_DICT
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.VERSION_ONE
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -40,6 +47,7 @@ import kotlinx.coroutines.launch
import pub.devrel.easypermissions.EasyPermissions import pub.devrel.easypermissions.EasyPermissions
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@Keep @Keep
class BluetoothMonitoringService : LifecycleService(), CoroutineScope { class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
@ -75,6 +83,9 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
private val awsClient = NetworkFactory.awsClient private val awsClient = NetworkFactory.awsClient
private val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
/** Defines callbacks for service binding, passed to bindService() */ /** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
@ -550,13 +561,13 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (ACTION_RECEIVED_STREETPASS == intent.action) { if (ACTION_RECEIVED_STREETPASS == intent.action) {
val connRecord: ConnectionRecord = intent.getParcelableExtra(STREET_PASS) val connRecord: ConnectionRecord? = intent.getParcelableExtra(STREET_PASS)
CentralLog.d( CentralLog.d(
TAG, TAG,
"StreetPass received: $connRecord" "StreetPass received: $connRecord"
) )
if (connRecord.msg.isNotEmpty()) { if (connRecord != null && connRecord.msg.isNotEmpty()) {
if (mBound) { if (mBound) {
val proximity = mService.proximity val proximity = mService.proximity
@ -567,16 +578,37 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
) )
} }
val record = StreetPassRecord( val remoteBlob: String = if (connRecord.version == VERSION_ONE) {
v = connRecord.version, with(receiver = connRecord) {
msg = connRecord.msg, val plainRecordByteArray = gson.toJson(StreetPassRecordDatabase.Companion.EncryptedRecord(
org = connRecord.org, peripheral.modelP, central.modelC, rssi, txPower, msg = msg))
modelP = connRecord.peripheral.modelP, .toByteArray(Charsets.UTF_8)
modelC = connRecord.central.modelC, Encryption.encryptPayload(plainRecordByteArray)
rssi = connRecord.rssi, }
txPower = connRecord.txPower } else {
) //For version after version 1, the message is already encrypted in msg and we can store it as remote BLOB
connRecord.msg
}
val localBlob : String = if (connRecord.version == VERSION_ONE) {
ENCRYPTED_EMPTY_DICT
} else {
with (receiver = connRecord) {
val modelP = if (DUMMY_DEVICE == peripheral.modelP) null else peripheral.modelP
val modelC = if (DUMMY_DEVICE == central.modelC) null else central.modelC
val rssi = if (rssi == DUMMY_RSSI) null else rssi
val txPower = if (txPower == DUMMY_TXPOWER) null else txPower
val plainLocalBlob = gson.toJson(LocalBlobV2(modelP, modelC, rssi, txPower))
.toByteArray(Charsets.UTF_8)
Encryption.encryptPayload(plainLocalBlob)
}
}
val record = StreetPassRecord(
v = if (connRecord.version == 1) TracerApp.protocolVersion else (connRecord.version),
org = connRecord.org,
localBlob = localBlob,
remoteBlob = remoteBlob
)
launch { launch {
CentralLog.d( CentralLog.d(

View file

@ -0,0 +1,177 @@
package au.gov.health.covidsafe.streetpass
import android.bluetooth.BluetoothGatt
import android.content.pm.ApplicationInfo
import android.os.Build
import au.gov.health.covidsafe.logging.CentralLog
import java.lang.NullPointerException
import java.lang.RuntimeException
import java.lang.reflect.Field
object StreetPassPairingFix {
private const val TAG = "StreetPassPairingFix"
private var initFailed = false
private var initComplete = false
private var bluetoothGattClass = BluetoothGatt::class.java
private var mAuthRetryStateField: Field? = null
private var mAuthRetryField: Field? = null
/**
* Initialises all the reflection references used by bypassAuthenticationRetry
*
* This has been checked against the source of Android 10_r36
*
* Returns true if object is in valid state
*/
@Synchronized
private fun tryInit(): Boolean {
// Check if function has already run and failed
if (initFailed || initComplete) {
return !initFailed
}
// This technique works only up to Android P/API 28. This is due to mAuthRetryState being
// a greylisted non-SDK interface.
// See
// https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces
// https://android.googlesource.com/platform/frameworks/base/+/45d2c252b19c08bbd20acaaa2f52ae8518150169%5E%21/core/java/android/bluetooth/BluetoothGatt.java
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P && ApplicationInfo().targetSdkVersion > Build.VERSION_CODES.P) {
CentralLog.i(TAG,
"Failed to initialise: mAuthRetryState is in restricted grey-list post API 28")
initFailed = true
initComplete = true
return !initFailed
}
CentralLog.i(TAG, "Initialising StreetPassParingFix fields")
try {
try {
// Get a reference to the mAuthRetryState
// This will throw NoSuchFieldException on older android, which is handled below
mAuthRetryStateField = bluetoothGattClass.getDeclaredField("mAuthRetryState")
CentralLog.i(TAG, "Found mAuthRetryState")
} catch (e: NoSuchFieldException) {
// Prior to https://android.googlesource.com/platform/frameworks/base/+/3854e2267487ecd129bdd0711c6d9dfbf8f7ed0d%5E%21/#F0,
// And at least after Nougat (7), mAuthRetryField (a boolean) was used instead
// of mAuthRetryState
CentralLog.i(TAG,
"No mAuthRetryState on this device, trying for mAuthRetry")
// This will throw NoSuchFieldException again on fail, which is handled below
mAuthRetryField = bluetoothGattClass.getDeclaredField("mAuthRetry")
CentralLog.i(TAG, "Found mAuthRetry")
}
// Should be good to go now
CentralLog.i(TAG, "Initialisation complete")
initComplete = true
initFailed = false
return !initFailed
} catch (e: NoSuchFieldException) {
// One of the fields was missing - likely an API version issue
CentralLog.i(TAG, "Unable to find field while initialising: "+ e.message)
} catch (e: SecurityException) {
// Sandbox didn't like reflection
CentralLog.i(TAG,
"Encountered sandbox exception while initialising: " + e.message)
} catch (e: NullPointerException) {
// Probably accessed an instance field as a static
CentralLog.i(TAG, "Encountered NPE while initialising: " + e.message)
} catch (e: RuntimeException) {
// For any other undocumented exception we just want to fail silentely
CentralLog.i(TAG, "Encountered Exception while initialising: " + e.message)
}
// If this point is reached the initialisation has failed
CentralLog.i(TAG,
"Failed to initialise, bypassAuthenticationRetry will quietly fail")
initComplete = true
initFailed = true
return !initFailed
}
/**
* This function will attempt to bypass the conditionals in BluetoothGatt.mBluetoothGattCallback
* that cause bonding to occur.
*
* The function will fail silently if any errors occur during initialisation or patching.
*
* See
* https://android.googlesource.com/platform/frameworks/base/+/76c1d9d5e15f48e54fc810c3efb683a0c5fd14b0/core/java/android/bluetooth/BluetoothGatt.java#367
* for an example of the conditional that is bypassed
*/
@Synchronized
fun bypassAuthenticationRetry(gatt: BluetoothGatt) {
if (!tryInit()) {
// Class failed to initialised correctly, return quietly
return
}
try {
// Attempt the bypass for newer android
if (mAuthRetryStateField != null) {
CentralLog.i(TAG, "Attempting to bypass mAuthRetryState bonding conditional")
// Set the field accessible (if required)
val mAuthRetryStateAccessible = mAuthRetryStateField!!.isAccessible
if (!mAuthRetryStateAccessible) {
mAuthRetryStateField!!.isAccessible = true
}
// The conditional branch that causes binding to occur in BluetoothGatt do not occur
// if mAuthRetryState == AUTH_RETRY_STATE_MITM (int 2), as this signifies that both
// steps of authenticated/encrypted reading have failed to establish. See
// https://android.googlesource.com/platform/frameworks/base/+/76c1d9d5e15f48e54fc810c3efb683a0c5fd14b0/core/java/android/bluetooth/BluetoothGatt.java#70
//
// Previously this class reflectively read the value of AUTH_RETRY_STATE_MITM,
// instead of using a constant, but reportedly this doesn't work API 27+.
//
// Write mAuthRetryState to this value so it appears that bonding has already failed
mAuthRetryStateField!!.setInt(gatt, 2) // Unwrap is safe
// Reset accessibility
mAuthRetryStateField!!.isAccessible = mAuthRetryStateAccessible
} else {
CentralLog.i(TAG, "Attempting to bypass mAuthRetry bonding conditional")
// Set the field accessible (if required)
val mAuthRetryAccessible = mAuthRetryField!!.isAccessible
if (!mAuthRetryAccessible) {
mAuthRetryField!!.isAccessible = true
}
// The conditional branch that causes binding to occur in BluetoothGatt do not occur
// if mAuthRetry == true, as this signifies an attempt was made to bind
//
// See https://android.googlesource.com/platform/frameworks/base/+/63b4f6f5db4d5ea0114d195a0f33970e7070f21b/core/java/android/bluetooth/BluetoothGatt.java#263
//
// Write mAuthRetry to true so it appears that bonding has already failed
mAuthRetryField!!.setBoolean(gatt, true)
// Reset accessibility
mAuthRetryField!!.isAccessible = mAuthRetryAccessible
}
} catch (e: SecurityException) {
// Sandbox didn't like reflection
CentralLog.i(TAG,
"Encountered sandbox exception in bypassAuthenticationRetry: " + e.message)
} catch (e: IllegalArgumentException) {
// Either a bad field access or wrong type was read
CentralLog.i(TAG,
"Encountered argument exception in bypassAuthenticationRetry: " + e.message)
} catch (e: NullPointerException) {
// Probably accessed an instance field as a static
CentralLog.i(TAG,
"Encountered NPE in bypassAuthenticationRetry: " + e.message)
} catch (e: ExceptionInInitializerError) {
CentralLog.i(TAG,
"Encountered reflection in bypassAuthenticationRetry: " + e.message)
}
}
}

View file

@ -16,8 +16,16 @@ import au.gov.health.covidsafe.logging.CentralLog
import au.gov.health.covidsafe.services.BluetoothMonitoringService import au.gov.health.covidsafe.services.BluetoothMonitoringService
import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.blacklistDuration import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.blacklistDuration
import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.maxQueueTime import au.gov.health.covidsafe.services.BluetoothMonitoringService.Companion.maxQueueTime
import au.gov.health.covidsafe.streetpass.persistence.Encryption
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_DEVICE
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_RSSI
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase.Companion.DUMMY_TXPOWER
import com.google.gson.GsonBuilder
import java.util.* import java.util.*
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
@Keep @Keep
class StreetPassWorker(val context: Context) { class StreetPassWorker(val context: Context) {
@ -39,6 +47,9 @@ class StreetPassWorker(val context: Context) {
private var currentPendingConnection: Work? = null private var currentPendingConnection: Work? = null
private var localBroadcastManager: LocalBroadcastManager = LocalBroadcastManager.getInstance(context) private var localBroadcastManager: LocalBroadcastManager = LocalBroadcastManager.getInstance(context)
private val gson = GsonBuilder().disableHtmlEscaping().create()
val onWorkTimeoutListener = object : Work.OnWorkTimeoutListener { val onWorkTimeoutListener = object : Work.OnWorkTimeoutListener {
override fun onWorkTimeout(work: Work) { override fun onWorkTimeout(work: Work) {
@ -485,6 +496,10 @@ class StreetPassWorker(val context: Context) {
service?.let { service?.let {
val characteristic = service.getCharacteristic(serviceUUID) val characteristic = service.getCharacteristic(serviceUUID)
if (characteristic != null) { if (characteristic != null) {
// Attempt to prevent bonding should the StreetPass characteristic
// require authentication or encryption
StreetPassPairingFix.bypassAuthenticationRetry(gatt)
val readSuccess = gatt.readCharacteristic(characteristic) val readSuccess = gatt.readCharacteristic(characteristic)
CentralLog.i( CentralLog.i(
TAG, TAG,
@ -587,18 +602,30 @@ class StreetPassWorker(val context: Context) {
if (Utils.bmValid(context)) { if (Utils.bmValid(context)) {
//may have failed to read, can try to write //may have failed to read, can try to write
//we are writing as the central device //we are writing as the central device
val thisCentralDevice = TracerApp.asCentralDevice() val thisCentralDevice = TracerApp.asCentralDevice()
val plainRecord = gson.toJson(EncryptedWriteRequestPayload(
thisCentralDevice.modelC,
work.connectable.rssi,
work.connectable.transmissionPower,
TracerApp.thisDeviceMsg())).toByteArray(Charsets.UTF_8)
val remoteBlob = Encryption.encryptPayload(plainRecord)
val writedata = WriteRequestPayload( val writedata = WriteRequestPayload(
v = TracerApp.protocolVersion, v = TracerApp.protocolVersion,
msg = TracerApp.thisDeviceMsg(), msg = remoteBlob,
org = TracerApp.ORG, org = TracerApp.ORG,
modelC = thisCentralDevice.modelC, modelC = DUMMY_DEVICE,
rssi = work.connectable.rssi, rssi = DUMMY_RSSI,
txPower = work.connectable.transmissionPower txPower = DUMMY_TXPOWER
) )
characteristic.value = writedata.getPayload() characteristic.value = writedata.getPayload()
// Attempt to prevent bonding should the StreetPass characteristic
// require authentication or encryption
StreetPassPairingFix.bypassAuthenticationRetry(gatt)
val writeSuccess = gatt.writeCharacteristic(characteristic) val writeSuccess = gatt.writeCharacteristic(characteristic)
CentralLog.i( CentralLog.i(
TAG, TAG,
@ -614,6 +641,8 @@ class StreetPassWorker(val context: Context) {
} }
} }
inner class EncryptedWriteRequestPayload(val modelC: String, val rssi: Int, val txPower: Int?, val msg : String)
override fun onCharacteristicWrite( override fun onCharacteristicWrite(
gatt: BluetoothGatt, gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic, characteristic: BluetoothGattCharacteristic,

View file

@ -0,0 +1,158 @@
package au.gov.health.covidsafe.streetpass.persistence
import android.util.Base64
import au.gov.health.covidsafe.BuildConfig
import java.math.BigInteger
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PublicKey
import java.security.interfaces.ECPublicKey
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher
import javax.crypto.KeyAgreement
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
data class EncryptionKeys(val ephPubKey: ByteArray, val aesKey: SecretKey, val macKey: SecretKey, val nonce: ByteArray)
object Encryption {
const val KEY_GEN_TIME_DELTA = 450000 // 7.5 minutes
private val TAG = this.javaClass.simpleName
// Get the server's ECDH public key
private fun readKey(): PublicKey {
val decodedKey: ByteArray = Base64.decode(BuildConfig.ENCRYPTION_PUBLIC_KEY, Base64.DEFAULT)
val keySpec = X509EncodedKeySpec(decodedKey)
return KeyFactory.getInstance("EC").generatePublic(keySpec)
}
// Compute a SHA-256 hash
private fun hash(content: ByteArray): ByteArray {
val hash: MessageDigest = MessageDigest.getInstance("SHA-256")
hash.update(content)
return hash.digest()
}
// Generate ECDH P256 key-pair
private fun makeECKeys(): KeyPair {
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance("EC")
kpg.initialize(256)
return kpg.generateKeyPair()
}
// Convert an ECDH public key coordinate to R
private fun getPublicKey(kp: KeyPair): ByteArray {
val key: PublicKey = kp.public
if (key is ECPublicKey) {
if (key.w.affineX == BigInteger.ZERO && key.w.affineY == BigInteger.ZERO) {
return ByteArray(1)
}
var x: ByteArray = key.w.affineX.toByteArray()
if (x.size == 33 && x[0] == 0.toByte()) {
x = x.sliceArray(1..32)
} else if (x.size >= 33) {
throw IllegalStateException("Unexpected x coordinate in ECDH public key")
} else if (x.size < 32) {
x = ByteArray(32 - x.size).plus(x)
}
// Using P256 so q = p, p (mod 2) = 1
// Compression flag is 0x2 when y (mod 2) = 0 and 0x3 when y (mod 2) = 1
val flag: Int = 2 or (key.w.affineY and 1.toBigInteger()).toInt()
val fba: ByteArray = byteArrayOf(flag.toByte())
return fba.plus(x)
}
throw IllegalStateException("Key pair does not contain an ECDH public key")
}
// Perform a key agreement against the server's long-term ECDH public key
private fun doKeyAgreement(kp: KeyPair): KeyAgreement {
val ka: KeyAgreement = KeyAgreement.getInstance("ECDH")
ka.init(kp.private)
ka.doPhase(serverPubKey, true)
return ka
}
// Compute a message authentication code for the given data
private fun computeMAC(key: SecretKey, data: ByteArray): ByteArray {
val mac: Mac = Mac.getInstance("HmacSHA256")
mac.init(key)
return mac.doFinal(data).sliceArray(0..15)
}
// Convert an int to a 2-byte big-endian ByteArray
private fun counterBytes(counter: Int): ByteArray {
return byteArrayOf(((counter and 0xFF00) shr 8).toByte(), (counter and 0x00FF).toByte())
}
// Create a new cipher instance for symmetric crypt
private fun makeSymCipher(): Cipher {
return Cipher.getInstance("AES/CBC/PKCS5Padding")
}
private val NONCE_PADDING = ByteArray(14) { 0x0E.toByte() }
private val serverPubKey: PublicKey = readKey()
private val symCipher: Cipher = makeSymCipher()
private var cachedEphPubKey: ByteArray? = null
private var cachedAesKey: SecretKey? = null
private var cachedMacKey: SecretKey? = null
private var keyGenTime: Long = Long.MIN_VALUE
private var counter: Int = 0
private fun generateKeys() {
// ECDH
val kp: KeyPair = makeECKeys()
val ka: KeyAgreement = doKeyAgreement(kp)
val ephSecret: ByteArray = ka.generateSecret()
cachedEphPubKey = getPublicKey(kp)
// KDF
val derivedKey: ByteArray = hash(ephSecret)
cachedAesKey = SecretKeySpec(derivedKey.sliceArray(0..15), "AES")
cachedMacKey = SecretKeySpec(derivedKey.sliceArray(16..31), "HmacSHA256")
}
fun encryptPayload(data: ByteArray): String {
val keys = encryptionKeys()
val prefix: ByteArray = keys.ephPubKey.plus(keys.nonce)
// Encrypt
// IV = AES(ctr, iv=null), AES(plaintext, iv=IV) === AES(ctr_with_padding || plaintext, iv=null)
// Using the latter construction to reduce key expansions
val ivParams = IvParameterSpec(ByteArray(16)) // null IV
symCipher.init(Cipher.ENCRYPT_MODE, keys.aesKey, ivParams)
val ciphertextWithIV: ByteArray = symCipher.doFinal(keys.nonce.plus(NONCE_PADDING).plus(data))
// MAC
val size: Int = ciphertextWithIV.size - 1
val blob: ByteArray = prefix.plus(ciphertextWithIV.sliceArray(16..size))
val mac: ByteArray = computeMAC(keys.macKey, blob)
return Base64.encodeToString(blob.plus(mac), Base64.DEFAULT)
}
@Synchronized
private fun encryptionKeys(): EncryptionKeys {
if (keyGenTime <= System.currentTimeMillis() - KEY_GEN_TIME_DELTA || counter >= 65535) {
generateKeys()
keyGenTime = System.currentTimeMillis()
counter = 0
} else {
counter++
}
return EncryptionKeys(cachedEphPubKey!!, cachedAesKey!!, cachedMacKey!!, counterBytes(counter))
}
}

View file

@ -11,23 +11,14 @@ class StreetPassRecord(
@ColumnInfo(name = "v") @ColumnInfo(name = "v")
var v: Int, var v: Int,
@ColumnInfo(name = "msg")
var msg: String,
@ColumnInfo(name = "org") @ColumnInfo(name = "org")
var org: String, var org: String,
@ColumnInfo(name = "modelP") @ColumnInfo(name = "localBlob")
val modelP: String, val localBlob: String,
@ColumnInfo(name = "modelC") @ColumnInfo(name = "remoteBlob")
val modelC: String, val remoteBlob: String
@ColumnInfo(name = "rssi")
val rssi: Int,
@ColumnInfo(name = "txPower")
val txPower: Int?
) { ) {
@ -39,7 +30,7 @@ class StreetPassRecord(
var timestamp: Long = System.currentTimeMillis() var timestamp: Long = System.currentTimeMillis()
override fun toString(): String { override fun toString(): String {
return "StreetPassRecord(v=$v, msg='$msg', org='$org', modelP='$modelP', modelC='$modelC', rssi=$rssi, txPower=$txPower, id=$id, timestamp=$timestamp)" return "StreetPassRecord(v=$v, , org='$org', id=$id, timestamp=$timestamp,localBlob=$localBlob, remoteBlob=$remoteBlob)"
} }
} }

View file

@ -1,18 +1,27 @@
package au.gov.health.covidsafe.streetpass.persistence package au.gov.health.covidsafe.streetpass.persistence
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import au.gov.health.covidsafe.LocalBlobV2
import au.gov.health.covidsafe.logging.CentralLog
import au.gov.health.covidsafe.status.persistence.StatusRecord import au.gov.health.covidsafe.status.persistence.StatusRecord
import au.gov.health.covidsafe.status.persistence.StatusRecordDao import au.gov.health.covidsafe.status.persistence.StatusRecordDao
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import kotlin.concurrent.thread
const val CURRENT_DB_VERSION = 3
@Database( @Database(
entities = [StreetPassRecord::class, StatusRecord::class], entities = [StreetPassRecord::class, StatusRecord::class],
version = 2, version = CURRENT_DB_VERSION,
exportSchema = true exportSchema = true
) )
abstract class StreetPassRecordDatabase : RoomDatabase() { abstract class StreetPassRecordDatabase : RoomDatabase() {
@ -21,21 +30,59 @@ abstract class StreetPassRecordDatabase : RoomDatabase() {
abstract fun statusDao(): StatusRecordDao abstract fun statusDao(): StatusRecordDao
companion object { companion object {
private val TAG = this.javaClass.simpleName
private const val ID_COLUMN_INDEX = 0
private const val TIMESTAMP_COLUMN_INDEX = 1
private const val VERSION_COLUMN_INDEX = 2
private const val MESSAGE_COLUMN_INDEX = 3
private const val ORG_COLUMN_INDEX = 4
private const val MODELP_COLUMN_INDEX = 5
private const val MODELC_COLUMN_INDEX = 6
private const val RSSI_COLUMN_INDEX = 7
private const val TX_POWER_COLUMN_INDEX = 8
private const val EMPTY_DICT = "{}"
private val EMPTY_DICT_BYTE_ARRAY = EMPTY_DICT.toByteArray(Charsets.UTF_8)
val ENCRYPTED_EMPTY_DICT = Encryption.encryptPayload(EMPTY_DICT_BYTE_ARRAY)
const val VERSION_ONE = 1
const val VERSION_TWO = 2
const val DUMMY_DEVICE = ""
const val DUMMY_RSSI = 999
const val DUMMY_TXPOWER = 999
var migrationCallback: MigrationCallBack? = null
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile @Volatile
private var INSTANCE: StreetPassRecordDatabase? = null private var INSTANCE: StreetPassRecordDatabase? = null
fun getDatabase(context: Context): StreetPassRecordDatabase { private val CALLBACK = object : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
migrationCallback?.migrationFinished()
}
}
fun getDatabase(context: Context, migrationCallBack: MigrationCallBack? = null): StreetPassRecordDatabase {
val tempInstance = INSTANCE val tempInstance = INSTANCE
if (tempInstance != null) { if (tempInstance != null) {
return tempInstance return tempInstance
} }
this.migrationCallback = migrationCallBack
synchronized(this) { synchronized(this) {
val instance = Room.databaseBuilder( val instance = Room.databaseBuilder(
context, context,
StreetPassRecordDatabase::class.java, StreetPassRecordDatabase::class.java,
"record_database" "record_database"
) )
.addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.addCallback(CALLBACK)
.build() .build()
INSTANCE = instance INSTANCE = instance
return instance return instance
@ -58,6 +105,70 @@ abstract class StreetPassRecordDatabase : RoomDatabase() {
} }
} }
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
migrationCallback?.migrationStarted()
//adding a temporary encrypted encounters table for the migration of old data
database.execSQL("CREATE TABLE IF NOT EXISTS `encrypted_record_table` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `v` INTEGER NOT NULL, `org` TEXT NOT NULL, `localBlob` TEXT NOT NULL, `remoteBlob` TEXT NOT NULL)")
encryptExistingRecords(database)
database.execSQL("DROP TABLE `record_table`")
database.execSQL("ALTER TABLE `encrypted_record_table` RENAME TO `record_table`")
}
}
fun encryptExistingRecords(db: SupportSQLiteDatabase) {
val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
val allRecs = db.query("SELECT * FROM record_table")
CentralLog.d(TAG, "starting encryption of ${allRecs.count} records")
if (allRecs.moveToFirst()) {
do {
val contentValues = ContentValues()
val id = allRecs.getInt(ID_COLUMN_INDEX)
val version = allRecs.getInt(VERSION_COLUMN_INDEX)
val timestamp = allRecs.getLong(TIMESTAMP_COLUMN_INDEX)
val msg = allRecs.getString(MESSAGE_COLUMN_INDEX)
val org = allRecs.getString(ORG_COLUMN_INDEX)
val modelP = allRecs.getString(MODELP_COLUMN_INDEX)
val modelC = allRecs.getString(MODELC_COLUMN_INDEX)
val rssi = allRecs.getInt(RSSI_COLUMN_INDEX)
val txPower = allRecs.getInt(TX_POWER_COLUMN_INDEX)
val plainRecord = gson.toJson(EncryptedRecord(modelP, modelC, rssi, txPower, msg)).toByteArray(Charsets.UTF_8)
val remoteBlob: String = if (version == 1) {
Encryption.encryptPayload(plainRecord)
} else {
msg
}
val localBlob: String = if (version == 1) {
ENCRYPTED_EMPTY_DICT
} else {
val modelP = if (DUMMY_DEVICE == modelP) null else modelP
val modelC = if (DUMMY_DEVICE == modelC) null else modelC
val rssi = if (DUMMY_RSSI == rssi) null else rssi
val txPower = if (DUMMY_TXPOWER == txPower) null else txPower
val plainRecord = gson.toJson(LocalBlobV2(modelP, modelC, rssi, txPower)).toByteArray(Charsets.UTF_8)
Encryption.encryptPayload(plainRecord)
}
contentValues.put("v", VERSION_TWO)
contentValues.put("org", org)
contentValues.put("localBlob", localBlob)
contentValues.put("remoteBlob", remoteBlob)
contentValues.put("id", id)
contentValues.put("timestamp", timestamp)
db.insert("encrypted_record_table", CONFLICT_REPLACE, contentValues)
} while (allRecs.moveToNext())
}
CentralLog.d(TAG, "encryption done")
}
class EncryptedRecord(var modelP: String, var modelC: String, var rssi: Int, var txPower: Int?, var msg: String)
// This method will check if column exists in your table
fun isFieldExist(db: SupportSQLiteDatabase, tableName: String, fieldName: String): Boolean { fun isFieldExist(db: SupportSQLiteDatabase, tableName: String, fieldName: String): Boolean {
var isExist = false var isExist = false
val res = val res =
@ -72,5 +183,9 @@ abstract class StreetPassRecordDatabase : RoomDatabase() {
return isExist return isExist
} }
} }
}
interface MigrationCallBack {
fun migrationStarted()
fun migrationFinished()
} }

View file

@ -4,12 +4,12 @@ import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
class StreetPassRecordViewModel(record: StreetPassRecord, val number: Int) { class StreetPassRecordViewModel(record: StreetPassRecord, val number: Int) {
val version = record.v val version = record.v
val modelC = record.modelC val modelC = "Encrypted"
val modelP = record.modelP val modelP = "Encrypted"
val msg = record.msg val msg = record.remoteBlob
val timeStamp = record.timestamp val timeStamp = record.timestamp
val rssi = record.rssi val rssi = 0
val transmissionPower = record.txPower val transmissionPower = 0
val org = record.org val org = record.org
constructor(record: StreetPassRecord) : this(record, 1) constructor(record: StreetPassRecord) : this(record, 1)

View file

@ -58,6 +58,14 @@ abstract class PagerChildFragment : BaseFragment() {
sealed class UploadButtonLayout { sealed class UploadButtonLayout {
class ContinueLayout(@StringRes val buttonText: Int, val buttonListener: (() -> Unit)?) : UploadButtonLayout() class ContinueLayout(@StringRes val buttonText: Int, val buttonListener: (() -> Unit)?) : UploadButtonLayout()
class TwoChoiceContinueLayout(
@StringRes val primaryButtonText: Int,
val primaryButtonListener: (() -> Unit)?,
@StringRes val secondaryButtonText: Int,
val secondaryButtonListener: (() -> Unit)?
) : UploadButtonLayout()
class QuestionLayout(val buttonYesListener: () -> Unit, val buttonNoListener: () -> Unit) : UploadButtonLayout() class QuestionLayout(val buttonYesListener: () -> Unit, val buttonNoListener: () -> Unit) : UploadButtonLayout()
} }

View file

@ -1,10 +1,13 @@
package au.gov.health.covidsafe.ui.home package au.gov.health.covidsafe.ui.home
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
@ -25,6 +28,10 @@ import kotlinx.android.synthetic.main.fragment_home_setup_complete_header.*
import kotlinx.android.synthetic.main.fragment_home_setup_incomplete_content.* import kotlinx.android.synthetic.main.fragment_home_setup_incomplete_content.*
import pub.devrel.easypermissions.AppSettingsDialog import pub.devrel.easypermissions.AppSettingsDialog
import pub.devrel.easypermissions.EasyPermissions import pub.devrel.easypermissions.EasyPermissions
import java.text.SimpleDateFormat
import java.util.*
private const val FOURTEEN_DAYS_IN_MILLIS = 14 * 24 * 60 * 60 * 1000L
class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks { class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
@ -107,6 +114,8 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
registerBroadcast() registerBroadcast()
} }
refreshSetupCompleteOrIncompleteUi() refreshSetupCompleteOrIncompleteUi()
home_header_no_bluetooth_pairing.movementMethod = LinkMovementMethod.getInstance()
} }
override fun onPause() { override fun onPause() {
@ -132,71 +141,82 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
home_root.removeAllViews() home_root.removeAllViews()
} }
private fun refreshSetupCompleteOrIncompleteUi() { private fun isDataUploadedInPast14Days(context: Context): Boolean {
val isUploaded = context?.let { val isUploaded = Preference.isDataUploaded(context)
Preference.isDataUploaded(it)
} ?: run {
false
}
home_been_tested_button.visibility = if (isUploaded) GONE else VISIBLE
when {
!allPermissionsEnabled() -> {
home_header_setup_complete_header_uploaded.visibility = GONE
home_header_setup_complete_header_divider.visibility = GONE
home_header_setup_complete_header.setText(R.string.home_header_inactive_title)
home_header_picture_setup_complete.setImageResource(R.drawable.ic_logo_home_inactive)
home_header_help.setImageResource(R.drawable.ic_help_outline_black)
context?.let { context ->
val backGroundColor = ContextCompat.getColor(context, R.color.grey)
header_background.setBackgroundColor(backGroundColor)
header_background_overlap.setBackgroundColor(backGroundColor)
val textColor = ContextCompat.getColor(context, R.color.slack_black) if (!isUploaded) {
home_header_setup_complete_header_uploaded.setTextColor(textColor) return false
home_header_setup_complete_header.setTextColor(textColor)
} }
val millisSinceDataUploaded = System.currentTimeMillis() - Preference.getDataUploadedDateMs(context)
return (millisSinceDataUploaded < FOURTEEN_DAYS_IN_MILLIS)
}
private fun getDataUploadDateHtmlString(context: Context): String {
val dataUploadedDateMillis = Preference.getDataUploadedDateMs(context)
val format = SimpleDateFormat("d MMM yyyy", Locale.ENGLISH)
val dateString = format.format(Date(dataUploadedDateMillis))
return "<b>$dateString</b>"
}
@SuppressLint("SetTextI18n")
private fun refreshSetupCompleteOrIncompleteUi() {
context?.let {
val isAllPermissionsEnabled = allPermissionsEnabled()
val isDataUploadedInPast14Days = isDataUploadedInPast14Days(it)
val line1 = it.getString(
if (isAllPermissionsEnabled) {
R.string.home_header_active_title
} else {
R.string.home_header_inactive_title
}
)
val line2 = if (isDataUploadedInPast14Days) {
"<br/><br/>" + it.getString(R.string.home_header_uploaded_on_date, getDataUploadDateHtmlString(it))
} else {
""
}
val line3 = "<br/>" + it.getString(
if (isAllPermissionsEnabled) {
R.string.home_header_active_no_action_required
} else {
R.string.home_header_inactive_check_your_permissions
}
)
val headerHtmlText = "$line1$line2$line3"
home_header_setup_complete_header.text =
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Html.fromHtml(headerHtmlText, Html.FROM_HTML_MODE_COMPACT)
} else {
Html.fromHtml(headerHtmlText)
}
if (isAllPermissionsEnabled) {
home_header_picture_setup_complete.setAnimation("spinner_home.json")
content_setup_incomplete_group.visibility = GONE
ContextCompat.getColor(it, R.color.lighter_green).let { bgColor ->
header_background.setBackgroundColor(bgColor)
header_background_overlap.setBackgroundColor(bgColor)
}
} else {
home_header_picture_setup_complete.setImageResource(R.drawable.ic_logo_home_inactive)
content_setup_incomplete_group.visibility = VISIBLE content_setup_incomplete_group.visibility = VISIBLE
updateBlueToothStatus() updateBlueToothStatus()
updatePushNotificationStatus() updatePushNotificationStatus()
updateBatteryOptimizationStatus() updateBatteryOptimizationStatus()
updateLocationStatus() updateLocationStatus()
} ContextCompat.getColor(it, R.color.grey).let { bgColor ->
isUploaded -> { header_background.setBackgroundColor(bgColor)
home_header_setup_complete_header_uploaded.visibility = VISIBLE header_background_overlap.setBackgroundColor(bgColor)
home_header_setup_complete_header_divider.visibility = VISIBLE
home_header_setup_complete_header.setText(R.string.home_header_active_title)
home_header_picture_setup_complete.setImageResource(R.drawable.ic_logo_home_uploaded)
home_header_picture_setup_complete.setAnimation("spinner_home_upload_complete.json")
home_header_help.setImageResource(R.drawable.ic_help_outline_white)
content_setup_incomplete_group.visibility = GONE
context?.let { context ->
val backGroundColor = ContextCompat.getColor(context, R.color.dark_green)
header_background.setBackgroundColor(backGroundColor)
header_background_overlap.setBackgroundColor(backGroundColor)
val textColor = ContextCompat.getColor(context, R.color.white)
home_header_setup_complete_header_uploaded.setTextColor(textColor)
home_header_setup_complete_header.setTextColor(textColor)
} }
} }
else -> { home_been_tested_button.visibility = if (isDataUploadedInPast14Days) GONE else VISIBLE
home_header_setup_complete_header_uploaded.visibility = GONE
home_header_setup_complete_header_divider.visibility = GONE
home_header_setup_complete_header.setText(R.string.home_header_active_title)
home_header_help.setImageResource(R.drawable.ic_help_outline_black)
home_header_picture_setup_complete.setAnimation("spinner_home.json")
content_setup_incomplete_group.visibility = GONE
context?.let { context ->
val backGroundColor = ContextCompat.getColor(context, R.color.lighter_green)
header_background.setBackgroundColor(backGroundColor)
header_background_overlap.setBackgroundColor(backGroundColor)
val textColor = ContextCompat.getColor(context, R.color.slack_black)
home_header_setup_complete_header_uploaded.setTextColor(textColor)
home_header_setup_complete_header.setTextColor(textColor)
}
}
} }
} }

View file

@ -65,11 +65,29 @@ class OnboardingActivity : FragmentActivity(), HasBlockingState, PagerContainer
} }
override fun refreshButton(updateButtonLayout: UploadButtonLayout) { override fun refreshButton(updateButtonLayout: UploadButtonLayout) {
if (updateButtonLayout is UploadButtonLayout.ContinueLayout) { when (updateButtonLayout) {
is UploadButtonLayout.ContinueLayout -> {
onboarding_next.setText(updateButtonLayout.buttonText) onboarding_next.setText(updateButtonLayout.buttonText)
onboarding_next.setOnClickListener { onboarding_next.setOnClickListener {
updateButtonLayout.buttonListener?.invoke() updateButtonLayout.buttonListener?.invoke()
} }
onboarding_next_secondary.visibility = GONE
}
is UploadButtonLayout.TwoChoiceContinueLayout -> {
onboarding_next.setText(updateButtonLayout.primaryButtonText)
onboarding_next.setOnClickListener {
updateButtonLayout.primaryButtonListener?.invoke()
}
onboarding_next_secondary.setText(updateButtonLayout.secondaryButtonText)
onboarding_next_secondary.setOnClickListener {
updateButtonLayout.secondaryButtonListener?.invoke()
}
onboarding_next_secondary.visibility = VISIBLE
}
} }
} }

View file

@ -0,0 +1,68 @@
package au.gov.health.covidsafe.ui.onboarding.fragment.permission
import android.bluetooth.BluetoothAdapter
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import au.gov.health.covidsafe.R
import au.gov.health.covidsafe.ui.PagerChildFragment
import au.gov.health.covidsafe.ui.UploadButtonLayout
import kotlinx.android.synthetic.main.fragment_permission.root
import kotlinx.android.synthetic.main.fragment_permission_device_name.*
class PermissionDeviceNameFragment : PagerChildFragment() {
override val navigationIcon: Int? = R.drawable.ic_up
override var stepProgress: Int? = 5
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
: View? = inflater.inflate(R.layout.fragment_permission_device_name, container, false)
override fun onResume() {
super.onResume()
context?.let {
val deviceName = "<b>${BluetoothAdapter.getDefaultAdapter()?.name}</b>"
val paragraph1 = it.getString(R.string.change_device_name_content_line_1, deviceName)
val paragraph2 = "<br/><br/>" + it.getString(R.string.change_device_name_content_line_2)
val paragraphs = "$paragraph1$paragraph2"
change_device_name_content.text =
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Html.fromHtml(paragraphs, Html.FROM_HTML_MODE_COMPACT)
} else {
Html.fromHtml(paragraphs)
}
}
}
private fun navigateToNextPage() {
navigateTo(R.id.action_permissionDeviceNameFragment_to_permissionSuccessFragment)
}
override fun getUploadButtonLayout() = UploadButtonLayout.TwoChoiceContinueLayout(
R.string.change_device_name_primary_action,
{
BluetoothAdapter.getDefaultAdapter()?.name = change_device_name_text_box.text.toString()
navigateToNextPage()
},
R.string.change_device_name_secondary_action,
{
navigateToNextPage()
}
)
override fun updateButtonState() {
enableContinueButton()
}
override fun onDestroyView() {
super.onDestroyView()
root.removeAllViews()
}
}

View file

@ -33,7 +33,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb
} }
override val navigationIcon: Int? = R.drawable.ic_up override val navigationIcon: Int? = R.drawable.ic_up
override var stepProgress: Int? = 5 override var stepProgress: Int? = 4
private var navigationStarted = false private var navigationStarted = false
@ -58,7 +58,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb
private fun navigateToNextPage() { private fun navigateToNextPage() {
navigationStarted = false navigationStarted = false
if (hasAllPermissionsAndBluetoothOn()) { if (hasAllPermissionsAndBluetoothOn()) {
navigateTo(R.id.action_permissionFragment_to_permissionSuccessFragment) navigateTo(R.id.action_permissionFragment_to_permissionDeviceNameFragment )
} else { } else {
navigateToMainActivity() navigateToMainActivity()
} }

View file

@ -2,23 +2,30 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.permissionsuccess
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_permission_success.*
import au.gov.health.covidsafe.HomeActivity import au.gov.health.covidsafe.HomeActivity
import au.gov.health.covidsafe.R import au.gov.health.covidsafe.R
import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.PagerChildFragment
import au.gov.health.covidsafe.ui.UploadButtonLayout import au.gov.health.covidsafe.ui.UploadButtonLayout
import kotlinx.android.synthetic.main.fragment_permission_success.*
class PermissionSuccessFragment : PagerChildFragment() { class PermissionSuccessFragment : PagerChildFragment() {
override val navigationIcon: Int? = R.drawable.ic_up override val navigationIcon: Int? = R.drawable.ic_up
override var stepProgress: Int? = 5 override var stepProgress: Int? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?)
: View? = inflater.inflate(R.layout.fragment_permission_success, container, false) : View? = inflater.inflate(R.layout.fragment_permission_success, container, false)
override fun onResume() {
super.onResume()
permission_success_content.movementMethod = LinkMovementMethod.getInstance()
}
private fun navigateToNextPage() { private fun navigateToNextPage() {
val intent = Intent(context, HomeActivity::class.java) val intent = Intent(context, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK

View file

@ -0,0 +1,85 @@
package au.gov.health.covidsafe.ui.splash
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import au.gov.health.covidsafe.streetpass.persistence.CURRENT_DB_VERSION
import au.gov.health.covidsafe.streetpass.persistence.MigrationCallBack
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordDatabase
import kotlinx.coroutines.*
class SplashViewModel(context: Context) : ViewModel() {
private val SPLASH_TIME: Long = 2000
val splashNavigationLiveData = MutableLiveData<SplashNavigationEvent>(SplashNavigationEvent.ShowSplashScreen)
private var migrated = false
private var splashScreenPassed = false
val db = StreetPassRecordDatabase.getDatabase(context, object : MigrationCallBack {
override fun migrationStarted() {
migrated = false
if (splashScreenPassed) {
viewModelScope.launch {
splashNavigationLiveData.value = SplashNavigationEvent.ShowMigrationScreen
}
}
}
override fun migrationFinished() {
migrated = true
if (splashScreenPassed) {
viewModelScope.launch {
splashNavigationLiveData.value = SplashNavigationEvent.GoToNextScreen
}
}
}
})
fun setupUI() {
this.viewModelScope.launch {
val splashScreenCoroutine = async(context = Dispatchers.IO) {
delay(SPLASH_TIME)
viewModelScope.launch {
if (migrated) {
splashNavigationLiveData.value = SplashNavigationEvent.GoToNextScreen
} else {
splashNavigationLiveData.value = SplashNavigationEvent.ShowMigrationScreen
}
splashScreenPassed = true
}
}
val migratingCoroutine = async(context = Dispatchers.IO) {
val readableDatabase = db.openHelper.readableDatabase
migrated = !readableDatabase.needUpgrade(CURRENT_DB_VERSION)
viewModelScope.launch {
if (migrated && splashScreenPassed) {
splashNavigationLiveData.value = SplashNavigationEvent.GoToNextScreen
} else if (!migrated) {
splashNavigationLiveData.value = SplashNavigationEvent.ShowMigrationScreen
}
}
}
splashScreenCoroutine.join()
migratingCoroutine.join()
}
}
fun release() {
StreetPassRecordDatabase.migrationCallback = null
this.viewModelScope.cancel()
}
}
class SplashViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = SplashViewModel(context) as T
}
sealed class SplashNavigationEvent {
object ShowSplashScreen : SplashNavigationEvent()
object ShowMigrationScreen : SplashNavigationEvent()
object GoToNextScreen : SplashNavigationEvent()
}

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import au.gov.health.covidsafe.R import au.gov.health.covidsafe.R
import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.PagerChildFragment
import au.gov.health.covidsafe.ui.UploadButtonLayout import au.gov.health.covidsafe.ui.UploadButtonLayout
@ -17,6 +18,13 @@ class UploadFinishedFragment : PagerChildFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_upload_finished, container, false) inflater.inflate(R.layout.fragment_upload_finished, container, false)
override fun onResume() {
super.onResume()
// set accessibility focus to the title
header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.action_upload_done) { override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(R.string.action_upload_done) {
activity?.onBackPressed() activity?.onBackPressed()
} }

View file

@ -4,10 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import au.gov.health.covidsafe.R import au.gov.health.covidsafe.R
import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.PagerChildFragment
import au.gov.health.covidsafe.ui.UploadButtonLayout import au.gov.health.covidsafe.ui.UploadButtonLayout
import kotlinx.android.synthetic.main.fragment_upload_page_4.* import kotlinx.android.synthetic.main.fragment_upload_initial.*
import kotlinx.android.synthetic.main.fragment_upload_page_4.root
class UploadInitialFragment : PagerChildFragment() { class UploadInitialFragment : PagerChildFragment() {
@ -19,6 +21,13 @@ class UploadInitialFragment : PagerChildFragment() {
inflater.inflate(R.layout.fragment_upload_initial, container, false) inflater.inflate(R.layout.fragment_upload_initial, container, false)
override fun onResume() {
super.onResume()
// set accessibility focus to the title
upload_initial_headline.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
override fun updateButtonState() { override fun updateButtonState() {
enableContinueButton() enableContinueButton()
} }

View file

@ -6,6 +6,7 @@ import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import au.gov.health.covidsafe.R import au.gov.health.covidsafe.R
import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.PagerChildFragment
import au.gov.health.covidsafe.ui.UploadButtonLayout import au.gov.health.covidsafe.ui.UploadButtonLayout
@ -30,7 +31,11 @@ class UploadStepFourFragment : PagerChildFragment() {
upload_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked -> upload_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
updateButtonState() updateButtonState()
} }
// set accessibility focus to the title
header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
} }
override fun updateButtonState() { override fun updateButtonState() {
if (upload_consent_checkbox.isChecked) { if (upload_consent_checkbox.isChecked) {
enableContinueButton() enableContinueButton()
@ -42,7 +47,7 @@ class UploadStepFourFragment : PagerChildFragment() {
override val navigationIcon: Int? = R.drawable.ic_up override val navigationIcon: Int? = R.drawable.ic_up
override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout( override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(
R.string.action_agree) { R.string.action_continue) {
navigateToVerifyUploadPin() navigateToVerifyUploadPin()
} }

View file

@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import au.gov.health.covidsafe.R import au.gov.health.covidsafe.R
import au.gov.health.covidsafe.ui.PagerChildFragment import au.gov.health.covidsafe.ui.PagerChildFragment
@ -46,6 +47,9 @@ class VerifyUploadPinFragment : PagerChildFragment() {
updateButtonState() updateButtonState()
hideInvalidOtp() hideInvalidOtp()
} }
// set accessibility focus to the title
header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
} }
override fun onPause() { override fun onPause() {

View file

@ -5,6 +5,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="@dimen/keyline_7"
tools:context=".ui.onboarding.OnboardingActivity"> tools:context=".ui.onboarding.OnboardingActivity">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
@ -69,7 +70,18 @@
android:layout_marginStart="@dimen/keyline_5" android:layout_marginStart="@dimen/keyline_5"
android:layout_marginTop="@dimen/keyline_5" android:layout_marginTop="@dimen/keyline_5"
android:layout_marginEnd="@dimen/keyline_5" android:layout_marginEnd="@dimen/keyline_5"
android:layout_marginBottom="@dimen/keyline_7"
android:text="@string/intro_button" /> android:text="@string/intro_button" />
<TextView
android:id="@+id/onboarding_next_secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/keyline_5"
android:layout_marginTop="@dimen/keyline_5"
android:layout_marginEnd="@dimen/keyline_5"
android:gravity="center_horizontal"
android:text="@string/change_device_name_new_device_name"
android:textAppearance="?textAppearanceBody1"
android:textColor="@color/dark_green"/>
</LinearLayout> </LinearLayout>

View file

@ -21,7 +21,26 @@
app:layout_constraintHeight_percent="0.33" app:layout_constraintHeight_percent="0.33"
/> />
<ImageView <androidx.appcompat.widget.AppCompatTextView
style="?textAppearanceBody2"
android:id="@+id/splash_migration_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_2"
android:layout_marginLeft="@dimen/keyline_2"
android:layout_marginRight="@dimen/keyline_2"
android:text="@string/migration_in_progress"
android:gravity="center"
android:visibility="gone"
app:layout_constraintWidth_percent="0.75"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/splash_screen_crest"
app:layout_constraintBottom_toTopOf="@+id/splash_screen_logo"
tools:text="@string/migration_in_progress"
/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/splash_screen_logo" android:id="@+id/splash_screen_logo"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
@ -32,8 +51,11 @@
app:layout_constraintBottom_toTopOf="@+id/help_stop_covid" app:layout_constraintBottom_toTopOf="@+id/help_stop_covid"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/splash_screen_crest" app:layout_constraintTop_toBottomOf="@+id/splash_migration_text"
app:layout_constraintVertical_chainStyle="spread" /> app:layout_constraintVertical_chainStyle="spread"
app:lottie_autoPlay="false"
app:lottie_loop="true"
app:lottie_speed="1"/>
<ImageView <ImageView
android:id="@+id/help_stop_covid" android:id="@+id/help_stop_covid"

View file

@ -25,22 +25,6 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_unfold_less_black_24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_unfold_more_black_24dp" />
</LinearLayout> </LinearLayout>
@ -54,15 +38,6 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/plot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_arrow_forward_black_24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -89,13 +64,22 @@
</LinearLayout> </LinearLayout>
<TextView <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/info" android:id="@+id/shareDatabase"
android:textSize="12sp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_home_share"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -17,6 +17,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topInset" app:layout_constraintTop_toBottomOf="@+id/topInset"
app:layout_constraintWidth_default="wrap" app:layout_constraintWidth_default="wrap"
app:navigationContentDescription="@string/navigation_back_button_content_description"
app:navigationIcon="@drawable/ic_up" app:navigationIcon="@drawable/ic_up"
app:title="@string/title_help"> app:title="@string/title_help">

View file

@ -26,39 +26,6 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_header_picture_setup_complete" /> app:layout_constraintTop_toBottomOf="@+id/home_header_picture_setup_complete" />
<TextView
android:id="@+id/home_header_setup_complete_header_uploaded"
style="?textAppearanceBody1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:gravity="center_horizontal"
android:paddingLeft="@dimen/keyline_5"
android:paddingRight="@dimen/keyline_5"
android:text="@string/home_header_uploaded_title"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_header_picture_setup_complete_space" />
<View
android:id="@+id/home_header_setup_complete_header_divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="@dimen/keyline_4"
android:layout_marginBottom="@dimen/keyline_4"
android:background="@color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_header_setup_complete_header_uploaded"
app:layout_constraintWidth_percent="0.5" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/home_header_setup_complete_header_uploaded_2"
style="?textAppearanceBody1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="home_header_setup_complete_header_divider,home_header_picture_setup_complete_space" />
<TextView <TextView
android:id="@+id/home_header_setup_complete_header" android:id="@+id/home_header_setup_complete_header"
style="?textAppearanceBody1" style="?textAppearanceBody1"
@ -72,13 +39,26 @@
android:text="@string/home_header_active_title" android:text="@string/home_header_active_title"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_header_setup_complete_header_uploaded_2" /> app:layout_constraintTop_toBottomOf="@+id/home_header_picture_setup_complete_space" />
<TextView
android:id="@+id/home_header_no_bluetooth_pairing"
style="?textAppearanceBody1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
android:paddingLeft="@dimen/keyline_5"
android:paddingRight="@dimen/keyline_5"
android:gravity="center_horizontal"
android:text="@string/home_header_no_pairing"
android:textColorLink="@color/slack_black"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/home_header_setup_complete_header" />
<Space <Space
android:id="@+id/home_header_label_setup_complete_subtitle_bottom_space" android:id="@+id/home_header_label_setup_complete_subtitle_bottom_space"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/keyline_7" android:layout_height="@dimen/keyline_0"
app:layout_constraintTop_toBottomOf="@+id/home_header_setup_complete_header" /> app:layout_constraintTop_toBottomOf="@+id/home_header_no_bluetooth_pairing" />
</merge> </merge>

View file

@ -17,6 +17,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/keyline_4" android:layout_marginLeft="@dimen/keyline_4"
android:layout_marginRight="@dimen/keyline_4" android:layout_marginRight="@dimen/keyline_4"
android:layout_marginTop="@dimen/keyline_7"
app:layout_constraintTop_toBottomOf="@+id/header_barrier" app:layout_constraintTop_toBottomOf="@+id/header_barrier"
card_view:cardBackgroundColor="@color/white" card_view:cardBackgroundColor="@color/white"
card_view:cardCornerRadius="6dp" card_view:cardCornerRadius="6dp"
@ -28,7 +29,6 @@
android:id="@+id/home_setup_incomplete_permissions_group" android:id="@+id/home_setup_incomplete_permissions_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:fillViewport="true"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/keyline_5"
android:paddingEnd="@dimen/keyline_5"
android:paddingBottom="@dimen/keyline_7">
<ImageView
android:id="@+id/permission_picture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/ic_permission"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/change_device_name_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_6"
android:text="@string/change_device_name_headline"
android:contentDescription="@string/change_device_name_headline_content_description"
android:textAppearance="?textAppearanceHeadline2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/permission_picture" />
<TextView
android:id="@+id/change_device_name_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_4"
android:text="@string/change_device_name_content_line_1"
android:textAppearance="?textAppearanceBody1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/change_device_name_headline" />
<TextView
android:id="@+id/change_device_name_new_device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_6"
android:text="@string/change_device_name_new_device_name"
android:textAppearance="?textAppearanceBody1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/change_device_name_content" />
<EditText
android:id="@+id/change_device_name_text_box"
android:layout_width="0dp"
android:layout_height="@dimen/text_field_height"
android:layout_marginTop="@dimen/keyline_1"
android:background="@drawable/edittext_modified_states"
android:maxLines="1"
android:paddingStart="@dimen/keyline_1"
android:paddingEnd="@dimen/keyline_1"
android:singleLine="true"
android:textColor="@color/slack_black"
android:textColorHighlight="@color/dark_cerulean_3"
android:textCursorDrawable="@null"
android:textSize="@dimen/text_body_small"
android:text="@string/change_device_name_default_device_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/change_device_name_new_device_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -40,6 +40,7 @@
android:paddingStart="@dimen/keyline_5" android:paddingStart="@dimen/keyline_5"
android:paddingEnd="@dimen/keyline_5" android:paddingEnd="@dimen/keyline_5"
android:text="@string/permission_success_content" android:text="@string/permission_success_content"
android:textColorLink="?attr/colorPrimary"
android:textAppearance="?textAppearanceBody1" android:textAppearance="?textAppearanceBody1"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -33,6 +33,7 @@
android:layout_marginEnd="@dimen/keyline_5" android:layout_marginEnd="@dimen/keyline_5"
android:textAppearance="?textAppearanceHeadline2" android:textAppearance="?textAppearanceHeadline2"
android:text="@string/upload_finished_header" android:text="@string/upload_finished_header"
android:contentDescription="@string/upload_finished_header_content_description"
app:layout_constraintBottom_toTopOf="@+id/subHeader" app:layout_constraintBottom_toTopOf="@+id/subHeader"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -26,6 +26,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyline_6" android:layout_marginTop="@dimen/keyline_6"
android:text="@string/upload_step_1_header" android:text="@string/upload_step_1_header"
android:contentDescription="@string/upload_step_1_header_content_description"
android:textAppearance="?textAppearanceHeadline2" android:textAppearance="?textAppearanceHeadline2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -15,7 +15,8 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_up" /> app:navigationIcon="@drawable/ic_up"
app:navigationContentDescription="@string/navigation_back_button_content_description"/>
<fragment <fragment
android:id="@+id/fragment_nav_host_upload" android:id="@+id/fragment_nav_host_upload"

View file

@ -18,7 +18,9 @@
android:layout_marginTop="@dimen/keyline_4" android:layout_marginTop="@dimen/keyline_4"
android:layout_marginEnd="@dimen/keyline_5" android:layout_marginEnd="@dimen/keyline_5"
android:textAppearance="?textAppearanceHeadline2" android:textAppearance="?textAppearanceHeadline2"
android:text="@string/upload_step_4_header"/> android:text="@string/upload_step_4_header"
android:contentDescription="@string/upload_step_4_header_content_description"
/>
<TextView <TextView
android:id="@+id/subHeader" android:id="@+id/subHeader"

View file

@ -20,6 +20,7 @@
android:layout_marginTop="@dimen/keyline_4" android:layout_marginTop="@dimen/keyline_4"
android:layout_marginEnd="@dimen/keyline_5" android:layout_marginEnd="@dimen/keyline_5"
android:text="@string/upload_step_verify_pin_header" android:text="@string/upload_step_verify_pin_header"
android:contentDescription="@string/upload_step_verify_pin_header_content_description"
app:layout_constraintBottom_toTopOf="@+id/subHeader" app:layout_constraintBottom_toTopOf="@+id/subHeader"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -104,7 +104,21 @@
android:label="PermissionFragment" android:label="PermissionFragment"
tools:layout="@layout/fragment_permission"> tools:layout="@layout/fragment_permission">
<action <action
android:id="@+id/action_permissionFragment_to_permissionSuccessFragment" android:id="@+id/action_permissionFragment_to_permissionDeviceNameFragment"
app:destination="@id/permissionDeviceNameFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/permissionDeviceNameFragment"
android:name="au.gov.health.covidsafe.ui.onboarding.fragment.permission.PermissionDeviceNameFragment"
android:label="PermissionDeviceNameFragment"
tools:layout="@layout/fragment_permission_device_name">
<action
android:id="@+id/action_permissionDeviceNameFragment_to_permissionSuccessFragment"
app:destination="@id/permissionSuccessFragment" app:destination="@id/permissionSuccessFragment"
app:enterAnim="@anim/slide_in_right" app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left" app:exitAnim="@anim/slide_out_left"

View file

@ -17,13 +17,13 @@
<string name="share_this_app_content">Join me in stopping the spread of COVID-19! Download COVIDSafe, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives https://covidsafe.gov.au</string> <string name="share_this_app_content">Join me in stopping the spread of COVID-19! Download COVIDSafe, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives https://covidsafe.gov.au</string>
<string name="share_this_app_content_html">Join me in stopping the spread of COVID-19! Download <a href="https://covidsafe.gov.au">COVIDSafe</a>>, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives <a href="https://covidsafe.gov.au">covidsafe.gov.au</a></string> <string name="share_this_app_content_html">Join me in stopping the spread of COVID-19! Download <a href="https://covidsafe.gov.au">COVIDSafe</a>>, an app from the Australian Government. #COVID19 #coronavirusaustralia #stayhomesavelives <a href="https://covidsafe.gov.au">covidsafe.gov.au</a></string>
<string name="service_ok_title">COVIDSafe is active</string>
<string name="service_ok_body"> Keep COVIDSafe active when you leave home or are in public places.</string>
<string name="service_not_ok_title">COVIDSafe is not active</string> <string name="service_not_ok_title">COVIDSafe is not active</string>
<string name="service_not_ok_body">Make sure COVIDSafe is active before you leave home or when in public places.</string> <string name="service_not_ok_body">Make sure COVIDSafe is active before you leave home or when in public places.</string>
<string name="service_not_ok_action">Check app now</string> <string name="service_not_ok_action">Check app now</string>
<!-- Splash Screen -->
<string name="migration_in_progress"> COVIDSafe update in progress. \n\n Please make sure you phone is not switched off until the update is complete.</string>
<!-- OnBoarding Intro --> <!-- OnBoarding Intro -->
<string name="intro_headline">Together we can stop the spread of COVID-19</string> <string name="intro_headline">Together we can stop the spread of COVID-19</string>
<string name="intro_headline_content_description">Heading, Together we can stop the spread of COVID-19</string> <string name="intro_headline_content_description">Heading, Together we can stop the spread of COVID-19</string>
@ -40,7 +40,7 @@
<string name="how_it_works_terms_conditions">Read our <a href="https://www.covidsafe.gov.au/terms-and-conditions">Terms and conditions</a></string> <string name="how_it_works_terms_conditions">Read our <a href="https://www.covidsafe.gov.au/terms-and-conditions">Terms and conditions</a></string>
<string name="how_it_works_button">Next</string> <string name="how_it_works_button">Next</string>
<!-- OnBoarding Data Privcay --> <!-- OnBoarding Data Privacy -->
<string name="data_privacy_headline">Registration and privacy</string> <string name="data_privacy_headline">Registration and privacy</string>
<string name="data_privacy_headline_content_description">Heading, Registration and privacy</string> <string name="data_privacy_headline_content_description">Heading, Registration and privacy</string>
<string name="data_privacy_content">It is important that you read the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> before you register for COVIDSafe.\n\nIf you are under 16 years of age, your parent/guardian must also read the <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a>.\n\nUse of COVIDSafe is completely voluntary. You can install or delete the application at any time. If you delete COVIDSafe, <a href="https://www.covidsafe.gov.au/help-topics.html">you may also ask for your information</a> to be deleted from the secure server.\n\nTo register for COVIDSafe, you will need to enter a name, mobile number, age range and postcode.\n\nInformation you submit when you register, and information about your use of COVIDSafe will be collected and stored on a highly secure server.\n\nCOVIDSafe will not collect your location information.\n\nCOVIDSafe will note the time of contact and an anonymous ID code of other COVIDSafe users you come into contact with.\n\nOther COVIDSafe users you come into contact with will record an anonymous ID code and the time of contact with your device.\n\nIf another user tests positive to COVID-19, they may upload their contact information and a state or territory health official may contact you for tracing purposes.\n\nYour registration details will only be used or disclosed for contact tracing and for the proper and lawful functioning of COVIDSafe.\n\nMore information is available at the <a href="https://www.health.gov.au/">Australian Government Department of Health website</a>.\n\nSee the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details about your rights about your information and how it will be handled and shared.</string> <string name="data_privacy_content">It is important that you read the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> before you register for COVIDSafe.\n\nIf you are under 16 years of age, your parent/guardian must also read the <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a>.\n\nUse of COVIDSafe is completely voluntary. You can install or delete the application at any time. If you delete COVIDSafe, <a href="https://www.covidsafe.gov.au/help-topics.html">you may also ask for your information</a> to be deleted from the secure server.\n\nTo register for COVIDSafe, you will need to enter a name, mobile number, age range and postcode.\n\nInformation you submit when you register, and information about your use of COVIDSafe will be collected and stored on a highly secure server.\n\nCOVIDSafe will not collect your location information.\n\nCOVIDSafe will note the time of contact and an anonymous ID code of other COVIDSafe users you come into contact with.\n\nOther COVIDSafe users you come into contact with will record an anonymous ID code and the time of contact with your device.\n\nIf another user tests positive to COVID-19, they may upload their contact information and a state or territory health official may contact you for tracing purposes.\n\nYour registration details will only be used or disclosed for contact tracing and for the proper and lawful functioning of COVIDSafe.\n\nMore information is available at the <a href="https://www.health.gov.au/">Australian Government Department of Health website</a>.\n\nSee the COVIDSafe <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details about your rights about your information and how it will be handled and shared.</string>
@ -57,11 +57,9 @@
<!-- OnBoarding Personal details --> <!-- OnBoarding Personal details -->
<string name="personal_details_headline">Enter your details</string> <string name="personal_details_headline">Enter your details</string>
<string name="personal_details_headline_content_description">Heading, Enter your details</string> <string name="personal_details_headline_content_description">Heading, Enter your details</string>
<string name="personal_details_name_title">Full name (First, Last)</string> <string name="personal_details_name_title">Full name</string>
<string name="personal_details_name_hint">Firstname Lastname</string> <string name="personal_details_name_content_description">Enter full name</string>
<string name="personal_details_name_content_description">Enter full name (First, Last)</string> <string name="personal_details_age_title">Age range (select)</string>
<string name="personal_details_age_title">Age (select)</string>
<string name="personal_details_age_hint">Age range</string>
<string name="personal_details_age_content_description">Select age range</string> <string name="personal_details_age_content_description">Select age range</string>
<string name="personal_details_post_code">Postcode</string> <string name="personal_details_post_code">Postcode</string>
<string name="personal_details_post_code_hint">e.g. 2000</string> <string name="personal_details_post_code_hint">e.g. 2000</string>
@ -119,20 +117,40 @@
<!-- OnBoarding Permission --> <!-- OnBoarding Permission -->
<string name="permission_headline">App permissions</string> <string name="permission_headline">App permissions</string>
<string name="permission_content">COVIDSafe needs Bluetooth® and notifications enabled to work.\n\nSelect Proceed to enable:\n\n1. Bluetooth®\n\n2. Location Permissions\n\n3. Battery Optimiser\n\n\nAndroid needs Location Permissions for Bluetooth® to work.</string> <string name="permission_content">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.</string>
<string name="permission_button">Proceed</string> <string name="permission_button">Proceed</string>
<string name="permission_location_rationale">Android requires location access to enable Bluetooth® functions for COVIDSafe. COVIDSafe cannot work properly without it</string> <string name="permission_location_rationale">Android requires location access to enable Bluetooth® functions for COVIDSafe. COVIDSafe cannot work properly without it</string>
<!-- Onboarding Change Device Name -->
<string name="change_device_name_headline">Your device name</string>
<string name="change_device_name_headline_content_description">Heading, Your device name</string>
<string name="change_device_name_content_line_1">The current name of your device is %s.</string>
<string name="change_device_name_content_line_2">Other Bluetooth® devices around you will be able to see this name. You may like to consider making the device name anonymous.</string>
<string name="change_device_name_new_device_name">New device name</string>
<string name="change_device_name_default_device_name">Android phone</string>
<string name="change_device_name_primary_action">Change and continue</string>
<string name="change_device_name_secondary_action">Skip and keep as it is</string>
<!-- OnBoarding Permission Success--> <!-- OnBoarding Permission Success-->
<string name="permission_success_headline">You\'ve successfully registered</string> <string name="permission_success_headline">You\'ve successfully registered</string>
<string name="permission_success_content">1. Keep your phone with you when you leave home.\n\n2. Keep the app running.\n\n3. Keep Bluetooth® on.</string> <string name="permission_success_content">
1. When you leave home, keep your phone with you and make sure COVIDSafe is active.\n\n
2. Bluetooth® should be kept ON.\n\n
3. Battery optimisation should be OFF.\n\n
4. COVIDSafe does not send pairing requests. <a href="https://www.covidsafe.gov.au/help-topics.html#bluetooth-pairing-request">Learn more</a>.
</string>
<string name="permission_success_warning">Keep push notifications on for COVIDSafe so we can notify you quickly if the app isn\'t working properly.</string> <string name="permission_success_warning">Keep push notifications on for COVIDSafe so we can notify you quickly if the app isn\'t working properly.</string>
<string name="permission_success_button">Continue</string> <string name="permission_success_button">Continue</string>
<!-- Home --> <!-- Home -->
<string name="home_header_active_title">COVIDSafe is active.\nNo further action is required.</string> <string name="home_header_active_title">COVIDSafe is active.</string>
<string name="home_header_inactive_title">COVIDSafe is not active.\nCheck your permissions.</string> <string name="home_header_active_no_action_required">No further action required.</string>
<string name="home_header_uploaded_title">Thank you for helping stop the spread of COVID-19. Your information has been uploaded.</string> <string name="home_header_inactive_title">COVIDSafe is not active.</string>
<string name="home_header_inactive_check_your_permissions">Check your permissions.</string>
<string name="home_header_uploaded_on_date">Your information was uploaded on %s.</string>
<string name="home_header_no_pairing">COVIDSafe does not send <a href="https://www.covidsafe.gov.au/help-topics.html#bluetooth-pairing-request">pairing requests</a>.</string>
<string name="home_bluetooth_permission">Bluetooth®: %s</string> <string name="home_bluetooth_permission">Bluetooth®: %s</string>
<string name="home_non_battery_optimization_permission">Battery optimization: %s</string> <string name="home_non_battery_optimization_permission">Battery optimization: %s</string>
<string name="home_location_permission">Location: %s</string> <string name="home_location_permission">Location: %s</string>
@ -168,7 +186,6 @@
<string name="home_set_complete_external_link_help_topics_title">Help topics</string> <string name="home_set_complete_external_link_help_topics_title">Help topics</string>
<string name="home_set_complete_external_link_help_topics_content">If you have issues or questions about the app.</string> <string name="home_set_complete_external_link_help_topics_content">If you have issues or questions about the app.</string>
<string name="home_version_number">Version Number:%s</string> <string name="home_version_number">Version Number:%s</string>
<string name="action_report_an_issue">Report an issue</string> <string name="action_report_an_issue">Report an issue</string>
@ -177,22 +194,24 @@
<string name="upload_answer_no">No</string> <string name="upload_answer_no">No</string>
<string name="upload_step_1_header">Is a health official asking you to upload your information?</string> <string name="upload_step_1_header">Is a health official asking you to upload your information?</string>
<string name="upload_step_1_header_content_description">Heading, Is a health official asking you to upload your information?</string>
<string name="upload_step_1_body">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 youll need to provide consent to upload your information. </string> <string name="upload_step_1_body">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 youll need to provide consent to upload your information. </string>
<string name="upload_step_4_header">Upload consent</string> <string name="upload_step_4_header">Upload consent</string>
<string name="upload_step_4_header_content_description">Heading, Upload consent</string>
<string name="upload_step_4_sub_header">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 <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details.</string> <string name="upload_step_4_sub_header">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 <a href="https://www.health.gov.au/using-our-websites/privacy/privacy-notice-for-covidsafe-app">privacy policy</a> for further details.</string>
<string name='upload_consent'>I consent to upload my information</string> <string name='upload_consent'>I consent to upload my information</string>
<string name="upload_step_verify_pin_header">Upload your information</string> <string name="upload_step_verify_pin_header">Upload your information</string>
<string name="upload_step_verify_pin_header_content_description">Heading, Upload your information</string>
<string name="upload_step_verify_pin_sub_header">A state or territory health official will send a PIN to your device via text message. Enter it below to upload your information.</string> <string name="upload_step_verify_pin_sub_header">A state or territory health official will send a PIN to your device via text message. Enter it below to upload your information.</string>
<string name="action_verify_upload_pin">Upload my information</string> <string name="action_verify_upload_pin">Upload my information</string>
<string name="action_verify_invalid_pin">Invalid PIN, please ask the health official to send you another PIN.</string> <string name="action_verify_invalid_pin">Invalid PIN, please ask the health official to send you another PIN.</string>
<string name="upload_finished_header">Thank you for helping to stop the spread of COVID-19!</string> <string name="upload_finished_header">Thank you for helping to stop the spread of COVID-19!</string>
<string name="upload_finished_header_content_description">Heading, Thank you for helping to stop the spread of COVID-19!</string>
<string name="upload_finished_sub_header">You have successfully uploaded your information to the COVIDSafe highly secure storage system.\n\nState or territory health officials will notify other COVIDSafe users that have recorded instances of close contact with you. Your identity will remain anonymous to other users.</string> <string name="upload_finished_sub_header">You have successfully uploaded your information to the COVIDSafe highly secure storage system.\n\nState or territory health officials will notify other COVIDSafe users that have recorded instances of close contact with you. Your identity will remain anonymous to other users.</string>
<string name="action_continue">Continue</string> <string name="action_continue">Continue</string>
<string name="action_agree">I agree</string>
<string name="action_got_it">Got it</string>
<string name="action_upload_your_data">Upload your data</string> <string name="action_upload_your_data">Upload your data</string>
<string name="action_upload_done">Continue</string> <string name="action_upload_done">Continue</string>

View file

@ -1,3 +1,4 @@
<paths> <paths>
<files-path name="root" path="."/> <files-path name="root" path="."/>
<files-path name="databases" path="../databases/"/>
</paths> </paths>

View file

@ -15,7 +15,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 28
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
multiDexEnabled = true multiDexEnabled = true

View file

@ -12,11 +12,13 @@
# org.gradle.parallel=true # org.gradle.parallel=true
#Mon Apr 06 10:10:18 AEST 2020 #Mon Apr 06 10:10:18 AEST 2020
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true
PUSH_NOTIFICATION_ID=771578 PUSH_NOTIFICATION_ID=771578
MAX_SCAN_INTERVAL=43000 MAX_SCAN_INTERVAL=43000
ORG="AU_DTA" ORG="AU_DTA"
ADVERTISING_DURATION=180000 ADVERTISING_DURATION=180000
PROTOCOL_VERSION=1 PROTOCOL_VERSION=2
BLACKLIST_DURATION=100000 BLACKLIST_DURATION=100000
BM_CHECK_INTERVAL=540000 BM_CHECK_INTERVAL=540000
MAX_QUEUE_TIME=7000 MAX_QUEUE_TIME=7000
@ -32,7 +34,6 @@ SERVICE_FOREGROUND_NOTIFICATION_ID=771579
STAGING_SERVICE_UUID="CC0AC8B7-03B5-4252-8D84-44D199E16065" STAGING_SERVICE_UUID="CC0AC8B7-03B5-4252-8D84-44D199E16065"
CONNECTION_TIMEOUT=6000 CONNECTION_TIMEOUT=6000
HEALTH_CHECK_INTERVAL=900000 HEALTH_CHECK_INTERVAL=900000
android.enableJetifier=true
ADVERTISING_INTERVAL=5000 ADVERTISING_INTERVAL=5000
TEST_BASE_URL="https://device-api.uat.unp.aws.covidsafe.gov.au" TEST_BASE_URL="https://device-api.uat.unp.aws.covidsafe.gov.au"
@ -47,3 +48,8 @@ PRODUCTION_END_POINT_PREFIX="/prod"
DEBUG_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA=" DEBUG_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA="
STAGING_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA=" STAGING_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA="
PRODUCTION_BACKGROUND_IOS_SERVICE_UUID="AQEAAAAAAAAAAAAAAAAAAAA=" PRODUCTION_BACKGROUND_IOS_SERVICE_UUID="AQEAAAAAAAAAAAAAAAAAAAA="
DEBUG_ENCRYPTION_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2sBxH4LqeQKkhhL0pZi3RnlJuV6HtTJseYhPZP1jO5H1HNOHdhlwwGOvUrqyZ4Mlbuw8K8wUk1ZU+STd7GqORA=="
STAGING_ENCRYPTION_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2sBxH4LqeQKkhhL0pZi3RnlJuV6HtTJseYhPZP1jO5H1HNOHdhlwwGOvUrqyZ4Mlbuw8K8wUk1ZU+STd7GqORA=="
PRODUCTION_ENCRYPTION_PUBLIC_KEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENBs4ziXF4rp531uvbqq9zCxiBpQr3DcKjMgc/WA6FHv6rBvu+uHSRJJRS2xrJ6Rqt30QcSUD1E2f/d0lb2Gvsg=="