Preferences Encryption:
- encrypted JSON format support - using master password & password prompt - refactored alerts
This commit is contained in:
parent
fde84207ab
commit
1c97f8a720
59 changed files with 2004 additions and 196 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,26 +1,73 @@
|
||||||
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)
|
||||||
class PrefIOError(message: String) : Exception(message)
|
class PrefIOError(message: String) : Exception(message)
|
||||||
class PrefFormatError(message: String) : Exception(message)
|
class PrefFormatError(message: String) : Exception(message)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
15
app/src/main/res/drawable/alert_border_error.xml
Normal file
15
app/src/main/res/drawable/alert_border_error.xml
Normal 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>
|
||||||
|
|
15
app/src/main/res/drawable/alert_border_warning.xml
Normal file
15
app/src/main/res/drawable/alert_border_warning.xml
Normal 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>
|
||||||
|
|
16
app/src/main/res/drawable/ic_header_error.xml
Normal file
16
app/src/main/res/drawable/ic_header_error.xml
Normal 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>
|
16
app/src/main/res/drawable/ic_header_export.xml
Normal file
16
app/src/main/res/drawable/ic_header_export.xml
Normal 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>
|
16
app/src/main/res/drawable/ic_header_import.xml
Normal file
16
app/src/main/res/drawable/ic_header_import.xml
Normal 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>
|
16
app/src/main/res/drawable/ic_header_key.xml
Normal file
16
app/src/main/res/drawable/ic_header_key.xml
Normal 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>
|
16
app/src/main/res/drawable/ic_header_log.xml
Normal file
16
app/src/main/res/drawable/ic_header_log.xml
Normal 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>
|
16
app/src/main/res/drawable/ic_header_warning.xml
Normal file
16
app/src/main/res/drawable/ic_header_warning.xml
Normal 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>
|
|
@ -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>
|
|
9
app/src/main/res/drawable/ic_meta_date.xml
Normal file
9
app/src/main/res/drawable/ic_meta_date.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_meta_encryption.xml
Normal file
9
app/src/main/res/drawable/ic_meta_encryption.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_meta_error.xml
Normal file
10
app/src/main/res/drawable/ic_meta_error.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_meta_flavour.xml
Normal file
9
app/src/main/res/drawable/ic_meta_flavour.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_meta_format.xml
Normal file
9
app/src/main/res/drawable/ic_meta_format.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_meta_model.xml
Normal file
9
app/src/main/res/drawable/ic_meta_model.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_meta_name.xml
Normal file
9
app/src/main/res/drawable/ic_meta_name.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_meta_ok.xml
Normal file
10
app/src/main/res/drawable/ic_meta_ok.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_meta_version.xml
Normal file
9
app/src/main/res/drawable/ic_meta_version.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_meta_warning.xml
Normal file
10
app/src/main/res/drawable/ic_meta_warning.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_toast_check.xml
Normal file
10
app/src/main/res/drawable/ic_toast_check.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_toast_delete_confirm.xml
Normal file
10
app/src/main/res/drawable/ic_toast_delete_confirm.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_toast_error.xml
Normal file
10
app/src/main/res/drawable/ic_toast_error.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_toast_info.xml
Normal file
10
app/src/main/res/drawable/ic_toast_info.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_toast_warn.xml
Normal file
10
app/src/main/res/drawable/ic_toast_warn.xml
Normal 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>
|
12
app/src/main/res/drawable/toast_border_ok.xml
Normal file
12
app/src/main/res/drawable/toast_border_ok.xml
Normal 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>
|
||||||
|
|
|
@ -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>
|
30
app/src/main/res/layout/dialog_alert_import_summary.xml
Normal file
30
app/src/main/res/layout/dialog_alert_import_summary.xml
Normal 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>
|
19
app/src/main/res/layout/dialog_alert_two_messages.xml
Normal file
19
app/src/main/res/layout/dialog_alert_two_messages.xml
Normal 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>
|
14
app/src/main/res/layout/import_summary_details.xml
Normal file
14
app/src/main/res/layout/import_summary_details.xml
Normal 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>
|
36
app/src/main/res/layout/import_summary_item.xml
Normal file
36
app/src/main/res/layout/import_summary_item.xml
Normal 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>
|
||||||
|
|
32
app/src/main/res/layout/toast.xml
Normal file
32
app/src/main/res/layout/toast.xml
Normal 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>
|
7
app/src/main/res/values/attrs.xml
Normal file
7
app/src/main/res/values/attrs.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<attr name="dialogTitleBackground" format="reference" />
|
||||||
|
<attr name="dialogTitleColor" format="reference" />
|
||||||
|
<attr name="dialogTitleIconTint" format="reference" />
|
||||||
|
|
||||||
|
</resources>
|
|
@ -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>
|
||||||
|
|
|
@ -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 → %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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue