diff --git a/app/build.gradle b/app/build.gradle index 8b50ad30ed..3b55fd2235 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,6 +156,7 @@ allprojects { flatDir { dirs 'libs' } + maven { url 'https://jitpack.io' } } } @@ -218,6 +219,8 @@ dependencies { implementation "com.jakewharton:butterknife:${butterknifeVersion}" annotationProcessor "com.jakewharton:butterknife-compiler:${butterknifeVersion}" + implementation 'com.github.DavidProdinger:weekdays-selector:1.0.4' + testImplementation "junit:junit:4.12" testImplementation "org.json:json:20140107" testImplementation "org.mockito:mockito-core:2.7.22" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dc9201d86c..bc12ee8a94 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + @@ -150,7 +151,10 @@ android:name=".services.DataService" android:exported="false" /> + (); @@ -109,6 +110,7 @@ public class L { logElements.add(new LogElement(DATASERVICE, true)); logElements.add(new LogElement(DATATREATMENTS, true)); logElements.add(new LogElement(EVENTS, false, true)); + logElements.add(new LogElement(LOCATION, true)); logElements.add(new LogElement(NOTIFICATION, true)); logElements.add(new LogElement(NSCLIENT, true)); logElements.add(new LogElement(OVERVIEW, true)); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationEvent.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationEvent.java new file mode 100644 index 0000000000..c6d104f61c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationEvent.java @@ -0,0 +1,73 @@ +package info.nightscout.androidaps.plugins.general.automation; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.plugins.general.automation.actions.Action; +import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; + +public class AutomationEvent { + + private Trigger trigger; + private List actions = new ArrayList<>(); + private String title; + + public void setTitle(String title) { this.title = title; } + + public void setTrigger(Trigger trigger) { this.trigger = trigger; } + + public Trigger getTrigger() { + return trigger; + } + + public List getActions() { + return actions; + } + + public void addAction(Action action) { actions.add(action); } + + public String getTitle() { + return title; + } + + public String toJSON() { + JSONObject o = new JSONObject(); + try { + // title + o.put("title", title); + // trigger + o.put("trigger", trigger.toJSON()); + // actions + JSONArray array = new JSONArray(); + for (Action a : actions) { + array.put(a.toJSON()); + } + o.put("actions", array); + } catch (JSONException e) { + e.printStackTrace(); + } + return o.toString(); + } + + public AutomationEvent fromJSON(String data) { + try { + JSONObject d = new JSONObject(data); + // title + title = d.getString("title"); + // trigger + trigger = Trigger.instantiate(d.getString("trigger")); + // actions + JSONArray array = d.getJSONArray("actions"); + for (int i = 0; i < array.length(); i++) { + actions.add(Action.instantiate(array.getJSONObject(i))); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return this; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.java new file mode 100644 index 0000000000..359ddfd813 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationFragment.java @@ -0,0 +1,373 @@ +package info.nightscout.androidaps.plugins.general.automation; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.v4.app.FragmentManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.HashSet; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.common.SubscriberFragment; +import info.nightscout.androidaps.plugins.general.automation.actions.Action; +import info.nightscout.androidaps.plugins.general.automation.dialogs.ChooseTriggerDialog; +import info.nightscout.androidaps.plugins.general.automation.dialogs.EditActionDialog; +import info.nightscout.androidaps.plugins.general.automation.dialogs.EditEventDialog; +import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector; + +public class AutomationFragment extends SubscriberFragment { + + @BindView(R.id.eventListView) + RecyclerView mEventListView; + + private EventListAdapter mEventListAdapter; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.automation_fragment, container, false); + unbinder = ButterKnife.bind(this, view); + + final AutomationPlugin plugin = AutomationPlugin.getPlugin(); + mEventListAdapter = new EventListAdapter(plugin.getAutomationEvents()); + mEventListView.setLayoutManager(new LinearLayoutManager(getContext())); + mEventListView.setAdapter(mEventListAdapter); + + EditEventDialog.setOnClickListener(event -> { + plugin.getAutomationEvents().add(event); + mEventListAdapter.notifyDataSetChanged(); + }); + + updateGUI(); + + return view; + } + + @Override + public void updateGUI() { + Activity activity = getActivity(); + if (activity != null) + activity.runOnUiThread(() -> mEventListAdapter.notifyDataSetChanged()); + } + + @OnClick(R.id.fabAddEvent) + void onClickAddEvent(View v) { + EditEventDialog dialog = EditEventDialog.newInstance(new AutomationEvent()); + dialog.show(getFragmentManager(), "EditEventDialog"); + } + + /** + * RecyclerViewAdapter to display event lists. + */ + public static class EventListAdapter extends RecyclerView.Adapter { + private final List mEventList; + + public EventListAdapter(List events) { + this.mEventList = events; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.automation_event_item, parent, false); + return new ViewHolder(v, parent.getContext()); + } + + private void addImage(@DrawableRes int res, Context context, LinearLayout layout) { + ImageView iv = new ImageView(context); + iv.setImageResource(res); + iv.setLayoutParams(new LinearLayout.LayoutParams(MainApp.dpToPx(24),MainApp.dpToPx(24))); + layout.addView(iv); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + final AutomationEvent event = mEventList.get(position); + holder.eventTitle.setText(event.getTitle()); + holder.iconLayout.removeAllViews(); + + // trigger icons + HashSet triggerIcons = new HashSet<>(); + TriggerConnector.fillIconSet((TriggerConnector)event.getTrigger(), triggerIcons); + for(int res : triggerIcons) { + addImage(res, holder.context, holder.iconLayout); + } + + // arrow icon + ImageView iv = new ImageView(holder.context); + iv.setImageResource(R.drawable.ic_arrow_forward_white_24dp); + iv.setLayoutParams(new LinearLayout.LayoutParams(MainApp.dpToPx(24),MainApp.dpToPx(24))); + iv.setPadding(MainApp.dpToPx(4), 0, MainApp.dpToPx(4), 0); + holder.iconLayout.addView(iv); + + // action icons + HashSet actionIcons = new HashSet<>(); + for(Action action : event.getActions()) { + if (action.icon().isPresent()) + actionIcons.add(action.icon().get()); + } + for(int res : actionIcons) { + addImage(res, holder.context, holder.iconLayout); + } + + // TODO: check null + //holder.eventDescription.setText(event.getTrigger().friendlyDescription()); + } + + @Override + public int getItemCount() { + return mEventList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + final RelativeLayout rootLayout; + final LinearLayout iconLayout; + final TextView eventTitle; + final Context context; + + public ViewHolder(View view, Context context) { + super(view); + this.context = context; + eventTitle = view.findViewById(R.id.viewEventTitle); + rootLayout = view.findViewById(R.id.rootLayout); + iconLayout = view.findViewById(R.id.iconLayout); + } + } + } + + /** + * RecyclerViewAdapter to display event lists. + */ + public static class ActionListAdapter extends RecyclerView.Adapter { + private final List mActionList; + private final FragmentManager mFragmentManager; + + public ActionListAdapter(FragmentManager manager, List events) { + this.mActionList = events; + this.mFragmentManager = manager; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.automation_action_item, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + final Action action = mActionList.get(position); + holder.actionTitle.setText(action.friendlyName()); + holder.itemRoot.setOnClickListener(v -> { + if (action.hasDialog()) { + EditActionDialog dialog = EditActionDialog.newInstance(action); + dialog.show(mFragmentManager, "EditActionDialog"); + } + }); + } + + @Override + public int getItemCount() { + return mActionList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + TextView actionTitle; + TextView actionDescription; + LinearLayout itemRoot; + + public ViewHolder(View view) { + super(view); + itemRoot = view.findViewById(R.id.itemRoot); + actionTitle = view.findViewById(R.id.viewActionTitle); + actionDescription = view.findViewById(R.id.viewActionDescription); + } + } + } + + /** + * Custom Adapter to display triggers dynamically with nested linear layouts. + */ + public static class TriggerListAdapter { + private final LinearLayout mRootLayout; + private final Context mContext; + private final TriggerConnector mRootConnector; + private final FragmentManager mFragmentManager; + + public TriggerListAdapter(Context context, FragmentManager fragmentManager, LinearLayout rootLayout, TriggerConnector rootTrigger) { + mRootLayout = rootLayout; + mContext = context; + mFragmentManager = fragmentManager; + mRootConnector = rootTrigger; + build(); + } + + + public void destroy() { + mRootLayout.removeAllViews(); + } + + private void build() { + for(int i = 0; i < mRootConnector.size(); ++i) { + final Trigger trigger = mRootConnector.get(i); + + // spinner + if (i > 0) { + createSpinner(trigger); + } + + // trigger layout + mRootLayout.addView(trigger.createView(mContext, mFragmentManager)); + + // buttons + createButtons(trigger); + } + + if (mRootConnector.size() == 0) { + Button buttonAdd = new Button(mContext); + buttonAdd.setText("Add New"); + buttonAdd.setOnClickListener(v -> { + ChooseTriggerDialog dialog = ChooseTriggerDialog.newInstance(); + dialog.setOnClickListener(newTriggerObject -> { + mRootConnector.add(newTriggerObject); + rebuild(); + }); + dialog.show(mFragmentManager, "ChooseTriggerDialog"); + }); + mRootLayout.addView(buttonAdd); + } + } + + private Spinner createSpinner() { + Spinner spinner = new Spinner(mContext); + ArrayAdapter spinnerArrayAdapter = new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, TriggerConnector.Type.labels()); + spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(spinnerArrayAdapter); + return spinner; + } + + private void createSpinner(Trigger trigger) { + final TriggerConnector connector = trigger.getConnector(); + final int initialPosition = connector.getConnectorType().ordinal(); + Spinner spinner = createSpinner(); + spinner.setSelection(initialPosition); + spinner.setBackgroundColor(MainApp.gc(R.color.black_overlay)); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, MainApp.dpToPx(8), 0, MainApp.dpToPx(8)); + spinner.setLayoutParams(params); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position != initialPosition) { + // conector type changed + changeConnector(trigger, connector, TriggerConnector.Type.values()[position]); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { } + }); + mRootLayout.addView(spinner); + } + + private void createButtons(Trigger trigger) { + // do not create buttons for TriggerConnector + if (trigger instanceof TriggerConnector) { + return; + } + + // Button Layout + LinearLayout buttonLayout = new LinearLayout(mContext); + buttonLayout.setOrientation(LinearLayout.HORIZONTAL); + buttonLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + mRootLayout.addView(buttonLayout); + + // Button [-] + Button buttonRemove = new Button(mContext); + buttonRemove.setText("del"); + buttonRemove.setOnClickListener(v -> { + final TriggerConnector connector = trigger.getConnector(); + connector.remove(trigger); + connector.simplify().rebuildView(); + }); + buttonLayout.addView(buttonRemove); + + // Button [+] + Button buttonAdd = new Button(mContext); + buttonAdd.setText("add"); + buttonAdd.setOnClickListener(v -> { + ChooseTriggerDialog dialog = ChooseTriggerDialog.newInstance(); + dialog.show(mFragmentManager, "ChooseTriggerDialog"); + dialog.setOnClickListener(newTriggerObject -> { + TriggerConnector connector = trigger.getConnector(); + connector.add(connector.pos(trigger)+1, newTriggerObject); + connector.simplify().rebuildView(); + }); + }); + buttonLayout.addView(buttonAdd); + + // Button [*] + Button buttonCopy = new Button(mContext); + buttonCopy.setText("copy"); + buttonCopy.setOnClickListener(v -> { + TriggerConnector connector = trigger.getConnector(); + connector.add(connector.pos(trigger)+1, trigger.duplicate()); + connector.simplify().rebuildView(); + }); + buttonLayout.addView(buttonCopy); + } + + public static void changeConnector(final Trigger trigger, final TriggerConnector connector, final TriggerConnector.Type newConnectorType) { + if (connector.size() > 2) { + // split connector + int pos = connector.pos(trigger) - 1; + + TriggerConnector newConnector = new TriggerConnector(newConnectorType); + + // move trigger from pos and pos+1 into new connector + for(int i = 0; i < 2; ++i) { + Trigger t = connector.get(pos); + newConnector.add(t); + connector.remove(t); + } + + connector.add(pos, newConnector); + } else { + connector.changeConnectorType(newConnectorType); + } + + connector.simplify().rebuildView(); + } + + public void rebuild() { + destroy(); + build(); + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.java new file mode 100644 index 0000000000..a65c4fee4b --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/AutomationPlugin.java @@ -0,0 +1,120 @@ +package info.nightscout.androidaps.plugins.general.automation; + +import android.content.Context; +import android.content.Intent; +import android.location.Location; + +import com.squareup.otto.Subscribe; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.events.EventChargingState; +import info.nightscout.androidaps.events.EventLocationChange; +import info.nightscout.androidaps.events.EventNetworkChange; +import info.nightscout.androidaps.events.EventPreferenceChange; +import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.interfaces.PluginDescription; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.events.EventAutosensCalculationFinished; +import info.nightscout.androidaps.services.LocationService; + +public class AutomationPlugin extends PluginBase { + + static AutomationPlugin plugin = null; + + public static AutomationPlugin getPlugin() { + if (plugin == null) + plugin = new AutomationPlugin(); + return plugin; + } + + private final List automationEvents = new ArrayList<>(); + private EventLocationChange eventLocationChange; + private EventChargingState eventChargingState; + private EventNetworkChange eventNetworkChange; + + private AutomationPlugin() { + super(new PluginDescription() + .mainType(PluginType.GENERAL) + .fragmentClass(AutomationFragment.class.getName()) + .pluginName(R.string.automation) + .shortName(R.string.automation_short) + .preferencesId(R.xml.pref_automation) + .description(R.string.automation_description) + ); + } + + @Override + protected void onStart() { + Context context = MainApp.instance().getApplicationContext(); + context.startService(new Intent(context, LocationService.class)); + + MainApp.bus().register(this); + super.onStart(); + } + + @Override + protected void onStop() { + Context context = MainApp.instance().getApplicationContext(); + context.stopService(new Intent(context, LocationService.class)); + + MainApp.bus().unregister(this); + } + + public List getAutomationEvents() { + return automationEvents; + } + + public EventLocationChange getEventLocationChange() { + return eventLocationChange; + } + + public EventChargingState getEventChargingState() { + return eventChargingState; + } + + public EventNetworkChange getEventNetworkChange() { + return eventNetworkChange; + } + + @Subscribe + public void onEventPreferenceChange(EventPreferenceChange e) { + if (e.isChanged(R.string.key_location)) { + Context context = MainApp.instance().getApplicationContext(); + context.stopService(new Intent(context, LocationService.class)); + context.startService(new Intent(context, LocationService.class)); + } + } + + @Subscribe + public void onEventLocationChange(EventLocationChange e) { + eventLocationChange = e; + processActions(); + } + + @Subscribe + public void onEventChargingState(EventChargingState e) { + eventChargingState = e; + processActions(); + } + + @Subscribe + public void onEventNetworkChange(EventNetworkChange e) { + eventNetworkChange = e; + processActions(); + } + + @Subscribe + public void onEventAutosensCalculationFinished(EventAutosensCalculationFinished e) { + processActions(); + } + + // TODO keepalive + + void processActions() { + + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.java new file mode 100644 index 0000000000..d1522640f3 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/Action.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.plugins.general.automation.actions; + +import android.widget.LinearLayout; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; + +import info.nightscout.androidaps.queue.Callback; + +public abstract class Action { + + public abstract int friendlyName(); + + abstract void doAction(Callback callback); + + public void generateDialog(LinearLayout root) { } + + public boolean hasDialog() { return false; } + + public String toJSON() { + JSONObject o = new JSONObject(); + try { + o.put("type", this.getClass().getName()); + } catch (JSONException e) { + e.printStackTrace(); + } + return o.toString(); + } + + public abstract Optional icon(); + + public void copy(Action action) { } + + /*package*/ Action fromJSON(String data) { + return this; + } + + public static Action instantiate(JSONObject object) { + try { + String type = object.getString("type"); + JSONObject data = object.getJSONObject("data"); + Class clazz = Class.forName(type); + return ((Action) clazz.newInstance()).fromJSON(data.toString()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | JSONException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopDisable.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopDisable.java new file mode 100644 index 0000000000..794d4a31ce --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopDisable.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.plugins.general.automation.actions; + +import com.google.common.base.Optional; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; +import info.nightscout.androidaps.queue.Callback; + +public class ActionLoopDisable extends Action { + @Override + public int friendlyName() { + return R.string.disableloop; + } + + @Override + void doAction(Callback callback) { + if (LoopPlugin.getPlugin().isEnabled(PluginType.LOOP)) { + LoopPlugin.getPlugin().setPluginEnabled(PluginType.LOOP, false); + ConfigBuilderPlugin.getPlugin().storeSettings("ActionLoopDisable"); + ConfigBuilderPlugin.getPlugin().getCommandQueue().cancelTempBasal(true, new Callback() { + @Override + public void run() { + MainApp.bus().post(new EventRefreshOverview("ActionLoopDisable")); + if (callback != null) + callback.result(result).run(); + } + }); + } else { + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.alreadydisabled)).run(); + } + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.ic_stop_24dp); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopEnable.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopEnable.java new file mode 100644 index 0000000000..6e8839f353 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopEnable.java @@ -0,0 +1,38 @@ +package info.nightscout.androidaps.plugins.general.automation.actions; + +import com.google.common.base.Optional; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; +import info.nightscout.androidaps.queue.Callback; + +public class ActionLoopEnable extends Action { + @Override + public int friendlyName() { + return R.string.enableloop; + } + + @Override + void doAction(Callback callback) { + if (!LoopPlugin.getPlugin().isEnabled(PluginType.LOOP)) { + LoopPlugin.getPlugin().setPluginEnabled(PluginType.LOOP, true); + ConfigBuilderPlugin.getPlugin().storeSettings("ActionLoopEnable"); + MainApp.bus().post(new EventRefreshOverview("ActionLoopEnable")); + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.ok)).run(); + } else { + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.alreadyenabled)).run(); + } + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.ic_play_circle_outline_24dp); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopResume.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopResume.java new file mode 100644 index 0000000000..c734acd541 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopResume.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.general.automation.actions; + +import com.google.common.base.Optional; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; +import info.nightscout.androidaps.plugins.general.nsclient.NSUpload; +import info.nightscout.androidaps.queue.Callback; + +public class ActionLoopResume extends Action { + @Override + public int friendlyName() { + return R.string.resumeloop; + } + + @Override + void doAction(Callback callback) { + if (LoopPlugin.getPlugin().isSuspended()) { + LoopPlugin.getPlugin().suspendTo(0); + ConfigBuilderPlugin.getPlugin().storeSettings("ActionLoopResume"); + NSUpload.uploadOpenAPSOffline(0); + MainApp.bus().post(new EventRefreshOverview("ActionLoopResume")); + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.ok)).run(); + } else { + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.notsuspended)).run(); + } + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.ic_replay_24dp); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopSuspend.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopSuspend.java new file mode 100644 index 0000000000..4938a6939d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionLoopSuspend.java @@ -0,0 +1,37 @@ +package info.nightscout.androidaps.plugins.general.automation.actions; + +import com.google.common.base.Optional; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.events.EventRefreshOverview; +import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; +import info.nightscout.androidaps.queue.Callback; + +public class ActionLoopSuspend extends Action { + private int minutes; + + @Override + public int friendlyName() { + return R.string.suspendloop; + } + + @Override + void doAction(Callback callback) { + if (!LoopPlugin.getPlugin().isSuspended()) { + LoopPlugin.getPlugin().suspendLoop(minutes); + MainApp.bus().post(new EventRefreshOverview("ActionLoopSuspend")); + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.ok)).run(); + } else { + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.alreadysuspended)).run(); + } + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.ic_pause_circle_outline_24dp); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionStartTempTarget.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionStartTempTarget.java new file mode 100644 index 0000000000..f7afe1d06c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/actions/ActionStartTempTarget.java @@ -0,0 +1,110 @@ +package info.nightscout.androidaps.plugins.general.automation.actions; + +import android.widget.LinearLayout; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; + +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.PumpEnactResult; +import info.nightscout.androidaps.db.Source; +import info.nightscout.androidaps.db.TempTarget; +import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; +import info.nightscout.androidaps.plugins.general.automation.elements.InputBg; +import info.nightscout.androidaps.plugins.general.automation.elements.InputDuration; +import info.nightscout.androidaps.plugins.general.automation.elements.Label; +import info.nightscout.androidaps.queue.Callback; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.JsonHelper; + +public class ActionStartTempTarget extends Action { + private String reason; + + private InputBg value; + private InputDuration duration = new InputDuration(0, InputDuration.TimeUnit.MINUTES); + + public ActionStartTempTarget() { + value = new InputBg(Constants.MGDL); + } + + public ActionStartTempTarget(String units) { + value = new InputBg(units); + } + + @Override + public int friendlyName() { + return R.string.starttemptarget; + } + + @Override + void doAction(Callback callback) { + TempTarget tempTarget = new TempTarget().date(DateUtil.now()).duration((int)duration.getMinutes()).reason(reason).source(Source.USER).low(value.getMgdl()).high(value.getMgdl()); + TreatmentsPlugin.getPlugin().addToHistoryTempTarget(tempTarget); + if (callback != null) + callback.result(new PumpEnactResult().success(true).comment(R.string.ok)).run(); + } + + @Override + public void generateDialog(LinearLayout root) { + int unitResId = value.getUnits().equals(Constants.MGDL) ? R.string.mgdl : R.string.mmol; + Label labelBg = new Label(MainApp.gs(R.string.careportal_newnstreatment_percentage_label), MainApp.gs(unitResId), value); + labelBg.generateDialog(root); + Label labelDuration = new Label(MainApp.gs(R.string.careportal_newnstreatment_duration_min_label), "min", duration); + labelDuration.generateDialog(root); + } + + @Override + public boolean hasDialog() { + return true; + } + + @Override + public String toJSON() { + JSONObject o = new JSONObject(); + try { + o.put("type", ActionStartTempTarget.class.getName()); + JSONObject data = new JSONObject(); + data.put("reason", reason); + data.put("valueInMg", value.getMgdl()); + data.put("units", value.getUnits()); + data.put("durationInMinutes", duration.getMinutes()); + o.put("data", data); + } catch (JSONException e) { + e.printStackTrace(); + } + return o.toString(); + } + + @Override + Action fromJSON(String data) { + try { + JSONObject d = new JSONObject(data); + reason = JsonHelper.safeGetString(d, "reason"); + value.setUnits(JsonHelper.safeGetString(d, "units")); + value.setMgdl(JsonHelper.safeGetInt(d, "valueInMg")); + duration.setMinutes(JsonHelper.safeGetDouble(d, "durationInMinutes")); + } catch (JSONException e) { + e.printStackTrace(); + } + return this; + } + + @Override + public void copy(Action action) { + if (action instanceof ActionStartTempTarget) { + ActionStartTempTarget src = (ActionStartTempTarget)action; + this.duration = src.duration; + this.value = src.value; + this.reason = src.reason; + } + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.icon_cp_cgm_target); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseActionDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseActionDialog.java new file mode 100644 index 0000000000..12956dd108 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseActionDialog.java @@ -0,0 +1,139 @@ +package info.nightscout.androidaps.plugins.general.automation.dialogs; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.automation.actions.Action; +import info.nightscout.androidaps.plugins.general.automation.actions.ActionLoopDisable; +import info.nightscout.androidaps.plugins.general.automation.actions.ActionLoopEnable; +import info.nightscout.androidaps.plugins.general.automation.actions.ActionLoopResume; +import info.nightscout.androidaps.plugins.general.automation.actions.ActionLoopSuspend; +import info.nightscout.androidaps.plugins.general.automation.actions.ActionStartTempTarget; + +public class ChooseActionDialog extends DialogFragment { + + public interface OnClickListener { + void onClick(Action newActionObject); + } + + private static OnClickListener mClickListener = null; + + private static final List actionDummyObjects = new ArrayList() {{ + add(new ActionLoopDisable()); + add(new ActionLoopEnable()); + add(new ActionLoopResume()); + add(new ActionLoopSuspend()); + add(new ActionStartTempTarget()); + }}; + + private Unbinder mUnbinder; + + @BindView(R.id.radioGroup) + RadioGroup mRadioGroup; + + public static ChooseActionDialog newInstance() { + Bundle args = new Bundle(); + + ChooseActionDialog fragment = new ChooseActionDialog(); + fragment.setArguments(args); + + return fragment; + } + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.automation_dialog_choose_action, container, false); + mUnbinder = ButterKnife.bind(this, view); + + for(Action a : actionDummyObjects) { + RadioButton radioButton = new RadioButton(getContext()); + radioButton.setText(a.friendlyName()); + radioButton.setTag(a); + mRadioGroup.addView(radioButton); + } + + // restore checked radio button + int checkedIndex = 0; + if (savedInstanceState != null) { + checkedIndex = savedInstanceState.getInt("checkedIndex"); + } + + ((RadioButton)mRadioGroup.getChildAt(checkedIndex)).setChecked(true); + + return view; + } + + private int getCheckedIndex() { + for(int i = 0; i < mRadioGroup.getChildCount(); ++i) { + if (((RadioButton)mRadioGroup.getChildAt(i)).isChecked()) + return i; + } + return -1; + } + + private Class getActionClass() { + int radioButtonID = mRadioGroup.getCheckedRadioButtonId(); + RadioButton radioButton = mRadioGroup.findViewById(radioButtonID); + if (radioButton != null) { + Object tag = radioButton.getTag(); + if (tag instanceof Action) + return tag.getClass(); + } + return null; + } + + private Action instantiateAction() { + Class actionClass = getActionClass(); + if (actionClass != null) { + try { + return (Action) actionClass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return null; + } + + + public static void setOnClickListener(OnClickListener clickListener) { + mClickListener = clickListener; + } + + @Override + public void onDestroyView() { + mUnbinder.unbind(); + super.onDestroyView(); + } + + @OnClick(R.id.ok) + public void onButtonOk(View view) { + if (mClickListener != null) + mClickListener.onClick(instantiateAction()); + + dismiss(); + } + + @OnClick(R.id.cancel) + public void onButtonCancel(View view) { + dismiss(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putInt("checkedIndex", getCheckedIndex()); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java new file mode 100644 index 0000000000..cdb6b0739f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java @@ -0,0 +1,132 @@ +package info.nightscout.androidaps.plugins.general.automation.dialogs; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerBg; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerTime; + +public class ChooseTriggerDialog extends DialogFragment { + + public interface OnClickListener { + void onClick(Trigger newTriggerObject); + } + + private static final List triggerDummyObjects = new ArrayList() {{ + add(new TriggerBg()); + add(new TriggerTime()); + }}; + + private Unbinder mUnbinder; + private OnClickListener mClickListener = null; + + @BindView(R.id.radioGroup) + RadioGroup mRadioGroup; + + public static ChooseTriggerDialog newInstance() { + Bundle args = new Bundle(); + + ChooseTriggerDialog fragment = new ChooseTriggerDialog(); + fragment.setArguments(args); + + return fragment; + } + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.automation_dialog_choose_trigger, container, false); + mUnbinder = ButterKnife.bind(this, view); + + for(Trigger t : triggerDummyObjects) { + RadioButton radioButton = new RadioButton(getContext()); + radioButton.setText(t.friendlyName()); + radioButton.setTag(t); + mRadioGroup.addView(radioButton); + } + + // restore checked radio button + int checkedIndex = 0; + if (savedInstanceState != null) { + checkedIndex = savedInstanceState.getInt("checkedIndex"); + } + + ((RadioButton)mRadioGroup.getChildAt(checkedIndex)).setChecked(true); + + return view; + } + + private int getCheckedIndex() { + for(int i = 0; i < mRadioGroup.getChildCount(); ++i) { + if (((RadioButton)mRadioGroup.getChildAt(i)).isChecked()) + return i; + } + return -1; + } + + private Class getTriggerClass() { + int radioButtonID = mRadioGroup.getCheckedRadioButtonId(); + RadioButton radioButton = mRadioGroup.findViewById(radioButtonID); + if (radioButton != null) { + Object tag = radioButton.getTag(); + if (tag instanceof Trigger) + return tag.getClass(); + } + return null; + } + + private Trigger instantiateTrigger() { + Class triggerClass = getTriggerClass(); + if (triggerClass != null) { + try { + return (Trigger) triggerClass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return null; + } + + + public void setOnClickListener(OnClickListener clickListener) { + mClickListener = clickListener; + } + + @Override + public void onDestroyView() { + mUnbinder.unbind(); + super.onDestroyView(); + } + + @OnClick(R.id.ok) + public void onButtonOk(View view) { + if (mClickListener != null) + mClickListener.onClick(instantiateTrigger()); + + dismiss(); + } + + @OnClick(R.id.cancel) + public void onButtonCancel(View view) { + dismiss(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putInt("checkedIndex", getCheckedIndex()); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditActionDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditActionDialog.java new file mode 100644 index 0000000000..696ccd6f8e --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditActionDialog.java @@ -0,0 +1,90 @@ +package info.nightscout.androidaps.plugins.general.automation.dialogs; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.json.JSONException; +import org.json.JSONObject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.automation.actions.Action; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector; + +public class EditActionDialog extends DialogFragment { + private static Action resultAction; + + private Unbinder mUnbinder; + private Action mAction; + + @BindView(R.id.layout_root) + LinearLayout mRootLayout; + + @BindView(R.id.viewActionTitle) + TextView mViewActionTitle; + + public static EditActionDialog newInstance(Action action) { + Bundle args = new Bundle(); + EditActionDialog fragment = new EditActionDialog(); + fragment.setArguments(args); + resultAction = action; + + return fragment; + } + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.automation_dialog_action, container, false); + mUnbinder = ButterKnife.bind(this, view); + + if (savedInstanceState != null) { + String actionData = savedInstanceState.getString("action"); + if (actionData != null) { + try { + mAction = Action.instantiate(new JSONObject(actionData)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + if (mAction == null) + mAction = resultAction; + + mViewActionTitle.setText(mAction.friendlyName()); + mRootLayout.removeAllViews(); + mAction.generateDialog(mRootLayout); + + return view; + } + + @Override + public void onDestroyView() { + mUnbinder.unbind(); + super.onDestroyView(); + } + + @OnClick(R.id.ok) + public void onButtonOk(View view) { + resultAction.copy(mAction); + dismiss(); + } + + @OnClick(R.id.cancel) + public void onButtonCancel(View view) { + dismiss(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putString("action", mAction.toJSON()); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditEventDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditEventDialog.java new file mode 100644 index 0000000000..2289b0dd18 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditEventDialog.java @@ -0,0 +1,153 @@ +package info.nightscout.androidaps.plugins.general.automation.dialogs; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.TextInputEditText; +import android.support.v4.app.DialogFragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.automation.AutomationEvent; +import info.nightscout.androidaps.plugins.general.automation.AutomationFragment; +import info.nightscout.androidaps.plugins.general.automation.AutomationPlugin; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector; + +public class EditEventDialog extends DialogFragment { + public interface OnClickListener { + void onClick(AutomationEvent event); + } + + private static OnClickListener mClickListener = null; + private static AutomationEvent mEvent; + + public static void setOnClickListener(OnClickListener clickListener) { + mClickListener = clickListener; + } + + @BindView(R.id.inputEventTitle) + TextInputEditText mEditEventTitle; + + @BindView(R.id.editTrigger) + TextView mEditTrigger; + + @BindView(R.id.editAction) + TextView mEditAction; + + @BindView(R.id.triggerDescription) + TextView mTriggerDescription; + + @BindView(R.id.actionListView) + RecyclerView mActionListView; + + private Unbinder mUnbinder; + private AutomationFragment.ActionListAdapter mActionListAdapter; + + public static EditEventDialog newInstance(AutomationEvent event) { + mEvent = event; // FIXME + + Bundle args = new Bundle(); + EditEventDialog fragment = new EditEventDialog(); + fragment.setArguments(args); + + return fragment; + } + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.automation_dialog_event, container, false); + mUnbinder = ButterKnife.bind(this, view); + + // load data from bundle + if (savedInstanceState != null) { + String eventData = savedInstanceState.getString("event"); + if (eventData != null) mEvent.fromJSON(eventData); + } else { + mEvent.setTrigger(new TriggerConnector(TriggerConnector.Type.OR)); + } + + // display root trigger + mTriggerDescription.setText(mEvent.getTrigger().friendlyDescription()); + + // setup trigger click event listener + EditTriggerDialog.setOnClickListener(trigger -> { + mEvent.setTrigger(trigger); + mTriggerDescription.setText(mEvent.getTrigger().friendlyDescription()); + }); + mEditTrigger.setOnClickListener(v -> { + EditTriggerDialog dialog = EditTriggerDialog.newInstance(mEvent.getTrigger()); + dialog.show(getFragmentManager(), "EditTriggerDialog"); + }); + + // setup action list view + mActionListAdapter = new AutomationFragment.ActionListAdapter(getFragmentManager(), mEvent.getActions()); + mActionListView.setLayoutManager(new LinearLayoutManager(getContext())); + mActionListView.setAdapter(mActionListAdapter); + + // setup action click event listener + ChooseActionDialog.setOnClickListener(newActionObject -> { + mEvent.addAction(newActionObject); + mActionListAdapter.notifyDataSetChanged(); + }); + mEditAction.setOnClickListener(v -> { + ChooseActionDialog dialog = ChooseActionDialog.newInstance(); + dialog.show(getFragmentManager(), "ChooseActionDialog"); + }); + + + return view; + } + + @Override + public void onDestroyView() { + mUnbinder.unbind(); + super.onDestroyView(); + } + + @OnClick(R.id.ok) + public void onButtonOk(View view) { + // check for title + String title = mEditEventTitle.getText().toString(); + if (title.isEmpty()) { + Toast.makeText(getContext(), R.string.automation_missing_task_name, Toast.LENGTH_LONG).show(); + return; + } + mEvent.setTitle(title); + + // check for at least one trigger + TriggerConnector con = (TriggerConnector) mEvent.getTrigger(); + if (con.size() == 0) { + Toast.makeText(getContext(), R.string.automation_missing_trigger, Toast.LENGTH_LONG).show(); + return; + } + + // check for at least one action + if (mEvent.getActions().isEmpty()) { + Toast.makeText(getContext(), R.string.automation_missing_action, Toast.LENGTH_LONG).show(); + return; + } + + if (mClickListener != null) mClickListener.onClick(mEvent); + dismiss(); + } + + @OnClick(R.id.cancel) + public void onButtonCancel(View view) { + dismiss(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putString("event", mEvent.toJSON()); + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditTriggerDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditTriggerDialog.java new file mode 100644 index 0000000000..a22ed1380c --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/EditTriggerDialog.java @@ -0,0 +1,85 @@ +package info.nightscout.androidaps.plugins.general.automation.dialogs; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Unbinder; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; + +public class EditTriggerDialog extends DialogFragment { + + public interface OnClickListener { + void onClick(Trigger newTriggerObject); + } + + private static OnClickListener mClickListener = null; + + @BindView(R.id.layoutTrigger) + LinearLayout mLayoutTrigger; + + private Trigger mTrigger; + private Unbinder mUnbinder; + + public static EditTriggerDialog newInstance(Trigger trigger) { + Bundle args = new Bundle(); + args.putString("trigger", trigger.toJSON()); + EditTriggerDialog fragment = new EditTriggerDialog(); + fragment.setArguments(args); + return fragment; + } + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.automation_dialog_edit_trigger, container, false); + mUnbinder = ButterKnife.bind(this, view); + + // load data from bundle + Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments(); + if (bundle != null) { + String triggerData = bundle.getString("trigger"); + if (triggerData != null) mTrigger = Trigger.instantiate(triggerData); + } + + // display root trigger + mLayoutTrigger.addView(mTrigger.createView(getContext(), getFragmentManager())); + + return view; + } + + public static void setOnClickListener(OnClickListener clickListener) { + mClickListener = clickListener; + } + + @Override + public void onDestroyView() { + mUnbinder.unbind(); + super.onDestroyView(); + } + + @OnClick(R.id.ok) + public void onButtonOk(View view) { + if (mClickListener != null) + mClickListener.onClick(mTrigger); + + dismiss(); + } + + @OnClick(R.id.cancel) + public void onButtonCancel(View view) { + dismiss(); + } + + @Override + public void onSaveInstanceState(Bundle bundle) { + bundle.putString("trigger", mTrigger.toJSON()); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/Element.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/Element.java new file mode 100644 index 0000000000..de7b0e4dfd --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/Element.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.plugins.general.automation.elements; + +import android.widget.LinearLayout; + +public class Element { + public void generateDialog(LinearLayout root) { } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputBg.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputBg.java new file mode 100644 index 0000000000..b5613345b9 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputBg.java @@ -0,0 +1,87 @@ +package info.nightscout.androidaps.plugins.general.automation.elements; + +import android.text.Editable; +import android.text.TextWatcher; +import android.widget.LinearLayout; + +import java.text.DecimalFormat; + +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.utils.NumberPicker; + +public class InputBg extends Element { + final private TextWatcher textWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + // TODO: validate inputs + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }; + + private String units; + private double value; + private final double minValue, maxValue, step; + private final DecimalFormat decimalFormat; + + public InputBg(String units) { + this.units = units; + + // set default initial value + if (units.equals(Constants.MMOL)) { + // mmol + value = 5.5; + minValue = 2; + maxValue = 30; + step = 0.1; + decimalFormat = new DecimalFormat("0.0"); + } else { + // mg/dL + value = 100; + minValue = 36; + maxValue = 540; + step = 1; + decimalFormat = new DecimalFormat("0"); + } + } + + @Override + public void generateDialog(LinearLayout root) { + NumberPicker numberPicker = new NumberPicker(root.getContext(), null); + numberPicker.setParams(0d, minValue, maxValue, step, decimalFormat, false, textWatcher); + numberPicker.setValue(value); + numberPicker.setOnValueChangedListener(value -> this.value = value); + root.addView(numberPicker); + } + + public String getUnits() { + return units; + } + + public void setUnits(String units) { + if (!this.units.equals(units)) { + String previousUnits = this.units; + this.units = units; + value = Profile.toUnits(Profile.toMgdl(value, previousUnits), Profile.toMmol(value, previousUnits), units); + } + } + + public double getValue() { + return value; + } + + public int getMgdl() { + return (int)Profile.toMgdl(value, units); + } + + public void setMgdl(int value) { + this.value = Profile.fromMgdlToUnits(value, units); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.java new file mode 100644 index 0000000000..abb699da72 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDuration.java @@ -0,0 +1,61 @@ +package info.nightscout.androidaps.plugins.general.automation.elements; + +import android.widget.LinearLayout; + +import java.text.DecimalFormat; + +import info.nightscout.androidaps.utils.NumberPicker; + +public class InputDuration extends Element { + public enum TimeUnit { + MINUTES, + HOURS + } + + private TimeUnit unit; + private double value; + + public InputDuration(double value, TimeUnit unit) { + this.unit = unit; + this.value = value; + } + + @Override + public void generateDialog(LinearLayout root) { + NumberPicker numberPicker = new NumberPicker(root.getContext(), null); + if (unit.equals(TimeUnit.MINUTES)) { + // Minutes + numberPicker.setParams(0d, 0d, 24 * 60d, 10d, new DecimalFormat("0"), false); + } else { + // Hours + numberPicker.setParams(0d, 0d, 24d, 1d, new DecimalFormat("0"), false); + } + numberPicker.setValue(value); + numberPicker.setOnValueChangedListener(value -> this.value = value); + root.addView(numberPicker); + } + + public TimeUnit getUnit() { + return unit; + } + + public double getValue() { + return value; + } + + public void setMinutes(double value) { + if (unit.equals(TimeUnit.MINUTES)) { + this.value = value; + } else { + this.value = value / 60d; + } + } + + public double getMinutes() { + if (unit.equals(TimeUnit.MINUTES)) { + return value; + } else { + return value * 60d; + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/Label.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/Label.java new file mode 100644 index 0000000000..939b3aa2ac --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/Label.java @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.plugins.general.automation.elements; + +import android.graphics.Typeface; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import info.nightscout.androidaps.MainApp; + +public class Label extends Element { + private final Element element; + private final String textPre; + private final String textPost; + + public Label(String textPre, String textPost, Element element) { + this.element = element; + this.textPre = textPre; + this.textPost = textPost; + } + + @Override + public void generateDialog(LinearLayout root) { + // container layout + LinearLayout layout = new LinearLayout(root.getContext()); + layout.setOrientation(LinearLayout.HORIZONTAL); + layout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + // text view pre element + int px = MainApp.dpToPx(10); + TextView textViewPre = new TextView(root.getContext()); + textViewPre.setText(textPre); + textViewPre.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + textViewPre.setWidth(MainApp.dpToPx(120)); + textViewPre.setPadding(px, px, px, px); + textViewPre.setTypeface(textViewPre.getTypeface(), Typeface.BOLD); + layout.addView(textViewPre); + + // add element to layout + element.generateDialog(layout); + + // text view post element + if (textPost != null) { + px = MainApp.dpToPx(5); + TextView textViewPost = new TextView(root.getContext()); + textViewPost.setText(textPost); + textViewPost.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + textViewPost.setWidth(MainApp.dpToPx(45)); + textViewPost.setPadding(px, px, px, px); + textViewPost.setTypeface(textViewPost.getTypeface(), Typeface.BOLD); + layout.addView(textViewPost); + } + + // add layout to root layout + root.addView(layout); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/Trigger.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/Trigger.java new file mode 100644 index 0000000000..2f766a14c8 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/Trigger.java @@ -0,0 +1,142 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import android.support.v4.app.FragmentManager; +import android.content.Context; +import android.support.annotation.StringRes; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; + +public abstract class Trigger { + + public enum Comparator { + IS_LOWER, + IS_EQUAL_OR_LOWER, + IS_EQUAL, + IS_EQUAL_OR_GREATER, + IS_GREATER, + IS_NOT_AVAILABLE; + + public @StringRes int getStringRes() { + switch (this) { + case IS_LOWER: + return R.string.islower; + case IS_EQUAL_OR_LOWER: + return R.string.isequalorlower; + case IS_EQUAL: + return R.string.isequal; + case IS_EQUAL_OR_GREATER: + return R.string.isequalorgreater; + case IS_GREATER: + return R.string.isgreater; + case IS_NOT_AVAILABLE: + return R.string.isnotavailable; + default: + return R.string.unknown; + } + } + + public boolean check(T obj1, T obj2) { + if (obj1 == null || obj2 == null) + return this.equals(Comparator.IS_NOT_AVAILABLE); + + int comparison = obj1.compareTo(obj2); + switch (this) { + case IS_LOWER: + return comparison < 0; + case IS_EQUAL_OR_LOWER: + return comparison <= 0; + case IS_EQUAL: + return comparison == 0; + case IS_EQUAL_OR_GREATER: + return comparison >= 0; + case IS_GREATER: + return comparison > 0; + default: + return false; + } + } + + public static List labels() { + List list = new ArrayList<>(); + for(Comparator c : values()) { + list.add(MainApp.gs(c.getStringRes())); + } + return list; + } + } + + protected TriggerConnector connector = null; + + Trigger() { + } + + public TriggerConnector getConnector() { + return connector; + } + + public abstract boolean shouldRun(); + + public abstract String toJSON(); + + /*package*/ abstract Trigger fromJSON(String data); + + public abstract int friendlyName(); + + public abstract String friendlyDescription(); + + public abstract Optional icon(); + + void notifyAboutRun(long time) { + } + + public abstract Trigger duplicate(); + + public static Trigger instantiate(String json) { + try { + return instantiate(new JSONObject(json)); + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + + public static Trigger instantiate(JSONObject object) { + try { + String type = object.getString("type"); + JSONObject data = object.getJSONObject("data"); + Class clazz = Class.forName(type); + return ((Trigger) clazz.newInstance()).fromJSON(data.toString()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | JSONException e) { + e.printStackTrace(); + } + return null; + } + + public View createView(Context context, FragmentManager fragmentManager) { + final int padding = MainApp.dpToPx(4); + + LinearLayout root = new LinearLayout(context); + root.setPadding(padding, padding, padding, padding); + root.setOrientation(LinearLayout.VERTICAL); + root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + TextView title = new TextView(context); + title.setText(friendlyName()); + root.addView(title); + + return root; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBg.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBg.java new file mode 100644 index 0000000000..cd3217b414 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBg.java @@ -0,0 +1,208 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import android.support.v4.app.FragmentManager; +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +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 android.widget.TextView; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.DecimalFormat; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.GlucoseStatus; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; +import info.nightscout.androidaps.utils.JsonHelper; +import info.nightscout.androidaps.utils.NumberPicker; + +public class TriggerBg extends Trigger { + + private double threshold = 100.0; // FIXME + private Comparator comparator = Comparator.IS_EQUAL; + private String units = ProfileFunctions.getInstance().getProfileUnits(); + + final private TextWatcher textWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + // TODO: validate inputs + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }; + + public TriggerBg() { + super(); + } + + private TriggerBg(TriggerBg triggerBg) { + super(); + comparator = triggerBg.comparator; + units = triggerBg.units; + threshold = triggerBg.threshold; + } + + public double getThreshold() { + return threshold; + } + + public Comparator getComparator() { + return comparator; + } + + public String getUnits() { + return units; + } + + @Override + public synchronized boolean shouldRun() { + GlucoseStatus glucoseStatus = GlucoseStatus.getGlucoseStatusData(); + + if (glucoseStatus == null && comparator.equals(Comparator.IS_NOT_AVAILABLE)) + return true; + if (glucoseStatus == null) + return false; + + return comparator.check(glucoseStatus.glucose, Profile.toMgdl(threshold, units)); + } + + @Override + public synchronized String toJSON() { + JSONObject o = new JSONObject(); + try { + o.put("type", TriggerBg.class.getName()); + JSONObject data = new JSONObject(); + data.put("threshold", threshold); + data.put("comparator", comparator.toString()); + data.put("units", units); + o.put("data", data); + } catch (JSONException e) { + e.printStackTrace(); + } + return o.toString(); + } + + @Override + Trigger fromJSON(String data) { + try { + JSONObject d = new JSONObject(data); + threshold = JsonHelper.safeGetDouble(d, "threshold"); + comparator = Comparator.valueOf(JsonHelper.safeGetString(d, "comparator")); + units = JsonHelper.safeGetString(d, "units"); + } catch (JSONException e) { + e.printStackTrace(); + } + return this; + } + + @Override + public int friendlyName() { + return R.string.glucose; + } + + @Override + public String friendlyDescription() { + if (comparator.equals(Comparator.IS_NOT_AVAILABLE)) + return MainApp.gs(R.string.glucoseisnotavailable); + else + return MainApp.gs(R.string.glucosecompared, MainApp.gs(comparator.getStringRes()), threshold, units); + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.icon_cp_bgcheck); + } + + @Override + public Trigger duplicate() { + return new TriggerBg(this); + } + + TriggerBg threshold(double threshold) { + this.threshold = threshold; + return this; + } + + TriggerBg comparator(Comparator comparator) { + this.comparator = comparator; + return this; + } + + TriggerBg units(String units) { + this.units = units; + return this; + } + + @Override + public View createView(Context context, FragmentManager fragmentManager) { + LinearLayout root = (LinearLayout) super.createView(context, fragmentManager); + + // spinner for comparator + Spinner spinner = new Spinner(context); + ArrayAdapter spinnerArrayAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, Comparator.labels()); + spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(spinnerArrayAdapter); + LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 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) { + comparator = Comparator.values()[position]; + } + + @Override + public void onNothingSelected(AdapterView parent) { } + }); + spinner.setSelection(comparator.ordinal()); + root.addView(spinner); + + // horizontal layout + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.HORIZONTAL); + layout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + root.addView(layout); + + // input field for threshold + NumberPicker numberPicker = new NumberPicker(context, null); + numberPicker.setParams(0d, 0d, (double) 500, 1d, new DecimalFormat("0"), false, textWatcher); + numberPicker.setValue(threshold); + numberPicker.setOnValueChangedListener(value -> threshold = value); + layout.addView(numberPicker); + + // text view for unit + TextView tvUnits = new TextView(context); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.MATCH_PARENT + ); + params.setMargins(MainApp.dpToPx(6), 0, 0, 0); + tvUnits.setLayoutParams(params); + tvUnits.setText(units); + tvUnits.setGravity(Gravity.CENTER_VERTICAL); + layout.addView(tvUnits); + + return root; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerConnector.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerConnector.java new file mode 100644 index 0000000000..db4541f373 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerConnector.java @@ -0,0 +1,262 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import android.support.v4.app.FragmentManager; +import android.content.Context; +import android.support.annotation.StringRes; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.google.common.base.Optional; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.general.automation.AutomationFragment; +import info.nightscout.androidaps.utils.JsonHelper; + +public class TriggerConnector extends Trigger { + public enum Type { + AND, + OR, + XOR; + + public boolean apply(boolean a, boolean b) { + switch (this) { + case AND: + return a && b; + case OR: + return a || b; + case XOR: + return a ^ b; + } + return false; + } + + public @StringRes int getStringRes() { + switch (this) { + case OR: + return R.string.or; + case XOR: + return R.string.xor; + + default: + case AND: + return R.string.and; + } + } + + public static List labels() { + List list = new ArrayList<>(); + for(Type t : values()) { + list.add(MainApp.gs(t.getStringRes())); + } + return list; + } + } + + public static void fillIconSet(TriggerConnector connector, HashSet set) { + for(Trigger t : connector.list) { + if (t instanceof TriggerConnector) { + fillIconSet((TriggerConnector) t, set); + } else { + Optional icon = t.icon(); + if (icon.isPresent()) { + set.add(icon.get()); + } + } + } + } + + protected List list = new ArrayList<>(); + private Type connectorType; + + public TriggerConnector() { + connectorType = Type.AND; + } + + public TriggerConnector(Type connectorType) { + this.connectorType = connectorType; + } + + public void changeConnectorType(Type type) { this.connectorType = type; } + + public Type getConnectorType() { return connectorType; } + + public synchronized void add(Trigger t) { + list.add(t); + t.connector = this; + } + + public synchronized void add(int pos, Trigger t) { + list.add(pos, t); + t.connector = this; + } + + public synchronized boolean remove(Trigger t) { + return list.remove(t); + } + + public int size() { + return list.size(); + } + + public Trigger get(int i) { + return list.get(i); + } + + public int pos(Trigger trigger) { + for(int i = 0; i < list.size(); ++i) { + if (list.get(i) == trigger) return i; + } + return -1; + } + + @Override + public synchronized boolean shouldRun() { + boolean result = true; + + // check first trigger + if (list.size() > 0) + result = list.get(0).shouldRun(); + + // check all others + for (int i = 1; i < list.size(); ++i) { + result = connectorType.apply(result, list.get(i).shouldRun()); + } + + return result; + } + + @Override + public synchronized String toJSON() { + JSONObject o = new JSONObject(); + try { + o.put("type", TriggerConnector.class.getName()); + JSONObject data = new JSONObject(); + data.put("connectorType", connectorType.toString()); + JSONArray array = new JSONArray(); + for (Trigger t : list) { + array.put(t.toJSON()); + } + data.put("triggerList", array); + o.put("data", data); + } catch (JSONException e) { + e.printStackTrace(); + } + return o.toString(); + } + + @Override + Trigger fromJSON(String data) { + try { + JSONObject d = new JSONObject(data); + connectorType = Type.valueOf(JsonHelper.safeGetString(d, "connectorType")); + JSONArray array = d.getJSONArray("triggerList"); + for (int i = 0; i < array.length(); i++) { + Trigger newItem = instantiate(new JSONObject(array.getString(i))); + add(newItem); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return this; + } + + @Override + public int friendlyName() { + return connectorType.getStringRes(); + } + + @Override + public String friendlyDescription() { + int counter = 0; + StringBuilder result = new StringBuilder(); + for (Trigger t : list) { + if (counter++ > 0) result.append(" " + MainApp.gs(friendlyName()) + " "); + result.append(t.friendlyDescription()); + } + return result.toString(); + } + + @Override + public Optional icon() { + return Optional.absent(); + } + + @Override + public Trigger duplicate() { + return null; + } + + private AutomationFragment.TriggerListAdapter adapter; + + public void rebuildView() { + if (adapter != null) + adapter.rebuild(); + } + + @Override + public View createView(Context context, FragmentManager fragmentManager) { + final int padding = MainApp.dpToPx(5); + + LinearLayout root = new LinearLayout(context); + root.setOrientation(LinearLayout.VERTICAL); + root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + root.setPadding(padding,padding,padding,padding); + root.setBackgroundResource(R.drawable.border_automation_unit); + + LinearLayout triggerListLayout = new LinearLayout(context); + triggerListLayout.setOrientation(LinearLayout.VERTICAL); + triggerListLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + root.addView(triggerListLayout); + + adapter = new AutomationFragment.TriggerListAdapter(context, fragmentManager, triggerListLayout, this); + + return root; + } + + public TriggerConnector simplify() { + // simplify children + for(int i = 0; i < size(); ++i) { + if (get(i) instanceof TriggerConnector) { + TriggerConnector t = (TriggerConnector) get(i); + t.simplify(); + } + } + + // drop connector with only 1 element + if (size() == 1 && get(0) instanceof TriggerConnector) { + TriggerConnector c = (TriggerConnector) get(0); + remove(c); + changeConnectorType(c.getConnectorType()); + for (Trigger t : c.list) { + add(t); + } + c.list.clear(); + return simplify(); + } + + // merge connectors + if (connector != null && (connector.getConnectorType().equals(connectorType) || size() == 1)) { + final int pos = connector.pos(this); + connector.remove(this); + // move triggers of child connector into parent connector + for (int i = size()-1; i >= 0; --i) { + connector.add(pos, get(i)); + } + list.clear(); + return connector.simplify(); + } + + return this; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerTime.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerTime.java new file mode 100644 index 0000000000..faeeafcb1d --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerTime.java @@ -0,0 +1,303 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import android.support.v4.app.FragmentManager; +import android.content.Context; +import android.support.annotation.StringRes; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.dpro.widgets.WeekdaysPicker; +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.JsonHelper; +import info.nightscout.androidaps.utils.T; + +public class TriggerTime extends Trigger { + + public enum DayOfWeek { + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY, + SUNDAY; + + private static final int[] calendarInts = new int[] { + Calendar.MONDAY, + Calendar.TUESDAY, + Calendar.WEDNESDAY, + Calendar.THURSDAY, + Calendar.FRIDAY, + Calendar.SATURDAY, + Calendar.SUNDAY + }; + + private static final int[] fullNames = new int[] { + R.string.weekday_monday, + R.string.weekday_tuesday, + R.string.weekday_wednesday, + R.string.weekday_thursday, + R.string.weekday_friday, + R.string.weekday_saturday, + R.string.weekday_sunday + }; + + private static final int[] shortNames = new int[] { + R.string.weekday_monday_short, + R.string.weekday_tuesday_short, + R.string.weekday_wednesday_short, + R.string.weekday_thursday_short, + R.string.weekday_friday_short, + R.string.weekday_saturday_short, + R.string.weekday_sunday_short + }; + + public int toCalendarInt() { + return calendarInts[ordinal()]; + } + + public static DayOfWeek fromCalendarInt(int day) { + for(int i = 0; i < calendarInts.length; ++i) { + if (calendarInts[i] == day) + return values()[i]; + } + return null; + } + + public @StringRes int getFullName() { + return fullNames[ordinal()]; + } + + public @StringRes int getShortName() { + return shortNames[ordinal()]; + } + } + + private final boolean[] weekdays = new boolean[DayOfWeek.values().length]; + + private long lastRun; + + // Single execution + private long runAt; + + // Recurring + private boolean recurring; + private int hour; + private int minute; + + private long validTo; + + public TriggerTime() { + super(); + setAll(false); + } + + private TriggerTime(TriggerTime triggerTime) { + super(); + lastRun = triggerTime.lastRun; + runAt = triggerTime.runAt; + recurring = triggerTime.recurring; + hour = triggerTime.hour; + minute = triggerTime.minute; + validTo = triggerTime.validTo; + + for(int i = 0; i < weekdays.length; ++i) { + weekdays[i] = triggerTime.weekdays[i]; + } + } + + public void setAll(boolean value) { + for(DayOfWeek day : DayOfWeek.values()) { + set(day, value); + } + } + + public TriggerTime set(DayOfWeek day, boolean value) { + weekdays[day.ordinal()] = value; + return this; + } + + public boolean isSet(DayOfWeek day) { + return weekdays[day.ordinal()]; + } + + public long getLastRun() { + return lastRun; + } + + public long getRunAt() { + return runAt; + } + + public boolean isRecurring() { + return recurring; + } + + @Override + public boolean shouldRun() { + if (recurring) { + if (validTo != 0 && DateUtil.now() > validTo) + return false; + Calendar c = Calendar.getInstance(); + int scheduledDayOfWeek = c.get(Calendar.DAY_OF_WEEK); + + Calendar scheduledCal = DateUtil.gregorianCalendar(); + scheduledCal.set(Calendar.HOUR_OF_DAY, hour); + scheduledCal.set(Calendar.MINUTE, minute); + scheduledCal.set(Calendar.SECOND, 0); + long scheduled = scheduledCal.getTimeInMillis(); + + if (isSet(DayOfWeek.fromCalendarInt(scheduledDayOfWeek))) { + if (DateUtil.now() >= scheduled && DateUtil.now() - scheduled < T.mins(5).msecs()) { + if (lastRun < scheduled) + return true; + } + } + return false; + } else { + long now = DateUtil.now(); + if (now >= runAt && now - runAt < T.mins(5).msecs()) + return true; + return false; + } + } + + @Override + public String toJSON() { + JSONObject object = new JSONObject(); + JSONObject data = new JSONObject(); + try { + data.put("lastRun", lastRun); + data.put("runAt", runAt); + data.put("recurring", recurring); + for(int i = 0; i < weekdays.length; ++i) { + data.put(DayOfWeek.values()[i].name(), weekdays[i]); + } + data.put("hour", hour); + data.put("minute", minute); + data.put("validTo", validTo); + object.put("type", TriggerTime.class.getName()); + object.put("data", data); + } catch (JSONException e) { + e.printStackTrace(); + } + return object.toString(); + } + + @Override + Trigger fromJSON(String data) { + JSONObject o; + try { + o = new JSONObject(data); + lastRun = JsonHelper.safeGetLong(o, "lastRun"); + runAt = JsonHelper.safeGetLong(o, "runAt"); + recurring = JsonHelper.safeGetBoolean(o, "recurring"); + for(int i = 0; i < weekdays.length; ++i) { + weekdays[i] = JsonHelper.safeGetBoolean(o, DayOfWeek.values()[i].name()); + } + hour = JsonHelper.safeGetInt(o, "hour"); + minute = JsonHelper.safeGetInt(o, "minute"); + validTo = JsonHelper.safeGetLong(o, "validTo"); + } catch (JSONException e) { + e.printStackTrace(); + } + return this; + } + + @Override + public int friendlyName() { + return R.string.time; + } + + @Override + public String friendlyDescription() { + if (recurring) { + // TODO + return "Every "; + } else { + return MainApp.gs(R.string.atspecifiedtime, DateUtil.dateAndTimeString(runAt)); + } + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.ic_access_alarm_24dp); + } + + @Override + void notifyAboutRun(long time) { + lastRun = time; + } + + @Override + public Trigger duplicate() { + return new TriggerTime(this); + } + + TriggerTime lastRun(long lastRun) { + this.lastRun = lastRun; + return this; + } + + TriggerTime runAt(long runAt) { + this.runAt = runAt; + return this; + } + + TriggerTime recurring(boolean recurring) { + this.recurring = recurring; + return this; + } + + TriggerTime validTo(long validTo) { + this.validTo = validTo; + return this; + } + + TriggerTime hour(int hour) { + this.hour = hour; + return this; + } + + TriggerTime minute(int minute) { + this.minute = minute; + return this; + } + + private List getSelectedDays() { + List selectedDays = new ArrayList<>(); + for(int i = 0; i < weekdays.length; ++i) { + DayOfWeek day = DayOfWeek.values()[i]; + boolean selected = weekdays[i]; + if (selected) selectedDays.add(day.toCalendarInt()); + } + return selectedDays; + } + + @Override + public View createView(Context context, FragmentManager fragmentManager) { + LinearLayout root = (LinearLayout) super.createView(context, fragmentManager); + + // TODO: Replace external tool WeekdaysPicker with a self-made GUI element + WeekdaysPicker weekdaysPicker = new WeekdaysPicker(context); + weekdaysPicker.setEditable(true); + weekdaysPicker.setSelectedDays(getSelectedDays()); + weekdaysPicker.setOnWeekdaysChangeListener((view, i, list) -> set(DayOfWeek.fromCalendarInt(i), list.contains(i))); + weekdaysPicker.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(weekdaysPicker); + return root; + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/persistentNotification/PersistentNotificationPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/persistentNotification/PersistentNotificationPlugin.java index 0287c21260..cd97b21f06 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/persistentNotification/PersistentNotificationPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/persistentNotification/PersistentNotificationPlugin.java @@ -14,6 +14,14 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; import android.support.v4.app.TaskStackBuilder; +// Android Auto +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.RemoteInput; + + + + import com.squareup.otto.Subscribe; import info.nightscout.androidaps.Constants; diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.java index 95ceccab3d..9720e2d253 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/smsCommunicator/SmsCommunicatorPlugin.java @@ -242,6 +242,7 @@ public class SmsCommunicatorPlugin extends PluginBase { LoopPlugin loopPlugin = MainApp.getSpecificPlugin(LoopPlugin.class); if (loopPlugin != null && loopPlugin.isEnabled(PluginType.LOOP)) { loopPlugin.setPluginEnabled(PluginType.LOOP, false); + ConfigBuilderPlugin.getPlugin().storeSettings("SMS_LOOP_STOP"); ConfigBuilderPlugin.getPlugin().getCommandQueue().cancelTempBasal(true, new Callback() { @Override public void run() { @@ -260,6 +261,7 @@ public class SmsCommunicatorPlugin extends PluginBase { loopPlugin = MainApp.getSpecificPlugin(LoopPlugin.class); if (loopPlugin != null && !loopPlugin.isEnabled(PluginType.LOOP)) { loopPlugin.setPluginEnabled(PluginType.LOOP, true); + ConfigBuilderPlugin.getPlugin().storeSettings("SMS_LOOP_START"); reply = MainApp.gs(R.string.smscommunicator_loophasbeenenabled); sendSMS(new Sms(receivedSms.phoneNumber, reply, System.currentTimeMillis())); MainApp.bus().post(new EventRefreshOverview("SMS_LOOP_START")); diff --git a/app/src/main/java/info/nightscout/androidaps/services/LocationService.java b/app/src/main/java/info/nightscout/androidaps/services/LocationService.java new file mode 100644 index 0000000000..b2d40b02bc --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/services/LocationService.java @@ -0,0 +1,146 @@ +package info.nightscout.androidaps.services; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.app.ActivityCompat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.events.EventLocationChange; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.utils.SP; + +public class LocationService extends Service { + private static Logger log = LoggerFactory.getLogger(L.LOCATION); + + private LocationManager mLocationManager = null; + private static final int LOCATION_INTERVAL = 1000; + private static final float LOCATION_DISTANCE = 10f; + + public LocationService() { + MainApp.bus().register(this); + } + + private class LocationListener implements android.location.LocationListener { + Location mLastLocation; + + public LocationListener(String provider) { + if (L.isEnabled(L.LOCATION)) + log.debug("LocationListener " + provider); + mLastLocation = new Location(provider); + } + + @Override + public void onLocationChanged(Location location) { + if (L.isEnabled(L.LOCATION)) + log.debug("onLocationChanged: " + location); + mLastLocation.set(location); + MainApp.bus().post(new EventLocationChange(location)); + } + + @Override + public void onProviderDisabled(String provider) { + if (L.isEnabled(L.LOCATION)) + log.debug("onProviderDisabled: " + provider); + } + + @Override + public void onProviderEnabled(String provider) { + if (L.isEnabled(L.LOCATION)) + log.debug("onProviderEnabled: " + provider); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (L.isEnabled(L.LOCATION)) + log.debug("onStatusChanged: " + provider); + } + } + + LocationListener mLocationListener = new LocationListener(LocationManager.PASSIVE_PROVIDER); + + @Override + public IBinder onBind(Intent arg0) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (L.isEnabled(L.LOCATION)) + log.debug("onStartCommand"); + super.onStartCommand(intent, flags, startId); + return START_STICKY; + } + + @Override + public void onCreate() { + + if (L.isEnabled(L.LOCATION)) + log.debug("onCreate"); + + initializeLocationManager(); + + try { + if (SP.getString(R.string.key_location, "NONE").equals("NETWORK")) + mLocationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + LOCATION_INTERVAL, + LOCATION_DISTANCE, + mLocationListener + ); + if (SP.getString(R.string.key_location, "NONE").equals("GPS")) + mLocationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + LOCATION_INTERVAL, + LOCATION_DISTANCE, + mLocationListener + ); + if (SP.getString(R.string.key_location, "NONE").equals("PASSIVE")) + mLocationManager.requestLocationUpdates( + LocationManager.PASSIVE_PROVIDER, + LOCATION_INTERVAL, + LOCATION_DISTANCE, + mLocationListener + ); + } catch (java.lang.SecurityException ex) { + log.error("fail to request location update, ignore", ex); + } catch (IllegalArgumentException ex) { + log.error("network provider does not exist, " + ex.getMessage()); + } + } + + @Override + public void onDestroy() { + if (L.isEnabled(L.LOCATION)) + log.debug("onDestroy"); + super.onDestroy(); + MainApp.bus().unregister(this); + if (mLocationManager != null) { + try { + if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return; + } + mLocationManager.removeUpdates(mLocationListener); + } catch (Exception ex) { + log.error("fail to remove location listener, ignore", ex); + } + } + } + + private void initializeLocationManager() { + if (L.isEnabled(L.LOCATION)) + log.debug("initializeLocationManager - LOCATION_INTERVAL: " + LOCATION_INTERVAL + " LOCATION_DISTANCE: " + LOCATION_DISTANCE); + if (mLocationManager == null) { + mLocationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE); + } + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java b/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java index 7cc4424244..9ce121962b 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/DateUtil.java @@ -187,4 +187,8 @@ public class DateUtil { long diff = Math.abs(date - now()); return diff < T.mins(2).msecs(); } + + public static GregorianCalendar gregorianCalendar() { + return new GregorianCalendar(); + } } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/NumberPicker.java b/app/src/main/java/info/nightscout/androidaps/utils/NumberPicker.java index 71c16442dc..f32b79c439 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/NumberPicker.java +++ b/app/src/main/java/info/nightscout/androidaps/utils/NumberPicker.java @@ -34,6 +34,10 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, View.OnTouchListener, View.OnClickListener { private static Logger log = LoggerFactory.getLogger(NumberPicker.class); + public interface OnValueChangedListener { + void onValueChanged(double value); + } + TextView editText; Button minusButton; Button plusButton; @@ -48,6 +52,7 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, private Handler mHandler; private ScheduledExecutorService mUpdater; + private OnValueChangedListener mOnValueChangedListener; private class UpdateCounterTask implements Runnable { private boolean mInc; @@ -139,10 +144,15 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, @Override public void afterTextChanged(Editable s) { value = SafeParse.stringToDouble(editText.getText().toString()); + callValueChangedListener(); } }); } + public void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) { + mOnValueChangedListener = onValueChangedListener; + } + public void setTextWatcher(TextWatcher textWatcher) { this.textWatcher = textWatcher; editText.addTextChangedListener(textWatcher); @@ -164,6 +174,7 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, this.step = step; this.formater = formater; this.allowZero = allowZero; + callValueChangedListener(); editText.setKeyListener(DigitsKeyListener.getInstance(minValue < 0, step != Math.rint(step))); @@ -178,6 +189,7 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, if (textWatcher != null) editText.removeTextChangedListener(textWatcher); this.value = value; + callValueChangedListener(); updateEditText(); if (textWatcher != null) editText.addTextChangedListener(textWatcher); @@ -199,6 +211,7 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, value += step * multiplier; if (value > maxValue) { value = maxValue; + callValueChangedListener(); ToastUtils.showToastInUiThread(MainApp.instance().getApplicationContext(), MainApp.gs(R.string.youareonallowedlimit)); stopUpdating(); } @@ -209,6 +222,7 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, value -= step * multiplier; if (value < minValue) { value = minValue; + callValueChangedListener(); ToastUtils.showToastInUiThread(MainApp.instance().getApplicationContext(), MainApp.gs(R.string.youareonallowedlimit)); stopUpdating(); } @@ -222,6 +236,11 @@ public class NumberPicker extends LinearLayout implements View.OnKeyListener, editText.setText(formater.format(value)); } + private void callValueChangedListener() { + if (mOnValueChangedListener != null) + mOnValueChangedListener.onValueChanged(value); + } + private void startUpdating(boolean inc) { if (mUpdater != null) { log.debug("Another executor is still active"); diff --git a/app/src/main/java/info/nightscout/utils/MidnightTime.java b/app/src/main/java/info/nightscout/utils/MidnightTime.java new file mode 100644 index 0000000000..523fe13826 --- /dev/null +++ b/app/src/main/java/info/nightscout/utils/MidnightTime.java @@ -0,0 +1,43 @@ +package info.nightscout.utils; + +import android.util.LongSparseArray; + +import java.util.Calendar; + +public class MidnightTime { + private static LongSparseArray times = new LongSparseArray(); + + private static long hits = 0; + private static long misses = 0; + + public static long calc() { + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + + public static long calc(long time) { + Long m = (Long) times.get(time); + if (m != null) { + ++hits; + return m; + } + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(time); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + m = c.getTimeInMillis(); + times.append(time, m); + ++misses; + return m; + } + + public static String log() { + return "Hits: " + hits + " misses: " + misses + " stored: " + times.size(); + } +} diff --git a/app/src/main/res/drawable/border_automation_unit.xml b/app/src/main/res/drawable/border_automation_unit.xml new file mode 100644 index 0000000000..eb85656295 --- /dev/null +++ b/app/src/main/res/drawable/border_automation_unit.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_access_alarm_24dp.xml b/app/src/main/res/drawable/ic_access_alarm_24dp.xml new file mode 100644 index 0000000000..3e1d84e037 --- /dev/null +++ b/app/src/main/res/drawable/ic_access_alarm_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_add_black_24dp.xml b/app/src/main/res/drawable/ic_add_black_24dp.xml new file mode 100644 index 0000000000..0258249cc4 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml b/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml new file mode 100644 index 0000000000..5304b93e18 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_circle_outline_24dp.xml b/app/src/main/res/drawable/ic_pause_circle_outline_24dp.xml new file mode 100644 index 0000000000..4a469646bd --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_circle_outline_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_circle_outline_24dp.xml b/app/src/main/res/drawable/ic_play_circle_outline_24dp.xml new file mode 100644 index 0000000000..9997bf2034 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_circle_outline_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_replay_24dp.xml b/app/src/main/res/drawable/ic_replay_24dp.xml new file mode 100644 index 0000000000..32942990ac --- /dev/null +++ b/app/src/main/res/drawable/ic_replay_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_stop_24dp.xml b/app/src/main/res/drawable/ic_stop_24dp.xml new file mode 100644 index 0000000000..88299804a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_stop_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/automation_action_item.xml b/app/src/main/res/layout/automation_action_item.xml new file mode 100644 index 0000000000..b6ed902642 --- /dev/null +++ b/app/src/main/res/layout/automation_action_item.xml @@ -0,0 +1,32 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_dialog_action.xml b/app/src/main/res/layout/automation_dialog_action.xml new file mode 100644 index 0000000000..223f459453 --- /dev/null +++ b/app/src/main/res/layout/automation_dialog_action.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_dialog_choose_action.xml b/app/src/main/res/layout/automation_dialog_choose_action.xml new file mode 100644 index 0000000000..291f175cd4 --- /dev/null +++ b/app/src/main/res/layout/automation_dialog_choose_action.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_dialog_choose_trigger.xml b/app/src/main/res/layout/automation_dialog_choose_trigger.xml new file mode 100644 index 0000000000..37e456702e --- /dev/null +++ b/app/src/main/res/layout/automation_dialog_choose_trigger.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_dialog_edit_action.xml b/app/src/main/res/layout/automation_dialog_edit_action.xml new file mode 100644 index 0000000000..d354ebe2a4 --- /dev/null +++ b/app/src/main/res/layout/automation_dialog_edit_action.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_dialog_edit_trigger.xml b/app/src/main/res/layout/automation_dialog_edit_trigger.xml new file mode 100644 index 0000000000..e627d0bb5f --- /dev/null +++ b/app/src/main/res/layout/automation_dialog_edit_trigger.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_dialog_event.xml b/app/src/main/res/layout/automation_dialog_event.xml new file mode 100644 index 0000000000..f4d5352888 --- /dev/null +++ b/app/src/main/res/layout/automation_dialog_event.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_event_item.xml b/app/src/main/res/layout/automation_event_item.xml new file mode 100644 index 0000000000..a481c39755 --- /dev/null +++ b/app/src/main/res/layout/automation_event_item.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/automation_fragment.xml b/app/src/main/res/layout/automation_fragment.xml new file mode 100644 index 0000000000..a43225cb44 --- /dev/null +++ b/app/src/main/res/layout/automation_fragment.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values-ja/insight_alerts.xml b/app/src/main/res/values-ja/insight_alerts.xml new file mode 100644 index 0000000000..70489fbc5e --- /dev/null +++ b/app/src/main/res/values-ja/insight_alerts.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 9ba3919b9f..c87ab94e09 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -118,6 +118,18 @@ @string/yes + + @string/use_passive_location + @string/use_network_location + @string/use_gps_location + + + + PASSIVE + NETWORK + GPS + + Generic AAPS Accu-Chek Spirit diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d1b16a7ad..2d9ccea4c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + Treatments safety Max allowed bolus [U] Max allowed carbs [g] @@ -579,6 +579,7 @@ Loop suspended Suspended (%1$d m) Superbolus (%1$d m) + Suspend loop Suspend loop for 1h Suspend loop for 2h Suspend loop for 3h @@ -1298,6 +1299,50 @@ Min. recovery duration [s] Recovery duration Timeout during handshake - reset bluetooth + Sun + Sat + Fri + Thu + Wed + Tue + Mon + Sunday + Saturday + Friday + Thursday + Wednesday + Tuesday + Monday + User defined automation tasks + Please enter a task name. + Please specify at least one trigger. + Please specify at least one action. + Already enabled + Already disabled + Already suspended + Resume loop + Not suspended + Start temp target + is lower than + is equal or lower than + is equal to + is equal or greater than + is greater than + is not available + unknown + Glucose is not available + Glucose %1$s %2$.2f %3$s + And + Or + Exclusive or + At %1$s + Use network location + Use GPS location + Use passive location + Location service + location + Auto + Automation == ∑ %1$s U U/h diff --git a/app/src/main/res/xml/pref_automation.xml b/app/src/main/res/xml/pref_automation.xml new file mode 100644 index 0000000000..2a590bbaa1 --- /dev/null +++ b/app/src/main/res/xml/pref_automation.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/ComposeTriggerTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/ComposeTriggerTest.java new file mode 100644 index 0000000000..8d676cd229 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/ComposeTriggerTest.java @@ -0,0 +1,137 @@ +package info.nightscout.androidaps.plugins.general.automation; + +import junit.framework.Assert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import info.nightscout.androidaps.plugins.general.automation.triggers.DummyTrigger; +import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({}) +public class ComposeTriggerTest { + @Test + public void testTriggerList() { + TriggerConnector root = new TriggerConnector(); + + // add some triggers + Trigger t0 = new DummyTrigger(); + root.add(t0); + Trigger t1 = new DummyTrigger(); + root.add(t1); + Trigger t2 = new DummyTrigger(); + root.add(t2); + + Assert.assertEquals(3, root.size()); + Assert.assertEquals(t0, root.get(0)); + Assert.assertEquals(t1, root.get(1)); + Assert.assertEquals(t2, root.get(2)); + + // remove a trigger + root.remove(t1); + + Assert.assertEquals(2, root.size()); + Assert.assertEquals(t0, root.get(0)); + Assert.assertEquals(t2, root.get(1)); + } + + @Test + public void testChangeConnector() { + // initialize scenario + TriggerConnector root = new TriggerConnector(TriggerConnector.Type.AND); + Trigger t[] = new Trigger[4]; + for(int i = 0; i < t.length; ++i) { + t[i] = new DummyTrigger(); + root.add(t[i]); + } + Assert.assertEquals(4, root.size()); + + // change connector of t1,t2 from "and" to "or" + Assert.assertEquals(root, t[2].getConnector()); + AutomationFragment.TriggerListAdapter.changeConnector(t[2], t[2].getConnector(), TriggerConnector.Type.OR); + + Assert.assertEquals(3, root.size()); + Assert.assertEquals(t[0], root.get(0)); + Assert.assertEquals(t[3], root.get(2)); + Assert.assertTrue(root.get(1) instanceof TriggerConnector); + + TriggerConnector newConnector = (TriggerConnector) root.get(1); + Assert.assertEquals(2, newConnector.size()); + Assert.assertEquals(t[1], newConnector.get(0)); + Assert.assertEquals(t[2], newConnector.get(1)); + + // undo + Assert.assertEquals(newConnector, t[2].getConnector()); + AutomationFragment.TriggerListAdapter.changeConnector(t[2], t[2].getConnector(), TriggerConnector.Type.AND); + Assert.assertEquals(4, root.size()); + for(int i = 0; i < 4; ++i) { + Assert.assertEquals(t[i], root.get(i)); + } + } + + @Test + public void testConnectorSimplify() { + // initialize scenario + /* + * parent + * -> child + * -> t0 + * -> t1 + */ + TriggerConnector parent = new TriggerConnector(TriggerConnector.Type.AND); + TriggerConnector child = new TriggerConnector(TriggerConnector.Type.AND); + Trigger t0 = new DummyTrigger(); + Trigger t1 = new DummyTrigger(); + child.add(t0); + child.add(t1); + parent.add(child); + Assert.assertEquals(1, parent.size()); + Assert.assertEquals(child, parent.get(0)); + Assert.assertEquals(2, child.size()); + Assert.assertEquals(t0, child.get(0)); + Assert.assertEquals(t1, child.get(1)); + + // simplify + parent.simplify(); + + /* + * parent + * -> t0 + * -> t1 + */ + Assert.assertEquals(2, parent.size()); + Assert.assertEquals(t0, parent.get(0)); + Assert.assertEquals(t1, parent.get(1)); + + // add a new child at position 1 + /* + * parent + * -> t0 + * -> newChild + * -> t2 + * -> t1 + */ + TriggerConnector newChild = new TriggerConnector(TriggerConnector.Type.AND); + Trigger t2 = new DummyTrigger(); + newChild.add(t2); + parent.add(1, newChild); + + // simplify + parent.simplify(); + + /* + * parent + * -> t0 + * -> t2 + * -> t1 + */ + Assert.assertEquals(3, parent.size()); + Assert.assertEquals(t0, parent.get(0)); + Assert.assertEquals(t2, parent.get(1)); + Assert.assertEquals(t1, parent.get(2)); + } +} \ No newline at end of file diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/DummyTrigger.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/DummyTrigger.java new file mode 100644 index 0000000000..087df4326e --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/DummyTrigger.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +public class DummyTrigger extends Trigger { + private boolean result; + + public DummyTrigger() { + this.result = false; + } + + public DummyTrigger(boolean result) { + this.result = result; + } + + @Override + public boolean shouldRun() { + return result; + } + + @Override + public String toJSON() { return null; } + + @Override + Trigger fromJSON(String data) { + return null; + } + + @Override + public int friendlyName() { + return 0; + } + + @Override + public String friendlyDescription() { + return null; + } + + @Override + public Trigger duplicate() { return new DummyTrigger(result); } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBgTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBgTest.java new file mode 100644 index 0000000000..00373c0668 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerBgTest.java @@ -0,0 +1,105 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import com.squareup.otto.Bus; + +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 java.util.ArrayList; +import java.util.List; + +import info.AAPSMocker; +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.db.BgReading; +import info.nightscout.androidaps.plugins.ConfigBuilder.ProfileFunctions; +import info.nightscout.androidaps.plugins.NSClientInternal.data.NSSgv; +import info.nightscout.utils.DateUtil; +import info.nightscout.utils.T; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({MainApp.class, Bus.class, ProfileFunctions.class, DateUtil.class}) +public class TriggerBgTest { + + @Test + public void shouldRunTest() { + when(MainApp.getDbHelper().getBgreadingsDataFromTime(anyLong(), anyBoolean())).thenReturn(generateOneCurrentRecordBgData()); + + TriggerBg t = new TriggerBg().units(Constants.MMOL).threshold(4.1d).comparator(Trigger.Comparator.IS_EQUAL); + Assert.assertFalse(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(214).comparator(Trigger.Comparator.IS_EQUAL); + Assert.assertTrue(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(214).comparator(Trigger.Comparator.IS_EQUAL_OR_GREATER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(214).comparator(Trigger.Comparator.IS_EQUAL_OR_LOWER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(215).comparator(Trigger.Comparator.IS_EQUAL); + Assert.assertFalse(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(215).comparator(Trigger.Comparator.IS_EQUAL_OR_LOWER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(215).comparator(Trigger.Comparator.IS_EQUAL_OR_GREATER); + Assert.assertFalse(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(213).comparator(Trigger.Comparator.IS_EQUAL_OR_GREATER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerBg().units(Constants.MGDL).threshold(213).comparator(Trigger.Comparator.IS_EQUAL_OR_LOWER); + Assert.assertFalse(t.shouldRun()); + + when(MainApp.getDbHelper().getBgreadingsDataFromTime(anyLong(), anyBoolean())).thenReturn(new ArrayList<>()); + t = new TriggerBg().units(Constants.MGDL).threshold(213).comparator(Trigger.Comparator.IS_EQUAL_OR_LOWER); + Assert.assertFalse(t.shouldRun()); + t = new TriggerBg().comparator(Trigger.Comparator.IS_NOT_AVAILABLE); + Assert.assertTrue(t.shouldRun()); + } + + String bgJson = "{\"data\":{\"comparator\":\"IS_EQUAL\",\"threshold\":4.1,\"units\":\"mmol\"},\"type\":\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerBg\"}"; + + @Test + public void toJSONTest() { + TriggerBg t = new TriggerBg().units(Constants.MMOL).threshold(4.1d).comparator(Trigger.Comparator.IS_EQUAL); + Assert.assertEquals(bgJson, t.toJSON()); + } + + @Test + public void fromJSONTest() throws JSONException { + TriggerBg t = new TriggerBg().units(Constants.MMOL).threshold(4.1d).comparator(Trigger.Comparator.IS_EQUAL); + + TriggerBg t2 = (TriggerBg) Trigger.instantiate(new JSONObject(t.toJSON())); + Assert.assertEquals(Trigger.Comparator.IS_EQUAL, t2.getComparator()); + Assert.assertEquals(4.1d, t2.getThreshold(), 0.01d); + Assert.assertEquals(Constants.MMOL, t2.getUnits()); + } + + @Before + public void mock() { + AAPSMocker.mockMainApp(); + AAPSMocker.mockBus(); + AAPSMocker.mockDatabaseHelper(); + AAPSMocker.mockProfileFunctions(); + + PowerMockito.mockStatic(DateUtil.class); + when(DateUtil.now()).thenReturn(1514766900000L + T.mins(1).msecs()); + + } + + List generateOneCurrentRecordBgData() { + List list = new ArrayList<>(); + try { + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":214,\"mills\":1514766900000,\"direction\":\"Flat\"}")))); + } catch (JSONException e) { + e.printStackTrace(); + } + return list; + } + +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerConnectorTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerConnectorTest.java new file mode 100644 index 0000000000..a5ba819b18 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerConnectorTest.java @@ -0,0 +1,98 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({}) +public class TriggerConnectorTest { + + @Test + public void testTriggerList() { + TriggerConnector t = new TriggerConnector(); + TriggerConnector t2 = new TriggerConnector(); + TriggerConnector t3 = new TriggerConnector(); + + Assert.assertTrue(t.size() == 0); + + t.add(t2); + Assert.assertTrue(t.size() == 1); + Assert.assertEquals(t2, t.get(0)); + + t.add(t3); + Assert.assertTrue(t.size() == 2); + Assert.assertEquals(t2, t.get(0)); + Assert.assertEquals(t3, t.get(1)); + + Assert.assertTrue(t.remove(t2)); + Assert.assertTrue(t.size() == 1); + Assert.assertEquals(t3, t.get(0)); + + Assert.assertTrue(t.shouldRun()); + } + + @Test + public void testListTriggerOR() { + TriggerConnector t = new TriggerConnector(TriggerConnector.Type.OR); + t.add(new DummyTrigger(false)); + t.add(new DummyTrigger(false)); + Assert.assertFalse(t.shouldRun()); + + t.add(new DummyTrigger(true)); + t.add(new DummyTrigger(false)); + Assert.assertTrue(t.shouldRun()); + } + + @Test + public void testListTriggerXOR() { + TriggerConnector t = new TriggerConnector(TriggerConnector.Type.XOR); + t.add(new DummyTrigger(false)); + t.add(new DummyTrigger(false)); + Assert.assertFalse(t.shouldRun()); + + t.add(new DummyTrigger(true)); + t.add(new DummyTrigger(false)); + Assert.assertTrue(t.shouldRun()); + + t.add(new DummyTrigger(true)); + t.add(new DummyTrigger(false)); + Assert.assertFalse(t.shouldRun()); + } + + @Test + public void testListTriggerAND() { + TriggerConnector t = new TriggerConnector(TriggerConnector.Type.AND); + t.add(new DummyTrigger(true)); + t.add(new DummyTrigger(true)); + Assert.assertTrue(t.shouldRun()); + + t.add(new DummyTrigger(true)); + t.add(new DummyTrigger(false)); + Assert.assertFalse(t.shouldRun()); + } + + String empty = "{\"data\":{\"connectorType\":\"AND\",\"triggerList\":[]},\"type\":\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector\"}"; + String oneItem = "{\"data\":{\"connectorType\":\"AND\",\"triggerList\":[\"{\\\"data\\\":{\\\"connectorType\\\":\\\"AND\\\",\\\"triggerList\\\":[]},\\\"type\\\":\\\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector\\\"}\"]},\"type\":\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerConnector\"}"; + + @Test + public void toJSONTest() { + TriggerConnector t = new TriggerConnector(); + Assert.assertEquals(empty, t.toJSON()); + t.add(new TriggerConnector()); + Assert.assertEquals(oneItem, t.toJSON()); + } + @Test + public void fromJSONTest() throws JSONException { + TriggerConnector t = new TriggerConnector(); + t.add(new TriggerConnector()); + + TriggerConnector t2 = (TriggerConnector) Trigger.instantiate(new JSONObject(t.toJSON())); + Assert.assertEquals(1, t2.size()); + Assert.assertTrue(t2.get(0) instanceof TriggerConnector); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerTimeTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerTimeTest.java new file mode 100644 index 0000000000..375253e7bf --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerTimeTest.java @@ -0,0 +1,89 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import com.squareup.otto.Bus; + +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 java.util.GregorianCalendar; + +import info.AAPSMocker; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerTime; +import info.nightscout.utils.DateUtil; +import info.nightscout.utils.T; + +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({MainApp.class, Bus.class, DateUtil.class, GregorianCalendar.class}) +public class TriggerTimeTest { + + long now = 1514766900000L; + + @Test + public void shouldRunTest() { + + // scheduled 1 min before + TriggerTime t = new TriggerTime().runAt(now - T.mins(1).msecs()); + Assert.assertTrue(t.shouldRun()); + + // scheduled 1 min in the future + t = new TriggerTime().runAt(now + T.mins(1).msecs()); + Assert.assertFalse(t.shouldRun()); + + // limit by validTo + t = new TriggerTime().recurring(true).hour(1).minute(34).validTo(1); + t.setAll(true); + Assert.assertFalse(t.shouldRun()); + + // scheduled 1 min before + t = new TriggerTime().recurring(true).hour(1).minute(34); + t.setAll(true); + Assert.assertTrue(t.shouldRun()); + + // already run + t = new TriggerTime().recurring(true).hour(1).minute(34).lastRun(now - 1); + t.setAll(true); + Assert.assertFalse(t.shouldRun()); + + } + + String timeJson = "{\"data\":{\"runAt\":1514766840000,\"THURSDAY\":false,\"lastRun\":0,\"SUNDAY\":false,\"recurring\":false,\"TUESDAY\":false,\"FRIDAY\":false,\"minute\":0,\"WEDNESDAY\":false,\"MONDAY\":false,\"hour\":0,\"SATURDAY\":false,\"validTo\":0},\"type\":\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerTime\"}"; + + @Test + public void toJSONTest() { + TriggerTime t = new TriggerTime().runAt(now - T.mins(1).msecs()); + Assert.assertEquals(timeJson, t.toJSON()); + } + + @Test + public void fromJSONTest() throws JSONException { + TriggerTime t = new TriggerTime().runAt(now - T.mins(1).msecs()); + + TriggerTime t2 = (TriggerTime) Trigger.instantiate(new JSONObject(t.toJSON())); + Assert.assertEquals(now - T.mins(1).msecs(), t2.getRunAt()); + Assert.assertEquals(false, t2.isRecurring()); + } + + @Before + public void mock() { + AAPSMocker.mockMainApp(); + AAPSMocker.mockBus(); + + PowerMockito.mockStatic(DateUtil.class); + when(DateUtil.now()).thenReturn(now); + + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(now); + when(DateUtil.gregorianCalendar()).thenReturn(calendar); + } +} diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2PluginTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2PluginTest.java index 288df852f9..140b0165bd 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2PluginTest.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/DanaRv2PluginTest.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2; +package info.nightscout.androidaps.plugins.PumpdanaRv2; import android.content.Context; @@ -13,14 +13,17 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.Constants; import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; import info.nightscout.androidaps.interfaces.Constraint; import info.nightscout.androidaps.interfaces.PluginType; -import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPump; -import info.nightscout.androidaps.utils.SP; -import info.nightscout.androidaps.utils.ToastUtils; +import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPump; +import info.nightscout.androidaps.plugins.PumpDanaRv2.DanaRv2Plugin; +import info.nightscout.utils.SP; +import info.nightscout.utils.ToastUtils; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; /** * Created by Rumen on 01.08.2018 diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MessageHashTable_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MessageHashTable_v2Test.java index 9fa40854a4..63e3af0fa4 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MessageHashTable_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MessageHashTable_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import org.junit.Test; import org.junit.runner.RunWith; @@ -8,8 +8,10 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.pump.danaR.comm.MessageBase; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.plugins.PumpDanaR.comm.MessageBase; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MessageHashTable_v2; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgStatusAPS_v2; +import info.nightscout.utils.SP; import static org.junit.Assert.*; /** diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgCheckValue_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgCheckValue_v2Test.java index acc2675c92..924672f3e0 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgCheckValue_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgCheckValue_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import org.junit.Test; import org.junit.runner.RunWith; @@ -8,13 +8,15 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.configBuilder.ConfigBuilderPlugin; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPlugin; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPump; -import info.nightscout.androidaps.plugins.pump.danaRv2.DanaRv2Plugin; -import info.nightscout.androidaps.plugins.treatments.Treatment; +import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPump; +import info.nightscout.androidaps.plugins.PumpDanaR.comm.MsgCheckValue; +import info.nightscout.androidaps.plugins.PumpDanaRv2.DanaRv2Plugin; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgCheckValue_v2; +import info.nightscout.androidaps.plugins.Treatments.Treatment; import info.nightscout.androidaps.queue.CommandQueue; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.utils.SP; import static org.junit.Assert.*; import static org.powermock.api.mockito.PowerMockito.when; diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgHistoryEvents_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgHistoryEvents_v2Test.java index 334c556ccb..f462894924 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgHistoryEvents_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgHistoryEvents_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import org.junit.Test; @@ -9,9 +9,12 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.treatments.TreatmentService; -import info.nightscout.androidaps.plugins.treatments.TreatmentsPlugin; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.plugins.NSClientInternal.NSUpload; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgHistoryEvents_v2; +import info.nightscout.androidaps.plugins.Treatments.Treatment; +import info.nightscout.androidaps.plugins.Treatments.TreatmentService; +import info.nightscout.androidaps.plugins.Treatments.TreatmentsPlugin; +import info.nightscout.utils.SP; import static org.junit.Assert.*; diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgSetHistoryEntry_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgSetHistoryEntry_v2Test.java index 50f80550b5..a9e4aee39e 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgSetHistoryEntry_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgSetHistoryEntry_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import org.junit.Test; import org.junit.runner.RunWith; @@ -8,9 +8,10 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPlugin; -import info.nightscout.androidaps.plugins.pump.danaRv2.DanaRv2Plugin; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin; +import info.nightscout.androidaps.plugins.PumpDanaRv2.DanaRv2Plugin; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgSetHistoryEntry_v2; +import info.nightscout.utils.SP; import static org.junit.Assert.*; /** diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusAPS_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusAPS_v2Test.java index 63e7011603..c8ae1030ee 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusAPS_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusAPS_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import android.content.Context; @@ -11,9 +11,10 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPump; -import info.nightscout.androidaps.plugins.pump.danaR.comm.MessageBase; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPump; +import info.nightscout.androidaps.plugins.PumpDanaR.comm.MessageBase; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgStatusAPS_v2; +import info.nightscout.utils.SP; import static org.junit.Assert.*; /** * Created by Rumen Georgiev on 30.10.2018 diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusBolusExtended_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusBolusExtended_v2Test.java index 2f12a15240..bde05289c2 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusBolusExtended_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusBolusExtended_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import android.content.Context; @@ -11,9 +11,13 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPump; -import info.nightscout.androidaps.plugins.pump.danaR.comm.MessageBase; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.plugins.NSClientInternal.NSUpload; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPump; +import info.nightscout.androidaps.plugins.PumpDanaRv2.DanaRv2Plugin; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgStatusBolusExtended_v2; +import info.nightscout.androidaps.plugins.PumpDanaR.comm.MessageBase; +import info.nightscout.androidaps.plugins.Treatments.Treatment; +import info.nightscout.utils.SP; import static org.junit.Assert.*; diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusTempBasal_v2Test.java b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusTempBasal_v2Test.java index 70a7a36f0e..cecb246e5f 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusTempBasal_v2Test.java +++ b/app/src/test/java/info/nightscout/androidaps/plugins/pump/danaRv2/comm/MsgStatusTempBasal_v2Test.java @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.plugins.pump.danaRv2.comm; +package info.nightscout.androidaps.plugins.PumpdanaRv2.comm; import org.junit.Test; import org.junit.runner.RunWith; @@ -8,10 +8,11 @@ import org.powermock.modules.junit4.PowerMockRunner; import info.AAPSMocker; import info.nightscout.androidaps.MainApp; import info.nightscout.androidaps.logging.L; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPlugin; -import info.nightscout.androidaps.plugins.pump.danaR.DanaRPump; -import info.nightscout.androidaps.plugins.pump.danaRv2.DanaRv2Plugin; -import info.nightscout.androidaps.utils.SP; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPlugin; +import info.nightscout.androidaps.plugins.PumpDanaR.DanaRPump; +import info.nightscout.androidaps.plugins.PumpDanaRv2.DanaRv2Plugin; +import info.nightscout.androidaps.plugins.PumpDanaRv2.comm.MsgStatusTempBasal_v2; +import info.nightscout.utils.SP; import static org.junit.Assert.*; /**