From b95f88295691bdebf4a5b3799d6c6a6f200ecb0c Mon Sep 17 00:00:00 2001 From: Roumen Georgiev Date: Mon, 30 Mar 2020 15:53:12 +0300 Subject: [PATCH 01/14] initial work --- .../general/automation/AutomationPlugin.kt | 3 +- .../automation/elements/DropdownMenu.java | 95 ++++++++ .../automation/triggers/TriggerBTDevice.java | 227 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + .../triggers/TriggerBTDeviceTest.java | 124 ++++++++++ 5 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/DropdownMenu.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDevice.java create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDeviceTest.java diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt index 0d220c2dd4..63606806e4 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.kt @@ -228,7 +228,8 @@ object AutomationPlugin : PluginBase(PluginDescription() TriggerLocation(), TriggerAutosensValue(), TriggerBolusAgo(), - TriggerPumpLastConnection() + TriggerPumpLastConnection(), + TriggerBTDevice() ) } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/DropdownMenu.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/DropdownMenu.java new file mode 100644 index 0000000000..072660ccd5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/DropdownMenu.java @@ -0,0 +1,95 @@ +package info.nightscout.androidaps.plugins.general.automation.elements; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Spinner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.logging.L; + +public class DropdownMenu extends Element { + private static Logger log = LoggerFactory.getLogger(L.AUTOMATION); + private ArrayList itemList; + private String selected; + + public DropdownMenu(String name) { + super(); + this.selected = name; + } + + public DropdownMenu(DropdownMenu another) { + super(); + selected = another.getValue(); + } + + + @Override + public void addToLayout(LinearLayout root) { + if (itemList == null) { + log.error("ItemList is empty!"); + itemList = new ArrayList<>(); + } + ArrayAdapter adapter = new ArrayAdapter<>(root.getContext(), + R.layout.spinner_centered, itemList); + Spinner spinner = new Spinner(root.getContext()); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + spinnerParams.setMargins(0, MainApp.dpToPx(4), 0, MainApp.dpToPx(4)); + spinner.setLayoutParams(spinnerParams); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + setValue(itemList.get(position).toString()); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + spinner.setSelection(0); + LinearLayout l = new LinearLayout(root.getContext()); + l.setOrientation(LinearLayout.VERTICAL); + l.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + l.addView(spinner); + root.addView(l); + } + + public DropdownMenu setValue(String name) { + this.selected = name; + return this; + } + + public String getValue() { + return selected; + } + + public void setList(ArrayList values){ + if (itemList == null) + itemList = new ArrayList<>(); + log.debug("values size is "+values.size()); + itemList = new ArrayList<>(values); + log.debug("items size is "+itemList.size()); + } + + // For testing only + public void add(String item){ + if (itemList == null) + itemList = new ArrayList<>(); + itemList.add(item); + log.debug("Added " + item + "("+itemList.size()+")"); + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDevice.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDevice.java new file mode 100644 index 0000000000..24dafada91 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDevice.java @@ -0,0 +1,227 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.widget.LinearLayout; + +import androidx.fragment.app.FragmentManager; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Set; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.plugins.general.automation.elements.ComparatorExists; +import info.nightscout.androidaps.plugins.general.automation.elements.InputString; +import info.nightscout.androidaps.plugins.general.automation.elements.LayoutBuilder; +import info.nightscout.androidaps.plugins.general.automation.elements.StaticLabel; +import info.nightscout.androidaps.plugins.general.automation.elements.DropdownMenu; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.JsonHelper; +import info.nightscout.androidaps.utils.T; + +public class TriggerBTDevice extends Trigger { + private static Logger log = LoggerFactory.getLogger(L.AUTOMATION); + + private InputString deviceName = new InputString(); + DropdownMenu listOfDevices = new DropdownMenu(""); + ComparatorExists comparator = new ComparatorExists(); + private boolean connectedToDevice = false; + + public TriggerBTDevice() { + super(); + } + + private TriggerBTDevice(TriggerBTDevice TriggerBTDevice) { + super(); + deviceName.setValue(TriggerBTDevice.deviceName.getValue()); + comparator = new ComparatorExists(TriggerBTDevice.comparator); + connectedToDevice = TriggerBTDevice.connectedToDevice; + listOfDevices.setList(devicesPaired()); + lastRun = TriggerBTDevice.lastRun; + } + + public ComparatorExists getComparator() { + return comparator; + } + + @Override + public synchronized boolean shouldRun() { + log.debug("Connected "+connectedToDevice+"! Time left "+ (5 - T.msecs(DateUtil.now()-lastRun).mins())); + if (lastRun > DateUtil.now() - T.mins(5).msecs()) + return false; + + checkConnected(); + + if (connectedToDevice && comparator.getValue() == ComparatorExists.Compare.EXISTS) { + if (L.isEnabled(L.AUTOMATION)) + log.debug("Ready for execution: " + friendlyDescription()); + return true; + } + + return false; + } + + @Override + public synchronized String toJSON() { + JSONObject o = new JSONObject(); + try { + o.put("type", TriggerBTDevice.class.getName()); + JSONObject data = new JSONObject(); + data.put("lastRun", lastRun); + data.put("comparator", comparator.getValue().toString()); + if (!deviceName.getValue().equals("")) + data.put("name", deviceName.getValue()); + else + data.put("name", listOfDevices.getValue()); + + o.put("data", data); + } catch (JSONException e) { + log.error("Unhandled exception", e); + } + log.debug("JSON saved "+o.toString()); + return o.toString(); + } + + @Override + Trigger fromJSON(String data) { + try { + JSONObject d = new JSONObject(data); + lastRun = JsonHelper.safeGetLong(d, "lastRun"); + deviceName.setValue(JsonHelper.safeGetString(d, "name")); + listOfDevices.setList(devicesPaired()); + comparator.setValue(ComparatorExists.Compare.valueOf(JsonHelper.safeGetString(d, "comparator"))); + } catch (Exception e) { + log.error("Unhandled exception", e); + } + return this; + } + + @Override + public int friendlyName() { + return R.string.btdevice; + } + + @Override + public String friendlyDescription() { + return MainApp.gs(R.string.btdevicecompared, deviceName.getValue(), MainApp.gs(comparator.getValue().getStringRes())); + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.ic_bluetooth_white_48dp); + } + + @Override + public Trigger duplicate() { + return new TriggerBTDevice(this); + } + + TriggerBTDevice lastRun(long lastRun) { + this.lastRun = lastRun; + return this; + } + + public TriggerBTDevice comparator(ComparatorExists.Compare compare) { + this.comparator = new ComparatorExists().setValue(compare); + return this; + } + + @Override + public void generateDialog(LinearLayout root, FragmentManager fragmentManager) { + ArrayList pairedDevices = devicesPaired(); + if (pairedDevices.size() == 0) { + // No list of paired devices comming from BT adapter -> show a text input + new LayoutBuilder() + .add(new StaticLabel(R.string.btdevice)) + .add(deviceName) + .add(comparator) + .build(root); + } else { + listOfDevices.setList(pairedDevices); + new LayoutBuilder() + .add(new StaticLabel(R.string.btdevice)) + .add(listOfDevices) + .add(comparator) + .build(root); + } + } + + // Get the list of paired BT devices to use in dropdown menu + private ArrayList devicesPaired(){ + BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + ArrayList s = new ArrayList<>(); + if (mBluetoothAdapter == null) + return s; + Set pairedDevices = mBluetoothAdapter.getBondedDevices(); + + for(BluetoothDevice bt : pairedDevices) { + s.add(bt.getName()); + } + return s; + } + + public void checkConnected() { + BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mBluetoothAdapter == null) + return; + + int state = mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET); // Checks only for connected HEADSET, no other type of BT devices + if (state != BluetoothProfile.STATE_CONNECTED) { + connectedToDevice = false; + return; + } + try + { + Context context = MainApp.instance().getApplicationContext(); + mBluetoothAdapter.getProfileProxy(context, serviceListener, BluetoothProfile.STATE_CONNECTED); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + private BluetoothProfile.ServiceListener serviceListener = new BluetoothProfile.ServiceListener() + { + @Override + public void onServiceDisconnected(int profile) + { } + + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) + { + + for (BluetoothDevice device : proxy.getConnectedDevices()) + { + connectedToDevice = deviceName.getValue().equals(device.getName()); + } + + BluetoothAdapter.getDefaultAdapter().closeProfileProxy(profile, proxy); + } + }; + + public void setDeviceName(String newName) { + deviceName.setValue(newName); + } + + public String getDeviceName() { + return deviceName.getValue(); + } + + public void setConnectedState(boolean newstate){ + this.connectedToDevice = newstate; + } + +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a4c79cb63b..49dbd3cc61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1390,6 +1390,8 @@ exists not exists Temp target %1$s + Bluetooth connection to device %1$s %2$s + Connection to Bluetooth device WiFi SSID %1$s %2$s Autosens %1$s %2$s %% Autosens % diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDeviceTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDeviceTest.java new file mode 100644 index 0000000000..ddd7a04805 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBTDeviceTest.java @@ -0,0 +1,124 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothProfile; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import info.AAPSMocker; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; +import info.nightscout.androidaps.plugins.general.automation.elements.ComparatorExists; +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.SP; + +import static org.mockito.ArgumentMatchers.any; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({MainApp.class, ProfileFunctions.class, DateUtil.class, IobCobCalculatorPlugin.class, SP.class, L.class, BluetoothAdapter.class, BluetoothProfile.class}) +public class TriggerBTDeviceTest { + + long now = 1514766900000L; + String someName = "Headset"; + String btJson = "{\"data\":{\"comparator\":\"EXISTS\",\"lastRun\":0,\"name\":\"Headset\"},\"type\":\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerBTDevice\"}"; + + @Before + public void mock() { + AAPSMocker.mockMainApp(); + AAPSMocker.mockProfileFunctions(); + AAPSMocker.mockSP(); + AAPSMocker.mockL(); + + PowerMockito.mockStatic(DateUtil.class); + when(DateUtil.now()).thenReturn(now); + when(SP.getInt(any(), any())).thenReturn(48); + + + } + + @Test + public void comparator() { + TriggerBTDevice t = new TriggerBTDevice().comparator(ComparatorExists.Compare.EXISTS); + Assert.assertEquals(t.comparator.getValue(), ComparatorExists.Compare.EXISTS); + } + + @Test + public void shouldRun() { + BluetoothAdapter btAdapter = PowerMockito.mock(BluetoothAdapter.class); + PowerMockito.mockStatic(BluetoothAdapter.class); + PowerMockito.mockStatic(BluetoothProfile.class); + TriggerBTDevice t = new TriggerBTDevice().comparator(ComparatorExists.Compare.EXISTS); + Assert.assertFalse(t.shouldRun()); // no bluetooth adapter + when(BluetoothAdapter.getDefaultAdapter()).thenReturn(btAdapter); + when(btAdapter.isEnabled()).thenReturn(true); + Assert.assertFalse(t.shouldRun()); // no device connected + when(btAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)).thenReturn(BluetoothProfile.STATE_CONNECTED); + t.setDeviceName(someName); + Assert.assertFalse(t.shouldRun()); // no mocked connection + t.setConnectedState(true); + Assert.assertTrue(t.shouldRun()); + + } + + @Test + public void toJSON() { + TriggerBTDevice t = new TriggerBTDevice().comparator(ComparatorExists.Compare.EXISTS); + t.setDeviceName(someName); + Assert.assertEquals(btJson, t.toJSON()); + } + + @Test + public void fromJSON() throws JSONException { + TriggerBTDevice t = new TriggerBTDevice().comparator(ComparatorExists.Compare.EXISTS); + t.setDeviceName(someName); + TriggerBTDevice t2 = (TriggerBTDevice) Trigger.instantiate(new JSONObject(t.toJSON())); + Assert.assertEquals(ComparatorExists.Compare.EXISTS, t2.getComparator().getValue()); + Assert.assertEquals("Headset", t2.getDeviceName()); + } + + @Test + public void friendlyName() { + } + + @Test + public void friendlyDescription() { + } + + @Test + public void icon() { + Assert.assertEquals(Optional.of(R.drawable.ic_bluetooth_white_48dp), new TriggerBTDevice().icon()); + } + + @Test + public void duplicate() { + TriggerBTDevice t = new TriggerBTDevice().comparator(ComparatorExists.Compare.EXISTS); + t.setDeviceName(someName); + TriggerBTDevice t1 = (TriggerBTDevice) t.duplicate(); + Assert.assertEquals("Headset", t1.getDeviceName()); + Assert.assertEquals(ComparatorExists.Compare.EXISTS, t.getComparator().getValue()); + } + + @Test + public void lastRun() { + TriggerBTDevice t = new TriggerBTDevice().lastRun(now); + Assert.assertEquals(now, t.lastRun); + } + + @Test + public void generateDialog() { + } +} \ No newline at end of file From 56d0d2850d632029e2c68414b8160852acceb226 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 31 Mar 2020 21:54:49 +0200 Subject: [PATCH 02/14] remove some dependencies --- .../plugins/pump/medtronic/MedtronicFragment.kt | 14 +++++++++++--- .../pump/medtronic/comm/ui/MedtronicUIComm.java | 11 ++--------- .../service/RileyLinkMedtronicService.java | 5 ----- .../plugins/pump/medtronic/util/MedtronicUtil.java | 11 ----------- .../plugins/treatments/TreatmentsPlugin.java | 3 ++- 5 files changed, 15 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicFragment.kt index ad1d1054f6..05ff1cf463 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicFragment.kt @@ -37,6 +37,7 @@ import info.nightscout.androidaps.queue.Callback import info.nightscout.androidaps.queue.events.EventQueueChanged import info.nightscout.androidaps.utils.DateUtil import info.nightscout.androidaps.utils.FabricPrivacy +import info.nightscout.androidaps.utils.OKDialog import info.nightscout.androidaps.utils.SetWarnColor import info.nightscout.androidaps.utils.T import info.nightscout.androidaps.utils.extensions.plusAssign @@ -86,13 +87,13 @@ class MedtronicFragment : DaggerFragment() { if (MedtronicUtil.getPumpStatus().verifyConfiguration()) { startActivity(Intent(context, MedtronicHistoryActivity::class.java)) } else { - MedtronicUtil.displayNotConfiguredDialog(context) + displayNotConfiguredDialog() } } medtronic_refresh.setOnClickListener { if (!MedtronicUtil.getPumpStatus().verifyConfiguration()) { - MedtronicUtil.displayNotConfiguredDialog(context) + displayNotConfiguredDialog() } else { medtronic_refresh.isEnabled = false medtronicPumpPlugin.resetStatusState() @@ -108,7 +109,7 @@ class MedtronicFragment : DaggerFragment() { if (MedtronicUtil.getPumpStatus().verifyConfiguration()) { startActivity(Intent(context, RileyLinkStatusActivity::class.java)) } else { - MedtronicUtil.displayNotConfiguredDialog(context) + displayNotConfiguredDialog() } } } @@ -247,6 +248,13 @@ class MedtronicFragment : DaggerFragment() { } } + private fun displayNotConfiguredDialog() { + context?.let { + OKDialog.show(it, resourceHelper.gs(R.string.combo_warning), + resourceHelper.gs(R.string.medtronic_error_operation_not_possible_no_configuration), null) + } + } + // GUI functions @Synchronized fun updateGUI() { diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/ui/MedtronicUIComm.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/ui/MedtronicUIComm.java index b2a5c97233..b4cebbb1cf 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/ui/MedtronicUIComm.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/ui/MedtronicUIComm.java @@ -1,7 +1,6 @@ package info.nightscout.androidaps.plugins.pump.medtronic.comm.ui; import info.nightscout.androidaps.logging.AAPSLogger; -import info.nightscout.androidaps.logging.L; import info.nightscout.androidaps.logging.LTag; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkConst; @@ -43,8 +42,7 @@ public class MedtronicUIComm { public synchronized MedtronicUITask executeCommand(MedtronicCommandType commandType, Object... parameters) { - if (isLogEnabled()) - aapsLogger.warn(LTag.PUMP, "Execute Command: " + commandType.name()); + aapsLogger.warn(LTag.PUMP, "Execute Command: " + commandType.name()); MedtronicUITask task = new MedtronicUITask(commandType, parameters); @@ -78,7 +76,7 @@ public class MedtronicUIComm { // } // } - if (!task.isReceived() && isLogEnabled()) { + if (!task.isReceived()) { aapsLogger.warn(LTag.PUMP, "Reply not received for " + commandType); } @@ -112,9 +110,4 @@ public class MedtronicUIComm { public void startTunning() { RileyLinkUtil.sendBroadcastMessage(RileyLinkConst.IPC.MSG_PUMP_tunePump); } - - private boolean isLogEnabled() { - return L.isEnabled(L.PUMP); - } - } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java index 84beb5871d..c3e403f723 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/service/RileyLinkMedtronicService.java @@ -64,11 +64,6 @@ public class RileyLinkMedtronicService extends RileyLinkService { } - public static MedtronicCommunicationManager getCommunicationManager() { - return instance.medtronicCommunicationManager; - } - - @Override public void onConfigurationChanged(Configuration newConfig) { aapsLogger.warn(LTag.PUMPCOMM, "onConfigurationChanged"); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicUtil.java b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicUtil.java index 2ac9d4ef56..7f02a42d21 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicUtil.java @@ -1,13 +1,10 @@ package info.nightscout.androidaps.plugins.pump.medtronic.util; -import android.content.Context; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.joda.time.LocalTime; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -15,8 +12,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import info.nightscout.androidaps.MainApp; -import info.nightscout.androidaps.R; import info.nightscout.androidaps.interfaces.PluginType; import info.nightscout.androidaps.logging.L; import info.nightscout.androidaps.logging.StacktraceLoggerWrapper; @@ -41,7 +36,6 @@ import info.nightscout.androidaps.plugins.pump.medtronic.defs.PumpDeviceState; import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus; import info.nightscout.androidaps.plugins.pump.medtronic.events.EventMedtronicDeviceStatusChange; import info.nightscout.androidaps.plugins.pump.medtronic.service.RileyLinkMedtronicService; -import info.nightscout.androidaps.utils.OKDialog; import info.nightscout.androidaps.utils.resources.ResourceHelper; /** @@ -532,9 +526,4 @@ public class MedtronicUtil extends RileyLinkUtil { } - public static void displayNotConfiguredDialog(Context context) { - OKDialog.show(context, MainApp.gs(R.string.combo_warning), - MainApp.gs(R.string.medtronic_error_operation_not_possible_no_configuration), null); - } - } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java index 6615e47e22..9704d82f43 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/treatments/TreatmentsPlugin.java @@ -54,6 +54,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; import info.nightscout.androidaps.plugins.general.overview.events.EventDismissNotification; import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensResult; +import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; import info.nightscout.androidaps.plugins.pump.medtronic.data.MedtronicHistoryData; import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil; import info.nightscout.androidaps.utils.DateUtil; @@ -571,7 +572,7 @@ public class TreatmentsPlugin extends PluginBase implements TreatmentsInterface // return true if new record is created @Override public boolean addToHistoryTreatment(DetailedBolusInfo detailedBolusInfo, boolean allowUpdate) { - boolean medtronicPump = MedtronicUtil.isMedtronicPump(); + boolean medtronicPump = activePlugin.getActivePump() instanceof MedtronicPumpPlugin; getAapsLogger().debug(MedtronicHistoryData.doubleBolusDebug, LTag.DATATREATMENTS, "DoubleBolusDebug: addToHistoryTreatment::isMedtronicPump={} " + medtronicPump); From bc5a089c5f89dd5a2467bd08924fa9e147ea4f28 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 1 Apr 2020 09:07:36 +0200 Subject: [PATCH 03/14] GraphData refactor --- .../historyBrowser/HistoryBrowseActivity.java | 2 +- .../general/overview/OverviewFragment.java | 2 +- .../general/overview/graphData/GraphData.java | 726 ------------------ .../general/overview/graphData/GraphData.kt | 569 ++++++++++++++ .../graphExtensions/ScaledDataPoint.java | 6 + 5 files changed, 577 insertions(+), 728 deletions(-) delete mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.kt diff --git a/app/src/main/java/info/nightscout/androidaps/historyBrowser/HistoryBrowseActivity.java b/app/src/main/java/info/nightscout/androidaps/historyBrowser/HistoryBrowseActivity.java index 3c081e3309..6a47e3749b 100644 --- a/app/src/main/java/info/nightscout/androidaps/historyBrowser/HistoryBrowseActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/historyBrowser/HistoryBrowseActivity.java @@ -303,7 +303,7 @@ public class HistoryBrowseActivity extends NoSplashAppCompatActivity { // add basal data if (pump.getPumpDescription().isTempBasalCapable && showBasal) { - graphData.addBasals(fromTime, toTime, lowLine / graphData.maxY / 1.2d); + graphData.addBasals(fromTime, toTime, lowLine / graphData.getMaxY() / 1.2d); } // **** NOW line **** diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.java index eef9521d09..a3d86716bb 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/OverviewFragment.java @@ -1463,7 +1463,7 @@ public class OverviewFragment extends DaggerFragment implements View.OnClickList // add basal data if (pump.getPumpDescription().isTempBasalCapable && sp.getBoolean("showbasals", true)) { - graphData.addBasals(fromTime, now, lowLine / graphData.maxY / 1.2d); + graphData.addBasals(fromTime, now, lowLine / graphData.getMaxY() / 1.2d); } // add target line diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.java deleted file mode 100644 index bcb11363c8..0000000000 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.java +++ /dev/null @@ -1,726 +0,0 @@ -package info.nightscout.androidaps.plugins.general.overview.graphData; - -import android.graphics.Color; -import android.graphics.DashPathEffect; -import android.graphics.Paint; - -import com.jjoe64.graphview.GraphView; -import com.jjoe64.graphview.series.BarGraphSeries; -import com.jjoe64.graphview.series.DataPoint; -import com.jjoe64.graphview.series.LineGraphSeries; -import com.jjoe64.graphview.series.Series; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.inject.Inject; - -import dagger.android.HasAndroidInjector; -import info.nightscout.androidaps.Constants; -import info.nightscout.androidaps.MainApp; -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.data.IobTotal; -import info.nightscout.androidaps.data.Profile; -import info.nightscout.androidaps.db.BgReading; -import info.nightscout.androidaps.db.CareportalEvent; -import info.nightscout.androidaps.db.ExtendedBolus; -import info.nightscout.androidaps.db.ProfileSwitch; -import info.nightscout.androidaps.db.TempTarget; -import info.nightscout.androidaps.interfaces.ActivePluginProvider; -import info.nightscout.androidaps.interfaces.TreatmentsInterface; -import info.nightscout.androidaps.logging.AAPSLogger; -import info.nightscout.androidaps.logging.LTag; -import info.nightscout.androidaps.plugins.aps.loop.APSResult; -import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; -import info.nightscout.androidaps.plugins.aps.openAPSSMB.SMBDefaults; -import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.AreaGraphSeries; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.DataPointWithLabelInterface; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.DoubleDataPoint; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.FixedLineGraphSeries; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.PointsWithLabelGraphSeries; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.Scale; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.ScaledDataPoint; -import info.nightscout.androidaps.plugins.general.overview.graphExtensions.TimeAsXAxisLabelFormatter; -import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensData; -import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensResult; -import info.nightscout.androidaps.plugins.iob.iobCobCalculator.BasalData; -import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin; -import info.nightscout.androidaps.plugins.treatments.Treatment; -import info.nightscout.androidaps.utils.DecimalFormatter; -import info.nightscout.androidaps.utils.Round; -import info.nightscout.androidaps.utils.resources.ResourceHelper; - -/** - * Created by mike on 18.10.2017. - */ - -public class GraphData { - - @Inject AAPSLogger aapsLogger; - @Inject ProfileFunction profileFunction; - @Inject ResourceHelper resourceHelper; - @Inject ActivePluginProvider activePlugin; - - private GraphView graph; - public double maxY = Double.MIN_VALUE; - private double minY = Double.MAX_VALUE; - private List bgReadingsArray; - private String units; - private List series = new ArrayList<>(); - private TreatmentsInterface treatmentsPlugin; - - - private IobCobCalculatorPlugin iobCobCalculatorPlugin; // Cannot be injected: HistoryBrowser - - public GraphData(HasAndroidInjector injector, GraphView graph, IobCobCalculatorPlugin iobCobCalculatorPlugin) { - injector.androidInjector().inject(this); - units = profileFunction.getUnits(); - this.graph = graph; - this.iobCobCalculatorPlugin = iobCobCalculatorPlugin; - treatmentsPlugin = activePlugin.getActiveTreatments(); - } - - public void addBgReadings(long fromTime, long toTime, double lowLine, double highLine, List predictions) { - double maxBgValue = Double.MIN_VALUE; - //bgReadingsArray = MainApp.getDbHelper().getBgreadingsDataFromTime(fromTime, true); - bgReadingsArray = iobCobCalculatorPlugin.getBgReadings(); - List bgListArray = new ArrayList<>(); - - if (bgReadingsArray == null || bgReadingsArray.size() == 0) { - aapsLogger.debug(LTag.OVERVIEW, "No BG data."); - maxY = 10; - minY = 0; - return; - } - - for (BgReading bg : bgReadingsArray) { - if (bg.date < fromTime || bg.date > toTime) continue; - if (bg.value > maxBgValue) maxBgValue = bg.value; - bgListArray.add(bg); - } - if (predictions != null) { - Collections.sort(predictions, (o1, o2) -> Double.compare(o1.getX(), o2.getX())); - for (BgReading prediction : predictions) { - if (prediction.value >= 40) - bgListArray.add(prediction); - } - } - - maxBgValue = Profile.fromMgdlToUnits(maxBgValue, units); - maxBgValue = units.equals(Constants.MGDL) ? Round.roundTo(maxBgValue, 40d) + 80 : Round.roundTo(maxBgValue, 2d) + 4; - if (highLine > maxBgValue) maxBgValue = highLine; - int numOfVertLines = units.equals(Constants.MGDL) ? (int) (maxBgValue / 40 + 1) : (int) (maxBgValue / 2 + 1); - - DataPointWithLabelInterface[] bg = new DataPointWithLabelInterface[bgListArray.size()]; - bg = bgListArray.toArray(bg); - - - maxY = maxBgValue; - minY = 0; - // set manual y bounds to have nice steps - graph.getGridLabelRenderer().setNumVerticalLabels(numOfVertLines); - - addSeries(new PointsWithLabelGraphSeries<>(bg)); - } - - public void addInRangeArea(long fromTime, long toTime, double lowLine, double highLine) { - AreaGraphSeries inRangeAreaSeries; - - DoubleDataPoint[] inRangeAreaDataPoints = new DoubleDataPoint[]{ - new DoubleDataPoint(fromTime, lowLine, highLine), - new DoubleDataPoint(toTime, lowLine, highLine) - }; - inRangeAreaSeries = new AreaGraphSeries<>(inRangeAreaDataPoints); - inRangeAreaSeries.setColor(0); - inRangeAreaSeries.setDrawBackground(true); - inRangeAreaSeries.setBackgroundColor(resourceHelper.gc(R.color.inrangebackground)); - - addSeries(inRangeAreaSeries); - } - - // scale in % of vertical size (like 0.3) - public void addBasals(long fromTime, long toTime, double scale) { - LineGraphSeries basalsLineSeries; - LineGraphSeries absoluteBasalsLineSeries; - LineGraphSeries baseBasalsSeries; - LineGraphSeries tempBasalsSeries; - - double maxBasalValueFound = 0d; - Scale basalScale = new Scale(); - - List baseBasalArray = new ArrayList<>(); - List tempBasalArray = new ArrayList<>(); - List basalLineArray = new ArrayList<>(); - List absoluteBasalLineArray = new ArrayList<>(); - double lastLineBasal = 0; - double lastAbsoluteLineBasal = -1; - double lastBaseBasal = 0; - double lastTempBasal = 0; - for (long time = fromTime; time < toTime; time += 60 * 1000L) { - Profile profile = profileFunction.getProfile(time); - if (profile == null) continue; - BasalData basalData = iobCobCalculatorPlugin.getBasalData(profile, time); - double baseBasalValue = basalData.basal; - double absoluteLineValue = baseBasalValue; - double tempBasalValue = 0; - double basal = 0d; - if (basalData.isTempBasalRunning) { - absoluteLineValue = tempBasalValue = basalData.tempBasalAbsolute; - if (tempBasalValue != lastTempBasal) { - tempBasalArray.add(new ScaledDataPoint(time, lastTempBasal, basalScale)); - tempBasalArray.add(new ScaledDataPoint(time, basal = tempBasalValue, basalScale)); - } - if (lastBaseBasal != 0d) { - baseBasalArray.add(new ScaledDataPoint(time, lastBaseBasal, basalScale)); - baseBasalArray.add(new ScaledDataPoint(time, 0d, basalScale)); - lastBaseBasal = 0d; - } - } else { - if (baseBasalValue != lastBaseBasal) { - baseBasalArray.add(new ScaledDataPoint(time, lastBaseBasal, basalScale)); - baseBasalArray.add(new ScaledDataPoint(time, basal = baseBasalValue, basalScale)); - lastBaseBasal = baseBasalValue; - } - if (lastTempBasal != 0) { - tempBasalArray.add(new ScaledDataPoint(time, lastTempBasal, basalScale)); - tempBasalArray.add(new ScaledDataPoint(time, 0d, basalScale)); - } - } - - if (baseBasalValue != lastLineBasal) { - basalLineArray.add(new ScaledDataPoint(time, lastLineBasal, basalScale)); - basalLineArray.add(new ScaledDataPoint(time, baseBasalValue, basalScale)); - } - if (absoluteLineValue != lastAbsoluteLineBasal) { - absoluteBasalLineArray.add(new ScaledDataPoint(time, lastAbsoluteLineBasal, basalScale)); - absoluteBasalLineArray.add(new ScaledDataPoint(time, basal, basalScale)); - } - - lastAbsoluteLineBasal = absoluteLineValue; - lastLineBasal = baseBasalValue; - lastTempBasal = tempBasalValue; - maxBasalValueFound = Math.max(maxBasalValueFound, Math.max(tempBasalValue, baseBasalValue)); - } - - basalLineArray.add(new ScaledDataPoint(toTime, lastLineBasal, basalScale)); - baseBasalArray.add(new ScaledDataPoint(toTime, lastBaseBasal, basalScale)); - tempBasalArray.add(new ScaledDataPoint(toTime, lastTempBasal, basalScale)); - absoluteBasalLineArray.add(new ScaledDataPoint(toTime, lastAbsoluteLineBasal, basalScale)); - - ScaledDataPoint[] baseBasal = new ScaledDataPoint[baseBasalArray.size()]; - baseBasal = baseBasalArray.toArray(baseBasal); - baseBasalsSeries = new LineGraphSeries<>(baseBasal); - baseBasalsSeries.setDrawBackground(true); - baseBasalsSeries.setBackgroundColor(resourceHelper.gc(R.color.basebasal)); - baseBasalsSeries.setThickness(0); - - ScaledDataPoint[] tempBasal = new ScaledDataPoint[tempBasalArray.size()]; - tempBasal = tempBasalArray.toArray(tempBasal); - tempBasalsSeries = new LineGraphSeries<>(tempBasal); - tempBasalsSeries.setDrawBackground(true); - tempBasalsSeries.setBackgroundColor(resourceHelper.gc(R.color.tempbasal)); - tempBasalsSeries.setThickness(0); - - ScaledDataPoint[] basalLine = new ScaledDataPoint[basalLineArray.size()]; - basalLine = basalLineArray.toArray(basalLine); - basalsLineSeries = new LineGraphSeries<>(basalLine); - Paint paint = new Paint(); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(resourceHelper.getDisplayMetrics().scaledDensity * 2); - paint.setPathEffect(new DashPathEffect(new float[]{2, 4}, 0)); - paint.setColor(resourceHelper.gc(R.color.basal)); - basalsLineSeries.setCustomPaint(paint); - - ScaledDataPoint[] absoluteBasalLine = new ScaledDataPoint[absoluteBasalLineArray.size()]; - absoluteBasalLine = absoluteBasalLineArray.toArray(absoluteBasalLine); - absoluteBasalsLineSeries = new LineGraphSeries<>(absoluteBasalLine); - Paint absolutePaint = new Paint(); - absolutePaint.setStyle(Paint.Style.STROKE); - absolutePaint.setStrokeWidth(resourceHelper.getDisplayMetrics().scaledDensity * 2); - absolutePaint.setColor(resourceHelper.gc(R.color.basal)); - absoluteBasalsLineSeries.setCustomPaint(absolutePaint); - - basalScale.setMultiplier(maxY * scale / maxBasalValueFound); - - addSeries(baseBasalsSeries); - addSeries(tempBasalsSeries); - addSeries(basalsLineSeries); - addSeries(absoluteBasalsLineSeries); - } - - public void addTargetLine(long fromTime, long toTime, Profile profile, LoopPlugin.LastRun lastRun) { - LineGraphSeries targetsSeries; - - Scale targetsScale = new Scale(); - targetsScale.setMultiplier(1); - - List targetsSeriesArray = new ArrayList<>(); - double lastTarget = -1; - - if (lastRun != null && lastRun.constraintsProcessed != null) { - APSResult apsResult = lastRun.constraintsProcessed; - long latestPredictionsTime = apsResult.getLatestPredictionsTime(); - if (latestPredictionsTime > toTime) { - toTime = latestPredictionsTime; - } - } - - for (long time = fromTime; time < toTime; time += 5 * 60 * 1000L) { - TempTarget tt = treatmentsPlugin.getTempTargetFromHistory(time); - double value; - if (tt == null) { - value = Profile.fromMgdlToUnits((profile.getTargetLowMgdl(time) + profile.getTargetHighMgdl(time)) / 2, profileFunction.getUnits()); - } else { - value = Profile.fromMgdlToUnits(tt.target(), profileFunction.getUnits()); - } - if (lastTarget != value) { - if (lastTarget != -1) - targetsSeriesArray.add(new DataPoint(time, lastTarget)); - targetsSeriesArray.add(new DataPoint(time, value)); - } - lastTarget = value; - } - targetsSeriesArray.add(new DataPoint(toTime, lastTarget)); - - DataPoint[] targets = new DataPoint[targetsSeriesArray.size()]; - targets = targetsSeriesArray.toArray(targets); - targetsSeries = new LineGraphSeries<>(targets); - targetsSeries.setDrawBackground(false); - targetsSeries.setColor(resourceHelper.gc(R.color.tempTargetBackground)); - targetsSeries.setThickness(2); - - addSeries(targetsSeries); - } - - public void addTreatments(long fromTime, long endTime) { - List filteredTreatments = new ArrayList<>(); - - List treatments = treatmentsPlugin.getTreatmentsFromHistory(); - - for (int tx = 0; tx < treatments.size(); tx++) { - Treatment t = treatments.get(tx); - if (t.getX() < fromTime || t.getX() > endTime) continue; - if (t.isSMB && !t.isValid) continue; - t.setY(getNearestBg((long) t.getX())); - filteredTreatments.add(t); - } - - // ProfileSwitch - List profileSwitches = treatmentsPlugin.getProfileSwitchesFromHistory().getList(); - - for (int tx = 0; tx < profileSwitches.size(); tx++) { - DataPointWithLabelInterface t = profileSwitches.get(tx); - if (t.getX() < fromTime || t.getX() > endTime) continue; - filteredTreatments.add(t); - } - - // Extended bolus - if (!activePlugin.getActivePump().isFakingTempsByExtendedBoluses()) { - List extendedBoluses = treatmentsPlugin.getExtendedBolusesFromHistory().getList(); - - for (int tx = 0; tx < extendedBoluses.size(); tx++) { - DataPointWithLabelInterface t = extendedBoluses.get(tx); - if (t.getX() + t.getDuration() < fromTime || t.getX() > endTime) continue; - if (t.getDuration() == 0) continue; - t.setY(getNearestBg((long) t.getX())); - filteredTreatments.add(t); - } - } - - // Careportal - List careportalEvents = MainApp.getDbHelper().getCareportalEventsFromTime(fromTime - 6 * 60 * 60 * 1000, true); - - for (int tx = 0; tx < careportalEvents.size(); tx++) { - DataPointWithLabelInterface t = careportalEvents.get(tx); - if (t.getX() + t.getDuration() < fromTime || t.getX() > endTime) continue; - t.setY(getNearestBg((long) t.getX())); - filteredTreatments.add(t); - } - - DataPointWithLabelInterface[] treatmentsArray = new DataPointWithLabelInterface[filteredTreatments.size()]; - treatmentsArray = filteredTreatments.toArray(treatmentsArray); - addSeries(new PointsWithLabelGraphSeries<>(treatmentsArray)); - } - - private double getNearestBg(long date) { - if (bgReadingsArray == null) - return Profile.fromMgdlToUnits(100, units); - for (int r = 0; r < bgReadingsArray.size(); r++) { - BgReading reading = bgReadingsArray.get(r); - if (reading.date > date) continue; - return Profile.fromMgdlToUnits(reading.value, units); - } - return bgReadingsArray.size() > 0 - ? Profile.fromMgdlToUnits(bgReadingsArray.get(0).value, units) : Profile.fromMgdlToUnits(100, units); - } - - public void addActivity(long fromTime, long toTime, boolean useForScale, double scale) { - FixedLineGraphSeries actSeriesHist; - List actArrayHist = new ArrayList<>(); - FixedLineGraphSeries actSeriesPred; - List actArrayPred = new ArrayList<>(); - - double now = System.currentTimeMillis(); - Scale actScale = new Scale(); - IobTotal total; - double maxIAValue = 0; - - for (long time = fromTime; time <= toTime; time += 5 * 60 * 1000L) { - Profile profile = profileFunction.getProfile(time); - double act; - if (profile == null) continue; - total = iobCobCalculatorPlugin.calculateFromTreatmentsAndTempsSynchronized(time, profile); - act = total.activity; - - if (time <= now) - actArrayHist.add(new ScaledDataPoint(time, act, actScale)); - else - actArrayPred.add(new ScaledDataPoint(time, act, actScale)); - - maxIAValue = Math.max(maxIAValue, Math.abs(act)); - } - - ScaledDataPoint[] actData = new ScaledDataPoint[actArrayHist.size()]; - actData = actArrayHist.toArray(actData); - actSeriesHist = new FixedLineGraphSeries<>(actData); - actSeriesHist.setDrawBackground(false); - actSeriesHist.setColor(resourceHelper.gc(R.color.activity)); - actSeriesHist.setThickness(3); - - addSeries(actSeriesHist); - - actData = new ScaledDataPoint[actArrayPred.size()]; - actData = actArrayPred.toArray(actData); - actSeriesPred = new FixedLineGraphSeries<>(actData); - - Paint paint = new Paint(); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(3); - paint.setPathEffect(new DashPathEffect(new float[]{4, 4}, 0)); - paint.setColor(resourceHelper.gc(R.color.activity)); - actSeriesPred.setCustomPaint(paint); - - if (useForScale) { - maxY = maxIAValue; - minY = -maxIAValue; - } - actScale.setMultiplier(maxY * scale / maxIAValue); - - addSeries(actSeriesPred); - } - - // scale in % of vertical size (like 0.3) - public void addIob(long fromTime, long toTime, boolean useForScale, double scale, boolean showPrediction) { - FixedLineGraphSeries iobSeries; - List iobArray = new ArrayList<>(); - Double maxIobValueFound = Double.MIN_VALUE; - double lastIob = 0; - Scale iobScale = new Scale(); - - for (long time = fromTime; time <= toTime; time += 5 * 60 * 1000L) { - Profile profile = profileFunction.getProfile(time); - double iob = 0d; - if (profile != null) - iob = iobCobCalculatorPlugin.calculateFromTreatmentsAndTempsSynchronized(time, profile).iob; - if (Math.abs(lastIob - iob) > 0.02) { - if (Math.abs(lastIob - iob) > 0.2) - iobArray.add(new ScaledDataPoint(time, lastIob, iobScale)); - iobArray.add(new ScaledDataPoint(time, iob, iobScale)); - maxIobValueFound = Math.max(maxIobValueFound, Math.abs(iob)); - lastIob = iob; - } - } - - ScaledDataPoint[] iobData = new ScaledDataPoint[iobArray.size()]; - iobData = iobArray.toArray(iobData); - iobSeries = new FixedLineGraphSeries<>(iobData); - iobSeries.setDrawBackground(true); - iobSeries.setBackgroundColor(0x80FFFFFF & resourceHelper.gc(R.color.iob)); //50% - iobSeries.setColor(resourceHelper.gc(R.color.iob)); - iobSeries.setThickness(3); - - if (showPrediction) { - AutosensResult lastAutosensResult; - AutosensData autosensData = iobCobCalculatorPlugin.getLastAutosensDataSynchronized("GraphData"); - if (autosensData == null) - lastAutosensResult = new AutosensResult(); - else - lastAutosensResult = autosensData.autosensResult; - boolean isTempTarget = treatmentsPlugin.getTempTargetFromHistory(System.currentTimeMillis()) != null; - - List iobPred = new ArrayList<>(); - IobTotal[] iobPredArray = iobCobCalculatorPlugin.calculateIobArrayForSMB(lastAutosensResult, SMBDefaults.exercise_mode, SMBDefaults.half_basal_exercise_target, isTempTarget); - for (IobTotal i : iobPredArray) { - iobPred.add(i.setColor(resourceHelper.gc(R.color.iobPredAS))); - maxIobValueFound = Math.max(maxIobValueFound, Math.abs(i.iob)); - } - DataPointWithLabelInterface[] iobp = new DataPointWithLabelInterface[iobPred.size()]; - iobp = iobPred.toArray(iobp); - addSeries(new PointsWithLabelGraphSeries<>(iobp)); - - - List iobPred2 = new ArrayList<>(); - IobTotal[] iobPredArray2 = iobCobCalculatorPlugin.calculateIobArrayForSMB(new AutosensResult(), SMBDefaults.exercise_mode, SMBDefaults.half_basal_exercise_target, isTempTarget); - for (IobTotal i : iobPredArray2) { - iobPred2.add(i.setColor(resourceHelper.gc(R.color.iobPred))); - maxIobValueFound = Math.max(maxIobValueFound, Math.abs(i.iob)); - } - DataPointWithLabelInterface[] iobp2 = new DataPointWithLabelInterface[iobPred2.size()]; - iobp2 = iobPred2.toArray(iobp2); - addSeries(new PointsWithLabelGraphSeries<>(iobp2)); - - aapsLogger.debug(LTag.AUTOSENS, "IOB pred for AS=" + DecimalFormatter.to2Decimal(lastAutosensResult.ratio) + ": " + iobCobCalculatorPlugin.iobArrayToString(iobPredArray)); - aapsLogger.debug(LTag.AUTOSENS, "IOB pred for AS=" + DecimalFormatter.to2Decimal(1) + ": " + iobCobCalculatorPlugin.iobArrayToString(iobPredArray2)); - } - - if (useForScale) { - maxY = maxIobValueFound; - minY = -maxIobValueFound; - } - - iobScale.setMultiplier(maxY * scale / maxIobValueFound); - - addSeries(iobSeries); - } - - // scale in % of vertical size (like 0.3) - public void addCob(long fromTime, long toTime, boolean useForScale, double scale) { - List minFailoverActiveList = new ArrayList<>(); - FixedLineGraphSeries cobSeries; - List cobArray = new ArrayList<>(); - Double maxCobValueFound = 0d; - int lastCob = 0; - Scale cobScale = new Scale(); - - for (long time = fromTime; time <= toTime; time += 5 * 60 * 1000L) { - AutosensData autosensData = iobCobCalculatorPlugin.getAutosensData(time); - if (autosensData != null) { - int cob = (int) autosensData.cob; - if (cob != lastCob) { - if (autosensData.carbsFromBolus > 0) - cobArray.add(new ScaledDataPoint(time, lastCob, cobScale)); - cobArray.add(new ScaledDataPoint(time, cob, cobScale)); - maxCobValueFound = Math.max(maxCobValueFound, cob); - lastCob = cob; - } - if (autosensData.failoverToMinAbsorbtionRate) { - autosensData.setScale(cobScale); - autosensData.setChartTime(time); - minFailoverActiveList.add(autosensData); - } - } - } - - // COB - ScaledDataPoint[] cobData = new ScaledDataPoint[cobArray.size()]; - cobData = cobArray.toArray(cobData); - cobSeries = new FixedLineGraphSeries<>(cobData); - cobSeries.setDrawBackground(true); - cobSeries.setBackgroundColor(0x80FFFFFF & resourceHelper.gc(R.color.cob)); //50% - cobSeries.setColor(resourceHelper.gc(R.color.cob)); - cobSeries.setThickness(3); - - if (useForScale) { - maxY = maxCobValueFound; - minY = 0; - } - - cobScale.setMultiplier(maxY * scale / maxCobValueFound); - - addSeries(cobSeries); - - DataPointWithLabelInterface[] minFailover = new DataPointWithLabelInterface[minFailoverActiveList.size()]; - minFailover = minFailoverActiveList.toArray(minFailover); - addSeries(new PointsWithLabelGraphSeries<>(minFailover)); - } - - // scale in % of vertical size (like 0.3) - public void addDeviations(long fromTime, long toTime, boolean useForScale, double scale) { - class DeviationDataPoint extends ScaledDataPoint { - public int color; - - private DeviationDataPoint(double x, double y, int color, Scale scale) { - super(x, y, scale); - this.color = color; - } - } - - BarGraphSeries devSeries; - List devArray = new ArrayList<>(); - Double maxDevValueFound = 0d; - Scale devScale = new Scale(); - - for (long time = fromTime; time <= toTime; time += 5 * 60 * 1000L) { - AutosensData autosensData = iobCobCalculatorPlugin.getAutosensData(time); - if (autosensData != null) { - int color = resourceHelper.gc(R.color.deviationblack); // "=" - if (autosensData.type.equals("") || autosensData.type.equals("non-meal")) { - if (autosensData.pastSensitivity.equals("C")) - color = resourceHelper.gc(R.color.deviationgrey); - if (autosensData.pastSensitivity.equals("+")) - color = resourceHelper.gc(R.color.deviationgreen); - if (autosensData.pastSensitivity.equals("-")) - color = resourceHelper.gc(R.color.deviationred); - } else if (autosensData.type.equals("uam")) { - color = resourceHelper.gc(R.color.uam); - } else if (autosensData.type.equals("csf")) { - color = resourceHelper.gc(R.color.deviationgrey); - } - devArray.add(new DeviationDataPoint(time, autosensData.deviation, color, devScale)); - maxDevValueFound = Math.max(maxDevValueFound, Math.abs(autosensData.deviation)); - } - } - - // DEVIATIONS - DeviationDataPoint[] devData = new DeviationDataPoint[devArray.size()]; - devData = devArray.toArray(devData); - devSeries = new BarGraphSeries<>(devData); - devSeries.setValueDependentColor(data -> data.color); - - if (useForScale) { - maxY = maxDevValueFound; - minY = -maxY; - } - - devScale.setMultiplier(maxY * scale / maxDevValueFound); - - addSeries(devSeries); - } - - // scale in % of vertical size (like 0.3) - public void addRatio(long fromTime, long toTime, boolean useForScale, double scale) { - LineGraphSeries ratioSeries; - List ratioArray = new ArrayList<>(); - double maxRatioValueFound = Double.MIN_VALUE; - double minRatioValueFound = Double.MAX_VALUE; - Scale ratioScale = new Scale(); - - for (long time = fromTime; time <= toTime; time += 5 * 60 * 1000L) { - AutosensData autosensData = iobCobCalculatorPlugin.getAutosensData(time); - if (autosensData != null) { - ratioArray.add(new ScaledDataPoint(time, autosensData.autosensResult.ratio - 1, ratioScale)); - maxRatioValueFound = Math.max(maxRatioValueFound, autosensData.autosensResult.ratio - 1); - minRatioValueFound = Math.min(minRatioValueFound, autosensData.autosensResult.ratio - 1); - } - } - - // RATIOS - ScaledDataPoint[] ratioData = new ScaledDataPoint[ratioArray.size()]; - ratioData = ratioArray.toArray(ratioData); - ratioSeries = new LineGraphSeries<>(ratioData); - ratioSeries.setColor(resourceHelper.gc(R.color.ratio)); - ratioSeries.setThickness(3); - - if (useForScale) { - maxY = Math.max(maxRatioValueFound, Math.abs(minRatioValueFound)); - minY = -maxY; - } - - ratioScale.setMultiplier(maxY * scale / Math.max(maxRatioValueFound, Math.abs(minRatioValueFound))); - - addSeries(ratioSeries); - } - - // scale in % of vertical size (like 0.3) - public void addDeviationSlope(long fromTime, long toTime, boolean useForScale, double scale) { - LineGraphSeries dsMaxSeries; - LineGraphSeries dsMinSeries; - List dsMaxArray = new ArrayList<>(); - List dsMinArray = new ArrayList<>(); - double maxFromMaxValueFound = 0d; - double maxFromMinValueFound = 0d; - Scale dsMaxScale = new Scale(); - Scale dsMinScale = new Scale(); - - for (long time = fromTime; time <= toTime; time += 5 * 60 * 1000L) { - AutosensData autosensData = iobCobCalculatorPlugin.getAutosensData(time); - if (autosensData != null) { - dsMaxArray.add(new ScaledDataPoint(time, autosensData.slopeFromMaxDeviation, dsMaxScale)); - dsMinArray.add(new ScaledDataPoint(time, autosensData.slopeFromMinDeviation, dsMinScale)); - maxFromMaxValueFound = Math.max(maxFromMaxValueFound, Math.abs(autosensData.slopeFromMaxDeviation)); - maxFromMinValueFound = Math.max(maxFromMinValueFound, Math.abs(autosensData.slopeFromMinDeviation)); - } - } - - // Slopes - ScaledDataPoint[] ratioMaxData = new ScaledDataPoint[dsMaxArray.size()]; - ratioMaxData = dsMaxArray.toArray(ratioMaxData); - dsMaxSeries = new LineGraphSeries<>(ratioMaxData); - dsMaxSeries.setColor(resourceHelper.gc(R.color.devslopepos)); - dsMaxSeries.setThickness(3); - - ScaledDataPoint[] ratioMinData = new ScaledDataPoint[dsMinArray.size()]; - ratioMinData = dsMinArray.toArray(ratioMinData); - dsMinSeries = new LineGraphSeries<>(ratioMinData); - dsMinSeries.setColor(resourceHelper.gc(R.color.devslopeneg)); - dsMinSeries.setThickness(3); - - if (useForScale) { - maxY = Math.max(maxFromMaxValueFound, maxFromMinValueFound); - minY = -maxY; - } - - dsMaxScale.setMultiplier(maxY * scale / maxFromMaxValueFound); - dsMinScale.setMultiplier(maxY * scale / maxFromMinValueFound); - - addSeries(dsMaxSeries); - addSeries(dsMinSeries); - } - - // scale in % of vertical size (like 0.3) - public void addNowLine(long now) { - LineGraphSeries seriesNow; - DataPoint[] nowPoints = new DataPoint[]{ - new DataPoint(now, 0), - new DataPoint(now, maxY) - }; - - seriesNow = new LineGraphSeries<>(nowPoints); - seriesNow.setDrawDataPoints(false); - // custom paint to make a dotted line - Paint paint = new Paint(); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(2); - paint.setPathEffect(new DashPathEffect(new float[]{10, 20}, 0)); - paint.setColor(Color.WHITE); - seriesNow.setCustomPaint(paint); - - addSeries(seriesNow); - } - - public void formatAxis(long fromTime, long endTime) { - graph.getViewport().setMaxX(endTime); - graph.getViewport().setMinX(fromTime); - graph.getViewport().setXAxisBoundsManual(true); - graph.getGridLabelRenderer().setLabelFormatter(new TimeAsXAxisLabelFormatter("HH")); - graph.getGridLabelRenderer().setNumHorizontalLabels(7); // only 7 because of the space - } - - private void addSeries(Series s) { - series.add(s); - } - - public void performUpdate() { - // clear old data - graph.getSeries().clear(); - - // add precalculated series - for (Series s : series) { - if (!s.isEmpty()) { - s.onGraphViewAttached(graph); - graph.getSeries().add(s); - } - } - - double step = 1d; - if (maxY < 1) step = 0.1d; - graph.getViewport().setMaxY(Round.ceilTo(maxY, step)); - graph.getViewport().setMinY(Round.floorTo(minY, step)); - graph.getViewport().setYAxisBoundsManual(true); - - // draw it - graph.onDataChanged(false, false); - } -} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.kt new file mode 100644 index 0000000000..fc7d1eb2a1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphData/GraphData.kt @@ -0,0 +1,569 @@ +package info.nightscout.androidaps.plugins.general.overview.graphData + +import android.graphics.Color +import android.graphics.DashPathEffect +import android.graphics.Paint +import com.jjoe64.graphview.GraphView +import com.jjoe64.graphview.series.BarGraphSeries +import com.jjoe64.graphview.series.DataPoint +import com.jjoe64.graphview.series.LineGraphSeries +import com.jjoe64.graphview.series.Series +import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.Constants +import info.nightscout.androidaps.MainApp +import info.nightscout.androidaps.R +import info.nightscout.androidaps.data.IobTotal +import info.nightscout.androidaps.data.Profile +import info.nightscout.androidaps.db.BgReading +import info.nightscout.androidaps.interfaces.ActivePluginProvider +import info.nightscout.androidaps.interfaces.TreatmentsInterface +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.logging.LTag +import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin.LastRun +import info.nightscout.androidaps.plugins.aps.openAPSSMB.SMBDefaults +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction +import info.nightscout.androidaps.plugins.general.overview.graphExtensions.* +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensResult +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin +import info.nightscout.androidaps.utils.DecimalFormatter +import info.nightscout.androidaps.utils.Round +import info.nightscout.androidaps.utils.resources.ResourceHelper +import java.util.* +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +class GraphData(injector: HasAndroidInjector, private val graph: GraphView, private val iobCobCalculatorPlugin: IobCobCalculatorPlugin) { + + // IobCobCalculatorPlugin Cannot be injected: HistoryBrowser + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var profileFunction: ProfileFunction + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var activePlugin: ActivePluginProvider + + private val treatmentsPlugin: TreatmentsInterface + + var maxY = Double.MIN_VALUE + private var minY = Double.MAX_VALUE + private var bgReadingsArray: List? = null + private val units: String + private val series: MutableList> = ArrayList() + + init { + injector.androidInjector().inject(this) + units = profileFunction.getUnits() + treatmentsPlugin = activePlugin.activeTreatments + } + + @Suppress("UNUSED_PARAMETER") + fun addBgReadings(fromTime: Long, toTime: Long, lowLine: Double, highLine: Double, predictions: MutableList?) { + var maxBgValue = Double.MIN_VALUE + bgReadingsArray = iobCobCalculatorPlugin.bgReadings + if (bgReadingsArray?.isEmpty() != false) { + aapsLogger.debug(LTag.OVERVIEW, "No BG data.") + maxY = 10.0 + minY = 0.0 + return + } + val bgListArray: MutableList = ArrayList() + for (bg in bgReadingsArray!!) { + if (bg.date < fromTime || bg.date > toTime) continue + if (bg.value > maxBgValue) maxBgValue = bg.value + bgListArray.add(bg) + } + if (predictions != null) { + predictions.sortWith(Comparator { o1: BgReading, o2: BgReading -> o1.x.compareTo(o2.x) }) + for (prediction in predictions) if (prediction.value >= 40) bgListArray.add(prediction) + } + maxBgValue = Profile.fromMgdlToUnits(maxBgValue, units) + maxBgValue = if (units == Constants.MGDL) Round.roundTo(maxBgValue, 40.0) + 80 else Round.roundTo(maxBgValue, 2.0) + 4 + if (highLine > maxBgValue) maxBgValue = highLine + val numOfVerticalLines = if (units == Constants.MGDL) (maxBgValue / 40 + 1).toInt() else (maxBgValue / 2 + 1).toInt() + maxY = maxBgValue + minY = 0.0 + // set manual y bounds to have nice steps + graph.gridLabelRenderer.numVerticalLabels = numOfVerticalLines + addSeries(PointsWithLabelGraphSeries(Array(bgListArray.size) { i -> bgListArray[i] })) + } + + fun addInRangeArea(fromTime: Long, toTime: Long, lowLine: Double, highLine: Double) { + val inRangeAreaSeries: AreaGraphSeries + val inRangeAreaDataPoints = arrayOf( + DoubleDataPoint(fromTime.toDouble(), lowLine, highLine), + DoubleDataPoint(toTime.toDouble(), lowLine, highLine) + ) + inRangeAreaSeries = AreaGraphSeries(inRangeAreaDataPoints) + inRangeAreaSeries.color = 0 + inRangeAreaSeries.isDrawBackground = true + inRangeAreaSeries.backgroundColor = resourceHelper.gc(R.color.inrangebackground) + addSeries(inRangeAreaSeries) + } + + // scale in % of vertical size (like 0.3) + fun addBasals(fromTime: Long, toTime: Long, scale: Double) { + var maxBasalValueFound = 0.0 + val basalScale = Scale() + val baseBasalArray: MutableList = ArrayList() + val tempBasalArray: MutableList = ArrayList() + val basalLineArray: MutableList = ArrayList() + val absoluteBasalLineArray: MutableList = ArrayList() + var lastLineBasal = 0.0 + var lastAbsoluteLineBasal = -1.0 + var lastBaseBasal = 0.0 + var lastTempBasal = 0.0 + var time = fromTime + while (time < toTime) { + val profile = profileFunction.getProfile(time) + if (profile == null) { + time += 60 * 1000L + continue + } + val basalData = iobCobCalculatorPlugin.getBasalData(profile, time) + val baseBasalValue = basalData.basal + var absoluteLineValue = baseBasalValue + var tempBasalValue = 0.0 + var basal = 0.0 + if (basalData.isTempBasalRunning) { + tempBasalValue = basalData.tempBasalAbsolute + absoluteLineValue = tempBasalValue + if (tempBasalValue != lastTempBasal) { + tempBasalArray.add(ScaledDataPoint(time, lastTempBasal, basalScale)) + tempBasalArray.add(ScaledDataPoint(time, tempBasalValue.also { basal = it }, basalScale)) + } + if (lastBaseBasal != 0.0) { + baseBasalArray.add(ScaledDataPoint(time, lastBaseBasal, basalScale)) + baseBasalArray.add(ScaledDataPoint(time, 0.0, basalScale)) + lastBaseBasal = 0.0 + } + } else { + if (baseBasalValue != lastBaseBasal) { + baseBasalArray.add(ScaledDataPoint(time, lastBaseBasal, basalScale)) + baseBasalArray.add(ScaledDataPoint(time, baseBasalValue.also { basal = it }, basalScale)) + lastBaseBasal = baseBasalValue + } + if (lastTempBasal != 0.0) { + tempBasalArray.add(ScaledDataPoint(time, lastTempBasal, basalScale)) + tempBasalArray.add(ScaledDataPoint(time, 0.0, basalScale)) + } + } + if (baseBasalValue != lastLineBasal) { + basalLineArray.add(ScaledDataPoint(time, lastLineBasal, basalScale)) + basalLineArray.add(ScaledDataPoint(time, baseBasalValue, basalScale)) + } + if (absoluteLineValue != lastAbsoluteLineBasal) { + absoluteBasalLineArray.add(ScaledDataPoint(time, lastAbsoluteLineBasal, basalScale)) + absoluteBasalLineArray.add(ScaledDataPoint(time, basal, basalScale)) + } + lastAbsoluteLineBasal = absoluteLineValue + lastLineBasal = baseBasalValue + lastTempBasal = tempBasalValue + maxBasalValueFound = max(maxBasalValueFound, max(tempBasalValue, baseBasalValue)) + time += 60 * 1000L + } + + // final points + basalLineArray.add(ScaledDataPoint(toTime, lastLineBasal, basalScale)) + baseBasalArray.add(ScaledDataPoint(toTime, lastBaseBasal, basalScale)) + tempBasalArray.add(ScaledDataPoint(toTime, lastTempBasal, basalScale)) + absoluteBasalLineArray.add(ScaledDataPoint(toTime, lastAbsoluteLineBasal, basalScale)) + + // create series + addSeries(LineGraphSeries(Array(baseBasalArray.size) { i -> baseBasalArray[i] }).also { + it.isDrawBackground = true + it.backgroundColor = resourceHelper.gc(R.color.basebasal) + it.thickness = 0 + }) + addSeries(LineGraphSeries(Array(tempBasalArray.size) { i -> tempBasalArray[i] }).also { + it.isDrawBackground = true + it.backgroundColor = resourceHelper.gc(R.color.tempbasal) + it.thickness = 0 + }) + addSeries(LineGraphSeries(Array(basalLineArray.size) { i -> basalLineArray[i] }).also { + it.setCustomPaint(Paint().also { paint -> + paint.style = Paint.Style.STROKE + paint.strokeWidth = resourceHelper.getDisplayMetrics().scaledDensity * 2 + paint.pathEffect = DashPathEffect(floatArrayOf(2f, 4f), 0f) + paint.color = resourceHelper.gc(R.color.basal) + }) + }) + addSeries(LineGraphSeries(Array(absoluteBasalLineArray.size) { i -> absoluteBasalLineArray[i] }).also { + it.setCustomPaint(Paint().also { absolutePaint -> + absolutePaint.style = Paint.Style.STROKE + absolutePaint.strokeWidth = resourceHelper.getDisplayMetrics().scaledDensity * 2 + absolutePaint.color = resourceHelper.gc(R.color.basal) + }) + }) + basalScale.setMultiplier(maxY * scale / maxBasalValueFound) + } + + fun addTargetLine(fromTime: Long, toTimeParam: Long, profile: Profile, lastRun: LastRun?) { + var toTime = toTimeParam + val targetsSeriesArray: MutableList = ArrayList() + var lastTarget = -1.0 + lastRun?.constraintsProcessed?.let { toTime = max(it.latestPredictionsTime, toTime) } + var time = fromTime + while (time < toTime) { + val tt = treatmentsPlugin.getTempTargetFromHistory(time) + var value: Double + value = if (tt == null) { + Profile.fromMgdlToUnits((profile.getTargetLowMgdl(time) + profile.getTargetHighMgdl(time)) / 2, units) + } else { + Profile.fromMgdlToUnits(tt.target(), units) + } + if (lastTarget != value) { + if (lastTarget != -1.0) targetsSeriesArray.add(DataPoint(time.toDouble(), lastTarget)) + targetsSeriesArray.add(DataPoint(time.toDouble(), value)) + } + lastTarget = value + time += 5 * 60 * 1000L + } + // final point + targetsSeriesArray.add(DataPoint(toTime.toDouble(), lastTarget)) + // create series + addSeries(LineGraphSeries(Array(targetsSeriesArray.size) { i -> targetsSeriesArray[i] }).also { + it.isDrawBackground = false + it.color = resourceHelper.gc(R.color.tempTargetBackground) + it.thickness = 2 + }) + } + + fun addTreatments(fromTime: Long, endTime: Long) { + val filteredTreatments: MutableList = ArrayList() + val treatments = treatmentsPlugin.treatmentsFromHistory + for (tx in treatments.indices) { + val t = treatments[tx] + if (t.x < fromTime || t.x > endTime) continue + if (t.isSMB && !t.isValid) continue + t.y = getNearestBg(t.x.toLong()) + filteredTreatments.add(t) + } + + // ProfileSwitch + val profileSwitches = treatmentsPlugin.profileSwitchesFromHistory.list + for (tx in profileSwitches.indices) { + val t: DataPointWithLabelInterface = profileSwitches[tx] + if (t.x < fromTime || t.x > endTime) continue + filteredTreatments.add(t) + } + + // Extended bolus + if (!activePlugin.activePump.isFakingTempsByExtendedBoluses) { + val extendedBoluses = treatmentsPlugin.extendedBolusesFromHistory.list + for (tx in extendedBoluses.indices) { + val t: DataPointWithLabelInterface = extendedBoluses[tx] + if (t.x + t.duration < fromTime || t.x > endTime) continue + if (t.duration == 0L) continue + t.y = getNearestBg(t.x.toLong()) + filteredTreatments.add(t) + } + } + + // Careportal + val careportalEvents = MainApp.getDbHelper().getCareportalEventsFromTime(fromTime - 6 * 60 * 60 * 1000, true) + for (tx in careportalEvents.indices) { + val t: DataPointWithLabelInterface = careportalEvents[tx] + if (t.x + t.duration < fromTime || t.x > endTime) continue + t.y = getNearestBg(t.x.toLong()) + filteredTreatments.add(t) + } + addSeries(PointsWithLabelGraphSeries(Array(filteredTreatments.size) { i -> filteredTreatments[i] })) + } + + private fun getNearestBg(date: Long): Double { + bgReadingsArray?.let { bgReadingsArray -> + for (r in bgReadingsArray.indices) { + val reading = bgReadingsArray[r] + if (reading.date > date) continue + return Profile.fromMgdlToUnits(reading.value, units) + } + return if (bgReadingsArray.isNotEmpty()) Profile.fromMgdlToUnits(bgReadingsArray[0].value, units) else Profile.fromMgdlToUnits(100.0, units) + } ?: return Profile.fromMgdlToUnits(100.0, units) + } + + fun addActivity(fromTime: Long, toTime: Long, useForScale: Boolean, scale: Double) { + val actArrayHist: MutableList = ArrayList() + val actArrayPred: MutableList = ArrayList() + val now = System.currentTimeMillis().toDouble() + val actScale = Scale() + var total: IobTotal + var maxIAValue = 0.0 + var time = fromTime + while (time <= toTime) { + val profile = profileFunction.getProfile(time) + if (profile == null) { + time += 5 * 60 * 1000L + continue + } + total = iobCobCalculatorPlugin.calculateFromTreatmentsAndTempsSynchronized(time, profile) + val act: Double = total.activity + if (time <= now) actArrayHist.add(ScaledDataPoint(time, act, actScale)) else actArrayPred.add(ScaledDataPoint(time, act, actScale)) + maxIAValue = max(maxIAValue, abs(act)) + time += 5 * 60 * 1000L + } + addSeries(FixedLineGraphSeries(Array(actArrayHist.size) { i -> actArrayHist[i] }).also { + it.isDrawBackground = false + it.color = resourceHelper.gc(R.color.activity) + it.thickness = 3 + }) + addSeries(FixedLineGraphSeries(Array(actArrayPred.size) { i -> actArrayPred[i] }).also { + it.setCustomPaint(Paint().also { paint -> + paint.style = Paint.Style.STROKE + paint.strokeWidth = 3f + paint.pathEffect = DashPathEffect(floatArrayOf(4f, 4f), 0f) + paint.color = resourceHelper.gc(R.color.activity) + }) + }) + if (useForScale) { + maxY = maxIAValue + minY = -maxIAValue + } + actScale.setMultiplier(maxY * scale / maxIAValue) + } + + // scale in % of vertical size (like 0.3) + fun addIob(fromTime: Long, toTime: Long, useForScale: Boolean, scale: Double, showPrediction: Boolean) { + val iobSeries: FixedLineGraphSeries + val iobArray: MutableList = ArrayList() + var maxIobValueFound = Double.MIN_VALUE + var lastIob = 0.0 + val iobScale = Scale() + var time = fromTime + while (time <= toTime) { + val profile = profileFunction.getProfile(time) + var iob = 0.0 + if (profile != null) iob = iobCobCalculatorPlugin.calculateFromTreatmentsAndTempsSynchronized(time, profile).iob + if (abs(lastIob - iob) > 0.02) { + if (abs(lastIob - iob) > 0.2) iobArray.add(ScaledDataPoint(time, lastIob, iobScale)) + iobArray.add(ScaledDataPoint(time, iob, iobScale)) + maxIobValueFound = max(maxIobValueFound, abs(iob)) + lastIob = iob + } + time += 5 * 60 * 1000L + } + iobSeries = FixedLineGraphSeries(Array(iobArray.size) { i -> iobArray[i] }).also { + it.isDrawBackground = true + it.backgroundColor = -0x7f000001 and resourceHelper.gc(R.color.iob) //50% + it.color = resourceHelper.gc(R.color.iob) + it.thickness = 3 + } + if (showPrediction) { + val autosensData = iobCobCalculatorPlugin.getLastAutosensDataSynchronized("GraphData") + val lastAutosensResult = autosensData?.autosensResult ?: AutosensResult() + val isTempTarget = treatmentsPlugin.getTempTargetFromHistory(System.currentTimeMillis()) != null + val iobPred: MutableList = ArrayList() + val iobPredArray = iobCobCalculatorPlugin.calculateIobArrayForSMB(lastAutosensResult, SMBDefaults.exercise_mode, SMBDefaults.half_basal_exercise_target, isTempTarget) + for (i in iobPredArray) { + iobPred.add(i.setColor(resourceHelper.gc(R.color.iobPredAS))) + maxIobValueFound = max(maxIobValueFound, abs(i.iob)) + } + addSeries(PointsWithLabelGraphSeries(Array(iobPred.size) { i -> iobPred[i] })) + val iobPred2: MutableList = ArrayList() + val iobPredArray2 = iobCobCalculatorPlugin.calculateIobArrayForSMB(AutosensResult(), SMBDefaults.exercise_mode, SMBDefaults.half_basal_exercise_target, isTempTarget) + for (i in iobPredArray2) { + iobPred2.add(i.setColor(resourceHelper.gc(R.color.iobPred))) + maxIobValueFound = max(maxIobValueFound, abs(i.iob)) + } + addSeries(PointsWithLabelGraphSeries(Array(iobPred2.size) { i -> iobPred2[i] })) + aapsLogger.debug(LTag.AUTOSENS, "IOB pred for AS=" + DecimalFormatter.to2Decimal(lastAutosensResult.ratio) + ": " + iobCobCalculatorPlugin.iobArrayToString(iobPredArray)) + aapsLogger.debug(LTag.AUTOSENS, "IOB pred for AS=" + DecimalFormatter.to2Decimal(1.0) + ": " + iobCobCalculatorPlugin.iobArrayToString(iobPredArray2)) + } + if (useForScale) { + maxY = maxIobValueFound + minY = -maxIobValueFound + } + iobScale.setMultiplier(maxY * scale / maxIobValueFound) + addSeries(iobSeries) + } + + // scale in % of vertical size (like 0.3) + fun addCob(fromTime: Long, toTime: Long, useForScale: Boolean, scale: Double) { + val minFailOverActiveList: MutableList = ArrayList() + val cobArray: MutableList = ArrayList() + var maxCobValueFound = 0.0 + var lastCob = 0 + val cobScale = Scale() + var time = fromTime + while (time <= toTime) { + iobCobCalculatorPlugin.getAutosensData(time)?.let { autosensData -> + val cob = autosensData.cob.toInt() + if (cob != lastCob) { + if (autosensData.carbsFromBolus > 0) cobArray.add(ScaledDataPoint(time, lastCob.toDouble(), cobScale)) + cobArray.add(ScaledDataPoint(time, cob.toDouble(), cobScale)) + maxCobValueFound = max(maxCobValueFound, cob.toDouble()) + lastCob = cob + } + if (autosensData.failoverToMinAbsorbtionRate) { + autosensData.setScale(cobScale) + autosensData.setChartTime(time) + minFailOverActiveList.add(autosensData) + } + } + time += 5 * 60 * 1000L + } + + // COB + addSeries(FixedLineGraphSeries(Array(cobArray.size) { i -> cobArray[i] }).also { + it.isDrawBackground = true + it.backgroundColor = -0x7f000001 and resourceHelper.gc(R.color.cob) //50% + it.color = resourceHelper.gc(R.color.cob) + it.thickness = 3 + }) + if (useForScale) { + maxY = maxCobValueFound + minY = 0.0 + } + cobScale.setMultiplier(maxY * scale / maxCobValueFound) + addSeries(PointsWithLabelGraphSeries(Array(minFailOverActiveList.size) { i -> minFailOverActiveList[i] })) + } + + // scale in % of vertical size (like 0.3) + fun addDeviations(fromTime: Long, toTime: Long, useForScale: Boolean, scale: Double) { + class DeviationDataPoint(x: Double, y: Double, var color: Int, scale: Scale) : ScaledDataPoint(x, y, scale) + + val devArray: MutableList = ArrayList() + var maxDevValueFound = 0.0 + val devScale = Scale() + var time = fromTime + while (time <= toTime) { + iobCobCalculatorPlugin.getAutosensData(time)?.let { autosensData -> + var color = resourceHelper.gc(R.color.deviationblack) // "=" + if (autosensData.type == "" || autosensData.type == "non-meal") { + if (autosensData.pastSensitivity == "C") color = resourceHelper.gc(R.color.deviationgrey) + if (autosensData.pastSensitivity == "+") color = resourceHelper.gc(R.color.deviationgreen) + if (autosensData.pastSensitivity == "-") color = resourceHelper.gc(R.color.deviationred) + } else if (autosensData.type == "uam") { + color = resourceHelper.gc(R.color.uam) + } else if (autosensData.type == "csf") { + color = resourceHelper.gc(R.color.deviationgrey) + } + devArray.add(DeviationDataPoint(time.toDouble(), autosensData.deviation, color, devScale)) + maxDevValueFound = max(maxDevValueFound, abs(autosensData.deviation)) + } + time += 5 * 60 * 1000L + } + + // DEVIATIONS + addSeries(BarGraphSeries(Array(devArray.size) { i -> devArray[i] }).also { + it.setValueDependentColor { data: DeviationDataPoint -> data.color } + }) + if (useForScale) { + maxY = maxDevValueFound + minY = -maxY + } + devScale.setMultiplier(maxY * scale / maxDevValueFound) + } + + // scale in % of vertical size (like 0.3) + fun addRatio(fromTime: Long, toTime: Long, useForScale: Boolean, scale: Double) { + val ratioArray: MutableList = ArrayList() + var maxRatioValueFound = Double.MIN_VALUE + var minRatioValueFound = Double.MAX_VALUE + val ratioScale = Scale() + var time = fromTime + while (time <= toTime) { + iobCobCalculatorPlugin.getAutosensData(time)?.let { autosensData -> + ratioArray.add(ScaledDataPoint(time, autosensData.autosensResult.ratio - 1, ratioScale)) + maxRatioValueFound = max(maxRatioValueFound, autosensData.autosensResult.ratio - 1) + minRatioValueFound = min(minRatioValueFound, autosensData.autosensResult.ratio - 1) + } + time += 5 * 60 * 1000L + } + + // RATIOS + addSeries(LineGraphSeries(Array(ratioArray.size) { i -> ratioArray[i] }).also { + it.color = resourceHelper.gc(R.color.ratio) + it.thickness = 3 + }) + if (useForScale) { + maxY = max(maxRatioValueFound, abs(minRatioValueFound)) + minY = -maxY + } + ratioScale.setMultiplier(maxY * scale / max(maxRatioValueFound, abs(minRatioValueFound))) + } + + // scale in % of vertical size (like 0.3) + fun addDeviationSlope(fromTime: Long, toTime: Long, useForScale: Boolean, scale: Double) { + val dsMaxArray: MutableList = ArrayList() + val dsMinArray: MutableList = ArrayList() + var maxFromMaxValueFound = 0.0 + var maxFromMinValueFound = 0.0 + val dsMaxScale = Scale() + val dsMinScale = Scale() + var time = fromTime + while (time <= toTime) { + iobCobCalculatorPlugin.getAutosensData(time)?.let { autosensData -> + dsMaxArray.add(ScaledDataPoint(time, autosensData.slopeFromMaxDeviation, dsMaxScale)) + dsMinArray.add(ScaledDataPoint(time, autosensData.slopeFromMinDeviation, dsMinScale)) + maxFromMaxValueFound = max(maxFromMaxValueFound, abs(autosensData.slopeFromMaxDeviation)) + maxFromMinValueFound = max(maxFromMinValueFound, abs(autosensData.slopeFromMinDeviation)) + } + time += 5 * 60 * 1000L + } + + // Slopes + addSeries(LineGraphSeries(Array(dsMaxArray.size) { i -> dsMaxArray[i] }).also { + it.color = resourceHelper.gc(R.color.devslopepos) + it.thickness = 3 + }) + addSeries(LineGraphSeries(Array(dsMinArray.size) { i -> dsMinArray[i] }).also { + it.color = resourceHelper.gc(R.color.devslopeneg) + it.thickness = 3 + }) + if (useForScale) { + maxY = max(maxFromMaxValueFound, maxFromMinValueFound) + minY = -maxY + } + dsMaxScale.setMultiplier(maxY * scale / maxFromMaxValueFound) + dsMinScale.setMultiplier(maxY * scale / maxFromMinValueFound) + } + + // scale in % of vertical size (like 0.3) + fun addNowLine(now: Long) { + val nowPoints = arrayOf( + DataPoint(now.toDouble(), 0.0), + DataPoint(now.toDouble(), maxY) + ) + addSeries(LineGraphSeries(nowPoints).also { + it.isDrawDataPoints = false + // custom paint to make a dotted line + it.setCustomPaint(Paint().also { paint -> + paint.style = Paint.Style.STROKE + paint.strokeWidth = 2f + paint.pathEffect = DashPathEffect(floatArrayOf(10f, 20f), 0f) + paint.color = Color.WHITE + }) + }) + } + + fun formatAxis(fromTime: Long, endTime: Long) { + graph.viewport.setMaxX(endTime.toDouble()) + graph.viewport.setMinX(fromTime.toDouble()) + graph.viewport.isXAxisBoundsManual = true + graph.gridLabelRenderer.labelFormatter = TimeAsXAxisLabelFormatter("HH") + graph.gridLabelRenderer.numHorizontalLabels = 7 // only 7 because of the space + } + + private fun addSeries(s: Series<*>) = series.add(s) + + fun performUpdate() { + // clear old data + graph.series.clear() + + // add pre calculated series + for (s in series) { + if (!s.isEmpty) { + s.onGraphViewAttached(graph) + graph.series.add(s) + } + } + var step = 1.0 + if (maxY < 1) step = 0.1 + graph.viewport.setMaxY(Round.ceilTo(maxY, step)) + graph.viewport.setMinY(Round.floorTo(minY, step)) + graph.viewport.isYAxisBoundsManual = true + + // draw it + graph.onDataChanged(false, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphExtensions/ScaledDataPoint.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphExtensions/ScaledDataPoint.java index ac63790a71..8a00446092 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphExtensions/ScaledDataPoint.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/graphExtensions/ScaledDataPoint.java @@ -23,6 +23,12 @@ public class ScaledDataPoint implements DataPointInterface, Serializable { this.scale = scale; } + public ScaledDataPoint(long x, double y, Scale scale) { + this.x=x; + this.y=y; + this.scale = scale; + } + public ScaledDataPoint(Date x, double y, Scale scale) { this.x = x.getTime(); this.y = y; From 7b4174afe0428e6046dd7ea4b8f48cea6a01af71 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 1 Apr 2020 09:08:18 +0200 Subject: [PATCH 04/14] gradle update --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7ffc201be4..dba467c04a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:3.6.2' classpath 'com.google.gms:google-services:4.3.3' classpath 'io.fabric.tools:gradle:1.31.2' From f4b7f642c91cb42e33e0426c5cefa6a39e3d83a3 Mon Sep 17 00:00:00 2001 From: TebbeUbben Date: Wed, 25 Mar 2020 23:51:04 +0100 Subject: [PATCH 05/14] Register plugins with Dagger --- .../info/nightscout/androidaps/MainApp.java | 165 +------- .../dependencyInjection/AppModule.kt | 17 +- .../dependencyInjection/PluginsModule.kt | 372 ++++++++++++++++++ .../plugins/configBuilder/PluginStore.kt | 11 +- 4 files changed, 396 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index 6270ac2ffb..03e802a43c 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -25,6 +25,8 @@ import net.danlew.android.joda.JodaTimeAndroid; import org.json.JSONException; +import java.util.List; + import javax.inject.Inject; import dagger.android.AndroidInjector; @@ -33,65 +35,14 @@ import dagger.android.HasAndroidInjector; import info.nightscout.androidaps.data.Profile; import info.nightscout.androidaps.db.DatabaseHelper; import info.nightscout.androidaps.dependencyInjection.DaggerAppComponent; +import info.nightscout.androidaps.interfaces.PluginBase; import info.nightscout.androidaps.logging.AAPSLogger; import info.nightscout.androidaps.logging.LTag; -import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; -import info.nightscout.androidaps.plugins.aps.openAPSAMA.OpenAPSAMAPlugin; -import info.nightscout.androidaps.plugins.aps.openAPSMA.OpenAPSMAPlugin; -import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin; 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.constraints.dstHelper.DstHelperPlugin; -import info.nightscout.androidaps.plugins.constraints.objectives.ObjectivesPlugin; -import info.nightscout.androidaps.plugins.constraints.safety.SafetyPlugin; -import info.nightscout.androidaps.plugins.constraints.signatureVerifier.SignatureVerifierPlugin; -import info.nightscout.androidaps.plugins.constraints.storage.StorageConstraintPlugin; -import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerPlugin; import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerUtils; -import info.nightscout.androidaps.plugins.general.actions.ActionsPlugin; -import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin; -import info.nightscout.androidaps.plugins.general.careportal.CareportalPlugin; -import info.nightscout.androidaps.plugins.general.dataBroadcaster.DataBroadcastPlugin; -import info.nightscout.androidaps.plugins.general.food.FoodPlugin; -import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin; -import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin; import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; -import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin; -import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin; -import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin; -import info.nightscout.androidaps.plugins.general.tidepool.TidepoolPlugin; -import info.nightscout.androidaps.plugins.general.wear.WearPlugin; -import info.nightscout.androidaps.plugins.general.xdripStatusline.StatusLinePlugin; -import info.nightscout.androidaps.plugins.insulin.InsulinOrefFreePeakPlugin; -import info.nightscout.androidaps.plugins.insulin.InsulinOrefRapidActingPlugin; -import info.nightscout.androidaps.plugins.insulin.InsulinOrefUltraRapidActingPlugin; -import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin; -import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin; -import info.nightscout.androidaps.plugins.profile.ns.NSProfilePlugin; -import info.nightscout.androidaps.plugins.pump.combo.ComboPlugin; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPlugin; -import info.nightscout.androidaps.plugins.pump.danaRKorean.DanaRKoreanPlugin; -import info.nightscout.androidaps.plugins.pump.danaRS.DanaRSPlugin; -import info.nightscout.androidaps.plugins.pump.danaRv2.DanaRv2Plugin; -import info.nightscout.androidaps.plugins.pump.insight.LocalInsightPlugin; -import info.nightscout.androidaps.plugins.pump.mdi.MDIPlugin; -import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin; -import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin; -import info.nightscout.androidaps.plugins.sensitivity.SensitivityAAPSPlugin; -import info.nightscout.androidaps.plugins.sensitivity.SensitivityOref0Plugin; -import info.nightscout.androidaps.plugins.sensitivity.SensitivityOref1Plugin; -import info.nightscout.androidaps.plugins.sensitivity.SensitivityWeightedAveragePlugin; -import info.nightscout.androidaps.plugins.source.DexcomPlugin; -import info.nightscout.androidaps.plugins.source.EversensePlugin; -import info.nightscout.androidaps.plugins.source.GlimpPlugin; -import info.nightscout.androidaps.plugins.source.MM640gPlugin; -import info.nightscout.androidaps.plugins.source.NSClientSourcePlugin; -import info.nightscout.androidaps.plugins.source.PoctechPlugin; -import info.nightscout.androidaps.plugins.source.RandomBgPlugin; -import info.nightscout.androidaps.plugins.source.TomatoPlugin; -import info.nightscout.androidaps.plugins.source.XdripPlugin; -import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; import info.nightscout.androidaps.receivers.ChargingStateReceiver; import info.nightscout.androidaps.receivers.DataReceiver; import info.nightscout.androidaps.receivers.KeepAliveReceiver; @@ -130,60 +81,9 @@ public class MainApp extends DaggerApplication { @Inject SP sp; @Inject ProfileFunction profileFunction; - @Inject ActionsPlugin actionsPlugin; - @Inject AutomationPlugin automationPlugin; - @Inject ComboPlugin comboPlugin; - @Inject CareportalPlugin careportalPlugin; @Inject ConfigBuilderPlugin configBuilderPlugin; - @Inject DanaRPlugin danaRPlugin; - @Inject DanaRSPlugin danaRSPlugin; - @Inject DanaRv2Plugin danaRv2Plugin; - @Inject DanaRKoreanPlugin danaRKoreanPlugin; - @Inject DataBroadcastPlugin dataBroadcastPlugin; - @Inject DstHelperPlugin dstHelperPlugin; - @Inject FoodPlugin foodPlugin; - @Inject InsulinOrefFreePeakPlugin insulinOrefFreePeakPlugin; - @Inject InsulinOrefRapidActingPlugin insulinOrefRapidActingPlugin; - @Inject InsulinOrefUltraRapidActingPlugin insulinOrefUltraRapidActingPlugin; - @Inject IobCobCalculatorPlugin iobCobCalculatorPlugin; - @Inject LocalInsightPlugin localInsightPlugin; - @Inject LocalProfilePlugin localProfilePlugin; - @Inject LoopPlugin loopPlugin; - @Inject MedtronicPumpPlugin medtronicPumpPlugin; - @Inject MDIPlugin mdiPlugin; - @Inject NSProfilePlugin nsProfilePlugin; - @Inject ObjectivesPlugin objectivesPlugin; - @Inject SafetyPlugin safetyPlugin; - @Inject SmsCommunicatorPlugin smsCommunicatorPlugin; - @Inject OpenAPSMAPlugin openAPSMAPlugin; - @Inject OpenAPSAMAPlugin openAPSAMAPlugin; - @Inject OpenAPSSMBPlugin openAPSSMBPlugin; - @Inject OverviewPlugin overviewPlugin; - @Inject PersistentNotificationPlugin persistentNotificationPlugin; - @Inject RandomBgPlugin randomBgPlugin; - @Inject SensitivityOref1Plugin sensitivityOref1Plugin; - @Inject SensitivityAAPSPlugin sensitivityAAPSPlugin; - @Inject SensitivityOref0Plugin sensitivityOref0Plugin; - @Inject SensitivityWeightedAveragePlugin sensitivityWeightedAveragePlugin; - @Inject SignatureVerifierPlugin signatureVerifierPlugin; - @Inject StorageConstraintPlugin storageConstraintPlugin; - @Inject DexcomPlugin dexcomPlugin; - @Inject EversensePlugin eversensePlugin; - @Inject GlimpPlugin glimpPlugin; - @Inject MaintenancePlugin maintenancePlugin; - @Inject MM640gPlugin mM640GPlugin; - @Inject NSClientPlugin nsClientPlugin; - @Inject NSClientSourcePlugin nSClientSourcePlugin; - @Inject PoctechPlugin poctechPlugin; - @Inject TomatoPlugin tomatoPlugin; - @Inject XdripPlugin xdripPlugin; - @Inject StatusLinePlugin statusLinePlugin; - @Inject TidepoolPlugin tidepoolPlugin; - @Inject TreatmentsPlugin treatmentsPlugin; - @Inject VirtualPumpPlugin virtualPumpPlugin; - @Inject VersionCheckerPlugin versionCheckerPlugin; - @Inject WearPlugin wearPlugin; @Inject KeepAliveReceiver.KeepAliveManager keepAliveManager; + @Inject List plugins; @Override public void onCreate() { @@ -231,62 +131,7 @@ public class MainApp extends DaggerApplication { versionCheckersUtils.triggerCheckVersion(); // Register all tabs in app here - pluginStore.add(overviewPlugin); - pluginStore.add(iobCobCalculatorPlugin); - if (!Config.NSCLIENT) pluginStore.add(actionsPlugin); - pluginStore.add(insulinOrefRapidActingPlugin); - pluginStore.add(insulinOrefUltraRapidActingPlugin); - pluginStore.add(insulinOrefFreePeakPlugin); - pluginStore.add(sensitivityOref0Plugin); - pluginStore.add(sensitivityAAPSPlugin); - pluginStore.add(sensitivityWeightedAveragePlugin); - pluginStore.add(sensitivityOref1Plugin); - if (Config.PUMPDRIVERS) pluginStore.add(danaRPlugin); - if (Config.PUMPDRIVERS) pluginStore.add(danaRKoreanPlugin); - if (Config.PUMPDRIVERS) pluginStore.add(danaRv2Plugin); - if (Config.PUMPDRIVERS) pluginStore.add(danaRSPlugin); - if (Config.PUMPDRIVERS) pluginStore.add(localInsightPlugin); - if (Config.PUMPDRIVERS) pluginStore.add(comboPlugin); - if (Config.PUMPDRIVERS) pluginStore.add(medtronicPumpPlugin); - if (!Config.NSCLIENT) pluginStore.add(mdiPlugin); - if (!Config.NSCLIENT) pluginStore.add(virtualPumpPlugin); - if (Config.NSCLIENT) pluginStore.add(careportalPlugin); - if (Config.APS) pluginStore.add(loopPlugin); - if (Config.APS) pluginStore.add(openAPSMAPlugin); - if (Config.APS) pluginStore.add(openAPSAMAPlugin); - if (Config.APS) pluginStore.add(openAPSSMBPlugin); - pluginStore.add(nsProfilePlugin); - if (!Config.NSCLIENT) pluginStore.add(localProfilePlugin); - pluginStore.add(treatmentsPlugin); - if (!Config.NSCLIENT) pluginStore.add(safetyPlugin); - if (!Config.NSCLIENT) pluginStore.add(versionCheckerPlugin); - if (Config.APS) pluginStore.add(storageConstraintPlugin); - if (Config.APS) pluginStore.add(signatureVerifierPlugin); - if (Config.APS) pluginStore.add(objectivesPlugin); - pluginStore.add(xdripPlugin); - pluginStore.add(nSClientSourcePlugin); - pluginStore.add(mM640GPlugin); - pluginStore.add(glimpPlugin); - pluginStore.add(dexcomPlugin); - pluginStore.add(poctechPlugin); - pluginStore.add(tomatoPlugin); - pluginStore.add(eversensePlugin); - pluginStore.add(randomBgPlugin); - if (!Config.NSCLIENT) pluginStore.add(smsCommunicatorPlugin); - pluginStore.add(foodPlugin); - - pluginStore.add(wearPlugin); - pluginStore.add(statusLinePlugin); - pluginStore.add(persistentNotificationPlugin); - pluginStore.add(nsClientPlugin); -// if (engineeringMode) pluginsList.add(tidepoolPlugin); - pluginStore.add(maintenancePlugin); - pluginStore.add(automationPlugin); - pluginStore.add(dstHelperPlugin); - pluginStore.add(dataBroadcastPlugin); - - pluginStore.add(configBuilderPlugin); - + pluginStore.setPlugins(plugins); configBuilderPlugin.initialize(); NSUpload.uploadAppStart(); diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt index 28429bf870..511f216463 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt @@ -3,10 +3,12 @@ package info.nightscout.androidaps.dependencyInjection import android.content.Context import androidx.preference.PreferenceManager import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides import dagger.android.ContributesAndroidInjector import dagger.android.HasAndroidInjector +import info.nightscout.androidaps.Config import info.nightscout.androidaps.MainApp import info.nightscout.androidaps.data.Profile import info.nightscout.androidaps.data.ProfileStore @@ -15,6 +17,7 @@ import info.nightscout.androidaps.db.BgReading import info.nightscout.androidaps.db.ProfileSwitch import info.nightscout.androidaps.interfaces.ActivePluginProvider import info.nightscout.androidaps.interfaces.CommandQueueProvider +import info.nightscout.androidaps.interfaces.PluginBase import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.AAPSLoggerProduction import info.nightscout.androidaps.plugins.aps.loop.APSResult @@ -55,7 +58,7 @@ import info.nightscout.androidaps.utils.wizard.BolusWizard import info.nightscout.androidaps.utils.wizard.QuickWizardEntry import javax.inject.Singleton -@Module(includes = [AppModule.AppBindings::class]) +@Module(includes = [AppModule.AppBindings::class, PluginsModule::class]) open class AppModule { @Provides @@ -88,6 +91,18 @@ open class AppModule { */ } + @Provides + fun providesPlugins(@PluginsModule.AllConfigs allConfigs: Map<@JvmSuppressWildcards Int, @JvmSuppressWildcards PluginBase>, + @PluginsModule.PumpDriver pumpDrivers: Lazy>, + @PluginsModule.NotNSClient notNsClient: Lazy>, + @PluginsModule.APS aps: Lazy>): List<@JvmSuppressWildcards PluginBase> { + val plugins = allConfigs.toMutableMap() + if (Config.PUMPDRIVERS) plugins += pumpDrivers.get() + if (Config.APS) plugins += aps.get() + if (!Config.NSCLIENT) plugins += notNsClient.get() + return plugins.toList().sortedBy { it.first }.map { it.second } + } + @Module interface AppBindings { diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt new file mode 100644 index 0000000000..2a9c45d361 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PluginsModule.kt @@ -0,0 +1,372 @@ +package info.nightscout.androidaps.dependencyInjection + +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet +import info.nightscout.androidaps.Config +import info.nightscout.androidaps.interfaces.PluginBase +import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin +import info.nightscout.androidaps.plugins.aps.openAPSAMA.OpenAPSAMAPlugin +import info.nightscout.androidaps.plugins.aps.openAPSMA.OpenAPSMAPlugin +import info.nightscout.androidaps.plugins.aps.openAPSSMB.OpenAPSSMBPlugin +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin +import info.nightscout.androidaps.plugins.constraints.dstHelper.DstHelperPlugin +import info.nightscout.androidaps.plugins.constraints.objectives.ObjectivesPlugin +import info.nightscout.androidaps.plugins.constraints.safety.SafetyPlugin +import info.nightscout.androidaps.plugins.constraints.signatureVerifier.SignatureVerifierPlugin +import info.nightscout.androidaps.plugins.constraints.storage.StorageConstraintPlugin +import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerPlugin +import info.nightscout.androidaps.plugins.general.actions.ActionsPlugin +import info.nightscout.androidaps.plugins.general.careportal.CareportalPlugin +import info.nightscout.androidaps.plugins.general.dataBroadcaster.DataBroadcastPlugin +import info.nightscout.androidaps.plugins.general.food.FoodPlugin +import info.nightscout.androidaps.plugins.general.maintenance.MaintenancePlugin +import info.nightscout.androidaps.plugins.general.nsclient.NSClientPlugin +import info.nightscout.androidaps.plugins.general.overview.OverviewPlugin +import info.nightscout.androidaps.plugins.general.persistentNotification.PersistentNotificationPlugin +import info.nightscout.androidaps.plugins.general.smsCommunicator.SmsCommunicatorPlugin +import info.nightscout.androidaps.plugins.general.wear.WearPlugin +import info.nightscout.androidaps.plugins.general.xdripStatusline.StatusLinePlugin +import info.nightscout.androidaps.plugins.insulin.InsulinOrefFreePeakPlugin +import info.nightscout.androidaps.plugins.insulin.InsulinOrefRapidActingPlugin +import info.nightscout.androidaps.plugins.insulin.InsulinOrefUltraRapidActingPlugin +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin +import info.nightscout.androidaps.plugins.profile.local.LocalProfilePlugin +import info.nightscout.androidaps.plugins.profile.ns.NSProfilePlugin +import info.nightscout.androidaps.plugins.pump.combo.ComboPlugin +import info.nightscout.androidaps.plugins.pump.danaR.DanaRPlugin +import info.nightscout.androidaps.plugins.pump.danaRKorean.DanaRKoreanPlugin +import info.nightscout.androidaps.plugins.pump.danaRS.DanaRSPlugin +import info.nightscout.androidaps.plugins.pump.danaRv2.DanaRv2Plugin +import info.nightscout.androidaps.plugins.pump.insight.LocalInsightPlugin +import info.nightscout.androidaps.plugins.pump.mdi.MDIPlugin +import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicPumpPlugin +import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpPlugin +import info.nightscout.androidaps.plugins.sensitivity.SensitivityAAPSPlugin +import info.nightscout.androidaps.plugins.sensitivity.SensitivityOref0Plugin +import info.nightscout.androidaps.plugins.sensitivity.SensitivityOref1Plugin +import info.nightscout.androidaps.plugins.sensitivity.SensitivityWeightedAveragePlugin +import info.nightscout.androidaps.plugins.source.* +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin +import javax.inject.Qualifier + +@Module +abstract class PluginsModule { + + @Binds + @AllConfigs + @IntoMap + @IntKey(0) + abstract fun bindOverviewPlugin(plugin: OverviewPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(10) + abstract fun bindIobCobCalculatorPlugin(plugin: IobCobCalculatorPlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(20) + abstract fun bindActionsPlugin(plugin: ActionsPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(30) + abstract fun bindInsulinOrefRapidActingPlugin(plugin: InsulinOrefRapidActingPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(40) + abstract fun bindInsulinOrefUltraRapidActingPlugin(plugin: InsulinOrefUltraRapidActingPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(50) + abstract fun bindInsulinOrefFreePeakPlugin(plugin: InsulinOrefFreePeakPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(60) + abstract fun bindSensitivityOref0Plugin(plugin: SensitivityOref0Plugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(70) + abstract fun bindSensitivityAAPSPlugin(plugin: SensitivityAAPSPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(80) + abstract fun bindSensitivityWeightedAveragePlugin(plugin: SensitivityWeightedAveragePlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(90) + abstract fun bindSensitivityOref1Plugin(plugin: SensitivityOref1Plugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(100) + abstract fun bindDanaRPlugin(plugin: DanaRPlugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(110) + abstract fun bindDanaRKoreanPlugin(plugin: DanaRKoreanPlugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(120) + abstract fun bindDanaRv2Plugin(plugin: DanaRv2Plugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(130) + abstract fun bindDanaRSPlugin(plugin: DanaRSPlugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(140) + abstract fun bindLocalInsightPlugin(plugin: LocalInsightPlugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(150) + abstract fun bindComboPlugin(plugin: ComboPlugin): PluginBase + + @Binds + @PumpDriver + @IntoMap + @IntKey(160) + abstract fun bindMedtronicPumpPlugin(plugin: MedtronicPumpPlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(170) + abstract fun bindMDIPlugin(plugin: MDIPlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(180) + abstract fun bindVirtualPumpPlugin(plugin: VirtualPumpPlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(190) + abstract fun bindCareportalPlugin(plugin: CareportalPlugin): PluginBase + + @Binds + @APS + @IntoMap + @IntKey(200) + abstract fun bindLoopPlugin(plugin: LoopPlugin): PluginBase + + @Binds + @APS + @IntoMap + @IntKey(210) + abstract fun bindOpenAPSMAPlugin(plugin: OpenAPSMAPlugin): PluginBase + + @Binds + @APS + @IntoMap + @IntKey(220) + abstract fun bindOpenAPSAMAPlugin(plugin: OpenAPSAMAPlugin): PluginBase + + @Binds + @APS + @IntoMap + @IntKey(230) + abstract fun bindOpenAPSSMBPlugin(plugin: OpenAPSSMBPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(240) + abstract fun bindNSProfilePlugin(plugin: NSProfilePlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(250) + abstract fun bindLocalProfilePlugin(plugin: LocalProfilePlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(260) + abstract fun bindTreatmentsPlugin(plugin: TreatmentsPlugin): PluginBase + + @Binds + @NotNSClient + @IntoSet + abstract fun bindSafetyPlugin(plugin: SafetyPlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(270) + abstract fun bindVersionCheckerPlugin(plugin: VersionCheckerPlugin): PluginBase + + @Binds + @NotNSClient + @IntoMap + @IntKey(280) + abstract fun bindSmsCommunicatorPlugin(plugin: SmsCommunicatorPlugin): PluginBase + + + @Binds + @APS + @IntoMap + @IntKey(290) + abstract fun bindStorageConstraintPlugin(plugin: StorageConstraintPlugin): PluginBase + + @Binds + @APS + @IntoMap + @IntKey(300) + abstract fun bindSignatureVerifierPlugin(plugin: SignatureVerifierPlugin): PluginBase + + @Binds + @APS + @IntoMap + @IntKey(310) + abstract fun bindObjectivesPlugin(plugin: ObjectivesPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(320) + abstract fun bindFoodPlugin(plugin: FoodPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(330) + abstract fun bindWearPlugin(plugin: WearPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(340) + abstract fun bindStatusLinePlugin(plugin: StatusLinePlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(350) + abstract fun bindPersistentNotificationPlugin(plugin: PersistentNotificationPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(360) + abstract fun bindNSClientPlugin(plugin: NSClientPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(370) + abstract fun bindMaintenancePlugin(plugin: MaintenancePlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(380) + abstract fun bindDstHelperPlugin(plugin: DstHelperPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(390) + abstract fun bindDataBroadcastPlugin(plugin: DataBroadcastPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(400) + abstract fun bindXdripPlugin(plugin: XdripPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(410) + abstract fun bindNSClientSourcePlugin(plugin: NSClientSourcePlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(420) + abstract fun bindMM640gPlugin(plugin: MM640gPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(430) + abstract fun bindGlimpPlugin(plugin: GlimpPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(440) + abstract fun bindDexcomPlugin(plugin: DexcomPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(450) + abstract fun bindPoctechPlugin(plugin: PoctechPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(460) + abstract fun bindTomatoPlugin(plugin: TomatoPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(470) + abstract fun bindRandomBgPlugin(plugin: RandomBgPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(480) + abstract fun bindConfigBuilderPlugin(plugin: ConfigBuilderPlugin): PluginBase + + @Qualifier + annotation class AllConfigs + + @Qualifier + annotation class PumpDriver + + @Qualifier + annotation class NotNSClient + + @Qualifier + annotation class APS + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/PluginStore.kt b/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/PluginStore.kt index 0bce3e34eb..31692b93bb 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/PluginStore.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/configBuilder/PluginStore.kt @@ -6,6 +6,7 @@ import info.nightscout.androidaps.logging.LTag import java.util.* import javax.inject.Inject import javax.inject.Singleton +import kotlin.collections.ArrayList @Singleton class PluginStore @Inject constructor( @@ -26,8 +27,7 @@ class PluginStore @Inject constructor( return pluginStore!! } } - - var plugins = ArrayList() + lateinit var plugins: List<@JvmSuppressWildcards PluginBase> private var activeBgSource: BgSourceInterface? = null private var activePump: PumpInterface? = null @@ -41,11 +41,6 @@ class PluginStore @Inject constructor( verifySelectionInCategories() } - fun add(pluginBase: PluginBase): ActivePluginProvider { - plugins.add(pluginBase) - return this - } - fun getDefaultPlugin(type: PluginType): PluginBase { for (p in plugins) if (p.getType() == type && p.isDefault()) return p @@ -243,6 +238,6 @@ class PluginStore @Inject constructor( override fun getActiveTreatments(): TreatmentsInterface = activeTreatments ?: checkNotNull(activeTreatments) { "No treatments selected" } - override fun getPluginsList(): ArrayList = plugins + override fun getPluginsList(): ArrayList = ArrayList(plugins) } \ No newline at end of file From fde84207aba863726a07fa79cd6e4644b7a4f6b3 Mon Sep 17 00:00:00 2001 From: dlvoy Date: Fri, 21 Feb 2020 08:26:58 +0100 Subject: [PATCH 06/14] Preferences Encryption - export as JSON --- .../dependencyInjection/AppModule.kt | 3 + .../maintenance/ImportExportPrefs.java | 129 ---------------- .../general/maintenance/ImportExportPrefs.kt | 142 ++++++++++++++++++ .../maintenance/MaintenanceFragment.kt | 9 +- .../maintenance/formats/ClassicPrefsFormat.kt | 54 +++++++ .../formats/EncryptedPrefsFormat.kt | 58 +++++++ .../maintenance/formats/PrefsFormat.kt | 26 ++++ .../androidaps/setupwizard/SWDefinition.kt | 5 +- 8 files changed, 291 insertions(+), 135 deletions(-) delete mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.java create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt index 511f216463..cc75cf16be 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt @@ -36,6 +36,7 @@ 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.overview.notifications.NotificationWithAction import info.nightscout.androidaps.plugins.general.smsCommunicator.AuthRequest import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensData @@ -249,6 +250,8 @@ open class AppModule { @ContributesAndroidInjector fun graphDataInjector(): GraphData + @ContributesAndroidInjector fun importExportPrefsInjector(): ImportExportPrefs + @Binds fun bindContext(mainApp: MainApp): Context @Binds fun bindInjector(mainApp: MainApp): HasAndroidInjector diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.java deleted file mode 100644 index 59c8e87144..0000000000 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.java +++ /dev/null @@ -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 prefsMap = prefs.getAll(); - for (Map.Entry 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); - } - }); - } -} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt new file mode 100644 index 0000000000..ede8d79bc5 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt @@ -0,0 +1,142 @@ +package info.nightscout.androidaps.plugins.general.maintenance + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Environment +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import info.nightscout.androidaps.R +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.utils.OKDialog.show +import info.nightscout.androidaps.utils.OKDialog.showConfirmation +import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +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 +) + +@Singleton +class ImportExportPrefs @Inject constructor ( + private var log: AAPSLogger, + private val resourceHelper: ResourceHelper, + private val sp : SP, + private val rxBus: RxBusWrapper +) +{ + + 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) { + exportSharedPreferences(f.context) + } + + fun verifyStoragePermissions(fragment: Fragment) { + val permission = ContextCompat.checkSelfPermission(fragment.context!!, + 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 exportSharedPreferences(context: Context?) { + showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", Runnable { + try { + val entries: MutableMap = mutableMapOf() + for ((key, value) in sp.getAll()) { + entries[key] = value.toString() + } + + val prefs = Prefs(entries, mapOf()) + + ClassicPrefsFormat.savePreferences(file, prefs) + EncryptedPrefsFormat.savePreferences(encFile, prefs) + + ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.exported)) + } catch (e: FileNotFoundException) { + ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + encFile) + log.error(TAG,"Unhandled exception", e) + } catch (e: IOException) { + log.error(TAG,"Unhandled exception", e) + } + }) + } + + fun importSharedPreferences(fragment: Fragment) { + importSharedPreferences(fragment.context) + } + + fun importSharedPreferences(context: Context?) { + + val importFile = prefsImportFile() + + showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.import_from) + " " + importFile + " ?", Runnable { + + val format : PrefsFormat = if (encFile.exists()) EncryptedPrefsFormat else ClassicPrefsFormat + + try { + val prefs = format.loadPreferences(importFile) + + sp.clear() + for ((key, value) in prefs.values) { + if (value == "true" || value == "false") { + sp.putBoolean(key, value.toBoolean()) + } else { + sp.putString(key, value) + } + } + + sp.putBoolean(R.string.key_setupwizard_processed, true) + show(context, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable { + log.debug(TAG,"Exiting") + rxBus.send(EventAppExit()) + if (context is Activity) { + context.finish() + } + System.runFinalization() + System.exit(0) + }) + + + } catch (e: PrefFileNotFoundError) { + ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + importFile) + log.error(TAG,"Unhandled exception", e) + } catch (e: PrefIOError) { + log.error(TAG,"Unhandled exception", e) + } + + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenanceFragment.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenanceFragment.kt index 7de5d303ed..2e42294fc1 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenanceFragment.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenanceFragment.kt @@ -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)) } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt new file mode 100644 index 0000000000..c71775c33c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt @@ -0,0 +1,54 @@ +package info.nightscout.androidaps.plugins.general.maintenance.formats + +import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs +import java.io.* + + +object ClassicPrefsFormat : PrefsFormat { + + const val FORMAT_KEY = "old" + + override fun savePreferences(file:File, prefs: Prefs) { + try { + val fw = FileWriter(file) + val pw = PrintWriter(fw) + for ((key, value) in prefs.values) { + pw.println(key + "::" + value) + } + pw.close() + fw.close() + } catch (e: FileNotFoundException) { + throw PrefFileNotFoundError(file.absolutePath) + } catch (e: IOException) { + throw PrefIOError(file.absolutePath) + } + } + + override fun loadPreferences(file:File): Prefs { + var line: String + var lineParts: Array + val entries: MutableMap = mutableMapOf() + val metadata: MutableMap = mutableMapOf() + try { + val reader = BufferedReader(FileReader(file)) + while (reader.readLine().also { line = it } != null) { + lineParts = line.split("::").toTypedArray() + if (lineParts.size == 2) { + entries[lineParts[0]] = lineParts[1] + } + } + reader.close() + + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN) + + return Prefs(entries, metadata) + + } catch (e: FileNotFoundException) { + throw PrefFileNotFoundError(file.absolutePath) + } catch (e: IOException) { + throw PrefIOError(file.absolutePath) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt new file mode 100644 index 0000000000..7e32029c46 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt @@ -0,0 +1,58 @@ +package info.nightscout.androidaps.plugins.general.maintenance.formats + +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException + +object EncryptedPrefsFormat : PrefsFormat { + + const val FORMAT_KEY = "new_v1" + + override fun savePreferences(file:File, prefs: Prefs) { + + val container = JSONObject() + + try { + for ((key, value) in prefs.values) { + container.put(key, value) + } + + file.writeText(container.toString(2)); + + } catch (e: FileNotFoundException) { + throw PrefFileNotFoundError(file.absolutePath) + } catch (e: IOException) { + throw PrefIOError(file.absolutePath) + } + } + + override fun loadPreferences(file:File): Prefs { + + val entries: MutableMap = mutableMapOf() + val metadata: MutableMap = mutableMapOf() + try { + + val jsonBody = file.readText() + val container = JSONObject(jsonBody) + + for (key in container.keys()) { + entries.put(key, container[key].toString()) + } + + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.OK) + + 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) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt new file mode 100644 index 0000000000..d8beb30786 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt @@ -0,0 +1,26 @@ +package info.nightscout.androidaps.plugins.general.maintenance.formats + +import java.io.File + +enum class PrefsMetadataKey(val key: String) { + FILE_FORMAT("fileFormat") +} + +data class PrefMetadata(var value : String, var status : PrefsStatus) + +data class Prefs(val values : Map, val metadata : Map) + +interface PrefsFormat { + fun savePreferences(file: File, prefs: Prefs) + fun loadPreferences(file: File) : Prefs +} + +enum class PrefsStatus { + OK, + WARN, + ERROR +} + +class PrefFileNotFoundError(message: String) : Exception(message) +class PrefIOError(message: String) : Exception(message) +class PrefFormatError(message: String) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt b/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt index 5164e8388e..3912b48433 100644 --- a/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt +++ b/app/src/main/java/info/nightscout/androidaps/setupwizard/SWDefinition.kt @@ -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) From 1c97f8a720fd970ced021201bdf506a26c2a1165 Mon Sep 17 00:00:00 2001 From: Dominik Dzienia Date: Tue, 11 Feb 2020 19:34:56 +0100 Subject: [PATCH 07/14] Preferences Encryption: - encrypted JSON format support - using master password & password prompt - refactored alerts --- .../dependencyInjection/AppModule.kt | 14 +- .../dependencyInjection/FragmentsModule.kt | 3 + .../general/maintenance/ImportExportPrefs.kt | 286 +++++++++++++++--- .../general/maintenance/MaintenancePlugin.kt | 12 + .../maintenance/formats/ClassicPrefsFormat.kt | 46 +-- .../formats/EncryptedPrefsFormat.kt | 199 +++++++++++- .../maintenance/formats/PrefsFormat.kt | 69 ++++- .../nightscout/androidaps/utils/OKDialog.kt | 66 ++-- .../androidaps/utils/ToastUtils.java | 72 ++++- .../nightscout/androidaps/utils/UIUtils.kt | 6 + .../utils/alertDialogs/AlertDialogHelper.kt | 31 ++ .../alertDialogs/PrefImportSummaryDialog.kt | 140 +++++++++ .../alertDialogs/TwoMessagesAlertDialog.kt | 51 ++++ .../utils/alertDialogs/WarningDialog.kt | 49 +++ .../utils/protection/PasswordCheck.kt | 44 +-- .../androidaps/utils/storage/FileStrorage.kt | 17 ++ .../androidaps/utils/storage/Storage.kt | 11 + .../main/res/drawable/alert_border_error.xml | 15 + .../res/drawable/alert_border_warning.xml | 15 + app/src/main/res/drawable/ic_header_error.xml | 16 + .../main/res/drawable/ic_header_export.xml | 16 + .../main/res/drawable/ic_header_import.xml | 16 + app/src/main/res/drawable/ic_header_key.xml | 16 + app/src/main/res/drawable/ic_header_log.xml | 16 + .../main/res/drawable/ic_header_warning.xml | 16 + app/src/main/res/drawable/ic_key_48dp.xml | 17 -- app/src/main/res/drawable/ic_meta_date.xml | 9 + .../main/res/drawable/ic_meta_encryption.xml | 9 + app/src/main/res/drawable/ic_meta_error.xml | 10 + app/src/main/res/drawable/ic_meta_flavour.xml | 9 + app/src/main/res/drawable/ic_meta_format.xml | 9 + app/src/main/res/drawable/ic_meta_model.xml | 9 + app/src/main/res/drawable/ic_meta_name.xml | 9 + app/src/main/res/drawable/ic_meta_ok.xml | 10 + app/src/main/res/drawable/ic_meta_version.xml | 9 + app/src/main/res/drawable/ic_meta_warning.xml | 10 + app/src/main/res/drawable/ic_toast_check.xml | 10 + .../res/drawable/ic_toast_delete_confirm.xml | 10 + app/src/main/res/drawable/ic_toast_error.xml | 10 + app/src/main/res/drawable/ic_toast_info.xml | 10 + app/src/main/res/drawable/ic_toast_warn.xml | 10 + app/src/main/res/drawable/toast_border_ok.xml | 12 + .../main/res/layout/dialog_alert_custom.xml | 19 +- .../layout/dialog_alert_import_summary.xml | 30 ++ .../res/layout/dialog_alert_two_messages.xml | 19 ++ .../res/layout/import_summary_details.xml | 14 + .../main/res/layout/import_summary_item.xml | 36 +++ app/src/main/res/layout/toast.xml | 32 ++ app/src/main/res/values/attrs.xml | 7 + app/src/main/res/values/colors.xml | 19 ++ app/src/main/res/values/protection.xml | 1 + app/src/main/res/values/strings.xml | 47 +++ app/src/main/res/values/styles.xml | 27 ++ app/src/main/res/xml/pref_maintenance.xml | 4 + .../maintenance/ClassicPrefsFormatTest.kt | 65 ++++ .../maintenance/EncryptedPrefsFormatTest.kt | 226 ++++++++++++++ .../testing/mockers/AAPSMocker.java | 56 ++++ .../testing/mocks/SharedPreferencesMock.java | 158 ++++++++++ .../testing/utils/SingleStringStorage.kt | 26 ++ 59 files changed, 2004 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt create mode 100644 app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt create mode 100644 app/src/main/res/drawable/alert_border_error.xml create mode 100644 app/src/main/res/drawable/alert_border_warning.xml create mode 100644 app/src/main/res/drawable/ic_header_error.xml create mode 100644 app/src/main/res/drawable/ic_header_export.xml create mode 100644 app/src/main/res/drawable/ic_header_import.xml create mode 100644 app/src/main/res/drawable/ic_header_key.xml create mode 100644 app/src/main/res/drawable/ic_header_log.xml create mode 100644 app/src/main/res/drawable/ic_header_warning.xml delete mode 100644 app/src/main/res/drawable/ic_key_48dp.xml create mode 100644 app/src/main/res/drawable/ic_meta_date.xml create mode 100644 app/src/main/res/drawable/ic_meta_encryption.xml create mode 100644 app/src/main/res/drawable/ic_meta_error.xml create mode 100644 app/src/main/res/drawable/ic_meta_flavour.xml create mode 100644 app/src/main/res/drawable/ic_meta_format.xml create mode 100644 app/src/main/res/drawable/ic_meta_model.xml create mode 100644 app/src/main/res/drawable/ic_meta_name.xml create mode 100644 app/src/main/res/drawable/ic_meta_ok.xml create mode 100644 app/src/main/res/drawable/ic_meta_version.xml create mode 100644 app/src/main/res/drawable/ic_meta_warning.xml create mode 100644 app/src/main/res/drawable/ic_toast_check.xml create mode 100644 app/src/main/res/drawable/ic_toast_delete_confirm.xml create mode 100644 app/src/main/res/drawable/ic_toast_error.xml create mode 100644 app/src/main/res/drawable/ic_toast_info.xml create mode 100644 app/src/main/res/drawable/ic_toast_warn.xml create mode 100644 app/src/main/res/drawable/toast_border_ok.xml create mode 100644 app/src/main/res/layout/dialog_alert_import_summary.xml create mode 100644 app/src/main/res/layout/dialog_alert_two_messages.xml create mode 100644 app/src/main/res/layout/import_summary_details.xml create mode 100644 app/src/main/res/layout/import_summary_item.xml create mode 100644 app/src/main/res/layout/toast.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/ClassicPrefsFormatTest.kt create mode 100644 app/src/test/java/info/nightscout/androidaps/plugins/general/maintenance/EncryptedPrefsFormatTest.kt create mode 100644 app/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java create mode 100644 app/src/test/java/info/nightscout/androidaps/testing/mocks/SharedPreferencesMock.java create mode 100644 app/src/test/java/info/nightscout/androidaps/testing/utils/SingleStringStorage.kt diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt index cc75cf16be..4a081f85fb 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/AppModule.kt @@ -26,7 +26,6 @@ import info.nightscout.androidaps.plugins.aps.openAPSMA.DetermineBasalResultMA import info.nightscout.androidaps.plugins.aps.openAPSMA.LoggerCallback import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalAdapterSMBJS import info.nightscout.androidaps.plugins.aps.openAPSSMB.DetermineBasalResultSMB -import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin import info.nightscout.androidaps.plugins.configBuilder.PluginStore import info.nightscout.androidaps.plugins.configBuilder.ProfileFunction import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctionImplementation @@ -37,6 +36,8 @@ import info.nightscout.androidaps.plugins.general.automation.elements.* import info.nightscout.androidaps.plugins.general.automation.triggers.* import info.nightscout.androidaps.plugins.general.overview.graphData.GraphData import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs +import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat +import info.nightscout.androidaps.plugins.general.maintenance.formats.EncryptedPrefsFormat import info.nightscout.androidaps.plugins.general.overview.notifications.NotificationWithAction import info.nightscout.androidaps.plugins.general.smsCommunicator.AuthRequest import info.nightscout.androidaps.plugins.iob.iobCobCalculator.AutosensData @@ -46,7 +47,6 @@ import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobThread import info.nightscout.androidaps.plugins.treatments.Treatment import info.nightscout.androidaps.queue.CommandQueue import info.nightscout.androidaps.queue.commands.* -import info.nightscout.androidaps.setupwizard.SWDefinition import info.nightscout.androidaps.setupwizard.SWEventListener import info.nightscout.androidaps.setupwizard.SWScreen import info.nightscout.androidaps.setupwizard.elements.* @@ -55,6 +55,8 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.resources.ResourceHelperImplementation import info.nightscout.androidaps.utils.sharedPreferences.SP import info.nightscout.androidaps.utils.sharedPreferences.SPImplementation +import info.nightscout.androidaps.utils.storage.FileStorage +import info.nightscout.androidaps.utils.storage.Storage import info.nightscout.androidaps.utils.wizard.BolusWizard import info.nightscout.androidaps.utils.wizard.QuickWizardEntry import javax.inject.Singleton @@ -104,6 +106,12 @@ open class AppModule { return plugins.toList().sortedBy { it.first }.map { it.second } } + @Provides + @Singleton + fun provideStorage(): Storage { + return FileStorage() + } + @Module interface AppBindings { @@ -251,6 +259,8 @@ open class AppModule { @ContributesAndroidInjector fun graphDataInjector(): GraphData @ContributesAndroidInjector fun importExportPrefsInjector(): ImportExportPrefs + @ContributesAndroidInjector fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat + @ContributesAndroidInjector fun classicPrefsFormatInjector(): ClassicPrefsFormat @Binds fun bindContext(mainApp: MainApp): Context @Binds fun bindInjector(mainApp: MainApp): HasAndroidInjector diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt index 63ed16fb4c..d151912151 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/FragmentsModule.kt @@ -40,6 +40,7 @@ import info.nightscout.androidaps.plugins.pump.virtual.VirtualPumpFragment import info.nightscout.androidaps.plugins.source.BGSourceFragment import info.nightscout.androidaps.plugins.treatments.TreatmentsFragment import info.nightscout.androidaps.plugins.treatments.fragments.* +import info.nightscout.androidaps.utils.protection.PasswordCheck @Module @Suppress("unused") @@ -112,4 +113,6 @@ abstract class FragmentsModule { @ContributesAndroidInjector abstract fun contributesTreatmentDialog(): TreatmentDialog @ContributesAndroidInjector abstract fun contributesWizardDialog(): WizardDialog @ContributesAndroidInjector abstract fun contributesWizardInfoDialog(): WizardInfoDialog + + @ContributesAndroidInjector abstract fun contributesPasswordCheck(): PasswordCheck } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt index ede8d79bc5..f5ad429641 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt @@ -2,25 +2,42 @@ package info.nightscout.androidaps.plugins.general.maintenance import android.Manifest import android.app.Activity +import android.bluetooth.BluetoothAdapter import android.content.Context +import android.content.Intent import android.content.pm.PackageManager +import android.os.Build import android.os.Environment +import android.provider.Settings +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import info.nightscout.androidaps.BuildConfig import info.nightscout.androidaps.R +import info.nightscout.androidaps.activities.PreferencesActivity import info.nightscout.androidaps.events.EventAppExit import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.logging.LTag import info.nightscout.androidaps.plugins.bus.RxBusWrapper import info.nightscout.androidaps.plugins.general.maintenance.formats.* +import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword +import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.OKDialog import info.nightscout.androidaps.utils.OKDialog.show -import info.nightscout.androidaps.utils.OKDialog.showConfirmation import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.alertDialogs.PrefImportSummaryDialog +import info.nightscout.androidaps.utils.alertDialogs.TwoMessagesAlertDialog +import info.nightscout.androidaps.utils.alertDialogs.WarningDialog +import info.nightscout.androidaps.utils.buildHelper.BuildHelper +import info.nightscout.androidaps.utils.protection.PasswordCheck import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP +import org.joda.time.DateTime +import org.joda.time.Days import java.io.File import java.io.FileNotFoundException import java.io.IOException +import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -34,14 +51,20 @@ private val PERMISSIONS_STORAGE = arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE ) +private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60 + @Singleton -class ImportExportPrefs @Inject constructor ( +class ImportExportPrefs @Inject constructor( private var log: AAPSLogger, private val resourceHelper: ResourceHelper, - private val sp : SP, - private val rxBus: RxBusWrapper -) -{ + private val sp: SP, + private val buildHelper: BuildHelper, + private val otp: OneTimePassword, + private val rxBus: RxBusWrapper, + private val passwordCheck: PasswordCheck, + private val classicPrefsFormat: ClassicPrefsFormat, + private val encryptedPrefsFormat: EncryptedPrefsFormat +) { val TAG = LTag.CORE @@ -50,16 +73,16 @@ class ImportExportPrefs @Inject constructor ( private val file = File(path, resourceHelper.gs(R.string.app_name) + "Preferences") private val encFile = File(path, resourceHelper.gs(R.string.app_name) + "Preferences.json") - fun prefsImportFile() : File { + fun prefsImportFile(): File { return if (encFile.exists()) encFile else file } - fun prefsFileExists() : Boolean { + fun prefsFileExists(): Boolean { return encFile.exists() || file.exists() } fun exportSharedPreferences(f: Fragment) { - exportSharedPreferences(f.context) + exportSharedPreferences(f.activity) } fun verifyStoragePermissions(fragment: Fragment) { @@ -71,72 +94,247 @@ class ImportExportPrefs @Inject constructor ( } } - private fun exportSharedPreferences(context: Context?) { - showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", Runnable { + private fun prepareMetadata(context: Context?): Map { + + val metadata: MutableMap = mutableMapOf() + + if (context != null) { + metadata[PrefsMetadataKey.DEVICE_NAME] = PrefMetadata(detectUserName(context), PrefsStatus.OK) + } + + metadata[PrefsMetadataKey.CREATED_AT] = PrefMetadata(DateUtil.toISOString(Date()), PrefsStatus.OK) + metadata[PrefsMetadataKey.AAPS_VERSION] = PrefMetadata(BuildConfig.VERSION_NAME, PrefsStatus.OK) + metadata[PrefsMetadataKey.AAPS_FLAVOUR] = PrefMetadata(BuildConfig.FLAVOR, PrefsStatus.OK) + metadata[PrefsMetadataKey.DEVICE_MODEL] = PrefMetadata(getCurrentDeviceModelString(), PrefsStatus.OK) + + if (prefsEncryptionIsDisabled()) { + metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Disabled", PrefsStatus.DISABLED) + } else { + metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata("Enabled", PrefsStatus.OK) + } + + return metadata + } + + private fun detectUserName(context: Context): String { + // based on https://medium.com/@pribble88/how-to-get-an-android-device-nickname-4b4700b3068c + val n1 = Settings.System.getString(context.contentResolver, "bluetooth_name") + val n2 = Settings.Secure.getString(context.contentResolver, "bluetooth_name") + val n3 = BluetoothAdapter.getDefaultAdapter()?.name + val n4 = Settings.System.getString(context.contentResolver, "device_name") + val n5 = Settings.Secure.getString(context.contentResolver, "lock_screen_owner_info") + val n6 = Settings.Global.getString(context.contentResolver, "device_name") + + // name we use for SMS OTP token in communicator + val otpName = otp.name().trim() + val defaultOtpName = resourceHelper.gs(R.string.smscommunicator_default_user_display_name) + + // name we detect from OS + val systemName = n1 ?: n2 ?: n3 ?: n4 ?: n5 ?: n6 ?: defaultOtpName + val name = if (otpName.length > 0 && otpName != defaultOtpName) otpName else systemName + return name + } + + private fun getCurrentDeviceModelString() = + Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")" + + private fun prefsEncryptionIsDisabled() = + buildHelper.isEngineeringMode() && !sp.getBoolean(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs), true) + + private fun askForMasterPass(activity: Activity?, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) { + passwordCheck.queryPassword(activity!!, R.string.master_password, R.string.key_master_password, { password -> + then(password) + }, { + ToastUtils.warnToast(activity, resourceHelper.gs(canceledMsg)) + }) + } + + private fun askForMasterPassIfNeeded(activity: Activity?, @StringRes canceledMsg: Int, then: ((password: String) -> Unit)) { + if (prefsEncryptionIsDisabled()) { + then("") + } else { + askForMasterPass(activity, canceledMsg, then) + } + } + + private fun assureMasterPasswordSet(activity: Activity?, @StringRes wrongPwdTitle: Int): Boolean { + if (!sp.contains(R.string.key_master_password) || (sp.getString(R.string.key_master_password, "") == "")) { + WarningDialog.showWarning(activity!!, + resourceHelper.gs(wrongPwdTitle), + resourceHelper.gs(R.string.master_password_missing, resourceHelper.gs(R.string.configbuilder_general), resourceHelper.gs(R.string.protection)), + R.string.nav_preferences, { + val intent = Intent(activity, PreferencesActivity::class.java).apply { + putExtra("id", R.xml.pref_general) + } + activity.startActivity(intent) + }) + return false + } + return true + } + + private fun askToConfirmExport(activity: Activity?, then: ((password: String) -> Unit)) { + if (!prefsEncryptionIsDisabled() && !assureMasterPasswordSet(activity, R.string.nav_export)) return + + TwoMessagesAlertDialog.showAlert(activity!!, resourceHelper.gs(R.string.nav_export), + resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", + resourceHelper.gs(R.string.password_preferences_encrypt_prompt), { + askForMasterPassIfNeeded(activity, R.string.preferences_export_canceled, then) + }, null, R.drawable.ic_header_export) + } + + private fun askToConfirmImport(activity: Activity?, fileToImport: File, then: ((password: String) -> Unit)) { + + if (encFile.exists()) { + if (!assureMasterPasswordSet(activity, R.string.nav_import)) return + + TwoMessagesAlertDialog.showAlert(activity!!, resourceHelper.gs(R.string.nav_import), + resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", + resourceHelper.gs(R.string.password_preferences_decrypt_prompt), { + askForMasterPass(activity, R.string.preferences_import_canceled, then) + }, null, R.drawable.ic_header_import) + + } else { + OKDialog.showConfirmation(activity!!, resourceHelper.gs(R.string.nav_import), + resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", + Runnable { then("") }) + } + } + + private fun exportSharedPreferences(activity: Activity?) { + askToConfirmExport(activity) { password -> try { val entries: MutableMap = mutableMapOf() for ((key, value) in sp.getAll()) { entries[key] = value.toString() } - val prefs = Prefs(entries, mapOf()) + val prefs = Prefs(entries, prepareMetadata(activity)) - ClassicPrefsFormat.savePreferences(file, prefs) - EncryptedPrefsFormat.savePreferences(encFile, prefs) + classicPrefsFormat.savePreferences(file, prefs) + encryptedPrefsFormat.savePreferences(encFile, prefs, password) - ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.exported)) + ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported)) } catch (e: FileNotFoundException) { - ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + encFile) - log.error(TAG,"Unhandled exception", e) + ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + encFile) + log.error(TAG, "Unhandled exception", e) } catch (e: IOException) { - log.error(TAG,"Unhandled exception", e) + ToastUtils.errorToast(activity, e.message) + log.error(TAG, "Unhandled exception", e) } - }) + } } fun importSharedPreferences(fragment: Fragment) { - importSharedPreferences(fragment.context) + importSharedPreferences(fragment.activity) } - fun importSharedPreferences(context: Context?) { + fun importSharedPreferences(activity: Activity?) { val importFile = prefsImportFile() - showConfirmation(context!!, resourceHelper.gs(R.string.maintenance), resourceHelper.gs(R.string.import_from) + " " + importFile + " ?", Runnable { + askToConfirmImport(activity, importFile) { password -> - val format : PrefsFormat = if (encFile.exists()) EncryptedPrefsFormat else ClassicPrefsFormat + val format: PrefsFormat = if (encFile.exists()) encryptedPrefsFormat else classicPrefsFormat try { - val prefs = format.loadPreferences(importFile) - sp.clear() - for ((key, value) in prefs.values) { - if (value == "true" || value == "false") { - sp.putBoolean(key, value.toBoolean()) + val prefs = format.loadPreferences(importFile, password) + prefs.metadata = checkMetadata(prefs.metadata) + + // import is OK when we do not have errors (warnings are allowed) + val importOk = checkIfImportIsOk(prefs) + + // if at end we allow to import preferences + val importPossible = (importOk || buildHelper.isEngineeringMode()) && (prefs.values.size > 0) + + PrefImportSummaryDialog.showSummary(activity!!, importOk, importPossible, prefs, { + if (importPossible) { + sp.clear() + for ((key, value) in prefs.values) { + if (value == "true" || value == "false") { + sp.putBoolean(key, value.toBoolean()) + } else { + sp.putString(key, value) + } + } + + restartAppAfterImport(activity) } else { - sp.putString(key, value) + // for impossible imports it should not be called + ToastUtils.errorToast(activity, "Cannot import preferences!") } - } - - sp.putBoolean(R.string.key_setupwizard_processed, true) - show(context, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable { - log.debug(TAG,"Exiting") - rxBus.send(EventAppExit()) - if (context is Activity) { - context.finish() - } - System.runFinalization() - System.exit(0) }) - } catch (e: PrefFileNotFoundError) { - ToastUtils.showToastInUiThread(context, resourceHelper.gs(R.string.filenotfound) + " " + importFile) - log.error(TAG,"Unhandled exception", e) + ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + importFile) + log.error(TAG, "Unhandled exception", e) } catch (e: PrefIOError) { - log.error(TAG,"Unhandled exception", e) + log.error(TAG, "Unhandled exception", e) + ToastUtils.errorToast(activity, e.message) } + } + } + // check metadata for known issues, change their status and add info with explanations + private fun checkMetadata(metadata: Map): Map { + val meta = metadata.toMutableMap() + + if (meta.containsKey(PrefsMetadataKey.AAPS_FLAVOUR)) { + val flavourOfPrefs = meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.value + if (meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.value != BuildConfig.FLAVOR) { + meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.AAPS_FLAVOUR]!!.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR) + } + } + + if (meta.containsKey(PrefsMetadataKey.DEVICE_MODEL)) { + if (meta[PrefsMetadataKey.DEVICE_MODEL]!!.value != getCurrentDeviceModelString()) { + meta[PrefsMetadataKey.DEVICE_MODEL]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.DEVICE_MODEL]!!.info = resourceHelper.gs(R.string.metadata_warning_different_device) + } + } + + if (meta.containsKey(PrefsMetadataKey.CREATED_AT)) { + try { + val date1 = DateTime.parse(meta[PrefsMetadataKey.CREATED_AT]!!.value); + val date2 = DateTime.now() + + val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays() + + if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) { + meta[PrefsMetadataKey.CREATED_AT]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.CREATED_AT]!!.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString()) + } + } catch (e: Exception) { + meta[PrefsMetadataKey.CREATED_AT]!!.status = PrefsStatus.WARN + meta[PrefsMetadataKey.CREATED_AT]!!.info = resourceHelper.gs(R.string.metadata_warning_date_format) + } + } + + return meta + } + + private fun checkIfImportIsOk(prefs: Prefs): Boolean { + var importOk = true + + for ((_, value) in prefs.metadata) { + if (value.status == PrefsStatus.ERROR) + importOk = false; + } + return importOk + } + + private fun restartAppAfterImport(context: Context?) { + sp.putBoolean(R.string.key_setupwizard_processed, true) + show(context!!, resourceHelper.gs(R.string.setting_imported), resourceHelper.gs(R.string.restartingapp), Runnable { + log.debug(TAG, "Exiting") + rxBus.send(EventAppExit()) + if (context is Activity) { + context.finish() + } + System.runFinalization() + System.exit(0) }) } } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt index cd4d333995..4a3e5e83ab 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/MaintenancePlugin.kt @@ -4,6 +4,8 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.core.content.FileProvider +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference import dagger.android.HasAndroidInjector import info.nightscout.androidaps.BuildConfig import info.nightscout.androidaps.Config @@ -16,6 +18,7 @@ import info.nightscout.androidaps.plugins.general.nsclient.data.NSSettingsStatus import info.nightscout.androidaps.utils.buildHelper.BuildHelper import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP +import info.nightscout.androidaps.utils.textValidator.ValidatingEditTextPreference import java.io.* import java.util.* import java.util.zip.ZipEntry @@ -203,4 +206,13 @@ class MaintenancePlugin @Inject constructor( emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) return emailIntent } + + override fun preprocessPreferences(preferenceFragment: PreferenceFragmentCompat) { + super.preprocessPreferences(preferenceFragment) + val encryptSwitch = preferenceFragment.findPreference(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs)) as SwitchPreference? + ?: return + encryptSwitch.isVisible = buildHelper.isEngineeringMode() + encryptSwitch.isEnabled = buildHelper.isEngineeringMode() + } + } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt index c71775c33c..4689f549a6 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt @@ -1,22 +1,30 @@ package info.nightscout.androidaps.plugins.general.maintenance.formats -import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs -import java.io.* +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.storage.Storage +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton +@Singleton +class ClassicPrefsFormat @Inject constructor( + private var resourceHelper: ResourceHelper, + private var storage: Storage +) : PrefsFormat { -object ClassicPrefsFormat : PrefsFormat { + companion object { + val FORMAT_KEY = "aaps_old" + } - const val FORMAT_KEY = "old" - - override fun savePreferences(file:File, prefs: Prefs) { + override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { try { - val fw = FileWriter(file) - val pw = PrintWriter(fw) - for ((key, value) in prefs.values) { - pw.println(key + "::" + value) + val contents = prefs.values.entries.joinToString("\n") { entry -> + "${entry.key}::${entry.value}" } - pw.close() - fw.close() + storage.putFileContents(file, contents) } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) } catch (e: IOException) { @@ -24,31 +32,29 @@ object ClassicPrefsFormat : PrefsFormat { } } - override fun loadPreferences(file:File): Prefs { - var line: String + override fun loadPreferences(file: File, masterPassword: String?): Prefs { var lineParts: Array val entries: MutableMap = mutableMapOf() val metadata: MutableMap = mutableMapOf() try { - val reader = BufferedReader(FileReader(file)) - while (reader.readLine().also { line = it } != null) { + + val rawLines = storage.getFileContents(file).split("\n") + rawLines.forEach { line -> lineParts = line.split("::").toTypedArray() if (lineParts.size == 2) { entries[lineParts[0]] = lineParts[1] } } - reader.close() - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN) + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format)) return Prefs(entries, metadata) - } catch (e: FileNotFoundException) { + } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) } catch (e: IOException) { throw PrefIOError(file.absolutePath) } } - } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt index 7e32029c46..a2debad281 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt @@ -1,25 +1,90 @@ package info.nightscout.androidaps.plugins.general.maintenance.formats +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.CryptoUtil +import info.nightscout.androidaps.utils.hexStringToByteArray +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.storage.Storage +import info.nightscout.androidaps.utils.toHex import org.json.JSONException import org.json.JSONObject import java.io.File import java.io.FileNotFoundException import java.io.IOException +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton -object EncryptedPrefsFormat : PrefsFormat { +@Singleton +class EncryptedPrefsFormat @Inject constructor( + private var resourceHelper: ResourceHelper, + private var storage: Storage +) : PrefsFormat { - const val FORMAT_KEY = "new_v1" + companion object { + val FORMAT_KEY_ENC = "aaps_encrypted" + val FORMAT_KEY_NOENC = "aaps_structured" - override fun savePreferences(file:File, prefs: Prefs) { + private val KEY_CONSCIENCE = "if you remove/change this, please make sure you know the consequences!" + } + + override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { val container = JSONObject() + val content = JSONObject() + val meta = JSONObject() + + val encStatus = prefs.metadata[PrefsMetadataKey.ENCRYPTION]?.status ?: PrefsStatus.OK + var encrypted = encStatus == PrefsStatus.OK && masterPassword != null try { - for ((key, value) in prefs.values) { - container.put(key, value) + for ((key, value) in prefs.values.toSortedMap()) { + content.put(key, value) } - file.writeText(container.toString(2)); + for ((metaKey, metaEntry) in prefs.metadata) { + if (metaKey == PrefsMetadataKey.FILE_FORMAT) + continue; + if (metaKey == PrefsMetadataKey.ENCRYPTION) + continue; + meta.put(metaKey.key, metaEntry.value) + } + + container.put(PrefsMetadataKey.FILE_FORMAT.key, if (encrypted) FORMAT_KEY_ENC else FORMAT_KEY_NOENC); + container.put("metadata", meta) + + val security = JSONObject() + security.put("file_hash", "--to-be-calculated--") + var encodedContent = "" + + if (encrypted) { + val salt = CryptoUtil.mineSalt() + val rawContent = content.toString() + val contentAttempt = CryptoUtil.encrypt(masterPassword!!, salt, rawContent) + if (contentAttempt != null) { + encodedContent = contentAttempt + security.put("algorithm", "v1") + security.put("salt", salt.toHex()) + security.put("content_hash", CryptoUtil.sha256(rawContent)) + } else { + // fallback when encryption does not work + encrypted = false + } + } + + if (!encrypted) { + security.put("algorithm", "none") + } + + container.put("security", security) + container.put("content", if (encrypted) encodedContent else content) + + var fileContents = container.toString(2) + val fileHash = CryptoUtil.hmac256(fileContents, KEY_CONSCIENCE) + + fileContents = fileContents.replace(Regex("(\\\"file_hash\\\"\\s*\\:\\s*\\\")(--to-be-calculated--)(\\\")"), "$1" + fileHash + "$3") + + storage.putFileContents(file, fileContents) } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) @@ -28,31 +93,133 @@ object EncryptedPrefsFormat : PrefsFormat { } } - override fun loadPreferences(file:File): Prefs { + override fun loadPreferences(file: File, masterPassword: String?): Prefs { val entries: MutableMap = mutableMapOf() val metadata: MutableMap = mutableMapOf() + val issues = LinkedList() try { - val jsonBody = file.readText() + val jsonBody = storage.getFileContents(file) + val fileContents = jsonBody.replace(Regex("(?is)(\\\"file_hash\\\"\\s*\\:\\s*\\\")([^\"]*)(\\\")"), "$1--to-be-calculated--$3") + val calculatedFileHash = CryptoUtil.hmac256(fileContents, KEY_CONSCIENCE) val container = JSONObject(jsonBody) - for (key in container.keys()) { - entries.put(key, container[key].toString()) - } + if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) { + val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key) - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.OK) + if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) { + throw PrefFormatError("Unsupported file format: "+fileFormat) + } + + val meta = container.getJSONObject("metadata") + val security = container.getJSONObject("security") + + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK) + for (key in meta.keys()) { + val metaKey = PrefsMetadataKey.fromKey(key) + if (metaKey != null) { + metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK) + } + } + + val encrypted = fileFormat == FORMAT_KEY_ENC + var secure: PrefsStatus = PrefsStatus.OK + var decryptedOk = false + var contentJsonObj: JSONObject? = null + var insecurityReason = resourceHelper.gs(R.string.prefdecrypt_settings_tampered) + + if (security.has("file_hash")) { + if (calculatedFileHash != security.getString("file_hash")) { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_modified)) + } + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_missing_file_hash)) + } + + if (encrypted) { + if (security.has("algorithm") && security.get("algorithm") == "v1") { + if (security.has("salt") && security.has("content_hash")) { + + val salt = security.getString("salt").hexStringToByteArray() + val decrypted = CryptoUtil.decrypt(masterPassword!!, salt, container.getString("content")) + + if (decrypted != null) { + try { + val contentHash = CryptoUtil.sha256(decrypted) + + if (contentHash == security.getString("content_hash")) { + contentJsonObj = JSONObject(decrypted) + decryptedOk = true + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_modified)) + } + + } catch (e: JSONException) { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_parsing)) + } + + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_pass)) + insecurityReason = resourceHelper.gs(R.string.prefdecrypt_wrong_password) + } + + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_format)) + } + } else { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_algorithm)) + } + + } else { + + if (secure == PrefsStatus.OK) { + secure = PrefsStatus.WARN + } + + if (!(security.has("algorithm") && security.get("algorithm") == "none")) { + secure = PrefsStatus.ERROR + issues.add(resourceHelper.gs(R.string.prefdecrypt_issue_wrong_algorithm)) + } + + contentJsonObj = container.getJSONObject("content") + decryptedOk = true + } + + if (decryptedOk && contentJsonObj != null) { + for (key in contentJsonObj.keys()) { + entries.put(key, contentJsonObj[key].toString()) + } + } + + val issuesStr: String? = if (issues.size > 0) issues.joinToString("\n") else null + val encryptionDescStr = if (encrypted) { + if (secure == PrefsStatus.OK) resourceHelper.gs(R.string.prefdecrypt_settings_secure) else insecurityReason + } else { + if (secure != PrefsStatus.ERROR) resourceHelper.gs(R.string.prefdecrypt_settings_unencrypted) else resourceHelper.gs(R.string.prefdecrypt_settings_tampered) + } + + metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr) + } else { + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR) + } return Prefs(entries, metadata) - } catch (e: FileNotFoundException) { + } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) } catch (e: IOException) { throw PrefIOError(file.absolutePath) - } catch (e: JSONException){ - throw PrefFormatError("Mallformed preferences JSON file: "+e) + } catch (e: JSONException) { + throw PrefFormatError("Mallformed preferences JSON file: " + e) } } - } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt index d8beb30786..a47add0dd7 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt @@ -1,26 +1,73 @@ package info.nightscout.androidaps.plugins.general.maintenance.formats +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import info.nightscout.androidaps.R import java.io.File -enum class PrefsMetadataKey(val key: String) { - FILE_FORMAT("fileFormat") +enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringRes val label:Int) { + + FILE_FORMAT("format", R.drawable.ic_meta_format, R.string.metadata_label_format), + CREATED_AT("created_at", R.drawable.ic_meta_date, R.string.metadata_label_created_at), + AAPS_VERSION("aaps_version", R.drawable.ic_meta_version, R.string.metadata_label_aaps_version), + AAPS_FLAVOUR("aaps_flavour", R.drawable.ic_meta_flavour, R.string.metadata_label_aaps_flavour), + DEVICE_NAME("device_name", R.drawable.ic_meta_name, R.string.metadata_label_device_name), + DEVICE_MODEL("device_model", R.drawable.ic_meta_model, R.string.metadata_label_device_model), + ENCRYPTION("encryption", R.drawable.ic_meta_encryption, R.string.metadata_label_encryption); + + companion object { + private val keyToEnumMap = HashMap() + + init { + for (value in values()) { + keyToEnumMap.put(value.key, value) + } + } + + fun fromKey(key: String): PrefsMetadataKey? { + if (keyToEnumMap.containsKey(key)) { + return keyToEnumMap.get(key) + } else { + return null + } + } + + + } + + fun formatForDisplay(context: Context, value:String): String { + return when (this) { + FILE_FORMAT -> when (value) { + ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old) + EncryptedPrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new) + EncryptedPrefsFormat.FORMAT_KEY_NOENC -> context.getString(R.string.metadata_format_debug) + else -> context.getString(R.string.metadata_format_other) + } + CREATED_AT -> value.replace("T", " ").replace("Z", " (UTC)") + else -> value + } + } + } -data class PrefMetadata(var value : String, var status : PrefsStatus) +data class PrefMetadata(var value : String, var status : PrefsStatus, var info : String? = null) -data class Prefs(val values : Map, val metadata : Map) +data class Prefs(val values : Map, var metadata : Map) interface PrefsFormat { - fun savePreferences(file: File, prefs: Prefs) - fun loadPreferences(file: File) : Prefs + fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null) + fun loadPreferences(file: File, masterPassword: String? = null) : Prefs } -enum class PrefsStatus { - OK, - WARN, - ERROR +enum class PrefsStatus(@DrawableRes val icon:Int) { + OK(R.drawable.ic_meta_ok), + WARN(R.drawable.ic_meta_warning), + ERROR(R.drawable.ic_meta_error), + UNKNOWN(R.drawable.ic_meta_error), + DISABLED(R.drawable.ic_meta_error) } class PrefFileNotFoundError(message: String) : Exception(message) class PrefIOError(message: String) : Exception(message) -class PrefFormatError(message: String) : Exception(message) \ No newline at end of file +class PrefFormatError(message: String) : Exception(message) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt index 285b3aaa10..186b81ae36 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/OKDialog.kt @@ -4,17 +4,10 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.DialogInterface -import android.os.Handler import android.os.SystemClock import android.text.Spanned -import android.view.LayoutInflater -import android.view.View -import android.widget.ImageView -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.view.ContextThemeWrapper -import info.nightscout.androidaps.MainApp import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.alertDialogs.AlertDialogHelper object OKDialog { @SuppressLint("InflateParams") @@ -23,11 +16,9 @@ object OKDialog { fun show(context: Context, title: String, message: String, runnable: Runnable? = null) { var notEmptytitle = title if (notEmptytitle.isEmpty()) notEmptytitle = context.getString(R.string.message) - val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = notEmptytitle - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme)) - .setCustomTitle(titleLayout) + + AlertDialogHelper.Builder(context) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, notEmptytitle)) .setMessage(message) .setPositiveButton(context.getString(R.string.ok)) { dialog: DialogInterface, _: Int -> dialog.dismiss() @@ -38,23 +29,15 @@ object OKDialog { .setCanceledOnTouchOutside(false) } - fun runOnUiThread(theRunnable: Runnable?) { - @Suppress("DEPRECATION") - val mainHandler = Handler(MainApp.instance().applicationContext.mainLooper) - theRunnable?.let { mainHandler.post(it) } - } - @SuppressLint("InflateParams") @JvmStatic @JvmOverloads fun show(activity: Activity, title: String, message: Spanned, runnable: Runnable? = null) { var notEmptytitle = title if (notEmptytitle.isEmpty()) notEmptytitle = activity.getString(R.string.message) - val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = notEmptytitle - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme)) - .setCustomTitle(titleLayout) + + AlertDialogHelper.Builder(activity) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(activity, notEmptytitle)) .setMessage(message) .setPositiveButton(activity.getString(R.string.ok)) { dialog: DialogInterface, _: Int -> dialog.dismiss() @@ -79,12 +62,9 @@ object OKDialog { @JvmStatic @JvmOverloads fun showConfirmation(activity: Activity, title: String, message: Spanned, ok: Runnable?, cancel: Runnable? = null) { - val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme)) + AlertDialogHelper.Builder(activity) .setMessage(message) - .setCustomTitle(titleLayout) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(activity, title)) .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() SystemClock.sleep(100) @@ -103,12 +83,9 @@ object OKDialog { @SuppressLint("InflateParams") @JvmStatic fun showConfirmation(activity: Activity, title: String, message: String, ok: Runnable?, cancel: Runnable? = null) { - val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme)) + AlertDialogHelper.Builder(activity) .setMessage(message) - .setCustomTitle(titleLayout) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(activity, title)) .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() SystemClock.sleep(100) @@ -133,12 +110,9 @@ object OKDialog { @JvmStatic @JvmOverloads fun showConfirmation(context: Context, title: String, message: Spanned, ok: Runnable?, cancel: Runnable? = null) { - val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme)) + AlertDialogHelper.Builder(context) .setMessage(message) - .setCustomTitle(titleLayout) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title)) .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() SystemClock.sleep(100) @@ -164,12 +138,9 @@ object OKDialog { @JvmStatic @JvmOverloads fun showConfirmation(context: Context, title: String, message: String, ok: Runnable?, cancel: Runnable? = null) { - val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme)) + AlertDialogHelper.Builder(context) .setMessage(message) - .setCustomTitle(titleLayout) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title)) .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() SystemClock.sleep(100) @@ -188,12 +159,9 @@ object OKDialog { @JvmStatic @JvmOverloads fun showConfirmation(context: Context, title: String, message: String, ok: DialogInterface.OnClickListener?, cancel: DialogInterface.OnClickListener? = null) { - val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = title - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_check_while_48dp) - AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme)) + AlertDialogHelper.Builder(context) .setMessage(message) - .setCustomTitle(titleLayout) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title)) .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int -> dialog.dismiss() SystemClock.sleep(100) diff --git a/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java b/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java index 7822e8a80f..26062011f6 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/ToastUtils.java @@ -1,26 +1,94 @@ package info.nightscout.androidaps.utils; +import android.annotation.SuppressLint; import android.content.Context; import android.media.MediaPlayer; import android.os.Handler; import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.DrawableRes; +import androidx.appcompat.view.ContextThemeWrapper; + import info.nightscout.androidaps.MainApp; -import info.nightscout.androidaps.plugins.bus.RxBus; +import info.nightscout.androidaps.R; import info.nightscout.androidaps.plugins.bus.RxBusWrapper; import info.nightscout.androidaps.plugins.general.overview.events.EventNewNotification; import info.nightscout.androidaps.plugins.general.overview.notifications.Notification; public class ToastUtils { + + public static class Long { + + public static void warnToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_warn, false); + } + + public static void infoToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_info,false); + } + + public static void okToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_check,false); + } + + public static void errorToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_error,false); + } + } + public static void showToastInUiThread(final Context ctx, final int stringId) { showToastInUiThread(ctx, MainApp.gs(stringId)); } + public static void warnToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_warn, true); + } + + public static void infoToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_info, true); + } + + public static void okToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_check, true); + } + + public static void errorToast(final Context ctx, final String string) { + graphicalToast(ctx, string, R.drawable.ic_toast_error, true); + } + + public static void graphicalToast(final Context ctx, final String string, @DrawableRes int iconId) { + graphicalToast(ctx, string, iconId, true); + } + + @SuppressLint("InflateParams") + public static void graphicalToast(final Context ctx, final String string, @DrawableRes int iconId, boolean isShort) { + Handler mainThread = new Handler(Looper.getMainLooper()); + mainThread.post(() -> { + View toastRoot =LayoutInflater.from(new ContextThemeWrapper(ctx, R.style.AppTheme)).inflate(R.layout.toast, null); + TextView toastMessage = toastRoot.findViewById(android.R.id.message); + toastMessage.setText(string); + + ImageView toastIcon = toastRoot.findViewById(android.R.id.icon); + toastIcon.setImageResource(iconId); + + Toast toast = new Toast(ctx); + toast.setDuration(isShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG); + toast.setView(toastRoot); + toast.show(); + }); + } + public static void showToastInUiThread(final Context ctx, final String string) { Handler mainThread = new Handler(Looper.getMainLooper()); - mainThread.post(() -> Toast.makeText(ctx, string, Toast.LENGTH_SHORT).show()); + mainThread.post(() -> { + Toast.makeText(ctx, string, Toast.LENGTH_SHORT).show(); + }); } public static void showToastInUiThread(final Context ctx, final RxBusWrapper rxBus, diff --git a/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt b/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt index 99c4a4af31..ad57e519da 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/UIUtils.kt @@ -1,6 +1,8 @@ package info.nightscout.androidaps.utils +import android.os.Handler import android.view.View +import info.nightscout.androidaps.MainApp /** * Created by adrian on 2019-12-20. @@ -8,3 +10,7 @@ import android.view.View fun Boolean.toVisibility() = if (this) View.VISIBLE else View.GONE +fun runOnUiThread(theRunnable: Runnable?) { + val mainHandler = Handler(MainApp.instance().applicationContext.mainLooper) + theRunnable?.let { mainHandler.post(it) } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt new file mode 100644 index 0000000000..afc5491652 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/AlertDialogHelper.kt @@ -0,0 +1,31 @@ +package info.nightscout.androidaps.utils.alertDialogs + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.LayoutRes +import androidx.annotation.StyleRes +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.view.ContextThemeWrapper +import info.nightscout.androidaps.R + +object AlertDialogHelper { + + @Suppress("FunctionName") + fun Builder(context: Context, @StyleRes themeResId: Int = R.style.AppTheme) = + AlertDialog.Builder(ContextThemeWrapper(context, themeResId)) + + fun buildCustomTitle(context: Context, title: String, + @DrawableRes iconResource: Int = R.drawable.ic_check_while_48dp, + @StyleRes themeResId: Int = R.style.AppTheme, + @LayoutRes layoutResource: Int = R.layout.dialog_alert_custom): View? { + val titleLayout = LayoutInflater.from(ContextThemeWrapper(context, themeResId)).inflate(layoutResource, null) + (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = title + (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(iconResource) + return titleLayout + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt new file mode 100644 index 0000000000..db5731f8fc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/PrefImportSummaryDialog.kt @@ -0,0 +1,140 @@ +package info.nightscout.androidaps.utils.alertDialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.graphics.Color +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.webkit.WebView +import android.widget.Button +import android.widget.ImageView +import android.widget.TableLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.annotation.StyleRes +import androidx.appcompat.view.ContextThemeWrapper +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.general.maintenance.formats.Prefs +import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsStatus +import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.runOnUiThread +import java.util.* + +object PrefImportSummaryDialog { + + @SuppressLint("InflateParams") + @JvmStatic + @JvmOverloads + fun showSummary(context: Context, importOk: Boolean, importPossible: Boolean, prefs: Prefs, ok: (() -> Unit)?, cancel: (() -> Unit)? = null) { + + @StyleRes val theme: Int = if (importOk) R.style.AppTheme else { + if (importPossible) R.style.AppThemeWarningDialog else R.style.AppThemeErrorDialog + } + + @StringRes val messageRes: Int = if (importOk) R.string.check_preferences_before_import else { + if (importPossible) R.string.check_preferences_dangerous_import else R.string.check_preferences_cannot_import + } + + @DrawableRes val headerIcon: Int = if (importOk) R.drawable.ic_header_import else { + if (importPossible) R.drawable.ic_header_warning else R.drawable.ic_header_error + } + + val themedCtx = ContextThemeWrapper(context, theme) + + val innerLayout = LayoutInflater.from(themedCtx).inflate(R.layout.dialog_alert_import_summary, null) + val table = (innerLayout.findViewById(R.id.summary_table) as TableLayout) + val detailsBtn = (innerLayout.findViewById(R.id.summary_details_btn) as Button) + + var idx = 0 + val details = LinkedList() + + + for ((metaKey, metaEntry) in prefs.metadata) { + val rowLayout = LayoutInflater.from(themedCtx).inflate(R.layout.import_summary_item, null) + val label = (rowLayout.findViewById(R.id.summary_text) as TextView) + label.setText(metaKey.formatForDisplay(context, metaEntry.value)) + (rowLayout.findViewById(R.id.summary_icon) as ImageView).setImageResource(metaKey.icon) + (rowLayout.findViewById(R.id.status_icon) as ImageView).setImageResource(metaEntry.status.icon) + + if (metaEntry.status == PrefsStatus.WARN) label.setTextColor(themedCtx.getColor(R.color.metadataTextWarning)) + else if (metaEntry.status == PrefsStatus.ERROR) label.setTextColor(themedCtx.getColor(R.color.metadataTextError)) + + if (metaEntry.info != null) { + details.add("${context.getString(metaKey.label)}: ${metaEntry.value}
${metaEntry.info}") + rowLayout.isClickable = true + rowLayout.setOnClickListener { + val msg = "[${context.getString(metaKey.label)}] ${metaEntry.info}" + when (metaEntry.status) { + PrefsStatus.WARN -> ToastUtils.Long.warnToast(context, msg) + PrefsStatus.ERROR -> ToastUtils.Long.errorToast(context, msg) + else -> ToastUtils.Long.infoToast(context, msg) + } + } + } else { + rowLayout.isClickable = true + rowLayout.setOnClickListener { ToastUtils.Long.infoToast(context, context.getString(metaKey.label)) } + } + + table.addView(rowLayout, idx++) + } + + if (details.size > 0) { + detailsBtn.visibility = View.VISIBLE + detailsBtn.setOnClickListener { + val detailsLayout = LayoutInflater.from(context).inflate(R.layout.import_summary_details, null) + val wview = detailsLayout.findViewById(R.id.details_webview) as WebView + wview.loadData("" + details.joinToString("
"), "text/html; charset=utf-8", "utf-8"); + wview.setBackgroundColor(Color.TRANSPARENT); + wview.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null); + + AlertDialogHelper.Builder(context, R.style.AppTheme) + .setCustomTitle(AlertDialogHelper.buildCustomTitle( + context, + context.getString(R.string.check_preferences_details_title), + R.drawable.ic_header_log, + R.style.AppTheme)) + .setView(detailsLayout) + .setPositiveButton(android.R.string.ok) { dialogInner: DialogInterface, _: Int -> + dialogInner.dismiss() + } + .show() + } + } + + val builder = AlertDialogHelper.Builder(context, theme) + .setMessage(context.getString(messageRes)) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(R.string.nav_import), headerIcon, theme)) + .setView(innerLayout) + + .setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + SystemClock.sleep(100) + if (cancel != null) { + runOnUiThread(Runnable { + cancel() + }) + } + } + + if (importPossible) { + builder.setPositiveButton( + if (importOk) R.string.check_preferences_import_btn else R.string.check_preferences_import_anyway_btn + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + SystemClock.sleep(100) + if (ok != null) { + runOnUiThread(Runnable { + ok() + }) + } + } + } + + val dialog = builder.show() + dialog.setCanceledOnTouchOutside(false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt new file mode 100644 index 0000000000..7e8a4d47a6 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/TwoMessagesAlertDialog.kt @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.utils.alertDialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.runOnUiThread + +object TwoMessagesAlertDialog { + + @SuppressLint("InflateParams") + @JvmStatic + @JvmOverloads + fun showAlert(context: Context, title: String, message: String, secondMessage: String, ok: (() -> Unit)?, cancel: (() -> Unit)? = null, @DrawableRes icon: Int? = null) { + + val secondMessageLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_two_messages, null) + (secondMessageLayout.findViewById(R.id.password_prompt_title) as TextView).text = secondMessage + + val dialog = AlertDialogHelper.Builder(context) + .setMessage(message) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title, icon + ?: R.drawable.ic_check_while_48dp)) + .setView(secondMessageLayout) + .setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + SystemClock.sleep(100) + if (ok != null) { + runOnUiThread(Runnable { + ok() + }) + } + } + .setNegativeButton(android.R.string.cancel) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + SystemClock.sleep(100) + if (cancel != null) { + runOnUiThread(Runnable { + cancel() + }) + } + } + .show() + dialog.setCanceledOnTouchOutside(false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt new file mode 100644 index 0000000000..cd0654f3bf --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/alertDialogs/WarningDialog.kt @@ -0,0 +1,49 @@ +package info.nightscout.androidaps.utils.alertDialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.SystemClock +import androidx.annotation.StringRes +import info.nightscout.androidaps.R +import info.nightscout.androidaps.utils.runOnUiThread + +// if you need error dialog - duplicate to ErrorDialog and make it and use: AppThemeErrorDialog & R.drawable.ic_header_error instead + +object WarningDialog { + + @SuppressLint("InflateParams") + @JvmStatic + @JvmOverloads + fun showWarning(context: Context, title: String, message: String, @StringRes positiveButton: Int = -1, ok: (() -> Unit)? = null, cancel: (() -> Unit)? = null) { + + val builder = AlertDialogHelper.Builder(context, R.style.AppThemeWarningDialog) + .setMessage(message) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, title, R.drawable.ic_header_warning, R.style.AppThemeWarningDialog)) + .setNegativeButton(R.string.dismiss) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + SystemClock.sleep(100) + if (cancel != null) { + runOnUiThread(Runnable { + cancel() + }) + } + } + + if (positiveButton != -1) { + builder.setPositiveButton(positiveButton) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + SystemClock.sleep(100) + if (ok != null) { + runOnUiThread(Runnable { + ok() + }) + } + } + } + + val dialog = builder.show() + dialog.setCanceledOnTouchOutside(true) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt index 65f1e76e44..1907ac01b5 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/protection/PasswordCheck.kt @@ -1,20 +1,16 @@ package info.nightscout.androidaps.utils.protection import android.annotation.SuppressLint -import android.app.AlertDialog import android.content.Context import android.os.Build import android.view.LayoutInflater import android.view.View import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView import androidx.annotation.StringRes -import androidx.appcompat.view.ContextThemeWrapper -import androidx.fragment.app.FragmentActivity import info.nightscout.androidaps.R import info.nightscout.androidaps.utils.CryptoUtil import info.nightscout.androidaps.utils.ToastUtils +import info.nightscout.androidaps.utils.alertDialogs.AlertDialogHelper import info.nightscout.androidaps.utils.sharedPreferences.SP import javax.inject.Inject import javax.inject.Singleton @@ -26,41 +22,36 @@ val AUTOFILL_HINT_NEW_PASSWORD = "newPassword" class PasswordCheck @Inject constructor(val sp: SP) { @SuppressLint("InflateParams") - fun queryPassword(activity: FragmentActivity, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)?, cancel: (()->Unit)? = null, fail: (()->Unit)? = null) { + fun queryPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)?, cancel: (()->Unit)? = null, fail: (()->Unit)? = null) { val password = sp.getString(preference, "") if (password == "") { ok?.invoke("") return } - val titleLayout = activity.layoutInflater.inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = activity.getString(labelId) - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_key_48dp) - - val promptsView = LayoutInflater.from(activity).inflate(R.layout.passwordprompt, null) - - val alertDialogBuilder = AlertDialog.Builder(ContextThemeWrapper(activity, R.style.AppTheme)) + val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null) + val alertDialogBuilder = AlertDialogHelper.Builder(context) alertDialogBuilder.setView(promptsView) val userInput = promptsView.findViewById(R.id.passwordprompt_pass) as EditText if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val autoFillHintPasswordKind = activity.getString(preference) + val autoFillHintPasswordKind = context.getString(preference) userInput.setAutofillHints(View.AUTOFILL_HINT_PASSWORD, "aaps_${autoFillHintPasswordKind}") userInput.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_YES } alertDialogBuilder .setCancelable(false) - .setCustomTitle(titleLayout) - .setPositiveButton(activity.getString(R.string.ok)) { _, _ -> + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key)) + .setPositiveButton(context.getString(R.string.ok)) { _, _ -> val enteredPassword = userInput.text.toString() if (CryptoUtil.checkPassword(enteredPassword, password)) ok?.invoke(enteredPassword) else { - ToastUtils.showToastInUiThread(activity, activity.getString(R.string.wrongpassword)) + ToastUtils.errorToast(context, context.getString(R.string.wrongpassword)) fail?.invoke() } } - .setNegativeButton(activity.getString(R.string.cancel) + .setNegativeButton(context.getString(R.string.cancel) ) { dialog, _ -> cancel?.invoke() dialog.cancel() @@ -72,12 +63,7 @@ class PasswordCheck @Inject constructor(val sp: SP) { @SuppressLint("InflateParams") fun setPassword(context: Context, @StringRes labelId: Int, @StringRes preference: Int, ok: ( (String) -> Unit)? = null, cancel: (()->Unit)? = null, clear: (()->Unit)? = null) { val promptsView = LayoutInflater.from(context).inflate(R.layout.passwordprompt, null) - - val titleLayout = LayoutInflater.from(context).inflate(R.layout.dialog_alert_custom, null) - (titleLayout.findViewById(R.id.alertdialog_title) as TextView).text = context.getText(labelId) - (titleLayout.findViewById(R.id.alertdialog_icon) as ImageView).setImageResource(R.drawable.ic_key_48dp) - - val alertDialogBuilder = AlertDialog.Builder(ContextThemeWrapper(context, R.style.AppTheme)) + val alertDialogBuilder = AlertDialogHelper.Builder(context) alertDialogBuilder.setView(promptsView) val userInput = promptsView.findViewById(R.id.passwordprompt_pass) as EditText @@ -90,20 +76,20 @@ class PasswordCheck @Inject constructor(val sp: SP) { alertDialogBuilder .setCancelable(false) - .setCustomTitle(titleLayout) + .setCustomTitle(AlertDialogHelper.buildCustomTitle(context, context.getString(labelId), R.drawable.ic_header_key)) .setPositiveButton(context.getString(R.string.ok)) { _, _ -> val enteredPassword = userInput.text.toString() if (enteredPassword.isNotEmpty()) { sp.putString(preference, CryptoUtil.hashPassword(enteredPassword)) - ToastUtils.showToastInUiThread(context, context.getString(R.string.password_set)) + ToastUtils.okToast(context, context.getString(R.string.password_set)) ok?.invoke(enteredPassword) } else { if (sp.contains(preference)) { sp.remove(preference) - ToastUtils.showToastInUiThread(context, context.getString(R.string.password_cleared)) + ToastUtils.graphicalToast(context, context.getString(R.string.password_cleared), R.drawable.ic_toast_delete_confirm) clear?.invoke() } else { - ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed)) + ToastUtils.warnToast(context, context.getString(R.string.password_not_changed)) cancel?.invoke() } } @@ -111,7 +97,7 @@ class PasswordCheck @Inject constructor(val sp: SP) { } .setNegativeButton(context.getString(R.string.cancel) ) { dialog, _ -> - ToastUtils.showToastInUiThread(context, context.getString(R.string.password_not_changed)) + ToastUtils.infoToast(context, context.getString(R.string.password_not_changed)) cancel?.invoke() dialog.cancel() } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt b/app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt new file mode 100644 index 0000000000..40be1e65c3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/storage/FileStrorage.kt @@ -0,0 +1,17 @@ +package info.nightscout.androidaps.utils.storage + +import java.io.File +import javax.inject.Singleton + +@Singleton +class FileStorage : Storage { + + override fun getFileContents(file: File): String { + return file.readText() + } + + override fun putFileContents(file: File, contents: String) { + file.writeText(contents) + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt b/app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt new file mode 100644 index 0000000000..475cd2675e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/utils/storage/Storage.kt @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.utils.storage + +import java.io.File + +// This may seems unnecessary abstraction - but it will simplify testing +interface Storage { + + fun getFileContents(file: File) : String + fun putFileContents(file: File, contents: String) + +} diff --git a/app/src/main/res/drawable/alert_border_error.xml b/app/src/main/res/drawable/alert_border_error.xml new file mode 100644 index 0000000000..d1bcae1348 --- /dev/null +++ b/app/src/main/res/drawable/alert_border_error.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/alert_border_warning.xml b/app/src/main/res/drawable/alert_border_warning.xml new file mode 100644 index 0000000000..c73a9517a5 --- /dev/null +++ b/app/src/main/res/drawable/alert_border_warning.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_header_error.xml b/app/src/main/res/drawable/ic_header_error.xml new file mode 100644 index 0000000000..09c29c1558 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_error.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_export.xml b/app/src/main/res/drawable/ic_header_export.xml new file mode 100644 index 0000000000..d59103ab07 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_export.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_import.xml b/app/src/main/res/drawable/ic_header_import.xml new file mode 100644 index 0000000000..e929fad69b --- /dev/null +++ b/app/src/main/res/drawable/ic_header_import.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_key.xml b/app/src/main/res/drawable/ic_header_key.xml new file mode 100644 index 0000000000..416cc50bd3 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_key.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_log.xml b/app/src/main/res/drawable/ic_header_log.xml new file mode 100644 index 0000000000..6ef0e68d81 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_log.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_header_warning.xml b/app/src/main/res/drawable/ic_header_warning.xml new file mode 100644 index 0000000000..3d7afd8ef3 --- /dev/null +++ b/app/src/main/res/drawable/ic_header_warning.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_key_48dp.xml b/app/src/main/res/drawable/ic_key_48dp.xml deleted file mode 100644 index 813dc24d00..0000000000 --- a/app/src/main/res/drawable/ic_key_48dp.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_date.xml b/app/src/main/res/drawable/ic_meta_date.xml new file mode 100644 index 0000000000..190392aaa1 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_date.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_encryption.xml b/app/src/main/res/drawable/ic_meta_encryption.xml new file mode 100644 index 0000000000..597b35a77a --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_encryption.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_error.xml b/app/src/main/res/drawable/ic_meta_error.xml new file mode 100644 index 0000000000..e5d213f774 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_error.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_flavour.xml b/app/src/main/res/drawable/ic_meta_flavour.xml new file mode 100644 index 0000000000..a7fe7f3471 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_flavour.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_format.xml b/app/src/main/res/drawable/ic_meta_format.xml new file mode 100644 index 0000000000..2023a4934d --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_format.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_model.xml b/app/src/main/res/drawable/ic_meta_model.xml new file mode 100644 index 0000000000..eca3e69c59 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_model.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_name.xml b/app/src/main/res/drawable/ic_meta_name.xml new file mode 100644 index 0000000000..ad120e037a --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_name.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_ok.xml b/app/src/main/res/drawable/ic_meta_ok.xml new file mode 100644 index 0000000000..663897c160 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_ok.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_meta_version.xml b/app/src/main/res/drawable/ic_meta_version.xml new file mode 100644 index 0000000000..ddf95f2fa9 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_version.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_meta_warning.xml b/app/src/main/res/drawable/ic_meta_warning.xml new file mode 100644 index 0000000000..9cf5ce2c36 --- /dev/null +++ b/app/src/main/res/drawable/ic_meta_warning.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_check.xml b/app/src/main/res/drawable/ic_toast_check.xml new file mode 100644 index 0000000000..26b9f05b49 --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_check.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_delete_confirm.xml b/app/src/main/res/drawable/ic_toast_delete_confirm.xml new file mode 100644 index 0000000000..de6371045a --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_delete_confirm.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_error.xml b/app/src/main/res/drawable/ic_toast_error.xml new file mode 100644 index 0000000000..fb49272c0a --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_error.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_info.xml b/app/src/main/res/drawable/ic_toast_info.xml new file mode 100644 index 0000000000..9d54be6827 --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_info.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toast_warn.xml b/app/src/main/res/drawable/ic_toast_warn.xml new file mode 100644 index 0000000000..8864b16500 --- /dev/null +++ b/app/src/main/res/drawable/ic_toast_warn.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toast_border_ok.xml b/app/src/main/res/drawable/toast_border_ok.xml new file mode 100644 index 0000000000..1c62848b31 --- /dev/null +++ b/app/src/main/res/drawable/toast_border_ok.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/layout/dialog_alert_custom.xml b/app/src/main/res/layout/dialog_alert_custom.xml index eea5fc6882..f006387d28 100644 --- a/app/src/main/res/layout/dialog_alert_custom.xml +++ b/app/src/main/res/layout/dialog_alert_custom.xml @@ -1,4 +1,8 @@ + + android:layout_height="wrap_content" + android:tint="?dialogTitleIconTint" /> + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="?dialogTitleColor" /> + - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_alert_import_summary.xml b/app/src/main/res/layout/dialog_alert_import_summary.xml new file mode 100644 index 0000000000..fb6e5a0708 --- /dev/null +++ b/app/src/main/res/layout/dialog_alert_import_summary.xml @@ -0,0 +1,30 @@ + + + + + +