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

Update to 2.2.1

- Code refactorization
- Allow empty venueCategory or venueCapacity in deeplink
- Add vaccination module
- Improve splashscreen
parent be9bbef6
......@@ -38,6 +38,10 @@ android {
includeAndroidResources = true
}
}
lintOptions {
disable "GradleDependency"
}
}
dependencies {
......
......@@ -17,7 +17,6 @@ buildscript {
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:_"
classpath "com.google.protobuf:protobuf-gradle-plugin:_"
classpath "io.realm:realm-gradle-plugin:_"
......
......@@ -44,6 +44,11 @@ android {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
buildFeatures.viewBinding = true
lintOptions {
disable "GradleDependency",
"VectorPath"
}
}
dependencies {
......@@ -78,5 +83,6 @@ dependencies {
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:_"
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:_'
implementation project(path: ':domain')
implementation project(path: ':robert')
}
......@@ -21,26 +21,8 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import retrofit2.HttpException
import retrofit2.Response
import timber.log.Timber
import java.io.File
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun String.download(context: Context): okhttp3.Response {
return withContext(Dispatchers.IO) {
val okHttpClient = OkHttpClient.getDefaultOKHttpClient(context, this@download, BuildConfig.SERVER_CERTIFICATE_SHA256)
val request: Request = Request.Builder()
.url(this@download)
.build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful && response.body != null) {
response
} else {
Timber.e(response.body?.string())
throw HttpException(Response.error<Any>(response.body!!, response))
}
}
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun String.saveTo(context: Context, file: File) {
withContext(Dispatchers.IO) {
......@@ -75,7 +57,7 @@ fun String?.safeEmojiSpanify(): CharSequence? {
}
}
fun String.fixFormatter() = this
fun String.fixFormatter(): String = this
.replace("%@", "%s")
.replace(Regex("%\\d\\$@")) { matchResult ->
matchResult.value.replace('@', 's')
......
package com.lunabeestudio.stopcovid.coreui.extension
import android.view.View
import android.widget.TextView
fun TextView.setTextOrHide(
value: String?,
ifVisibleBlock: (TextView.() -> Unit)? = null
) {
if (value.isNullOrEmpty()) {
visibility = View.GONE
} else {
text = value
ifVisibleBlock?.let { apply(it) }
visibility = View.VISIBLE
}
}
\ No newline at end of file
......@@ -61,7 +61,7 @@ fun View.showBottomSheet() {
fun View.showSnackBar(
message: String,
duration: Int = Snackbar.LENGTH_LONG,
errorSnackBar: Boolean = false
errorSnackBar: Boolean = false,
) {
this.showSnackBar(message, duration) {
if (errorSnackBar) {
......@@ -76,7 +76,7 @@ fun View.showSnackBar(
inline fun View.showSnackBar(
message: String,
length: Int = Snackbar.LENGTH_LONG,
f: Snackbar.() -> Unit
f: Snackbar.() -> Unit,
) {
val snack = Snackbar.make(this, message, length)
snack.f()
......@@ -86,4 +86,13 @@ inline fun View.showSnackBar(
fun View.addRipple(): Unit = with(TypedValue()) {
context.theme.resolveAttribute(R.attr.selectableItemBackground, this, true)
setBackgroundResource(resourceId)
}
fun View.setOnClickListenerOrHideRipple(onClickListener: View.OnClickListener?) {
if (onClickListener != null) {
addRipple()
} else {
background = null
}
setOnClickListener(onClickListener)
}
\ No newline at end of file
package com.lunabeestudio.stopcovid.coreui.fastitem
import android.content.res.ColorStateList
import android.util.LayoutDirection
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getColor
import androidx.core.view.isVisible
import androidx.core.widget.TextViewCompat
import com.lunabeestudio.stopcovid.coreui.databinding.ItemActionBinding
import com.lunabeestudio.stopcovid.coreui.databinding.ItemCardWithActionsBinding
import com.lunabeestudio.stopcovid.coreui.extension.safeEmojiSpanify
import com.lunabeestudio.stopcovid.coreui.extension.setOnClickListenerOrHideRipple
import com.lunabeestudio.stopcovid.coreui.extension.setTextOrHide
import com.lunabeestudio.stopcovid.coreui.model.Action
import com.lunabeestudio.stopcovid.coreui.model.CardTheme
import com.lunabeestudio.stopcovid.extension.setImageResourceOrHide
import com.mikepenz.fastadapter.binding.AbstractBindingItem
class CardWithActionsItem(private val cardTheme: CardTheme) : AbstractBindingItem<ItemCardWithActionsBinding>() {
override val type: Int = cardTheme.name.hashCode()
var cardTitle: String? = null
@DrawableRes
var cardTitleIcon: Int? = null
@ColorInt
var cardTitleColorInt: Int? = null
var mainTitle: String? = null
var mainHeader: String? = null
var mainBody: String? = null
var mainMaxLines: Int? = null
var mainLayoutDirection: Int = LayoutDirection.INHERIT
@DrawableRes
var mainImage: Int? = null
var onCardClick: (() -> Unit)? = null
var contentDescription: String? = null
var actions: List<Action>? = emptyList()
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): ItemCardWithActionsBinding {
val context = inflater.context
val themedInflater = LayoutInflater.from(ContextThemeWrapper(context, cardTheme.themeId))
val itemCardWithActionsBinding = ItemCardWithActionsBinding.inflate(themedInflater, parent, false)
itemCardWithActionsBinding.rootLayout.background = cardTheme.backgroundDrawableRes?.let { ContextCompat.getDrawable(context, it) }
return itemCardWithActionsBinding
}
override fun bindView(binding: ItemCardWithActionsBinding, payloads: List<Any>) {
binding.cardTitleTextView.setTextOrHide(cardTitle) {
cardTitleColorInt?.let {
val color = getColor(context, it);
setTextColor(color)
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(color))
}
setCompoundDrawablesWithIntrinsicBounds(cardTitleIcon ?: 0, 0, 0, 0)
}
var mainLayoutVisible = false
binding.mainHeaderTextView.setTextOrHide(mainHeader) { mainLayoutVisible = true }
binding.mainTitleTextView.setTextOrHide(mainTitle) { mainLayoutVisible = true }
binding.mainBodyTextView.setTextOrHide(mainBody) {
mainLayoutVisible = true
this@CardWithActionsItem.mainMaxLines?.let { maxLines = it }
}
binding.mainImageView.setImageResourceOrHide(mainImage)
if (mainLayoutVisible) {
binding.mainLayout.visibility = View.VISIBLE
binding.mainLayout.layoutDirection = mainLayoutDirection
binding.contentLayout.setOnClickListenerOrHideRipple(onCardClick?.let {
View.OnClickListener {
it()
}
})
} else {
binding.mainLayout.visibility = View.GONE
}
binding.contentLayout.isVisible = (mainLayoutVisible || binding.cardTitleTextView.isVisible)
if (actions.isNullOrEmpty()) {
binding.actionsLinearLayout.visibility = View.GONE
} else {
val count = actions?.size ?: 0
val viewCount = binding.actionsLinearLayout.childCount
if (count < viewCount) {
for (i in count until viewCount) {
binding.actionsLinearLayout.removeViewAt(0)
}
} else if (count > viewCount) {
for (i in viewCount until count) {
ItemActionBinding.inflate(
LayoutInflater.from(binding.root.context),
binding.actionsLinearLayout,
true
)
}
}
actions?.forEachIndexed { index, (icon, label, showBadge, onClickListener) ->
val actionBinding = ItemActionBinding.bind(binding.actionsLinearLayout.getChildAt(index))
actionBinding.actionDivider.isVisible = (index == 0 && mainLayoutVisible) || (index > 0)
actionBinding.textView.text = label.safeEmojiSpanify()
actionBinding.leftIconImageView.setImageResourceOrHide(icon)
actionBinding.badgeView.isVisible = showBadge
actionBinding.actionRootLayout.setOnClickListener(onClickListener)
}
if (!binding.actionsLinearLayout.isVisible) {
binding.actionsLinearLayout.visibility = View.VISIBLE
}
}
}
override fun unbindView(binding: ItemCardWithActionsBinding) {
super.unbindView(binding)
binding.mainBodyTextView.maxLines = Int.MAX_VALUE
binding.mainBodyTextView.visibility = View.VISIBLE
}
}
fun cardWithActionItem(
cardTheme: CardTheme = CardTheme.Default,
block: (CardWithActionsItem.() -> Unit),
): CardWithActionsItem = CardWithActionsItem(cardTheme)
.apply(block)
\ No newline at end of file
......@@ -33,10 +33,9 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
StringsManager.liveStrings.observeEventAndConsume(viewLifecycleOwner) {
StringsManager.liveStrings.observe(viewLifecycleOwner) {
refreshScreen()
}
refreshScreen()
}
protected fun stringsFormat(key: String, vararg args: Any?): String? {
......
......@@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import com.lunabeestudio.stopcovid.coreui.databinding.FragmentRecyclerViewBinding
import com.lunabeestudio.stopcovid.coreui.extension.closeKeyboardOnScroll
import com.lunabeestudio.stopcovid.coreui.extension.viewLifecycleOwnerOrNull
import com.mikepenz.fastadapter.GenericItem
import com.mikepenz.fastadapter.adapters.FastItemAdapter
import com.mikepenz.fastadapter.adapters.GenericFastItemAdapter
......@@ -52,7 +53,7 @@ abstract class FastAdapterFragment : BaseFragment() {
override fun refreshScreen() {
refreshScreenJob?.cancel()
refreshScreenJob = viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
refreshScreenJob = viewLifecycleOwnerOrNull()?.lifecycleScope?.launch(Dispatchers.Main) {
delay(10)
val items = getItems()
if (items.isEmpty()) {
......
......@@ -11,10 +11,17 @@
package com.lunabeestudio.stopcovid.coreui.manager
import android.content.Context
import com.google.gson.Gson
import com.lunabeestudio.domain.model.Configuration
import com.lunabeestudio.stopcovid.coreui.BuildConfig
import com.lunabeestudio.stopcovid.coreui.extension.saveTo
import com.lunabeestudio.stopcovid.coreui.model.ApiConfiguration
import com.lunabeestudio.stopcovid.coreui.model.ConfigurationWrapper
import com.lunabeestudio.stopcovid.coreui.model.toDomain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.json.JSONObject
import timber.log.Timber
import java.io.File
......@@ -22,7 +29,9 @@ object ConfigManager {
private const val URL: String = BuildConfig.SERVER_URL + BuildConfig.CONFIG_JSON
suspend fun fetchOrLoad(context: Context): String {
private val gson = Gson()
suspend fun fetchOrLoad(context: Context): Configuration {
val file = File(context.filesDir, BuildConfig.CONFIG_JSON)
Timber.v("Fetching remote config at $URL")
try {
......@@ -33,12 +42,18 @@ object ConfigManager {
return loadLocal(context, file)
}
private suspend fun loadLocal(context: Context, file: File): String {
fun load(context: Context): Configuration {
val file = File(context.filesDir, BuildConfig.CONFIG_JSON)
Timber.v("Pre load local config")
return runBlocking { loadLocal(context, file) }
}
private suspend fun loadLocal(context: Context, file: File): Configuration {
return withContext(Dispatchers.IO) {
if (file.exists()) {
try {
Timber.v("Loading $file to object")
file.readText()
file.readText().apiToConfiguration()
} catch (e: Exception) {
Timber.e(e)
Timber.v("Loading default file to object")
......@@ -51,15 +66,24 @@ object ConfigManager {
}
}
private suspend fun getDefaultAssetFile(context: Context): String {
private suspend fun getDefaultAssetFile(context: Context): Configuration {
@Suppress("BlockingMethodInNonBlockingContext")
return withContext(Dispatchers.IO) {
context.assets.open("Config/${BuildConfig.CONFIG_JSON}").use {
it.readBytes().toString(Charsets.UTF_8)
it.readBytes().toString(Charsets.UTF_8).apiToConfiguration()
}
}
}
private fun String.apiToConfiguration(): Configuration {
val configList = gson.fromJson(this, ConfigurationWrapper::class.java).config
val jsonObject = JSONObject()
configList.forEach {
jsonObject.put(it.name, if (it.value is List<*>) gson.toJson(it.value) else it.value)
}
return gson.fromJson(jsonObject.toString(), ApiConfiguration::class.java).toDomain(gson)
}
fun clearLocal(context: Context) {
File(context.filesDir, BuildConfig.CONFIG_JSON).delete()
}
......
package com.lunabeestudio.stopcovid.coreui.model
import android.view.View
import androidx.annotation.DrawableRes
data class Action(
@DrawableRes
val icon: Int? = null,
val label: String? = null,
val showBadge: Boolean = false,
val onClickListener: View.OnClickListener,
)
\ 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/17/12 - for the TOUS-ANTI-COVID project
*/
package com.lunabeestudio.stopcovid.coreui.model
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.lunabeestudio.domain.model.Configuration
internal class ApiConfiguration(
@SerializedName("version")
val version: Int,
@SerializedName("app.apiVersion")
val apiVersion: String,
@SerializedName("app.warningApiVersion")
val warningApiVersion: String,
@SerializedName("app.displayAttestation")
val displayAttestation: Boolean,
@SerializedName("app.displayVaccination")
val displayVaccination: Boolean,
@SerializedName("app.dataRetentionPeriod")
val dataRetentionPeriod: Int,
@SerializedName("app.quarantinePeriod")
val quarantinePeriod: Int,
@SerializedName("app.checkStatusFrequency")
val checkStatusFrequencyHour: Float,
@SerializedName("app.minStatusRetryDuration")
val minStatusRetryDuration: Float,
@SerializedName("app.randomStatusHour")
val randomStatusHour: Float,
@SerializedName("app.preSymptomsSpan")
val preSymptomsSpan: Int,
@SerializedName("app.minHourContactNotif")
val minHourContactNotif: Int,
@SerializedName("app.maxHourContactNotif")
val maxHourContactNotif: Int,
@SerializedName("app.keyfigures.displayDepartmentLevel")
val displayDepartmentLevel: Boolean,
@SerializedName("ble.calibration")
val calibration: String,
@SerializedName("ble.filterConfig")
val filterConfig: String,
@SerializedName("ble.filterMode")
val filterMode: String,
@SerializedName("ble.serviceUUID")
val serviceUUID: String,
@SerializedName("ble.characteristicUUID")
val characteristicUUID: String,
@SerializedName("ble.backgroundServiceManufacturerData")
val backgroundServiceManufacturerData: String,
@SerializedName("app.qrCode.deletionHours")
val qrCodeDeletionHours: Float,
@SerializedName("app.qrCode.expiredHours")
val qrCodeExpiredHours: Float,
@SerializedName("app.qrCode.formattedString")
val qrCodeFormattedString: String,
@SerializedName("app.qrCode.formattedStringDisplayed")
val qrCodeFormattedStringDisplayed: String,
@SerializedName("app.qrCode.footerString")
val qrCodeFooterString: String,
@SerializedName("app.venuesTimestampRoundingInterval")
val venuesTimestampRoundingInterval: Int,
@SerializedName("app.proximityReactivation.reminderHours")
val proximityReactivationReminderHours: String,
@SerializedName("app.venuesRetentionPeriod")
val venuesRetentionPeriod: Int,
@SerializedName("app.privateEventVenueType")
val privateEventVenueType: String,
@SerializedName("app.displayRecordVenues")
val displayRecordVenues: Boolean,
@SerializedName("app.displayPrivateEvent")
val displayPrivateEvent: Boolean,
@SerializedName("app.displayIsolation")
val displayIsolation: Boolean,
@SerializedName("app.positiveSampleSpan")
val positiveSampleSpan: Int,
@SerializedName("app.isolation.duration")
val isolationDuration: Long,
@SerializedName("app.postIsolation.duration")
val postIsolationDuration: Long,
@SerializedName("app.venuesSalt")
val venuesSalt: Int,
)
internal fun ApiConfiguration.toDomain(gson: Gson) = Configuration(
version = version,
apiVersion = apiVersion,
warningApiVersion = warningApiVersion,
displayAttestation = displayAttestation,
displayVaccination = displayVaccination,
dataRetentionPeriod = dataRetentionPeriod,
quarantinePeriod = quarantinePeriod,
checkStatusFrequencyHour = checkStatusFrequencyHour,
minStatusRetryDuration = minStatusRetryDuration,
randomStatusHour = randomStatusHour,
preSymptomsSpan = preSymptomsSpan,
minHourContactNotif = minHourContactNotif,
maxHourContactNotif = maxHourContactNotif,
displayDepartmentLevel = displayDepartmentLevel,
calibration = gson.fromJson<List<ApiDeviceParameterCorrection>>(calibration,
object : TypeToken<List<ApiDeviceParameterCorrection>>() {}.type).map { it.toDomain() },
filterConfig = filterConfig,
filterMode = filterMode,
serviceUUID = serviceUUID,
characteristicUUID = characteristicUUID,
backgroundServiceManufacturerData = backgroundServiceManufacturerData,
qrCodeDeletionHours = qrCodeDeletionHours,
qrCodeExpiredHours = qrCodeExpiredHours,
qrCodeFormattedString = qrCodeFormattedString,
qrCodeFormattedStringDisplayed = qrCodeFormattedStringDisplayed,
qrCodeFooterString = qrCodeFooterString,
venuesTimestampRoundingInterval = venuesTimestampRoundingInterval,
proximityReactivationReminderHours = gson.fromJson(proximityReactivationReminderHours, object : TypeToken<List<Int>>() {}.type),
venuesRetentionPeriod = venuesRetentionPeriod,
privateEventVenueType = privateEventVenueType,
displayRecordVenues = displayRecordVenues,
displayPrivateEvent = displayPrivateEvent,
displayIsolation = displayIsolation,
positiveSampleSpan = positiveSampleSpan,
isolationDuration = isolationDuration,
postIsolationDuration = postIsolationDuration,
venuesSalt = venuesSalt
)
\ 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/17/12 - for the TOUS-ANTI-COVID project
*/
package com.lunabeestudio.stopcovid.coreui.model
import com.google.gson.annotations.SerializedName
import com.lunabeestudio.domain.model.DeviceParameterCorrection
internal class ApiDeviceParameterCorrection(
@SerializedName("device_handset_model")
val deviceHandsetModel: String,
@SerializedName("tx_RSS_correction_factor")
val txRSSCorrectionFactor: Double,
@SerializedName("rx_RSS_correction_factor")
val rxRSSCorrectionFactor: Double,
)
internal fun ApiDeviceParameterCorrection.toDomain() = DeviceParameterCorrection(
deviceHandsetModel = deviceHandsetModel,
txRssCorrectionFactor = txRSSCorrectionFactor,
rxRssCorrectionFactor = rxRSSCorrectionFactor,
)
\ 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/24/12 - for the TOUS-ANTI-COVID project
*/
package com.lunabeestudio.stopcovid.coreui.model
import androidx.annotation.DrawableRes
import androidx.annotation.StyleRes
import com.lunabeestudio.stopcovid.coreui.R
enum class CardTheme {
Default,