From 1c97f8a720fd970ced021201bdf506a26c2a1165 Mon Sep 17 00:00:00 2001 From: Dominik Dzienia Date: Tue, 11 Feb 2020 19:34:56 +0100 Subject: [PATCH] Preferences Encryption: - encrypted JSON format support - using master password & password prompt - refactored alerts --- .../dependencyInjection/AppModule.kt | 14 +- .../dependencyInjection/FragmentsModule.kt | 3 + .../general/maintenance/ImportExportPrefs.kt | 286 +++++++++++++++--- .../general/maintenance/MaintenancePlugin.kt | 12 + .../maintenance/formats/ClassicPrefsFormat.kt | 46 +-- .../formats/EncryptedPrefsFormat.kt | 199 +++++++++++- .../maintenance/formats/PrefsFormat.kt | 69 ++++- .../nightscout/androidaps/utils/OKDialog.kt | 66 ++-- .../androidaps/utils/ToastUtils.java | 72 ++++- .../nightscout/androidaps/utils/UIUtils.kt | 6 + .../utils/alertDialogs/AlertDialogHelper.kt | 31 ++ .../alertDialogs/PrefImportSummaryDialog.kt | 140 +++++++++ .../alertDialogs/TwoMessagesAlertDialog.kt | 51 ++++ .../utils/alertDialogs/WarningDialog.kt | 49 +++ .../utils/protection/PasswordCheck.kt | 44 +-- .../androidaps/utils/storage/FileStrorage.kt | 17 ++ .../androidaps/utils/storage/Storage.kt | 11 + .../main/res/drawable/alert_border_error.xml | 15 + .../res/drawable/alert_border_warning.xml | 15 + app/src/main/res/drawable/ic_header_error.xml | 16 + .../main/res/drawable/ic_header_export.xml | 16 + .../main/res/drawable/ic_header_import.xml | 16 + app/src/main/res/drawable/ic_header_key.xml | 16 + app/src/main/res/drawable/ic_header_log.xml | 16 + .../main/res/drawable/ic_header_warning.xml | 16 + app/src/main/res/drawable/ic_key_48dp.xml | 17 -- app/src/main/res/drawable/ic_meta_date.xml | 9 + .../main/res/drawable/ic_meta_encryption.xml | 9 + app/src/main/res/drawable/ic_meta_error.xml | 10 + app/src/main/res/drawable/ic_meta_flavour.xml | 9 + app/src/main/res/drawable/ic_meta_format.xml | 9 + app/src/main/res/drawable/ic_meta_model.xml | 9 + app/src/main/res/drawable/ic_meta_name.xml | 9 + app/src/main/res/drawable/ic_meta_ok.xml | 10 + app/src/main/res/drawable/ic_meta_version.xml | 9 + app/src/main/res/drawable/ic_meta_warning.xml | 10 + app/src/main/res/drawable/ic_toast_check.xml | 10 + .../res/drawable/ic_toast_delete_confirm.xml | 10 + app/src/main/res/drawable/ic_toast_error.xml | 10 + app/src/main/res/drawable/ic_toast_info.xml | 10 + app/src/main/res/drawable/ic_toast_warn.xml | 10 + app/src/main/res/drawable/toast_border_ok.xml | 12 + .../main/res/layout/dialog_alert_custom.xml | 19 +- .../layout/dialog_alert_import_summary.xml | 30 ++ .../res/layout/dialog_alert_two_messages.xml | 19 ++ .../res/layout/import_summary_details.xml | 14 + .../main/res/layout/import_summary_item.xml | 36 +++ app/src/main/res/layout/toast.xml | 32 ++ app/src/main/res/values/attrs.xml | 7 + app/src/main/res/values/colors.xml | 19 ++ app/src/main/res/values/protection.xml | 1 + app/src/main/res/values/strings.xml | 47 +++ app/src/main/res/values/styles.xml | 27 ++ app/src/main/res/xml/pref_maintenance.xml | 4 + .../maintenance/ClassicPrefsFormatTest.kt | 65 ++++ .../maintenance/EncryptedPrefsFormatTest.kt | 226 ++++++++++++++ .../testing/mockers/AAPSMocker.java | 56 ++++ .../testing/mocks/SharedPreferencesMock.java | 158 ++++++++++ .../testing/utils/SingleStringStorage.kt | 26 ++ 59 files changed, 2004 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt create mode 100644 app/src/main/res/drawable/alert_border_error.xml create mode 100644 app/src/main/res/drawable/alert_border_warning.xml create mode 100644 app/src/main/res/drawable/ic_header_error.xml create mode 100644 app/src/main/res/drawable/ic_header_export.xml create mode 100644 app/src/main/res/drawable/ic_header_import.xml create mode 100644 app/src/main/res/drawable/ic_header_key.xml create mode 100644 app/src/main/res/drawable/ic_header_log.xml create mode 100644 app/src/main/res/drawable/ic_header_warning.xml delete mode 100644 app/src/main/res/drawable/ic_key_48dp.xml create mode 100644 app/src/main/res/drawable/ic_meta_date.xml create mode 100644 app/src/main/res/drawable/ic_meta_encryption.xml create mode 100644 app/src/main/res/drawable/ic_meta_error.xml create mode 100644 app/src/main/res/drawable/ic_meta_flavour.xml create mode 100644 app/src/main/res/drawable/ic_meta_format.xml create mode 100644 app/src/main/res/drawable/ic_meta_model.xml create mode 100644 app/src/main/res/drawable/ic_meta_name.xml create mode 100644 app/src/main/res/drawable/ic_meta_ok.xml create mode 100644 app/src/main/res/drawable/ic_meta_version.xml create mode 100644 app/src/main/res/drawable/ic_meta_warning.xml create mode 100644 app/src/main/res/drawable/ic_toast_check.xml create mode 100644 app/src/main/res/drawable/ic_toast_delete_confirm.xml create mode 100644 app/src/main/res/drawable/ic_toast_error.xml create mode 100644 app/src/main/res/drawable/ic_toast_info.xml create mode 100644 app/src/main/res/drawable/ic_toast_warn.xml create mode 100644 app/src/main/res/drawable/toast_border_ok.xml create mode 100644 app/src/main/res/layout/dialog_alert_import_summary.xml create mode 100644 app/src/main/res/layout/dialog_alert_two_messages.xml create mode 100644 app/src/main/res/layout/import_summary_details.xml create mode 100644 app/src/main/res/layout/import_summary_item.xml create mode 100644 app/src/main/res/layout/toast.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/ClassicPrefsFormatTest.kt create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/EncryptedPrefsFormatTest.kt create mode 100644 app/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java create mode 100644 app/src/test/java/info/nightscout/androidaps/testing/mocks/SharedPreferencesMock.java create mode 100644 app/src/test/java/info/nightscout/androidaps/testing/utils/SingleStringStorage.kt diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt index cc75cf16be..4a081f85fb 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt @@ -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 @@ -37,6 +36,8 @@ import info.nightscout.androidaps.plugins.general.automation.elements.* import info.nightscout.androidaps.plugins.general.automation.triggers.* import info.nightscout.androidaps.plugins.general.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 @@ -46,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.* @@ -55,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 @@ -104,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 { @@ -251,6 +259,8 @@ 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 diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt index 63ed16fb4c..d151912151 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt index ede8d79bc5..f5ad429641 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt @@ -2,25 +2,42 @@ 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.OKDialog.showConfirmation 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 @@ -34,14 +51,20 @@ private val PERMISSIONS_STORAGE = arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE ) +private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60 + @Singleton -class ImportExportPrefs @Inject constructor ( +class ImportExportPrefs @Inject constructor( private var log: AAPSLogger, private val resourceHelper: ResourceHelper, - private val sp : SP, - private val rxBus: RxBusWrapper -) -{ + 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 @@ -50,16 +73,16 @@ class ImportExportPrefs @Inject constructor ( 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 { + fun prefsImportFile(): File { return if (encFile.exists()) encFile else file } - fun prefsFileExists() : Boolean { + fun prefsFileExists(): Boolean { return encFile.exists() || file.exists() } fun exportSharedPreferences(f: Fragment) { - exportSharedPreferences(f.context) + exportSharedPreferences(f.activity) } fun verifyStoragePermissions(fragment: Fragment) { @@ -71,72 +94,247 @@ class ImportExportPrefs @Inject constructor ( } } - private fun exportSharedPreferences(context: Context?) { - showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", Runnable { + private fun prepareMetadata(context: Context?): Map { + + val metadata: MutableMap = mutableMapOf() + + if (context != null) { + metadata[PrefsMetadataKey.DEVICE_NAME] = PrefMetadata(detectUserName(context), PrefsStatus.OK) + } + + metadata[PrefsMetadataKey.CREATED_AT] = PrefMetadata(DateUtil.toISOString(Date()), PrefsStatus.OK) + metadata[PrefsMetadataKey.AAPS_VERSION] = PrefMetadata(BuildConfig.VERSION_NAME, PrefsStatus.OK) + metadata[PrefsMetadataKey.AAPS_FLAVOUR] = PrefMetadata(BuildConfig.FLAVOR, PrefsStatus.OK) + metadata[PrefsMetadataKey.DEVICE_MODEL] = PrefMetadata(getCurrentDeviceModelString(), PrefsStatus.OK) + + if (prefsEncryptionIsDisabled()) { + metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Disabled", PrefsStatus.DISABLED) + } else { + metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Enabled", PrefsStatus.OK) + } + + return metadata + } + + private fun detectUserName(context: Context): String { + // based on https://medium.com/@pribble88/how-to-get-an-android-device-nickname-4b4700b3068c + val n1 = Settings.System.getString(context.contentResolver, "bluetooth_name") + val n2 = Settings.Secure.getString(context.contentResolver, "bluetooth_name") + val n3 = BluetoothAdapter.getDefaultAdapter()?.name + val n4 = Settings.System.getString(context.contentResolver, "device_name") + val n5 = Settings.Secure.getString(context.contentResolver, "lock_screen_owner_info") + val n6 = Settings.Global.getString(context.contentResolver, "device_name") + + // name we use for SMS OTP token in communicator + val otpName = otp.name().trim() + val defaultOtpName = resourceHelper.gs(R.string.smscommunicator_default_user_display_name) + + // name we detect from OS + val systemName = n1 ?: n2 ?: n3 ?: n4 ?: n5 ?: n6 ?: defaultOtpName + val name = if (otpName.length > 0 && otpName != defaultOtpName) otpName else systemName + return name + } + + private fun getCurrentDeviceModelString() = + Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")" + + private fun prefsEncryptionIsDisabled() = + buildHelper.isEngineeringMode() && !sp.getBoolean(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs), true) + + private fun askForMasterPass(activity: Activity?, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) { + passwordCheck.queryPassword(activity!!, R.string.master_password, R.string.key_master_password, { password -> + then(password) + }, { + ToastUtils.warnToast(activity, resourceHelper.gs(canceledMsg)) + }) + } + + private fun askForMasterPassIfNeeded(activity: Activity?, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) { + if (prefsEncryptionIsDisabled()) { + then("") + } else { + askForMasterPass(activity, canceledMsg, then) + } + } + + private fun assureMasterPasswordSet(activity: Activity?, @StringRes wrongPwdTitle: Int): Boolean { + if (!sp.contains(R.string.key_master_password) || (sp.getString(R.string.key_master_password, "") == "")) { + WarningDialog.showWarning(activity!!, + resourceHelper.gs(wrongPwdTitle), + resourceHelper.gs(R.string.master_password_missing, resourceHelper.gs(R.string.configbuilder_general), resourceHelper.gs(R.string.protection)), + R.string.nav_preferences, { + val intent = Intent(activity, PreferencesActivity::class.java).apply { + putExtra("id", R.xml.pref_general) + } + activity.startActivity(intent) + }) + return false + } + return true + } + + private fun askToConfirmExport(activity: Activity?, then: ((password: String) -> Unit)) { + if (!prefsEncryptionIsDisabled() && !assureMasterPasswordSet(activity, R.string.nav_export)) return + + TwoMessagesAlertDialog.showAlert(activity!!, resourceHelper.gs(R.string.nav_export), + resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", + resourceHelper.gs(R.string.password_preferences_encrypt_prompt), { + askForMasterPassIfNeeded(activity, R.string.preferences_export_canceled, then) + }, null, R.drawable.ic_header_export) + } + + private fun askToConfirmImport(activity: Activity?, fileToImport: File, then: ((password: String) -> Unit)) { + + if (encFile.exists()) { + if (!assureMasterPasswordSet(activity, R.string.nav_import)) return + + TwoMessagesAlertDialog.showAlert(activity!!, resourceHelper.gs(R.string.nav_import), + resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", + resourceHelper.gs(R.string.password_preferences_decrypt_prompt), { + askForMasterPass(activity, R.string.preferences_import_canceled, then) + }, null, R.drawable.ic_header_import) + + } else { + OKDialog.showConfirmation(activity!!, resourceHelper.gs(R.string.nav_import), + resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", + Runnable { then("") }) + } + } + + private fun exportSharedPreferences(activity: Activity?) { + askToConfirmExport(activity) { password -> try { val entries: MutableMap = mutableMapOf() for ((key, value) in sp.getAll()) { entries[key] = value.toString() } - val prefs = Prefs(entries, mapOf()) + val prefs = Prefs(entries, prepareMetadata(activity)) - ClassicPrefsFormat.savePreferences(file, prefs) - EncryptedPrefsFormat.savePreferences(encFile, prefs) + classicPrefsFormat.savePreferences(file, prefs) + encryptedPrefsFormat.savePreferences(encFile, prefs, password) - ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.exported)) + ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported)) } catch (e: FileNotFoundException) { - ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + encFile) - log.error(TAG,"Unhandled exception", e) + ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + encFile) + log.error(TAG, "Unhandled exception", e) } catch (e: IOException) { - log.error(TAG,"Unhandled exception", e) + ToastUtils.errorToast(activity, e.message) + log.error(TAG, "Unhandled exception", e) } - }) + } } fun importSharedPreferences(fragment: Fragment) { - importSharedPreferences(fragment.context) + importSharedPreferences(fragment.activity) } - fun importSharedPreferences(context: Context?) { + fun importSharedPreferences(activity: Activity?) { val importFile = prefsImportFile() - showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.import_from) + " " + importFile + " ?", Runnable { + askToConfirmImport(activity, importFile) { password -> - val format : PrefsFormat = if (encFile.exists()) EncryptedPrefsFormat else ClassicPrefsFormat + val format: PrefsFormat = if (encFile.exists()) encryptedPrefsFormat else classicPrefsFormat try { - val prefs = format.loadPreferences(importFile) - sp.clear() - for ((key, value) in prefs.values) { - if (value == "true" || value == "false") { - sp.putBoolean(key, value.toBoolean()) + 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 { - sp.putString(key, value) + // for impossible imports it should not be called + ToastUtils.errorToast(activity, "Cannot import preferences!") } - } - - sp.putBoolean(R.string.key_setupwizard_processed, true) - show(context, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable { - log.debug(TAG,"Exiting") - rxBus.send(EventAppExit()) - if (context is Activity) { - context.finish() - } - System.runFinalization() - System.exit(0) }) - } catch (e: PrefFileNotFoundError) { - ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + importFile) - log.error(TAG,"Unhandled exception", e) + ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + importFile) + log.error(TAG, "Unhandled exception", e) } catch (e: PrefIOError) { - log.error(TAG,"Unhandled exception", e) + log.error(TAG, "Unhandled exception", e) + ToastUtils.errorToast(activity, e.message) } + } + } + // check metadata for known issues, change their status and add info with explanations + private fun checkMetadata(metadata: Map): Map { + val meta = metadata.toMutableMap() + + if (meta.containsKey(PrefsMetadataKey.AAPS_FLAVOUR)) { + val flavourOfPrefs = meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.value + if (meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.value != BuildConfig.FLAVOR) { + meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR) + } + } + + if (meta.containsKey(PrefsMetadataKey.DEVICE_MODEL)) { + if (meta[PrefsMetadataKey.DEVICE_MODEL]!!.value != getCurrentDeviceModelString()) { + meta[PrefsMetadataKey.DEVICE_MODEL]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.DEVICE_MODEL]!!.info = resourceHelper.gs(R.string.metadata_warning_different_device) + } + } + + if (meta.containsKey(PrefsMetadataKey.CREATED_AT)) { + try { + val date1 = DateTime.parse(meta[PrefsMetadataKey.CREATED_AT]!!.value); + val date2 = DateTime.now() + + val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays() + + if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) { + meta[PrefsMetadataKey.CREATED_AT]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.CREATED_AT]!!.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString()) + } + } catch (e: Exception) { + meta[PrefsMetadataKey.CREATED_AT]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.CREATED_AT]!!.info = resourceHelper.gs(R.string.metadata_warning_date_format) + } + } + + return meta + } + + private fun checkIfImportIsOk(prefs: Prefs): Boolean { + var importOk = true + + for ((_, value) in prefs.metadata) { + if (value.status == PrefsStatus.ERROR) + importOk = false; + } + return importOk + } + + private fun restartAppAfterImport(context: Context?) { + sp.putBoolean(R.string.key_setupwizard_processed, true) + show(context!!, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable { + log.debug(TAG, "Exiting") + rxBus.send(EventAppExit()) + if (context is Activity) { + context.finish() + } + System.runFinalization() + System.exit(0) }) } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt index cd4d333995..4a3e5e83ab 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt @@ -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() + } + } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt index c71775c33c..4689f549a6 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt @@ -1,22 +1,30 @@ package info.nightscout.androidaps.plugins.general.maintenance.formats -import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs -import java.io.* +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 { -object ClassicPrefsFormat : PrefsFormat { + companion object { + val FORMAT_KEY = "aaps_old" + } - const val FORMAT_KEY = "old" - - override fun savePreferences(file:File, prefs: Prefs) { + override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { try { - val fw = FileWriter(file) - val pw = PrintWriter(fw) - for ((key, value) in prefs.values) { - pw.println(key + "::" + value) + val contents = prefs.values.entries.joinToString("\n") { entry -> + "${entry.key}::${entry.value}" } - pw.close() - fw.close() + storage.putFileContents(file, contents) } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) } catch (e: IOException) { @@ -24,31 +32,29 @@ object ClassicPrefsFormat : PrefsFormat { } } - override fun loadPreferences(file:File): Prefs { - var line: String + override fun loadPreferences(file: File, masterPassword: String?): Prefs { var lineParts: Array val entries: MutableMap = mutableMapOf() val metadata: MutableMap = mutableMapOf() try { - val reader = BufferedReader(FileReader(file)) - while (reader.readLine().also { line = it } != null) { + + val rawLines = storage.getFileContents(file).split("\n") + rawLines.forEach { line -> lineParts = line.split("::").toTypedArray() if (lineParts.size == 2) { entries[lineParts[0]] = lineParts[1] } } - reader.close() - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN) + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format)) return Prefs(entries, metadata) - } catch (e: FileNotFoundException) { + } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) } catch (e: IOException) { throw PrefIOError(file.absolutePath) } } - } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt index 7e32029c46..a2debad281 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt @@ -1,25 +1,90 @@ 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 -object EncryptedPrefsFormat : PrefsFormat { +@Singleton +class EncryptedPrefsFormat @Inject constructor( + private var resourceHelper: ResourceHelper, + private var storage: Storage +) : PrefsFormat { - const val FORMAT_KEY = "new_v1" + companion object { + val FORMAT_KEY_ENC = "aaps_encrypted" + val FORMAT_KEY_NOENC = "aaps_structured" - override fun savePreferences(file:File, prefs: Prefs) { + private val KEY_CONSCIENCE = "if you remove/change this, please make sure you know the consequences!" + } + + override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { val container = JSONObject() + val 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) { - container.put(key, value) + for ((key, value) in prefs.values.toSortedMap()) { + content.put(key, value) } - file.writeText(container.toString(2)); + for ((metaKey, metaEntry) in prefs.metadata) { + if (metaKey == PrefsMetadataKey.FILE_FORMAT) + continue; + if (metaKey == PrefsMetadataKey.ENCRYPTION) + continue; + meta.put(metaKey.key, metaEntry.value) + } + + container.put(PrefsMetadataKey.FILE_FORMAT.key, if (encrypted) FORMAT_KEY_ENC else FORMAT_KEY_NOENC); + container.put("metadata", meta) + + val security = JSONObject() + security.put("file_hash", "--to-be-calculated--") + var encodedContent = "" + + if (encrypted) { + val salt = CryptoUtil.mineSalt() + val rawContent = content.toString() + val contentAttempt = CryptoUtil.encrypt(masterPassword!!, salt, rawContent) + if (contentAttempt != null) { + encodedContent = contentAttempt + security.put("algorithm", "v1") + security.put("salt", salt.toHex()) + security.put("content_hash", CryptoUtil.sha256(rawContent)) + } else { + // fallback when encryption does not work + encrypted = false + } + } + + if (!encrypted) { + security.put("algorithm", "none") + } + + container.put("security", security) + container.put("content", if (encrypted) encodedContent else content) + + var fileContents = container.toString(2) + val fileHash = CryptoUtil.hmac256(fileContents, KEY_CONSCIENCE) + + fileContents = fileContents.replace(Regex("(\\\"file_hash\\\"\\s*\\:\\s*\\\")(--to-be-calculated--)(\\\")"), "$1" + fileHash + "$3") + + storage.putFileContents(file, fileContents) } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) @@ -28,31 +93,133 @@ object EncryptedPrefsFormat : PrefsFormat { } } - override fun loadPreferences(file:File): Prefs { + override fun loadPreferences(file: File, masterPassword: String?): Prefs { val entries: MutableMap = mutableMapOf() val metadata: MutableMap = mutableMapOf() + val issues = LinkedList() try { - val jsonBody = file.readText() + val jsonBody = storage.getFileContents(file) + val fileContents = jsonBody.replace(Regex("(?is)(\\\"file_hash\\\"\\s*\\:\\s*\\\")([^\"]*)(\\\")"), "$1--to-be-calculated--$3") + val calculatedFileHash = CryptoUtil.hmac256(fileContents, KEY_CONSCIENCE) val container = JSONObject(jsonBody) - for (key in container.keys()) { - entries.put(key, container[key].toString()) - } + if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) { + val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key) - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.OK) + if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) { + throw PrefFormatError("Unsupported file format: "+fileFormat) + } + + val meta = container.getJSONObject("metadata") + val security = container.getJSONObject("security") + + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK) + for (key in meta.keys()) { + val metaKey = PrefsMetadataKey.fromKey(key) + if (metaKey != null) { + metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK) + } + } + + val encrypted = fileFormat == FORMAT_KEY_ENC + var secure: PrefsStatus = PrefsStatus.OK + var decryptedOk = false + var contentJsonObj: JSONObject? = null + var insecurityReason = resourceHelper.gs(R.string.prefdecrypt_settings_tampered) + + if (security.has("file_hash")) { + if (calculatedFileHash != security.getString("file_hash")) { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_modified)) + } + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_missing_file_hash)) + } + + if (encrypted) { + if (security.has("algorithm") && security.get("algorithm") == "v1") { + if (security.has("salt") && security.has("content_hash")) { + + val salt = security.getString("salt").hexStringToByteArray() + val decrypted = CryptoUtil.decrypt(masterPassword!!, salt, container.getString("content")) + + if (decrypted != null) { + try { + val contentHash = CryptoUtil.sha256(decrypted) + + if (contentHash == security.getString("content_hash")) { + contentJsonObj = JSONObject(decrypted) + decryptedOk = true + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_modified)) + } + + } catch (e: JSONException) { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_parsing)) + } + + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_pass)) + insecurityReason = resourceHelper.gs(R.string.prefdecrypt_wrong_password) + } + + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_format)) + } + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_algorithm)) + } + + } else { + + if (secure == PrefsStatus.OK) { + secure = PrefsStatus.WARN + } + + if (!(security.has("algorithm") && security.get("algorithm") == "none")) { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_algorithm)) + } + + contentJsonObj = container.getJSONObject("content") + decryptedOk = true + } + + if (decryptedOk && contentJsonObj != null) { + for (key in contentJsonObj.keys()) { + entries.put(key, contentJsonObj[key].toString()) + } + } + + val issuesStr: String? = if (issues.size > 0) issues.joinToString("\n") else null + val encryptionDescStr = if (encrypted) { + if (secure == PrefsStatus.OK) resourceHelper.gs(R.string.prefdecrypt_settings_secure) else insecurityReason + } else { + if (secure != PrefsStatus.ERROR) resourceHelper.gs(R.string.prefdecrypt_settings_unencrypted) else resourceHelper.gs(R.string.prefdecrypt_settings_tampered) + } + + metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr) + } else { + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR) + } return Prefs(entries, metadata) - } catch (e: FileNotFoundException) { + } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) } catch (e: IOException) { throw PrefIOError(file.absolutePath) - } catch (e: JSONException){ - throw PrefFormatError("Mallformed preferences JSON file: "+e) + } catch (e: JSONException) { + throw PrefFormatError("Mallformed preferences JSON file: " + e) } } - } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt index d8beb30786..a47add0dd7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt @@ -1,26 +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) { - FILE_FORMAT("fileFormat") +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() + + init { + for (value in values()) { + keyToEnumMap.put(value.key, value) + } + } + + fun fromKey(key: String): PrefsMetadataKey? { + if (keyToEnumMap.containsKey(key)) { + return keyToEnumMap.get(key) + } else { + return null + } + } + + + } + + fun formatForDisplay(context: Context, value:String): String { + return when (this) { + FILE_FORMAT -> when (value) { + ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old) + EncryptedPrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new) + EncryptedPrefsFormat.FORMAT_KEY_NOENC -> context.getString(R.string.metadata_format_debug) + else -> context.getString(R.string.metadata_format_other) + } + CREATED_AT -> value.replace("T", " ").replace("Z", " (UTC)") + else -> value + } + } + } -data class PrefMetadata(var value : String, var status : PrefsStatus) +data class PrefMetadata(var value : String, var status : PrefsStatus, var info : String? = null) -data class Prefs(val values : Map, val metadata : Map) +data class Prefs(val values : Map, var metadata : Map) interface PrefsFormat { - fun savePreferences(file: File, prefs: Prefs) - fun loadPreferences(file: File) : Prefs + fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null) + fun loadPreferences(file: File, masterPassword: String? = null) : Prefs } -enum class PrefsStatus { - OK, - WARN, - ERROR +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) \ No newline at end of file +class PrefFormatError(message: String) : Exception(message) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt index 285b3aaa10..186b81ae36 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt @@ -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(R.id.alertdialog_title) as TextView).text = notEmptytitle - (titleLayout.findViewById(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(R.id.alertdialog_title) as TextView).text = notEmptytitle - (titleLayout.findViewById(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(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(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(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(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(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(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(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(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(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(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) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java b/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java index 7822e8a80f..26062011f6 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java @@ -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, diff --git a/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt b/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt index 99c4a4af31..ad57e519da 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt @@ -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) } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt new file mode 100644 index 0000000000..afc5491652 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt @@ -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(R.id.alertdialog_title) as TextView).text = title + (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(iconResource) + return titleLayout + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt new file mode 100644 index 0000000000..db5731f8fc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt @@ -0,0 +1,140 @@ +package info.nightscout.androidaps.utils.alertDialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.graphics.Color +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.webkit.WebView +import android.widget.Button +import android.widget.ImageView +import android.widget.TableLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.annotation.StyleRes +import androidx.appcompat.view.ContextThemeWrapper +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.general.maintenance.formats.Prefs +import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsStatus +import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.runOnUiThread +import java.util.* + +object PrefImportSummaryDialog { + + @SuppressLint("InflateParams") + @JvmStatic + @JvmOverloads + fun showSummary(context: Context, importOk: Boolean, importPossible: Boolean, prefs: Prefs, ok: (() -> Unit)?, cancel: (() -> Unit)? = null) { + + @StyleRes val theme: Int = if (importOk) R.style.AppTheme else { + if (importPossible) R.style.AppThemeWarningDialog else R.style.AppThemeErrorDialog + } + + @StringRes val messageRes: Int = if (importOk) R.string.check_preferences_before_import else { + if (importPossible) R.string.check_preferences_dangerous_import else R.string.check_preferences_cannot_import + } + + @DrawableRes val headerIcon: Int = if (importOk) R.drawable.ic_header_import else { + if (importPossible) R.drawable.ic_header_warning else R.drawable.ic_header_error + } + + val themedCtx = ContextThemeWrapper(context, theme) + + val innerLayout = LayoutInflater.from(themedCtx).inflate(R.layout.dialog_alert_import_summary, null) + val table = (innerLayout.findViewById(R.id.summary_table) as TableLayout) + val detailsBtn = (innerLayout.findViewById(R.id.summary_details_btn) as Button) + + var idx = 0 + val details = LinkedList() + + + for ((metaKey, metaEntry) in prefs.metadata) { + val rowLayout = LayoutInflater.from(themedCtx).inflate(R.layout.import_summary_item, null) + val label = (rowLayout.findViewById(R.id.summary_text) as TextView) + label.setText(metaKey.formatForDisplay(context, metaEntry.value)) + (rowLayout.findViewById(R.id.summary_icon) as ImageView).setImageResource(metaKey.icon) + (rowLayout.findViewById(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("${context.getString(metaKey.label)}: ${metaEntry.value}
${metaEntry.info}") + 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(R.id.details_webview) as WebView + wview.loadData("" + details.joinToString("
"), "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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt new file mode 100644 index 0000000000..7e8a4d47a6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.utils.alertDialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.runOnUiThread + +object TwoMessagesAlertDialog { + + @SuppressLint("InflateParams") + @JvmStatic + @JvmOverloads + fun showAlert(context: Context, title: String, message: String, secondMessage: String, ok: (() -> Unit)?, cancel: (() -> Unit)? = null, @DrawableRes icon: Int? = null) { + + val secondMessageLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_two_messages, null) + (secondMessageLayout.findViewById(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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt new file mode 100644 index 0000000000..cd0654f3bf --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt index 65f1e76e44..1907ac01b5 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt @@ -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(R.id.alertdialog_title) as TextView).text = activity.getString(labelId) - (titleLayout.findViewById(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(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(R.id.alertdialog_title) as TextView).text = context.getText(labelId) - (titleLayout.findViewById(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(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() } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt b/app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt new file mode 100644 index 0000000000..40be1e65c3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt b/app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt new file mode 100644 index 0000000000..475cd2675e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt @@ -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) + +} diff --git a/app/src/main/res/drawable/alert_border_error.xml b/app/src/main/res/drawable/alert_border_error.xml new file mode 100644 index 0000000000..d1bcae1348 --- /dev/null +++ b/app/src/main/res/drawable/alert_border_error.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/alert_border_warning.xml b/app/src/main/res/drawable/alert_border_warning.xml new file mode 100644 index 0000000000..c73a9517a5 --- /dev/null +++ b/app/src/main/res/drawable/alert_border_warning.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_header_error.xml b/app/src/main/res/drawable/ic_header_error.xml new file mode 100644 index 0000000000..09c29c1558 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_error.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_export.xml b/app/src/main/res/drawable/ic_header_export.xml new file mode 100644 index 0000000000..d59103ab07 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_export.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_import.xml b/app/src/main/res/drawable/ic_header_import.xml new file mode 100644 index 0000000000..e929fad69b --- /dev/null +++ b/app/src/main/res/drawable/ic_header_import.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_key.xml b/app/src/main/res/drawable/ic_header_key.xml new file mode 100644 index 0000000000..416cc50bd3 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_key.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_log.xml b/app/src/main/res/drawable/ic_header_log.xml new file mode 100644 index 0000000000..6ef0e68d81 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_log.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_warning.xml b/app/src/main/res/drawable/ic_header_warning.xml new file mode 100644 index 0000000000..3d7afd8ef3 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_warning.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_key_48dp.xml b/app/src/main/res/drawable/ic_key_48dp.xml deleted file mode 100644 index 813dc24d00..0000000000 --- a/app/src/main/res/drawable/ic_key_48dp.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_date.xml b/app/src/main/res/drawable/ic_meta_date.xml new file mode 100644 index 0000000000..190392aaa1 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_date.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_encryption.xml b/app/src/main/res/drawable/ic_meta_encryption.xml new file mode 100644 index 0000000000..597b35a77a --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_encryption.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_error.xml b/app/src/main/res/drawable/ic_meta_error.xml new file mode 100644 index 0000000000..e5d213f774 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_error.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_flavour.xml b/app/src/main/res/drawable/ic_meta_flavour.xml new file mode 100644 index 0000000000..a7fe7f3471 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_flavour.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_format.xml b/app/src/main/res/drawable/ic_meta_format.xml new file mode 100644 index 0000000000..2023a4934d --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_format.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_model.xml b/app/src/main/res/drawable/ic_meta_model.xml new file mode 100644 index 0000000000..eca3e69c59 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_model.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_name.xml b/app/src/main/res/drawable/ic_meta_name.xml new file mode 100644 index 0000000000..ad120e037a --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_name.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_ok.xml b/app/src/main/res/drawable/ic_meta_ok.xml new file mode 100644 index 0000000000..663897c160 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_ok.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_meta_version.xml b/app/src/main/res/drawable/ic_meta_version.xml new file mode 100644 index 0000000000..ddf95f2fa9 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_version.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_warning.xml b/app/src/main/res/drawable/ic_meta_warning.xml new file mode 100644 index 0000000000..9cf5ce2c36 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_warning.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_check.xml b/app/src/main/res/drawable/ic_toast_check.xml new file mode 100644 index 0000000000..26b9f05b49 --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_check.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_delete_confirm.xml b/app/src/main/res/drawable/ic_toast_delete_confirm.xml new file mode 100644 index 0000000000..de6371045a --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_delete_confirm.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_error.xml b/app/src/main/res/drawable/ic_toast_error.xml new file mode 100644 index 0000000000..fb49272c0a --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_error.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_info.xml b/app/src/main/res/drawable/ic_toast_info.xml new file mode 100644 index 0000000000..9d54be6827 --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_info.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_warn.xml b/app/src/main/res/drawable/ic_toast_warn.xml new file mode 100644 index 0000000000..8864b16500 --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_warn.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toast_border_ok.xml b/app/src/main/res/drawable/toast_border_ok.xml new file mode 100644 index 0000000000..1c62848b31 --- /dev/null +++ b/app/src/main/res/drawable/toast_border_ok.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/layout/dialog_alert_custom.xml b/app/src/main/res/layout/dialog_alert_custom.xml index eea5fc6882..f006387d28 100644 --- a/app/src/main/res/layout/dialog_alert_custom.xml +++ b/app/src/main/res/layout/dialog_alert_custom.xml @@ -1,4 +1,8 @@ + + android:layout_height="wrap_content" + android:tint="?dialogTitleIconTint" /> + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="?dialogTitleColor" /> + - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_alert_import_summary.xml b/app/src/main/res/layout/dialog_alert_import_summary.xml new file mode 100644 index 0000000000..fb6e5a0708 --- /dev/null +++ b/app/src/main/res/layout/dialog_alert_import_summary.xml @@ -0,0 +1,30 @@ + + + + + +