Adds support for preferences directory, list of preferences to import, additional check on imported preferences metadata

This commit is contained in:
Dominik Dzienia 2020-05-05 18:35:10 +02:00
parent d8f27c609c
commit 9a3cc5af82
16 changed files with 737 additions and 112 deletions

View file

@ -31,6 +31,7 @@ ext {
retrofit2Version = '2.8.1' retrofit2Version = '2.8.1'
okhttp3Version = '4.6.0' okhttp3Version = '4.6.0'
coroutinesVersion = '1.3.5' coroutinesVersion = '1.3.5'
activityVersion = '1.2.0-alpha03'
} }
@ -264,6 +265,8 @@ dependencies {
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.percentlayout:percentlayout:1.0.0' implementation 'androidx.percentlayout:percentlayout:1.0.0'
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.activity:activity:${activityVersion}"
implementation "androidx.activity:activity-ktx:${activityVersion}"
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.wdullaer:materialdatetimepicker:4.2.3' implementation 'com.wdullaer:materialdatetimepicker:4.2.3'

View file

@ -82,6 +82,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".plugins.pump.danaRS.activities.PairingHelperActivity" /> <activity android:name=".plugins.pump.danaRS.activities.PairingHelperActivity" />
<activity android:name=".plugins.general.maintenance.activities.PrefImportListActivity" />
<activity android:name=".historyBrowser.HistoryBrowseActivity" /> <activity android:name=".historyBrowser.HistoryBrowseActivity" />
<activity android:name=".activities.SurveyActivity" /> <activity android:name=".activities.SurveyActivity" />
<activity android:name=".activities.StatsActivity" /> <activity android:name=".activities.StatsActivity" />

View file

@ -6,6 +6,7 @@ import info.nightscout.androidaps.MainActivity
import info.nightscout.androidaps.activities.* import info.nightscout.androidaps.activities.*
import info.nightscout.androidaps.historyBrowser.HistoryBrowseActivity import info.nightscout.androidaps.historyBrowser.HistoryBrowseActivity
import info.nightscout.androidaps.plugins.general.maintenance.activities.LogSettingActivity import info.nightscout.androidaps.plugins.general.maintenance.activities.LogSettingActivity
import info.nightscout.androidaps.plugins.general.maintenance.activities.PrefImportListActivity
import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity
import info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity import info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity
import info.nightscout.androidaps.plugins.pump.common.dialog.RileyLinkBLEScanActivity import info.nightscout.androidaps.plugins.pump.common.dialog.RileyLinkBLEScanActivity
@ -48,4 +49,5 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector abstract fun contributesStatsActivity(): StatsActivity @ContributesAndroidInjector abstract fun contributesStatsActivity(): StatsActivity
@ContributesAndroidInjector abstract fun contributesSurveyActivity(): SurveyActivity @ContributesAndroidInjector abstract fun contributesSurveyActivity(): SurveyActivity
@ContributesAndroidInjector abstract fun contributesTDDStatsActivity(): TDDStatsActivity @ContributesAndroidInjector abstract fun contributesTDDStatsActivity(): TDDStatsActivity
@ContributesAndroidInjector abstract fun contributesPrefImportListActivity(): PrefImportListActivity
} }

View file

@ -3,6 +3,7 @@ package info.nightscout.androidaps.dependencyInjection
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs
import info.nightscout.androidaps.plugins.general.maintenance.PrefFileListProvider
import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat
import info.nightscout.androidaps.plugins.general.maintenance.formats.EncryptedPrefsFormat import info.nightscout.androidaps.plugins.general.maintenance.formats.EncryptedPrefsFormat
import info.nightscout.androidaps.utils.CryptoUtil import info.nightscout.androidaps.utils.CryptoUtil
@ -15,4 +16,5 @@ abstract class PreferencesModule {
@ContributesAndroidInjector abstract fun importExportPrefsInjector(): ImportExportPrefs @ContributesAndroidInjector abstract fun importExportPrefsInjector(): ImportExportPrefs
@ContributesAndroidInjector abstract fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat @ContributesAndroidInjector abstract fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat
@ContributesAndroidInjector abstract fun classicPrefsFormatInjector(): ClassicPrefsFormat @ContributesAndroidInjector abstract fun classicPrefsFormatInjector(): ClassicPrefsFormat
@ContributesAndroidInjector abstract fun prefImportListProviderInjector(): PrefFileListProvider
} }

View file

@ -116,6 +116,14 @@ class VersionCheckerUtils @Inject constructor(
private fun String?.toNumberList() = private fun String?.toNumberList() =
this?.numericVersionPart().takeIf { !it.isNullOrBlank() }?.split(".")?.map { it.toInt() } this?.numericVersionPart().takeIf { !it.isNullOrBlank() }?.split(".")?.map { it.toInt() }
fun versionDigits(versionString: String?): IntArray {
val digits = mutableListOf<Int>()
versionString?.numericVersionPart().toNumberList()?.let {
digits.addAll(it.take(4))
}
return digits.toIntArray()
}
fun findVersion(file: String?): String? { fun findVersion(file: String?): String? {
val regex = "(.*)version(.*)\"(((\\d+)\\.)+(\\d+))\"(.*)".toRegex() val regex = "(.*)version(.*)\"(((\\d+)\\.)+(\\d+))\"(.*)".toRegex()
return file?.lines()?.filter { regex.matches(it) }?.mapNotNull { regex.matchEntire(it)?.groupValues?.getOrNull(3) }?.firstOrNull() return file?.lines()?.filter { regex.matches(it) }?.mapNotNull { regex.matchEntire(it)?.groupValues?.getOrNull(3) }?.firstOrNull()

View file

@ -6,12 +6,12 @@ import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.provider.Settings import android.provider.Settings
import androidx.activity.invoke
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import info.nightscout.androidaps.BuildConfig import info.nightscout.androidaps.BuildConfig
import info.nightscout.androidaps.R import info.nightscout.androidaps.R
import info.nightscout.androidaps.activities.PreferencesActivity import info.nightscout.androidaps.activities.PreferencesActivity
@ -22,9 +22,9 @@ import info.nightscout.androidaps.plugins.bus.RxBusWrapper
import info.nightscout.androidaps.plugins.general.maintenance.formats.* import info.nightscout.androidaps.plugins.general.maintenance.formats.*
import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword
import info.nightscout.androidaps.utils.DateUtil import info.nightscout.androidaps.utils.DateUtil
import info.nightscout.androidaps.utils.ToastUtils
import info.nightscout.androidaps.utils.alertDialogs.OKDialog import info.nightscout.androidaps.utils.alertDialogs.OKDialog
import info.nightscout.androidaps.utils.alertDialogs.OKDialog.show import info.nightscout.androidaps.utils.alertDialogs.OKDialog.show
import info.nightscout.androidaps.utils.ToastUtils
import info.nightscout.androidaps.utils.alertDialogs.PrefImportSummaryDialog import info.nightscout.androidaps.utils.alertDialogs.PrefImportSummaryDialog
import info.nightscout.androidaps.utils.alertDialogs.TwoMessagesAlertDialog import info.nightscout.androidaps.utils.alertDialogs.TwoMessagesAlertDialog
import info.nightscout.androidaps.utils.alertDialogs.WarningDialog import info.nightscout.androidaps.utils.alertDialogs.WarningDialog
@ -32,8 +32,6 @@ import info.nightscout.androidaps.utils.buildHelper.BuildHelper
import info.nightscout.androidaps.utils.protection.PasswordCheck import info.nightscout.androidaps.utils.protection.PasswordCheck
import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import org.joda.time.DateTime
import org.joda.time.Days
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
@ -51,37 +49,25 @@ private val PERMISSIONS_STORAGE = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) )
private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60
@Singleton @Singleton
class ImportExportPrefs @Inject constructor( class ImportExportPrefs @Inject constructor(
private var log: AAPSLogger, private var log: AAPSLogger,
private val resourceHelper: ResourceHelper, private val resourceHelper: ResourceHelper,
private val sp: SP, private val sp: SP,
private val buildHelper: BuildHelper, private val buildHelper: BuildHelper,
private val otp: OneTimePassword,
private val rxBus: RxBusWrapper, private val rxBus: RxBusWrapper,
private val passwordCheck: PasswordCheck, private val passwordCheck: PasswordCheck,
private val classicPrefsFormat: ClassicPrefsFormat, private val classicPrefsFormat: ClassicPrefsFormat,
private val encryptedPrefsFormat: EncryptedPrefsFormat private val encryptedPrefsFormat: EncryptedPrefsFormat,
private val prefFileList: PrefFileListProvider
) { ) {
val TAG = LTag.CORE val TAG = LTag.CORE
private val path = File(Environment.getExternalStorageDirectory().toString())
private val file = File(path, resourceHelper.gs(R.string.app_name) + "Preferences")
private val encFile = File(path, resourceHelper.gs(R.string.app_name) + "Preferences.json")
fun prefsImportFile(): File {
return if (encFile.exists()) encFile else file
}
fun prefsFileExists(): Boolean { fun prefsFileExists(): Boolean {
return encFile.exists() || file.exists() return prefFileList.listPreferenceFiles().size > 0
} }
fun exportSharedPreferences(f: Fragment) { fun exportSharedPreferences(f: Fragment) {
f.activity?.let { exportSharedPreferences(it) } f.activity?.let { exportSharedPreferences(it) }
} }
@ -135,9 +121,6 @@ class ImportExportPrefs @Inject constructor(
return name return name
} }
private fun getCurrentDeviceModelString() =
Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")"
private fun prefsEncryptionIsDisabled() = private fun prefsEncryptionIsDisabled() =
buildHelper.isEngineeringMode() && !sp.getBoolean(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs), true) buildHelper.isEngineeringMode() && !sp.getBoolean(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs), true)
@ -173,36 +156,41 @@ class ImportExportPrefs @Inject constructor(
return true return true
} }
private fun askToConfirmExport(activity: Activity, then: ((password: String) -> Unit)) { private fun askToConfirmExport(activity: Activity, fileToExport: File, then: ((password: String) -> Unit)) {
if (!prefsEncryptionIsDisabled() && !assureMasterPasswordSet(activity, R.string.nav_export)) return if (!prefsEncryptionIsDisabled() && !assureMasterPasswordSet(activity, R.string.nav_export)) return
TwoMessagesAlertDialog.showAlert(activity, resourceHelper.gs(R.string.nav_export), TwoMessagesAlertDialog.showAlert(activity, resourceHelper.gs(R.string.nav_export),
resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", resourceHelper.gs(R.string.export_to) + " " + fileToExport + " ?",
resourceHelper.gs(R.string.password_preferences_encrypt_prompt), { resourceHelper.gs(R.string.password_preferences_encrypt_prompt), {
askForMasterPassIfNeeded(activity, R.string.preferences_export_canceled, then) askForMasterPassIfNeeded(activity, R.string.preferences_export_canceled, then)
}, null, R.drawable.ic_header_export) }, null, R.drawable.ic_header_export)
} }
private fun askToConfirmImport(activity: Activity, fileToImport: File, then: ((password: String) -> Unit)) { private fun askToConfirmImport(activity: Activity, fileToImport: PrefsFile, then: ((password: String) -> Unit)) {
if (encFile.exists()) { if (fileToImport.handler == PrefsFormatsHandler.ENCRYPTED) {
if (!assureMasterPasswordSet(activity, R.string.nav_import)) return if (!assureMasterPasswordSet(activity, R.string.nav_import)) return
TwoMessagesAlertDialog.showAlert(activity, resourceHelper.gs(R.string.nav_import), TwoMessagesAlertDialog.showAlert(activity, resourceHelper.gs(R.string.nav_import),
resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", resourceHelper.gs(R.string.import_from) + " " + fileToImport.file + " ?",
resourceHelper.gs(R.string.password_preferences_decrypt_prompt), { resourceHelper.gs(R.string.password_preferences_decrypt_prompt), {
askForMasterPass(activity, R.string.preferences_import_canceled, then) askForMasterPass(activity, R.string.preferences_import_canceled, then)
}, null, R.drawable.ic_header_import) }, null, R.drawable.ic_header_import)
} else { } else {
OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.nav_import), OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.nav_import),
resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", resourceHelper.gs(R.string.import_from) + " " + fileToImport.file + " ?",
Runnable { then("") }) Runnable { then("") })
} }
} }
private fun exportSharedPreferences(activity: Activity) { private fun exportSharedPreferences(activity: Activity) {
askToConfirmExport(activity) { password ->
prefFileList.ensureExportDirExists()
val legacyFile = prefFileList.legacyFile()
val newFile = prefFileList.newExportFile()
askToConfirmExport(activity, newFile) { password ->
try { try {
val entries: MutableMap<String, String> = mutableMapOf() val entries: MutableMap<String, String> = mutableMapOf()
for ((key, value) in sp.getAll()) { for ((key, value) in sp.getAll()) {
@ -211,12 +199,14 @@ class ImportExportPrefs @Inject constructor(
val prefs = Prefs(entries, prepareMetadata(activity)) val prefs = Prefs(entries, prepareMetadata(activity))
classicPrefsFormat.savePreferences(file, prefs) if (BuildConfig.DEBUG && buildHelper.isEngineeringMode()) {
encryptedPrefsFormat.savePreferences(encFile, prefs, password) classicPrefsFormat.savePreferences(legacyFile, prefs)
}
encryptedPrefsFormat.savePreferences(newFile, prefs, password)
ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported)) ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + encFile) ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + newFile)
log.error(TAG, "Unhandled exception", e) log.error(TAG, "Unhandled exception", e)
} catch (e: IOException) { } catch (e: IOException) {
ToastUtils.errorToast(activity, e.message) ToastUtils.errorToast(activity, e.message)
@ -226,21 +216,38 @@ class ImportExportPrefs @Inject constructor(
} }
fun importSharedPreferences(fragment: Fragment) { fun importSharedPreferences(fragment: Fragment) {
fragment.activity?.let { importSharedPreferences(it) } fragment.activity?.let { fragmentAct ->
val callForPrefFile = fragmentAct.prepareCall(PrefsFileContract()) {
it?.let {
importSharedPreferences(fragmentAct, it)
}
}
callForPrefFile.invoke()
}
} }
fun importSharedPreferences(activity: Activity) { fun importSharedPreferences(activity: FragmentActivity) {
val callForPrefFile = activity.prepareCall(PrefsFileContract()) {
it?.let {
importSharedPreferences(activity, it)
}
}
callForPrefFile.invoke()
}
val importFile = prefsImportFile() private fun importSharedPreferences(activity: Activity, importFile: PrefsFile) {
askToConfirmImport(activity, importFile) { password -> askToConfirmImport(activity, importFile) { password ->
val format: PrefsFormat = if (encFile.exists()) encryptedPrefsFormat else classicPrefsFormat val format: PrefsFormat = when (importFile.handler) {
PrefsFormatsHandler.CLASSIC -> classicPrefsFormat
PrefsFormatsHandler.ENCRYPTED -> encryptedPrefsFormat
}
try { try {
val prefs = format.loadPreferences(importFile, password) val prefs = format.loadPreferences(importFile.file, password)
prefs.metadata = checkMetadata(prefs.metadata) prefs.metadata = prefFileList.checkMetadata(prefs.metadata)
// import is OK when we do not have errors (warnings are allowed) // import is OK when we do not have errors (warnings are allowed)
val importOk = checkIfImportIsOk(prefs) val importOk = checkIfImportIsOk(prefs)
@ -276,45 +283,6 @@ class ImportExportPrefs @Inject constructor(
} }
} }
// check metadata for known issues, change their status and add info with explanations
private fun checkMetadata(metadata: Map<PrefsMetadataKey, PrefMetadata>): Map<PrefsMetadataKey, PrefMetadata> {
val meta = metadata.toMutableMap()
meta[PrefsMetadataKey.AAPS_FLAVOUR]?.let { flavour ->
val flavourOfPrefs = flavour.value
if (flavour.value != BuildConfig.FLAVOR) {
flavour.status = PrefsStatus.WARN
flavour.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR)
}
}
meta[PrefsMetadataKey.DEVICE_MODEL]?.let { model ->
if (model.value != getCurrentDeviceModelString()) {
model.status = PrefsStatus.WARN
model.info = resourceHelper.gs(R.string.metadata_warning_different_device)
}
}
meta[PrefsMetadataKey.CREATED_AT]?.let { createdAt ->
try {
val date1 = DateTime.parse(createdAt.value);
val date2 = DateTime.now()
val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays()
if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) {
createdAt.status = PrefsStatus.WARN
createdAt.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString())
}
} catch (e: Exception) {
createdAt.status = PrefsStatus.WARN
createdAt.info = resourceHelper.gs(R.string.metadata_warning_date_format)
}
}
return meta
}
private fun checkIfImportIsOk(prefs: Prefs): Boolean { private fun checkIfImportIsOk(prefs: Prefs): Boolean {
var importOk = true var importOk = true

View file

@ -0,0 +1,219 @@
package info.nightscout.androidaps.plugins.general.maintenance
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContract
import info.nightscout.androidaps.BuildConfig
import info.nightscout.androidaps.R
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerUtils
import info.nightscout.androidaps.plugins.general.maintenance.activities.PrefImportListActivity
import info.nightscout.androidaps.plugins.general.maintenance.formats.*
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import info.nightscout.androidaps.utils.storage.Storage
import kotlinx.android.parcel.Parcelize
import kotlinx.android.parcel.RawValue
import org.joda.time.DateTime
import org.joda.time.Days
import org.joda.time.Hours
import org.joda.time.LocalDateTime
import org.joda.time.format.DateTimeFormat
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
enum class PrefsImportDir {
ROOT_DIR,
AAPS_DIR
}
@Parcelize
data class PrefsFile(
val file: File,
val baseDir: File,
val dirKind: PrefsImportDir,
val handler: PrefsFormatsHandler,
// metadata here is used only for list display
val metadata: @RawValue Map<PrefsMetadataKey, PrefMetadata>
) : Parcelable
class PrefsFileContract : ActivityResultContract<Void, PrefsFile>() {
companion object {
const val OUTPUT_PARAM = "prefs_file"
}
override fun parseResult(resultCode: Int, intent: Intent?): PrefsFile? {
return when (resultCode) {
Activity.RESULT_OK -> intent?.getParcelableExtra(OUTPUT_PARAM)
else -> null
}
}
override fun createIntent(context: Context, input: Void?): Intent {
return Intent(context, PrefImportListActivity::class.java)
}
}
fun getCurrentDeviceModelString() =
Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")"
@Singleton
class PrefFileListProvider @Inject constructor(
private val resourceHelper: ResourceHelper,
private val classicPrefsFormat: ClassicPrefsFormat,
private val encryptedPrefsFormat: EncryptedPrefsFormat,
private val storage: Storage,
private val versionCheckerUtils: VersionCheckerUtils
) {
companion object {
private val path = File(Environment.getExternalStorageDirectory().toString())
private val aapsPath = File(path, "AAPS" + File.separator + "preferences")
private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60
}
/**
* This function tries to list possible preference files from main SDCard root dir and AAPS/preferences dir
* and tries to do quick assessment for preferences format plausibility.
* It does NOT load full metadata or is 100% accurate - it tries to do QUICK detection, based on:
* - file name and extension
* - predicted file contents
*/
fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList<PrefsFile> {
val prefFiles = mutableListOf<PrefsFile>()
// searching rood dir for legacy files
path.walk().maxDepth(1).filter { it.isFile && (it.name.endsWith(".json") || it.name.contains("Preferences")) }.forEach {
val contents = storage.getFileContents(it)
val detectedNew = encryptedPrefsFormat.isPreferencesFile(it, contents)
val detectedOld = !detectedNew && classicPrefsFormat.isPreferencesFile(it, contents)
if (detectedNew || detectedOld) {
val formatHandler = if (detectedNew) PrefsFormatsHandler.ENCRYPTED else PrefsFormatsHandler.CLASSIC
prefFiles.add(PrefsFile(it, path, PrefsImportDir.ROOT_DIR, formatHandler, metadataFor(loadMetadata, formatHandler, contents)))
}
}
// searching dedicated dir, only for new JSON format
aapsPath.walk().filter { it.isFile && it.name.endsWith(".json") }.forEach {
val contents = storage.getFileContents(it)
if (encryptedPrefsFormat.isPreferencesFile(it, contents)) {
prefFiles.add(PrefsFile(it, aapsPath, PrefsImportDir.AAPS_DIR, PrefsFormatsHandler.ENCRYPTED, metadataFor(loadMetadata, PrefsFormatsHandler.ENCRYPTED, contents)))
}
}
// we sort only if we have metadata to be used for that
if (loadMetadata) {
prefFiles.sortWith(
compareByDescending<PrefsFile> { it.handler }
.thenBy { it.metadata[PrefsMetadataKey.AAPS_FLAVOUR]?.status }
.thenByDescending { it.metadata[PrefsMetadataKey.CREATED_AT]?.value }
)
}
return prefFiles
}
private fun metadataFor(loadMetadata: Boolean, formatHandler: PrefsFormatsHandler, contents: String): PrefMetadataMap {
if (!loadMetadata) {
return mapOf()
}
return checkMetadata(when (formatHandler) {
PrefsFormatsHandler.CLASSIC -> classicPrefsFormat.loadMetadata(contents)
PrefsFormatsHandler.ENCRYPTED -> encryptedPrefsFormat.loadMetadata(contents)
})
}
fun legacyFile(): File {
return File(path, resourceHelper.gs(R.string.app_name) + "Preferences")
}
fun ensureExportDirExists() {
if (!aapsPath.exists()) {
aapsPath.mkdirs()
}
}
fun newExportFile(): File {
val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss"))
return File(aapsPath, timeLocal + "_" + BuildConfig.FLAVOR + ".json")
}
// check metadata for known issues, change their status and add info with explanations
fun checkMetadata(metadata: Map<PrefsMetadataKey, PrefMetadata>): Map<PrefsMetadataKey, PrefMetadata> {
val meta = metadata.toMutableMap()
meta[PrefsMetadataKey.AAPS_FLAVOUR]?.let { flavour ->
val flavourOfPrefs = flavour.value
if (flavour.value != BuildConfig.FLAVOR) {
flavour.status = PrefsStatus.WARN
flavour.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR)
}
}
meta[PrefsMetadataKey.DEVICE_MODEL]?.let { model ->
if (model.value != getCurrentDeviceModelString()) {
model.status = PrefsStatus.WARN
model.info = resourceHelper.gs(R.string.metadata_warning_different_device)
}
}
meta[PrefsMetadataKey.CREATED_AT]?.let { createdAt ->
try {
val date1 = DateTime.parse(createdAt.value);
val date2 = DateTime.now()
val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays()
if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) {
createdAt.status = PrefsStatus.WARN
createdAt.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString())
}
} catch (e: Exception) {
createdAt.status = PrefsStatus.WARN
createdAt.info = resourceHelper.gs(R.string.metadata_warning_date_format)
}
}
meta[PrefsMetadataKey.AAPS_VERSION]?.let { version ->
val currentAppVer = versionCheckerUtils.versionDigits(BuildConfig.VERSION_NAME)
val metadataVer = versionCheckerUtils.versionDigits(version.value)
if ((currentAppVer.size >= 2) && (metadataVer.size >= 2) && (Math.abs(currentAppVer[1] - metadataVer[1]) > 1)) {
version.status = PrefsStatus.WARN
version.info = resourceHelper.gs(R.string.metadata_warning_different_version)
}
if ((currentAppVer.isNotEmpty()) && (metadataVer.isNotEmpty()) && (currentAppVer[0] != metadataVer[0])) {
version.status = PrefsStatus.WARN
version.info = resourceHelper.gs(R.string.metadata_urgent_different_version)
}
}
return meta
}
fun formatExportedAgo(utcTime: String): String {
val refTime = DateTime.now()
val itTime = DateTime.parse(utcTime)
val days = Days.daysBetween(itTime, refTime).days
val hours = Hours.hoursBetween(itTime, refTime).hours
return if (hours == 0) {
resourceHelper.gs(R.string.exported_less_than_hour_ago)
} else if ((hours < 24) && (hours > 0)) {
resourceHelper.gs(R.string.exported_ago, resourceHelper.gq(R.plurals.objective_hours, hours, hours))
} else if ((days < IMPORT_AGE_NOT_YET_OLD_DAYS) && (days > 0)) {
resourceHelper.gs(R.string.exported_ago, resourceHelper.gq(R.plurals.objective_days, days, days))
} else {
resourceHelper.gs(R.string.exported_at, utcTime.substring(0, 10))
}
}
}

View file

@ -0,0 +1,135 @@
package info.nightscout.androidaps.plugins.general.maintenance.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.android.support.DaggerAppCompatActivity
import info.nightscout.androidaps.R
import info.nightscout.androidaps.plugins.general.maintenance.PrefFileListProvider
import info.nightscout.androidaps.plugins.general.maintenance.PrefsFile
import info.nightscout.androidaps.plugins.general.maintenance.PrefsFileContract
import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsFormatsHandler
import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsMetadataKey
import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsStatus
import info.nightscout.androidaps.utils.LocaleHelper
import info.nightscout.androidaps.utils.resources.ResourceHelper
import kotlinx.android.synthetic.main.maintenance_importlist_activity.*
import javax.inject.Inject
class PrefImportListActivity : DaggerAppCompatActivity() {
@Inject lateinit var resourceHelper: ResourceHelper
@Inject lateinit var prefFileListProvider: PrefFileListProvider
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme)
setContentView(R.layout.maintenance_importlist_activity)
title = resourceHelper.gs(R.string.preferences_import_list_title)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(true)
importlist_recyclerview.layoutManager = LinearLayoutManager(this)
importlist_recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listPreferenceFiles(loadMetadata = true))
}
inner class RecyclerViewAdapter internal constructor(private var prefFileList: List<PrefsFile>) : RecyclerView.Adapter<RecyclerViewAdapter.PrefFileViewHolder>() {
inner class PrefFileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileName: TextView = itemView.findViewById(R.id.filelist_name)
var fileDir: TextView = itemView.findViewById(R.id.filelist_dir)
var metaDateTime: TextView = itemView.findViewById(R.id.meta_date_time)
var metaDeviceName: TextView = itemView.findViewById(R.id.meta_device_name)
var metaAppVersion: TextView = itemView.findViewById(R.id.meta_app_version)
var metaVariantFormat: TextView = itemView.findViewById(R.id.meta_variant_format)
var metalineName: View = itemView.findViewById(R.id.metaline_name)
var metaDateTimeIcon: View = itemView.findViewById(R.id.meta_date_time_icon)
init {
itemView.isClickable = true
itemView.setOnClickListener { v: View ->
val prefFile = fileName.tag as PrefsFile
val i = Intent()
i.putExtra(PrefsFileContract.OUTPUT_PARAM, prefFile)
setResult(Activity.RESULT_OK, i)
finish()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrefFileViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.maintenance_importlist_item, parent, false)
return PrefFileViewHolder(v)
}
override fun getItemCount(): Int {
return prefFileList.size
}
override fun onBindViewHolder(holder: PrefFileViewHolder, position: Int) {
val prefFile = prefFileList[position]
holder.fileName.text = prefFile.file.name
holder.fileName.tag = prefFile
holder.fileDir.text = resourceHelper.gs(R.string.in_directory, prefFile.file.parentFile.absolutePath)
val visible = if (prefFile.handler == PrefsFormatsHandler.CLASSIC) View.GONE else View.VISIBLE
holder.metalineName.visibility = visible
holder.metaDateTimeIcon.visibility = visible
holder.metaAppVersion.visibility = visible
if (prefFile.handler == PrefsFormatsHandler.CLASSIC) {
holder.metaVariantFormat.text = resourceHelper.gs(R.string.metadata_format_old)
holder.metaVariantFormat.setTextColor(resourceHelper.gc(R.color.metadataTextWarning))
holder.metaDateTime.text = " "
} else {
prefFile.metadata[PrefsMetadataKey.AAPS_FLAVOUR]?.let {
holder.metaVariantFormat.text = it.value
val color = if (it.status == PrefsStatus.OK) R.color.metadataOk else R.color.metadataTextWarning
holder.metaVariantFormat.setTextColor(resourceHelper.gc(color))
}
prefFile.metadata[PrefsMetadataKey.CREATED_AT]?.let {
holder.metaDateTime.text = prefFileListProvider.formatExportedAgo(it.value)
}
prefFile.metadata[PrefsMetadataKey.AAPS_VERSION]?.let {
holder.metaAppVersion.text = it.value
val color = if (it.status == PrefsStatus.OK) R.color.metadataOk else R.color.metadataTextWarning
holder.metaAppVersion.setTextColor(resourceHelper.gc(color))
}
prefFile.metadata[PrefsMetadataKey.DEVICE_NAME]?.let {
holder.metaDeviceName.text = it.value
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
return true
}
return false
}
public override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(LocaleHelper.wrap(newBase))
}
}

View file

@ -1,5 +1,6 @@
package info.nightscout.androidaps.plugins.general.maintenance.formats package info.nightscout.androidaps.plugins.general.maintenance.formats
import info.nightscout.androidaps.Constants
import info.nightscout.androidaps.R import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.storage.Storage import info.nightscout.androidaps.utils.storage.Storage
@ -19,6 +20,11 @@ class ClassicPrefsFormat @Inject constructor(
val FORMAT_KEY = "aaps_old" val FORMAT_KEY = "aaps_old"
} }
override fun isPreferencesFile(file: File, preloadedContents: String?): Boolean {
val contents = preloadedContents ?: storage.getFileContents(file)
return contents.contains("units::" + Constants.MGDL) || contents.contains("units::" + Constants.MMOL)
}
override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) {
try { try {
val contents = prefs.values.entries.joinToString("\n") { entry -> val contents = prefs.values.entries.joinToString("\n") { entry ->
@ -35,7 +41,6 @@ class ClassicPrefsFormat @Inject constructor(
override fun loadPreferences(file: File, masterPassword: String?): Prefs { override fun loadPreferences(file: File, masterPassword: String?): Prefs {
var lineParts: Array<String> var lineParts: Array<String>
val entries: MutableMap<String, String> = mutableMapOf() val entries: MutableMap<String, String> = mutableMapOf()
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
try { try {
val rawLines = storage.getFileContents(file).split("\n") val rawLines = storage.getFileContents(file).split("\n")
@ -46,9 +51,7 @@ class ClassicPrefsFormat @Inject constructor(
} }
} }
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format)) return Prefs(entries, loadMetadata())
return Prefs(entries, metadata)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath) throw PrefFileNotFoundError(file.absolutePath)
@ -57,4 +60,10 @@ class ClassicPrefsFormat @Inject constructor(
} }
} }
override fun loadMetadata(contents: String?): PrefMetadataMap {
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format))
return metadata
}
} }

View file

@ -27,6 +27,16 @@ class EncryptedPrefsFormat @Inject constructor(
val FORMAT_KEY_NOENC = "aaps_structured" val FORMAT_KEY_NOENC = "aaps_structured"
private val KEY_CONSCIENCE = "if you remove/change this, please make sure you know the consequences!" private val KEY_CONSCIENCE = "if you remove/change this, please make sure you know the consequences!"
private val FORMAT_TEST_REGEX = Regex("(\\\"format\\\"\\s*\\:\\s*\\\"aaps_[^\"]*\\\")")
}
override fun isPreferencesFile(file: File, preloadedContents: String?): Boolean {
return if (file.absolutePath.endsWith(".json")) {
val contents = preloadedContents ?: storage.getFileContents(file)
FORMAT_TEST_REGEX.containsMatchIn(contents)
} else {
false
}
} }
override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) {
@ -97,7 +107,6 @@ class EncryptedPrefsFormat @Inject constructor(
override fun loadPreferences(file: File, masterPassword: String?): Prefs { override fun loadPreferences(file: File, masterPassword: String?): Prefs {
val entries: MutableMap<String, String> = mutableMapOf() val entries: MutableMap<String, String> = mutableMapOf()
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
val issues = LinkedList<String>() val issues = LinkedList<String>()
try { try {
@ -105,25 +114,11 @@ class EncryptedPrefsFormat @Inject constructor(
val fileContents = jsonBody.replace(Regex("(?is)(\\\"file_hash\\\"\\s*\\:\\s*\\\")([^\"]*)(\\\")"), "$1--to-be-calculated--$3") val fileContents = jsonBody.replace(Regex("(?is)(\\\"file_hash\\\"\\s*\\:\\s*\\\")([^\"]*)(\\\")"), "$1--to-be-calculated--$3")
val calculatedFileHash = cryptoUtil.hmac256(fileContents, KEY_CONSCIENCE) val calculatedFileHash = cryptoUtil.hmac256(fileContents, KEY_CONSCIENCE)
val container = JSONObject(jsonBody) val container = JSONObject(jsonBody)
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = loadMetadata(container)
if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) { if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content")) {
val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key) val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key)
if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) {
throw PrefFormatError("Unsupported file format: " + fileFormat)
}
val meta = container.getJSONObject("metadata")
val security = container.getJSONObject("security") val security = container.getJSONObject("security")
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK)
for (key in meta.keys()) {
val metaKey = PrefsMetadataKey.fromKey(key)
if (metaKey != null) {
metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK)
}
}
val encrypted = fileFormat == FORMAT_KEY_ENC val encrypted = fileFormat == FORMAT_KEY_ENC
var secure: PrefsStatus = PrefsStatus.OK var secure: PrefsStatus = PrefsStatus.OK
var decryptedOk = false var decryptedOk = false
@ -208,8 +203,6 @@ class EncryptedPrefsFormat @Inject constructor(
} }
metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr) metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr)
} else {
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR)
} }
return Prefs(entries, metadata) return Prefs(entries, metadata)
@ -223,4 +216,35 @@ class EncryptedPrefsFormat @Inject constructor(
} }
} }
override fun loadMetadata(contents: String?): PrefMetadataMap {
contents?.let {
val container = JSONObject(contents)
return loadMetadata(container)
}
return mutableMapOf()
}
private fun loadMetadata(container: JSONObject): MutableMap<PrefsMetadataKey, PrefMetadata> {
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) {
val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key)
if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) {
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.metadata_format_other), PrefsStatus.ERROR)
} else {
val meta = container.getJSONObject("metadata")
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK)
for (key in meta.keys()) {
val metaKey = PrefsMetadataKey.fromKey(key)
if (metaKey != null) {
metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK)
}
}
}
} else {
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR)
}
return metadata;
}
} }

View file

@ -1,12 +1,14 @@
package info.nightscout.androidaps.plugins.general.maintenance.formats package info.nightscout.androidaps.plugins.general.maintenance.formats
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.androidaps.R import info.nightscout.androidaps.R
import kotlinx.android.parcel.Parcelize
import java.io.File import java.io.File
enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringRes val label:Int) { enum class PrefsMetadataKey(val key: String, @DrawableRes val icon: Int, @StringRes val label: Int) {
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),
@ -33,10 +35,9 @@ enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringR
} }
} }
} }
fun formatForDisplay(context: Context, value:String): String { fun formatForDisplay(context: Context, value: String): String {
return when (this) { return when (this) {
FILE_FORMAT -> when (value) { FILE_FORMAT -> when (value) {
ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old) ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old)
@ -51,16 +52,21 @@ enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringR
} }
data class PrefMetadata(var value : String, var status : PrefsStatus, var info : String? = null) @Parcelize
data class PrefMetadata(var value: String, var status: PrefsStatus, var info: String? = null) : Parcelable
data class Prefs(val values : Map<String, String>, var metadata : Map<PrefsMetadataKey, PrefMetadata>) typealias PrefMetadataMap = Map<PrefsMetadataKey, PrefMetadata>
data class Prefs(val values: Map<String, String>, var metadata: PrefMetadataMap)
interface PrefsFormat { interface PrefsFormat {
fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null) fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null)
fun loadPreferences(file: File, masterPassword: String? = null) : Prefs 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) { enum class PrefsStatus(@DrawableRes val icon: Int) {
OK(R.drawable.ic_meta_ok), OK(R.drawable.ic_meta_ok),
WARN(R.drawable.ic_meta_warning), WARN(R.drawable.ic_meta_warning),
ERROR(R.drawable.ic_meta_error), ERROR(R.drawable.ic_meta_error),
@ -68,6 +74,11 @@ enum class PrefsStatus(@DrawableRes val icon:Int) {
DISABLED(R.drawable.ic_meta_error) DISABLED(R.drawable.ic_meta_error)
} }
enum class PrefsFormatsHandler {
CLASSIC,
ENCRYPTED
}
class PrefFileNotFoundError(message: String) : Exception(message) class PrefFileNotFoundError(message: String) : Exception(message)
class PrefIOError(message: String) : Exception(message) class PrefIOError(message: String) : Exception(message)
class PrefFormatError(message: String) : Exception(message) class PrefFormatError(message: String) : Exception(message)

View file

@ -0,0 +1,24 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="info.nightscout.androidaps.plugins.general.maintenance.activities.PrefImportListActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="4dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/importlist_recyclerview"
android:layout_width="match_parent"
android:scrollbars="vertical"
android:fadeScrollbars="true"
android:scrollbarStyle="outsideOverlay"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/careportal_cardview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
card_view:cardBackgroundColor="?android:colorBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:layout_marginBottom="3dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="true"
android:orientation="horizontal">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="5dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="6dp"
android:layout_marginBottom="1dp"
android:src="@drawable/ic_meta_format"
android:tint="@color/importListFileName" />
<TextView
android:id="@+id/filelist_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="none"
android:maxLines="2"
android:paddingEnd="10dp"
android:scrollHorizontally="false"
android:text="File name here"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/importListFileName"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="true"
android:orientation="horizontal">
<TextView
android:id="@+id/filelist_dir"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="29dp"
android:layout_weight="1"
android:ellipsize="none"
android:maxLines="2"
android:paddingEnd="10dp"
android:scrollHorizontally="false"
android:text="File dir here"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/importListFileName"
android:textSize="11sp"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:id="@+id/metaline_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="6dp"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="30dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="1dp"
android:src="@drawable/ic_meta_name"
android:tint="@color/importListAdditionalInfo" />
<TextView
android:id="@+id/meta_device_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="exported on this Patient phone"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/importListAdditionalInfo"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="3dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/meta_date_time_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="30dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="1dp"
android:src="@drawable/ic_meta_date"
android:tint="@color/importListAdditionalInfo" />
<TextView
android:id="@+id/meta_date_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="exported how long ago"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/importListAdditionalInfo"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/meta_app_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="v10.10.10.10"
android:textColor="@color/metadataTextWarning"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/meta_variant_format"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="FLAVOUR"
android:textColor="@color/metadataOk"
tools:ignore="HardcodedText" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="2dip"
android:layout_marginStart="5dp"
android:layout_marginTop="7dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="3dp"
android:background="@color/listdelimiter" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -90,7 +90,11 @@
<color name="toastWarn">#FF8C00</color> <color name="toastWarn">#FF8C00</color>
<color name="toastInfo">#03A9F4</color> <color name="toastInfo">#03A9F4</color>
<color name="metadataOk">#77dd77</color>
<color name="metadataTextWarning">#FF8C00</color> <color name="metadataTextWarning">#FF8C00</color>
<color name="metadataTextError">#FF5555</color> <color name="metadataTextError">#FF5555</color>
<color name="importListFileName">#FFFFFF</color>
<color name="importListAdditionalInfo">#BBBBBB</color>
</resources> </resources>

View file

@ -257,6 +257,8 @@
<string name="preferences_export_canceled">Export canceled! Preferences were NOT exported!</string> <string name="preferences_export_canceled">Export canceled! Preferences were NOT exported!</string>
<string name="preferences_import_canceled">Import canceled! Preferences were NOT imported!</string> <string name="preferences_import_canceled">Import canceled! Preferences were NOT imported!</string>
<string name="preferences_import_list_title">Select file to import</string>
<string name="check_preferences_before_import">Please check preferences before importing:</string> <string name="check_preferences_before_import">Please check preferences before importing:</string>
<string name="check_preferences_cannot_import">Preferences cannot be imported!</string> <string name="check_preferences_cannot_import">Preferences cannot be imported!</string>
<string name="check_preferences_dangerous_import">Preferences should not be imported!</string> <string name="check_preferences_dangerous_import">Preferences should not be imported!</string>
@ -270,6 +272,8 @@
<string name="metadata_warning_outdated_format">You are using the outdated legacy format from old versions of AAPS, which is not secure! Only use it as a last resort, if you do not have an export in current, JSON format.</string> <string name="metadata_warning_outdated_format">You are using the outdated legacy format from old versions of AAPS, which is not secure! Only use it as a last resort, if you do not have an export in current, JSON format.</string>
<string name="metadata_warning_old_export">Imported preferences are already %1$s days old! Maybe you have more up-to-date preferences or you choose the wrong file? Remember to export preferences regularly!</string> <string name="metadata_warning_old_export">Imported preferences are already %1$s days old! Maybe you have more up-to-date preferences or you choose the wrong file? Remember to export preferences regularly!</string>
<string name="metadata_warning_date_format">Invalid date-time format!</string> <string name="metadata_warning_date_format">Invalid date-time format!</string>
<string name="metadata_warning_different_version">Preferences from different minor version of application. It is OK if you are importing after upgrade, but check after import if preferences are still correct!</string>
<string name="metadata_urgent_different_version">Preferences from different major version of application. Major versions differ significantly and may have incompatible preferences! Make sure after import that preferences are still correct!</string>
<string name="metadata_label_format">File format</string> <string name="metadata_label_format">File format</string>
<string name="metadata_label_created_at">Created at</string> <string name="metadata_label_created_at">Created at</string>
@ -297,6 +301,12 @@
<string name="prefdecrypt_issue_wrong_format">Missing encryption configuration, settings format is invalid!</string> <string name="prefdecrypt_issue_wrong_format">Missing encryption configuration, settings format is invalid!</string>
<string name="prefdecrypt_issue_wrong_algorithm">Unsupported or not specified encryption algorithm!</string> <string name="prefdecrypt_issue_wrong_algorithm">Unsupported or not specified encryption algorithm!</string>
<string name="exported_today">exported today</string>
<string name="exported_ago" comment="at placeholder we add pluralized number of hours/minutes">exported %1$s ago</string>
<string name="exported_at" comment="at placeholder we add export date">exported at %1$s</string>
<string name="exported_less_than_hour_ago">exported less than hour ago</string>
<string name="in_directory" comment="placeholder is for exported file path">in directory: %1$s</string>
<string name="danarpump">DanaR</string> <string name="danarpump">DanaR</string>
<string name="connecting">Connecting</string> <string name="connecting">Connecting</string>
<string name="connected">Connected</string> <string name="connected">Connected</string>

View file

@ -6,6 +6,7 @@ import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.plugins.bus.RxBusWrapper import info.nightscout.androidaps.plugins.bus.RxBusWrapper
import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -25,6 +26,41 @@ class VersionCheckerUtilsKtTest : TestBase() {
versionCheckerUtils = VersionCheckerUtils(aapsLogger, sp, resourceHelper, rxBus, context) versionCheckerUtils = VersionCheckerUtils(aapsLogger, sp, resourceHelper, rxBus, context)
} }
@Test
fun `should handle invalid version`() {
assertArrayEquals(intArrayOf(), versionCheckerUtils.versionDigits("definitely not version string"))
}
@Test
fun `should handle empty version`() {
assertArrayEquals(intArrayOf(), versionCheckerUtils.versionDigits(""))
}
@Test
fun `should parse 2 digit version`() {
assertArrayEquals(intArrayOf(0, 999), versionCheckerUtils.versionDigits("0.999-beta"))
}
@Test
fun `should parse 3 digit version`() {
assertArrayEquals(intArrayOf(6, 83, 93), versionCheckerUtils.versionDigits("6.83.93"))
}
@Test
fun `should parse 4 digit version`() {
assertArrayEquals(intArrayOf(42, 7, 13, 101), versionCheckerUtils.versionDigits("42.7.13.101"))
}
@Test
fun `should parse 4 digit version with extra`() {
assertArrayEquals(intArrayOf(1, 2, 3, 4), versionCheckerUtils.versionDigits("1.2.3.4-RC5"))
}
@Test
fun `should parse version but only 4 digits are taken`() {
assertArrayEquals(intArrayOf(67, 8, 31, 5), versionCheckerUtils.versionDigits("67.8.31.5.153.4.2"))
}
/* /*
@Test @Test
fun `should keep 2 digit version`() { fun `should keep 2 digit version`() {