mirror of
https://github.com/AU-COVIDSafe/mobile-android.git
synced 2025-01-18 08:46:35 +00:00
COVIDSafe code from version 1.0.18 (#2)
This commit is contained in:
parent
696e4ed498
commit
3b77cc31e5
52 changed files with 6784 additions and 663 deletions
|
@ -1,6 +1,6 @@
|
|||
# COVIDSafe app
|
||||
|
||||
# Please report any security vulnerabilities using the details from [https://covidsafe.gov.au/.well-known/security.txt](https://covidsafe.gov.au/.well-known/security.txt)
|
||||
# Please report any security vulnerabilities using the details from [https://covidsafe.gov.au/.well-known/security.txt](https://covidsafe.gov.au/.well-known/security.txt)
|
||||
|
||||
# [Terms and Conditions for access to COVIDSafe App code](https://github.com/AU-COVIDSafe/mobile-android/blob/master/LICENSE.md)
|
||||
By accessing the App Code I accept and agree to the following terms:
|
||||
|
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/build
|
||||
/schemas
|
||||
|
|
|
@ -35,16 +35,25 @@ android {
|
|||
applicationId "au.gov.health.covidsafe"
|
||||
resValue "string", "build_config_package", "au.gov.health.covidsafe"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 17
|
||||
versionName "1.0.17"
|
||||
/*
|
||||
TargetSdk is currently set to 28 because we are using a greylisted api in SDK 29
|
||||
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()}\""
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = ["room.schemaLocation":
|
||||
"$projectDir/schemas".toString()]
|
||||
arguments = ["room.schemaLocation": "$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", "BASE_URL", TEST_BASE_URL
|
||||
buildConfigField "String", "IOS_BACKGROUND_UUID", DEBUG_BACKGROUND_IOS_SERVICE_UUID
|
||||
buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", DEBUG_ENCRYPTION_PUBLIC_KEY
|
||||
|
||||
|
||||
String ssid = STAGING_SERVICE_UUID
|
||||
|
@ -106,6 +116,8 @@ android {
|
|||
buildConfigField "String", "END_POINT_PREFIX", STAGING_END_POINT_PREFIX
|
||||
buildConfigField "String", "BASE_URL", STAGING_BASE_URL
|
||||
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", "BASE_URL", PROD_BASE_URL
|
||||
buildConfigField "String", "IOS_BACKGROUND_UUID", PRODUCTION_BACKGROUND_IOS_SERVICE_UUID
|
||||
buildConfigField "String", "ENCRYPTION_PUBLIC_KEY", PRODUCTION_ENCRYPTION_PUBLIC_KEY
|
||||
|
||||
|
||||
|
||||
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 {
|
||||
sourceCompatibility 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.lifecycle:lifecycle-service:2.2.0"
|
||||
implementation 'com.github.razir.progressbutton:progressbutton:2.0.1'
|
||||
|
||||
androidTestImplementation "androidx.room:room-testing:2.2.5"
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -10,6 +10,22 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -6,18 +6,23 @@ import android.view.View
|
|||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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 io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecordStorage
|
||||
import au.gov.health.covidsafe.streetpass.view.RecordViewModel
|
||||
import kotlinx.android.synthetic.main.database_peek.*
|
||||
import java.io.File
|
||||
|
||||
private const val TAG = "PeekActivity"
|
||||
|
||||
class PeekActivity : AppCompatActivity() {
|
||||
|
||||
|
@ -47,20 +52,6 @@ class PeekActivity : AppCompatActivity() {
|
|||
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)
|
||||
start.setOnClickListener {
|
||||
|
@ -106,11 +97,30 @@ class PeekActivity : AppCompatActivity() {
|
|||
|
||||
}
|
||||
|
||||
val plot = findViewById<FloatingActionButton>(R.id.plot)
|
||||
plot.setOnClickListener { view ->
|
||||
val intent = Intent(this, PlotActivity::class.java)
|
||||
intent.putExtra("time_period", nextTimePeriod())
|
||||
startActivity(intent)
|
||||
|
||||
shareDatabase.setOnClickListener {
|
||||
val authority = "${BuildConfig.APPLICATION_ID}.fileprovider"
|
||||
val databaseFilePath= getDatabasePath("record_database").absolutePath
|
||||
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) {
|
|
@ -27,16 +27,6 @@
|
|||
android:theme="@style/MyTheme.DayNight"
|
||||
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
|
||||
android:name="au.gov.health.covidsafe.SplashActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
|
@ -58,9 +48,6 @@
|
|||
android:name="au.gov.health.covidsafe.HomeActivity"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="au.gov.health.covidsafe.SelfIsolationDoneActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<receiver android:name="au.gov.health.covidsafe.boot.StartOnBootReceiver">
|
||||
<intent-filter>
|
||||
|
@ -75,16 +62,6 @@
|
|||
|
||||
<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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
|
|
5514
app/src/main/assets/spinner_migrating_db.json
Normal file
5514
app/src/main/assets/spinner_migrating_db.json
Normal file
File diff suppressed because it is too large
Load diff
3
app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt
Normal file
3
app/src/main/java/au/gov/health/covidsafe/LocalBlobV2.kt
Normal file
|
@ -0,0 +1,3 @@
|
|||
package au.gov.health.covidsafe
|
||||
|
||||
class LocalBlobV2(val modelP : String?, val modelC : String?, val txPower : Int?, val rssi : Int?)
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -16,12 +16,6 @@ class RecordListAdapter internal constructor(context: Context) :
|
|||
private var records = emptyList<StreetPassRecordViewModel>() // Cached copy of records
|
||||
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) {
|
||||
val modelCView: TextView = itemView.findViewById(R.id.modelc)
|
||||
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.filterModelP.setOnClickListener {
|
||||
val model = it.tag as StreetPassRecordViewModel
|
||||
setMode(MODE.MODEL_P, model)
|
||||
}
|
||||
|
||||
holder.filterModelC.setOnClickListener {
|
||||
val model = it.tag as StreetPassRecordViewModel
|
||||
setMode(MODE.MODEL_C, model)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filter(sample: StreetPassRecordViewModel?): List<StreetPassRecordViewModel> {
|
||||
return when (mode) {
|
||||
MODE.COLLAPSE -> prepareCollapsedData(sourceData)
|
||||
|
||||
MODE.ALL -> prepareViewData(sourceData)
|
||||
|
||||
MODE.MODEL_P -> filterByModelP(sample, sourceData)
|
||||
|
||||
MODE.MODEL_C -> filterByModelC(sample, sourceData)
|
||||
|
||||
else -> {
|
||||
prepareViewData(sourceData)
|
||||
}
|
||||
}
|
||||
private fun setRecords(records: List<StreetPassRecordViewModel>) {
|
||||
this.records = records
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
internal fun setSourceData(records: List<StreetPassRecord>) {
|
||||
this.sourceData = records
|
||||
setRecords(prepareViewData(this.sourceData))
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,24 +1,29 @@
|
|||
package au.gov.health.covidsafe
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
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.splash.SplashNavigationEvent
|
||||
import au.gov.health.covidsafe.ui.splash.SplashViewModel
|
||||
import au.gov.health.covidsafe.ui.splash.SplashViewModelFactory
|
||||
import kotlinx.android.synthetic.main.activity_splash.*
|
||||
import java.util.*
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
|
||||
private val SPLASH_TIME: Long = 2000
|
||||
private lateinit var viewModel: SplashViewModel
|
||||
|
||||
private var retryProviderInstall: Boolean = false
|
||||
private val ERROR_DIALOG_REQUEST_CODE = 1
|
||||
|
||||
private var updateFlag = false
|
||||
|
||||
private lateinit var mHandler: Handler
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -26,10 +31,21 @@ class SplashActivity : AppCompatActivity() {
|
|||
setContentView(R.layout.activity_splash)
|
||||
hideSystemUI()
|
||||
mHandler = Handler()
|
||||
viewModel = ViewModelProvider(this, SplashViewModelFactory(this)).get(SplashViewModel::class.java)
|
||||
|
||||
Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver,
|
||||
Settings.Secure.ANDROID_ID))
|
||||
Preference.putDeviceID(this, Settings.Secure.getString(this.contentResolver, 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() {
|
||||
|
@ -37,30 +53,23 @@ class SplashActivity : AppCompatActivity() {
|
|||
mHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!updateFlag) {
|
||||
mHandler.postDelayed({
|
||||
goToNextScreen()
|
||||
finish()
|
||||
}, SPLASH_TIME)
|
||||
}
|
||||
private fun migrationScreen() {
|
||||
splash_screen_logo.setImageResource(R.drawable.ic_logo_home_inactive)
|
||||
splash_screen_logo.setAnimation("spinner_migrating_db.json")
|
||||
splash_screen_logo.playAnimation()
|
||||
splash_migration_text.visibility = VISIBLE
|
||||
help_stop_covid.visibility = GONE
|
||||
}
|
||||
|
||||
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)) {
|
||||
OnboardingActivity::class.java
|
||||
} else if (dateUploaded.before(fourteenDaysAgo)) {
|
||||
SelfIsolationDoneActivity::class.java
|
||||
} else {
|
||||
HomeActivity::class.java
|
||||
}))
|
||||
viewModel.splashNavigationLiveData.removeObservers(this)
|
||||
viewModel.release()
|
||||
finish()
|
||||
}
|
||||
|
||||
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_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package au.gov.health.covidsafe.bluetooth.gatt
|
||||
|
||||
import au.gov.health.covidsafe.BuildConfig
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import au.gov.health.covidsafe.BuildConfig
|
||||
import au.gov.health.covidsafe.streetpass.PeripheralDevice
|
||||
|
||||
const val ACTION_RECEIVED_STREETPASS =
|
||||
"${BuildConfig.APPLICATION_ID}.ACTION_RECEIVED_STREETPASS"
|
||||
|
@ -20,12 +19,12 @@ const val ACTION_DEVICE_PROCESSED = "${BuildConfig.APPLICATION_ID}.ACTION_DEVICE
|
|||
const val ACTION_GATT_DISCONNECTED = "${BuildConfig.APPLICATION_ID}.ACTION_GATT_DISCONNECTED"
|
||||
|
||||
class ReadRequestPayload(
|
||||
val v: Int,
|
||||
val msg: String,
|
||||
val org: String,
|
||||
peripheral: PeripheralDevice
|
||||
val v: Int,
|
||||
val msg: String,
|
||||
val org: String,
|
||||
modelP: String?
|
||||
) {
|
||||
val modelP = peripheral.modelP
|
||||
val modelP = modelP ?: ""
|
||||
|
||||
fun getPayload(): ByteArray {
|
||||
return gson.toJson(this).toByteArray(Charsets.UTF_8)
|
||||
|
|
|
@ -9,6 +9,9 @@ import au.gov.health.covidsafe.Utils
|
|||
import au.gov.health.covidsafe.logging.CentralLog
|
||||
import au.gov.health.covidsafe.streetpass.CentralDevice
|
||||
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 kotlin.properties.Delegates
|
||||
|
||||
|
@ -20,6 +23,9 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) {
|
|||
private var serviceUUID: UUID by Delegates.notNull()
|
||||
var bluetoothGattServer: BluetoothGattServer? = null
|
||||
|
||||
val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
|
||||
|
||||
|
||||
init {
|
||||
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
this.serviceUUID = UUID.fromString(serviceUUIDString)
|
||||
|
@ -69,12 +75,18 @@ class GattServer constructor(val context: Context, serviceUUIDString: String) {
|
|||
if (serviceUUID == characteristic?.uuid) {
|
||||
|
||||
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, {
|
||||
ReadRequestPayload(
|
||||
v = TracerApp.protocolVersion,
|
||||
msg = TracerApp.thisDeviceMsg(),
|
||||
org = TracerApp.ORG,
|
||||
peripheral = TracerApp.asPeripheralDevice()
|
||||
v = TracerApp.protocolVersion,
|
||||
msg = remoteBlob,
|
||||
org = TracerApp.ORG,
|
||||
modelP = null //This is going to be stored as empty in the db as DUMMY value
|
||||
).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(
|
||||
device: BluetoothDevice?,
|
||||
|
|
|
@ -11,9 +11,7 @@ import android.os.PowerManager
|
|||
import androidx.annotation.Keep
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import au.gov.health.covidsafe.BuildConfig
|
||||
import au.gov.health.covidsafe.Preference
|
||||
import au.gov.health.covidsafe.Utils
|
||||
import au.gov.health.covidsafe.*
|
||||
import au.gov.health.covidsafe.bluetooth.BLEAdvertiser
|
||||
import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STATUS
|
||||
import au.gov.health.covidsafe.bluetooth.gatt.ACTION_RECEIVED_STREETPASS
|
||||
|
@ -31,8 +29,17 @@ import au.gov.health.covidsafe.streetpass.ConnectionRecord
|
|||
import au.gov.health.covidsafe.streetpass.StreetPassScanner
|
||||
import au.gov.health.covidsafe.streetpass.StreetPassServer
|
||||
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.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 com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -40,6 +47,7 @@ import kotlinx.coroutines.launch
|
|||
import pub.devrel.easypermissions.EasyPermissions
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@Keep
|
||||
class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
||||
|
||||
|
@ -75,6 +83,9 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
|||
|
||||
private val awsClient = NetworkFactory.awsClient
|
||||
|
||||
private val gson: Gson = GsonBuilder().disableHtmlEscaping().create()
|
||||
|
||||
|
||||
/** Defines callbacks for service binding, passed to bindService() */
|
||||
private val connection = object : ServiceConnection {
|
||||
|
||||
|
@ -550,13 +561,13 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
|||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
||||
if (ACTION_RECEIVED_STREETPASS == intent.action) {
|
||||
val connRecord: ConnectionRecord = intent.getParcelableExtra(STREET_PASS)
|
||||
val connRecord: ConnectionRecord? = intent.getParcelableExtra(STREET_PASS)
|
||||
CentralLog.d(
|
||||
TAG,
|
||||
"StreetPass received: $connRecord"
|
||||
)
|
||||
|
||||
if (connRecord.msg.isNotEmpty()) {
|
||||
if (connRecord != null && connRecord.msg.isNotEmpty()) {
|
||||
|
||||
if (mBound) {
|
||||
val proximity = mService.proximity
|
||||
|
@ -567,23 +578,44 @@ class BluetoothMonitoringService : LifecycleService(), CoroutineScope {
|
|||
)
|
||||
}
|
||||
|
||||
val remoteBlob: String = if (connRecord.version == VERSION_ONE) {
|
||||
with(receiver = connRecord) {
|
||||
val plainRecordByteArray = gson.toJson(StreetPassRecordDatabase.Companion.EncryptedRecord(
|
||||
peripheral.modelP, central.modelC, rssi, txPower, msg = msg))
|
||||
.toByteArray(Charsets.UTF_8)
|
||||
Encryption.encryptPayload(plainRecordByteArray)
|
||||
}
|
||||
} 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 = connRecord.version,
|
||||
msg = connRecord.msg,
|
||||
v = if (connRecord.version == 1) TracerApp.protocolVersion else (connRecord.version),
|
||||
org = connRecord.org,
|
||||
modelP = connRecord.peripheral.modelP,
|
||||
modelC = connRecord.central.modelC,
|
||||
rssi = connRecord.rssi,
|
||||
txPower = connRecord.txPower
|
||||
localBlob = localBlob,
|
||||
remoteBlob = remoteBlob
|
||||
)
|
||||
|
||||
launch {
|
||||
CentralLog.d(
|
||||
TAG,
|
||||
"Coroutine - Saving StreetPassRecord: ${Utils.getDate(record.timestamp)} $record")
|
||||
|
||||
launch{
|
||||
CentralLog.d(
|
||||
TAG,
|
||||
"Coroutine - Saving StreetPassRecord: ${Utils.getDate(record.timestamp)} $record")
|
||||
|
||||
streetPassRecordStorage.saveRecord(record)
|
||||
streetPassRecordStorage.saveRecord(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,16 +23,16 @@ data class CentralDevice(
|
|||
|
||||
@Parcelize
|
||||
data class ConnectionRecord(
|
||||
val version: Int,
|
||||
val version: Int,
|
||||
|
||||
val msg: String,
|
||||
val org: String,
|
||||
val msg: String,
|
||||
val org: String,
|
||||
|
||||
val peripheral: PeripheralDevice,
|
||||
val central: CentralDevice,
|
||||
val peripheral: PeripheralDevice,
|
||||
val central: CentralDevice,
|
||||
|
||||
var rssi: Int,
|
||||
var txPower: Int?
|
||||
var rssi: Int,
|
||||
var txPower: Int?
|
||||
) : Parcelable {
|
||||
override fun toString(): String {
|
||||
return "Central ${central.modelC} - ${central.address} ---> Peripheral ${peripheral.modelP} - ${peripheral.address}"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.Companion.blacklistDuration
|
||||
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.concurrent.PriorityBlockingQueue
|
||||
|
||||
@Keep
|
||||
class StreetPassWorker(val context: Context) {
|
||||
|
||||
|
@ -39,6 +47,9 @@ class StreetPassWorker(val context: Context) {
|
|||
private var currentPendingConnection: Work? = null
|
||||
private var localBroadcastManager: LocalBroadcastManager = LocalBroadcastManager.getInstance(context)
|
||||
|
||||
private val gson = GsonBuilder().disableHtmlEscaping().create()
|
||||
|
||||
|
||||
val onWorkTimeoutListener = object : Work.OnWorkTimeoutListener {
|
||||
override fun onWorkTimeout(work: Work) {
|
||||
|
||||
|
@ -485,6 +496,10 @@ class StreetPassWorker(val context: Context) {
|
|||
service?.let {
|
||||
val characteristic = service.getCharacteristic(serviceUUID)
|
||||
if (characteristic != null) {
|
||||
// Attempt to prevent bonding should the StreetPass characteristic
|
||||
// require authentication or encryption
|
||||
StreetPassPairingFix.bypassAuthenticationRetry(gatt)
|
||||
|
||||
val readSuccess = gatt.readCharacteristic(characteristic)
|
||||
CentralLog.i(
|
||||
TAG,
|
||||
|
@ -587,18 +602,30 @@ class StreetPassWorker(val context: Context) {
|
|||
if (Utils.bmValid(context)) {
|
||||
//may have failed to read, can try to write
|
||||
//we are writing as the central device
|
||||
|
||||
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(
|
||||
v = TracerApp.protocolVersion,
|
||||
msg = TracerApp.thisDeviceMsg(),
|
||||
msg = remoteBlob,
|
||||
org = TracerApp.ORG,
|
||||
modelC = thisCentralDevice.modelC,
|
||||
rssi = work.connectable.rssi,
|
||||
txPower = work.connectable.transmissionPower
|
||||
modelC = DUMMY_DEVICE,
|
||||
rssi = DUMMY_RSSI,
|
||||
txPower = DUMMY_TXPOWER
|
||||
)
|
||||
|
||||
characteristic.value = writedata.getPayload()
|
||||
|
||||
// Attempt to prevent bonding should the StreetPass characteristic
|
||||
// require authentication or encryption
|
||||
StreetPassPairingFix.bypassAuthenticationRetry(gatt)
|
||||
|
||||
val writeSuccess = gatt.writeCharacteristic(characteristic)
|
||||
CentralLog.i(
|
||||
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(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -11,23 +11,14 @@ class StreetPassRecord(
|
|||
@ColumnInfo(name = "v")
|
||||
var v: Int,
|
||||
|
||||
@ColumnInfo(name = "msg")
|
||||
var msg: String,
|
||||
|
||||
@ColumnInfo(name = "org")
|
||||
var org: String,
|
||||
|
||||
@ColumnInfo(name = "modelP")
|
||||
val modelP: String,
|
||||
@ColumnInfo(name = "localBlob")
|
||||
val localBlob: String,
|
||||
|
||||
@ColumnInfo(name = "modelC")
|
||||
val modelC: String,
|
||||
|
||||
@ColumnInfo(name = "rssi")
|
||||
val rssi: Int,
|
||||
|
||||
@ColumnInfo(name = "txPower")
|
||||
val txPower: Int?
|
||||
@ColumnInfo(name = "remoteBlob")
|
||||
val remoteBlob: String
|
||||
|
||||
) {
|
||||
|
||||
|
@ -39,7 +30,7 @@ class StreetPassRecord(
|
|||
var timestamp: Long = System.currentTimeMillis()
|
||||
|
||||
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)"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
package au.gov.health.covidsafe.streetpass.persistence
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import au.gov.health.covidsafe.LocalBlobV2
|
||||
import au.gov.health.covidsafe.logging.CentralLog
|
||||
import au.gov.health.covidsafe.status.persistence.StatusRecord
|
||||
import au.gov.health.covidsafe.status.persistence.StatusRecordDao
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
const val CURRENT_DB_VERSION = 3
|
||||
|
||||
@Database(
|
||||
entities = [StreetPassRecord::class, StatusRecord::class],
|
||||
version = 2,
|
||||
exportSchema = true
|
||||
version = CURRENT_DB_VERSION,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class StreetPassRecordDatabase : RoomDatabase() {
|
||||
|
||||
|
@ -21,22 +30,60 @@ abstract class StreetPassRecordDatabase : RoomDatabase() {
|
|||
abstract fun statusDao(): StatusRecordDao
|
||||
|
||||
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
|
||||
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
|
||||
if (tempInstance != null) {
|
||||
return tempInstance
|
||||
}
|
||||
this.migrationCallback = migrationCallBack
|
||||
synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context,
|
||||
StreetPassRecordDatabase::class.java,
|
||||
"record_database"
|
||||
context,
|
||||
StreetPassRecordDatabase::class.java,
|
||||
"record_database"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.build()
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addCallback(CALLBACK)
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
return instance
|
||||
}
|
||||
|
@ -58,10 +105,74 @@ 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 {
|
||||
var isExist = false
|
||||
val res =
|
||||
db.query("PRAGMA table_info($tableName)", null)
|
||||
db.query("PRAGMA table_info($tableName)", null)
|
||||
res.moveToFirst()
|
||||
do {
|
||||
val currentColumn = res.getString(1)
|
||||
|
@ -72,5 +183,9 @@ abstract class StreetPassRecordDatabase : RoomDatabase() {
|
|||
return isExist
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface MigrationCallBack {
|
||||
fun migrationStarted()
|
||||
fun migrationFinished()
|
||||
}
|
|
@ -4,12 +4,12 @@ import au.gov.health.covidsafe.streetpass.persistence.StreetPassRecord
|
|||
|
||||
class StreetPassRecordViewModel(record: StreetPassRecord, val number: Int) {
|
||||
val version = record.v
|
||||
val modelC = record.modelC
|
||||
val modelP = record.modelP
|
||||
val msg = record.msg
|
||||
val modelC = "Encrypted"
|
||||
val modelP = "Encrypted"
|
||||
val msg = record.remoteBlob
|
||||
val timeStamp = record.timestamp
|
||||
val rssi = record.rssi
|
||||
val transmissionPower = record.txPower
|
||||
val rssi = 0
|
||||
val transmissionPower = 0
|
||||
val org = record.org
|
||||
|
||||
constructor(record: StreetPassRecord) : this(record, 1)
|
||||
|
|
|
@ -58,6 +58,14 @@ abstract class PagerChildFragment : BaseFragment() {
|
|||
|
||||
sealed class 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()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package au.gov.health.covidsafe.ui.home
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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 pub.devrel.easypermissions.AppSettingsDialog
|
||||
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 {
|
||||
|
||||
|
@ -107,6 +114,8 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
|
|||
registerBroadcast()
|
||||
}
|
||||
refreshSetupCompleteOrIncompleteUi()
|
||||
|
||||
home_header_no_bluetooth_pairing.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
@ -132,71 +141,82 @@ class HomeFragment : BaseFragment(), EasyPermissions.PermissionCallbacks {
|
|||
home_root.removeAllViews()
|
||||
}
|
||||
|
||||
private fun refreshSetupCompleteOrIncompleteUi() {
|
||||
val isUploaded = context?.let {
|
||||
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)
|
||||
private fun isDataUploadedInPast14Days(context: Context): Boolean {
|
||||
val isUploaded = Preference.isDataUploaded(context)
|
||||
|
||||
val textColor = ContextCompat.getColor(context, R.color.slack_black)
|
||||
home_header_setup_complete_header_uploaded.setTextColor(textColor)
|
||||
home_header_setup_complete_header.setTextColor(textColor)
|
||||
if (!isUploaded) {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
updateBlueToothStatus()
|
||||
updatePushNotificationStatus()
|
||||
updateBatteryOptimizationStatus()
|
||||
updateLocationStatus()
|
||||
}
|
||||
isUploaded -> {
|
||||
home_header_setup_complete_header_uploaded.visibility = VISIBLE
|
||||
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)
|
||||
ContextCompat.getColor(it, R.color.grey).let { bgColor ->
|
||||
header_background.setBackgroundColor(bgColor)
|
||||
header_background_overlap.setBackgroundColor(bgColor)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
home_been_tested_button.visibility = if (isDataUploadedInPast14Days) GONE else VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,28 @@ class OnboardingActivity : FragmentActivity(), HasBlockingState, PagerContainer
|
|||
}
|
||||
|
||||
override fun refreshButton(updateButtonLayout: UploadButtonLayout) {
|
||||
if (updateButtonLayout is UploadButtonLayout.ContinueLayout) {
|
||||
onboarding_next.setText(updateButtonLayout.buttonText)
|
||||
onboarding_next.setOnClickListener {
|
||||
updateButtonLayout.buttonListener?.invoke()
|
||||
when (updateButtonLayout) {
|
||||
is UploadButtonLayout.ContinueLayout -> {
|
||||
onboarding_next.setText(updateButtonLayout.buttonText)
|
||||
onboarding_next.setOnClickListener {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb
|
|||
}
|
||||
|
||||
override val navigationIcon: Int? = R.drawable.ic_up
|
||||
override var stepProgress: Int? = 5
|
||||
override var stepProgress: Int? = 4
|
||||
|
||||
private var navigationStarted = false
|
||||
|
||||
|
@ -58,7 +58,7 @@ class PermissionFragment : PagerChildFragment(), EasyPermissions.PermissionCallb
|
|||
private fun navigateToNextPage() {
|
||||
navigationStarted = false
|
||||
if (hasAllPermissionsAndBluetoothOn()) {
|
||||
navigateTo(R.id.action_permissionFragment_to_permissionSuccessFragment)
|
||||
navigateTo(R.id.action_permissionFragment_to_permissionDeviceNameFragment )
|
||||
} else {
|
||||
navigateToMainActivity()
|
||||
}
|
||||
|
|
|
@ -2,23 +2,30 @@ package au.gov.health.covidsafe.ui.onboarding.fragment.permissionsuccess
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_permission_success.*
|
||||
import au.gov.health.covidsafe.HomeActivity
|
||||
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_success.*
|
||||
|
||||
class PermissionSuccessFragment : PagerChildFragment() {
|
||||
|
||||
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?)
|
||||
: View? = inflater.inflate(R.layout.fragment_permission_success, container, false)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
permission_success_content.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
private fun navigateToNextPage() {
|
||||
val intent = Intent(context, HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import au.gov.health.covidsafe.R
|
||||
import au.gov.health.covidsafe.ui.PagerChildFragment
|
||||
import au.gov.health.covidsafe.ui.UploadButtonLayout
|
||||
|
@ -17,6 +18,13 @@ class UploadFinishedFragment : PagerChildFragment() {
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
||||
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) {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
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_upload_page_4.*
|
||||
import kotlinx.android.synthetic.main.fragment_upload_initial.*
|
||||
import kotlinx.android.synthetic.main.fragment_upload_page_4.root
|
||||
|
||||
class UploadInitialFragment : PagerChildFragment() {
|
||||
|
||||
|
@ -19,6 +21,13 @@ class UploadInitialFragment : PagerChildFragment() {
|
|||
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() {
|
||||
enableContinueButton()
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.text.method.LinkMovementMethod
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import au.gov.health.covidsafe.R
|
||||
import au.gov.health.covidsafe.ui.PagerChildFragment
|
||||
import au.gov.health.covidsafe.ui.UploadButtonLayout
|
||||
|
@ -30,7 +31,11 @@ class UploadStepFourFragment : PagerChildFragment() {
|
|||
upload_consent_checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
updateButtonState()
|
||||
}
|
||||
|
||||
// set accessibility focus to the title
|
||||
header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
|
||||
}
|
||||
|
||||
override fun updateButtonState() {
|
||||
if (upload_consent_checkbox.isChecked) {
|
||||
enableContinueButton()
|
||||
|
@ -42,7 +47,7 @@ class UploadStepFourFragment : PagerChildFragment() {
|
|||
override val navigationIcon: Int? = R.drawable.ic_up
|
||||
|
||||
override fun getUploadButtonLayout() = UploadButtonLayout.ContinueLayout(
|
||||
R.string.action_agree) {
|
||||
R.string.action_continue) {
|
||||
navigateToVerifyUploadPin()
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.core.os.bundleOf
|
||||
import au.gov.health.covidsafe.R
|
||||
import au.gov.health.covidsafe.ui.PagerChildFragment
|
||||
|
@ -46,6 +47,9 @@ class VerifyUploadPinFragment : PagerChildFragment() {
|
|||
updateButtonState()
|
||||
hideInvalidOtp()
|
||||
}
|
||||
|
||||
// set accessibility focus to the title
|
||||
header.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/keyline_7"
|
||||
tools:context=".ui.onboarding.OnboardingActivity">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
|
@ -69,7 +70,18 @@
|
|||
android:layout_marginStart="@dimen/keyline_5"
|
||||
android:layout_marginTop="@dimen/keyline_5"
|
||||
android:layout_marginEnd="@dimen/keyline_5"
|
||||
android:layout_marginBottom="@dimen/keyline_7"
|
||||
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>
|
|
@ -21,7 +21,26 @@
|
|||
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:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
|
@ -32,8 +51,11 @@
|
|||
app:layout_constraintBottom_toTopOf="@+id/help_stop_covid"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/splash_screen_crest"
|
||||
app:layout_constraintVertical_chainStyle="spread" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/splash_migration_text"
|
||||
app:layout_constraintVertical_chainStyle="spread"
|
||||
app:lottie_autoPlay="false"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_speed="1"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/help_stop_covid"
|
||||
|
|
|
@ -25,22 +25,6 @@
|
|||
app:layout_constraintBottom_toBottomOf="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>
|
||||
|
||||
|
||||
|
@ -54,15 +38,6 @@
|
|||
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
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -89,13 +64,22 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info"
|
||||
android:textSize="12sp"
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/shareDatabase"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_home_share"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="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>
|
|
@ -17,6 +17,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/topInset"
|
||||
app:layout_constraintWidth_default="wrap"
|
||||
app:navigationContentDescription="@string/navigation_back_button_content_description"
|
||||
app:navigationIcon="@drawable/ic_up"
|
||||
app:title="@string/title_help">
|
||||
|
||||
|
|
|
@ -26,39 +26,6 @@
|
|||
app:layout_constraintRight_toRightOf="parent"
|
||||
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
|
||||
android:id="@+id/home_header_setup_complete_header"
|
||||
style="?textAppearanceBody1"
|
||||
|
@ -72,13 +39,26 @@
|
|||
android:text="@string/home_header_active_title"
|
||||
app:layout_constraintLeft_toLeftOf="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
|
||||
android:id="@+id/home_header_label_setup_complete_subtitle_bottom_space"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/keyline_7"
|
||||
app:layout_constraintTop_toBottomOf="@+id/home_header_setup_complete_header" />
|
||||
|
||||
|
||||
android:layout_height="@dimen/keyline_0"
|
||||
app:layout_constraintTop_toBottomOf="@+id/home_header_no_bluetooth_pairing" />
|
||||
</merge>
|
|
@ -17,6 +17,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/keyline_4"
|
||||
android:layout_marginRight="@dimen/keyline_4"
|
||||
android:layout_marginTop="@dimen/keyline_7"
|
||||
app:layout_constraintTop_toBottomOf="@+id/header_barrier"
|
||||
card_view:cardBackgroundColor="@color/white"
|
||||
card_view:cardCornerRadius="6dp"
|
||||
|
@ -28,7 +29,6 @@
|
|||
android:id="@+id/home_setup_incomplete_permissions_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/keyline_4"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
|
79
app/src/main/res/layout/fragment_permission_device_name.xml
Normal file
79
app/src/main/res/layout/fragment_permission_device_name.xml
Normal 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>
|
|
@ -40,6 +40,7 @@
|
|||
android:paddingStart="@dimen/keyline_5"
|
||||
android:paddingEnd="@dimen/keyline_5"
|
||||
android:text="@string/permission_success_content"
|
||||
android:textColorLink="?attr/colorPrimary"
|
||||
android:textAppearance="?textAppearanceBody1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
android:layout_marginEnd="@dimen/keyline_5"
|
||||
android:textAppearance="?textAppearanceHeadline2"
|
||||
android:text="@string/upload_finished_header"
|
||||
android:contentDescription="@string/upload_finished_header_content_description"
|
||||
app:layout_constraintBottom_toTopOf="@+id/subHeader"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/keyline_6"
|
||||
android:text="@string/upload_step_1_header"
|
||||
android:contentDescription="@string/upload_step_1_header_content_description"
|
||||
android:textAppearance="?textAppearanceHeadline2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="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
|
||||
android:id="@+id/fragment_nav_host_upload"
|
||||
|
|
|
@ -18,7 +18,9 @@
|
|||
android:layout_marginTop="@dimen/keyline_4"
|
||||
android:layout_marginEnd="@dimen/keyline_5"
|
||||
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
|
||||
android:id="@+id/subHeader"
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
android:layout_marginTop="@dimen/keyline_4"
|
||||
android:layout_marginEnd="@dimen/keyline_5"
|
||||
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_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -104,7 +104,21 @@
|
|||
android:label="PermissionFragment"
|
||||
tools:layout="@layout/fragment_permission">
|
||||
<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:enterAnim="@anim/slide_in_right"
|
||||
app:exitAnim="@anim/slide_out_left"
|
||||
|
|
|
@ -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_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_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>
|
||||
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
|
@ -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_button">Next</string>
|
||||
|
||||
<!-- OnBoarding Data Privcay -->
|
||||
<!-- OnBoarding Data Privacy -->
|
||||
<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_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 -->
|
||||
<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_name_title">Full name (First, Last)</string>
|
||||
<string name="personal_details_name_hint">Firstname Lastname</string>
|
||||
<string name="personal_details_name_content_description">Enter full name (First, Last)</string>
|
||||
<string name="personal_details_age_title">Age (select)</string>
|
||||
<string name="personal_details_age_hint">Age range</string>
|
||||
<string name="personal_details_name_title">Full name</string>
|
||||
<string name="personal_details_name_content_description">Enter full name</string>
|
||||
<string name="personal_details_age_title">Age range (select)</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_hint">e.g. 2000</string>
|
||||
|
@ -119,20 +117,40 @@
|
|||
|
||||
<!-- OnBoarding Permission -->
|
||||
<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_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-->
|
||||
<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_button">Continue</string>
|
||||
|
||||
<!-- Home -->
|
||||
<string name="home_header_active_title">COVIDSafe is active.\nNo further action is required.</string>
|
||||
<string name="home_header_inactive_title">COVIDSafe is not active.\nCheck your permissions.</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_active_title">COVIDSafe is active.</string>
|
||||
<string name="home_header_active_no_action_required">No further action required.</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_non_battery_optimization_permission">Battery optimization: %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_content">If you have issues or questions about the app.</string>
|
||||
|
||||
|
||||
<string name="home_version_number">Version Number:%s</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_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’ you’ll need to provide consent to upload your information. </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_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_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="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="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="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_done">Continue</string>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<paths>
|
||||
<files-path name="root" path="."/>
|
||||
<files-path name="databases" path="../databases/"/>
|
||||
</paths>
|
|
@ -15,7 +15,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 28
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled = true
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
# org.gradle.parallel=true
|
||||
#Mon Apr 06 10:10:18 AEST 2020
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
PUSH_NOTIFICATION_ID=771578
|
||||
MAX_SCAN_INTERVAL=43000
|
||||
ORG="AU_DTA"
|
||||
ADVERTISING_DURATION=180000
|
||||
PROTOCOL_VERSION=1
|
||||
PROTOCOL_VERSION=2
|
||||
BLACKLIST_DURATION=100000
|
||||
BM_CHECK_INTERVAL=540000
|
||||
MAX_QUEUE_TIME=7000
|
||||
|
@ -32,7 +34,6 @@ SERVICE_FOREGROUND_NOTIFICATION_ID=771579
|
|||
STAGING_SERVICE_UUID="CC0AC8B7-03B5-4252-8D84-44D199E16065"
|
||||
CONNECTION_TIMEOUT=6000
|
||||
HEALTH_CHECK_INTERVAL=900000
|
||||
android.enableJetifier=true
|
||||
ADVERTISING_INTERVAL=5000
|
||||
|
||||
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="
|
||||
STAGING_BACKGROUND_IOS_SERVICE_UUID="AQAgAAAAAAAAAAAAAAAAAAA="
|
||||
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=="
|
||||
|
||||
|
|
Loading…
Reference in a new issue