Merge pull request #2545 from dlvoy/dagger3-encprefs

Preferences Encryption
This commit is contained in:
Milos Kozak 2020-04-01 23:31:29 +02:00 committed by GitHub
commit 64ee80b48b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2200 additions and 240 deletions

View file

@ -26,7 +26,6 @@ import info.nightscout.androidaps.plugins.aps.openAPSMA.DetermineBasalResultMA
import info.nightscout.androidaps.plugins.aps.openAPSMA.LoggerCallback
import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalAdapterSMBJS
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.ProfileFunction
import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctionImplementation
@ -36,6 +35,9 @@ import info.nightscout.androidaps.plugins.general.automation.actions.*
import info.nightscout.androidaps.plugins.general.automation.elements.*
import info.nightscout.androidaps.plugins.general.automation.triggers.*
import info.nightscout.androidaps.plugins.general.overview.graphData.GraphData
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.smsCommunicator.AuthRequest
import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensData
@ -45,7 +47,6 @@ import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobThread
import info.nightscout.androidaps.plugins.treatments.Treatment
import info.nightscout.androidaps.queue.CommandQueue
import info.nightscout.androidaps.queue.commands.*
import info.nightscout.androidaps.setupwizard.SWDefinition
import info.nightscout.androidaps.setupwizard.SWEventListener
import info.nightscout.androidaps.setupwizard.SWScreen
import info.nightscout.androidaps.setupwizard.elements.*
@ -54,6 +55,8 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper
import info.nightscout.androidaps.utils.resources.ResourceHelperImplementation
import info.nightscout.androidaps.utils.sharedPreferences.SP
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.QuickWizardEntry
import javax.inject.Singleton
@ -103,6 +106,12 @@ open class AppModule {
return plugins.toList().sortedBy { it.first }.map { it.second }
}
@Provides
@Singleton
fun provideStorage(): Storage {
return FileStorage()
}
@Module
interface AppBindings {
@ -249,6 +258,10 @@ open class AppModule {
@ContributesAndroidInjector fun graphDataInjector(): GraphData
@ContributesAndroidInjector fun importExportPrefsInjector(): ImportExportPrefs
@ContributesAndroidInjector fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat
@ContributesAndroidInjector fun classicPrefsFormatInjector(): ClassicPrefsFormat
@Binds fun bindContext(mainApp: MainApp): Context
@Binds fun bindInjector(mainApp: MainApp): HasAndroidInjector

View file

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

View file

@ -1,129 +0,0 @@
package info.nightscout.androidaps.plugins.general.maintenance;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Environment;
import androidx.preference.PreferenceManager;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import info.nightscout.androidaps.MainApp;
import info.nightscout.androidaps.R;
import info.nightscout.androidaps.events.EventAppExit;
import info.nightscout.androidaps.logging.L;
import info.nightscout.androidaps.logging.StacktraceLoggerWrapper;
import info.nightscout.androidaps.plugins.bus.RxBus;
import info.nightscout.androidaps.utils.OKDialog;
import info.nightscout.androidaps.utils.SP;
import info.nightscout.androidaps.utils.ToastUtils;
/**
* Created by mike on 03.07.2016.
*/
public class ImportExportPrefs {
private static Logger log = StacktraceLoggerWrapper.getLogger(L.CORE);
private static File path = new File(Environment.getExternalStorageDirectory().toString());
static public final File file = new File(path, MainApp.gs(R.string.app_name) + "Preferences");
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
static void verifyStoragePermissions(Fragment fragment) {
int permission = ContextCompat.checkSelfPermission(fragment.getContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
fragment.requestPermissions(PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
}
static void exportSharedPreferences(final Fragment f) {
exportSharedPreferences(f.getContext());
}
private static void exportSharedPreferences(final Context context) {
OKDialog.showConfirmation(context, MainApp.gs(R.string.maintenance), MainApp.gs(R.string.export_to) + " " + file + " ?", () -> {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
try {
FileWriter fw = new FileWriter(file);
PrintWriter pw = new PrintWriter(fw);
Map<String, ?> prefsMap = prefs.getAll();
for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {
pw.println(entry.getKey() + "::" + entry.getValue().toString());
}
pw.close();
fw.close();
ToastUtils.showToastInUiThread(context, MainApp.gs(R.string.exported));
} catch (FileNotFoundException e) {
ToastUtils.showToastInUiThread(context, MainApp.gs(R.string.filenotfound) + " " + file);
log.error("Unhandled exception", e);
} catch (IOException e) {
log.error("Unhandled exception", e);
}
});
}
static void importSharedPreferences(final Fragment fragment) {
importSharedPreferences(fragment.getContext());
}
public static void importSharedPreferences(final Context context) {
OKDialog.showConfirmation(context, MainApp.gs(R.string.maintenance), MainApp.gs(R.string.import_from) + " " + file + " ?", () -> {
String line;
String[] lineParts;
try {
SP.clear();
BufferedReader reader = new BufferedReader(new FileReader(file));
while ((line = reader.readLine()) != null) {
lineParts = line.split("::");
if (lineParts.length == 2) {
if (lineParts[1].equals("true") || lineParts[1].equals("false")) {
SP.putBoolean(lineParts[0], Boolean.parseBoolean(lineParts[1]));
} else {
SP.putString(lineParts[0], lineParts[1]);
}
}
}
reader.close();
SP.putBoolean(R.string.key_setupwizard_processed, true);
OKDialog.show(context, MainApp.gs(R.string.setting_imported), MainApp.gs(R.string.restartingapp), () -> {
log.debug("Exiting");
RxBus.Companion.getINSTANCE().send(new EventAppExit());
if (context instanceof Activity) {
((Activity) context).finish();
}
System.runFinalization();
System.exit(0);
});
} catch (FileNotFoundException e) {
ToastUtils.showToastInUiThread(context, MainApp.gs(R.string.filenotfound) + " " + file);
log.error("Unhandled exception", e);
} catch (IOException e) {
log.error("Unhandled exception", e);
}
});
}
}

View file

@ -0,0 +1,340 @@
package info.nightscout.androidaps.plugins.general.maintenance
import android.Manifest
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import info.nightscout.androidaps.BuildConfig
import info.nightscout.androidaps.R
import info.nightscout.androidaps.activities.PreferencesActivity
import info.nightscout.androidaps.events.EventAppExit
import info.nightscout.androidaps.logging.AAPSLogger
import info.nightscout.androidaps.logging.LTag
import info.nightscout.androidaps.plugins.bus.RxBusWrapper
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.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.sharedPreferences.SP
import org.joda.time.DateTime
import org.joda.time.Days
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
/**
* Created by mike on 03.07.2016.
*/
private const val REQUEST_EXTERNAL_STORAGE = 1
private val PERMISSIONS_STORAGE = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60
@Singleton
class ImportExportPrefs @Inject constructor(
private var log: AAPSLogger,
private val resourceHelper: ResourceHelper,
private val sp: SP,
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
private val path = File(Environment.getExternalStorageDirectory().toString())
private val file = File(path, resourceHelper.gs(R.string.app_name) + "Preferences")
private val encFile = File(path, resourceHelper.gs(R.string.app_name) + "Preferences.json")
fun prefsImportFile(): File {
return if (encFile.exists()) encFile else file
}
fun prefsFileExists(): Boolean {
return encFile.exists() || file.exists()
}
fun exportSharedPreferences(f: Fragment) {
f.activity?.let { exportSharedPreferences(it) }
}
fun verifyStoragePermissions(fragment: Fragment) {
fragment.context?.let {
val permission = ContextCompat.checkSelfPermission(it,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
fragment.requestPermissions(PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE)
}
}
}
private fun prepareMetadata(context: Context): Map<PrefsMetadataKey, PrefMetadata> {
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
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 {
val entries: MutableMap<String, String> = mutableMapOf()
for ((key, value) in sp.getAll()) {
entries[key] = value.toString()
}
val prefs = Prefs(entries, prepareMetadata(activity))
classicPrefsFormat.savePreferences(file, prefs)
encryptedPrefsFormat.savePreferences(encFile, prefs, password)
ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported))
} catch (e: FileNotFoundException) {
ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + encFile)
log.error(TAG, "Unhandled exception", e)
} catch (e: IOException) {
ToastUtils.errorToast(activity, e.message)
log.error(TAG, "Unhandled exception", e)
}
}
}
fun importSharedPreferences(fragment: Fragment) {
fragment.activity?.let { importSharedPreferences(it) }
}
fun importSharedPreferences(activity: Activity) {
val importFile = prefsImportFile()
askToConfirmImport(activity, importFile) { password ->
val format: PrefsFormat = if (encFile.exists()) encryptedPrefsFormat else classicPrefsFormat
try {
val prefs = format.loadPreferences(importFile, password)
prefs.metadata = checkMetadata(prefs.metadata)
// 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 {
// for impossible imports it should not be called
ToastUtils.errorToast(activity, "Cannot import preferences!")
}
})
} catch (e: PrefFileNotFoundError) {
ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + importFile)
log.error(TAG, "Unhandled exception", e)
} catch (e: PrefIOError) {
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()
meta[PrefsMetadataKey.AAPS_FLAVOUR]?.let { flavour ->
val flavourOfPrefs = flavour.value
if (flavour.value != BuildConfig.FLAVOR) {
flavour.status = PrefsStatus.WARN
flavour.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR)
}
}
meta[PrefsMetadataKey.DEVICE_MODEL]?.let { model ->
if (model.value != getCurrentDeviceModelString()) {
model.status = PrefsStatus.WARN
model.info = resourceHelper.gs(R.string.metadata_warning_different_device)
}
}
meta[PrefsMetadataKey.CREATED_AT]?.let { createdAt ->
try {
val date1 = DateTime.parse(createdAt.value);
val date2 = DateTime.now()
val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays()
if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) {
createdAt.status = PrefsStatus.WARN
createdAt.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString())
}
} catch (e: Exception) {
createdAt.status = PrefsStatus.WARN
createdAt.info = resourceHelper.gs(R.string.metadata_warning_date_format)
}
}
return meta
}
private fun checkIfImportIsOk(prefs: Prefs): Boolean {
var importOk = true
for ((_, value) in prefs.metadata) {
if (value.status == PrefsStatus.ERROR)
importOk = false;
}
return importOk
}
private fun restartAppAfterImport(context: Context) {
sp.putBoolean(R.string.key_setupwizard_processed, true)
show(context, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable {
log.debug(TAG, "Exiting")
rxBus.send(EventAppExit())
if (context is Activity) {
context.finish()
}
System.runFinalization()
System.exit(0)
})
}
}

View file

@ -23,6 +23,7 @@ class MaintenanceFragment : DaggerFragment() {
@Inject lateinit var resourceHelper: ResourceHelper
@Inject lateinit var treatmentsPlugin: TreatmentsPlugin
@Inject lateinit var foodPlugin: FoodPlugin
@Inject lateinit var importExportPrefs: ImportExportPrefs
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.maintenance_fragment, container, false)
@ -45,13 +46,13 @@ class MaintenanceFragment : DaggerFragment() {
}
nav_export.setOnClickListener {
// start activity for checking permissions...
ImportExportPrefs.verifyStoragePermissions(this)
ImportExportPrefs.exportSharedPreferences(this)
importExportPrefs.verifyStoragePermissions(this)
importExportPrefs.exportSharedPreferences(this)
}
nav_import.setOnClickListener {
// start activity for checking permissions...
ImportExportPrefs.verifyStoragePermissions(this)
ImportExportPrefs.importSharedPreferences(this)
importExportPrefs.verifyStoragePermissions(this)
importExportPrefs.importSharedPreferences(this)
}
nav_logsettings.setOnClickListener { startActivity(Intent(activity, LogSettingActivity::class.java)) }
}

View file

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

View file

@ -0,0 +1,60 @@
package info.nightscout.androidaps.plugins.general.maintenance.formats
import info.nightscout.androidaps.R
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 {
companion object {
val FORMAT_KEY = "aaps_old"
}
override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) {
try {
val contents = prefs.values.entries.joinToString("\n") { entry ->
"${entry.key}::${entry.value}"
}
storage.putFileContents(file, contents)
} catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) {
throw PrefIOError(file.absolutePath)
}
}
override fun loadPreferences(file: File, masterPassword: String?): Prefs {
var lineParts: Array<String>
val entries: MutableMap<String, String> = mutableMapOf()
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
try {
val rawLines = storage.getFileContents(file).split("\n")
rawLines.forEach { line ->
lineParts = line.split("::").toTypedArray()
if (lineParts.size == 2) {
entries[lineParts[0]] = lineParts[1]
}
}
metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format))
return Prefs(entries, metadata)
} catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) {
throw PrefIOError(file.absolutePath)
}
}
}

View file

@ -0,0 +1,225 @@
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.JSONObject
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EncryptedPrefsFormat @Inject constructor(
private var resourceHelper: ResourceHelper,
private var storage: Storage
) : PrefsFormat {
companion object {
val FORMAT_KEY_ENC = "aaps_encrypted"
val FORMAT_KEY_NOENC = "aaps_structured"
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 content = JSONObject()
val meta = JSONObject()
val encStatus = prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status ?: PrefsStatus.OK
var encrypted = encStatus == PrefsStatus.OK && masterPassword != null
try {
for ((key, value) in prefs.values.toSortedMap()) {
content.put(key, value)
}
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) {
throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) {
throw PrefIOError(file.absolutePath)
}
}
override fun loadPreferences(file: File, masterPassword: String?): Prefs {
val entries: MutableMap<String, String> = mutableMapOf()
val metadata: MutableMap<PrefsMetadataKey, PrefMetadata> = mutableMapOf()
val issues = LinkedList<String>()
try {
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)
if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) {
val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key)
if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) {
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)
} catch (e: FileNotFoundException) {
throw PrefFileNotFoundError(file.absolutePath)
} catch (e: IOException) {
throw PrefIOError(file.absolutePath)
} catch (e: JSONException) {
throw PrefFormatError("Mallformed preferences JSON file: " + e)
}
}
}

View file

@ -0,0 +1,73 @@
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
enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringRes val label:Int) {
FILE_FORMAT("format", R.drawable.ic_meta_format, R.string.metadata_label_format),
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, var info : String? = null)
data class Prefs(val values : Map<String, String>, var metadata : Map<PrefsMetadataKey, PrefMetadata>)
interface PrefsFormat {
fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null)
fun loadPreferences(file: File, masterPassword: String? = null) : Prefs
}
enum class PrefsStatus(@DrawableRes val icon:Int) {
OK(R.drawable.ic_meta_ok),
WARN(R.drawable.ic_meta_warning),
ERROR(R.drawable.ic_meta_error),
UNKNOWN(R.drawable.ic_meta_error),
DISABLED(R.drawable.ic_meta_error)
}
class PrefFileNotFoundError(message: String) : Exception(message)
class PrefIOError(message: String) : Exception(message)
class PrefFormatError(message: String) : Exception(message)

View file

@ -59,6 +59,7 @@ class SWDefinition @Inject constructor(
private val nsClientPlugin: NSClientPlugin,
private val nsProfilePlugin: NSProfilePlugin,
private val protectionCheck: ProtectionCheck,
private val importExportPrefs: ImportExportPrefs,
private val androidPermission: AndroidPermission
) {
@ -160,8 +161,8 @@ class SWDefinition @Inject constructor(
.add(SWBreak(injector))
.add(SWButton(injector)
.text(R.string.nav_import)
.action(Runnable { ImportExportPrefs.importSharedPreferences(activity) }))
.visibility(SWValidator { ImportExportPrefs.file.exists() && !androidPermission.permissionNotGranted(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) })
.action(Runnable { importExportPrefs.importSharedPreferences(activity) }))
.visibility(SWValidator { importExportPrefs.prefsFileExists() && !androidPermission.permissionNotGranted(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) })
private val screenNsClient = SWScreen(injector, R.string.nsclientinternal_title)
.skippable(true)
.add(SWInfotext(injector)

View file

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

View file

@ -1,26 +1,94 @@
package info.nightscout.androidaps.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaPlayer;
import android.os.Handler;
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 androidx.annotation.DrawableRes;
import androidx.appcompat.view.ContextThemeWrapper;
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.general.overview.events.EventNewNotification;
import info.nightscout.androidaps.plugins.general.overview.notifications.Notification;
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) {
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) {
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,

View file

@ -1,6 +1,8 @@
package info.nightscout.androidaps.utils
import android.os.Handler
import android.view.View
import info.nightscout.androidaps.MainApp
/**
* 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 runOnUiThread(theRunnable: Runnable?) {
val mainHandler = Handler(MainApp.instance().applicationContext.mainLooper)
theRunnable?.let { mainHandler.post(it) }
}

View file

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

View file

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

View file

@ -0,0 +1,49 @@
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")
fun showAlert(context: Context, title: String, message: String, secondMessage: String, ok: (() -> Unit)?, cancel: (() -> Unit)? = null, @DrawableRes icon: Int? = null) {
val secondMessageLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_two_messages, null)
(secondMessageLayout.findViewById<View>(R.id.password_prompt_title) as TextView).text = secondMessage
val dialog = AlertDialogHelper.Builder(context)
.setMessage(message)
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title, icon
?: R.drawable.ic_check_while_48dp))
.setView(secondMessageLayout)
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (ok != null) {
runOnUiThread(Runnable {
ok()
})
}
}
.setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _: Int ->
dialog.dismiss()
SystemClock.sleep(100)
if (cancel != null) {
runOnUiThread(Runnable {
cancel()
})
}
}
.show()
dialog.setCanceledOnTouchOutside(false)
}
}

View file

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

View file

@ -1,20 +1,16 @@
package info.nightscout.androidaps.utils.protection
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.view.ContextThemeWrapper
import androidx.fragment.app.FragmentActivity
import info.nightscout.androidaps.R
import info.nightscout.androidaps.utils.CryptoUtil
import info.nightscout.androidaps.utils.ToastUtils
import info.nightscout.androidaps.utils.alertDialogs.AlertDialogHelper
import info.nightscout.androidaps.utils.sharedPreferences.SP
import javax.inject.Inject
import javax.inject.Singleton
@ -26,41 +22,36 @@ val AUTOFILL_HINT_NEW_PASSWORD = "newPassword"
class PasswordCheck @Inject constructor(val sp: SP) {
@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, "")
if (password == "") {
ok?.invoke("")
return
}
val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null)
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = activity.getString(labelId)
(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))
val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null)
val alertDialogBuilder = AlertDialogHelper.Builder(context)
alertDialogBuilder.setView(promptsView)
val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText
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.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES
}
alertDialogBuilder
.setCancelable(false)
.setCustomTitle(titleLayout)
.setPositiveButton(activity.getString(R.string.ok)) { _, _ ->
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key))
.setPositiveButton(context.getString(R.string.ok)) { _, _ ->
val enteredPassword = userInput.text.toString()
if (CryptoUtil.checkPassword(enteredPassword, password)) ok?.invoke(enteredPassword)
else {
ToastUtils.showToastInUiThread(activity, activity.getString(R.string.wrongpassword))
ToastUtils.errorToast(context, context.getString(R.string.wrongpassword))
fail?.invoke()
}
}
.setNegativeButton(activity.getString(R.string.cancel)
.setNegativeButton(context.getString(R.string.cancel)
) { dialog, _ ->
cancel?.invoke()
dialog.cancel()
@ -72,12 +63,7 @@ class PasswordCheck @Inject constructor(val sp: SP) {
@SuppressLint("InflateParams")
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 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))
val alertDialogBuilder = AlertDialogHelper.Builder(context)
alertDialogBuilder.setView(promptsView)
val userInput = promptsView.findViewById<View>(R.id.passwordprompt_pass) as EditText
@ -90,20 +76,20 @@ class PasswordCheck @Inject constructor(val sp: SP) {
alertDialogBuilder
.setCancelable(false)
.setCustomTitle(titleLayout)
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key))
.setPositiveButton(context.getString(R.string.ok)) { _, _ ->
val enteredPassword = userInput.text.toString()
if (enteredPassword.isNotEmpty()) {
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)
} else {
if (sp.contains(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()
} else {
ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed))
ToastUtils.warnToast(context, context.getString(R.string.password_not_changed))
cancel?.invoke()
}
}
@ -111,7 +97,7 @@ class PasswordCheck @Inject constructor(val sp: SP) {
}
.setNegativeButton(context.getString(R.string.cancel)
) { dialog, _ ->
ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed))
ToastUtils.infoToast(context, context.getString(R.string.password_not_changed))
cancel?.invoke()
dialog.cancel()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -8,14 +12,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/dialog_title_background"
android:background="?dialogTitleBackground"
android:orientation="horizontal"
android:padding="5dp">
<ImageView
android:id="@+id/alertdialog_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:tint="?dialogTitleIconTint" />
<TextView
android:id="@+id/alertdialog_title"
@ -23,10 +28,13 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="50dp"
android:layout_toEndOf="@id/alertdialog_icon"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceLarge" />
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?dialogTitleColor" />
</RelativeLayout>
<LinearLayout
@ -36,5 +44,4 @@
android:orientation="horizontal"
android:padding="5dp" />
</LinearLayout>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,6 +47,8 @@
<color name="dialog_title_background">#303030</color>
<color name="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="cardObjectiveText">#779ECB</color>
@ -87,4 +89,21 @@
<color name="ribbonTextCritical">#FFFFFF</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>

View file

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

View file

@ -253,6 +253,51 @@
<string name="dismiss">DISMISS</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="connecting">Connecting</string>
<string name="connected">Connected</string>
@ -1142,6 +1187,8 @@
<string name="error_adding_treatment_title">Treatment data incomplete</string>
<string name="maintenance_settings">Maintenance Settings</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_amount" translatable="false">maintenance_logs_amount</string>
<string name="key_logshipper_amount" translatable="false">logshipper_amount</string>

View file

@ -11,6 +11,9 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</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 name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
@ -67,4 +70,28 @@
<item name="android:textColor">#ff0000</item>
</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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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