Merge pull request #2545 from dlvoy/dagger3-encprefs
Preferences Encryption
This commit is contained in:
commit
64ee80b48b
62 changed files with 2200 additions and 240 deletions
|
@ -26,7 +26,6 @@ import info.nightscout.androidaps.plugins.aps.openAPSMA.DetermineBasalResultMA
|
|||
import info.nightscout.androidaps.plugins.aps.openAPSMA.LoggerCallback
|
||||
import info.nightscout.androidaps.plugins.aps.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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)) }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) }
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package info.nightscout.androidaps.utils.alertDialogs
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import info.nightscout.androidaps.R
|
||||
|
||||
object AlertDialogHelper {
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun Builder(context: Context, @StyleRes themeResId: Int = R.style.AppTheme) =
|
||||
AlertDialog.Builder(ContextThemeWrapper(context, themeResId))
|
||||
|
||||
fun buildCustomTitle(context: Context, title: String,
|
||||
@DrawableRes iconResource: Int = R.drawable.ic_check_while_48dp,
|
||||
@StyleRes themeResId: Int = R.style.AppTheme,
|
||||
@LayoutRes layoutResource: Int = R.layout.dialog_alert_custom): View? {
|
||||
val titleLayout = LayoutInflater.from(ContextThemeWrapper(context, themeResId)).inflate(layoutResource, null)
|
||||
(titleLayout.findViewById<View>(R.id.alertdialog_title) as TextView).text = title
|
||||
(titleLayout.findViewById<View>(R.id.alertdialog_icon) as ImageView).setImageResource(iconResource)
|
||||
return titleLayout
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package info.nightscout.androidaps.utils.alertDialogs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.SystemClock
|
||||
import androidx.annotation.StringRes
|
||||
import info.nightscout.androidaps.R
|
||||
import info.nightscout.androidaps.utils.runOnUiThread
|
||||
|
||||
// if you need error dialog - duplicate to ErrorDialog and make it and use: AppThemeErrorDialog & R.drawable.ic_header_error instead
|
||||
|
||||
object WarningDialog {
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun showWarning(context: Context, title: String, message: String, @StringRes positiveButton: Int = -1, ok: (() -> Unit)? = null, cancel: (() -> Unit)? = null) {
|
||||
|
||||
val builder = AlertDialogHelper.Builder(context, R.style.AppThemeWarningDialog)
|
||||
.setMessage(message)
|
||||
.setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title, R.drawable.ic_header_warning, R.style.AppThemeWarningDialog))
|
||||
.setNegativeButton(R.string.dismiss) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
SystemClock.sleep(100)
|
||||
if (cancel != null) {
|
||||
runOnUiThread(Runnable {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (positiveButton != -1) {
|
||||
builder.setPositiveButton(positiveButton) { dialog: DialogInterface, _: Int ->
|
||||
dialog.dismiss()
|
||||
SystemClock.sleep(100)
|
||||
if (ok != null) {
|
||||
runOnUiThread(Runnable {
|
||||
ok()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = builder.show()
|
||||
dialog.setCanceledOnTouchOutside(true)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +1,16 @@
|
|||
package info.nightscout.androidaps.utils.protection
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package info.nightscout.androidaps.utils.storage
|
||||
|
||||
import java.io.File
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class FileStorage : Storage {
|
||||
|
||||
override fun getFileContents(file: File): String {
|
||||
return file.readText()
|
||||
}
|
||||
|
||||
override fun putFileContents(file: File, contents: String) {
|
||||
file.writeText(contents)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package info.nightscout.androidaps.utils.storage
|
||||
|
||||
import java.io.File
|
||||
|
||||
// This may seems unnecessary abstraction - but it will simplify testing
|
||||
interface Storage {
|
||||
|
||||
fun getFileContents(file: File) : String
|
||||
fun putFileContents(file: File, contents: String)
|
||||
|
||||
}
|
15
app/src/main/res/drawable/alert_border_error.xml
Normal file
15
app/src/main/res/drawable/alert_border_error.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="PrivateResource"
|
||||
android:insetLeft="16dp"
|
||||
android:insetTop="16dp"
|
||||
android:insetRight="16dp"
|
||||
android:insetBottom="16dp">
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="2dp" />
|
||||
<solid android:color="@color/background_floating_material_dark" />
|
||||
<stroke android:color="@color/errorAlertBackground" android:width="3dp" />
|
||||
</shape></inset>
|
||||
|
15
app/src/main/res/drawable/alert_border_warning.xml
Normal file
15
app/src/main/res/drawable/alert_border_warning.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="PrivateResource"
|
||||
android:insetLeft="16dp"
|
||||
android:insetTop="16dp"
|
||||
android:insetRight="16dp"
|
||||
android:insetBottom="16dp">
|
||||
<shape
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="2dp" />
|
||||
<solid android:color="@color/background_floating_material_dark" />
|
||||
<stroke android:color="@color/warningAlertBackground" android:width="3dp" />
|
||||
</shape></inset>
|
||||
|
16
app/src/main/res/drawable/ic_header_error.xml
Normal file
16
app/src/main/res/drawable/ic_header_error.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M8.27,3L3,8.27V15.73L8.27,21H15.73C17.5,19.24 21,15.73 21,15.73V8.27L15.73,3M9.1,5H14.9L19,9.1V14.9L14.9,19H9.1L5,14.9V9.1M9.12,7.71L7.71,9.12L10.59,12L7.71,14.88L9.12,16.29L12,13.41L14.88,16.29L16.29,14.88L13.41,12L16.29,9.12L14.88,7.71L12,10.59" />
|
||||
</group>
|
||||
</vector>
|
16
app/src/main/res/drawable/ic_header_export.xml
Normal file
16
app/src/main/res/drawable/ic_header_export.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.66"
|
||||
android:scaleY="0.66">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z" />
|
||||
</group>
|
||||
</vector>
|
16
app/src/main/res/drawable/ic_header_import.xml
Normal file
16
app/src/main/res/drawable/ic_header_import.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.66"
|
||||
android:scaleY="0.66">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z" />
|
||||
</group>
|
||||
</vector>
|
16
app/src/main/res/drawable/ic_header_key.xml
Normal file
16
app/src/main/res/drawable/ic_header_key.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.66"
|
||||
android:scaleY="0.66">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22 18V22H18V19H15V16H12L9.74 13.74C9.19 13.91 8.61 14 8 14A6 6 0 0 1 2 8A6 6 0 0 1 8 2A6 6 0 0 1 14 8C14 8.61 13.91 9.19 13.74 9.74L22 18M7 5A2 2 0 0 0 5 7A2 2 0 0 0 7 9A2 2 0 0 0 9 7A2 2 0 0 0 7 5Z" />
|
||||
</group>
|
||||
</vector>
|
16
app/src/main/res/drawable/ic_header_log.xml
Normal file
16
app/src/main/res/drawable/ic_header_log.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M15,20A1,1 0 0,0 16,19V4H8A1,1 0 0,0 7,5V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H20V5A1,1 0 0,0 19,4A1,1 0 0,0 18,5V9L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H13A2,2 0 0,0 15,20M9,6H14V8H9V6M9,10H14V12H9V10M9,14H14V16H9V14Z" />
|
||||
</group>
|
||||
</vector>
|
16
app/src/main/res/drawable/ic_header_warning.xml
Normal file
16
app/src/main/res/drawable/ic_header_warning.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.75"
|
||||
android:scaleY="0.75">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" />
|
||||
</group>
|
||||
</vector>
|
|
@ -1,17 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
>
|
||||
<group
|
||||
android:scaleX="0.66"
|
||||
android:scaleY="0.66"
|
||||
android:pivotX="12"
|
||||
android:pivotY="12">
|
||||
<path
|
||||
android:pathData="M22 18V22H18V19H15V16H12L9.74 13.74C9.19 13.91 8.61 14 8 14A6 6 0 0 1 2 8A6 6 0 0 1 8 2A6 6 0 0 1 14 8C14 8.61 13.91 9.19 13.74 9.74L22 18M7 5A2 2 0 0 0 5 7A2 2 0 0 0 7 9A2 2 0 0 0 9 7A2 2 0 0 0 7 5Z"
|
||||
android:fillColor="#FF000000" />
|
||||
</group>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_date.xml
Normal file
9
app/src/main/res/drawable/ic_meta_date.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M15,13H16.5V15.82L18.94,17.23L18.19,18.53L15,16.69V13M19,8H5V19H9.67C9.24,18.09 9,17.07 9,16A7,7 0 0,1 16,9C17.07,9 18.09,9.24 19,9.67V8M5,21C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1H18V3H19A2,2 0 0,1 21,5V11.1C22.24,12.36 23,14.09 23,16A7,7 0 0,1 16,23C14.09,23 12.36,22.24 11.1,21H5M16,11.15A4.85,4.85 0 0,0 11.15,16C11.15,18.68 13.32,20.85 16,20.85A4.85,4.85 0 0,0 20.85,16C20.85,13.32 18.68,11.15 16,11.15Z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_encryption.xml
Normal file
9
app/src/main/res/drawable/ic_meta_encryption.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21M14.8,11V9.5C14.8,8.1 13.4,7 12,7C10.6,7 9.2,8.1 9.2,9.5V11C8.6,11 8,11.6 8,12.2V15.7C8,16.4 8.6,17 9.2,17H14.7C15.4,17 16,16.4 16,15.8V12.3C16,11.6 15.4,11 14.8,11M13.5,11H10.5V9.5C10.5,8.7 11.2,8.2 12,8.2C12.8,8.2 13.5,8.7 13.5,9.5V11Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_meta_error.xml
Normal file
10
app/src/main/res/drawable/ic_meta_error.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/metadataTextError"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_flavour.xml
Normal file
9
app/src/main/res/drawable/ic_meta_flavour.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,10.84 21.79,9.69 21.39,8.61L19.79,10.21C19.93,10.8 20,11.4 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4C12.6,4 13.2,4.07 13.79,4.21L15.4,2.6C14.31,2.21 13.16,2 12,2M19,2L15,6V7.5L12.45,10.05C12.3,10 12.15,10 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12C14,11.85 14,11.7 13.95,11.55L16.5,9H18L22,5H19V2M12,6A6,6 0 0,0 6,12A6,6 0 0,0 12,18A6,6 0 0,0 18,12H16A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8V6Z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_format.xml
Normal file
9
app/src/main/res/drawable/ic_meta_format.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M6 2C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H12V20H6V4H13V9H18V12H20V8L14 2M18 14C17.87 14 17.76 14.09 17.74 14.21L17.55 15.53C17.25 15.66 16.96 15.82 16.7 16L15.46 15.5C15.35 15.5 15.22 15.5 15.15 15.63L14.15 17.36C14.09 17.47 14.11 17.6 14.21 17.68L15.27 18.5C15.25 18.67 15.24 18.83 15.24 19C15.24 19.17 15.25 19.33 15.27 19.5L14.21 20.32C14.12 20.4 14.09 20.53 14.15 20.64L15.15 22.37C15.21 22.5 15.34 22.5 15.46 22.5L16.7 22C16.96 22.18 17.24 22.35 17.55 22.47L17.74 23.79C17.76 23.91 17.86 24 18 24H20C20.11 24 20.22 23.91 20.24 23.79L20.43 22.47C20.73 22.34 21 22.18 21.27 22L22.5 22.5C22.63 22.5 22.76 22.5 22.83 22.37L23.83 20.64C23.89 20.53 23.86 20.4 23.77 20.32L22.7 19.5C22.72 19.33 22.74 19.17 22.74 19C22.74 18.83 22.73 18.67 22.7 18.5L23.76 17.68C23.85 17.6 23.88 17.47 23.82 17.36L22.82 15.63C22.76 15.5 22.63 15.5 22.5 15.5L21.27 16C21 15.82 20.73 15.65 20.42 15.53L20.23 14.21C20.22 14.09 20.11 14 20 14M19 17.5C19.83 17.5 20.5 18.17 20.5 19C20.5 19.83 19.83 20.5 19 20.5C18.16 20.5 17.5 19.83 17.5 19C17.5 18.17 18.17 17.5 19 17.5Z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_model.xml
Normal file
9
app/src/main/res/drawable/ic_meta_model.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13 7H11V9H13V7M13 11H11V17H13V11M17 1H7C5.9 1 5 1.9 5 3V21C5 22.1 5.9 23 7 23H17C18.1 23 19 22.1 19 21V3C19 1.9 18.1 1 17 1M17 19H7V5H17V19Z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_name.xml
Normal file
9
app/src/main/res/drawable/ic_meta_name.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M22,3H2C0.91,3.04 0.04,3.91 0,5V19C0.04,20.09 0.91,20.96 2,21H22C23.09,20.96 23.96,20.09 24,19V5C23.96,3.91 23.09,3.04 22,3M22,19H2V5H22V19M14,17V15.75C14,14.09 10.66,13.25 9,13.25C7.34,13.25 4,14.09 4,15.75V17H14M9,7A2.5,2.5 0 0,0 6.5,9.5A2.5,2.5 0 0,0 9,12A2.5,2.5 0 0,0 11.5,9.5A2.5,2.5 0 0,0 9,7M14,7V8H20V7H14M14,9V10H20V9H14M14,11V12H18V11H14" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_meta_ok.xml
Normal file
10
app/src/main/res/drawable/ic_meta_ok.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="@color/toastOk"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_meta_version.xml
Normal file
9
app/src/main/res/drawable/ic_meta_version.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_meta_warning.xml
Normal file
10
app/src/main/res/drawable/ic_meta_warning.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/metadataTextWarning"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_toast_check.xml
Normal file
10
app/src/main/res/drawable/ic_toast_check.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/toastOk"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_toast_delete_confirm.xml
Normal file
10
app/src/main/res/drawable/ic_toast_delete_confirm.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/toastInfo"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8.46,11.88L9.87,10.47L12,12.59L14.12,10.47L15.53,11.88L13.41,14L15.53,16.12L14.12,17.53L12,15.41L9.88,17.53L8.47,16.12L10.59,14L8.46,11.88M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_toast_error.xml
Normal file
10
app/src/main/res/drawable/ic_toast_error.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/toastError"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_toast_info.xml
Normal file
10
app/src/main/res/drawable/ic_toast_info.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/toastInfo"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_toast_warn.xml
Normal file
10
app/src/main/res/drawable/ic_toast_warn.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/toastWarn"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</vector>
|
12
app/src/main/res/drawable/toast_border_ok.xml
Normal file
12
app/src/main/res/drawable/toast_border_ok.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:shape="rectangle"
|
||||
tools:ignore="PrivateResource">
|
||||
<corners android:radius="18dp" />
|
||||
<solid android:color="@color/background_floating_material_dark" />
|
||||
<stroke
|
||||
android:width="3dp"
|
||||
android:color="@color/toastBorder" />
|
||||
</shape>
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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>
|
30
app/src/main/res/layout/dialog_alert_import_summary.xml
Normal file
30
app/src/main/res/layout/dialog_alert_import_summary.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/summary_table"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:stretchColumns="2" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/summary_details_btn"
|
||||
style="?android:attr/buttonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_marginBottom="3dp"
|
||||
android:text="@string/check_preferences_details_btn"
|
||||
android:textColor="@color/colorTreatmentButton"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
19
app/src/main/res/layout/dialog_alert_two_messages.xml
Normal file
19
app/src/main/res/layout/dialog_alert_two_messages.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/password_prompt_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingEnd="5dp"
|
||||
android:text="@string/password_preferences_decrypt_prompt"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="@color/colorAccent" />
|
||||
|
||||
</LinearLayout>
|
14
app/src/main/res/layout/import_summary_details.xml
Normal file
14
app/src/main/res/layout/import_summary_details.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/details_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:background="@android:color/transparent" />
|
||||
</LinearLayout>
|
36
app/src/main/res/layout/import_summary_item.xml
Normal file
36
app/src/main/res/layout/import_summary_item.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TableRow xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_margin="4dp"
|
||||
android:src="@drawable/ic_toast_check" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/summary_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:src="@drawable/ic_meta_format"
|
||||
android:tint="#ffffff" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/summary_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Sample text here"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</TableRow>
|
||||
|
32
app/src/main/res/layout/toast.xml
Normal file
32
app/src/main/res/layout/toast.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/toast_border_ok">
|
||||
|
||||
<ImageView
|
||||
android:id="@android:id/icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:src="@drawable/ic_toast_check"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginRight="18dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="Toast goes here..."
|
||||
android:textColor="@color/toastBase"
|
||||
android:textSize="18sp"
|
||||
tools:ignore="HardcodedText,RtlHardcoded" />
|
||||
</LinearLayout>
|
7
app/src/main/res/values/attrs.xml
Normal file
7
app/src/main/res/values/attrs.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<resources>
|
||||
|
||||
<attr name="dialogTitleBackground" format="reference" />
|
||||
<attr name="dialogTitleColor" format="reference" />
|
||||
<attr name="dialogTitleIconTint" format="reference" />
|
||||
|
||||
</resources>
|
|
@ -47,6 +47,8 @@
|
|||
|
||||
<color name="dialog_title_background">#303030</color>
|
||||
<color name="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>
|
||||
|
|
|
@ -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 → %2$s)</string>
|
||||
|
||||
<string name="password_set">Password set!</string>
|
||||
<string name="password_not_set">Password not set</string>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package info.nightscout.androidaps.plugins.general.maintenance
|
||||
|
||||
import info.nightscout.androidaps.MainApp
|
||||
import info.nightscout.androidaps.TestBase
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.formats.Prefs
|
||||
import info.nightscout.androidaps.testing.mockers.AAPSMocker
|
||||
import info.nightscout.androidaps.testing.utils.SingleStringStorage
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest
|
||||
import org.powermock.modules.junit4.PowerMockRunner
|
||||
import java.io.File
|
||||
|
||||
@RunWith(PowerMockRunner::class)
|
||||
@PrepareForTest(AAPSMocker::class, MainApp::class, File::class)
|
||||
|
||||
class ClassicPrefsFormatTest : TestBase() {
|
||||
|
||||
@Mock lateinit var resourceHelper: ResourceHelper
|
||||
@Mock lateinit var sp: SP
|
||||
|
||||
@Before
|
||||
fun mock() {
|
||||
AAPSMocker.prepareMock()
|
||||
AAPSMocker.resetMockedSharedPrefs()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preferenceLoadingTest() {
|
||||
val test = "key1::val1\nkeyB::valB"
|
||||
|
||||
val classicFormat = ClassicPrefsFormat(resourceHelper, SingleStringStorage(test))
|
||||
val prefs = classicFormat.loadPreferences(AAPSMocker.getMockedFile(), "")
|
||||
|
||||
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2))
|
||||
Assert.assertThat(prefs.values["key1"], CoreMatchers.`is`("val1"))
|
||||
Assert.assertThat(prefs.values["keyB"], CoreMatchers.`is`("valB"))
|
||||
Assert.assertNull(prefs.values["key3"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preferenceSavingTest() {
|
||||
val storage = SingleStringStorage("")
|
||||
val classicFormat = ClassicPrefsFormat(resourceHelper, storage)
|
||||
val prefs = Prefs(
|
||||
mapOf(
|
||||
"key1" to "A",
|
||||
"keyB" to "2"
|
||||
),
|
||||
mapOf()
|
||||
)
|
||||
|
||||
classicFormat.savePreferences(AAPSMocker.getMockedFile(), prefs)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
package info.nightscout.androidaps.plugins.general.maintenance
|
||||
|
||||
import info.nightscout.androidaps.MainApp
|
||||
import info.nightscout.androidaps.TestBase
|
||||
import info.nightscout.androidaps.plugins.general.maintenance.formats.*
|
||||
import info.nightscout.androidaps.testing.mockers.AAPSMocker
|
||||
import info.nightscout.androidaps.testing.utils.SingleStringStorage
|
||||
import info.nightscout.androidaps.utils.resources.ResourceHelper
|
||||
import info.nightscout.androidaps.utils.sharedPreferences.SP
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.json.JSONException
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest
|
||||
import org.powermock.modules.junit4.PowerMockRunner
|
||||
import java.io.File
|
||||
|
||||
@PowerMockIgnore("javax.crypto.*")
|
||||
@RunWith(PowerMockRunner::class)
|
||||
@PrepareForTest(AAPSMocker::class, MainApp::class, File::class, ResourceHelper::class)
|
||||
|
||||
class EncryptedPrefsFormatTest : TestBase() {
|
||||
|
||||
@Mock lateinit var resourceHelper: ResourceHelper
|
||||
@Mock lateinit var sp: SP
|
||||
|
||||
@Before
|
||||
fun mock() {
|
||||
AAPSMocker.prepareMock()
|
||||
AAPSMocker.resetMockedSharedPrefs()
|
||||
Mockito.`when`(resourceHelper.gs(ArgumentMatchers.anyInt())).thenReturn("mock translation")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preferenceLoadingTest() {
|
||||
val frozenPrefs = "{\n" +
|
||||
" \"metadata\": {},\n" +
|
||||
" \"security\": {\n" +
|
||||
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
|
||||
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
|
||||
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
|
||||
" \"algorithm\": \"v1\"\n" +
|
||||
" },\n" +
|
||||
" \"format\": \"aaps_encrypted\",\n" +
|
||||
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
|
||||
"}"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
|
||||
|
||||
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2))
|
||||
Assert.assertThat(prefs.values["key1"], CoreMatchers.`is`("A"))
|
||||
Assert.assertThat(prefs.values["keyB"], CoreMatchers.`is`("2"))
|
||||
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.OK))
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.value, CoreMatchers.`is`(EncryptedPrefsFormat.FORMAT_KEY_ENC))
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.OK))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preferenceSavingTest() {
|
||||
val storage = SingleStringStorage("")
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefs = Prefs(
|
||||
mapOf(
|
||||
"key1" to "A",
|
||||
"keyB" to "2"
|
||||
),
|
||||
mapOf(
|
||||
PrefsMetadataKey.ENCRYPTION to PrefMetadata(EncryptedPrefsFormat.FORMAT_KEY_ENC, PrefsStatus.OK)
|
||||
)
|
||||
)
|
||||
encryptedFormat.savePreferences(AAPSMocker.getMockedFile(), prefs, "sikret")
|
||||
aapsLogger.debug(storage.contents)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun importExportStabilityTest() {
|
||||
val storage = SingleStringStorage("")
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefsIn = Prefs(
|
||||
mapOf(
|
||||
"testpref1" to "--1--",
|
||||
"testpref2" to "another"
|
||||
),
|
||||
mapOf(
|
||||
PrefsMetadataKey.ENCRYPTION to PrefMetadata(EncryptedPrefsFormat.FORMAT_KEY_ENC, PrefsStatus.OK)
|
||||
)
|
||||
)
|
||||
encryptedFormat.savePreferences(AAPSMocker.getMockedFile(), prefsIn, "tajemnica")
|
||||
val prefsOut = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "tajemnica")
|
||||
|
||||
Assert.assertThat(prefsOut.values.size, CoreMatchers.`is`(2))
|
||||
Assert.assertThat(prefsOut.values["testpref1"], CoreMatchers.`is`("--1--"))
|
||||
Assert.assertThat(prefsOut.values["testpref2"], CoreMatchers.`is`("another"))
|
||||
|
||||
Assert.assertThat(prefsOut.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.OK))
|
||||
Assert.assertThat(prefsOut.metadata[PrefsMetadataKey.FILE_FORMAT]?.value, CoreMatchers.`is`(EncryptedPrefsFormat.FORMAT_KEY_ENC))
|
||||
Assert.assertThat(prefsOut.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.OK))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongPasswordTest() {
|
||||
val frozenPrefs = "{\n" +
|
||||
" \"metadata\": {},\n" +
|
||||
" \"security\": {\n" +
|
||||
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
|
||||
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
|
||||
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
|
||||
" \"algorithm\": \"v1\"\n" +
|
||||
" },\n" +
|
||||
" \"format\": \"aaps_encrypted\",\n" +
|
||||
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
|
||||
"}"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "it-is-NOT-right-secret")
|
||||
|
||||
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0))
|
||||
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.OK))
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.value, CoreMatchers.`is`(EncryptedPrefsFormat.FORMAT_KEY_ENC))
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tamperedMetadataTest() {
|
||||
val frozenPrefs = "{\n" +
|
||||
" \"metadata\": {" +
|
||||
" \"created-by\":\"I am legit, trust me, no-one lies on internets!\"" +
|
||||
" },\n" +
|
||||
" \"security\": {\n" +
|
||||
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
|
||||
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
|
||||
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
|
||||
" \"algorithm\": \"v1\"\n" +
|
||||
" },\n" +
|
||||
" \"format\": \"aaps_encrypted\",\n" +
|
||||
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
|
||||
"}"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
|
||||
|
||||
// contents were not tampered and we can decrypt them
|
||||
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(2))
|
||||
|
||||
// but checksum fails on metadata, so overall security fails
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tamperedContentsTest() {
|
||||
val frozenPrefs = "{\n" +
|
||||
" \"metadata\": {},\n" +
|
||||
" \"security\": {\n" +
|
||||
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
|
||||
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
|
||||
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760a\",\n" +
|
||||
" \"algorithm\": \"v1\"\n" +
|
||||
" },\n" +
|
||||
" \"format\": \"aaps_encrypted\",\n" +
|
||||
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
|
||||
"}"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
|
||||
|
||||
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0))
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingFieldsTest() {
|
||||
val frozenPrefs = "{\n" +
|
||||
" \"format\": \"aaps_encrypted\",\n" +
|
||||
" \"content\": \"lets get rid of metadata and security!\"\n" +
|
||||
"}"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
val prefs = encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
|
||||
|
||||
Assert.assertThat(prefs.values.size, CoreMatchers.`is`(0))
|
||||
Assert.assertThat(prefs.metadata[PrefsMetadataKey.FILE_FORMAT]?.status, CoreMatchers.`is`(PrefsStatus.ERROR))
|
||||
}
|
||||
|
||||
@Test(expected = PrefFormatError::class)
|
||||
fun garbageInputTest() {
|
||||
val frozenPrefs = "whatever man, i duno care"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
|
||||
}
|
||||
|
||||
@Test(expected = PrefFormatError::class)
|
||||
fun unknownFormatTest() {
|
||||
val frozenPrefs = "{\n" +
|
||||
" \"metadata\": {},\n" +
|
||||
" \"security\": {\n" +
|
||||
" \"salt\": \"9581d7a9e56d8127ad6b74a876fa60b192b1c6f4343d857bc07e3874589f2fc9\",\n" +
|
||||
" \"file_hash\": \"9122fd04a4938030b62f6b9d6dda63a11c265e673c4aecbcb6dcd62327c025bb\",\n" +
|
||||
" \"content_hash\": \"23f999f6e6d325f649b61871fe046a94e110bf1587ff070fb66a0f8085b2760c\",\n" +
|
||||
" \"algorithm\": \"v1\"\n" +
|
||||
" },\n" +
|
||||
" \"format\": \"aaps_9000_new_format\",\n" +
|
||||
" \"content\": \"DJ5+HP/gq7icRQhbG9PEBJCMuNwBssIytfEQPCNkzn7PHMfMZuc09vYQg3qzFkmULLiotg==\"\n" +
|
||||
"}"
|
||||
|
||||
val storage = SingleStringStorage(frozenPrefs)
|
||||
val encryptedFormat = EncryptedPrefsFormat(resourceHelper, storage)
|
||||
encryptedFormat.loadPreferences(AAPSMocker.getMockedFile(), "sikret")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package info.nightscout.androidaps.testing.mockers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import info.nightscout.androidaps.MainApp;
|
||||
import info.nightscout.androidaps.testing.mocks.SharedPreferencesMock;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
|
||||
public class AAPSMocker {
|
||||
|
||||
private static final Map<String, SharedPreferences> mockedSharedPrefs = new HashMap<>();
|
||||
|
||||
public static void prepareMock() throws Exception {
|
||||
Context mockedContext = mock(Context.class);
|
||||
mockStatic(MainApp.class, InvocationOnMock::callRealMethod);
|
||||
|
||||
PowerMockito.when(mockedContext, "getSharedPreferences", ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()).thenAnswer(invocation -> {
|
||||
|
||||
final String key = invocation.getArgument(0);
|
||||
if (mockedSharedPrefs.containsKey(key)) {
|
||||
return mockedSharedPrefs.get(key);
|
||||
} else {
|
||||
SharedPreferencesMock newPrefs = new SharedPreferencesMock();
|
||||
mockedSharedPrefs.put(key, newPrefs);
|
||||
return newPrefs;
|
||||
}
|
||||
});
|
||||
|
||||
resetMockedSharedPrefs();
|
||||
}
|
||||
|
||||
public static void resetMockedSharedPrefs() {
|
||||
mockedSharedPrefs.clear();
|
||||
}
|
||||
|
||||
public static File getMockedFile() {
|
||||
File file = mock(File.class);
|
||||
when(file.exists()).thenReturn(true);
|
||||
when(file.canRead()).thenReturn(true);
|
||||
when(file.canWrite()).thenReturn(true);
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package info.nightscout.androidaps.testing.mocks;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class SharedPreferencesMock implements SharedPreferences {
|
||||
|
||||
private final EditorInternals editor = new EditorInternals();
|
||||
|
||||
class EditorInternals implements Editor {
|
||||
|
||||
Map<String, Object> innerMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Editor putString(String k, @Nullable String v) {
|
||||
innerMap.put(k, v);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putStringSet(String k, @Nullable Set<String> set) {
|
||||
innerMap.put(k, set);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putInt(String k, int i) {
|
||||
innerMap.put(k, i);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putLong(String k, long l) {
|
||||
innerMap.put(k, l);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putFloat(String k, float v) {
|
||||
innerMap.put(k, v);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putBoolean(String k, boolean b) {
|
||||
innerMap.put(k, b);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor remove(String k) {
|
||||
innerMap.remove(k);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor clear() {
|
||||
innerMap.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return editor.innerMap;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getString(String k, @Nullable String s) {
|
||||
if (editor.innerMap.containsKey(k)) {
|
||||
return (String) editor.innerMap.get(k);
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Set<String> getStringSet(String k, @Nullable Set<String> set) {
|
||||
if (editor.innerMap.containsKey(k)) {
|
||||
return (Set<String>) editor.innerMap.get(k);
|
||||
} else {
|
||||
return set;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String k, int i) {
|
||||
if (editor.innerMap.containsKey(k)) {
|
||||
return (Integer) editor.innerMap.get(k);
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String k, long l) {
|
||||
if (editor.innerMap.containsKey(k)) {
|
||||
return (Long) editor.innerMap.get(k);
|
||||
} else {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String k, float v) {
|
||||
if (editor.innerMap.containsKey(k)) {
|
||||
return (Float) editor.innerMap.get(k);
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String k, boolean b) {
|
||||
if (editor.innerMap.containsKey(k)) {
|
||||
return (Boolean) editor.innerMap.get(k);
|
||||
} else {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String k) {
|
||||
return editor.innerMap.containsKey(k);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return editor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package info.nightscout.androidaps.testing.utils
|
||||
|
||||
import info.nightscout.androidaps.utils.storage.Storage
|
||||
import java.io.File
|
||||
|
||||
class SingleStringStorage : Storage {
|
||||
|
||||
var contents: String = ""
|
||||
|
||||
constructor(contents: String) {
|
||||
this.contents = contents
|
||||
}
|
||||
|
||||
override fun getFileContents(file: File): String {
|
||||
return contents
|
||||
}
|
||||
|
||||
override fun putFileContents(file: File, putContents: String) {
|
||||
contents = putContents
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return contents
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue