Adds support for preferences directory, list of preferences to import, additional check on imported preferences metadata
This commit is contained in:
parent
d8f27c609c
commit
9a3cc5af82
16 changed files with 737 additions and 112 deletions
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
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) {
|
||||||
|
@ -33,7 +35,6 @@ 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 {
|
||||||
|
@ -51,13 +52,18 @@ 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) {
|
||||||
|
@ -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)
|
||||||
|
|
24
app/src/main/res/layout/maintenance_importlist_activity.xml
Normal file
24
app/src/main/res/layout/maintenance_importlist_activity.xml
Normal 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>
|
169
app/src/main/res/layout/maintenance_importlist_item.xml
Normal file
169
app/src/main/res/layout/maintenance_importlist_item.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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`() {
|
||||||
|
|
Loading…
Reference in a new issue