diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index 4398f80e75..209adfe1c4 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -31,6 +31,7 @@ import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderFragment; import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin; import info.nightscout.androidaps.plugins.ConstraintsObjectives.ObjectivesPlugin; import info.nightscout.androidaps.plugins.ConstraintsSafety.SafetyPlugin; +import info.nightscout.androidaps.plugins.Food.FoodPlugin; import info.nightscout.androidaps.plugins.Insulin.InsulinFastactingPlugin; import info.nightscout.androidaps.plugins.Insulin.InsulinFastactingProlongedPlugin; import info.nightscout.androidaps.plugins.Insulin.InsulinOrefFreePeakPlugin; @@ -149,6 +150,7 @@ public class MainApp extends Application { if (!Config.NSCLIENT) pluginsList.add(SourceGlimpPlugin.getPlugin()); if (Config.SMSCOMMUNICATORENABLED) pluginsList.add(SmsCommunicatorPlugin.getPlugin()); + pluginsList.add(FoodPlugin.getPlugin()); pluginsList.add(WearFragment.getPlugin(this)); pluginsList.add(StatuslinePlugin.getPlugin(this)); @@ -186,6 +188,9 @@ public class MainApp extends Application { lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_NEW_TREATMENT)); lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_CHANGED_TREATMENT)); lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_REMOVED_TREATMENT)); + lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_NEW_FOOD)); + lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_CHANGED_FOOD)); + lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_REMOVED_FOOD)); lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_NEW_SGV)); lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_NEW_PROFILE)); lbm.registerReceiver(dataReceiver, new IntentFilter(Intents.ACTION_NEW_STATUS)); diff --git a/app/src/main/java/info/nightscout/androidaps/Services/DataService.java b/app/src/main/java/info/nightscout/androidaps/Services/DataService.java index dc52107c26..5c152e8060 100644 --- a/app/src/main/java/info/nightscout/androidaps/Services/DataService.java +++ b/app/src/main/java/info/nightscout/androidaps/Services/DataService.java @@ -116,6 +116,9 @@ public class DataService extends IntentService { Intents.ACTION_REMOVED_TREATMENT.equals(action) || Intents.ACTION_NEW_STATUS.equals(action) || Intents.ACTION_NEW_DEVICESTATUS.equals(action) || + Intents.ACTION_NEW_FOOD.equals(action) || + Intents.ACTION_CHANGED_FOOD.equals(action) || + Intents.ACTION_REMOVED_FOOD.equals(action) || Intents.ACTION_NEW_CAL.equals(action) || Intents.ACTION_NEW_MBG.equals(action)) ) { @@ -413,6 +416,56 @@ public class DataService extends IntentService { log.error("Unhandled exception", e); } } + + if (intent.getAction().equals(Intents.ACTION_NEW_FOOD) || intent.getAction().equals(Intents.ACTION_CHANGED_FOOD)) { + try { + if (bundles.containsKey("food")) { + String trstring = bundles.getString("food"); + handleAddChangeFoodRecord(new JSONObject(trstring)); + } + if (bundles.containsKey("foods")) { + String trstring = bundles.getString("foods"); + JSONArray jsonArray = new JSONArray(trstring); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject trJson = jsonArray.getJSONObject(i); + handleAddChangeFoodRecord(trJson); + } + } + } catch (Exception e) { + log.error("Unhandled exception", e); + } + } + + if (intent.getAction().equals(Intents.ACTION_REMOVED_FOOD)) { + try { + if (bundles.containsKey("food")) { + String trstring = bundles.getString("food"); + JSONObject trJson = new JSONObject(trstring); + String _id = trJson.getString("_id"); + handleRemovedFoodRecord(_id); + } + + if (bundles.containsKey("foods")) { + String trstring = bundles.getString("foods"); + JSONArray jsonArray = new JSONArray(trstring); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject trJson = jsonArray.getJSONObject(i); + String _id = trJson.getString("_id"); + handleRemovedFoodRecord(_id); + } + } + } catch (Exception e) { + log.error("Unhandled exception", e); + } + } + } + + private void handleRemovedFoodRecord(String _id) { + MainApp.getDbHelper().foodHelper.deleteFoodById(_id); + } + + public void handleAddChangeFoodRecord(JSONObject trJson) throws JSONException { + MainApp.getDbHelper().foodHelper.createFoodFromJsonIfNotExists(trJson); } private void handleRemovedRecordFromNS(String _id) { diff --git a/app/src/main/java/info/nightscout/androidaps/db/FoodHelper.java b/app/src/main/java/info/nightscout/androidaps/db/FoodHelper.java index fd5406f974..992e328532 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/FoodHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/FoodHelper.java @@ -87,6 +87,8 @@ public class FoodHelper { log.debug("FOOD: Updating record by _id: " + old.toString()); scheduleFoodChange(); return true; + } else { + return false; } } } diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Food/FoodFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Food/FoodFragment.java new file mode 100644 index 0000000000..558cf0d02f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Food/FoodFragment.java @@ -0,0 +1,315 @@ +package info.nightscout.androidaps.plugins.Food; + +import android.app.Activity; +import android.content.DialogInterface; +import android.graphics.Paint; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import com.crashlytics.android.Crashlytics; +import com.squareup.otto.Subscribe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.db.Food; +import info.nightscout.androidaps.events.EventFoodDatabaseChanged; +import info.nightscout.androidaps.plugins.Common.SubscriberFragment; +import info.nightscout.utils.NSUpload; +import info.nightscout.utils.SpinnerHelper; + +/** + * Created by mike on 16.10.2017. + */ + +public class FoodFragment extends SubscriberFragment { + private static Logger log = LoggerFactory.getLogger(FoodFragment.class); + + EditText filter; + ImageView clearFilter; + SpinnerHelper category; + SpinnerHelper subcategory; + RecyclerView recyclerView; + + List unfiltered; + List filtered; + ArrayList categories; + ArrayList subcategories; + + final String EMPTY = MainApp.sResources.getString(R.string.none); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + try { + View view = inflater.inflate(R.layout.food_fragment, container, false); + + filter = (EditText) view.findViewById(R.id.food_filter); + clearFilter = (ImageView) view.findViewById(R.id.food_clearfilter); + category = new SpinnerHelper(view.findViewById(R.id.food_category)); + subcategory = new SpinnerHelper(view.findViewById(R.id.food_subcategory)); + recyclerView = (RecyclerView) view.findViewById(R.id.food_recyclerview); + recyclerView.setHasFixedSize(true); + LinearLayoutManager llm = new LinearLayoutManager(view.getContext()); + recyclerView.setLayoutManager(llm); + + clearFilter.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + filter.setText(""); + category.setSelection(0); + subcategory.setSelection(0); + filterData(); + } + }); + + category.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + fillSubcategories(); + filterData(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + fillSubcategories(); + filterData(); + } + }); + + subcategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + filterData(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + filterData(); + } + }); + + filter.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + filterData(); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + RecyclerViewAdapter adapter = new RecyclerViewAdapter(MainApp.getDbHelper().foodHelper.getFoodData()); + recyclerView.setAdapter(adapter); + + loadData(); + fillCategories(); + fillSubcategories(); + filterData(); + return view; + } catch (Exception e) { + Crashlytics.logException(e); + } + + return null; + } + + @Subscribe + @SuppressWarnings("unused") + public void onStatusEvent(final EventFoodDatabaseChanged ev) { + loadData(); + filterData(); + } + + void loadData() { + unfiltered = MainApp.getDbHelper().foodHelper.getFoodData(); + } + + void fillCategories() { + categories = new ArrayList<>(); + + for (Food f : unfiltered) { + if (f.category != null && !f.category.equals("")) + categories.add(f.category); + } + + // make it unique + categories = new ArrayList<>(new HashSet<>(categories)); + + categories.add(0, MainApp.sResources.getString(R.string.none)); + + ArrayAdapter adapterCategories = new ArrayAdapter<>(getContext(), + R.layout.spinner_centered, categories); + category.setAdapter(adapterCategories); + } + + void fillSubcategories() { + String categoryFilter = category.getSelectedItem().toString(); + subcategories = new ArrayList<>(); + + if (!categoryFilter.equals(EMPTY)) { + for (Food f : unfiltered) { + if (f.category != null && f.category.equals(categoryFilter)) + if (f.subcategory != null && !f.subcategory.equals("")) + subcategories.add(f.subcategory); + } + } + + // make it unique + subcategories = new ArrayList<>(new HashSet<>(subcategories)); + + subcategories.add(0, MainApp.sResources.getString(R.string.none)); + + ArrayAdapter adapterSubcategories = new ArrayAdapter<>(getContext(), + R.layout.spinner_centered, subcategories); + subcategory.setAdapter(adapterSubcategories); + } + + void filterData() { + String textFilter = filter.getText().toString(); + String categoryFilter = category.getSelectedItem().toString(); + String subcategoryFilter = subcategory.getSelectedItem().toString(); + + filtered = new ArrayList<>(); + + for (Food f : unfiltered) { + if (f.name == null || f.category == null || f.subcategory == null) + continue; + + if (!subcategoryFilter.equals(EMPTY) && !f.subcategory.equals(subcategoryFilter)) + continue; + if (!categoryFilter.equals(EMPTY) && !f.category.equals(categoryFilter)) + continue; + if (!textFilter.equals("") && !f.name.toLowerCase().contains(textFilter.toLowerCase())) + continue; + filtered.add(f); + } + + updateGUI(); + } + + @Override + protected void updateGUI() { + Activity activity = getActivity(); + if (activity != null) + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + recyclerView.swapAdapter(new FoodFragment.RecyclerViewAdapter(filtered), true); + } + }); + } + + public class RecyclerViewAdapter extends RecyclerView.Adapter { + + List foodList; + + RecyclerViewAdapter(List foodList) { + this.foodList = foodList; + } + + @Override + public FoodsViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.food_item, viewGroup, false); + return new FoodsViewHolder(v); + } + + @Override + public void onBindViewHolder(FoodsViewHolder holder, int position) { + Food food = foodList.get(position); + holder.ns.setVisibility(food._id != null ? View.VISIBLE : View.GONE); + holder.name.setText(food.name); + holder.portion.setText(food.portion + food.units); + holder.carbs.setText(food.carbs + MainApp.sResources.getString(R.string.shortgramm)); + holder.fat.setText(MainApp.sResources.getString(R.string.shortfat) + ": " + food.fat + MainApp.sResources.getString(R.string.shortgramm)); + if (food.fat == 0) + holder.fat.setVisibility(View.INVISIBLE); + holder.protein.setText(MainApp.sResources.getString(R.string.shortprotein) + ": " + food.protein + MainApp.sResources.getString(R.string.shortgramm)); + if (food.protein == 0) + holder.protein.setVisibility(View.INVISIBLE); + holder.energy.setText(MainApp.sResources.getString(R.string.shortenergy) + ": " + food.energy + MainApp.sResources.getString(R.string.shortkilojoul)); + if (food.energy == 0) + holder.energy.setVisibility(View.INVISIBLE); + holder.remove.setTag(food); + } + + @Override + public int getItemCount() { + return foodList.size(); + } + + class FoodsViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + TextView name; + TextView portion; + TextView carbs; + TextView fat; + TextView protein; + TextView energy; + TextView ns; + TextView remove; + + FoodsViewHolder(View itemView) { + super(itemView); + name = (TextView) itemView.findViewById(R.id.food_name); + portion = (TextView) itemView.findViewById(R.id.food_portion); + carbs = (TextView) itemView.findViewById(R.id.food_carbs); + fat = (TextView) itemView.findViewById(R.id.food_fat); + protein = (TextView) itemView.findViewById(R.id.food_protein); + energy = (TextView) itemView.findViewById(R.id.food_energy); + ns = (TextView) itemView.findViewById(R.id.ns_sign); + remove = (TextView) itemView.findViewById(R.id.food_remove); + remove.setOnClickListener(this); + remove.setPaintFlags(remove.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + } + + @Override + public void onClick(View v) { + final Food food = (Food) v.getTag(); + switch (v.getId()) { + + case R.id.food_remove: + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(MainApp.sResources.getString(R.string.confirmation)); + builder.setMessage(MainApp.sResources.getString(R.string.removerecord) + "\n" + food.name); + builder.setPositiveButton(MainApp.sResources.getString(R.string.ok), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + final String _id = food._id; + if (_id != null && !_id.equals("")) { + NSUpload.removeFoodFromNS(_id); + } + MainApp.getDbHelper().foodHelper.delete(food); + } + }); + builder.setNegativeButton(MainApp.sResources.getString(R.string.cancel), null); + builder.show(); + break; + + } + } + } + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Food/FoodPlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Food/FoodPlugin.java new file mode 100644 index 0000000000..dbd1549c11 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Food/FoodPlugin.java @@ -0,0 +1,80 @@ +package info.nightscout.androidaps.plugins.Food; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.PluginBase; + +/** + * Created by mike on 05.08.2016. + */ +public class FoodPlugin implements PluginBase { + private boolean fragmentEnabled = true; + private boolean fragmentVisible = false; + + private static FoodPlugin plugin = null; + + public static FoodPlugin getPlugin() { + if (plugin == null) + plugin = new FoodPlugin(); + return plugin; + } + + @Override + public String getFragmentClass() { + return FoodFragment.class.getName(); + } + + @Override + public int getType() { + return PluginBase.GENERAL; + } + + @Override + public String getName() { + return MainApp.instance().getString(R.string.food); + } + + @Override + public String getNameShort() { + // use long name as fallback (not visible in tabs) + return getName(); + } + + + @Override + public boolean isEnabled(int type) { + return type == GENERAL && fragmentEnabled; + } + + @Override + public boolean isVisibleInTabs(int type) { + return type == GENERAL && fragmentVisible; + } + + @Override + public boolean canBeHidden(int type) { + return true; + } + + @Override + public boolean hasFragment() { + return true; + } + + @Override + public boolean showInList(int type) { + return true; + } + + @Override + public void setFragmentEnabled(int type, boolean fragmentEnabled) { + if (type == GENERAL) this.fragmentEnabled = fragmentEnabled; + } + + @Override + public void setFragmentVisible(int type, boolean fragmentVisible) { + if (type == GENERAL) this.fragmentVisible = fragmentVisible; + } + + +} diff --git a/app/src/main/java/info/nightscout/utils/NSUpload.java b/app/src/main/java/info/nightscout/utils/NSUpload.java index 98ce8632ff..e11102198f 100644 --- a/app/src/main/java/info/nightscout/utils/NSUpload.java +++ b/app/src/main/java/info/nightscout/utils/NSUpload.java @@ -404,4 +404,23 @@ public class NSUpload { DbLogger.dbAdd(intent, data.toString()); } } + + public static void removeFoodFromNS(String _id) { + try { + Context context = MainApp.instance().getApplicationContext(); + Bundle bundle = new Bundle(); + bundle.putString("action", "dbRemove"); + bundle.putString("collection", "food"); + bundle.putString("_id", _id); + Intent intent = new Intent(Intents.ACTION_DATABASE); + intent.putExtras(bundle); + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + context.sendBroadcast(intent); + DbLogger.dbRemove(intent, _id); + } catch (Exception e) { + log.error("Unhandled exception", e); + } + + } + } diff --git a/app/src/main/res/layout/food_fragment.xml b/app/src/main/res/layout/food_fragment.xml new file mode 100644 index 0000000000..7b42eff11f --- /dev/null +++ b/app/src/main/res/layout/food_fragment.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/food_item.xml b/app/src/main/res/layout/food_item.xml new file mode 100644 index 0000000000..d4efc403a3 --- /dev/null +++ b/app/src/main/res/layout/food_item.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5f3037145..a0f33da8e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,7 +70,7 @@ No pump available No change requested Request - Rate + Rate Duration Reason Glucose @@ -685,10 +685,10 @@ Pass the Overview Notifications through as wear confirmation messages. Enable broadcasts to other apps (like xDrip). Enable local Broadcasts. - ACTIVITY & FEEDBACK - CARBS & BOLUS - CGM & OPENAPS - PUMP + ACTIVITY & FEEDBACK + CARBS & BOLUS + CGM & OPENAPS + PUMP Basal value [U/h] Duration [min] insulin_oref_peak @@ -746,5 +746,12 @@ Controls from Watch Set Temp-Targets and enter Treatments from the watch. Connection timed out + Food + g + ]]> + kJ + En + Pr + Fat