simplify :core:interfaces
This commit is contained in:
parent
91d19b51dc
commit
46c2abdc5b
|
@ -0,0 +1,7 @@
|
||||||
|
package info.nightscout.interfaces.maintenance
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PrefMetadata(var value: String, var status: PrefsStatus, var info: String? = null) : Parcelable
|
|
@ -0,0 +1,11 @@
|
||||||
|
package info.nightscout.interfaces.maintenance
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
interface PrefsMetadataKey {
|
||||||
|
|
||||||
|
val key: String
|
||||||
|
val icon: Int
|
||||||
|
val label: Int
|
||||||
|
fun formatForDisplay(context: Context, value: String): String
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package info.nightscout.interfaces.maintenance
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
interface PrefsStatus : Parcelable {
|
||||||
|
|
||||||
|
val icon: Int
|
||||||
|
}
|
|
@ -1,17 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Maintenance -->
|
|
||||||
<string name="metadata_label_format">File format</string>
|
|
||||||
<string name="metadata_label_created_at">Created at</string>
|
|
||||||
<string name="metadata_label_aaps_version">AAPS Version</string>
|
|
||||||
<string name="metadata_label_aaps_flavour">Build Variant</string>
|
|
||||||
<string name="metadata_label_device_name">Exporting device patient name</string>
|
|
||||||
<string name="metadata_label_device_model">Exporting device model</string>
|
|
||||||
<string name="metadata_label_encryption">File encryption</string>
|
|
||||||
<string name="metadata_format_new">New encrypted format</string>
|
|
||||||
<string name="metadata_format_debug">New debug format (unencrypted)</string>
|
|
||||||
<string name="metadata_format_other">Unknown export format</string>
|
|
||||||
|
|
||||||
<!-- PumpPluginBase -->
|
<!-- PumpPluginBase -->
|
||||||
<string name="pump_driver_changed" comment="26 characters max for translation">Pump driver changed.</string>
|
<string name="pump_driver_changed" comment="26 characters max for translation">Pump driver changed.</string>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package info.nightscout.implementation.utils
|
package info.nightscout.implementation.utils
|
||||||
|
|
||||||
import info.nightscout.interfaces.R
|
|
||||||
import info.nightscout.interfaces.utils.DecimalFormatter
|
import info.nightscout.interfaces.utils.DecimalFormatter
|
||||||
import info.nightscout.shared.interfaces.ResourceHelper
|
import info.nightscout.shared.interfaces.ResourceHelper
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
id 'kotlin-allopen'
|
id 'kotlin-allopen'
|
||||||
|
id 'kotlin-parcelize'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "${project.rootDir}/core/main/android_dependencies.gradle"
|
apply from: "${project.rootDir}/core/main/android_dependencies.gradle"
|
||||||
|
|
|
@ -44,7 +44,7 @@ import info.nightscout.interfaces.maintenance.Prefs
|
||||||
import info.nightscout.interfaces.maintenance.PrefsFile
|
import info.nightscout.interfaces.maintenance.PrefsFile
|
||||||
import info.nightscout.interfaces.maintenance.PrefsFormat
|
import info.nightscout.interfaces.maintenance.PrefsFormat
|
||||||
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
||||||
import info.nightscout.interfaces.maintenance.PrefsStatus
|
import info.nightscout.interfaces.maintenance.PrefsStatusImpl
|
||||||
import info.nightscout.interfaces.protection.PasswordCheck
|
import info.nightscout.interfaces.protection.PasswordCheck
|
||||||
import info.nightscout.interfaces.storage.Storage
|
import info.nightscout.interfaces.storage.Storage
|
||||||
import info.nightscout.interfaces.ui.UiInteraction
|
import info.nightscout.interfaces.ui.UiInteraction
|
||||||
|
@ -114,12 +114,12 @@ class ImportExportPrefsImpl @Inject constructor(
|
||||||
|
|
||||||
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
|
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
|
||||||
|
|
||||||
metadata[PrefsMetadataKey.DEVICE_NAME] = PrefMetadata(detectUserName(context), PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.DEVICE_NAME] = PrefMetadata(detectUserName(context), PrefsStatusImpl.OK)
|
||||||
metadata[PrefsMetadataKey.CREATED_AT] = PrefMetadata(dateUtil.toISOString(dateUtil.now()), PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.CREATED_AT] = PrefMetadata(dateUtil.toISOString(dateUtil.now()), PrefsStatusImpl.OK)
|
||||||
metadata[PrefsMetadataKey.AAPS_VERSION] = PrefMetadata(config.VERSION_NAME, PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.AAPS_VERSION] = PrefMetadata(config.VERSION_NAME, PrefsStatusImpl.OK)
|
||||||
metadata[PrefsMetadataKey.AAPS_FLAVOUR] = PrefMetadata(config.FLAVOR, PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.AAPS_FLAVOUR] = PrefMetadata(config.FLAVOR, PrefsStatusImpl.OK)
|
||||||
metadata[PrefsMetadataKey.DEVICE_MODEL] = PrefMetadata(config.currentDeviceModelString, PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.DEVICE_MODEL] = PrefMetadata(config.currentDeviceModelString, PrefsStatusImpl.OK)
|
||||||
metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Enabled", PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.ENCRYPTION] = PrefMetadata("Enabled", PrefsStatusImpl.OK)
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,7 @@ class ImportExportPrefsImpl @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// current master password was not the one used for decryption, so we prompt for old password...
|
// current master password was not the one used for decryption, so we prompt for old password...
|
||||||
if (!importOk && (prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status == PrefsStatus.ERROR)) {
|
if (!importOk && (prefs.metadata[PrefsMetadataKeyImpl.ENCRYPTION]?.status == PrefsStatusImpl.ERROR)) {
|
||||||
askForEncryptionPass(
|
askForEncryptionPass(
|
||||||
activity, R.string.preferences_import_canceled, R.string.old_master_password,
|
activity, R.string.preferences_import_canceled, R.string.old_master_password,
|
||||||
R.string.different_password_used, R.string.master_password_will_be_replaced
|
R.string.different_password_used, R.string.master_password_will_be_replaced
|
||||||
|
@ -303,6 +303,7 @@ class ImportExportPrefsImpl @Inject constructor(
|
||||||
override fun importCustomWatchface(fragment: Fragment) {
|
override fun importCustomWatchface(fragment: Fragment) {
|
||||||
fragment.activity?.let { importCustomWatchface(it) }
|
fragment.activity?.let { importCustomWatchface(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun importCustomWatchface(activity: FragmentActivity) {
|
override fun importCustomWatchface(activity: FragmentActivity) {
|
||||||
try {
|
try {
|
||||||
if (activity is DaggerAppCompatActivityWithResult)
|
if (activity is DaggerAppCompatActivityWithResult)
|
||||||
|
@ -374,7 +375,7 @@ class ImportExportPrefsImpl @Inject constructor(
|
||||||
var importOk = true
|
var importOk = true
|
||||||
|
|
||||||
for ((_, value) in prefs.metadata) {
|
for ((_, value) in prefs.metadata) {
|
||||||
if (value.status == PrefsStatus.ERROR)
|
if (value.status == PrefsStatusImpl.ERROR)
|
||||||
importOk = false
|
importOk = false
|
||||||
}
|
}
|
||||||
return importOk
|
return importOk
|
||||||
|
|
|
@ -14,7 +14,7 @@ import info.nightscout.interfaces.maintenance.PrefMetadataMap
|
||||||
import info.nightscout.interfaces.maintenance.PrefsFile
|
import info.nightscout.interfaces.maintenance.PrefsFile
|
||||||
import info.nightscout.interfaces.maintenance.PrefsImportDir
|
import info.nightscout.interfaces.maintenance.PrefsImportDir
|
||||||
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
||||||
import info.nightscout.interfaces.maintenance.PrefsStatus
|
import info.nightscout.interfaces.maintenance.PrefsStatusImpl
|
||||||
import info.nightscout.interfaces.storage.Storage
|
import info.nightscout.interfaces.storage.Storage
|
||||||
import info.nightscout.interfaces.versionChecker.VersionCheckerUtils
|
import info.nightscout.interfaces.versionChecker.VersionCheckerUtils
|
||||||
import info.nightscout.rx.bus.RxBus
|
import info.nightscout.rx.bus.RxBus
|
||||||
|
@ -89,8 +89,8 @@ class PrefFileListProviderImpl @Inject constructor(
|
||||||
// we sort only if we have metadata to be used for that
|
// we sort only if we have metadata to be used for that
|
||||||
if (loadMetadata) {
|
if (loadMetadata) {
|
||||||
prefFiles.sortWith(
|
prefFiles.sortWith(
|
||||||
compareByDescending<PrefsFile> { it.metadata[PrefsMetadataKey.AAPS_FLAVOUR]?.status }
|
compareByDescending<PrefsFile> { it.metadata[PrefsMetadataKeyImpl.AAPS_FLAVOUR]?.status as PrefsStatusImpl }
|
||||||
.thenByDescending { it.metadata[PrefsMetadataKey.CREATED_AT]?.value }
|
.thenByDescending { it.metadata[PrefsMetadataKeyImpl.CREATED_AT]?.value }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,22 +176,22 @@ class PrefFileListProviderImpl @Inject constructor(
|
||||||
override fun checkMetadata(metadata: Map<PrefsMetadataKey, PrefMetadata>): Map<PrefsMetadataKey, PrefMetadata> {
|
override fun checkMetadata(metadata: Map<PrefsMetadataKey, PrefMetadata>): Map<PrefsMetadataKey, PrefMetadata> {
|
||||||
val meta = metadata.toMutableMap()
|
val meta = metadata.toMutableMap()
|
||||||
|
|
||||||
meta[PrefsMetadataKey.AAPS_FLAVOUR]?.let { flavour ->
|
meta[PrefsMetadataKeyImpl.AAPS_FLAVOUR]?.let { flavour ->
|
||||||
val flavourOfPrefs = flavour.value
|
val flavourOfPrefs = flavour.value
|
||||||
if (flavour.value != config.get().FLAVOR) {
|
if (flavour.value != config.get().FLAVOR) {
|
||||||
flavour.status = PrefsStatus.WARN
|
flavour.status = PrefsStatusImpl.WARN
|
||||||
flavour.info = rh.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, config.get().FLAVOR)
|
flavour.info = rh.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, config.get().FLAVOR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta[PrefsMetadataKey.DEVICE_MODEL]?.let { model ->
|
meta[PrefsMetadataKeyImpl.DEVICE_MODEL]?.let { model ->
|
||||||
if (model.value != config.get().currentDeviceModelString) {
|
if (model.value != config.get().currentDeviceModelString) {
|
||||||
model.status = PrefsStatus.WARN
|
model.status = PrefsStatusImpl.WARN
|
||||||
model.info = rh.gs(R.string.metadata_warning_different_device)
|
model.info = rh.gs(R.string.metadata_warning_different_device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta[PrefsMetadataKey.CREATED_AT]?.let { createdAt ->
|
meta[PrefsMetadataKeyImpl.CREATED_AT]?.let { createdAt ->
|
||||||
try {
|
try {
|
||||||
val date1 = DateTime.parse(createdAt.value)
|
val date1 = DateTime.parse(createdAt.value)
|
||||||
val date2 = DateTime.now()
|
val date2 = DateTime.now()
|
||||||
|
@ -199,26 +199,26 @@ class PrefFileListProviderImpl @Inject constructor(
|
||||||
val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).days
|
val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).days
|
||||||
|
|
||||||
if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) {
|
if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) {
|
||||||
createdAt.status = PrefsStatus.WARN
|
createdAt.status = PrefsStatusImpl.WARN
|
||||||
createdAt.info = rh.gs(R.string.metadata_warning_old_export, daysOld.toString())
|
createdAt.info = rh.gs(R.string.metadata_warning_old_export, daysOld.toString())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
createdAt.status = PrefsStatus.WARN
|
createdAt.status = PrefsStatusImpl.WARN
|
||||||
createdAt.info = rh.gs(R.string.metadata_warning_date_format)
|
createdAt.info = rh.gs(R.string.metadata_warning_date_format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta[PrefsMetadataKey.AAPS_VERSION]?.let { version ->
|
meta[PrefsMetadataKeyImpl.AAPS_VERSION]?.let { version ->
|
||||||
val currentAppVer = versionCheckerUtils.versionDigits(config.get().VERSION_NAME)
|
val currentAppVer = versionCheckerUtils.versionDigits(config.get().VERSION_NAME)
|
||||||
val metadataVer = versionCheckerUtils.versionDigits(version.value)
|
val metadataVer = versionCheckerUtils.versionDigits(version.value)
|
||||||
|
|
||||||
if ((currentAppVer.size >= 2) && (metadataVer.size >= 2) && (abs(currentAppVer[1] - metadataVer[1]) > 1)) {
|
if ((currentAppVer.size >= 2) && (metadataVer.size >= 2) && (abs(currentAppVer[1] - metadataVer[1]) > 1)) {
|
||||||
version.status = PrefsStatus.WARN
|
version.status = PrefsStatusImpl.WARN
|
||||||
version.info = rh.gs(R.string.metadata_warning_different_version)
|
version.info = rh.gs(R.string.metadata_warning_different_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((currentAppVer.isNotEmpty()) && (metadataVer.isNotEmpty()) && (currentAppVer[0] != metadataVer[0])) {
|
if ((currentAppVer.isNotEmpty()) && (metadataVer.isNotEmpty()) && (currentAppVer[0] != metadataVer[0])) {
|
||||||
version.status = PrefsStatus.WARN
|
version.status = PrefsStatusImpl.WARN
|
||||||
version.info = rh.gs(R.string.metadata_urgent_different_version)
|
version.info = rh.gs(R.string.metadata_urgent_different_version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
package info.nightscout.interfaces.maintenance
|
package info.nightscout.configuration.maintenance
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import info.nightscout.interfaces.R
|
import info.nightscout.configuration.R
|
||||||
import kotlinx.parcelize.Parcelize
|
import info.nightscout.interfaces.maintenance.PrefsFormat
|
||||||
import java.io.File
|
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
||||||
|
|
||||||
enum class PrefsMetadataKey(val key: String, @DrawableRes val icon: Int, @StringRes val label: Int) {
|
enum class PrefsMetadataKeyImpl(override val key: String, @DrawableRes override val icon: Int, @StringRes override val label: Int) : PrefsMetadataKey {
|
||||||
|
|
||||||
FILE_FORMAT("format", R.drawable.ic_meta_format, R.string.metadata_label_format),
|
FILE_FORMAT("format", R.drawable.ic_meta_format, R.string.metadata_label_format),
|
||||||
CREATED_AT("created_at", R.drawable.ic_meta_date, R.string.metadata_label_created_at),
|
CREATED_AT("created_at", R.drawable.ic_meta_date, R.string.metadata_label_created_at),
|
||||||
|
@ -35,48 +34,16 @@ enum class PrefsMetadataKey(val key: String, @DrawableRes val icon: Int, @String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatForDisplay(context: Context, value: String): String {
|
override fun formatForDisplay(context: Context, value: String): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
FILE_FORMAT -> when (value) {
|
FILE_FORMAT -> when (value) {
|
||||||
PrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new)
|
PrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new)
|
||||||
PrefsFormat.FORMAT_KEY_NOENC -> context.getString(R.string.metadata_format_debug)
|
PrefsFormat.FORMAT_KEY_NOENC -> context.getString(R.string.metadata_format_debug)
|
||||||
else -> context.getString(R.string.metadata_format_other)
|
else -> context.getString(R.string.metadata_format_other)
|
||||||
}
|
}
|
||||||
|
|
||||||
CREATED_AT -> value.replace("T", " ").replace("Z", " (UTC)")
|
CREATED_AT -> value.replace("T", " ").replace("Z", " (UTC)")
|
||||||
else -> value
|
else -> value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class PrefMetadata(var value: String, var status: PrefsStatus, var info: String? = null) : Parcelable
|
|
||||||
|
|
||||||
typealias PrefMetadataMap = Map<PrefsMetadataKey, PrefMetadata>
|
|
||||||
|
|
||||||
data class Prefs(val values: Map<String, String>, var metadata: PrefMetadataMap)
|
|
||||||
|
|
||||||
interface PrefsFormat {
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val FORMAT_KEY_ENC = "aaps_encrypted"
|
|
||||||
const val FORMAT_KEY_NOENC = "aaps_structured"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null)
|
|
||||||
fun loadPreferences(file: File, masterPassword: String? = null): Prefs
|
|
||||||
fun loadMetadata(contents: String? = null): PrefMetadataMap
|
|
||||||
fun isPreferencesFile(file: File, preloadedContents: String? = null): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class PrefsStatus(@DrawableRes val icon: Int) {
|
|
||||||
OK(R.drawable.ic_meta_ok),
|
|
||||||
WARN(R.drawable.ic_meta_warning),
|
|
||||||
ERROR(R.drawable.ic_meta_error),
|
|
||||||
UNKNOWN(R.drawable.ic_meta_error),
|
|
||||||
DISABLED(R.drawable.ic_meta_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PrefFileNotFoundError(message: String) : Exception(message)
|
|
||||||
class PrefIOError(message: String) : Exception(message)
|
|
||||||
class PrefFormatError(message: String) : Exception(message)
|
|
|
@ -12,11 +12,11 @@ import info.nightscout.configuration.R
|
||||||
import info.nightscout.configuration.databinding.MaintenanceImportListActivityBinding
|
import info.nightscout.configuration.databinding.MaintenanceImportListActivityBinding
|
||||||
import info.nightscout.configuration.databinding.MaintenanceImportListItemBinding
|
import info.nightscout.configuration.databinding.MaintenanceImportListItemBinding
|
||||||
import info.nightscout.configuration.maintenance.PrefsFileContract
|
import info.nightscout.configuration.maintenance.PrefsFileContract
|
||||||
|
import info.nightscout.configuration.maintenance.PrefsMetadataKeyImpl
|
||||||
import info.nightscout.core.ui.activities.TranslatedDaggerAppCompatActivity
|
import info.nightscout.core.ui.activities.TranslatedDaggerAppCompatActivity
|
||||||
import info.nightscout.interfaces.maintenance.PrefFileListProvider
|
import info.nightscout.interfaces.maintenance.PrefFileListProvider
|
||||||
import info.nightscout.interfaces.maintenance.PrefsFile
|
import info.nightscout.interfaces.maintenance.PrefsFile
|
||||||
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
import info.nightscout.interfaces.maintenance.PrefsStatusImpl
|
||||||
import info.nightscout.interfaces.maintenance.PrefsStatus
|
|
||||||
import info.nightscout.shared.interfaces.ResourceHelper
|
import info.nightscout.shared.interfaces.ResourceHelper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -82,23 +82,23 @@ class PrefImportListActivity : TranslatedDaggerAppCompatActivity() {
|
||||||
metaDateTimeIcon.visibility = View.VISIBLE
|
metaDateTimeIcon.visibility = View.VISIBLE
|
||||||
metaAppVersion.visibility = View.VISIBLE
|
metaAppVersion.visibility = View.VISIBLE
|
||||||
|
|
||||||
prefFile.metadata[PrefsMetadataKey.AAPS_FLAVOUR]?.let {
|
prefFile.metadata[PrefsMetadataKeyImpl.AAPS_FLAVOUR]?.let {
|
||||||
metaVariantFormat.text = it.value
|
metaVariantFormat.text = it.value
|
||||||
val colorAttr = if (it.status == PrefsStatus.OK) info.nightscout.core.ui.R.attr.metadataTextOkColor else info.nightscout.core.ui.R.attr.metadataTextWarningColor
|
val colorAttr = if (it.status == PrefsStatusImpl.OK) info.nightscout.core.ui.R.attr.metadataTextOkColor else info.nightscout.core.ui.R.attr.metadataTextWarningColor
|
||||||
metaVariantFormat.setTextColor(rh.gac(metaVariantFormat.context, colorAttr))
|
metaVariantFormat.setTextColor(rh.gac(metaVariantFormat.context, colorAttr))
|
||||||
}
|
}
|
||||||
|
|
||||||
prefFile.metadata[PrefsMetadataKey.CREATED_AT]?.let {
|
prefFile.metadata[PrefsMetadataKeyImpl.CREATED_AT]?.let {
|
||||||
metaDateTime.text = prefFileListProvider.formatExportedAgo(it.value)
|
metaDateTime.text = prefFileListProvider.formatExportedAgo(it.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
prefFile.metadata[PrefsMetadataKey.AAPS_VERSION]?.let {
|
prefFile.metadata[PrefsMetadataKeyImpl.AAPS_VERSION]?.let {
|
||||||
metaAppVersion.text = it.value
|
metaAppVersion.text = it.value
|
||||||
val colorAttr = if (it.status == PrefsStatus.OK) info.nightscout.core.ui.R.attr.metadataTextOkColor else info.nightscout.core.ui.R.attr.metadataTextWarningColor
|
val colorAttr = if (it.status == PrefsStatusImpl.OK) info.nightscout.core.ui.R.attr.metadataTextOkColor else info.nightscout.core.ui.R.attr.metadataTextWarningColor
|
||||||
metaAppVersion.setTextColor(rh.gac(metaVariantFormat.context, colorAttr))
|
metaAppVersion.setTextColor(rh.gac(metaVariantFormat.context, colorAttr))
|
||||||
}
|
}
|
||||||
|
|
||||||
prefFile.metadata[PrefsMetadataKey.DEVICE_NAME]?.let {
|
prefFile.metadata[PrefsMetadataKeyImpl.DEVICE_NAME]?.let {
|
||||||
metaDeviceName.text = it.value
|
metaDeviceName.text = it.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import info.nightscout.configuration.R
|
import info.nightscout.configuration.R
|
||||||
import info.nightscout.core.ui.toast.ToastUtils
|
import info.nightscout.core.ui.toast.ToastUtils
|
||||||
import info.nightscout.interfaces.maintenance.Prefs
|
import info.nightscout.interfaces.maintenance.Prefs
|
||||||
import info.nightscout.interfaces.maintenance.PrefsStatus
|
import info.nightscout.interfaces.maintenance.PrefsStatusImpl
|
||||||
import info.nightscout.shared.extensions.runOnUiThread
|
import info.nightscout.shared.extensions.runOnUiThread
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
|
||||||
|
@ -57,8 +57,8 @@ object PrefImportSummaryDialog {
|
||||||
(rowLayout.findViewById<View>(R.id.summary_icon) as ImageView).setImageResource(metaKey.icon)
|
(rowLayout.findViewById<View>(R.id.summary_icon) as ImageView).setImageResource(metaKey.icon)
|
||||||
(rowLayout.findViewById<View>(R.id.status_icon) as ImageView).setImageResource(metaEntry.status.icon)
|
(rowLayout.findViewById<View>(R.id.status_icon) as ImageView).setImageResource(metaEntry.status.icon)
|
||||||
|
|
||||||
if (metaEntry.status == PrefsStatus.WARN) label.setTextColor(themedCtx.getColor(info.nightscout.interfaces.R.color.metadataTextWarning))
|
if (metaEntry.status == PrefsStatusImpl.WARN) label.setTextColor(themedCtx.getColor(info.nightscout.interfaces.R.color.metadataTextWarning))
|
||||||
else if (metaEntry.status == PrefsStatus.ERROR) label.setTextColor(themedCtx.getColor(info.nightscout.interfaces.R.color.metadataTextError))
|
else if (metaEntry.status == PrefsStatusImpl.ERROR) label.setTextColor(themedCtx.getColor(info.nightscout.interfaces.R.color.metadataTextError))
|
||||||
|
|
||||||
if (metaEntry.info != null) {
|
if (metaEntry.info != null) {
|
||||||
details.add("<b>${context.getString(metaKey.label)}</b>: ${metaEntry.value}<br/><i style=\"color:silver\">${metaEntry.info}</i>")
|
details.add("<b>${context.getString(metaKey.label)}</b>: ${metaEntry.value}<br/><i style=\"color:silver\">${metaEntry.info}</i>")
|
||||||
|
@ -66,8 +66,8 @@ object PrefImportSummaryDialog {
|
||||||
rowLayout.setOnClickListener {
|
rowLayout.setOnClickListener {
|
||||||
val msg = "[${context.getString(metaKey.label)}] ${metaEntry.info}"
|
val msg = "[${context.getString(metaKey.label)}] ${metaEntry.info}"
|
||||||
when (metaEntry.status) {
|
when (metaEntry.status) {
|
||||||
PrefsStatus.WARN -> ToastUtils.Long.warnToast(context, msg)
|
PrefsStatusImpl.WARN -> ToastUtils.Long.warnToast(context, msg)
|
||||||
PrefsStatus.ERROR -> ToastUtils.Long.errorToast(context, msg)
|
PrefsStatusImpl.ERROR -> ToastUtils.Long.errorToast(context, msg)
|
||||||
else -> ToastUtils.Long.infoToast(context, msg)
|
else -> ToastUtils.Long.infoToast(context, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package info.nightscout.configuration.maintenance.formats
|
package info.nightscout.configuration.maintenance.formats
|
||||||
|
|
||||||
import info.nightscout.configuration.R
|
import info.nightscout.configuration.R
|
||||||
|
import info.nightscout.configuration.maintenance.PrefsMetadataKeyImpl
|
||||||
import info.nightscout.core.utils.CryptoUtil
|
import info.nightscout.core.utils.CryptoUtil
|
||||||
import info.nightscout.core.utils.hexStringToByteArray
|
import info.nightscout.core.utils.hexStringToByteArray
|
||||||
import info.nightscout.core.utils.toHex
|
import info.nightscout.core.utils.toHex
|
||||||
|
@ -13,6 +14,7 @@ import info.nightscout.interfaces.maintenance.Prefs
|
||||||
import info.nightscout.interfaces.maintenance.PrefsFormat
|
import info.nightscout.interfaces.maintenance.PrefsFormat
|
||||||
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
import info.nightscout.interfaces.maintenance.PrefsMetadataKey
|
||||||
import info.nightscout.interfaces.maintenance.PrefsStatus
|
import info.nightscout.interfaces.maintenance.PrefsStatus
|
||||||
|
import info.nightscout.interfaces.maintenance.PrefsStatusImpl
|
||||||
import info.nightscout.interfaces.storage.Storage
|
import info.nightscout.interfaces.storage.Storage
|
||||||
import info.nightscout.shared.interfaces.ResourceHelper
|
import info.nightscout.shared.interfaces.ResourceHelper
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
@ -59,8 +61,8 @@ class EncryptedPrefsFormat @Inject constructor(
|
||||||
val content = JSONObject()
|
val content = JSONObject()
|
||||||
val meta = JSONObject()
|
val meta = JSONObject()
|
||||||
|
|
||||||
val encStatus = prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status ?: PrefsStatus.OK
|
val encStatus = prefs.metadata[PrefsMetadataKeyImpl.ENCRYPTION]?.status ?: PrefsStatusImpl.OK
|
||||||
var encrypted = encStatus == PrefsStatus.OK && masterPassword != null
|
var encrypted = encStatus == PrefsStatusImpl.OK && masterPassword != null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for ((key, value) in prefs.values.toSortedMap()) {
|
for ((key, value) in prefs.values.toSortedMap()) {
|
||||||
|
@ -68,14 +70,14 @@ class EncryptedPrefsFormat @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((metaKey, metaEntry) in prefs.metadata) {
|
for ((metaKey, metaEntry) in prefs.metadata) {
|
||||||
if (metaKey == PrefsMetadataKey.FILE_FORMAT)
|
if (metaKey == PrefsMetadataKeyImpl.FILE_FORMAT)
|
||||||
continue
|
continue
|
||||||
if (metaKey == PrefsMetadataKey.ENCRYPTION)
|
if (metaKey == PrefsMetadataKeyImpl.ENCRYPTION)
|
||||||
continue
|
continue
|
||||||
meta.put(metaKey.key, metaEntry.value)
|
meta.put(metaKey.key, metaEntry.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
container.put(PrefsMetadataKey.FILE_FORMAT.key, if (encrypted) PrefsFormat.FORMAT_KEY_ENC else PrefsFormat.FORMAT_KEY_NOENC)
|
container.put(PrefsMetadataKeyImpl.FILE_FORMAT.key, if (encrypted) PrefsFormat.FORMAT_KEY_ENC else PrefsFormat.FORMAT_KEY_NOENC)
|
||||||
container.put("metadata", meta)
|
container.put("metadata", meta)
|
||||||
|
|
||||||
val security = JSONObject()
|
val security = JSONObject()
|
||||||
|
@ -130,22 +132,22 @@ class EncryptedPrefsFormat @Inject constructor(
|
||||||
val container = JSONObject(jsonBody)
|
val container = JSONObject(jsonBody)
|
||||||
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = loadMetadata(container)
|
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = loadMetadata(container)
|
||||||
|
|
||||||
if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content")) {
|
if (container.has(PrefsMetadataKeyImpl.FILE_FORMAT.key) && container.has("security") && container.has("content")) {
|
||||||
val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key)
|
val fileFormat = container.getString(PrefsMetadataKeyImpl.FILE_FORMAT.key)
|
||||||
val security = container.getJSONObject("security")
|
val security = container.getJSONObject("security")
|
||||||
val encrypted = fileFormat == PrefsFormat.FORMAT_KEY_ENC
|
val encrypted = fileFormat == PrefsFormat.FORMAT_KEY_ENC
|
||||||
var secure: PrefsStatus = PrefsStatus.OK
|
var secure: PrefsStatus = PrefsStatusImpl.OK
|
||||||
var decryptedOk = false
|
var decryptedOk = false
|
||||||
var contentJsonObj: JSONObject? = null
|
var contentJsonObj: JSONObject? = null
|
||||||
var insecurityReason = rh.gs(R.string.prefdecrypt_settings_tampered)
|
var insecurityReason = rh.gs(R.string.prefdecrypt_settings_tampered)
|
||||||
|
|
||||||
if (security.has("file_hash")) {
|
if (security.has("file_hash")) {
|
||||||
if (calculatedFileHash != security.getString("file_hash")) {
|
if (calculatedFileHash != security.getString("file_hash")) {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_modified))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_modified))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_missing_file_hash))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_missing_file_hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,38 +166,38 @@ class EncryptedPrefsFormat @Inject constructor(
|
||||||
contentJsonObj = JSONObject(decrypted)
|
contentJsonObj = JSONObject(decrypted)
|
||||||
decryptedOk = true
|
decryptedOk = true
|
||||||
} else {
|
} else {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_modified))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_modified))
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_parsing))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_parsing))
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_pass))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_pass))
|
||||||
insecurityReason = rh.gs(R.string.prefdecrypt_wrong_password)
|
insecurityReason = rh.gs(R.string.prefdecrypt_wrong_password)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_format))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_format))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_algorithm))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_algorithm))
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (secure == PrefsStatus.OK) {
|
if (secure == PrefsStatusImpl.OK) {
|
||||||
secure = PrefsStatus.WARN
|
secure = PrefsStatusImpl.WARN
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(security.has("algorithm") && security.get("algorithm") == "none")) {
|
if (!(security.has("algorithm") && security.get("algorithm") == "none")) {
|
||||||
secure = PrefsStatus.ERROR
|
secure = PrefsStatusImpl.ERROR
|
||||||
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_algorithm))
|
issues.add(rh.gs(R.string.prefdecrypt_issue_wrong_algorithm))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,12 +213,12 @@ class EncryptedPrefsFormat @Inject constructor(
|
||||||
|
|
||||||
val issuesStr: String? = if (issues.size > 0) issues.joinToString("\n") else null
|
val issuesStr: String? = if (issues.size > 0) issues.joinToString("\n") else null
|
||||||
val encryptionDescStr = if (encrypted) {
|
val encryptionDescStr = if (encrypted) {
|
||||||
if (secure == PrefsStatus.OK) rh.gs(R.string.prefdecrypt_settings_secure) else insecurityReason
|
if (secure == PrefsStatusImpl.OK) rh.gs(R.string.prefdecrypt_settings_secure) else insecurityReason
|
||||||
} else {
|
} else {
|
||||||
if (secure != PrefsStatus.ERROR) rh.gs(R.string.prefdecrypt_settings_unencrypted) else rh.gs(R.string.prefdecrypt_settings_tampered)
|
if (secure != PrefsStatusImpl.ERROR) rh.gs(R.string.prefdecrypt_settings_unencrypted) else rh.gs(R.string.prefdecrypt_settings_tampered)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr)
|
metadata[PrefsMetadataKeyImpl.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Prefs(entries, metadata)
|
return Prefs(entries, metadata)
|
||||||
|
@ -243,22 +245,22 @@ class EncryptedPrefsFormat @Inject constructor(
|
||||||
|
|
||||||
private fun loadMetadata(container: JSONObject): MutableMap<PrefsMetadataKey, PrefMetadata> {
|
private fun loadMetadata(container: JSONObject): MutableMap<PrefsMetadataKey, PrefMetadata> {
|
||||||
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
|
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
|
||||||
if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) {
|
if (container.has(PrefsMetadataKeyImpl.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) {
|
||||||
val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key)
|
val fileFormat = container.getString(PrefsMetadataKeyImpl.FILE_FORMAT.key)
|
||||||
if ((fileFormat != PrefsFormat.FORMAT_KEY_ENC) && (fileFormat != PrefsFormat.FORMAT_KEY_NOENC)) {
|
if ((fileFormat != PrefsFormat.FORMAT_KEY_ENC) && (fileFormat != PrefsFormat.FORMAT_KEY_NOENC)) {
|
||||||
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(rh.gs(info.nightscout.interfaces.R.string.metadata_format_other), PrefsStatus.ERROR)
|
metadata[PrefsMetadataKeyImpl.FILE_FORMAT] = PrefMetadata(rh.gs(R.string.metadata_format_other), PrefsStatusImpl.ERROR)
|
||||||
} else {
|
} else {
|
||||||
val meta = container.getJSONObject("metadata")
|
val meta = container.getJSONObject("metadata")
|
||||||
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK)
|
metadata[PrefsMetadataKeyImpl.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatusImpl.OK)
|
||||||
for (key in meta.keys()) {
|
for (key in meta.keys()) {
|
||||||
val metaKey = PrefsMetadataKey.fromKey(key)
|
val metaKey = PrefsMetadataKeyImpl.fromKey(key)
|
||||||
if (metaKey != null) {
|
if (metaKey != null) {
|
||||||
metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK)
|
metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatusImpl.OK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(rh.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR)
|
metadata[PrefsMetadataKeyImpl.FILE_FORMAT] = PrefMetadata(rh.gs(R.string.prefdecrypt_wrong_json), PrefsStatusImpl.ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package info.nightscout.interfaces.maintenance
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import info.nightscout.configuration.R
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
typealias PrefMetadataMap = Map<PrefsMetadataKey, PrefMetadata>
|
||||||
|
|
||||||
|
data class Prefs(val values: Map<String, String>, var metadata: PrefMetadataMap)
|
||||||
|
|
||||||
|
interface PrefsFormat {
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val FORMAT_KEY_ENC = "aaps_encrypted"
|
||||||
|
const val FORMAT_KEY_NOENC = "aaps_structured"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null)
|
||||||
|
fun loadPreferences(file: File, masterPassword: String? = null): Prefs
|
||||||
|
fun loadMetadata(contents: String? = null): PrefMetadataMap
|
||||||
|
fun isPreferencesFile(file: File, preloadedContents: String? = null): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
enum class PrefsStatusImpl(@DrawableRes override val icon: Int) : PrefsStatus {
|
||||||
|
|
||||||
|
OK(R.drawable.ic_meta_ok),
|
||||||
|
WARN(R.drawable.ic_meta_warning),
|
||||||
|
ERROR(R.drawable.ic_meta_error),
|
||||||
|
UNKNOWN(R.drawable.ic_meta_error),
|
||||||
|
DISABLED(R.drawable.ic_meta_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrefFileNotFoundError(message: String) : Exception(message)
|
||||||
|
class PrefIOError(message: String) : Exception(message)
|
||||||
|
class PrefFormatError(message: String) : Exception(message)
|
|
@ -170,4 +170,16 @@
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
<string name="alert_dialog_storage_permission_text">Please reboot your phone or restart AAPS from the System Settings \notherwise Android APS will not have logging (important to track and verify that the algorithms are working correctly)!</string>
|
<string name="alert_dialog_storage_permission_text">Please reboot your phone or restart AAPS from the System Settings \notherwise Android APS will not have logging (important to track and verify that the algorithms are working correctly)!</string>
|
||||||
|
|
||||||
|
<!-- Maintenance -->
|
||||||
|
<string name="metadata_label_format">File format</string>
|
||||||
|
<string name="metadata_label_created_at">Created at</string>
|
||||||
|
<string name="metadata_label_aaps_version">AAPS Version</string>
|
||||||
|
<string name="metadata_label_aaps_flavour">Build Variant</string>
|
||||||
|
<string name="metadata_label_device_name">Exporting device patient name</string>
|
||||||
|
<string name="metadata_label_device_model">Exporting device model</string>
|
||||||
|
<string name="metadata_label_encryption">File encryption</string>
|
||||||
|
<string name="metadata_format_new">New encrypted format</string>
|
||||||
|
<string name="metadata_format_debug">New debug format (unencrypted)</string>
|
||||||
|
<string name="metadata_format_other">Unknown export format</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -120,7 +120,7 @@ class TreatmentsUserEntryFragment : DaggerFragment(), MenuProvider {
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class UserEntryAdapter internal constructor(var entries: List<UserEntry>) : RecyclerView.Adapter<UserEntryAdapter.UserEntryViewHolder>() {
|
inner class UserEntryAdapter internal constructor(private var entries: List<UserEntry>) : RecyclerView.Adapter<UserEntryAdapter.UserEntryViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserEntryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserEntryViewHolder {
|
||||||
val view: View = LayoutInflater.from(parent.context).inflate(R.layout.treatments_user_entry_item, parent, false)
|
val view: View = LayoutInflater.from(parent.context).inflate(R.layout.treatments_user_entry_item, parent, false)
|
||||||
|
|
Loading…
Reference in a new issue