Commit 009bbd4c authored by stopcovid@lunabee.com's avatar stopcovid@lunabee.com
Browse files

Update to 1.1.3

parent 9bc304da
......@@ -17,8 +17,8 @@ android {
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 12
versionName "1.3.0"
versionCode 13
versionName "1.4.0"
}
compileOptions {
......@@ -64,7 +64,7 @@ dependencies {
def androidXTestJUnitVersion = '1.1.2-rc01'
testImplementation "androidx.test.ext:junit:${androidXTestJUnitVersion}"
def coroutines_version = "1.3.7"
def coroutines_version = "1.3.8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
......
......@@ -43,6 +43,7 @@ class BleProximityNotification(
private val bleRecordProviderForScanWithoutPayload =
RecordProviderForScanWithoutPayload(settings)
private val bleRecordMapper = BleRecordMapper(settings)
private val bleScannedDeviceFilter = BleScannedDeviceFilter()
private lateinit var proximityPayloadProvider: ProximityPayloadProvider
private lateinit var callback: ProximityNotificationCallback
......@@ -121,7 +122,7 @@ class BleProximityNotification(
bleGattManager.requestRemoteRssi(device, false)?.let { rssi ->
val scannedDevice =
BleScannedDevice(device = device, rssi = rssi)
bleRecordProviderForScanWithoutPayload.fromScan(
bleRecordProviderForScanWithPayload.fromScan(
scannedDevice,
payload
)
......@@ -149,26 +150,9 @@ class BleProximityNotification(
private fun startScanner() {
val status = bleScanner.start(callback = object : BleScanner.Callback {
override fun onResult(results: List<BleScannedDevice>) {
if (results.isNotEmpty()) {
coroutineScope.launch(coroutineContextProvider.default) {
results.mapNotNull { scannedDevice ->
val serviceData = scannedDevice.serviceData
if (serviceData != null) {
// Android case
decodePayload(serviceData)
?.let {
bleRecordProviderForScanWithPayload.fromScan(
scannedDevice,
it
)
}
} else {
// iOS case
bleRecordProviderForScanWithoutPayload.fromScan(scannedDevice, null)
}
}.forEach { notifyProximity(it) }
handleScanResults(bleScannedDeviceFilter.filter(results))
}
}
}
......@@ -213,6 +197,24 @@ class BleProximityNotification(
callback.onProximity(proximityInfo)
}
private suspend fun handleScanResults(results: List<BleScannedDevice>) =
withContext(coroutineContextProvider.default) {
results.mapNotNull { scannedDevice ->
val serviceData = scannedDevice.serviceData
if (serviceData != null) {
// Android case
decodePayload(serviceData)?.let {
bleRecordProviderForScanWithPayload.fromScan(scannedDevice, it)
}
} else {
// iOS case
bleRecordProviderForScanWithoutPayload.fromScan(scannedDevice, null)
}
}.forEach { notifyProximity(it) }
}
private fun decodePayload(value: ByteArray) = BlePayload.fromOrNull(value)
private fun buildPayload() = BlePayload(
......
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Orange / Date - 2020/06/30 - for the STOP-COVID project
*/
package com.orange.proximitynotification.ble
import com.orange.proximitynotification.ble.scanner.BleScannedDevice
import java.util.Date
internal class BleScannedDeviceFilter {
private var mostRecentScanTimestamp = Date(0)
fun filter(scannedDevices: List<BleScannedDevice>): List<BleScannedDevice> {
val results = scannedDevices.sortedByDescending { it.timestamp }
.distinctBy {
it.serviceData?.contentHashCode() ?: run { it.device.address }
}
.filter { it.timestamp > mostRecentScanTimestamp }
results.firstOrNull()?.let { this.mostRecentScanTimestamp = it.timestamp }
return results
}
}
\ No newline at end of file
......@@ -98,7 +98,6 @@ class BleScannerImpl(
super.onBatchScanResults(results)
results
.distinctBy { it.device }
.toBleScannedDevices(settings.serviceUuid)
.takeIf { it.isNotEmpty() }
?.let { callback.onResult(it) }
......
......@@ -11,14 +11,16 @@
package com.orange.proximitynotification.ble.scanner
import android.os.ParcelUuid
import com.orange.proximitynotification.tools.nanosTimestampToDate
import no.nordicsemi.android.support.v18.scanner.ScanResult
import java.util.UUID
internal fun ScanResult.toBleScannedDevice(serviceUuid: UUID): BleScannedDevice =
BleScannedDevice(
device = device,
rssi = rssi,
serviceData = scanRecord?.serviceData?.get(ParcelUuid(serviceUuid)),
rssi = rssi
timestamp = timestampNanos.nanosTimestampToDate()
)
internal fun List<ScanResult>.toBleScannedDevices(serviceUuid: UUID): List<BleScannedDevice> =
......
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Orange / Date - 2020/06/30 - for the STOP-COVID project
*/
package com.orange.proximitynotification.tools
import android.os.SystemClock
import java.util.Date
internal fun Long.nanosTimestampToDate(): Date =
Date(System.currentTimeMillis() - SystemClock.elapsedRealtime() + (this / 1000000))
\ No newline at end of file
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Orange / Date - 2020/06/30 - for the STOP-COVID project
*/
package com.orange.proximitynotification.ble
import com.google.common.truth.Truth.assertThat
import com.orange.proximitynotification.ble.scanner.BleScannedDevice
import org.junit.Test
import java.util.Date
class BleScannedDeviceFilterTest {
private val filter = BleScannedDeviceFilter()
@Test
fun filter_with_empty_should_return_empty() {
// Given
val scans: List<BleScannedDevice> = emptyList()
// When
val result = filter.filter(scans)
// Then
assertThat(result).isEmpty()
}
@Test
fun filter_with_different_device_scans_should_return_same_device_scans() {
// Given
val now = Date()
val scans: List<BleScannedDevice> = listOf(
bleScannedDevice(bluetoothDevice("Device1"), timestamp = now, serviceData = null),
bleScannedDevice(bluetoothDevice("Device2"), timestamp = now, serviceData = null),
bleScannedDevice(bluetoothDevice("Device3"), timestamp = now, serviceData = byteArrayOf(1)),
bleScannedDevice(bluetoothDevice("Device4"), timestamp = now, serviceData = byteArrayOf(2))
)
// When
val result = filter.filter(scans)
// Then
assertThat(result).isEqualTo(scans)
}
@Test
fun filter_with_different_device_scans_but_same_service_data_should_squash() {
// Given
val now = Date()
val device1 = bluetoothDevice("Device1")
val device2 = bluetoothDevice("Device2")
val device3 = bluetoothDevice("Device3")
val device4 = bluetoothDevice("Device4")
val scans: List<BleScannedDevice> = listOf(
bleScannedDevice(device1, timestamp = now, serviceData = null),
bleScannedDevice(device2, timestamp = now, serviceData = null),
bleScannedDevice(device3, timestamp = now.minus(2), serviceData = byteArrayOf(1)),
bleScannedDevice(device4, timestamp = now, serviceData = byteArrayOf(1))
)
// When
val result = filter.filter(scans)
// Then
assertThat(result).containsExactly(
bleScannedDevice(device1, timestamp = now, serviceData = null),
bleScannedDevice(device2, timestamp = now, serviceData = null),
bleScannedDevice(device4, timestamp = now, serviceData = byteArrayOf(1))
)
}
@Test
fun filter_with_same_devices_scans_should_keep_most_recent_scans_and_order_them_by_timestamp() {
// Given
val device1 = bluetoothDevice("Device1")
val device2 = bluetoothDevice("Device2")
val serviceData = byteArrayOf(1)
val now = Date()
val scans: List<BleScannedDevice> = listOf(
bleScannedDevice(device = device2, serviceData = null, timestamp = now.minus(5)),
bleScannedDevice(device = device2, serviceData = null, timestamp = now.minus(4)),
bleScannedDevice(device = device1, serviceData = serviceData, timestamp = now.minus(3)),
bleScannedDevice(device = device1, serviceData = serviceData, timestamp = now.minus(2)),
bleScannedDevice(device = device1, serviceData = serviceData, timestamp = now.minus(1))
)
// When
val result = filter.filter(scans)
// Then
assertThat(result).containsExactly(
bleScannedDevice(device = device1, serviceData = serviceData, timestamp = now.minus(1)),
bleScannedDevice(device = device2, serviceData = null, timestamp = now.minus(4))
)
}
@Test
fun filter_with_old_scans_should_only_keep_most_recent_ones() {
// Given
val now = Date()
val scansBefore: List<BleScannedDevice> = listOf(
bleScannedDevice(bluetoothDevice("Device-skipped-1"), timestamp = now.minus(1_001), serviceData = null),
bleScannedDevice(bluetoothDevice("Device-skipped-2"), timestamp = now.minus(1_000), serviceData = null)
)
filter.filter(scansBefore)
val device1 = bluetoothDevice("Device1")
val device2 = bluetoothDevice("Device2")
val device3 = bluetoothDevice("Device3")
val scans: List<BleScannedDevice> = listOf(
bleScannedDevice(device1, timestamp = now.minus(1_001), serviceData = null),
bleScannedDevice(device2, timestamp = now.minus(1_000), serviceData = null),
bleScannedDevice(device3, timestamp = now.minus(999), serviceData = null)
)
// When
val result = filter.filter(scans)
// Then
assertThat(result).containsExactly(
bleScannedDevice(device3, timestamp = now.minus(999), serviceData = null)
)
}
private fun Date.minus(millis: Long) = Date(time - millis)
}
\ No newline at end of file
......@@ -45,11 +45,13 @@ internal fun payload(
internal fun bleScannedDevice(
device: BluetoothDevice = bluetoothDevice(),
rssi: Int = 0,
serviceData: ByteArray? = null
serviceData: ByteArray? = null,
timestamp: Date = Date()
) = BleScannedDevice(
device = device,
rssi = rssi,
serviceData = serviceData
serviceData = serviceData,
timestamp = timestamp
)
internal fun bluetoothDevice(address: String = "address"): BluetoothDevice {
......
......@@ -58,13 +58,15 @@ class ScanResultExtKtTest {
private fun givenScanResult(
device: BluetoothDevice = mock(),
rssi: Int = 0,
scanRecord: ScanRecord? = null
scanRecord: ScanRecord? = null,
timestampNanos: Long = 0L
): ScanResult {
val scanResult = mock<ScanResult>()
whenever(scanResult.device).thenReturn(device)
whenever(scanResult.rssi).thenReturn(rssi)
whenever(scanResult.scanRecord).thenReturn(scanRecord)
whenever(scanResult.timestampNanos).thenReturn(timestampNanos)
return scanResult
}
......
......@@ -16,11 +16,16 @@ buildscript {
jcenter()
}
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.2.2"
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0"
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.karumi:shot:4.3.0"
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
// https://issuetracker.google.com/issues/159151549#comment3
classpath 'org.ow2.asm:asm:8.0.1'
classpath 'org.ow2.asm:asm-util:8.0.1'
classpath 'org.ow2.asm:asm-commons:8.0.1'
}
}
......
......@@ -39,15 +39,13 @@ android {
}
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
viewBinding {
enabled = true
}
buildFeatures.viewBinding = true
}
dependencies {
// Fix https://stackoverflow.com/questions/41025200/android-view-inflateexception-error-inflating-class-android-webkit-webview/58131421#58131421
api 'androidx.appcompat:appcompat:1.2.0-rc01'
api 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
api 'androidx.constraintlayout:constraintlayout:2.0.0-beta8'
api 'androidx.core:core-ktx:1.3.0'
api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
......@@ -60,19 +58,19 @@ dependencies {
api 'androidx.emoji:emoji-appcompat:1.1.0'
api 'androidx.emoji:emoji-bundled:1.1.0'
api 'com.google.android.material:material:1.2.0-beta01'
api 'com.google.android.material:material:1.2.0-rc01'
api 'com.google.code.gson:gson:2.8.6'
api 'com.jakewharton.timber:timber:4.7.1'
api 'com.mikepenz:fastadapter:5.1.0'
api 'com.mikepenz:fastadapter-extensions-utils:5.1.0'
api 'com.mikepenz:fastadapter:5.2.2'
api 'com.mikepenz:fastadapter-extensions-utils:5.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.7.2'
implementation "com.squareup.okhttp3:okhttp-tls:4.7.2"
implementation 'com.squareup.okhttp3:logging-interceptor:4.8.0'
implementation "com.squareup.okhttp3:okhttp-tls:4.8.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
api "com.squareup.retrofit2:retrofit:2.9.0"
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
}
......@@ -42,11 +42,6 @@ abstract class FastAdapterFragment : BaseFragment() {
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
refreshScreen()
}
override fun refreshScreen() {
adapter.setNewList(getItems())
}
......
......@@ -57,19 +57,19 @@ android {
dependencies {
implementation "androidx.annotation:annotation:1.1.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.7.2"
implementation "com.squareup.okhttp3:okhttp-tls:4.7.2"
implementation "com.squareup.okhttp3:logging-interceptor:4.8.0"
implementation "com.squareup.okhttp3:okhttp-tls:4.8.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
api "com.squareup.retrofit2:retrofit:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.protobuf:protobuf-javalite:3.12.2'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
implementation 'org.bouncycastle:bcprov-jdk15on:1.66'
implementation project(path: ':domain')
implementation project(path: ':robert')
......@@ -78,6 +78,6 @@ dependencies {
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.test:rules:1.2.0"
androidTestImplementation "androidx.test.ext:truth:1.3.0-rc01"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:4.7.2"
androidTestImplementation "org.apache.commons:commons-text:1.8"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:4.8.0"
androidTestImplementation "org.apache.commons:commons-text:1.9"
}
......@@ -26,9 +26,9 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
implementation 'androidx.work:work-runtime-ktx:2.4.0'
implementation 'com.google.code.gson:gson:2.8.6'
......
......@@ -42,8 +42,8 @@ android {
applicationId "fr.gouv.android.stopcovid"
minSdkVersion 21
targetSdkVersion 29
versionCode 41
versionName "1.1.2"
versionCode 43
versionName "1.1.3"
testInstrumentationRunner = 'com.lunabeestudio.stopcovid.TestRunner'
......@@ -64,15 +64,15 @@ android {
animationsDisabled true
}
viewBinding {
enabled = true
}
buildFeatures.viewBinding = true
}
dependencies {
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.work:work-runtime-ktx:2.3.4'
implementation 'androidx.work:work-runtime-ktx:2.4.0'
implementation 'com.airbnb.android:lottie:3.4.1'
implementation('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false }
//noinspection NewerVersionAvailable,GradleDependency (New version require minSdk 24)
......
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* Authors
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Created by Lunabee Studio / Date - 2020/04/05 - for the STOP-COVID project
*/
package com.lunabeestudio.stopcovid.fastitem
import android.animation.Animator
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import com.lunabeestudio.stopcovid.R
import com.lunabeestudio.stopcovid.coreui.fastitem.BaseItem
class LottieItem : BaseItem<LottieItem.ViewHolder>(
R.layout.item_lottie, ::ViewHolder, R.id.item_lottie
) {
var state: State = State.OFF
override fun bindView(holder: ViewHolder, payloads: List<Any>) {
super.bindView(holder, payloads)
holder.onView.removeAllAnimatorListeners()
holder.offView.removeAllAnimatorListeners()
holder.offView.setMinFrame(1)
holder.offView.addLottieOnCompositionLoadedListener {
holder.onView.addLottieOnCompositionLoadedListener {
when (state) {
State.ON -> onCase(holder)
State.OFF -> offCase(holder)
State.OFF_TO_ON -> offToOnCase(holder)
State.ON_TO_OFF -> onToOffCase(holder)
}
}
}
}
private fun onToOffCase(holder: ViewHolder) {
holder.onView.speed = -5f
holder.onView.repeatCount = 0
holder.offView.progress = 1f
holder.offView.repeatCount = 0
holder.offView.speed = -1f
holder.onView.addAnimatorListener(object : EndAnimatorListener {
override fun onAnimationEnd(animation: Animator?) {
holder.onView.removeAnimatorListener(this)
holder.offView.addAnimatorListener(object : EndAnimatorListener {
override fun onAnimationEnd(animation: Animator?) {
holder.offView.removeAnimatorListener(this)
state = State.OFF
offCase(holder)
}
})
holder.offView.isVisible = true
holder.onView.isInvisible = true
holder.offView.post {
holder.offView.playAnimation()
}
}
})
holder.onView.isVisible = true
holder.offView.isInvisible = true
}
private fun offToOnCase(holder: ViewHolder) {
holder.offView.frame = 1
holder.offView.speed = 1f
holder.offView.repeatCount = 0
holder.offView.addAnimatorListener(object : EndAnimatorListener {
override fun onAnimationEnd(animation: Animator?) {
holder.offView.removeAnimatorListener(this)
state = State.ON
onCase(holder)
}
})
holder.offView.isVisible = true