Commit 6122b3db authored by stopcovid@lunabee.com's avatar stopcovid@lunabee.com
Browse files

Update to 3.1.3

- Improve report performance
- Fix regex issue thanks to LOVASOA
- Improve BLE stability on device without advertising
- App update available info
- Wallet v2
- Status loading indicator
- Widget proximity & user status
- Widget attestations
parent 568487ad
......@@ -21,6 +21,10 @@ Component: Barcode scanning library for Android, using ZXing for decoding
License Text URL: http://www.apache.org/licenses/LICENSE-2.0
Source Code: https://github.com/journeyapps/zxing-android-embedded
Component: PhotoView
License Text URL: http://www.apache.org/licenses/LICENSE-2.0
Source Code: https://github.com/Baseflow/PhotoView
================================================================================
BSD 3-Clause
================================================================================
......
......@@ -58,7 +58,7 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:_'
implementation "com.squareup.okhttp3:okhttp-tls:_"
implementation "com.squareup.retrofit2:converter-gson:_"
implementation "com.squareup.retrofit2:converter-moshi:_"
implementation "com.squareup.retrofit2:retrofit:_"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:_"
......
......@@ -18,14 +18,18 @@ private const val SHARED_PREFS_PROXIMITY_START_TIME: String = "Shared.Prefs.Prox
private const val SHARED_PREFS_PROXIMITY_ACTIVE_DURATION: String = "Shared.Prefs.Proximity.Active.Duration"
private const val SHARED_PREFS_STATUS_SUCCESS_COUNT: String = "Shared.Prefs.Status.Success.Count"
private const val SHARED_PREFS_IS_OPT_IN: String = "Shared.Prefs.Is.Opt.In"
private const val SHARED_PREFS_DELETE_ANALYTICS_AFTER_NEXT_STATUS: String = "Shared.Prefs.Delete.Analytics.After.Next.Status"
var SharedPreferences.installationUUID: String?
get() = getString(SHARED_PREFS_INSTALLATION_UUID, null)
set(value) = edit {
if (value.isNullOrBlank()) {
remove(SHARED_PREFS_INSTALLATION_UUID)
} else {
putString(SHARED_PREFS_INSTALLATION_UUID, value)
if (value != getString(SHARED_PREFS_INSTALLATION_UUID, null)) {
deleteAnalyticsAfterNextStatus = false
if (value.isNullOrBlank()) {
remove(SHARED_PREFS_INSTALLATION_UUID)
} else {
putString(SHARED_PREFS_INSTALLATION_UUID, value)
}
}
}
......@@ -59,4 +63,10 @@ var SharedPreferences.isOptIn: Boolean
get() = getBoolean(SHARED_PREFS_IS_OPT_IN, true)
set(value) = edit {
putBoolean(SHARED_PREFS_IS_OPT_IN, value)
}
var SharedPreferences.deleteAnalyticsAfterNextStatus: Boolean
get() = getBoolean(SHARED_PREFS_DELETE_ANALYTICS_AFTER_NEXT_STATUS, true)
set(value) = edit {
putBoolean(SHARED_PREFS_DELETE_ANALYTICS_AFTER_NEXT_STATUS, value)
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ import android.content.SharedPreferences
import android.os.Build
import androidx.core.util.AtomicFile
import androidx.lifecycle.LifecycleObserver
import com.lunabeestudio.analytics.extension.deleteAnalyticsAfterNextStatus
import com.lunabeestudio.analytics.extension.installationUUID
import com.lunabeestudio.analytics.extension.isOptIn
import com.lunabeestudio.analytics.extension.proximityActiveDuration
......@@ -68,6 +69,11 @@ object AnalyticsManager : LifecycleObserver {
getSharedPrefs(context).isOptIn = isOptIn
}
fun requestDeleteAnalytics(context: Context) {
reportAppEvent(context, AppEventName.e17)
getSharedPrefs(context).deleteAnalyticsAfterNextStatus = true
}
fun init(context: Context) {
getSharedPrefs(context).apply {
if (installationUUID == null) {
......@@ -135,6 +141,9 @@ object AnalyticsManager : LifecycleObserver {
} else {
reset(context)
}
if (getSharedPrefs(context).deleteAnalyticsAfterNextStatus) {
sendDeleteAnalytics(context, analyticsInfosProvider, token)
}
}
private suspend fun sendAppAnalytics(
......@@ -143,11 +152,11 @@ object AnalyticsManager : LifecycleObserver {
token: String,
receivedHelloMessagesCount: Int
) {
val appInfos = getAppInfos(analyticsInfosProvider, receivedHelloMessagesCount)
val appInfos = getAppInfos(context, analyticsInfosProvider, receivedHelloMessagesCount)
val appEvents = getAppEvents(context)
val appErrors = getErrors(context.filesDir)
val sendAnalyticsRQ = SendAnalyticsRQ(
installationUuid = sharedPreferences.installationUUID ?: UUID.randomUUID().toString(),
installationUuid = getSharedPrefs(context).installationUUID ?: UUID.randomUUID().toString(),
infos = appInfos,
events = appEvents.toAPI(),
errors = appErrors.toAPI()
......@@ -216,6 +225,35 @@ object AnalyticsManager : LifecycleObserver {
}
}
private suspend fun sendDeleteAnalytics(
context: Context,
analyticsInfosProvider: AnalyticsInfosProvider,
token: String
) {
withContext(Dispatchers.IO) {
getSharedPrefs(context).installationUUID?.let { installationUUID ->
val result = AnalyticsServerManager.deleteAnalytics(
context,
analyticsInfosProvider.getBaseUrl(),
analyticsInfosProvider.getCertificateSha256(),
analyticsInfosProvider.getApiVersion(),
token,
installationUUID,
)
when (result) {
is AnalyticsResult.Success -> {
withContext(Dispatchers.Main) {
getSharedPrefs(context).deleteAnalyticsAfterNextStatus = false
}
}
is AnalyticsResult.Failure -> {
Timber.e(result.error)
}
}
}
}
}
fun reset(context: Context) {
resetAppEvents(context)
resetHealthEvents(context)
......@@ -274,6 +312,7 @@ object AnalyticsManager : LifecycleObserver {
}
private fun getAppInfos(
context: Context,
infosProvider: AnalyticsInfosProvider,
receivedHelloMessagesCount: Int
): AppInfos {
......@@ -288,7 +327,7 @@ object AnalyticsManager : LifecycleObserver {
placesCount = infosProvider.getPlacesCount(),
formsCount = infosProvider.getFormsCount(),
certificatesCount = infosProvider.getCertificatesCount(),
statusSuccessCount = sharedPreferences.statusSuccessCount,
statusSuccessCount = getSharedPrefs(context).statusSuccessCount,
userHasAZipcode = infosProvider.userHaveAZipCode(),
)
}
......
......@@ -28,4 +28,5 @@ enum class AppEventName {
e14,
e15,
e16,
e17,
}
\ No newline at end of file
......@@ -14,8 +14,10 @@ import com.lunabeestudio.analytics.network.model.SendAnalyticsRQ
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
internal interface AnalyticsApi {
......@@ -24,4 +26,10 @@ internal interface AnalyticsApi {
@Path("apiVersion") apiVersion: String,
@Body body: SendAnalyticsRQ,
): Response<ResponseBody>
@DELETE("api/{apiVersion}/analytics")
suspend fun deleteAnalytics(
@Path("apiVersion") apiVersion: String,
@Query("installationUuid") installationUuid: String,
): Response<ResponseBody>
}
\ No newline at end of file
......@@ -12,8 +12,8 @@ package com.lunabeestudio.analytics.network
import android.content.Context
import android.os.Build
import com.google.gson.GsonBuilder
import com.lunabeestudio.analytics.BuildConfig
import com.lunabeestudio.analytics.manager.AnalyticsManager
import com.lunabeestudio.analytics.model.AnalyticsResult
import com.lunabeestudio.analytics.network.model.SendAnalyticsRQ
import okhttp3.CertificatePinner
......@@ -26,7 +26,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.tls.HandshakeCertificates
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
......@@ -37,7 +37,7 @@ internal object AnalyticsServerManager {
private fun getRetrofit(context: Context, baseUrl: String, certificateSha256: String, token: String): AnalyticsApi {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient.Builder().apply {
if (!BuildConfig.DEBUG) {
val requireTls12 = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)
......@@ -91,6 +91,36 @@ internal object AnalyticsServerManager {
}
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun deleteAnalytics(
context: Context,
baseUrl: String,
certificateSha256: String,
apiVersion: String,
token: String,
installationUuid: String,
): AnalyticsResult {
return try {
val result = getRetrofit(context, baseUrl, certificateSha256, token).deleteAnalytics(apiVersion, installationUuid)
if (result.isSuccessful) {
AnalyticsResult.Success()
} else {
AnalyticsManager.reportWSError(context, context.filesDir, "analytics", apiVersion, result.code(), result.message())
AnalyticsResult.Failure(HttpException(result))
}
} catch (e: Exception) {
AnalyticsManager.reportWSError(
context,
context.filesDir,
"analytics",
apiVersion,
(e as? HttpException)?.code() ?: 0,
e.message
)
AnalyticsResult.Failure(e)
}
}
private fun certificateFromString(context: Context, fileName: String): X509Certificate {
return CertificateFactory.getInstance("X.509").generateCertificate(
context.resources.openRawResource(
......
......@@ -19,7 +19,7 @@ android {
minSdkVersion 21
targetSdkVersion 30
buildConfigField 'String', 'BLE_VERSION', '"2.1.2"'
buildConfigField 'String', 'BLE_VERSION', '"2.2.0"'
}
compileOptions {
......
......@@ -16,14 +16,6 @@ data class ProximityNotificationError(
val cause: String? = null
) {
companion object {
/**
* Root error code when too much BLE operation fail which may indicate that Bluetooth Stack
* is unhealthy.
*/
const val UNHEALTHY_BLUETOOTH_ERROR_CODE = 1000
}
enum class Type {
/**
* BLE advertising error
......@@ -39,5 +31,10 @@ data class ProximityNotificationError(
* BLE gatt error
*/
BLE_GATT,
/**
* BLE proximity notification component error
*/
BLE_PROXIMITY_NOTIFICATION
}
}
\ No newline at end of file
......@@ -33,11 +33,9 @@ enum class ProximityNotificationEventId(val category: Category) {
BLE_GATT_CONNECT_ERROR(BLE_GATT),
BLE_GATT_CONNECT_SUCCESS(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI_TIMEOUT(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI_ERROR(BLE_GATT),
BLE_GATT_REQUEST_REMOTE_RSSI_SUCCESS(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD_TIMEOUT(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD_ERROR(BLE_GATT),
BLE_GATT_EXCHANGE_PAYLOAD_SUCCESS(BLE_GATT),
BLE_GATT_ON_CHARACTERISTIC_WRITE_REQUEST(BLE_GATT),
......@@ -57,14 +55,12 @@ enum class ProximityNotificationEventId(val category: Category) {
PROXIMITY_NOTIFICATION_START_BLE(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_STOP(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_STOP_BLE(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_RESTART(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_PAYLOAD_UPDATED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_BLE_SETTINGS_UPDATED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_BLUETOOTH_DISABLED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_BLUETOOTH_ENABLED(PROXIMITY_NOTIFICATION),
PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH(PROXIMITY_NOTIFICATION),
BLE_PROXIMITY_NOTIFICATION_WITHOUT_ADVERTISER(PROXIMITY_NOTIFICATION),
BLE_PROXIMITY_NOTIFICATION_FACTORY(PROXIMITY_NOTIFICATION);
......
......@@ -10,22 +10,25 @@
package com.orange.proximitynotification
import android.annotation.SuppressLint
import android.app.Notification
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
import android.os.PowerManager
import com.orange.proximitynotification.ble.BleProximityNotification
import com.orange.proximitynotification.ble.BleProximityNotificationFactory
import com.orange.proximitynotification.ble.BleSettings
import com.orange.proximitynotification.tools.BluetoothStateBroadcastReceiver
import com.orange.proximitynotification.tools.waitForState
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
......@@ -42,10 +45,19 @@ abstract class ProximityNotificationService : Service(),
private var bleProximityNotification: BleProximityNotification? = null
private var bluetoothStateBroadcastReceiver: BluetoothStateBroadcastReceiver? = null
private val restartInProgress = AtomicBoolean(false)
private val bluetoothRestartInProgress = AtomicBoolean(false)
private var restartBTWakeLock: PowerManager.WakeLock? = null
val isBluetoothRestartInProgress: Boolean
get() = bluetoothRestartInProgress.get()
val couldRestartFrequently: Boolean
get() = bleProximityNotification?.couldRestartFrequently == true
val shouldRestart: Boolean
get() = bleProximityNotification?.shouldRestartBluetooth == true
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default + job
......@@ -57,6 +69,14 @@ abstract class ProximityNotificationService : Service(),
private val bluetoothAdapter: BluetoothAdapter
get() = BluetoothAdapter.getDefaultAdapter()
private val cachedProximityPayloadIdProvider by lazy {
ProximityPayloadIdProviderWithCache(
this@ProximityNotificationService,
maxSize = bleSettings.maxCacheSize,
expiringTime = bleSettings.identityCacheTimeout
)
}
override fun onCreate() {
super.onCreate()
ProximityNotificationLogger.registerListener(this)
......@@ -75,7 +95,7 @@ abstract class ProximityNotificationService : Service(),
}
/**
* Starts ProximityNotification and foreground service.
* Starts [ProximityNotification] and foreground service.
*
* It registers a [BluetoothStateBroadcastReceiver] in order to be notified of state changes
* @see BluetoothStateBroadcastReceiver
......@@ -91,7 +111,7 @@ abstract class ProximityNotificationService : Service(),
}
/**
* Stops ProximityNotification and foreground service.
* Stops [ProximityNotification] and foreground service.
*
* It unregisters [BluetoothStateBroadcastReceiver] if any registered
* @see BluetoothStateBroadcastReceiver
......@@ -106,6 +126,39 @@ abstract class ProximityNotificationService : Service(),
unregisterBluetoothBroadcastReceiver()
}
/**
* Restart [ProximityNotification]
*
* If will restart Bluetooth stack if needed by [BleProximityNotification].
*/
suspend fun restart() {
if (restartInProgress.get()) {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART,
"Restart Proximity Notification already in progress"
)
return
}
if (bleProximityNotification?.isRunning != true) {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART,
"Restart Proximity Notification aborted since it is not running"
)
return
}
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART,
"Restart Proximity Notification"
)
doRestart()
}
/**
* Notify that [ProximityPayload] provided by [ProximityPayloadProvider] has changed.
*
......@@ -121,67 +174,65 @@ abstract class ProximityNotificationService : Service(),
bleProximityNotification?.notifyPayloadUpdated(proximityPayload)
}
/**
* Notify that [BleSettings] have changed.
* It will restart [BleProximityNotification] if it is running
*
*/
suspend fun notifyBleSettingsUpdate() {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_BLE_SETTINGS_UPDATED,
"BLE settings updated"
)
protected open fun doStart() {
launch(Dispatchers.Main.immediate + NonCancellable + exceptionHandler) {
startForeground(foregroundNotificationId, buildForegroundServiceNotification())
startBleProximityNotification()
}
}
if (isRunning()) {
protected open fun doStop() {
launch(Dispatchers.Main.immediate + NonCancellable + exceptionHandler) {
stopBleProximityNotification()
startBleProximityNotification()
stopForeground(true)
}
}
private suspend fun doRestart() {
try {
restartInProgress.set(true)
/**
* Force [BleProximityNotification] to restart.
*
* If will restart Bluetooth stack if needed by [BleProximityNotification].
* Otherwise it will restart [BleProximityNotification] by calling notifyBleSettingsUpdate
*/
suspend fun restart() {
val shouldRestartBluetooth =
bleProximityNotification?.shouldRestartBluetooth == true
if (isRunning()) {
if (bleProximityNotification?.shouldRestartBluetooth == true) {
restartBluetooth()
} else {
notifyBleSettingsUpdate()
withContext(Dispatchers.Main.immediate) {
stopBleProximityNotification()
}
}
}
/**
* @return true if ProximityNotification is running, false otherwise
*/
fun isRunning() = bleProximityNotification?.isRunning == true
protected open fun doStart() {
launch(Dispatchers.Main.immediate + NonCancellable + exceptionHandler) {
if (shouldRestartBluetooth) {
restartBluetooth()
}
if (!isBluetoothRestartInProgress) {
startForeground(foregroundNotificationId, buildForegroundServiceNotification())
withContext(Dispatchers.Main.immediate) {
check(bluetoothAdapter.isEnabled) { "Bluetooth is not enabled" }
startBleProximityNotification()
}
startBleProximityNotification()
}
}
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART,
"Restart Proximity Notification - success"
)
protected open fun doStop() {
launch(Dispatchers.Main.immediate + NonCancellable + exceptionHandler) {
stopBleProximityNotification()
} catch (t: Throwable) {
ProximityNotificationLogger.error(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART,
"Restart Proximity Notification - failure",
t
)
if (!isBluetoothRestartInProgress) {
stopForeground(true)
}
onError(
ProximityNotificationError(
ProximityNotificationError.Type.BLE_PROXIMITY_NOTIFICATION,
cause = "Restart failed (throwable = $t)"
)
)
} finally {
restartInProgress.set(false)
}
}
private fun registerBluetoothBroadcastReceiver() {
bluetoothRestartInProgress.set(false)
bluetoothStateBroadcastReceiver = BluetoothStateBroadcastReceiver(
......@@ -217,7 +268,7 @@ abstract class ProximityNotificationService : Service(),
BleProximityNotificationFactory.build(this, bleSettings, this).apply {
setUp(
this@ProximityNotificationService,
this@ProximityNotificationService,
cachedProximityPayloadIdProvider,
this@ProximityNotificationService
)
start()
......@@ -239,37 +290,80 @@ abstract class ProximityNotificationService : Service(),
* Beware, on some devices it could show a confirmation popup while disabling / enabling
* Bluetooth.
*/
protected suspend fun restartBluetooth() {
private suspend fun restartBluetooth(): Boolean {
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth"
)
withContext(Dispatchers.Main) {
try {
bluetoothRestartInProgress.set(true)
unregisterBluetoothBroadcastReceiver()
restartBTWakeLock?.takeIf { it.isHeld }?.release()
@SuppressLint("WakelockTimeout")
restartBTWakeLock = (getSystemService(Context.POWER_SERVICE) as? PowerManager)
?.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"ProximityNotificationService:RestartBTWakeLock"
)
?.apply { acquire() }
if (bluetoothRestartInProgress.getAndSet(true)) {
// Disable Bluetooth
bluetoothAdapter.waitForState(BluetoothAdapter.STATE_OFF) { iteration ->
val status = withContext(Dispatchers.Main) { bluetoothAdapter.disable() }
ProximityNotificationLogger.info(
ProximityNotificationEventId.PROXIMITY_NOTIFICATION_RESTART_BLUETOOTH,
"Restart Bluetooth - already in progress"
"Restart Bluetooth - disabling (status=$status, iteration=$iteration)"
)
}