Attention une mise à jour du service Gitlab va être effectuée le mardi 18 janvier (et non lundi 17 comme annoncé précédemment) entre 18h00 et 18h30. Cette mise à jour va générer une interruption du service dont nous ne maîtrisons pas complètement la durée mais qui ne devrait pas excéder quelques minutes.

Commit 05599b00 authored by stopcovid@lunabee.com's avatar stopcovid@lunabee.com
Browse files

Update to 2.2.0

- Isolation module
- Exemption certificates
- Contact tracing reminders
- Useful links
- key figures
- News sharing
- Venues
- Private events
- Performance improvements
- UI enhancing
- Bug fixes
parent 2964dcee
......@@ -14,7 +14,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "com.android.tools.build:gradle:_"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:_"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:_"
classpath "com.karumi:shot:_"
......
......@@ -31,7 +31,7 @@ android {
targetSdkVersion 30
buildConfigField 'String', 'BASE_URL', '"https://app.stopcovid.gouv.fr"'
buildConfigField 'String', 'SERVER_URL', 'BASE_URL+"/json/version-25/"'
buildConfigField 'String', 'SERVER_URL', 'BASE_URL+"/json/version-26/"'
buildConfigField 'String', 'SERVER_CERTIFICATE_SHA256', '"sha256/sXQojvwsiyblrpMQIVRXGC5u7AgknzTJm+VIK1kQmD8="'
buildConfigField 'String', 'CONFIG_JSON', '"config.json"'
}
......
......@@ -31,8 +31,11 @@ object UiConstants {
TIME("error", 5),
BLUETOOTH("error", 6),
NEWS("news", 7),
REMINDER("reminder", 8)
ACTIVATE_REMINDER("reminder", 8),
ISOLATION_REMINDER("reminder", 9)
}
val SUPPORTED_LANGUAGE: Array<String> = arrayOf("ar", "de", "en", "es", "fr", "it", "pt")
const val DEFAULT_LANGUAGE: String = "en"
}
\ No newline at end of file
......@@ -15,6 +15,8 @@ import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.lunabeestudio.stopcovid.coreui.UiConstants
import java.util.Locale
fun Context.isNightMode(): Boolean {
return when (resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)) {
......@@ -32,9 +34,11 @@ fun Context.isNightMode(): Boolean {
* @param subject The subject to use in the email
* @param body The body to use in the email
*/
fun Context.startEmailIntent(emailAddress: String,
fun Context.startEmailIntent(
emailAddress: String,
subject: String? = null,
body: String? = null) {
body: String? = null,
) {
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:")
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(emailAddress))
......@@ -47,16 +51,31 @@ fun Context.startEmailIntent(emailAddress: String,
startActivity(Intent.createChooser(intent, null))
}
fun Context.showPermissionRationale(strings: Map<String, String>,
fun Context.showPermissionRationale(
strings: Map<String, String>,
messageKey: String,
positiveKey: String,
positiveAction: () -> Unit) {
cancelable: Boolean,
positiveAction: () -> Unit,
negativeAction: (() -> Unit)?,
) {
MaterialAlertDialogBuilder(this)
.setTitle(strings["common.permissionsNeeded"])
.setMessage(strings[messageKey])
.setCancelable(cancelable)
.setPositiveButton(strings[positiveKey]) { _, _ ->
positiveAction()
}
.setNegativeButton(strings["common.cancel"], null)
.setNegativeButton(strings["common.cancel"]) { _, _ ->
negativeAction?.invoke()
}
.show()
}
fun Context.getFirstSupportedLanguage(): String {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
resources.configuration.locales.getFirstMatch(UiConstants.SUPPORTED_LANGUAGE)?.language ?: UiConstants.DEFAULT_LANGUAGE
} else {
Locale.getDefault().language.takeIf { UiConstants.SUPPORTED_LANGUAGE.contains(it) } ?: UiConstants.DEFAULT_LANGUAGE
}
}
\ No newline at end of file
......@@ -14,9 +14,35 @@ import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import timber.log.Timber
fun Fragment.openAppSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", requireActivity().packageName, null)
startActivity(intent)
}
\ No newline at end of file
}
/**
* Find a [NavController] given a [Fragment]
*
* Calling this on a Fragment that is not a [NavHostFragment] or within a [NavHostFragment]
* will return null.
*/
fun Fragment.findNavControllerOrNull(): NavController? =
try {
NavHostFragment.findNavController(this)
} catch (e: IllegalStateException) {
Timber.e(e, "Failed to find the NavController")
null
}
fun Fragment.viewLifecycleOwnerOrNull(): LifecycleOwner? =
try {
viewLifecycleOwner
} catch (e: IllegalStateException) {
Timber.e(e, "Failed to get lifecycle owner")
null
}
\ No newline at end of file
......@@ -79,4 +79,14 @@ fun String.fixFormatter() = this
.replace("%@", "%s")
.replace(Regex("%\\d\\$@")) { matchResult ->
matchResult.value.replace('@', 's')
}
\ No newline at end of file
}
fun String?.formatWithSameValue(value: Any?): String? {
return this?.let {
val placeholder = "%s"
val cleanedString = it.replace(placeholder, "")
val count = (this.count() - cleanedString.count()) / placeholder.count()
val values = Array(count) { value }
return String.format(it, *values)
}
}
......@@ -15,6 +15,7 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import androidx.fragment.app.Fragment
import com.lunabeestudio.robert.extension.observeEventAndConsume
import com.lunabeestudio.stopcovid.coreui.extension.stringsFormat
import com.lunabeestudio.stopcovid.coreui.manager.StringsManager
import kotlin.time.Duration
......@@ -27,14 +28,15 @@ abstract class BaseFragment : Fragment() {
abstract fun refreshScreen()
protected val strings: HashMap<String, String>
val strings: HashMap<String, String>
get() = StringsManager.strings
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
StringsManager.liveStrings.observe(viewLifecycleOwner) {
StringsManager.liveStrings.observeEventAndConsume(viewLifecycleOwner) {
refreshScreen()
}
refreshScreen()
}
protected fun stringsFormat(key: String, vararg args: Any?): String? {
......
......@@ -15,6 +15,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
......@@ -23,6 +24,10 @@ import com.lunabeestudio.stopcovid.coreui.extension.closeKeyboardOnScroll
import com.mikepenz.fastadapter.GenericItem
import com.mikepenz.fastadapter.adapters.FastItemAdapter
import com.mikepenz.fastadapter.adapters.GenericFastItemAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
abstract class FastAdapterFragment : BaseFragment() {
protected var binding: FragmentRecyclerViewBinding? = null
......@@ -30,11 +35,12 @@ abstract class FastAdapterFragment : BaseFragment() {
protected abstract fun getItems(): List<GenericItem>
protected abstract fun getAppBarLayout(): AppBarLayout?
private var onScrollListener: RecyclerView.OnScrollListener? = null
private var refreshScreenJob: Job? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? {
adapter.attachDefaultListeners = false
binding = FragmentRecyclerViewBinding.inflate(inflater, container, false)
......@@ -45,12 +51,16 @@ abstract class FastAdapterFragment : BaseFragment() {
}
override fun refreshScreen() {
val items = getItems()
if (items.isEmpty()) {
showEmpty()
} else {
adapter.setNewList(items)
showData()
refreshScreenJob?.cancel()
refreshScreenJob = viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
delay(10)
val items = getItems()
if (items.isEmpty()) {
showEmpty()
} else {
adapter.setNewList(items)
showData()
}
}
}
......
......@@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.lunabeestudio.stopcovid.coreui.BuildConfig
import com.lunabeestudio.stopcovid.coreui.UiConstants
import com.lunabeestudio.stopcovid.coreui.extension.getFirstSupportedLanguage
import com.lunabeestudio.stopcovid.coreui.extension.saveTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
......@@ -40,7 +41,7 @@ abstract class ServerManager {
protected suspend fun fetchLast(context: Context, forceRefresh: Boolean): Boolean {
return if (shouldRefresh(context) || forceRefresh) {
fetchLast(context, Locale.getDefault().language)
fetchLast(context, context.getFirstSupportedLanguage())
} else {
Timber.v("Only use local data")
false
......@@ -48,12 +49,10 @@ abstract class ServerManager {
}
protected suspend fun <T> loadLocal(context: Context): T? {
val currentLanguage = Locale.getDefault().language
val currentLanguage = context.getFirstSupportedLanguage()
return loadFromFiles(context, currentLanguage)
?: loadFromAssets(context, currentLanguage)
?: loadFromFiles(context, UiConstants.DEFAULT_LANGUAGE)
?: loadFromAssets(context, UiConstants.DEFAULT_LANGUAGE)
}
private suspend fun fetchLast(context: Context, languageCode: String): Boolean {
......@@ -114,7 +113,7 @@ abstract class ServerManager {
}
fun clearLocal(context: Context) {
val filename = "${prefix(context)}${Locale.getDefault().language}${extension()}"
val filename = "${prefix(context)}${context.getFirstSupportedLanguage()}${extension()}"
File(context.filesDir, filename).delete()
val defaultFilename = "${prefix(context)}${UiConstants.DEFAULT_LANGUAGE}${extension()}"
File(context.filesDir, defaultFilename).delete()
......
......@@ -14,38 +14,39 @@ import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.reflect.TypeToken
import com.lunabeestudio.robert.utils.Event
import com.lunabeestudio.stopcovid.coreui.R
import com.lunabeestudio.stopcovid.coreui.UiConstants
import com.lunabeestudio.stopcovid.coreui.extension.fixFormatter
import com.lunabeestudio.stopcovid.coreui.extension.getFirstSupportedLanguage
import java.lang.reflect.Type
import java.util.Locale
object StringsManager : ServerManager() {
var strings: HashMap<String, String> = hashMapOf()
private set(value) {
if (field != value) {
_liveStrings.postValue(value)
_liveStrings.postValue(Event(value))
}
field = value
}
private val _liveStrings: MutableLiveData<HashMap<String, String>> = MutableLiveData()
val liveStrings: LiveData<HashMap<String, String>>
private val _liveStrings: MutableLiveData<Event<HashMap<String, String>>> = MutableLiveData()
val liveStrings: LiveData<Event<HashMap<String, String>>>
get() = _liveStrings
private var prevLanguage: String? = null
suspend fun initialize(context: Context) {
prevLanguage = Locale.getDefault().language
prevLanguage = context.getFirstSupportedLanguage()
loadLocal<HashMap<String, String>>(context)?.let {
strings = it
}
}
suspend fun onAppForeground(context: Context) {
val languageHasChanged = prevLanguage != Locale.getDefault().language
prevLanguage = Locale.getDefault().language
val newLanguage = context.getFirstSupportedLanguage()
val languageHasChanged = prevLanguage != newLanguage
if (languageHasChanged) {
loadLocal<HashMap<String, String>>(context)?.let {
......@@ -56,6 +57,7 @@ object StringsManager : ServerManager() {
val hasFetch = fetchLast(context, languageHasChanged)
if (hasFetch) {
loadLocal<HashMap<String, String>>(context)?.let {
prevLanguage = newLanguage
strings = it
}
}
......
......@@ -13,5 +13,7 @@
<item name="colorPrimary">@color/color_malibu</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
<item name="dividerColor">@color/color_mine_shaft</item>
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="dividerColor" format="color" />
</resources>
\ No newline at end of file
......@@ -27,9 +27,13 @@
<color name="color_persian_green">#00AC8C</color>
<color name="color_mountain_meadow">#169B62</color>
<color name="color_persimmon">#FF6F4D</color>
<color name="color_orange">#FD704E</color>
<color name="color_trinidad">#E14800</color>
<color name="color_monza">#E1000F</color>
<color name="color_east_bay">#484D7A</color>
<color name="color_neon_carrot">#FF9940</color>
<color name="color_persian_blue">#0F31C8</color>
<color name="color_malibu">#85BBFF</color>
<color name="color_mercury">#E6E6E6</color>
<color name="color_mine_shaft">#313131</color>
</resources>
......@@ -35,6 +35,8 @@
<item name="materialCardViewStyle">@style/Widget.App.CardView</item>
<item name="bottomSheetDialogTheme">@style/ThemeOverlay.StopCovid.BottomSheetDialog</item>
<item name="dividerColor">@color/color_mercury</item>
</style>
<!-- Base application theme. -->
......
......@@ -77,6 +77,10 @@
<item name="android:textAllCaps">false</item>
</style>
<style name="Theme.StopCovid.Link.Inverse">
<item name="android:textColor">?colorOnPrimary</item>
</style>
<style name="Theme.StopCovid.Link.Big">
<item name="android:textAppearance">@style/TextAppearance.StopCovid.Title</item>
</style>
......
/*
* 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 TOUS-ANTI-COVID project
*/
package com.lunabeestudio.domain.model
class ReportResponse(
val reportValidationToken: String?,
)
\ No newline at end of file
package com.lunabeestudio.domain.model
data class VenueQrCode(
val id: String,
val uuid: String,
val qrType: VenueQrType,
val venueType: String,
val ntpTimestamp: Long,
val venueCategory: Int?,
val venueCapacity: Int?,
val payload: String
)
enum class VenueQrType(val value: Int) {
STATIC(0), DYNAMIC(1);
companion object {
fun fromValue(value: Int): VenueQrType? {
return when (value) {
0 -> STATIC
1 -> DYNAMIC
else -> null
}
}
}
}
\ 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 Lunabee Studio / Date - 2020/04/05 - for the TOUS-ANTI-COVID project
*/
package com.lunabeestudio.domain.model
class WStatusReport(
val atRisk: Boolean,
)
......@@ -26,9 +26,12 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
buildConfigField 'String', 'BASE_URL', '"https://api.stopcovid.gouv.fr"'
buildConfigField 'String', 'WARNING_BASE_URL', '"https://tacw.tousanticovid.gouv.fr"'
buildConfigField 'String', 'CERTIFICATE_SHA256', '"sha256/Up+TDyVDu8vKvd22TeAnXYxQqfPd2oNOU9Y04JahHpQ="'
buildConfigField 'String', 'WARNING_CERTIFICATE_SHA256', '"sha256/b7w+uqyD+XILNIlRc3XVmEROwFCVTv5yOchb2i5FJbo="'
buildConfigField 'String', 'URL_PATH', '"api/v1"'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
......
......@@ -13,6 +13,7 @@ package com.lunabeestudio.framework.local
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.random.Random
......@@ -25,6 +26,14 @@ class LocalCryptoManagerTest {
fun init() {
val context = ApplicationProvider.getApplicationContext<Context>()
localCryptoManager = LocalCryptoManager(context)
context.getSharedPreferences("crypto_prefs", Context.MODE_PRIVATE).edit().remove("secret_key_generated").commit()
}
@After
fun deInit() {
val context = ApplicationProvider.getApplicationContext<Context>()
context.getSharedPreferences("crypto_prefs", Context.MODE_PRIVATE).edit().remove("secret_key_generated").commit()
}
@Test
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment