Preferences Encryption:

- encrypted JSON format support
 - using master password & password prompt
- refactored alerts
This commit is contained in:
Dominik Dzienia 2020-02-11 19:34:56 +01:00
parent fde84207ab
commit 1c97f8a720
59 changed files with 2004 additions and 196 deletions

View file

@ -26,7 +26,6 @@ import info.nightscout.androidaps.plugins.aps.openAPSMA.DetermineBasalResultMA
import info.nightscout.androidaps.plugins.aps.openAPSMA.LoggerCallback import info.nightscout.androidaps.plugins.aps.openAPSMA.LoggerCallback
import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalAdapterSMBJS import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalAdapterSMBJS
import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalResultSMB import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalResultSMB
import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin
import info.nightscout.androidaps.plugins.configBuilder.PluginStore import info.nightscout.androidaps.plugins.configBuilder.PluginStore
import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction
import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctionImplementation import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctionImplementation
@ -37,6 +36,8 @@ import info.nightscout.androidaps.plugins.general.automation.elements.*
import info.nightscout.androidaps.plugins.general.automation.triggers.* import info.nightscout.androidaps.plugins.general.automation.triggers.*
import info.nightscout.androidaps.plugins.general.overview.graphData.GraphData import info.nightscout.androidaps.plugins.general.overview.graphData.GraphData
import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs
import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat
import info.nightscout.androidaps.plugins.general.maintenance.formats.EncryptedPrefsFormat
import info.nightscout.androidaps.plugins.general.overview.notifications.NotificationWithAction import info.nightscout.androidaps.plugins.general.overview.notifications.NotificationWithAction
import info.nightscout.androidaps.plugins.general.smsCommunicator.AuthRequest import info.nightscout.androidaps.plugins.general.smsCommunicator.AuthRequest
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensData import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensData
@ -46,7 +47,6 @@ import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobThread
import info.nightscout.androidaps.plugins.treatments.Treatment import info.nightscout.androidaps.plugins.treatments.Treatment
import info.nightscout.androidaps.queue.CommandQueue import info.nightscout.androidaps.queue.CommandQueue
import info.nightscout.androidaps.queue.commands.* import info.nightscout.androidaps.queue.commands.*
import info.nightscout.androidaps.setupwizard.SWDefinition
import info.nightscout.androidaps.setupwizard.SWEventListener import info.nightscout.androidaps.setupwizard.SWEventListener
import info.nightscout.androidaps.setupwizard.SWScreen import info.nightscout.androidaps.setupwizard.SWScreen
import info.nightscout.androidaps.setupwizard.elements.* import info.nightscout.androidaps.setupwizard.elements.*
@ -55,6 +55,8 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.resources.ResourceHelperImplementation import info.nightscout.androidaps.utils.resources.ResourceHelperImplementation
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import info.nightscout.androidaps.utils.sharedPreferences.SPImplementation import info.nightscout.androidaps.utils.sharedPreferences.SPImplementation
import info.nightscout.androidaps.utils.storage.FileStorage
import info.nightscout.androidaps.utils.storage.Storage
import info.nightscout.androidaps.utils.wizard.BolusWizard import info.nightscout.androidaps.utils.wizard.BolusWizard
import info.nightscout.androidaps.utils.wizard.QuickWizardEntry import info.nightscout.androidaps.utils.wizard.QuickWizardEntry
import javax.inject.Singleton import javax.inject.Singleton
@ -104,6 +106,12 @@ open class AppModule {
return plugins.toList().sortedBy { it.first }.map { it.second } return plugins.toList().sortedBy { it.first }.map { it.second }
} }
@Provides
@Singleton
fun provideStorage(): Storage {
return FileStorage()
}
@Module @Module
interface AppBindings { interface AppBindings {
@ -251,6 +259,8 @@ open class AppModule {
@ContributesAndroidInjector fun graphDataInjector(): GraphData @ContributesAndroidInjector fun graphDataInjector(): GraphData
@ContributesAndroidInjector fun importExportPrefsInjector(): ImportExportPrefs @ContributesAndroidInjector fun importExportPrefsInjector(): ImportExportPrefs
@ContributesAndroidInjector fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat
@ContributesAndroidInjector fun classicPrefsFormatInjector(): ClassicPrefsFormat
@Binds fun bindContext(mainApp: MainApp): Context @Binds fun bindContext(mainApp: MainApp): Context
@Binds fun bindInjector(mainApp: MainApp): HasAndroidInjector @Binds fun bindInjector(mainApp: MainApp): HasAndroidInjector

View file

@ -40,6 +40,7 @@ import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpFragment
import info.nightscout.androidaps.plugins.source.BGSourceFragment import info.nightscout.androidaps.plugins.source.BGSourceFragment
import info.nightscout.androidaps.plugins.treatments.TreatmentsFragment import info.nightscout.androidaps.plugins.treatments.TreatmentsFragment
import info.nightscout.androidaps.plugins.treatments.fragments.* import info.nightscout.androidaps.plugins.treatments.fragments.*
import info.nightscout.androidaps.utils.protection.PasswordCheck
@Module @Module
@Suppress("unused") @Suppress("unused")
@ -112,4 +113,6 @@ abstract class FragmentsModule {
@ContributesAndroidInjector abstract fun contributesTreatmentDialog(): TreatmentDialog @ContributesAndroidInjector abstract fun contributesTreatmentDialog(): TreatmentDialog
@ContributesAndroidInjector abstract fun contributesWizardDialog(): WizardDialog @ContributesAndroidInjector abstract fun contributesWizardDialog(): WizardDialog
@ContributesAndroidInjector abstract fun contributesWizardInfoDialog(): WizardInfoDialog @ContributesAndroidInjector abstract fun contributesWizardInfoDialog(): WizardInfoDialog
@ContributesAndroidInjector abstract fun contributesPasswordCheck(): PasswordCheck
} }

View file

@ -2,25 +2,42 @@ package info.nightscout.androidaps.plugins.general.maintenance
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.Settings
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 info.nightscout.androidaps.BuildConfig
import info.nightscout.androidaps.R import info.nightscout.androidaps.R
import info.nightscout.androidaps.activities.PreferencesActivity
import info.nightscout.androidaps.events.EventAppExit import info.nightscout.androidaps.events.EventAppExit
import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.bus.RxBusWrapper 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.utils.DateUtil
import info.nightscout.androidaps.utils.OKDialog
import info.nightscout.androidaps.utils.OKDialog.show import info.nightscout.androidaps.utils.OKDialog.show
import info.nightscout.androidaps.utils.OKDialog.showConfirmation
import info.nightscout.androidaps.utils.ToastUtils import info.nightscout.androidaps.utils.ToastUtils
import info.nightscout.androidaps.utils.alertDialogs.PrefImportSummaryDialog
import info.nightscout.androidaps.utils.alertDialogs.TwoMessagesAlertDialog
import info.nightscout.androidaps.utils.alertDialogs.WarningDialog
import info.nightscout.androidaps.utils.buildHelper.BuildHelper
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
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -34,14 +51,20 @@ 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 rxBus: RxBusWrapper private val buildHelper: BuildHelper,
) private val otp: OneTimePassword,
{ private val rxBus: RxBusWrapper,
private val passwordCheck: PasswordCheck,
private val classicPrefsFormat: ClassicPrefsFormat,
private val encryptedPrefsFormat: EncryptedPrefsFormat
) {
val TAG = LTag.CORE val TAG = LTag.CORE
@ -50,16 +73,16 @@ class ImportExportPrefs @Inject constructor (
private val file = File(path, resourceHelper.gs(R.string.app_name) + "Preferences") 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") private val encFile = File(path, resourceHelper.gs(R.string.app_name) + "Preferences.json")
fun prefsImportFile() : File { fun prefsImportFile(): File {
return if (encFile.exists()) encFile else file return if (encFile.exists()) encFile else file
} }
fun prefsFileExists() : Boolean { fun prefsFileExists(): Boolean {
return encFile.exists() || file.exists() return encFile.exists() || file.exists()
} }
fun exportSharedPreferences(f: Fragment) { fun exportSharedPreferences(f: Fragment) {
exportSharedPreferences(f.context) exportSharedPreferences(f.activity)
} }
fun verifyStoragePermissions(fragment: Fragment) { fun verifyStoragePermissions(fragment: Fragment) {
@ -71,72 +94,247 @@ class ImportExportPrefs @Inject constructor (
} }
} }
private fun exportSharedPreferences(context: Context?) { private fun prepareMetadata(context: Context?): Map<PrefsMetadataKey, PrefMetadata> {
showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", Runnable {
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
if (context != null) {
metadata[PrefsMetadataKey.DEVICE_NAME] = PrefMetadata(detectUserName(context), PrefsStatus.OK)
}
metadata[PrefsMetadataKey.CREATED_AT] = PrefMetadata(DateUtil.toISOString(Date()), PrefsStatus.OK)
metadata[PrefsMetadataKey.AAPS_VERSION] = PrefMetadata(BuildConfig.VERSION_NAME, PrefsStatus.OK)
metadata[PrefsMetadataKey.AAPS_FLAVOUR] = PrefMetadata(BuildConfig.FLAVOR, PrefsStatus.OK)
metadata[PrefsMetadataKey.DEVICE_MODEL] = PrefMetadata(getCurrentDeviceModelString(), PrefsStatus.OK)
if (prefsEncryptionIsDisabled()) {
metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Disabled", PrefsStatus.DISABLED)
} else {
metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Enabled", PrefsStatus.OK)
}
return metadata
}
private fun detectUserName(context: Context): String {
// based on https://medium.com/@pribble88/how-to-get-an-android-device-nickname-4b4700b3068c
val n1 = Settings.System.getString(context.contentResolver, "bluetooth_name")
val n2 = Settings.Secure.getString(context.contentResolver, "bluetooth_name")
val n3 = BluetoothAdapter.getDefaultAdapter()?.name
val n4 = Settings.System.getString(context.contentResolver, "device_name")
val n5 = Settings.Secure.getString(context.contentResolver, "lock_screen_owner_info")
val n6 = Settings.Global.getString(context.contentResolver, "device_name")
// name we use for SMS OTP token in communicator
val otpName = otp.name().trim()
val defaultOtpName = resourceHelper.gs(R.string.smscommunicator_default_user_display_name)
// name we detect from OS
val systemName = n1 ?: n2 ?: n3 ?: n4 ?: n5 ?: n6 ?: defaultOtpName
val name = if (otpName.length > 0 && otpName != defaultOtpName) otpName else systemName
return name
}
private fun getCurrentDeviceModelString() =
Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")"
private fun prefsEncryptionIsDisabled() =
buildHelper.isEngineeringMode() && !sp.getBoolean(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs), true)
private fun askForMasterPass(activity: Activity?, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) {
passwordCheck.queryPassword(activity!!, R.string.master_password, R.string.key_master_password, { password ->
then(password)
}, {
ToastUtils.warnToast(activity, resourceHelper.gs(canceledMsg))
})
}
private fun askForMasterPassIfNeeded(activity: Activity?, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) {
if (prefsEncryptionIsDisabled()) {
then("")
} else {
askForMasterPass(activity, canceledMsg, then)
}
}
private fun assureMasterPasswordSet(activity: Activity?, @StringRes wrongPwdTitle: Int): Boolean {
if (!sp.contains(R.string.key_master_password) || (sp.getString(R.string.key_master_password, "") == "")) {
WarningDialog.showWarning(activity!!,
resourceHelper.gs(wrongPwdTitle),
resourceHelper.gs(R.string.master_password_missing, resourceHelper.gs(R.string.configbuilder_general), resourceHelper.gs(R.string.protection)),
R.string.nav_preferences, {
val intent = Intent(activity, PreferencesActivity::class.java).apply {
putExtra("id", R.xml.pref_general)
}
activity.startActivity(intent)
})
return false
}
return true
}
private fun askToConfirmExport(activity: Activity?, then: ((password: String) -> Unit)) {
if (!prefsEncryptionIsDisabled() && !assureMasterPasswordSet(activity, R.string.nav_export)) return
TwoMessagesAlertDialog.showAlert(activity!!, resourceHelper.gs(R.string.nav_export),
resourceHelper.gs(R.string.export_to) + " " + encFile + " ?",
resourceHelper.gs(R.string.password_preferences_encrypt_prompt), {
askForMasterPassIfNeeded(activity, R.string.preferences_export_canceled, then)
}, null, R.drawable.ic_header_export)
}
private fun askToConfirmImport(activity: Activity?, fileToImport: File, then: ((password: String) -> Unit)) {
if (encFile.exists()) {
if (!assureMasterPasswordSet(activity, R.string.nav_import)) return
TwoMessagesAlertDialog.showAlert(activity!!, resourceHelper.gs(R.string.nav_import),
resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?",
resourceHelper.gs(R.string.password_preferences_decrypt_prompt), {
askForMasterPass(activity, R.string.preferences_import_canceled, then)
}, null, R.drawable.ic_header_import)
} else {
OKDialog.showConfirmation(activity!!, resourceHelper.gs(R.string.nav_import),
resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?",
Runnable { then("") })
}
}
private fun exportSharedPreferences(activity: Activity?) {
askToConfirmExport(activity) { 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()) {
entries[key] = value.toString() entries[key] = value.toString()
} }
val prefs = Prefs(entries, mapOf()) val prefs = Prefs(entries, prepareMetadata(activity))
ClassicPrefsFormat.savePreferences(file, prefs) classicPrefsFormat.savePreferences(file, prefs)
EncryptedPrefsFormat.savePreferences(encFile, prefs) encryptedPrefsFormat.savePreferences(encFile, prefs, password)
ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.exported)) ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + encFile) ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + encFile)
log.error(TAG,"Unhandled exception", e) log.error(TAG, "Unhandled exception", e)
} catch (e: IOException) { } catch (e: IOException) {
log.error(TAG,"Unhandled exception", e) ToastUtils.errorToast(activity, e.message)
log.error(TAG, "Unhandled exception", e)
} }
}) }
} }
fun importSharedPreferences(fragment: Fragment) { fun importSharedPreferences(fragment: Fragment) {
importSharedPreferences(fragment.context) importSharedPreferences(fragment.activity)
} }
fun importSharedPreferences(context: Context?) { fun importSharedPreferences(activity: Activity?) {
val importFile = prefsImportFile() val importFile = prefsImportFile()
showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.import_from) + " " + importFile + " ?", Runnable { askToConfirmImport(activity, importFile) { password ->
val format : PrefsFormat = if (encFile.exists()) EncryptedPrefsFormat else ClassicPrefsFormat val format: PrefsFormat = if (encFile.exists()) encryptedPrefsFormat else classicPrefsFormat
try { try {
val prefs = format.loadPreferences(importFile)
sp.clear() val prefs = format.loadPreferences(importFile, password)
for ((key, value) in prefs.values) { prefs.metadata = checkMetadata(prefs.metadata)
if (value == "true" || value == "false") {
sp.putBoolean(key, value.toBoolean()) // import is OK when we do not have errors (warnings are allowed)
val importOk = checkIfImportIsOk(prefs)
// if at end we allow to import preferences
val importPossible = (importOk || buildHelper.isEngineeringMode()) && (prefs.values.size > 0)
PrefImportSummaryDialog.showSummary(activity!!, importOk, importPossible, prefs, {
if (importPossible) {
sp.clear()
for ((key, value) in prefs.values) {
if (value == "true" || value == "false") {
sp.putBoolean(key, value.toBoolean())
} else {
sp.putString(key, value)
}
}
restartAppAfterImport(activity)
} else { } else {
sp.putString(key, value) // for impossible imports it should not be called
ToastUtils.errorToast(activity, "Cannot import preferences!")
} }
}
sp.putBoolean(R.string.key_setupwizard_processed, true)
show(context, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable {
log.debug(TAG,"Exiting")
rxBus.send(EventAppExit())
if (context is Activity) {
context.finish()
}
System.runFinalization()
System.exit(0)
}) })
} catch (e: PrefFileNotFoundError) { } catch (e: PrefFileNotFoundError) {
ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + importFile) ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + importFile)
log.error(TAG,"Unhandled exception", e) log.error(TAG, "Unhandled exception", e)
} catch (e: PrefIOError) { } catch (e: PrefIOError) {
log.error(TAG,"Unhandled exception", e) log.error(TAG, "Unhandled exception", e)
ToastUtils.errorToast(activity, e.message)
} }
}
}
// 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()
if (meta.containsKey(PrefsMetadataKey.AAPS_FLAVOUR)) {
val flavourOfPrefs = meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.value
if (meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.value != BuildConfig.FLAVOR) {
meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.status = PrefsStatus.WARN
meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR)
}
}
if (meta.containsKey(PrefsMetadataKey.DEVICE_MODEL)) {
if (meta[PrefsMetadataKey.DEVICE_MODEL]!!.value != getCurrentDeviceModelString()) {
meta[PrefsMetadataKey.DEVICE_MODEL]!!.status = PrefsStatus.WARN
meta[PrefsMetadataKey.DEVICE_MODEL]!!.info = resourceHelper.gs(R.string.metadata_warning_different_device)
}
}
if (meta.containsKey(PrefsMetadataKey.CREATED_AT)) {
try {
val date1 = DateTime.parse(meta[PrefsMetadataKey.CREATED_AT]!!.value);
val date2 = DateTime.now()
val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays()
if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) {
meta[PrefsMetadataKey.CREATED_AT]!!.status = PrefsStatus.WARN
meta[PrefsMetadataKey.CREATED_AT]!!.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString())
}
} catch (e: Exception) {
meta[PrefsMetadataKey.CREATED_AT]!!.status = PrefsStatus.WARN
meta[PrefsMetadataKey.CREATED_AT]!!.info = resourceHelper.gs(R.string.metadata_warning_date_format)
}
}
return meta
}
private fun checkIfImportIsOk(prefs: Prefs): Boolean {
var importOk = true
for ((_, value) in prefs.metadata) {
if (value.status == PrefsStatus.ERROR)
importOk = false;
}
return importOk
}
private fun restartAppAfterImport(context: Context?) {
sp.putBoolean(R.string.key_setupwizard_processed, true)
show(context!!, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable {
log.debug(TAG, "Exiting")
rxBus.send(EventAppExit())
if (context is Activity) {
context.finish()
}
System.runFinalization()
System.exit(0)
}) })
} }
} }

View file

@ -4,6 +4,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import info.nightscout.androidaps.BuildConfig import info.nightscout.androidaps.BuildConfig
import info.nightscout.androidaps.Config import info.nightscout.androidaps.Config
@ -16,6 +18,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus
import info.nightscout.androidaps.utils.buildHelper.BuildHelper import info.nightscout.androidaps.utils.buildHelper.BuildHelper
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 info.nightscout.androidaps.utils.textValidator.ValidatingEditTextPreference
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -203,4 +206,13 @@ class MaintenancePlugin @Inject constructor(
emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return emailIntent return emailIntent
} }
override fun preprocessPreferences(preferenceFragment: PreferenceFragmentCompat) {
super.preprocessPreferences(preferenceFragment)
val encryptSwitch = preferenceFragment.findPreference(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs)) as SwitchPreference?
?: return
encryptSwitch.isVisible = buildHelper.isEngineeringMode()
encryptSwitch.isEnabled = buildHelper.isEngineeringMode()
}
} }

View file

@ -1,22 +1,30 @@
package info.nightscout.androidaps.plugins.general.maintenance.formats package info.nightscout.androidaps.plugins.general.maintenance.formats
import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs import info.nightscout.androidaps.R
import java.io.* import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.storage.Storage
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ClassicPrefsFormat @Inject constructor(
private var resourceHelper: ResourceHelper,
private var storage: Storage
) : PrefsFormat {
object ClassicPrefsFormat : PrefsFormat { companion object {
val FORMAT_KEY = "aaps_old"
}
const val FORMAT_KEY = "old" override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) {
override fun savePreferences(file:File, prefs: Prefs) {
try { try {
val fw = FileWriter(file) val contents = prefs.values.entries.joinToString("\n") { entry ->
val pw = PrintWriter(fw) "${entry.key}::${entry.value}"
for ((key, value) in prefs.values) {
pw.println(key + "::" + value)
} }
pw.close() storage.putFileContents(file, contents)
fw.close()
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath) throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) { } catch (e: IOException) {
@ -24,31 +32,29 @@ object ClassicPrefsFormat : PrefsFormat {
} }
} }
override fun loadPreferences(file:File): Prefs { override fun loadPreferences(file: File, masterPassword: String?): Prefs {
var line: String
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() val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
try { try {
val reader = BufferedReader(FileReader(file))
while (reader.readLine().also { line = it } != null) { val rawLines = storage.getFileContents(file).split("\n")
rawLines.forEach { line ->
lineParts = line.split("::").toTypedArray() lineParts = line.split("::").toTypedArray()
if (lineParts.size == 2) { if (lineParts.size == 2) {
entries[lineParts[0]] = lineParts[1] entries[lineParts[0]] = lineParts[1]
} }
} }
reader.close()
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN) metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format))
return Prefs(entries, metadata) return Prefs(entries, metadata)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath) throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) { } catch (e: IOException) {
throw PrefIOError(file.absolutePath) throw PrefIOError(file.absolutePath)
} }
} }
} }

View file

@ -1,25 +1,90 @@
package info.nightscout.androidaps.plugins.general.maintenance.formats package info.nightscout.androidaps.plugins.general.maintenance.formats
import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.CryptoUtil
import info.nightscout.androidaps.utils.hexStringToByteArray
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.storage.Storage
import info.nightscout.androidaps.utils.toHex
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
object EncryptedPrefsFormat : PrefsFormat { @Singleton
class EncryptedPrefsFormat @Inject constructor(
private var resourceHelper: ResourceHelper,
private var storage: Storage
) : PrefsFormat {
const val FORMAT_KEY = "new_v1" companion object {
val FORMAT_KEY_ENC = "aaps_encrypted"
val FORMAT_KEY_NOENC = "aaps_structured"
override fun savePreferences(file:File, prefs: Prefs) { private val KEY_CONSCIENCE = "if you remove/change this, please make sure you know the consequences!"
}
override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) {
val container = JSONObject() val container = JSONObject()
val content = JSONObject()
val meta = JSONObject()
val encStatus = prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status ?: PrefsStatus.OK
var encrypted = encStatus == PrefsStatus.OK && masterPassword != null
try { try {
for ((key, value) in prefs.values) { for ((key, value) in prefs.values.toSortedMap()) {
container.put(key, value) content.put(key, value)
} }
file.writeText(container.toString(2)); for ((metaKey, metaEntry) in prefs.metadata) {
if (metaKey == PrefsMetadataKey.FILE_FORMAT)
continue;
if (metaKey == PrefsMetadataKey.ENCRYPTION)
continue;
meta.put(metaKey.key, metaEntry.value)
}
container.put(PrefsMetadataKey.FILE_FORMAT.key, if (encrypted) FORMAT_KEY_ENC else FORMAT_KEY_NOENC);
container.put("metadata", meta)
val security = JSONObject()
security.put("file_hash", "--to-be-calculated--")
var encodedContent = ""
if (encrypted) {
val salt = CryptoUtil.mineSalt()
val rawContent = content.toString()
val contentAttempt = CryptoUtil.encrypt(masterPassword!!, salt, rawContent)
if (contentAttempt != null) {
encodedContent = contentAttempt
security.put("algorithm", "v1")
security.put("salt", salt.toHex())
security.put("content_hash", CryptoUtil.sha256(rawContent))
} else {
// fallback when encryption does not work
encrypted = false
}
}
if (!encrypted) {
security.put("algorithm", "none")
}
container.put("security", security)
container.put("content", if (encrypted) encodedContent else content)
var fileContents = container.toString(2)
val fileHash = CryptoUtil.hmac256(fileContents, KEY_CONSCIENCE)
fileContents = fileContents.replace(Regex("(\\\"file_hash\\\"\\s*\\:\\s*\\\")(--to-be-calculated--)(\\\")"), "$1" + fileHash + "$3")
storage.putFileContents(file, fileContents)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath) throw PrefFileNotFoundError(file.absolutePath)
@ -28,31 +93,133 @@ object EncryptedPrefsFormat : PrefsFormat {
} }
} }
override fun loadPreferences(file:File): 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 metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
val issues = LinkedList<String>()
try { try {
val jsonBody = file.readText() val jsonBody = storage.getFileContents(file)
val fileContents = jsonBody.replace(Regex("(?is)(\\\"file_hash\\\"\\s*\\:\\s*\\\")([^\"]*)(\\\")"), "$1--to-be-calculated--$3")
val calculatedFileHash = CryptoUtil.hmac256(fileContents, KEY_CONSCIENCE)
val container = JSONObject(jsonBody) val container = JSONObject(jsonBody)
for (key in container.keys()) { if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) {
entries.put(key, container[key].toString()) val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key)
}
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.OK) 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")
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
var secure: PrefsStatus = PrefsStatus.OK
var decryptedOk = false
var contentJsonObj: JSONObject? = null
var insecurityReason = resourceHelper.gs(R.string.prefdecrypt_settings_tampered)
if (security.has("file_hash")) {
if (calculatedFileHash != security.getString("file_hash")) {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_modified))
}
} else {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_missing_file_hash))
}
if (encrypted) {
if (security.has("algorithm") && security.get("algorithm") == "v1") {
if (security.has("salt") && security.has("content_hash")) {
val salt = security.getString("salt").hexStringToByteArray()
val decrypted = CryptoUtil.decrypt(masterPassword!!, salt, container.getString("content"))
if (decrypted != null) {
try {
val contentHash = CryptoUtil.sha256(decrypted)
if (contentHash == security.getString("content_hash")) {
contentJsonObj = JSONObject(decrypted)
decryptedOk = true
} else {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_modified))
}
} catch (e: JSONException) {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_parsing))
}
} else {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_pass))
insecurityReason = resourceHelper.gs(R.string.prefdecrypt_wrong_password)
}
} else {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_format))
}
} else {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_algorithm))
}
} else {
if (secure == PrefsStatus.OK) {
secure = PrefsStatus.WARN
}
if (!(security.has("algorithm") && security.get("algorithm") == "none")) {
secure = PrefsStatus.ERROR
issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_algorithm))
}
contentJsonObj = container.getJSONObject("content")
decryptedOk = true
}
if (decryptedOk && contentJsonObj != null) {
for (key in contentJsonObj.keys()) {
entries.put(key, contentJsonObj[key].toString())
}
}
val issuesStr: String? = if (issues.size > 0) issues.joinToString("\n") else null
val encryptionDescStr = if (encrypted) {
if (secure == PrefsStatus.OK) resourceHelper.gs(R.string.prefdecrypt_settings_secure) else insecurityReason
} else {
if (secure != PrefsStatus.ERROR) resourceHelper.gs(R.string.prefdecrypt_settings_unencrypted) else resourceHelper.gs(R.string.prefdecrypt_settings_tampered)
}
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)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath) throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) { } catch (e: IOException) {
throw PrefIOError(file.absolutePath) throw PrefIOError(file.absolutePath)
} catch (e: JSONException){ } catch (e: JSONException) {
throw PrefFormatError("Mallformed preferences JSON file: "+e) throw PrefFormatError("Mallformed preferences JSON file: " + e)
} }
} }
} }

View file

@ -1,24 +1,71 @@
package info.nightscout.androidaps.plugins.general.maintenance.formats package info.nightscout.androidaps.plugins.general.maintenance.formats
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import info.nightscout.androidaps.R
import java.io.File import java.io.File
enum class PrefsMetadataKey(val key: String) { enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringRes val label:Int) {
FILE_FORMAT("fileFormat")
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),
AAPS_VERSION("aaps_version", R.drawable.ic_meta_version, R.string.metadata_label_aaps_version),
AAPS_FLAVOUR("aaps_flavour", R.drawable.ic_meta_flavour, R.string.metadata_label_aaps_flavour),
DEVICE_NAME("device_name", R.drawable.ic_meta_name, R.string.metadata_label_device_name),
DEVICE_MODEL("device_model", R.drawable.ic_meta_model, R.string.metadata_label_device_model),
ENCRYPTION("encryption", R.drawable.ic_meta_encryption, R.string.metadata_label_encryption);
companion object {
private val keyToEnumMap = HashMap<String, PrefsMetadataKey>()
init {
for (value in values()) {
keyToEnumMap.put(value.key, value)
}
}
fun fromKey(key: String): PrefsMetadataKey? {
if (keyToEnumMap.containsKey(key)) {
return keyToEnumMap.get(key)
} else {
return null
}
}
}
fun formatForDisplay(context: Context, value:String): String {
return when (this) {
FILE_FORMAT -> when (value) {
ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old)
EncryptedPrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new)
EncryptedPrefsFormat.FORMAT_KEY_NOENC -> context.getString(R.string.metadata_format_debug)
else -> context.getString(R.string.metadata_format_other)
}
CREATED_AT -> value.replace("T", " ").replace("Z", " (UTC)")
else -> value
}
}
} }
data class PrefMetadata(var value : String, var status : PrefsStatus) data class PrefMetadata(var value : String, var status : PrefsStatus, var info : String? = null)
data class Prefs(val values : Map<String, String>, val metadata : Map<PrefsMetadataKey, PrefMetadata>) data class Prefs(val values : Map<String, String>, var metadata : Map<PrefsMetadataKey, PrefMetadata>)
interface PrefsFormat { interface PrefsFormat {
fun savePreferences(file: File, prefs: Prefs) fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null)
fun loadPreferences(file: File) : Prefs fun loadPreferences(file: File, masterPassword: String? = null) : Prefs
} }
enum class PrefsStatus { enum class PrefsStatus(@DrawableRes val icon:Int) {
OK, OK(R.drawable.ic_meta_ok),
WARN, WARN(R.drawable.ic_meta_warning),
ERROR ERROR(R.drawable.ic_meta_error),
UNKNOWN(R.drawable.ic_meta_error),
DISABLED(R.drawable.ic_meta_error)
} }
class PrefFileNotFoundError(message: String) : Exception(message) class PrefFileNotFoundError(message: String) : Exception(message)

View file

@ -4,17 +4,10 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Handler
import android.os.SystemClock import android.os.SystemClock
import android.text.Spanned import android.text.Spanned
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import info.nightscout.androidaps.MainApp
import info.nightscout.androidaps.R import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.alertDialogs.AlertDialogHelper
object OKDialog { object OKDialog {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@ -23,11 +16,9 @@ object OKDialog {
fun show(context: Context, title: String, message: String, runnable: Runnable? = null) { fun show(context: Context, title: String, message: String, runnable: Runnable? = null) {
var notEmptytitle = title var notEmptytitle = title
if (notEmptytitle.isEmpty()) notEmptytitle = context.getString(R.string.message) if (notEmptytitle.isEmpty()) notEmptytitle = context.getString(R.string.message)
val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = notEmptytitle AlertDialogHelper.Builder(context)
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, notEmptytitle))
AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme))
.setCustomTitle(titleLayout)
.setMessage(message) .setMessage(message)
.setPositiveButton(context.getString(R.string.ok)) { dialog: DialogInterface, _: Int -> .setPositiveButton(context.getString(R.string.ok)) { dialog: DialogInterface, _: Int ->
dialog.dismiss() dialog.dismiss()
@ -38,23 +29,15 @@ object OKDialog {
.setCanceledOnTouchOutside(false) .setCanceledOnTouchOutside(false)
} }
fun runOnUiThread(theRunnable: Runnable?) {
@Suppress("DEPRECATION")
val mainHandler = Handler(MainApp.instance().applicationContext.mainLooper)
theRunnable?.let { mainHandler.post(it) }
}
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun show(activity: Activity, title: String, message: Spanned, runnable: Runnable? = null) { fun show(activity: Activity, title: String, message: Spanned, runnable: Runnable? = null) {
var notEmptytitle = title var notEmptytitle = title
if (notEmptytitle.isEmpty()) notEmptytitle = activity.getString(R.string.message) if (notEmptytitle.isEmpty()) notEmptytitle = activity.getString(R.string.message)
val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = notEmptytitle AlertDialogHelper.Builder(activity)
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) .setCustomTitle(AlertDialogHelper.buildCustomTitle(activity, notEmptytitle))
AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme))
.setCustomTitle(titleLayout)
.setMessage(message) .setMessage(message)
.setPositiveButton(activity.getString(R.string.ok)) { dialog: DialogInterface, _: Int -> .setPositiveButton(activity.getString(R.string.ok)) { dialog: DialogInterface, _: Int ->
dialog.dismiss() dialog.dismiss()
@ -79,12 +62,9 @@ object OKDialog {
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun showConfirmation(activity: Activity, title: String, message: Spanned, ok: Runnable?, cancel: Runnable? = null) { fun showConfirmation(activity: Activity, title: String, message: Spanned, ok: Runnable?, cancel: Runnable? = null) {
val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) AlertDialogHelper.Builder(activity)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp)
AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme))
.setMessage(message) .setMessage(message)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(activity, title))
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int ->
dialog.dismiss() dialog.dismiss()
SystemClock.sleep(100) SystemClock.sleep(100)
@ -103,12 +83,9 @@ object OKDialog {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@JvmStatic @JvmStatic
fun showConfirmation(activity: Activity, title: String, message: String, ok: Runnable?, cancel: Runnable? = null) { fun showConfirmation(activity: Activity, title: String, message: String, ok: Runnable?, cancel: Runnable? = null) {
val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) AlertDialogHelper.Builder(activity)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp)
AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme))
.setMessage(message) .setMessage(message)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(activity, title))
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int ->
dialog.dismiss() dialog.dismiss()
SystemClock.sleep(100) SystemClock.sleep(100)
@ -133,12 +110,9 @@ object OKDialog {
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun showConfirmation(context: Context, title: String, message: Spanned, ok: Runnable?, cancel: Runnable? = null) { fun showConfirmation(context: Context, title: String, message: Spanned, ok: Runnable?, cancel: Runnable? = null) {
val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) AlertDialogHelper.Builder(context)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp)
AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme))
.setMessage(message) .setMessage(message)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title))
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int ->
dialog.dismiss() dialog.dismiss()
SystemClock.sleep(100) SystemClock.sleep(100)
@ -164,12 +138,9 @@ object OKDialog {
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun showConfirmation(context: Context, title: String, message: String, ok: Runnable?, cancel: Runnable? = null) { fun showConfirmation(context: Context, title: String, message: String, ok: Runnable?, cancel: Runnable? = null) {
val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) AlertDialogHelper.Builder(context)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp)
AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme))
.setMessage(message) .setMessage(message)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title))
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int ->
dialog.dismiss() dialog.dismiss()
SystemClock.sleep(100) SystemClock.sleep(100)
@ -188,12 +159,9 @@ object OKDialog {
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun showConfirmation(context: Context, title: String, message: String, ok: DialogInterface.OnClickListener?, cancel: DialogInterface.OnClickListener? = null) { fun showConfirmation(context: Context, title: String, message: String, ok: DialogInterface.OnClickListener?, cancel: DialogInterface.OnClickListener? = null) {
val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) AlertDialogHelper.Builder(context)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp)
AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme))
.setMessage(message) .setMessage(message)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title))
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int -> .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int ->
dialog.dismiss() dialog.dismiss()
SystemClock.sleep(100) SystemClock.sleep(100)

View file

@ -1,26 +1,94 @@
package info.nightscout.androidaps.utils; package info.nightscout.androidaps.utils;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.appcompat.view.ContextThemeWrapper;
import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.MainApp;
import info.nightscout.androidaps.plugins.bus.RxBus; import info.nightscout.androidaps.R;
import info.nightscout.androidaps.plugins.bus.RxBusWrapper; import info.nightscout.androidaps.plugins.bus.RxBusWrapper;
import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification; import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification;
import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; import info.nightscout.androidaps.plugins.general.overview.notifications.Notification;
public class ToastUtils { public class ToastUtils {
public static class Long {
public static void warnToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_warn, false);
}
public static void infoToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_info,false);
}
public static void okToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_check,false);
}
public static void errorToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_error,false);
}
}
public static void showToastInUiThread(final Context ctx, final int stringId) { public static void showToastInUiThread(final Context ctx, final int stringId) {
showToastInUiThread(ctx, MainApp.gs(stringId)); showToastInUiThread(ctx, MainApp.gs(stringId));
} }
public static void warnToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_warn, true);
}
public static void infoToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_info, true);
}
public static void okToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_check, true);
}
public static void errorToast(final Context ctx, final String string) {
graphicalToast(ctx, string, R.drawable.ic_toast_error, true);
}
public static void graphicalToast(final Context ctx, final String string, @DrawableRes int iconId) {
graphicalToast(ctx, string, iconId, true);
}
@SuppressLint("InflateParams")
public static void graphicalToast(final Context ctx, final String string, @DrawableRes int iconId, boolean isShort) {
Handler mainThread = new Handler(Looper.getMainLooper());
mainThread.post(() -> {
View toastRoot =LayoutInflater.from(new ContextThemeWrapper(ctx, R.style.AppTheme)).inflate(R.layout.toast, null);
TextView toastMessage = toastRoot.findViewById(android.R.id.message);
toastMessage.setText(string);
ImageView toastIcon = toastRoot.findViewById(android.R.id.icon);
toastIcon.setImageResource(iconId);
Toast toast = new Toast(ctx);
toast.setDuration(isShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG);
toast.setView(toastRoot);
toast.show();
});
}
public static void showToastInUiThread(final Context ctx, final String string) { public static void showToastInUiThread(final Context ctx, final String string) {
Handler mainThread = new Handler(Looper.getMainLooper()); Handler mainThread = new Handler(Looper.getMainLooper());
mainThread.post(() -> Toast.makeText(ctx, string, Toast.LENGTH_SHORT).show()); mainThread.post(() -> {
Toast.makeText(ctx, string, Toast.LENGTH_SHORT).show();
});
} }
public static void showToastInUiThread(final Context ctx, final RxBusWrapper rxBus, public static void showToastInUiThread(final Context ctx, final RxBusWrapper rxBus,

View file

@ -1,6 +1,8 @@
package info.nightscout.androidaps.utils package info.nightscout.androidaps.utils
import android.os.Handler
import android.view.View import android.view.View
import info.nightscout.androidaps.MainApp
/** /**
* Created by adrian on 2019-12-20. * Created by adrian on 2019-12-20.
@ -8,3 +10,7 @@ import android.view.View
fun Boolean.toVisibility() = if (this) View.VISIBLE else View.GONE fun Boolean.toVisibility() = if (this) View.VISIBLE else View.GONE
fun runOnUiThread(theRunnable: Runnable?) {
val mainHandler = Handler(MainApp.instance().applicationContext.mainLooper)
theRunnable?.let { mainHandler.post(it) }
}

View file

@ -0,0 +1,31 @@
package info.nightscout.androidaps.utils.alertDialogs
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import info.nightscout.androidaps.R
object AlertDialogHelper {
@Suppress("FunctionName")
fun Builder(context: Context, @StyleRes themeResId: Int = R.style.AppTheme) =
AlertDialog.Builder(ContextThemeWrapper(context, themeResId))
fun buildCustomTitle(context: Context, title: String,
@DrawableRes iconResource: Int = R.drawable.ic_check_while_48dp,
@StyleRes themeResId: Int = R.style.AppTheme,
@LayoutRes layoutResource: Int = R.layout.dialog_alert_custom): View? {
val titleLayout = LayoutInflater.from(ContextThemeWrapper(context, themeResId)).inflate(layoutResource, null)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(iconResource)
return titleLayout
}
}

View file

@ -0,0 +1,140 @@
package info.nightscout.androidaps.utils.alertDialogs
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.graphics.Color
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.webkit.WebView
import android.widget.Button
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.appcompat.view.ContextThemeWrapper
import info.nightscout.androidaps.R
import info.nightscout.androidaps.plugins.general.maintenance.formats.Prefs
import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsStatus
import info.nightscout.androidaps.utils.ToastUtils
import info.nightscout.androidaps.utils.runOnUiThread
import java.util.*
object PrefImportSummaryDialog {
@SuppressLint("InflateParams")
@JvmStatic
@JvmOverloads
fun showSummary(context: Context, importOk: Boolean, importPossible: Boolean, prefs: Prefs, ok: (() -> Unit)?, cancel: (() -> Unit)? = null) {
@StyleRes val theme: Int = if (importOk) R.style.AppTheme else {
if (importPossible) R.style.AppThemeWarningDialog else R.style.AppThemeErrorDialog
}
@StringRes val messageRes: Int = if (importOk) R.string.check_preferences_before_import else {
if (importPossible) R.string.check_preferences_dangerous_import else R.string.check_preferences_cannot_import
}
@DrawableRes val headerIcon: Int = if (importOk) R.drawable.ic_header_import else {
if (importPossible) R.drawable.ic_header_warning else R.drawable.ic_header_error
}
val themedCtx = ContextThemeWrapper(context, theme)
val innerLayout = LayoutInflater.from(themedCtx).inflate(R.layout.dialog_alert_import_summary, null)
val table = (innerLayout.findViewById<View>(R.id.summary_table) as TableLayout)
val detailsBtn = (innerLayout.findViewById<View>(R.id.summary_details_btn) as Button)
var idx = 0
val details = LinkedList<String>()
for ((metaKey, metaEntry) in prefs.metadata) {
val rowLayout = LayoutInflater.from(themedCtx).inflate(R.layout.import_summary_item, null)
val label = (rowLayout.findViewById<View>(R.id.summary_text) as TextView)
label.setText(metaKey.formatForDisplay(context, metaEntry.value))
(rowLayout.findViewById<View>(R.id.summary_icon) as ImageView).setImageResource(metaKey.icon)
(rowLayout.findViewById<View>(R.id.status_icon) as ImageView).setImageResource(metaEntry.status.icon)
if (metaEntry.status == PrefsStatus.WARN) label.setTextColor(themedCtx.getColor(R.color.metadataTextWarning))
else if (metaEntry.status == PrefsStatus.ERROR) label.setTextColor(themedCtx.getColor(R.color.metadataTextError))
if (metaEntry.info != null) {
details.add("<b>${context.getString(metaKey.label)}</b>: ${metaEntry.value}<br/><i style=\"color:silver\">${metaEntry.info}</i>")
rowLayout.isClickable = true
rowLayout.setOnClickListener {
val msg = "[${context.getString(metaKey.label)}] ${metaEntry.info}"
when (metaEntry.status) {
PrefsStatus.WARN -> ToastUtils.Long.warnToast(context, msg)
PrefsStatus.ERROR -> ToastUtils.Long.errorToast(context, msg)
else -> ToastUtils.Long.infoToast(context, msg)
}
}
} else {
rowLayout.isClickable = true
rowLayout.setOnClickListener { ToastUtils.Long.infoToast(context, context.getString(metaKey.label)) }
}
table.addView(rowLayout, idx++)
}
if (details.size > 0) {
detailsBtn.visibility = View.VISIBLE
detailsBtn.setOnClickListener {
val detailsLayout = LayoutInflater.from(context).inflate(R.layout.import_summary_details, null)
val wview = detailsLayout.findViewById<View>(R.id.details_webview) as WebView
wview.loadData("<!doctype html><html><head><meta charset=\"utf-8\"><style>body { color: white; }</style></head><body>" + details.joinToString("<hr>"), "text/html; charset=utf-8", "utf-8");
wview.setBackgroundColor(Color.TRANSPARENT);
wview.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
AlertDialogHelper.Builder(context, R.style.AppTheme)
.setCustomTitle(AlertDialogHelper.buildCustomTitle(
context,
context.getString(R.string.check_preferences_details_title),
R.drawable.ic_header_log,
R.style.AppTheme))
.setView(detailsLayout)
.setPositiveButton(android.R.string.ok) { dialogInner: DialogInterface, _: Int ->
dialogInner.dismiss()
}
.show()
}
}
val builder = AlertDialogHelper.Builder(context, theme)
.setMessage(context.getString(messageRes))
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(R.string.nav_import), headerIcon, theme))
.setView(innerLayout)
.setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (cancel != null) {
runOnUiThread(Runnable {
cancel()
})
}
}
if (importPossible) {
builder.setPositiveButton(
if (importOk) R.string.check_preferences_import_btn else R.string.check_preferences_import_anyway_btn
) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (ok != null) {
runOnUiThread(Runnable {
ok()
})
}
}
}
val dialog = builder.show()
dialog.setCanceledOnTouchOutside(false)
}
}

View file

@ -0,0 +1,51 @@
package info.nightscout.androidaps.utils.alertDialogs
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.os.SystemClock
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.annotation.DrawableRes
import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.runOnUiThread
object TwoMessagesAlertDialog {
@SuppressLint("InflateParams")
@JvmStatic
@JvmOverloads
fun showAlert(context: Context, title: String, message: String, secondMessage: String, ok: (() -> Unit)?, cancel: (() -> Unit)? = null, @DrawableRes icon: Int? = null) {
val secondMessageLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_two_messages, null)
(secondMessageLayout.findViewById<View>(R.id.password_prompt_title) as TextView).text = secondMessage
val dialog = AlertDialogHelper.Builder(context)
.setMessage(message)
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title, icon
?: R.drawable.ic_check_while_48dp))
.setView(secondMessageLayout)
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (ok != null) {
runOnUiThread(Runnable {
ok()
})
}
}
.setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (cancel != null) {
runOnUiThread(Runnable {
cancel()
})
}
}
.show()
dialog.setCanceledOnTouchOutside(false)
}
}

View file

@ -0,0 +1,49 @@
package info.nightscout.androidaps.utils.alertDialogs
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.os.SystemClock
import androidx.annotation.StringRes
import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.runOnUiThread
// if you need error dialog - duplicate to ErrorDialog and make it and use: AppThemeErrorDialog & R.drawable.ic_header_error instead
object WarningDialog {
@SuppressLint("InflateParams")
@JvmStatic
@JvmOverloads
fun showWarning(context: Context, title: String, message: String, @StringRes positiveButton: Int = -1, ok: (() -> Unit)? = null, cancel: (() -> Unit)? = null) {
val builder = AlertDialogHelper.Builder(context, R.style.AppThemeWarningDialog)
.setMessage(message)
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title, R.drawable.ic_header_warning, R.style.AppThemeWarningDialog))
.setNegativeButton(R.string.dismiss) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (cancel != null) {
runOnUiThread(Runnable {
cancel()
})
}
}
if (positiveButton != -1) {
builder.setPositiveButton(positiveButton) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (ok != null) {
runOnUiThread(Runnable {
ok()
})
}
}
}
val dialog = builder.show()
dialog.setCanceledOnTouchOutside(true)
}
}

View file

@ -1,20 +1,16 @@
package info.nightscout.androidaps.utils.protection package info.nightscout.androidaps.utils.protection
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.FragmentActivity
import info.nightscout.androidaps.R import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.CryptoUtil import info.nightscout.androidaps.utils.CryptoUtil
import info.nightscout.androidaps.utils.ToastUtils import info.nightscout.androidaps.utils.ToastUtils
import info.nightscout.androidaps.utils.alertDialogs.AlertDialogHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SP
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -26,41 +22,36 @@ val AUTOFILL_HINT_NEW_PASSWORD = "newPassword"
class PasswordCheck @Inject constructor(val sp: SP) { class PasswordCheck @Inject constructor(val sp: SP) {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
fun queryPassword(activity: FragmentActivity, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)?, cancel: (()->Unit)? = null, fail: (()->Unit)? = null) { fun queryPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)?, cancel: (()->Unit)? = null, fail: (()->Unit)? = null) {
val password = sp.getString(preference, "") val password = sp.getString(preference, "")
if (password == "") { if (password == "") {
ok?.invoke("") ok?.invoke("")
return return
} }
val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = activity.getString(labelId) val alertDialogBuilder = AlertDialogHelper.Builder(context)
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_key_48dp)
val promptsView = LayoutInflater.from(activity).inflate(R.layout.passwordprompt, null)
val alertDialogBuilder = AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme))
alertDialogBuilder.setView(promptsView) alertDialogBuilder.setView(promptsView)
val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val autoFillHintPasswordKind = activity.getString(preference) val autoFillHintPasswordKind = context.getString(preference)
userInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD, "aaps_${autoFillHintPasswordKind}") userInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD, "aaps_${autoFillHintPasswordKind}")
userInput.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES userInput.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
} }
alertDialogBuilder alertDialogBuilder
.setCancelable(false) .setCancelable(false)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key))
.setPositiveButton(activity.getString(R.string.ok)) { _, _ -> .setPositiveButton(context.getString(R.string.ok)) { _, _ ->
val enteredPassword = userInput.text.toString() val enteredPassword = userInput.text.toString()
if (CryptoUtil.checkPassword(enteredPassword, password)) ok?.invoke(enteredPassword) if (CryptoUtil.checkPassword(enteredPassword, password)) ok?.invoke(enteredPassword)
else { else {
ToastUtils.showToastInUiThread(activity, activity.getString(R.string.wrongpassword)) ToastUtils.errorToast(context, context.getString(R.string.wrongpassword))
fail?.invoke() fail?.invoke()
} }
} }
.setNegativeButton(activity.getString(R.string.cancel) .setNegativeButton(context.getString(R.string.cancel)
) { dialog, _ -> ) { dialog, _ ->
cancel?.invoke() cancel?.invoke()
dialog.cancel() dialog.cancel()
@ -72,12 +63,7 @@ class PasswordCheck @Inject constructor(val sp: SP) {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
fun setPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)? = null, cancel: (()->Unit)? = null, clear: (()->Unit)? = null) { fun setPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)? = null, cancel: (()->Unit)? = null, clear: (()->Unit)? = null) {
val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null) val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null)
val alertDialogBuilder = AlertDialogHelper.Builder(context)
val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = context.getText(labelId)
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_key_48dp)
val alertDialogBuilder = AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme))
alertDialogBuilder.setView(promptsView) alertDialogBuilder.setView(promptsView)
val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText
@ -90,20 +76,20 @@ class PasswordCheck @Inject constructor(val sp: SP) {
alertDialogBuilder alertDialogBuilder
.setCancelable(false) .setCancelable(false)
.setCustomTitle(titleLayout) .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key))
.setPositiveButton(context.getString(R.string.ok)) { _, _ -> .setPositiveButton(context.getString(R.string.ok)) { _, _ ->
val enteredPassword = userInput.text.toString() val enteredPassword = userInput.text.toString()
if (enteredPassword.isNotEmpty()) { if (enteredPassword.isNotEmpty()) {
sp.putString(preference, CryptoUtil.hashPassword(enteredPassword)) sp.putString(preference, CryptoUtil.hashPassword(enteredPassword))
ToastUtils.showToastInUiThread(context, context.getString(R.string.password_set)) ToastUtils.okToast(context, context.getString(R.string.password_set))
ok?.invoke(enteredPassword) ok?.invoke(enteredPassword)
} else { } else {
if (sp.contains(preference)) { if (sp.contains(preference)) {
sp.remove(preference) sp.remove(preference)
ToastUtils.showToastInUiThread(context, context.getString(R.string.password_cleared)) ToastUtils.graphicalToast(context, context.getString(R.string.password_cleared), R.drawable.ic_toast_delete_confirm)
clear?.invoke() clear?.invoke()
} else { } else {
ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed)) ToastUtils.warnToast(context, context.getString(R.string.password_not_changed))
cancel?.invoke() cancel?.invoke()
} }
} }
@ -111,7 +97,7 @@ class PasswordCheck @Inject constructor(val sp: SP) {
} }
.setNegativeButton(context.getString(R.string.cancel) .setNegativeButton(context.getString(R.string.cancel)
) { dialog, _ -> ) { dialog, _ ->
ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed)) ToastUtils.infoToast(context, context.getString(R.string.password_not_changed))
cancel?.invoke() cancel?.invoke()
dialog.cancel() dialog.cancel()
} }

View file

@ -0,0 +1,17 @@
package info.nightscout.androidaps.utils.storage
import java.io.File
import javax.inject.Singleton
@Singleton
class FileStorage : Storage {
override fun getFileContents(file: File): String {
return file.readText()
}
override fun putFileContents(file: File, contents: String) {
file.writeText(contents)
}
}

View file

@ -0,0 +1,11 @@
package info.nightscout.androidaps.utils.storage
import java.io.File
// This may seems unnecessary abstraction - but it will simplify testing
interface Storage {
fun getFileContents(file: File) : String
fun putFileContents(file: File, contents: String)
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="PrivateResource"
android:insetLeft="16dp"
android:insetTop="16dp"
android:insetRight="16dp"
android:insetBottom="16dp">
<shape
android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="@color/background_floating_material_dark" />
<stroke android:color="@color/errorAlertBackground" android:width="3dp" />
</shape></inset>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="PrivateResource"
android:insetLeft="16dp"
android:insetTop="16dp"
android:insetRight="16dp"
android:insetBottom="16dp">
<shape
android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="@color/background_floating_material_dark" />
<stroke android:color="@color/warningAlertBackground" android:width="3dp" />
</shape></inset>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.8"
android:scaleY="0.8">
<path
android:fillColor="#000"
android:pathData="M8.27,3L3,8.27V15.73L8.27,21H15.73C17.5,19.24 21,15.73 21,15.73V8.27L15.73,3M9.1,5H14.9L19,9.1V14.9L14.9,19H9.1L5,14.9V9.1M9.12,7.71L7.71,9.12L10.59,12L7.71,14.88L9.12,16.29L12,13.41L14.88,16.29L16.29,14.88L13.41,12L16.29,9.12L14.88,7.71L12,10.59" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="#000"
android:pathData="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="#000"
android:pathData="M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="#FF000000"
android:pathData="M22 18V22H18V19H15V16H12L9.74 13.74C9.19 13.91 8.61 14 8 14A6 6 0 0 1 2 8A6 6 0 0 1 8 2A6 6 0 0 1 14 8C14 8.61 13.91 9.19 13.74 9.74L22 18M7 5A2 2 0 0 0 5 7A2 2 0 0 0 7 9A2 2 0 0 0 9 7A2 2 0 0 0 7 5Z" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.8"
android:scaleY="0.8">
<path
android:fillColor="#000"
android:pathData="M15,20A1,1 0 0,0 16,19V4H8A1,1 0 0,0 7,5V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H20V5A1,1 0 0,0 19,4A1,1 0 0,0 18,5V9L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H13A2,2 0 0,0 15,20M9,6H14V8H9V6M9,10H14V12H9V10M9,14H14V16H9V14Z" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.75"
android:scaleY="0.75">
<path
android:fillColor="#000"
android:pathData="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" />
</group>
</vector>

View file

@ -1,17 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
>
<group
android:scaleX="0.66"
android:scaleY="0.66"
android:pivotX="12"
android:pivotY="12">
<path
android:pathData="M22 18V22H18V19H15V16H12L9.74 13.74C9.19 13.91 8.61 14 8 14A6 6 0 0 1 2 8A6 6 0 0 1 8 2A6 6 0 0 1 14 8C14 8.61 13.91 9.19 13.74 9.74L22 18M7 5A2 2 0 0 0 5 7A2 2 0 0 0 7 9A2 2 0 0 0 9 7A2 2 0 0 0 7 5Z"
android:fillColor="#FF000000" />
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M15,13H16.5V15.82L18.94,17.23L18.19,18.53L15,16.69V13M19,8H5V19H9.67C9.24,18.09 9,17.07 9,16A7,7 0 0,1 16,9C17.07,9 18.09,9.24 19,9.67V8M5,21C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1H18V3H19A2,2 0 0,1 21,5V11.1C22.24,12.36 23,14.09 23,16A7,7 0 0,1 16,23C14.09,23 12.36,22.24 11.1,21H5M16,11.15A4.85,4.85 0 0,0 11.15,16C11.15,18.68 13.32,20.85 16,20.85A4.85,4.85 0 0,0 20.85,16C20.85,13.32 18.68,11.15 16,11.15Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21M14.8,11V9.5C14.8,8.1 13.4,7 12,7C10.6,7 9.2,8.1 9.2,9.5V11C8.6,11 8,11.6 8,12.2V15.7C8,16.4 8.6,17 9.2,17H14.7C15.4,17 16,16.4 16,15.8V12.3C16,11.6 15.4,11 14.8,11M13.5,11H10.5V9.5C10.5,8.7 11.2,8.2 12,8.2C12.8,8.2 13.5,8.7 13.5,9.5V11Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/metadataTextError"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,10.84 21.79,9.69 21.39,8.61L19.79,10.21C19.93,10.8 20,11.4 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.6,4 13.2,4.07 13.79,4.21L15.4,2.6C14.31,2.21 13.16,2 12,2M19,2L15,6V7.5L12.45,10.05C12.3,10 12.15,10 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12C14,11.85 14,11.7 13.95,11.55L16.5,9H18L22,5H19V2M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12H16A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8V6Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M6 2C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H12V20H6V4H13V9H18V12H20V8L14 2M18 14C17.87 14 17.76 14.09 17.74 14.21L17.55 15.53C17.25 15.66 16.96 15.82 16.7 16L15.46 15.5C15.35 15.5 15.22 15.5 15.15 15.63L14.15 17.36C14.09 17.47 14.11 17.6 14.21 17.68L15.27 18.5C15.25 18.67 15.24 18.83 15.24 19C15.24 19.17 15.25 19.33 15.27 19.5L14.21 20.32C14.12 20.4 14.09 20.53 14.15 20.64L15.15 22.37C15.21 22.5 15.34 22.5 15.46 22.5L16.7 22C16.96 22.18 17.24 22.35 17.55 22.47L17.74 23.79C17.76 23.91 17.86 24 18 24H20C20.11 24 20.22 23.91 20.24 23.79L20.43 22.47C20.73 22.34 21 22.18 21.27 22L22.5 22.5C22.63 22.5 22.76 22.5 22.83 22.37L23.83 20.64C23.89 20.53 23.86 20.4 23.77 20.32L22.7 19.5C22.72 19.33 22.74 19.17 22.74 19C22.74 18.83 22.73 18.67 22.7 18.5L23.76 17.68C23.85 17.6 23.88 17.47 23.82 17.36L22.82 15.63C22.76 15.5 22.63 15.5 22.5 15.5L21.27 16C21 15.82 20.73 15.65 20.42 15.53L20.23 14.21C20.22 14.09 20.11 14 20 14M19 17.5C19.83 17.5 20.5 18.17 20.5 19C20.5 19.83 19.83 20.5 19 20.5C18.16 20.5 17.5 19.83 17.5 19C17.5 18.17 18.17 17.5 19 17.5Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13 7H11V9H13V7M13 11H11V17H13V11M17 1H7C5.9 1 5 1.9 5 3V21C5 22.1 5.9 23 7 23H17C18.1 23 19 22.1 19 21V3C19 1.9 18.1 1 17 1M17 19H7V5H17V19Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M22,3H2C0.91,3.04 0.04,3.91 0,5V19C0.04,20.09 0.91,20.96 2,21H22C23.09,20.96 23.96,20.09 24,19V5C23.96,3.91 23.09,3.04 22,3M22,19H2V5H22V19M14,17V15.75C14,14.09 10.66,13.25 9,13.25C7.34,13.25 4,14.09 4,15.75V17H14M9,7A2.5,2.5 0 0,0 6.5,9.5A2.5,2.5 0 0,0 9,12A2.5,2.5 0 0,0 11.5,9.5A2.5,2.5 0 0,0 9,7M14,7V8H20V7H14M14,9V10H20V9H14M14,11V12H18V11H14" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="@color/toastOk"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/metadataTextWarning"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/toastOk"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/toastInfo"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8.46,11.88L9.87,10.47L12,12.59L14.12,10.47L15.53,11.88L13.41,14L15.53,16.12L14.12,17.53L12,15.41L9.88,17.53L8.47,16.12L10.59,14L8.46,11.88M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/toastError"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/toastInfo"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/toastWarn"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle"
tools:ignore="PrivateResource">
<corners android:radius="18dp" />
<solid android:color="@color/background_floating_material_dark" />
<stroke
android:width="3dp"
android:color="@color/toastBorder" />
</shape>

View file

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Loading this view directly, without proper Theme, will likely result in crash due to lack of ?dialog... attribute definitions
Please use AlertDialogHelper or wrap inflater context with ContextThemeWrapper(context, R.style.AppTheme)
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -8,14 +12,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:background="@color/dialog_title_background" android:background="?dialogTitleBackground"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="5dp"> android:padding="5dp">
<ImageView <ImageView
android:id="@+id/alertdialog_icon" android:id="@+id/alertdialog_icon"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
android:tint="?dialogTitleIconTint" />
<TextView <TextView
android:id="@+id/alertdialog_title" android:id="@+id/alertdialog_title"
@ -23,10 +28,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginLeft="10dp" android:layout_marginLeft="2dp"
android:layout_marginRight="10dp" android:layout_marginRight="50dp"
android:layout_toEndOf="@id/alertdialog_icon"
android:textAlignment="center" android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceLarge" /> android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?dialogTitleColor" />
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout
@ -36,5 +44,4 @@
android:orientation="horizontal" android:orientation="horizontal"
android:padding="5dp" /> android:padding="5dp" />
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="10dp">
<TableLayout
android:id="@+id/summary_table"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:stretchColumns="2" />
<Button
android:id="@+id/summary_details_btn"
style="?android:attr/buttonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="3dp"
android:text="@string/check_preferences_details_btn"
android:textColor="@color/colorTreatmentButton"
android:visibility="gone" />
</LinearLayout>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/password_prompt_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/password_preferences_decrypt_prompt"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/colorAccent" />
</LinearLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<WebView
android:id="@+id/details_webview"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="@android:color/transparent" />
</LinearLayout>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<TableRow xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent">
<ImageView
android:id="@+id/status_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="4dp"
android:src="@drawable/ic_toast_check" />
<ImageView
android:id="@+id/summary_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginTop="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="4dp"
android:src="@drawable/ic_meta_format"
android:tint="#ffffff" />
<TextView
android:id="@+id/summary_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:layout_weight="1"
android:text="Sample text here"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:ignore="HardcodedText" />
</TableRow>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/toast_border_ok">
<ImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:src="@drawable/ic_toast_check"
tools:ignore="RtlHardcoded" />
<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="6dp"
android:layout_marginTop="4dp"
android:layout_marginRight="18dp"
android:layout_marginBottom="4dp"
android:text="Toast goes here..."
android:textColor="@color/toastBase"
android:textSize="18sp"
tools:ignore="HardcodedText,RtlHardcoded" />
</LinearLayout>

View file

@ -0,0 +1,7 @@
<resources>
<attr name="dialogTitleBackground" format="reference" />
<attr name="dialogTitleColor" format="reference" />
<attr name="dialogTitleIconTint" format="reference" />
</resources>

View file

@ -47,6 +47,8 @@
<color name="dialog_title_background">#303030</color> <color name="dialog_title_background">#303030</color>
<color name="activity_title_background">#121212</color> <color name="activity_title_background">#121212</color>
<color name="dialog_title_color">#FFFFFF</color>
<color name="dialog_title_icon_tint">#FFFFFF</color>
<color name="cardColorBackground">#121212</color> <color name="cardColorBackground">#121212</color>
<color name="cardObjectiveText">#779ECB</color> <color name="cardObjectiveText">#779ECB</color>
@ -87,4 +89,21 @@
<color name="ribbonTextCritical">#FFFFFF</color> <color name="ribbonTextCritical">#FFFFFF</color>
<color name="splashBackground">#2E2E2E</color> <color name="splashBackground">#2E2E2E</color>
<color name="warningAlertBackground">#FFFB8C00</color>
<color name="warningAlertHeaderText">#FF000000</color>
<color name="errorAlertBackground">#FFFF5555</color>
<color name="errorAlertHeaderText">#FF000000</color>
<color name="toastBorder">#666666</color>
<color name="toastBase">#ffffff</color>
<color name="toastOk">#77dd77</color>
<color name="toastError">#ff0400</color>
<color name="toastWarn">#FF8C00</color>
<color name="toastInfo">#03A9F4</color>
<color name="metadataTextWarning">#FF8C00</color>
<color name="metadataTextError">#FF5555</color>
</resources> </resources>

View file

@ -14,6 +14,7 @@
<string name="custom_password">Custom password</string> <string name="custom_password">Custom password</string>
<string name="noprotection">No protection</string> <string name="noprotection">No protection</string>
<string name="protection">Protection</string> <string name="protection">Protection</string>
<string name="master_password_missing">Master password is not set!\n\nPlease set your Master password in Preferences (%1$s &#8594; %2$s)</string>
<string name="password_set">Password set!</string> <string name="password_set">Password set!</string>
<string name="password_not_set">Password not set</string> <string name="password_not_set">Password not set</string>

View file

@ -253,6 +253,51 @@
<string name="dismiss">DISMISS</string> <string name="dismiss">DISMISS</string>
<string name="language" translatable="false">Language</string> <string name="language" translatable="false">Language</string>
<string name="password_preferences_encrypt_prompt">You will be asked for master password, which will be used to encrypt exported preferences.</string>
<string name="password_preferences_decrypt_prompt">You will be asked for master password, which is needed to decrypt imported preferences.</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="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_dangerous_import">Preferences should not be imported!</string>
<string name="check_preferences_details_btn">Explain import issues…</string>
<string name="check_preferences_details_title">Import issues details</string>
<string name="check_preferences_import_btn">Import</string>
<string name="check_preferences_import_anyway_btn">Import anyway (DANGEROUS!)</string>
<string name="metadata_warning_different_flavour">Preferences were created with different variant of AAPS (%1$s) while you have: %2$s.\n\nSome settings may be missing or invalid - after importing please check and update your preferences.</string>
<string name="metadata_warning_different_device">Preferences were created on a different device. It is OK if you are importing from older/different phone, but make sure imported preferences are correct!</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_date_format">Invalid date-time format!</string>
<string name="metadata_label_format">File format</string>
<string name="metadata_label_created_at">Created at</string>
<string name="metadata_label_aaps_version">AAPS Version</string>
<string name="metadata_label_aaps_flavour">Build Variant</string>
<string name="metadata_label_device_name">Exporting device name</string>
<string name="metadata_label_device_model">Exporting device model</string>
<string name="metadata_label_encryption">File encryption</string>
<string name="metadata_format_old">Old export format</string>
<string name="metadata_format_new">New encrypted format</string>
<string name="metadata_format_debug">New debug format (unencrypted)</string>
<string name="metadata_format_other">Unknown export format</string>
<string name="prefdecrypt_settings_tampered">Settings file tampered</string>
<string name="prefdecrypt_settings_secure">Settings file is secure</string>
<string name="prefdecrypt_settings_unencrypted">Using not secure, unencrypted settings format</string>
<string name="prefdecrypt_wrong_json">JSON format error, missing required field (format, content, metadata or security)</string>
<string name="prefdecrypt_wrong_password">Decryption error, the given password cannot decrypt the file</string>
<string name="prefdecrypt_issue_missing_file_hash">File checksum (hash) missing, cannot verify the authenticity of settings!</string>
<string name="prefdecrypt_issue_modified">File was modified after export!</string>
<string name="prefdecrypt_issue_parsing">Decryption error, parsing preferences failed!</string>
<string name="prefdecrypt_issue_wrong_pass">Decryption error, the provided password is invalid or settings file was modified! It may happen that the imported file was exported with a different Master password.</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="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>
@ -1142,6 +1187,8 @@
<string name="error_adding_treatment_title">Treatment data incomplete</string> <string name="error_adding_treatment_title">Treatment data incomplete</string>
<string name="maintenance_settings">Maintenance Settings</string> <string name="maintenance_settings">Maintenance Settings</string>
<string name="maintenance_email">Email recipient</string> <string name="maintenance_email">Email recipient</string>
<string name="key_maintenance_encrypt_exported_prefs">maintenance_encrypt_exported_prefs</string>
<string name="maintenance_encrypt_exported_prefs">Encrypt exported settings</string>
<string name="key_maintenance_logs_email" translatable="false">maintenance_logs_email</string> <string name="key_maintenance_logs_email" translatable="false">maintenance_logs_email</string>
<string name="key_maintenance_logs_amount" translatable="false">maintenance_logs_amount</string> <string name="key_maintenance_logs_amount" translatable="false">maintenance_logs_amount</string>
<string name="key_logshipper_amount" translatable="false">logshipper_amount</string> <string name="key_logshipper_amount" translatable="false">logshipper_amount</string>

View file

@ -11,6 +11,9 @@
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="dialogTitleBackground">@color/dialog_title_background</item>
<item name="dialogTitleColor">@color/dialog_title_color</item>
<item name="dialogTitleIconTint">@color/dialog_title_icon_tint</item>
</style> </style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar"> <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
@ -67,4 +70,28 @@
<item name="android:textColor">#ff0000</item> <item name="android:textColor">#ff0000</item>
</style> </style>
<style name="AppThemeWarningDialog" parent="AppTheme">
<item name="alertDialogTheme">@style/AppThemeWarningDialogTheme</item>
<item name="dialogTitleBackground">@color/warningAlertBackground</item>
<item name="dialogTitleColor">@color/warningAlertHeaderText</item>
<item name="dialogTitleIconTint">@color/warningAlertHeaderText</item>
</style>
<style name="AppThemeWarningDialogTheme" parent="Theme.AppCompat.Dialog.MinWidth">
<item name="android:windowBackground">@drawable/alert_border_warning</item>
<item name="colorAccent">@color/warningAlertBackground</item>
</style>
<style name="AppThemeErrorDialog" parent="AppTheme">
<item name="alertDialogTheme">@style/AppThemeErrorDialogTheme</item>
<item name="dialogTitleBackground">@color/errorAlertBackground</item>
<item name="dialogTitleColor">@color/errorAlertHeaderText</item>
<item name="dialogTitleIconTint">@color/errorAlertHeaderText</item>
</style>
<style name="AppThemeErrorDialogTheme" parent="Theme.AppCompat.Dialog.MinWidth">
<item name="android:windowBackground">@drawable/alert_border_error</item>
<item name="colorAccent">@color/errorAlertBackground</item>
</style>
</resources> </resources>

View file

@ -26,6 +26,10 @@
validate:minNumber="1" validate:minNumber="1"
validate:testType="numericRange"/> validate:testType="numericRange"/>
<SwitchPreference
android:defaultValue="true"
android:key="@string/key_maintenance_encrypt_exported_prefs"
android:title="@string/maintenance_encrypt_exported_prefs" />
</PreferenceCategory> </PreferenceCategory>

View file

@ -0,0 +1,65 @@
package info.nightscout.androidaps.plugins.general.maintenance
import info.nightscout.androidaps.MainApp
import info.nightscout.androidaps.TestBase
import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat
import info.nightscout.androidaps.plugins.general.maintenance.formats.Prefs
import info.nightscout.androidaps.testing.mockers.AAPSMocker
import info.nightscout.androidaps.testing.utils.SingleStringStorage
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import org.hamcrest.CoreMatchers
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import java.io.File
@RunWith(PowerMockRunner::class)
@PrepareForTest(AAPSMocker::class, MainApp::class, File::class)
class ClassicPrefsFormatTest : TestBase() {
@Mock lateinit var resourceHelper: ResourceHelper
@Mock lateinit var sp: SP
@Before
fun mock() {
AAPSMocker.prepareMock()
AAPSMocker.resetMockedSharedPrefs()
}
@Test
fun preferenceLoadingTest() {
val test = "key1::val1\nkeyB::valB"
val classicFormat = ClassicPrefsFormat(resourceHelper, SingleStringStorage(test))
val prefs = classicFormat.loadPreferences(AAPSMocker.getMockedFile(), "")
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2))
Assert.assertThat(prefs.values["key1"], CoreMatchers.`is`("val1"))
Assert.assertThat(prefs.values["keyB"], CoreMatchers.`is`("valB"))
Assert.assertNull(prefs.values["key3"])
}
@Test
fun preferenceSavingTest() {
val storage = SingleStringStorage("")
val classicFormat = ClassicPrefsFormat(resourceHelper, storage)
val prefs = Prefs(
mapOf(
"key1" to "A",
"keyB" to "2"
),
mapOf()
)
classicFormat.savePreferences(AAPSMocker.getMockedFile(), prefs)
}
}

View file

@ -0,0 +1,226 @@
package info.nightscout.androidaps.plugins.general.maintenance
import info.nightscout.androidaps.MainApp
import info.nightscout.androidaps.TestBase
import info.nightscout.androidaps.plugins.general.maintenance.formats.*
import info.nightscout.androidaps.testing.mockers.AAPSMocker
import info.nightscout.androidaps.testing.utils.SingleStringStorage
import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import org.hamcrest.CoreMatchers
import org.json.JSONException
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.powermock.core.classloader.annotations.PowerMockIgnore
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import java.io.File
@PowerMockIgnore("javax.crypto.*")
@RunWith(PowerMockRunner::class)
@PrepareForTest(AAPSMocker::class, MainApp::class, File::class, ResourceHelper::class)
class EncryptedPrefsFormatTest : TestBase() {
@Mock lateinit var resourceHelper: ResourceHelper
@Mock lateinit var sp: SP
@Before
fun mock() {
AAPSMocker.prepareMock()
AAPSMocker.resetMockedSharedPrefs()
Mockito.`when`(resourceHelper.gs(ArgumentMatchers.anyInt())).thenReturn("mock translation")
}
@Test
fun preferenceLoadingTest() {
val frozenPrefs = "{\n" +
" \"metadata\": {},\n" +
" \"security\": {\n" +
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
" \"algorithm\": \"v1\"\n" +
" },\n" +
" \"format\": \"aaps_encrypted\",\n" +
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
"}"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2))
Assert.assertThat(prefs.values["key1"], CoreMatchers.`is`("A"))
Assert.assertThat(prefs.values["keyB"], CoreMatchers.`is`("2"))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.OK))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.value, CoreMatchers.`is`(EncryptedPrefsFormat.FORMAT_KEY_ENC))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.OK))
}
@Test
fun preferenceSavingTest() {
val storage = SingleStringStorage("")
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefs = Prefs(
mapOf(
"key1" to "A",
"keyB" to "2"
),
mapOf(
PrefsMetadataKey.ENCRYPTION to PrefMetadata(EncryptedPrefsFormat.FORMAT_KEY_ENC, PrefsStatus.OK)
)
)
encryptedFormat.savePreferences(AAPSMocker.getMockedFile(), prefs, "sikret")
aapsLogger.debug(storage.contents)
}
@Test
fun importExportStabilityTest() {
val storage = SingleStringStorage("")
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefsIn = Prefs(
mapOf(
"testpref1" to "--1--",
"testpref2" to "another"
),
mapOf(
PrefsMetadataKey.ENCRYPTION to PrefMetadata(EncryptedPrefsFormat.FORMAT_KEY_ENC, PrefsStatus.OK)
)
)
encryptedFormat.savePreferences(AAPSMocker.getMockedFile(), prefsIn, "tajemnica")
val prefsOut = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "tajemnica")
Assert.assertThat(prefsOut.values.size, CoreMatchers.`is`(2))
Assert.assertThat(prefsOut.values["testpref1"], CoreMatchers.`is`("--1--"))
Assert.assertThat(prefsOut.values["testpref2"], CoreMatchers.`is`("another"))
Assert.assertThat(prefsOut.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.OK))
Assert.assertThat(prefsOut.metadata[PrefsMetadataKey.FILE_FORMAT]?.value, CoreMatchers.`is`(EncryptedPrefsFormat.FORMAT_KEY_ENC))
Assert.assertThat(prefsOut.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.OK))
}
@Test
fun wrongPasswordTest() {
val frozenPrefs = "{\n" +
" \"metadata\": {},\n" +
" \"security\": {\n" +
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
" \"algorithm\": \"v1\"\n" +
" },\n" +
" \"format\": \"aaps_encrypted\",\n" +
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
"}"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "it-is-NOT-right-secret")
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.OK))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.value, CoreMatchers.`is`(EncryptedPrefsFormat.FORMAT_KEY_ENC))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
}
@Test
fun tamperedMetadataTest() {
val frozenPrefs = "{\n" +
" \"metadata\": {" +
" \"created-by\":\"I am legit, trust me, no-one lies on internets!\"" +
" },\n" +
" \"security\": {\n" +
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
" \"algorithm\": \"v1\"\n" +
" },\n" +
" \"format\": \"aaps_encrypted\",\n" +
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
"}"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
// contents were not tampered and we can decrypt them
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2))
// but checksum fails on metadata, so overall security fails
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
}
@Test
fun tamperedContentsTest() {
val frozenPrefs = "{\n" +
" \"metadata\": {},\n" +
" \"security\": {\n" +
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760a\",\n" +
" \"algorithm\": \"v1\"\n" +
" },\n" +
" \"format\": \"aaps_encrypted\",\n" +
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
"}"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
}
@Test
fun missingFieldsTest() {
val frozenPrefs = "{\n" +
" \"format\": \"aaps_encrypted\",\n" +
" \"content\": \"lets get rid of metadata and security!\"\n" +
"}"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0))
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
}
@Test(expected = PrefFormatError::class)
fun garbageInputTest() {
val frozenPrefs = "whatever man, i duno care"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
}
@Test(expected = PrefFormatError::class)
fun unknownFormatTest() {
val frozenPrefs = "{\n" +
" \"metadata\": {},\n" +
" \"security\": {\n" +
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
" \"algorithm\": \"v1\"\n" +
" },\n" +
" \"format\": \"aaps_9000_new_format\",\n" +
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
"}"
val storage = SingleStringStorage(frozenPrefs)
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
}
}

View file

@ -0,0 +1,56 @@
package info.nightscout.androidaps.testing.mockers;
import android.content.Context;
import android.content.SharedPreferences;
import org.mockito.ArgumentMatchers;
import org.mockito.invocation.InvocationOnMock;
import org.powermock.api.mockito.PowerMockito;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import info.nightscout.androidaps.MainApp;
import info.nightscout.androidaps.testing.mocks.SharedPreferencesMock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
public class AAPSMocker {
private static final Map<String, SharedPreferences> mockedSharedPrefs = new HashMap<>();
public static void prepareMock() throws Exception {
Context mockedContext = mock(Context.class);
mockStatic(MainApp.class, InvocationOnMock::callRealMethod);
PowerMockito.when(mockedContext, "getSharedPreferences", ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()).thenAnswer(invocation -> {
final String key = invocation.getArgument(0);
if (mockedSharedPrefs.containsKey(key)) {
return mockedSharedPrefs.get(key);
} else {
SharedPreferencesMock newPrefs = new SharedPreferencesMock();
mockedSharedPrefs.put(key, newPrefs);
return newPrefs;
}
});
resetMockedSharedPrefs();
}
public static void resetMockedSharedPrefs() {
mockedSharedPrefs.clear();
}
public static File getMockedFile() {
File file = mock(File.class);
when(file.exists()).thenReturn(true);
when(file.canRead()).thenReturn(true);
when(file.canWrite()).thenReturn(true);
return file;
}
}

View file

@ -0,0 +1,158 @@
package info.nightscout.androidaps.testing.mocks;
import android.content.SharedPreferences;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class SharedPreferencesMock implements SharedPreferences {
private final EditorInternals editor = new EditorInternals();
class EditorInternals implements Editor {
Map<String, Object> innerMap = new HashMap<>();
@Override
public Editor putString(String k, @Nullable String v) {
innerMap.put(k, v);
return this;
}
@Override
public Editor putStringSet(String k, @Nullable Set<String> set) {
innerMap.put(k, set);
return this;
}
@Override
public Editor putInt(String k, int i) {
innerMap.put(k, i);
return this;
}
@Override
public Editor putLong(String k, long l) {
innerMap.put(k, l);
return this;
}
@Override
public Editor putFloat(String k, float v) {
innerMap.put(k, v);
return this;
}
@Override
public Editor putBoolean(String k, boolean b) {
innerMap.put(k, b);
return this;
}
@Override
public Editor remove(String k) {
innerMap.remove(k);
return this;
}
@Override
public Editor clear() {
innerMap.clear();
return this;
}
@Override
public boolean commit() {
return true;
}
@Override
public void apply() {
}
}
@Override
public Map<String, ?> getAll() {
return editor.innerMap;
}
@Nullable
@Override
public String getString(String k, @Nullable String s) {
if (editor.innerMap.containsKey(k)) {
return (String) editor.innerMap.get(k);
} else {
return s;
}
}
@Nullable
@Override
public Set<String> getStringSet(String k, @Nullable Set<String> set) {
if (editor.innerMap.containsKey(k)) {
return (Set<String>) editor.innerMap.get(k);
} else {
return set;
}
}
@Override
public int getInt(String k, int i) {
if (editor.innerMap.containsKey(k)) {
return (Integer) editor.innerMap.get(k);
} else {
return i;
}
}
@Override
public long getLong(String k, long l) {
if (editor.innerMap.containsKey(k)) {
return (Long) editor.innerMap.get(k);
} else {
return l;
}
}
@Override
public float getFloat(String k, float v) {
if (editor.innerMap.containsKey(k)) {
return (Float) editor.innerMap.get(k);
} else {
return v;
}
}
@Override
public boolean getBoolean(String k, boolean b) {
if (editor.innerMap.containsKey(k)) {
return (Boolean) editor.innerMap.get(k);
} else {
return b;
}
}
@Override
public boolean contains(String k) {
return editor.innerMap.containsKey(k);
}
@Override
public Editor edit() {
return editor;
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
}
}

View file

@ -0,0 +1,26 @@
package info.nightscout.androidaps.testing.utils
import info.nightscout.androidaps.utils.storage.Storage
import java.io.File
class SingleStringStorage : Storage {
var contents: String = ""
constructor(contents: String) {
this.contents = contents
}
override fun getFileContents(file: File): String {
return contents
}
override fun putFileContents(file: File, putContents: String) {
contents = putContents
}
override fun toString(): String {
return contents
}
}