diff --git a/README.md b/README.md index 4ef282b0..83b0681c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ Welcome to grow tracker. This app was created to help record data about growing plants in order to monitor the growing conditions to help make the plants grow better, and identify potential issues during the grow process. -[Latest APK: (MD5) 83771de33b2f23c5157996e68abd8532 v2.3.2](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.3.2/v2.3.2-production.apk) +[Latest APK: (MD5) 45ed03e8b120c6be233a30e743f61e92 v2.4](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.4/v2.4-production.apk) -[Latest APK (Discrete): (MD5) aade560ad850ca6c6996cfa8524dbecb v2.3.2](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.3.2/v2.3.2-discrete.apk) +[Latest APK (Discrete): (MD5) e154652199c8598c2457f5b606237e1b v2.4](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.4/v2.4-discrete.apk) + +You can follow development, post questions, or grow logs in the [Subreddit](https://reddit.com/r/growutils) # Installation @@ -20,8 +22,14 @@ On documentation on creating addons, please see [ADDONS.md](ADDONS.md) 2. Download the APK from [here](https://github.com/7LPdWcaW/GrowTracker-Android/releases) 3. Click on downloaded app and install +# Updating + +You can either elect to update manually, or get notified on releases by installing the [Update plugin](https://github.com/7LPdWcaW/GrowUpdater-Android/releases) + **For updates, do not uninstall first, you will lose your existing plant data** +# Screenshots + [![install](screenshots/install-thumb.png)](screenshots/install.png) [![plant list](screenshots/1-thumb.png)](screenshots/1.png) [![discrete plant list](screenshots/1b-thumb.png)](screenshots/1b.png) @@ -40,6 +48,9 @@ On documentation on creating addons, please see [ADDONS.md](ADDONS.md) [![action options](screenshots/10-thumb.png)](screenshots/10.png) [![settings](screenshots/11-thumb.png)](screenshots/11.png) [![measurements](screenshots/12-thumb.png)](screenshots/12.png) +[![schedules](screenshots/13-thumb.png)](screenshots/13.png) +[![schedule details](screenshots/14-thumb.png)](screenshots/14.png) +[![schedule date](screenshots/15-thumb.png)](screenshots/15.png) # About the app @@ -47,12 +58,10 @@ The app uses a simple JSON structure to store all the data about the plants that The structure is very simple. Note: date timestamps are all unix timestamps from 1/1/1970 in milliseconds. All objects in arrays are in date order, where index 0 is the oldest and index (size - 1) is the newest. -## Prerequisites - -Lombok is required for this project before you are able to compile. You can install it by going to `preferences->plugins->browse repositories->lombok plugin` - ### Plant object +- Plant date in milliseconds + ``` { "id": , @@ -86,7 +95,9 @@ Temperature measured in ºC ### Action object (water) -Temperature measured in ºC +- Temperature measured in ºC +- Amount measured in ml +- Date is milliseconds Water action for waterings @@ -99,12 +110,14 @@ Water action for waterings "amount": , "date": 1431268453111, "type": "Water", - "temp": + "temp": } ``` ### Additive object - used for nutrients +- Amount is measured in ml + ``` { "description": , @@ -118,6 +131,8 @@ Action can be one of, `FIM`, `FLUSH`, `FOLIAR_FEED`, `LST`, `LOLLIPOP`, `PESTICIDE_APPLICATION`, `TOP`, `TRANSPLANTED`, `TRIM` +- Date in milliseconds + ``` { "action": , @@ -128,6 +143,8 @@ Action can be one of, ### Stage change +- Date in milliseconds + ``` { "newStage": , @@ -138,6 +155,8 @@ Action can be one of, ### Note +- Date in milliseconds + ``` { "notes": , @@ -158,7 +177,7 @@ You can decrypt your files using your passphrase either by writing a script that # License -Copyright 2014-2018 7LPdWcaW +Copyright 2014-2019 7LPdWcaW Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/build.gradle b/app/build.gradle index 28e6781d..8bf69b93 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,49 +1,59 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' repositories { - maven { url 'https://dl.bintray.com/kennyc1012/maven' } + maven { url 'https://dl.bintray.com/kennyc1012/maven' } + mavenCentral() } android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion 28 + buildToolsVersion "28.0.3" defaultConfig { applicationId "me.anon.grow" minSdkVersion 17 - targetSdkVersion 27 - versionCode 16 - versionName "2.3.2" + targetSdkVersion 28 + versionCode 19 + versionName "2.4" + + javaCompileOptions { + annotationProcessorOptions { + includeCompileClasspath false + } + } + + vectorDrawables.useSupportLibrary = true } lintOptions { abortOnError false } - flavorDimensions "type" + flavorDimensions "default" + productFlavors { production { + dimension "default" buildConfigField "Boolean", "DISCRETE", "false" manifestPlaceholders = [ - "appType": "original" + "appType": "original" ] resValue "string", "app_name", "GrowTracker" - - dimension "type" } discrete { + dimension "default" buildConfigField "Boolean", "DISCRETE", "true" manifestPlaceholders = [ - "appType": "discrete" + "appType": "discrete" ] resValue "string", "app_name", "Tracker" - - dimension "type" } } @@ -56,22 +66,27 @@ android { } dependencies { - annotationProcessor 'org.projectlombok:lombok:1.16.20' - compileOnly 'org.projectlombok:lombok:1.16.20' - compileOnly 'org.glassfish:javax.annotation:10.0-b28' - - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:recyclerview-v7:27.1.1' - implementation 'com.android.support:cardview-v7:27.1.1' - implementation 'com.android.support:design:27.1.1' - implementation "com.android.support:exifinterface:27.1.1" + implementation 'com.android.support:support-compat:28.0.0' + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.android.support:cardview-v7:28.0.0' + implementation 'com.android.support:design:28.0.0' + implementation "com.android.support:exifinterface:28.0.0" implementation 'com.esotericsoftware:kryo:3.0.3' - implementation 'com.google.code.gson:gson:2.8.1' + implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.squareup:otto:1.3.8' implementation 'com.kennyc:snackbar:2.0.2' - implementation 'com.github.PhilJay:MPAndroidChart:v2.1.0' + implementation 'com.github.PhilJay:MPAndroidChart:v2.1.6' implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.6.0' implementation 'net.lingala.zip4j:zip4j:1.3.2' + implementation 'com.google.android:flexbox:1.0.0' + + api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} + +androidExtensions { + experimental = true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1245a58d..3ba199b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,6 +61,9 @@ + + + diff --git a/app/src/main/java/me/anon/controller/adapter/ActionAdapter.java b/app/src/main/java/me/anon/controller/adapter/ActionAdapter.java index b0fd9bad..651bce10 100644 --- a/app/src/main/java/me/anon/controller/adapter/ActionAdapter.java +++ b/app/src/main/java/me/anon/controller/adapter/ActionAdapter.java @@ -12,36 +12,34 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import com.esotericsoftware.kryo.Kryo; +import java.io.File; import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; -import lombok.Getter; -import lombok.Setter; import me.anon.grow.R; import me.anon.lib.DateRenderer; import me.anon.lib.TempUnit; import me.anon.lib.Unit; import me.anon.lib.helper.TimeHelper; import me.anon.model.Action; -import me.anon.model.Additive; import me.anon.model.EmptyAction; import me.anon.model.NoteAction; import me.anon.model.Plant; import me.anon.model.StageChange; import me.anon.model.Water; import me.anon.view.ActionHolder; - -import static me.anon.lib.TempUnit.CELCIUS; -import static me.anon.lib.Unit.ML; +import me.anon.view.ImageActionHolder; /** * // TODO: Add class description @@ -50,7 +48,7 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -public class ActionAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter +public class ActionAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { public interface OnActionSelectListener { @@ -60,322 +58,405 @@ public interface OnActionSelectListener public void onActionDuplicate(Action action); } - @Setter private OnActionSelectListener onActionSelectListener; - @Getter private Plant plant; - @Getter private List actions = new ArrayList<>(); - @Getter private Unit measureUnit, deliveryUnit; - @Getter private TempUnit tempUnit; + private OnActionSelectListener onActionSelectListener; + private Plant plant; + private List actions = new ArrayList<>(); + private Unit measureUnit, deliveryUnit; + private TempUnit tempUnit; private boolean usingEc = false; - public void setActions(Plant plant, List actions) + /** + * Dummy image action placeholder class + */ + private static class ImageAction extends Action { - this.plant = plant; - this.actions = actions; + public ArrayList images = new ArrayList<>(); + + @Override public long getDate() + { + return getImageDate(images.get(0)); + } } - @Override public ActionHolder onCreateViewHolder(ViewGroup viewGroup, int i) + public void setOnActionSelectListener(OnActionSelectListener onActionSelectListener) { - return new ActionHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.action_item, viewGroup, false)); + this.onActionSelectListener = onActionSelectListener; } - @Override public void onBindViewHolder(final ActionHolder viewHolder, final int i) + public Plant getPlant() { - final Action action = actions.get(i); - - if (measureUnit == null) - { - measureUnit = Unit.getSelectedMeasurementUnit(viewHolder.itemView.getContext()); - } - - if (deliveryUnit == null) - { - deliveryUnit = Unit.getSelectedDeliveryUnit(viewHolder.itemView.getContext()); - } + return plant; + } - if (tempUnit == null) + public List getActions() + { + ArrayList actions = new ArrayList<>(); + for (Object item : this.actions) { - tempUnit = TempUnit.getSelectedTemperatureUnit(viewHolder.itemView.getContext()); + if (item.getClass() != ImageAction.class) actions.add((Action)item); } - usingEc = PreferenceManager.getDefaultSharedPreferences(viewHolder.itemView.getContext()).getBoolean("tds_ec", false); + return actions; + } - if (action == null) return; + public Unit getMeasureUnit() + { + return measureUnit; + } - DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(viewHolder.getDate().getContext()); - DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(viewHolder.getDate().getContext()); + public Unit getDeliveryUnit() + { + return deliveryUnit; + } - Date actionDate = new Date(action.getDate()); - Calendar actionCalendar = GregorianCalendar.getInstance(); - actionCalendar.setTime(actionDate); - String fullDateStr = dateFormat.format(actionDate) + " " + timeFormat.format(actionDate); - String dateStr = "" + new DateRenderer().timeAgo(action.getDate()).formattedDate + " ago"; + public TempUnit getTempUnit() + { + return tempUnit; + } - String dateDayStr = actionCalendar.get(Calendar.DAY_OF_MONTH) + " " + actionCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()); + public void setActions(Plant plant, List actions) + { + this.plant = plant; + this.actions = new ArrayList<>(); - if (viewHolder.getDateDay() != null) + ArrayList addedImages = new ArrayList<>(); + Collections.reverse(actions); + for (Action item : actions) { - String lastDateStr = ""; - - if (i - 1 >= 0) + ArrayList groupedImages = new ArrayList<>(); + for (String image : plant.getImages()) { - Date lastActionDate = new Date(actions.get(i - 1).getDate()); - Calendar lastActionCalendar = GregorianCalendar.getInstance(); - lastActionCalendar.setTime(lastActionDate); - lastDateStr = lastActionCalendar.get(Calendar.DAY_OF_MONTH) + " " + lastActionCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()); + long imageDate = getImageDate(image); + + if (imageDate <= item.getDate() && !addedImages.contains(image)) + { + groupedImages.add(image); + addedImages.add(image); + } } - if (!lastDateStr.equalsIgnoreCase(dateDayStr)) + if (!groupedImages.isEmpty()) { - viewHolder.getDateDay().setText(Html.fromHtml(dateDayStr)); - - String stageDay = ""; - StageChange current = null; - StageChange previous = null; - - for (int actionIndex = i; actionIndex < actions.size(); actionIndex++) + Collections.sort(groupedImages, new Comparator() { - if (actions.get(actionIndex) instanceof StageChange) + @Override public int compare(String o1, String o2) { - if (current == null) - { - current = (StageChange)actions.get(actionIndex); - } - else if (previous == null) - { - previous = (StageChange)actions.get(actionIndex); - } + long o1Date = getImageDate(o1); + long o2Date = getImageDate(o2); + + if (o2Date < o1Date) return -1; + if (o2Date > o1Date) return 1; + return 0; } - } + }); + ImageAction imageAction = new ImageAction(); + imageAction.images = groupedImages; + this.actions.add(imageAction); + } - int totalDays = (int)TimeHelper.toDays(Math.abs(action.getDate() - plant.getPlantDate())); - stageDay += totalDays; + this.actions.add(item); + } - if (previous == null) - { - previous = current; - } + if (addedImages.size() != plant.getImages().size()) + { + ArrayList remainingImages = new ArrayList<>(plant.getImages()); + remainingImages.removeAll(addedImages); - if (current != null) + Collections.sort(remainingImages, new Comparator() + { + @Override public int compare(String o1, String o2) { - if (action == current) - { - int currentDays = (int)TimeHelper.toDays(Math.abs(current.getDate() - previous.getDate())); - stageDay += "/" + currentDays + previous.getNewStage().getPrintString().substring(0, 1).toLowerCase(); - } - else - { - int currentDays = (int)TimeHelper.toDays(Math.abs(action.getDate() - current.getDate())); - stageDay += "/" + currentDays + current.getNewStage().getPrintString().substring(0, 1).toLowerCase(); - } + long o1Date = getImageDate(o1); + long o2Date = getImageDate(o2); + + if (o2Date < o1Date) return -1; + if (o2Date > o1Date) return 1; + return 0; } + }); - viewHolder.getStageDay().setText(stageDay); - } - else - { - viewHolder.getDateDay().setText(""); - viewHolder.getStageDay().setText(""); - } + ImageAction imageAction = new ImageAction(); + imageAction.images = remainingImages; + this.actions.add(imageAction); + } + + Collections.reverse(this.actions); + } + + private static long getImageDate(String image) + { + File currentImage = new File(image); + long fileDate = Long.parseLong(currentImage.getName().replaceAll("[^0-9]", "")); + + if (fileDate == 0) + { + fileDate = currentImage.lastModified(); } - if (i > 0) + return fileDate; + } + + @Override public int getItemViewType(int position) + { + if (actions.get(position).getClass() == ImageAction.class) { - long difference = actions.get(i - 1).getDate() - action.getDate(); - int days = (int)Math.round(((double)difference / 60d / 60d / 24d / 1000d)); + return 2; + } - dateStr += " (-" + days + "d)"; + return 1; + } + + @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) + { + if (viewType == 2) + { + return new ImageActionHolder(this, LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.action_image, viewGroup, false)); } - viewHolder.getFullDate().setText(Html.fromHtml(fullDateStr)); - viewHolder.getDate().setText(Html.fromHtml(dateStr)); - viewHolder.getSummary().setVisibility(View.GONE); + return new ActionHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.action_item, viewGroup, false)); + } - viewHolder.getCard().setCardBackgroundColor(0xffffffff); + @Override public void onBindViewHolder(final RecyclerView.ViewHolder vh, final int index) + { + final Action action = actions.get(index); + DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(vh.itemView.getContext()); + DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(vh.itemView.getContext()); + TextView dateDay = null; + TextView stageDay = null; - String summary = ""; - if (action.getClass() == Water.class) + if (vh instanceof ImageActionHolder) { - viewHolder.getCard().setCardBackgroundColor(0x9ABBDEFB); - viewHolder.getName().setText("Watered"); - StringBuilder waterStr = new StringBuilder(); + final ImageActionHolder viewHolder = (ImageActionHolder)vh; + viewHolder.bind(((ImageAction)action).images); + dateDay = viewHolder.getDateDay(); + stageDay = viewHolder.getStageDay(); + } + else if (vh instanceof ActionHolder) + { + final ActionHolder viewHolder = (ActionHolder)vh; - if (((Water)action).getPh() != null) + if (measureUnit == null) { - waterStr.append("In pH: "); - waterStr.append(((Water)action).getPh()); - waterStr.append(", "); + measureUnit = Unit.getSelectedMeasurementUnit(viewHolder.itemView.getContext()); } - if (((Water)action).getRunoff() != null) + if (deliveryUnit == null) { - waterStr.append("Out pH: "); - waterStr.append(((Water)action).getRunoff()); - waterStr.append(", "); + deliveryUnit = Unit.getSelectedDeliveryUnit(viewHolder.itemView.getContext()); } - summary += waterStr.toString().length() > 0 ? waterStr.toString().substring(0, waterStr.length() - 2) + "
" : ""; + if (tempUnit == null) + { + tempUnit = TempUnit.getSelectedTemperatureUnit(viewHolder.itemView.getContext()); + } + + usingEc = PreferenceManager.getDefaultSharedPreferences(viewHolder.itemView.getContext()).getBoolean("tds_ec", false); + + if (action == null) return; + + dateDay = viewHolder.getDateDay(); + stageDay = viewHolder.getStageDay(); - waterStr = new StringBuilder(); + Date actionDate = new Date(action.getDate()); + Calendar actionCalendar = GregorianCalendar.getInstance(); + actionCalendar.setTime(actionDate); + String fullDateStr = dateFormat.format(actionDate) + " " + timeFormat.format(actionDate); + String dateStr = "" + new DateRenderer().timeAgo(action.getDate()).formattedDate + " ago"; - if (((Water)action).getPpm() != null) + if (index > 0) { - String ppm = String.valueOf(((Water)action).getPpm().longValue()); - if (usingEc) - { - waterStr.append("EC: "); - ppm = String.valueOf((((Water)action).getPpm() * 2d) / 1000d); - } - else - { - waterStr.append("PPM: "); - } + long difference = actions.get(index - 1).getDate() - action.getDate(); + int days = (int)Math.round(((double)difference / 60d / 60d / 24d / 1000d)); - waterStr.append(ppm); - waterStr.append(", "); + dateStr += " (-" + days + "d)"; } - if (((Water)action).getAmount() != null) + viewHolder.getFullDate().setText(Html.fromHtml(fullDateStr)); + viewHolder.getDate().setText(Html.fromHtml(dateStr)); + viewHolder.getSummary().setVisibility(View.GONE); + viewHolder.getCard().setCardBackgroundColor(0xffffffff); + + String summary = ""; + if (action.getClass() == Water.class) { - waterStr.append("Amount: "); - waterStr.append(ML.to(deliveryUnit, ((Water)action).getAmount())); - waterStr.append(deliveryUnit.getLabel()); - waterStr.append(", "); + summary += ((Water)action).getSummary(viewHolder.itemView.getContext()); + viewHolder.getCard().setCardBackgroundColor(0x9ABBDEFB); + viewHolder.getName().setText("Watered"); } - - if (((Water)action).getTemp() != null) + else if (action instanceof EmptyAction && ((EmptyAction)action).getAction() != null) + { + viewHolder.getName().setText(((EmptyAction)action).getAction().getPrintString()); + viewHolder.getCard().setCardBackgroundColor(((EmptyAction)action).getAction().getColour()); + } + else if (action instanceof NoteAction) { - waterStr.append("Temp: "); - waterStr.append(CELCIUS.to(tempUnit, ((Water)action).getTemp())); - waterStr.append("º").append(tempUnit.getLabel()).append(", "); + viewHolder.getName().setText("Note"); + viewHolder.getCard().setCardBackgroundColor(0xffffffff); + } + else if (action instanceof StageChange) + { + viewHolder.getName().setText(((StageChange)action).getNewStage().getPrintString()); + viewHolder.getCard().setCardBackgroundColor(0x9AB39DDB); } - summary += waterStr.toString().length() > 0 ? waterStr.toString().substring(0, waterStr.length() - 2) + "
" : ""; + if (!TextUtils.isEmpty(action.getNotes())) + { + summary += summary.length() > 0 ? "

" : ""; + summary += action.getNotes(); + } - waterStr = new StringBuilder(); + if (summary.endsWith("
")) + { + summary = summary.substring(0, summary.length() - "
".length()); + } - if (((Water)action).getAdditives().size() > 0) + if (!TextUtils.isEmpty(summary)) { - waterStr.append("Additives:"); + viewHolder.getSummary().setText(Html.fromHtml(summary)); + viewHolder.getSummary().setVisibility(View.VISIBLE); + } - for (Additive additive : ((Water)action).getAdditives()) + viewHolder.getOverflow().setOnClickListener(new View.OnClickListener() + { + @Override public void onClick(final View v) { - if (additive == null || additive.getAmount() == null) continue; - - double converted = ML.to(measureUnit, additive.getAmount()); - String amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); - - waterStr.append("
    • "); - waterStr.append(additive.getDescription()); - waterStr.append(" - "); - waterStr.append(amountStr); - waterStr.append(measureUnit.getLabel()); - waterStr.append("/"); - waterStr.append(deliveryUnit.getLabel()); - } - } + PopupMenu menu = new PopupMenu(v.getContext(), v, Gravity.BOTTOM); + menu.inflate(R.menu.event_overflow); + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() + { + @Override public boolean onMenuItemClick(MenuItem item) + { + if (item.getItemId() == R.id.duplicate) + { + if (onActionSelectListener != null) + { + Kryo kryo = new Kryo(); + onActionSelectListener.onActionDuplicate(kryo.copy(action)); + } - summary += waterStr.toString(); - } - else if (action instanceof EmptyAction && ((EmptyAction)action).getAction() != null) - { - viewHolder.getName().setText(((EmptyAction)action).getAction().getPrintString()); - viewHolder.getCard().setCardBackgroundColor(((EmptyAction)action).getAction().getColour()); - } - else if (action instanceof NoteAction) - { - viewHolder.getName().setText("Note"); - viewHolder.getCard().setCardBackgroundColor(0xffffffff); - } - else if (action instanceof StageChange) - { - viewHolder.getName().setText(((StageChange)action).getNewStage().getPrintString()); - viewHolder.getCard().setCardBackgroundColor(0x9AB39DDB); - } + return true; + } + else if (item.getItemId() == R.id.copy) + { + if (onActionSelectListener != null) + { + Kryo kryo = new Kryo(); + onActionSelectListener.onActionCopy(kryo.copy(action)); + } - if (!TextUtils.isEmpty(action.getNotes())) - { - summary += summary.length() > 0 ? "

" : ""; - summary += action.getNotes(); - } + return true; + } + else if (item.getItemId() == R.id.edit) + { + if (onActionSelectListener != null) + { + onActionSelectListener.onActionEdit(action); + } - if (summary.endsWith("
")) - { - summary = summary.substring(0, summary.length() - "
".length()); - } + return true; + } + else if (item.getItemId() == R.id.delete) + { + new AlertDialog.Builder(v.getContext()) + .setTitle("Delete this event?") + .setMessage("Are you sure you want to delete " + viewHolder.getName().getText()) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() + { + @Override public void onClick(DialogInterface dialog, int which) + { + if (onActionSelectListener != null) + { + onActionSelectListener.onActionDeleted(action); + } + } + }) + .setNegativeButton("No", null) + .show(); - if (!TextUtils.isEmpty(summary)) - { - viewHolder.getSummary().setText(Html.fromHtml(summary)); - viewHolder.getSummary().setVisibility(View.VISIBLE); + return true; + } + + return false; + } + }); + + menu.show(); + } + }); } - viewHolder.getOverflow().setOnClickListener(new View.OnClickListener() + // plant date & stage + Date actionDate = new Date(action.getDate()); + Calendar actionCalendar = GregorianCalendar.getInstance(); + actionCalendar.setTime(actionDate); + + String dateDayStr = actionCalendar.get(Calendar.DAY_OF_MONTH) + " " + actionCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()); + + if (dateDay != null) { - @Override public void onClick(final View v) + String lastDateStr = ""; + + if (index - 1 >= 0) { - PopupMenu menu = new PopupMenu(v.getContext(), v, Gravity.BOTTOM); - menu.inflate(R.menu.event_overflow); - menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() + Date lastActionDate = new Date(actions.get(index - 1).getDate()); + Calendar lastActionCalendar = GregorianCalendar.getInstance(); + lastActionCalendar.setTime(lastActionDate); + lastDateStr = lastActionCalendar.get(Calendar.DAY_OF_MONTH) + " " + lastActionCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()); + } + + if (!lastDateStr.equalsIgnoreCase(dateDayStr)) + { + dateDay.setText(Html.fromHtml(dateDayStr)); + + String stageDayStr = ""; + StageChange current = null; + StageChange previous = null; + + for (int actionIndex = index; actionIndex < actions.size(); actionIndex++) { - @Override public boolean onMenuItemClick(MenuItem item) + if (actions.get(actionIndex) instanceof StageChange) { - if (item.getItemId() == R.id.duplicate) + if (current == null) { - if (onActionSelectListener != null) - { - Kryo kryo = new Kryo(); - onActionSelectListener.onActionDuplicate(kryo.copy(action)); - } - - return true; + current = (StageChange)actions.get(actionIndex); } - else if (item.getItemId() == R.id.copy) + else if (previous == null) { - if (onActionSelectListener != null) - { - Kryo kryo = new Kryo(); - onActionSelectListener.onActionCopy(kryo.copy(action)); - } - - return true; + previous = (StageChange)actions.get(actionIndex); } - else if (item.getItemId() == R.id.edit) - { - if (onActionSelectListener != null) - { - onActionSelectListener.onActionEdit(action); - } + } + } - return true; - } - else if (item.getItemId() == R.id.delete) - { - new AlertDialog.Builder(v.getContext()) - .setTitle("Delete this event?") - .setMessage("Are you sure you want to delete " + viewHolder.getName().getText()) - .setPositiveButton("Yes", new DialogInterface.OnClickListener() - { - @Override public void onClick(DialogInterface dialog, int which) - { - if (onActionSelectListener != null) - { - onActionSelectListener.onActionDeleted(action); - } - } - }) - .setNegativeButton("No", null) - .show(); + int totalDays = (int)TimeHelper.toDays(Math.abs(action.getDate() - plant.getPlantDate())); + stageDayStr += totalDays; - return true; - } + if (previous == null) + { + previous = current; + } - return false; + if (current != null) + { + if (action == current) + { + int currentDays = (int)TimeHelper.toDays(Math.abs(current.getDate() - previous.getDate())); + stageDayStr += "/" + currentDays + previous.getNewStage().getPrintString().substring(0, 1).toLowerCase(); } - }); + else + { + int currentDays = (int)TimeHelper.toDays(Math.abs(action.getDate() - current.getDate())); + stageDayStr += "/" + currentDays + current.getNewStage().getPrintString().substring(0, 1).toLowerCase(); + } + } - menu.show(); + stageDay.setText(stageDayStr); + } + else + { + dateDay.setText(""); + stageDay.setText(""); } - }); + } } @Override public int getItemCount() diff --git a/app/src/main/java/me/anon/controller/adapter/FeedingDateAdapter.kt b/app/src/main/java/me/anon/controller/adapter/FeedingDateAdapter.kt new file mode 100644 index 00000000..6939e94a --- /dev/null +++ b/app/src/main/java/me/anon/controller/adapter/FeedingDateAdapter.kt @@ -0,0 +1,40 @@ +package me.anon.controller.adapter + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.ViewGroup +import me.anon.grow.R +import me.anon.model.FeedingScheduleDate +import me.anon.model.Plant +import me.anon.model.PlantStage +import me.anon.view.FeedingDateHolder +import java.util.* + +/** + * // TODO: Add class description + */ +class FeedingDateAdapter : RecyclerView.Adapter() +{ + public var onItemSelectCallback: (date: FeedingScheduleDate) -> Unit = {} + public var items: ArrayList = arrayListOf() + set(value) + { + items.clear() + items.addAll(value) + notifyDataSetChanged() + } + public var plant: Plant = Plant() + public val plantStages: SortedMap by lazy { plant.calculateStageTime() } + + public fun getLastStage(): PlantStage = plantStages.toSortedMap().lastKey() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedingDateHolder + = FeedingDateHolder(this, LayoutInflater.from(parent.context).inflate(R.layout.feeding_date_stub, parent, false)) + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: FeedingDateHolder, position: Int) + { + holder.bind(items[position]) + } +} diff --git a/app/src/main/java/me/anon/controller/adapter/FeedingScheduleAdapter.kt b/app/src/main/java/me/anon/controller/adapter/FeedingScheduleAdapter.kt new file mode 100644 index 00000000..01ef1c72 --- /dev/null +++ b/app/src/main/java/me/anon/controller/adapter/FeedingScheduleAdapter.kt @@ -0,0 +1,34 @@ +package me.anon.controller.adapter + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.ViewGroup +import me.anon.grow.R +import me.anon.model.FeedingSchedule +import me.anon.view.ScheduleHolder + +/** + * Adapter for feeding schedule list + */ +class FeedingScheduleAdapter : RecyclerView.Adapter() +{ + public var onDeleteCallback: (schedule: FeedingSchedule) -> Unit = {} + public var onCopyCallback: (schedule: FeedingSchedule) -> Unit = {} + public var items: ArrayList = arrayListOf() + set(value) + { + items.clear() + items.addAll(value) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduleHolder + = ScheduleHolder(this, LayoutInflater.from(parent.context).inflate(R.layout.schedule_item, parent, false)) + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: ScheduleHolder, position: Int) + { + holder.bind(items[position]) + } +} diff --git a/app/src/main/java/me/anon/controller/adapter/ImageAdapter.java b/app/src/main/java/me/anon/controller/adapter/ImageAdapter.java index 0cb5246c..be990c7b 100644 --- a/app/src/main/java/me/anon/controller/adapter/ImageAdapter.java +++ b/app/src/main/java/me/anon/controller/adapter/ImageAdapter.java @@ -14,8 +14,6 @@ import java.util.Collections; import java.util.List; -import lombok.Getter; -import lombok.Setter; import me.anon.grow.MainApplication; import me.anon.grow.R; import me.anon.grow.fragment.ImageLightboxDialog; @@ -30,11 +28,26 @@ */ public class ImageAdapter extends RecyclerView.Adapter { - @Getter private List images = new ArrayList<>(); - @Getter private List selected = new ArrayList<>(); - @Setter private View.OnLongClickListener onLongClickListener; + private List images = new ArrayList<>(); + private List selected = new ArrayList<>(); + private View.OnLongClickListener onLongClickListener; private boolean inActionMode = false; + public List getImages() + { + return images; + } + + public List getSelected() + { + return selected; + } + + public void setOnLongClickListener(View.OnLongClickListener onLongClickListener) + { + this.onLongClickListener = onLongClickListener; + } + public void setImages(List images) { this.images.clear(); diff --git a/app/src/main/java/me/anon/controller/adapter/PlantAdapter.java b/app/src/main/java/me/anon/controller/adapter/PlantAdapter.java index 418dd9ac..f8e0866c 100644 --- a/app/src/main/java/me/anon/controller/adapter/PlantAdapter.java +++ b/app/src/main/java/me/anon/controller/adapter/PlantAdapter.java @@ -16,8 +16,6 @@ import java.util.Collections; import java.util.List; -import lombok.Getter; -import lombok.Setter; import me.anon.grow.MainApplication; import me.anon.grow.PlantDetailsActivity; import me.anon.grow.R; @@ -35,10 +33,35 @@ */ public class PlantAdapter extends RecyclerView.Adapter implements ItemTouchHelperAdapter { - @Getter private List plants = new ArrayList<>(); - @Getter @Setter private List showOnly = null; + private List plants = new ArrayList<>(); + private List showOnly = null; private Context context; - @Getter private Unit measureUnit, deliveryUnit; + private Unit measureUnit, deliveryUnit; + + public void setShowOnly(List showOnly) + { + this.showOnly = showOnly; + } + + public List getPlants() + { + return plants; + } + + public List getShowOnly() + { + return showOnly; + } + + public Unit getMeasureUnit() + { + return measureUnit; + } + + public Unit getDeliveryUnit() + { + return deliveryUnit; + } public PlantAdapter(Context context) { diff --git a/app/src/main/java/me/anon/controller/adapter/PlantSelectionAdapter.java b/app/src/main/java/me/anon/controller/adapter/PlantSelectionAdapter.java index e0641e19..1633136e 100644 --- a/app/src/main/java/me/anon/controller/adapter/PlantSelectionAdapter.java +++ b/app/src/main/java/me/anon/controller/adapter/PlantSelectionAdapter.java @@ -14,7 +14,6 @@ import java.util.ArrayList; -import lombok.Getter; import me.anon.grow.MainApplication; import me.anon.grow.R; import me.anon.model.Plant; @@ -22,10 +21,20 @@ public class PlantSelectionAdapter extends RecyclerView.Adapter { - @Getter private ArrayList plants = new ArrayList<>(); - @Getter private ArrayList selectedIds = new ArrayList<>(); + private ArrayList plants = new ArrayList<>(); + private ArrayList selectedIds = new ArrayList<>(); private Context context; + public ArrayList getPlants() + { + return plants; + } + + public ArrayList getSelectedIds() + { + return selectedIds; + } + public PlantSelectionAdapter(@Nullable ArrayList plants, @Nullable ArrayList selectedIds, Context context) { this.plants = plants; diff --git a/app/src/main/java/me/anon/controller/receiver/BackupService.java b/app/src/main/java/me/anon/controller/receiver/BackupService.java index 904569d8..3467549b 100644 --- a/app/src/main/java/me/anon/controller/receiver/BackupService.java +++ b/app/src/main/java/me/anon/controller/receiver/BackupService.java @@ -3,13 +3,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.os.Environment; -import java.io.File; - -import me.anon.lib.manager.FileManager; - -import static me.anon.lib.manager.PlantManager.FILES_DIR; +import me.anon.lib.helper.BackupHelper; /** * // TODO: Add class description @@ -18,7 +13,6 @@ public class BackupService extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - new File(Environment.getExternalStorageDirectory(), "/backups/GrowTracker/").mkdirs(); - FileManager.getInstance().copyFile(FILES_DIR + "/plants.json", Environment.getExternalStorageDirectory().getAbsolutePath() + "/backups/GrowTracker/" + System.currentTimeMillis() + ".bak"); + BackupHelper.backupJson(); } } diff --git a/app/src/main/java/me/anon/grow/AddPlantActivity.java b/app/src/main/java/me/anon/grow/AddPlantActivity.java index d6147bc5..5f8a6add 100644 --- a/app/src/main/java/me/anon/grow/AddPlantActivity.java +++ b/app/src/main/java/me/anon/grow/AddPlantActivity.java @@ -5,7 +5,6 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; -import lombok.experimental.Accessors; import me.anon.grow.fragment.PlantDetailsFragment; import me.anon.lib.Views; @@ -17,7 +16,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class AddPlantActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/AddWateringActivity.java b/app/src/main/java/me/anon/grow/AddWateringActivity.java index 2e2a1d4f..67546cd3 100644 --- a/app/src/main/java/me/anon/grow/AddWateringActivity.java +++ b/app/src/main/java/me/anon/grow/AddWateringActivity.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.support.v7.widget.Toolbar; -import lombok.experimental.Accessors; import me.anon.grow.fragment.WateringFragment; import me.anon.lib.Views; @@ -15,7 +14,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class AddWateringActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/EditWateringActivity.java b/app/src/main/java/me/anon/grow/EditWateringActivity.java index 5f57409d..e0c4ce5c 100644 --- a/app/src/main/java/me/anon/grow/EditWateringActivity.java +++ b/app/src/main/java/me/anon/grow/EditWateringActivity.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.support.v7.widget.Toolbar; -import lombok.experimental.Accessors; import me.anon.grow.fragment.WateringFragment; import me.anon.lib.Views; @@ -15,7 +14,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class EditWateringActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/EventsActivity.java b/app/src/main/java/me/anon/grow/EventsActivity.java index 412d1405..48e24bd5 100644 --- a/app/src/main/java/me/anon/grow/EventsActivity.java +++ b/app/src/main/java/me/anon/grow/EventsActivity.java @@ -1,10 +1,8 @@ package me.anon.grow; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import lombok.experimental.Accessors; import me.anon.grow.fragment.EventListFragment; import me.anon.lib.Views; @@ -16,7 +14,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class EventsActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/FeedingScheduleActivity.kt b/app/src/main/java/me/anon/grow/FeedingScheduleActivity.kt new file mode 100644 index 00000000..b47449ea --- /dev/null +++ b/app/src/main/java/me/anon/grow/FeedingScheduleActivity.kt @@ -0,0 +1,31 @@ +package me.anon.grow + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import kotlinx.android.synthetic.main.fragment_holder.* +import me.anon.grow.fragment.FeedingScheduleListFragment + +/** + * Activity holder for feeding schedule list + */ +class FeedingScheduleActivity : AppCompatActivity() +{ + companion object + { + public const val TAG_FRAGMENT = "fragment" + } + + override fun onCreate(savedInstanceState: Bundle?) + { + super.onCreate(savedInstanceState) + + setContentView(R.layout.fragment_holder) + setSupportActionBar(toolbar) + setTitle(R.string.feeding_schedules_title) + + if (fragmentManager.findFragmentByTag(TAG_FRAGMENT) == null) + { + fragmentManager.beginTransaction().replace(R.id.fragment_holder, FeedingScheduleListFragment(), TAG_FRAGMENT).commit() + } + } +} diff --git a/app/src/main/java/me/anon/grow/FeedingScheduleDetailsActivity.kt b/app/src/main/java/me/anon/grow/FeedingScheduleDetailsActivity.kt new file mode 100644 index 00000000..11a862e4 --- /dev/null +++ b/app/src/main/java/me/anon/grow/FeedingScheduleDetailsActivity.kt @@ -0,0 +1,39 @@ +package me.anon.grow + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import kotlinx.android.synthetic.main.fragment_holder.* +import me.anon.grow.fragment.FeedingScheduleDetailsFragment + +/** + * Activity holder for feeding schedule list + */ +class FeedingScheduleDetailsActivity : AppCompatActivity() +{ + companion object + { + public const val TAG_FRAGMENT = "fragment" + } + + override fun onCreate(savedInstanceState: Bundle?) + { + super.onCreate(savedInstanceState) + + setContentView(R.layout.fragment_holder) + setSupportActionBar(toolbar) + setTitle(R.string.schedule_details_title) + + if (fragmentManager.findFragmentByTag(TAG_FRAGMENT) == null) + { + fragmentManager.beginTransaction().replace(R.id.fragment_holder, FeedingScheduleDetailsFragment.newInstance(intent.extras?.getInt("schedule_index", -1) ?: -1), TAG_FRAGMENT).commit() + } + } + + override fun onBackPressed() + { + if ((fragmentManager.findFragmentByTag(TAG_FRAGMENT) as FeedingScheduleDetailsFragment).onBackPressed()) + { + super.onBackPressed() + } + } +} diff --git a/app/src/main/java/me/anon/grow/MainActivity.java b/app/src/main/java/me/anon/grow/MainActivity.java index 11caab3f..95fc33d2 100644 --- a/app/src/main/java/me/anon/grow/MainActivity.java +++ b/app/src/main/java/me/anon/grow/MainActivity.java @@ -21,8 +21,6 @@ import java.util.ArrayList; -import lombok.Getter; -import lombok.experimental.Accessors; import me.anon.grow.fragment.GardenDialogFragment; import me.anon.grow.fragment.PlantListFragment; import me.anon.lib.Views; @@ -40,16 +38,20 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener { private static final String TAG_FRAGMENT = "current_fragment"; @Views.InjectView(R.id.toolbar) private Toolbar toolbar; @Nullable @Views.InjectView(R.id.drawer_layout) private DrawerLayout drawer; - @Getter @Views.InjectView(R.id.navigation_view) private NavigationView navigation; + @Views.InjectView(R.id.navigation_view) private NavigationView navigation; private int selectedItem = 0; + public NavigationView getNavigation() + { + return navigation; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -117,7 +119,7 @@ public void setNavigationView() for (int index = 0, gardensSize = gardens.size(); index < gardensSize; index++) { Garden garden = gardens.get(index); - navigation.getMenu().add(R.id.gardens, 100 + index, 1, garden.getName()).setCheckable(true); + navigation.getMenu().findItem(R.id.garden_menu).getSubMenu().add(R.id.garden_menu, 100 + index, 1, garden.getName()).setCheckable(true); } MenuItem item = navigation.getMenu().findItem(selectedItem); @@ -211,6 +213,13 @@ else if (item.getItemId() == R.id.add) GardenManager.getInstance().getGardens().set(index, garden); } + MenuItem item = navigation.getMenu().findItem(selectedItem); + + if (item != null) + { + item.setChecked(false); + } + selectedItem = 100 + index; setNavigationView(); @@ -234,6 +243,11 @@ else if (item.getItemId() == R.id.settings) Intent settings = new Intent(this, SettingsActivity.class); startActivity(settings); } + else if (item.getItemId() == R.id.feeding_schedule) + { + Intent schedule = new Intent(this, FeedingScheduleActivity.class); + startActivity(schedule); + } else if (item.getItemId() == R.id.all) { selectedItem = item.getItemId(); @@ -241,7 +255,16 @@ else if (item.getItemId() == R.id.all) } else if (item.getItemId() >= 100 && item.getItemId() < Integer.MAX_VALUE) { + navigation.getMenu().findItem(R.id.garden_menu).getSubMenu().findItem(R.id.all).setChecked(false); + + MenuItem selected = navigation.getMenu().findItem(selectedItem); + if (selected != null) + { + selected.setChecked(false); + } + selectedItem = item.getItemId(); + item.setChecked(true); int gardenIndex = item.getItemId() - 100; getFragmentManager().beginTransaction().replace(R.id.fragment_holder, PlantListFragment.newInstance(GardenManager.getInstance().getGardens().get(gardenIndex)), TAG_FRAGMENT).commit(); } diff --git a/app/src/main/java/me/anon/grow/MainApplication.java b/app/src/main/java/me/anon/grow/MainApplication.java index 4c20a274..08cc957a 100644 --- a/app/src/main/java/me/anon/grow/MainApplication.java +++ b/app/src/main/java/me/anon/grow/MainApplication.java @@ -23,12 +23,11 @@ import java.net.URISyntaxException; import java.util.concurrent.TimeUnit; -import lombok.Getter; -import lombok.Setter; import me.anon.controller.receiver.BackupService; import me.anon.lib.handler.ExceptionHandler; import me.anon.lib.manager.GardenManager; import me.anon.lib.manager.PlantManager; +import me.anon.lib.manager.ScheduleManager; import me.anon.lib.stream.DecryptInputStream; /** @@ -40,11 +39,56 @@ */ public class MainApplication extends Application { - @Getter private static DisplayImageOptions displayImageOptions; - @Getter @Setter private static boolean encrypted = false; - @Getter @Setter private static String key = ""; - @Getter @Setter private static boolean failsafe = false; - @Getter @Setter private static boolean isTablet = false; + private static DisplayImageOptions displayImageOptions; + private static boolean encrypted = false; + private static String key = ""; + private static boolean failsafe = false; + private static boolean isTablet = false; + + public static void setEncrypted(boolean encrypted) + { + MainApplication.encrypted = encrypted; + } + + public static void setKey(String key) + { + MainApplication.key = key; + } + + public static void setFailsafe(boolean failsafe) + { + MainApplication.failsafe = failsafe; + } + + public static void setIsTablet(boolean isTablet) + { + MainApplication.isTablet = isTablet; + } + + public static DisplayImageOptions getDisplayImageOptions() + { + return displayImageOptions; + } + + public static boolean isEncrypted() + { + return encrypted; + } + + public static String getKey() + { + return key; + } + + public static boolean isFailsafe() + { + return failsafe; + } + + public static boolean isTablet() + { + return isTablet; + } @Override public void onCreate() { @@ -57,6 +101,7 @@ public class MainApplication extends Application PlantManager.getInstance().initialise(this); GardenManager.getInstance().initialise(this); + ScheduleManager.instance.initialise(this); registerBackupService(); displayImageOptions = new DisplayImageOptions.Builder() diff --git a/app/src/main/java/me/anon/grow/PlantDetailsActivity.java b/app/src/main/java/me/anon/grow/PlantDetailsActivity.java index 4ab9857a..09855423 100644 --- a/app/src/main/java/me/anon/grow/PlantDetailsActivity.java +++ b/app/src/main/java/me/anon/grow/PlantDetailsActivity.java @@ -5,7 +5,6 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; -import lombok.experimental.Accessors; import me.anon.grow.fragment.PlantDetailsFragment; import me.anon.lib.Views; @@ -17,7 +16,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class PlantDetailsActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/ScheduleDateDetailsActivity.kt b/app/src/main/java/me/anon/grow/ScheduleDateDetailsActivity.kt new file mode 100644 index 00000000..eb88ea6f --- /dev/null +++ b/app/src/main/java/me/anon/grow/ScheduleDateDetailsActivity.kt @@ -0,0 +1,36 @@ +package me.anon.grow + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import kotlinx.android.synthetic.main.fragment_holder.* +import me.anon.grow.fragment.ScheduleDateDetailsFragment + +/** + * Activity holder for feeding schedule list + */ +class ScheduleDateDetailsActivity : AppCompatActivity() +{ + companion object + { + public const val TAG_FRAGMENT = "fragment" + } + + override fun onCreate(savedInstanceState: Bundle?) + { + super.onCreate(savedInstanceState) + + setContentView(R.layout.fragment_holder) + setSupportActionBar(toolbar) + + if (fragmentManager.findFragmentByTag(TAG_FRAGMENT) == null) + { + fragmentManager.beginTransaction().replace( + R.id.fragment_holder, + ScheduleDateDetailsFragment.newInstance( + intent.extras?.getInt("schedule_index", -1) ?: -1, + intent.extras?.getInt("date_index", -1) ?: -1), + TAG_FRAGMENT + ).commit() + } + } +} diff --git a/app/src/main/java/me/anon/grow/SettingsActivity.java b/app/src/main/java/me/anon/grow/SettingsActivity.java index 774b60fe..fa5ff674 100644 --- a/app/src/main/java/me/anon/grow/SettingsActivity.java +++ b/app/src/main/java/me/anon/grow/SettingsActivity.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.support.v7.widget.Toolbar; -import lombok.experimental.Accessors; import me.anon.grow.fragment.SettingsFragment; import me.anon.lib.Views; @@ -14,7 +13,6 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Accessors(prefix = {"m", ""}, chain = true) public class SettingsActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/StatisticsActivity.java b/app/src/main/java/me/anon/grow/StatisticsActivity.java index 842f08b8..bfdf9f5f 100644 --- a/app/src/main/java/me/anon/grow/StatisticsActivity.java +++ b/app/src/main/java/me/anon/grow/StatisticsActivity.java @@ -1,10 +1,8 @@ package me.anon.grow; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import lombok.experimental.Accessors; import me.anon.grow.fragment.StatisticsFragment; import me.anon.lib.Views; @@ -16,7 +14,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class StatisticsActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/ViewPhotosActivity.java b/app/src/main/java/me/anon/grow/ViewPhotosActivity.java index deb17913..a920cd62 100644 --- a/app/src/main/java/me/anon/grow/ViewPhotosActivity.java +++ b/app/src/main/java/me/anon/grow/ViewPhotosActivity.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.support.v7.widget.Toolbar; -import lombok.experimental.Accessors; import me.anon.grow.fragment.ViewPhotosFragment; import me.anon.lib.Views; @@ -15,7 +14,6 @@ * @project GrowTracker */ @Views.Injectable -@Accessors(prefix = {"m", ""}, chain = true) public class ViewPhotosActivity extends BaseActivity { private static final String TAG_FRAGMENT = "current_fragment"; diff --git a/app/src/main/java/me/anon/grow/fragment/ActionDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/ActionDialogFragment.java index 90b6c106..e50e3be2 100644 --- a/app/src/main/java/me/anon/grow/fragment/ActionDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/ActionDialogFragment.java @@ -19,7 +19,6 @@ import java.util.Calendar; import java.util.Date; -import lombok.Setter; import me.anon.grow.R; import me.anon.lib.Views; import me.anon.model.Action; @@ -37,7 +36,12 @@ public static interface OnActionSelected @Views.InjectView(R.id.notes) private EditText notes; @Views.InjectView(R.id.date) private TextView date; - @Setter private OnActionSelected onActionSelected; + private OnActionSelected onActionSelected; + + public void setOnActionSelected(OnActionSelected onActionSelected) + { + this.onActionSelected = onActionSelected; + } private EmptyAction action; private boolean edit = false; diff --git a/app/src/main/java/me/anon/grow/fragment/AddAdditiveDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/AddAdditiveDialogFragment.java index 7ed658bc..cac90334 100644 --- a/app/src/main/java/me/anon/grow/fragment/AddAdditiveDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/AddAdditiveDialogFragment.java @@ -4,13 +4,15 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; +import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.text.TextUtils; import android.view.View; +import android.view.Window; +import android.view.inputmethod.InputMethodManager; import android.widget.TextView; -import lombok.Setter; import me.anon.grow.R; import me.anon.lib.Unit; import me.anon.lib.Views; @@ -35,7 +37,12 @@ public interface OnAdditiveSelectedListener private Additive additive; @Views.InjectView(R.id.description) private TextView description; @Views.InjectView(R.id.amount) private TextView amount; - @Setter private OnAdditiveSelectedListener onAdditiveSelectedListener; + private OnAdditiveSelectedListener onAdditiveSelectedListener; + + public void setOnAdditiveSelectedListener(OnAdditiveSelectedListener onAdditiveSelectedListener) + { + this.onAdditiveSelectedListener = onAdditiveSelectedListener; + } @SuppressLint("ValidFragment") public AddAdditiveDialogFragment(Additive additive) @@ -88,10 +95,19 @@ public void onClick(DialogInterface dialog, int whichButton) } }).create(); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialogInterface) { + if (additive == null) + { + InputMethodManager systemService = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + systemService.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + description.requestFocus(); + } + dialog.getButton(Dialog.BUTTON_NEUTRAL).setVisibility(additive == null ? View.GONE : View.VISIBLE); dialog.getButton(Dialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { diff --git a/app/src/main/java/me/anon/grow/fragment/DateDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/DateDialogFragment.java index 83129a32..a7c87f90 100644 --- a/app/src/main/java/me/anon/grow/fragment/DateDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/DateDialogFragment.java @@ -11,8 +11,6 @@ import java.util.Calendar; -import lombok.Setter; - public class DateDialogFragment extends Fragment { private long time; @@ -23,7 +21,12 @@ public static interface OnDateSelectedListener public void onCancelled(); } - @Setter private OnDateSelectedListener onDateSelected; + private OnDateSelectedListener onDateSelected; + + public void setOnDateSelected(OnDateSelectedListener onDateSelected) + { + this.onDateSelected = onDateSelected; + } @SuppressLint("ValidFragment") public DateDialogFragment(){} diff --git a/app/src/main/java/me/anon/grow/fragment/EventListFragment.java b/app/src/main/java/me/anon/grow/fragment/EventListFragment.java index 206ba431..9e10ce2b 100644 --- a/app/src/main/java/me/anon/grow/fragment/EventListFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/EventListFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -41,6 +42,7 @@ import me.anon.model.Plant; import me.anon.model.StageChange; import me.anon.model.Water; +import me.anon.view.ActionHolder; /** * // TODO: Add class description @@ -127,6 +129,11 @@ public static EventListFragment newInstance(int plantIndex) ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(adapter) { + @Override public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder current, @NonNull RecyclerView.ViewHolder target) + { + return current instanceof ActionHolder && target instanceof ActionHolder; + } + @Override public boolean isLongPressDragEnabled() { return selected.size() == Action.ActionName.values().length && watering && notes && stages; diff --git a/app/src/main/java/me/anon/grow/fragment/FeedingScheduleDetailsFragment.kt b/app/src/main/java/me/anon/grow/fragment/FeedingScheduleDetailsFragment.kt new file mode 100644 index 00000000..c7f8928c --- /dev/null +++ b/app/src/main/java/me/anon/grow/fragment/FeedingScheduleDetailsFragment.kt @@ -0,0 +1,207 @@ +package me.anon.grow.fragment + +import android.app.AlertDialog +import android.app.Fragment +import android.content.Intent +import android.os.Bundle +import android.text.Html +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.esotericsoftware.kryo.Kryo +import com.kenny.snackbar.SnackBar +import kotlinx.android.synthetic.main.feeding_date_stub.view.* +import kotlinx.android.synthetic.main.schedule_details_view.* +import me.anon.grow.R +import me.anon.grow.ScheduleDateDetailsActivity +import me.anon.lib.Unit +import me.anon.lib.helper.FabAnimator +import me.anon.lib.manager.ScheduleManager +import me.anon.lib.show +import me.anon.model.FeedingSchedule +import me.anon.model.FeedingScheduleDate + +/** + * // TODO: Add class description + */ +class FeedingScheduleDetailsFragment : Fragment() +{ + companion object + { + fun newInstance(scheduleIndex: Int = -1): FeedingScheduleDetailsFragment + { + return FeedingScheduleDetailsFragment().apply { + this.scheduleIndex = scheduleIndex + } + } + } + + private var scheduleIndex: Int = -1 + private var schedules = arrayListOf() + private val measureUnit: Unit by lazy { Unit.getSelectedMeasurementUnit(activity); } + private val deliveryUnit: Unit by lazy { Unit.getSelectedDeliveryUnit(activity); } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View + = inflater?.inflate(R.layout.schedule_details_view, container, false) ?: View(activity) + + override fun onActivityCreated(savedInstanceState: Bundle?) + { + super.onActivityCreated(savedInstanceState) + + if (scheduleIndex > -1) + { + schedules = ScheduleManager.instance.schedules[scheduleIndex].schedules + title.setText(ScheduleManager.instance.schedules[scheduleIndex].name) + description.setText(ScheduleManager.instance.schedules[scheduleIndex].description) + } + + populateSchedules() + + fab_complete.setOnClickListener { + when (scheduleIndex) + { + -1 -> { + val schedule: FeedingSchedule = FeedingSchedule( + name = title.text.toString(), + description = description.text.toString(), + schedules = schedules + ) + + ScheduleManager.instance.insert(schedule) + } + else -> { + ScheduleManager.instance.schedules[scheduleIndex].apply { + name = this@FeedingScheduleDetailsFragment.title.text.toString() + description = this@FeedingScheduleDetailsFragment.description.text.toString() + schedules = this@FeedingScheduleDetailsFragment.schedules + } + + ScheduleManager.instance.save() + } + } + + activity.finish() + } + } + + public fun onBackPressed(): Boolean + { + if (scheduleIndex > -1) + { + with (ScheduleManager.instance.schedules[scheduleIndex]) + { + if (name.isEmpty() && schedules.isEmpty()) + { + ScheduleManager.instance.schedules.removeAt(scheduleIndex) + return true + } + } + } + + return true + } + + /** + * Populates the schedules container + */ + private fun populateSchedules() + { + schedules.sortWith(Comparator { a, b -> + if (a.dateRange[0] < b.dateRange[0] && a.stageRange[0].ordinal < b.stageRange[0].ordinal) -1 + else if (a.dateRange[0] > b.dateRange[0] && a.stageRange[0].ordinal > b.stageRange[0].ordinal) 1 + else 0 + }) + + schedules_container.removeViews(0, schedules_container.indexOfChild(new_schedule)) + schedules.forEachIndexed { index, schedule -> + val feedingView = LayoutInflater.from(activity).inflate(R.layout.feeding_date_stub, schedules_container, false) + feedingView.title.text = "${schedule.dateRange[0]}${schedule.stageRange[0].printString[0]}" + if (schedule.dateRange[0] != schedule.dateRange[1]) + { + feedingView.title.text = "${feedingView.title.text} - ${schedule.dateRange[1]}${schedule.stageRange[1].printString[0]}" + } + + var waterStr = "" + for (additive in schedule.additives) + { + val converted = Unit.ML.to(measureUnit, additive.amount!!) + val amountStr = if (converted == Math.floor(converted)) converted.toInt().toString() else converted.toString() + + if (waterStr.isNotEmpty()) waterStr += "
" + waterStr += "• ${additive.description} - ${amountStr}${measureUnit.label}/${deliveryUnit.label}" + } + + feedingView.additives.text = Html.fromHtml(waterStr) + if (feedingView.additives.text.isEmpty()) feedingView.additives.visibility = View.GONE + + feedingView.delete.setOnClickListener { view -> + AlertDialog.Builder(view.context) + .setTitle(R.string.confirm_title) + .setMessage(R.string.confirm_delete_schedule) + .setPositiveButton(R.string.confirm_positive) { dialog, which -> + val index = schedules.indexOf(schedule) + schedules.remove(schedule) + populateSchedules() + + SnackBar().show(activity!!, R.string.schedule_deleted, R.string.undo, { + FabAnimator.animateUp(fab_complete) + }, { + FabAnimator.animateDown(fab_complete) + }, { + schedules.add(index, schedule) + populateSchedules() + }) + } + .setNegativeButton(R.string.confirm_negative, null) + .show() + } + + feedingView.copy.setOnClickListener { view -> + val newSchedule = Kryo().copy(schedule) + schedules.add(newSchedule) + populateSchedules() + + SnackBar().show(activity!!, R.string.schedule_copied, R.string.undo, { + FabAnimator.animateUp(fab_complete) + }, { + FabAnimator.animateDown(fab_complete) + }, { + schedules.remove(newSchedule) + populateSchedules() + }) + } + + feedingView.setOnClickListener { + startActivityForResult(Intent(it.context, ScheduleDateDetailsActivity::class.java).also { + it.putExtra("schedule_index", scheduleIndex) + it.putExtra("date_index", index) + }, 0) + } + schedules_container.addView(feedingView, schedules_container.childCount - 1) + } + + new_schedule.setOnClickListener { + if (scheduleIndex < 0) + { + schedules = arrayListOf() + ScheduleManager.instance.insert(FeedingSchedule( + name = title.text.toString(), + description = description.text.toString(), + schedules = schedules + )) + scheduleIndex = ScheduleManager.instance.schedules.size - 1 + } + + startActivityForResult(Intent(it.context, ScheduleDateDetailsActivity::class.java).also { + it.putExtra("schedule_index", scheduleIndex) + it.putExtra("date_index", -1) + }, 0) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) + { + super.onActivityResult(requestCode, resultCode, data) + populateSchedules() + } +} diff --git a/app/src/main/java/me/anon/grow/fragment/FeedingScheduleListFragment.kt b/app/src/main/java/me/anon/grow/fragment/FeedingScheduleListFragment.kt new file mode 100644 index 00000000..38f96525 --- /dev/null +++ b/app/src/main/java/me/anon/grow/fragment/FeedingScheduleListFragment.kt @@ -0,0 +1,92 @@ +package me.anon.grow.fragment + +import android.app.Fragment +import android.content.Intent +import android.os.Bundle +import android.support.v7.widget.DividerItemDecoration +import android.support.v7.widget.LinearLayoutManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout.VERTICAL +import com.esotericsoftware.kryo.Kryo +import com.kenny.snackbar.SnackBar +import kotlinx.android.synthetic.main.schedule_list_view.* +import me.anon.controller.adapter.FeedingScheduleAdapter +import me.anon.grow.FeedingScheduleDetailsActivity +import me.anon.grow.R +import me.anon.lib.helper.FabAnimator +import me.anon.lib.manager.ScheduleManager +import me.anon.lib.show + +/** + * Fragment for displaying list of feeding schedules + */ +class FeedingScheduleListFragment : Fragment() +{ + private val adapter = FeedingScheduleAdapter() + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View + = inflater?.inflate(R.layout.schedule_list_view, container, false) ?: View(activity) + + override fun onActivityCreated(savedInstanceState: Bundle?) + { + super.onActivityCreated(savedInstanceState) + + adapter.items = ScheduleManager.instance.schedules + recycler_view.adapter = adapter + recycler_view.layoutManager = LinearLayoutManager(activity) + recycler_view.addItemDecoration(DividerItemDecoration(activity, VERTICAL).also { + it.setDrawable(resources.getDrawable(R.drawable.left_inset_divider)) + }) + + adapter.onDeleteCallback = { schedule -> + val index = ScheduleManager.instance.schedules.indexOf(schedule) + ScheduleManager.instance.schedules.remove(schedule) + ScheduleManager.instance.save() + adapter.items = ScheduleManager.instance.schedules + adapter.notifyDataSetChanged() + + SnackBar().show(activity, R.string.schedule_deleted, R.string.undo, { + FabAnimator.animateUp(fab_add) + }, { + FabAnimator.animateDown(fab_add) + }, { + ScheduleManager.instance.schedules.add(index, schedule) + ScheduleManager.instance.save() + adapter.items = ScheduleManager.instance.schedules + adapter.notifyDataSetChanged() + }) + } + + adapter.onCopyCallback = { schedule -> + val newSchedule = Kryo().copy(schedule) + newSchedule.name += " (copy)" + ScheduleManager.instance.insert(newSchedule) + adapter.items = ScheduleManager.instance.schedules + adapter.notifyDataSetChanged() + + SnackBar().show(activity, R.string.schedule_copied, R.string.undo, { + FabAnimator.animateUp(fab_add) + }, { + FabAnimator.animateDown(fab_add) + }, { + ScheduleManager.instance.schedules.remove(newSchedule) + ScheduleManager.instance.save() + adapter.items = ScheduleManager.instance.schedules + adapter.notifyDataSetChanged() + }) + } + + fab_add.setOnClickListener { + startActivity(Intent(activity, FeedingScheduleDetailsActivity::class.java)) + } + } + + override fun onResume() + { + super.onResume() + adapter.items = ScheduleManager.instance.schedules + adapter.notifyDataSetChanged() + } +} diff --git a/app/src/main/java/me/anon/grow/fragment/FeedingSelectDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/FeedingSelectDialogFragment.java new file mode 100644 index 00000000..43ff2578 --- /dev/null +++ b/app/src/main/java/me/anon/grow/fragment/FeedingSelectDialogFragment.java @@ -0,0 +1,128 @@ +package me.anon.grow.fragment; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.LinearLayout; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import me.anon.controller.adapter.FeedingDateAdapter; +import me.anon.grow.R; +import me.anon.lib.Views; +import me.anon.lib.helper.TimeHelper; +import me.anon.model.FeedingSchedule; +import me.anon.model.FeedingScheduleDate; +import me.anon.model.Plant; +import me.anon.model.PlantStage; + +/** + * // TODO: Add class description + * + * @author 7LPdWcaW + * @documentation // TODO Reference flow doc + * @project GrowTracker + */ +@Views.Injectable +public class FeedingSelectDialogFragment extends DialogFragment +{ + public interface OnFeedingSelectedListener + { + public void onFeedingSelected(FeedingScheduleDate date); + } + + @Views.InjectView(R.id.recycler_view) private RecyclerView recyclerView; + private FeedingDateAdapter adapter; + private OnFeedingSelectedListener onFeedingSelected; + private Plant plant; + private FeedingSchedule schedule; + + public void setOnFeedingSelectedListener(OnFeedingSelectedListener onFeedingSelected) + { + this.onFeedingSelected = onFeedingSelected; + } + + @SuppressLint("ValidFragment") + public FeedingSelectDialogFragment(FeedingSchedule schedule, Plant plant) + { + this.plant = plant; + this.schedule = schedule; + } + + + @SuppressLint("ValidFragment") + public FeedingSelectDialogFragment() + { + } + + @Override public Dialog onCreateDialog(Bundle savedInstanceState) + { + View view = getActivity().getLayoutInflater().inflate(R.layout.feeding_list_dialog_view, null, false); + Views.inject(this, view); + + adapter = new FeedingDateAdapter(); + adapter.setPlant(plant); + adapter.setItems(schedule.getSchedules()); + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), LinearLayout.VERTICAL)); + + final AlertDialog dialog = new AlertDialog.Builder(getActivity()) + .setTitle("Feedings") + .setView(view) + .create(); + + adapter.setOnItemSelectCallback(new Function1() + { + @Override public Unit invoke(FeedingScheduleDate feedingScheduleDate) + { + if (onFeedingSelected != null) + { + onFeedingSelected.onFeedingSelected(feedingScheduleDate); + } + + dialog.dismiss(); + return null; + } + }); + + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + dialog.setOnShowListener(new DialogInterface.OnShowListener() + { + @Override public void onShow(DialogInterface dialog) + { + PlantStage lastStage = adapter.getLastStage(); + int days = (int)TimeHelper.toDays(adapter.getPlantStages().get(lastStage)); + int suggestedIndex = 0; + for (FeedingScheduleDate feedingScheduleDate : adapter.getItems()) + { + if (lastStage.ordinal() >= feedingScheduleDate.getStageRange()[0].ordinal()) + { + if (days >= feedingScheduleDate.getDateRange()[0] + && ((days <= feedingScheduleDate.getDateRange()[1] && lastStage.ordinal() == feedingScheduleDate.getStageRange()[0].ordinal()) + || (lastStage.ordinal() < feedingScheduleDate.getStageRange()[1].ordinal()))) + { + break; + } + } + + suggestedIndex++; + } + + recyclerView.scrollToPosition(suggestedIndex); + } + }); + + return dialog; + } +} diff --git a/app/src/main/java/me/anon/grow/fragment/GardenDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/GardenDialogFragment.java index 50474a15..20968649 100644 --- a/app/src/main/java/me/anon/grow/fragment/GardenDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/GardenDialogFragment.java @@ -15,7 +15,6 @@ import java.util.ArrayList; -import lombok.Setter; import me.anon.controller.adapter.PlantSelectionAdapter; import me.anon.grow.R; import me.anon.lib.Views; @@ -42,7 +41,12 @@ public interface OnEditGardenListener private PlantSelectionAdapter adapter; @Views.InjectView(R.id.name) private EditText name; @Views.InjectView(R.id.recycler_view) private RecyclerView recyclerView; - @Setter private OnEditGardenListener onEditGardenListener; + private OnEditGardenListener onEditGardenListener; + + public void setOnEditGardenListener(OnEditGardenListener onEditGardenListener) + { + this.onEditGardenListener = onEditGardenListener; + } @SuppressLint("ValidFragment") public GardenDialogFragment(Garden garden) diff --git a/app/src/main/java/me/anon/grow/fragment/NoteDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/NoteDialogFragment.java index e3eb24c5..7ddaa0a9 100644 --- a/app/src/main/java/me/anon/grow/fragment/NoteDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/NoteDialogFragment.java @@ -12,7 +12,6 @@ import android.view.View; import android.widget.EditText; -import lombok.Setter; import me.anon.grow.R; import me.anon.lib.Views; import me.anon.model.NoteAction; @@ -28,7 +27,12 @@ public static interface OnDialogConfirmed @Views.InjectView(R.id.notes) private EditText notes; private NoteAction action; - @Setter private OnDialogConfirmed onDialogConfirmed; + private OnDialogConfirmed onDialogConfirmed; + + public void setOnDialogConfirmed(OnDialogConfirmed onDialogConfirmed) + { + this.onDialogConfirmed = onDialogConfirmed; + } @SuppressLint("ValidFragment") public NoteDialogFragment(){} diff --git a/app/src/main/java/me/anon/grow/fragment/PinDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/PinDialogFragment.java index a4b99f49..954e3f81 100644 --- a/app/src/main/java/me/anon/grow/fragment/PinDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/PinDialogFragment.java @@ -14,7 +14,6 @@ import android.view.WindowManager; import android.widget.EditText; -import lombok.Setter; import me.anon.grow.R; import me.anon.lib.Views; @@ -33,9 +32,24 @@ public static interface OnDialogCancelled @Views.InjectView(R.id.pin) private EditText input; - @Setter private OnDialogConfirmed onDialogConfirmed; - @Setter private OnDialogCancelled onDialogCancelled; - @Setter private String title = "Pin"; + private OnDialogConfirmed onDialogConfirmed; + private OnDialogCancelled onDialogCancelled; + private String title = "Pin"; + + public void setOnDialogConfirmed(OnDialogConfirmed onDialogConfirmed) + { + this.onDialogConfirmed = onDialogConfirmed; + } + + public void setOnDialogCancelled(OnDialogCancelled onDialogCancelled) + { + this.onDialogCancelled = onDialogCancelled; + } + + public void setTitle(String title) + { + this.title = title; + } @SuppressLint("ValidFragment") public PinDialogFragment(){} diff --git a/app/src/main/java/me/anon/grow/fragment/PlantDetailsFragment.java b/app/src/main/java/me/anon/grow/fragment/PlantDetailsFragment.java index 0c1c2a0e..d7fb9f53 100644 --- a/app/src/main/java/me/anon/grow/fragment/PlantDetailsFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/PlantDetailsFragment.java @@ -5,6 +5,7 @@ import android.app.AlertDialog; import android.app.Fragment; import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.ProgressDialog; @@ -25,6 +26,7 @@ import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.content.FileProvider; +import android.support.v7.widget.CardView; import android.text.Html; import android.text.TextUtils; import android.view.LayoutInflater; @@ -34,6 +36,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; @@ -69,12 +72,12 @@ import me.anon.grow.R; import me.anon.grow.StatisticsActivity; import me.anon.grow.ViewPhotosActivity; +import me.anon.lib.DateRenderer; import me.anon.lib.ExportCallback; import me.anon.lib.Views; import me.anon.lib.helper.AddonHelper; import me.anon.lib.helper.ExportHelper; import me.anon.lib.helper.FabAnimator; -import me.anon.lib.helper.GsonHelper; import me.anon.lib.helper.PermissionHelper; import me.anon.lib.manager.GardenManager; import me.anon.lib.manager.PlantManager; @@ -110,6 +113,13 @@ public class PlantDetailsFragment extends Fragment @Views.InjectView(R.id.plant_date_container) private View dateContainer; @Views.InjectView(R.id.from_clone) private CheckBox clone; + @Views.InjectView(R.id.last_feeding) private CardView lastFeeding; + @Views.InjectView(R.id.last_feeding_date) private TextView lastFeedingDate; + @Views.InjectView(R.id.last_feeding_full_date) private TextView lastFeedingFullDate; + @Views.InjectView(R.id.last_feeding_name) private TextView lastFeedingName; + @Views.InjectView(R.id.last_feeding_summary) private TextView lastFeedingSummary; + @Views.InjectView(R.id.duplicate_feeding) private Button duplicateFeeding; + private int plantIndex = -1; private int gardenIndex = -1; private Plant plant; @@ -166,6 +176,7 @@ public static PlantDetailsFragment newInstance(int plantIndex, int gardenIndex) getActivity().setTitle("Add new plant"); plant.getActions().add(new StageChange(PlantStage.PLANTED)); + lastFeeding.setVisibility(View.GONE); } else { @@ -185,6 +196,8 @@ public static PlantDetailsFragment newInstance(int plantIndex, int gardenIndex) { medium.setText(plant.getMedium().getPrintString()); } + + setLastFeeding(); } setUi(); @@ -201,6 +214,53 @@ public static PlantDetailsFragment newInstance(int plantIndex, int gardenIndex) } } + private void setLastFeeding() + { + Water lastWater = null; + for (int index = plant.getActions().size() - 1; index >= 0; index--) + { + if (plant.getActions().get(index) instanceof Water) + { + lastWater = (Water)plant.getActions().get(index); + break; + } + } + + lastFeeding.setVisibility(View.GONE); + if (lastWater != null) + { + lastFeeding.setVisibility(View.VISIBLE); + lastFeeding.setCardBackgroundColor(0x9ABBDEFB); + + lastFeedingSummary.setText(Html.fromHtml(lastWater.getSummary(getActivity()))); + + DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(getActivity()); + DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(getActivity()); + Date actionDate = new Date(lastWater.getDate()); + lastFeedingFullDate.setText(dateFormat.format(actionDate) + " " + timeFormat.format(actionDate)); + lastFeedingDate.setText(Html.fromHtml("" + new DateRenderer().timeAgo(lastWater.getDate()).formattedDate + " ago")); + + final Water finalLastWater = lastWater; + duplicateFeeding.setOnClickListener(new View.OnClickListener() + { + @Override public void onClick(View v) + { + Kryo kryo = new Kryo(); + Water action = kryo.copy(finalLastWater); + + action.setDate(System.currentTimeMillis()); + PlantManager.getInstance().getPlants().get(plantIndex).getActions().add(action); + PlantManager.getInstance().save(); + + Intent editWater = new Intent(v.getContext(), EditWateringActivity.class); + editWater.putExtra("plant_index", plantIndex); + editWater.putExtra("action_index", PlantManager.getInstance().getPlants().get(plantIndex).getActions().size() - 1); + startActivityForResult(editWater, 4); + } + }); + } + } + private void setUi() { final DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(getActivity()); @@ -472,6 +532,10 @@ else if (requestCode == 3) // choose image from gallery } } } + else if (requestCode == 4) + { + setLastFeeding(); + } // both photo options if ((requestCode == 1 || requestCode == 3) && resultCode != Activity.RESULT_CANCELED) @@ -633,6 +697,7 @@ else if (item.getItemId() == R.id.duplicate) copy.setId(UUID.randomUUID().toString()); copy.getImages().clear(); + copy.setName(copy.getName() + " (copy)"); PlantManager.getInstance().addPlant(copy); SnackBar.show(getActivity(), "Plant duplicated", "open", new SnackBarListener() @@ -666,18 +731,22 @@ else if (item.getItemId() == R.id.export) Toast.makeText(getActivity(), "Exporting grow log...", Toast.LENGTH_SHORT).show(); NotificationManager notificationManager = (NotificationManager)getActivity().getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= 26) + { + NotificationChannel channel = new NotificationChannel("export", "Export status", NotificationManager.IMPORTANCE_DEFAULT); + notificationManager.createNotificationChannel(channel); + } + notificationManager.cancel(plantIndex); - Notification exportNotification = new NotificationCompat.Builder(getActivity()) + Notification exportNotification = new NotificationCompat.Builder(getActivity(), "export") .setContentText("Exporting grow log for " + plant.getName()) .setContentTitle("Exporting") .setContentIntent(PendingIntent.getActivity(getActivity(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT)) .setTicker("Exporting grow log for " + plant.getName()) - .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_name) - .setVibrate(new long[0]) - .getNotification(); + .build(); notificationManager.notify(plantIndex, exportNotification); @@ -687,18 +756,24 @@ else if (item.getItemId() == R.id.export) { if (file != null && file.exists() && getActivity() != null) { - NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); - Notification finishNotification = new NotificationCompat.Builder(getActivity()) + NotificationManager notificationManager = (NotificationManager)getActivity().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(plantIndex); + + Intent openIntent = new Intent(Intent.ACTION_VIEW); + Uri apkURI = FileProvider.getUriForFile(getActivity(), getActivity().getApplicationContext().getPackageName() + ".provider", file); + openIntent.setDataAndType(apkURI, "application/zip"); + openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + Notification finishNotification = new NotificationCompat.Builder(getActivity(), "export") .setContentText("Exported " + plant.getName() + " to " + file.getAbsolutePath()) .setTicker("Export of " + plant.getName() + " complete") .setContentTitle("Export Complete") - .setContentIntent(PendingIntent.getActivity(getActivity(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(PendingIntent.getActivity(getActivity(), 0, openIntent, PendingIntent.FLAG_UPDATE_CURRENT)) .setSmallIcon(R.drawable.ic_stat_done) .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setAutoCancel(true) - .setVibrate(new long[0]) - .getNotification(); + .build(); notificationManager.notify(plantIndex, finishNotification); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) diff --git a/app/src/main/java/me/anon/grow/fragment/PlantListFragment.java b/app/src/main/java/me/anon/grow/fragment/PlantListFragment.java index 6d6efa4a..b7710005 100644 --- a/app/src/main/java/me/anon/grow/fragment/PlantListFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/PlantListFragment.java @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; -import lombok.Setter; import me.anon.controller.adapter.PlantAdapter; import me.anon.controller.adapter.SimpleItemTouchHelperCallback; import me.anon.grow.AddPlantActivity; @@ -60,7 +59,12 @@ public class PlantListFragment extends Fragment { private PlantAdapter adapter; - @Setter private Garden garden; + private Garden garden; + + public void setGarden(Garden garden) + { + this.garden = garden; + } public static PlantListFragment newInstance(@Nullable Garden garden) { diff --git a/app/src/main/java/me/anon/grow/fragment/PlantSelectDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/PlantSelectDialogFragment.java index a878b04f..c7df7591 100644 --- a/app/src/main/java/me/anon/grow/fragment/PlantSelectDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/PlantSelectDialogFragment.java @@ -16,7 +16,6 @@ import java.util.ArrayList; -import lombok.Setter; import me.anon.controller.adapter.PlantSelectionAdapter; import me.anon.grow.R; import me.anon.lib.Views; @@ -47,7 +46,12 @@ public interface OnDialogActionListener private PlantSelectionAdapter adapter; @Views.InjectView(R.id.recycler_view) private RecyclerView recyclerView; private boolean showImages = true; - @Setter private OnDialogActionListener onDialogActionListener; + private OnDialogActionListener onDialogActionListener; + + public void setOnDialogActionListener(OnDialogActionListener onDialogActionListener) + { + this.onDialogActionListener = onDialogActionListener; + } @SuppressLint("ValidFragment") public PlantSelectDialogFragment() diff --git a/app/src/main/java/me/anon/grow/fragment/ScheduleDateDetailsFragment.kt b/app/src/main/java/me/anon/grow/fragment/ScheduleDateDetailsFragment.kt new file mode 100644 index 00000000..e5f605f8 --- /dev/null +++ b/app/src/main/java/me/anon/grow/fragment/ScheduleDateDetailsFragment.kt @@ -0,0 +1,255 @@ +package me.anon.grow.fragment + +import android.app.Activity +import android.app.Fragment +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +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.TextView +import kotlinx.android.synthetic.main.schedule_date_details_view.* +import me.anon.grow.R +import me.anon.lib.Unit +import me.anon.lib.manager.ScheduleManager +import me.anon.model.Additive +import me.anon.model.FeedingScheduleDate +import me.anon.model.PlantStage + +/** + * // TODO: Add class description + */ +class ScheduleDateDetailsFragment : Fragment() +{ + companion object + { + fun newInstance(scheduleIndex: Int = -1, dateIndex: Int = -1): ScheduleDateDetailsFragment + { + return ScheduleDateDetailsFragment().apply { + this.scheduleIndex = scheduleIndex + this.dateIndex = dateIndex + } + } + } + + private var scheduleIndex: Int = -1 + private var dateIndex: Int = -1 + private var additives: ArrayList = arrayListOf() + private val selectedMeasurementUnit: Unit by lazy { Unit.getSelectedMeasurementUnit(activity); } + private val selectedDeliveryUnit: Unit by lazy { Unit.getSelectedDeliveryUnit(activity); } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View + = inflater?.inflate(R.layout.schedule_date_details_view, container, false) ?: View(activity) + + override fun onActivityCreated(savedInstanceState: Bundle?) + { + super.onActivityCreated(savedInstanceState) + + to_stage.adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, PlantStage.values().map { it.printString }.toTypedArray()) + from_stage.adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, PlantStage.values().map { it.printString }.toTypedArray()) + + from_stage.onItemSelectedListener = object: AdapterView.OnItemSelectedListener + { + override fun onNothingSelected(parent: AdapterView<*>?){} + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) + { + if (to_stage.selectedItemPosition < position) + { + to_stage.setSelection(position) + } + } + } + + to_stage.onItemSelectedListener = object: AdapterView.OnItemSelectedListener + { + override fun onNothingSelected(parent: AdapterView<*>?){} + + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) + { + if (from_stage.selectedItemPosition > position) + { + from_stage.setSelection(position) + } + } + } + + from_date.addTextChangedListener(object: TextWatcher + { + override fun afterTextChanged(s: Editable?){} + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int){} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) + { + if (to_date.text.isEmpty()) to_date.setText(s) + } + }) + + if (dateIndex > -1) + { + ScheduleManager.instance.schedules[scheduleIndex].schedules[dateIndex].apply { + from_stage.setSelection(this.stageRange[0].ordinal) + to_stage.setSelection(this.stageRange[1].ordinal) + from_date.setText(this.dateRange[0].toString()) + to_date.setText(this.dateRange[1].toString()) + + this@ScheduleDateDetailsFragment.additives = additives + } + } + + populateAdditives() + + fab_complete.setOnClickListener { + if (from_date.text.isEmpty()) + { + from_date.error = "From date is required" + return@setOnClickListener + } + + val fromDate = from_date.text.toString().toInt() + val toDate = if (to_date.text.isEmpty()) fromDate else to_date.text.toString().toInt() + val fromStage = PlantStage.valueOfPrintString(from_stage.selectedItem as String)!! + val toStage = PlantStage.valueOfPrintString(to_stage.selectedItem as String)!! + + if (toDate < fromDate && fromStage.ordinal ?: -1 == toStage.ordinal ?: -1) + { + to_date.error = "Date can not be before from date" + return@setOnClickListener + } + + when (dateIndex) + { + -1 -> { + val date: FeedingScheduleDate = FeedingScheduleDate( + dateRange = arrayOf(fromDate, if (to_date.text.isEmpty()) fromDate else toDate), + stageRange = arrayOf(fromStage, toStage), + additives = additives + ) + + ScheduleManager.instance.schedules[scheduleIndex].schedules.add(date) + } + else -> { + ScheduleManager.instance.schedules[scheduleIndex].schedules[dateIndex].apply { + this.dateRange = arrayOf(fromDate, if (to_date.text.isEmpty()) fromDate else toDate) + this.stageRange = arrayOf(fromStage, toStage) + this.additives = this@ScheduleDateDetailsFragment.additives + } + } + } + + activity.setResult(Activity.RESULT_OK) + activity.finish() + } + } + + private fun populateAdditives() + { + for (additive in additives) + { + if (additive.amount == null) continue + + val converted = Unit.ML.to(selectedMeasurementUnit, additive.amount) + val amountStr = if (converted == Math.floor(converted)) converted.toInt().toString() else converted.toString() + + val additiveStub = LayoutInflater.from(activity).inflate(R.layout.additive_stub, additive_container, false) + (additiveStub as TextView).text = "${additive.description} - $amountStr${selectedMeasurementUnit.label}/${selectedDeliveryUnit.label}" + + additiveStub.setTag(additive) + additiveStub.setOnClickListener { view -> onNewAdditiveClick(view) } + additive_container.addView(additiveStub, additive_container.childCount - 1) + } + + new_additive.setOnClickListener { onNewAdditiveClick(it) } + } + + private fun onNewAdditiveClick(view: View) + { + val currentFocus = activity.currentFocus + val currentTag = view.tag + val fm = fragmentManager + val addAdditiveDialogFragment = AddAdditiveDialogFragment(if (view.tag is Additive) view.tag as Additive else null) + addAdditiveDialogFragment.setOnAdditiveSelectedListener(object : AddAdditiveDialogFragment.OnAdditiveSelectedListener + { + override fun onAdditiveSelected(additive: Additive) + { + if (TextUtils.isEmpty(additive.description)) + { + return + } + + var converted = Unit.ML.to(selectedMeasurementUnit, additive.amount!!) + var amountStr = if (converted == Math.floor(converted)) converted.toInt().toString() else converted.toString() + + val additiveStub = LayoutInflater.from(activity).inflate(R.layout.additive_stub, additive_container, false) + (additiveStub as TextView).text = "${additive.description} - $amountStr${selectedMeasurementUnit.label}/${selectedDeliveryUnit.label}" + + if (currentTag == null) + { + if (!additives.contains(additive)) + { + additives.add(additive) + + additiveStub.setTag(additive) + additiveStub.setOnClickListener { view -> onNewAdditiveClick(view) } + additive_container.addView(additiveStub, additive_container.childCount - 1) + } + } + else + { + for (childIndex in 0 until additive_container.childCount) + { + val tag = additive_container.getChildAt(childIndex).tag + + if (tag === currentTag) + { + converted = Unit.ML.to(selectedMeasurementUnit, additive.amount!!) + amountStr = if (converted == Math.floor(converted)) converted.toInt().toString() else converted.toString() + + additives[childIndex] = additive + + (additive_container.getChildAt(childIndex) as TextView).text = "${additive.description} - $amountStr${selectedMeasurementUnit.label}/${selectedDeliveryUnit.label}" + additive_container.getChildAt(childIndex).tag = additive + + break + } + } + } + + currentFocus?.let { + it.requestFocus() + it.requestFocusFromTouch() + } + } + + override fun onAdditiveDeleteRequested(additive: Additive) + { + if (additives.contains(additive)) + { + additives.remove(additive) + } + + for (childIndex in 0 until additive_container.childCount) + { + val tag = additive_container.getChildAt(childIndex).tag + + if (tag === additive) + { + additive_container.removeViewAt(childIndex) + break + } + } + + currentFocus?.let { + it.requestFocus() + it.requestFocusFromTouch() + } + } + }) + + addAdditiveDialogFragment.show(fm, "fragment_add_additive") + } +} diff --git a/app/src/main/java/me/anon/grow/fragment/SettingsFragment.java b/app/src/main/java/me/anon/grow/fragment/SettingsFragment.java index b79892e0..a46949da 100644 --- a/app/src/main/java/me/anon/grow/fragment/SettingsFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/SettingsFragment.java @@ -14,6 +14,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceCategory; @@ -26,12 +27,19 @@ import android.view.View; import android.widget.Toast; +import com.kenny.snackbar.SnackBar; import com.nostra13.universalimageloader.core.ImageLoader; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; import java.util.List; import me.anon.controller.receiver.BackupService; @@ -40,14 +48,19 @@ import me.anon.lib.TempUnit; import me.anon.lib.Unit; import me.anon.lib.helper.AddonHelper; +import me.anon.lib.helper.BackupHelper; import me.anon.lib.helper.EncryptionHelper; +import me.anon.lib.manager.FileManager; import me.anon.lib.manager.GardenManager; import me.anon.lib.manager.PlantManager; +import me.anon.lib.manager.ScheduleManager; import me.anon.lib.task.DecryptTask; import me.anon.lib.task.EncryptTask; import me.anon.model.Garden; import me.anon.model.Plant; +import static me.anon.lib.manager.PlantManager.FILES_DIR; + /** * // TODO: Add class description * @@ -90,6 +103,8 @@ public class SettingsFragment extends PreferenceFragment implements Preference.O findPreference("delivery_unit").setOnPreferenceClickListener(this); findPreference("measurement_unit").setOnPreferenceClickListener(this); findPreference("temperature_unit").setOnPreferenceClickListener(this); + findPreference("backup_now").setOnPreferenceClickListener(this); + findPreference("restore").setOnPreferenceClickListener(this); findPreference("failsafe").setEnabled(((CheckBoxPreference)findPreference("encrypt")).isChecked()); @@ -569,7 +584,7 @@ else if ("readme".equals(preference.getKey())) } else if ("export".equals(preference.getKey())) { - Uri contentUri = FileProvider.getUriForFile(getActivity(), getActivity().getPackageName() + ".provider", new File(PlantManager.FILES_DIR, "plants.json")); + Uri contentUri = FileProvider.getUriForFile(getActivity(), getActivity().getPackageName() + ".provider", new File(FILES_DIR, "plants.json")); Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); @@ -580,6 +595,152 @@ else if ("export".equals(preference.getKey())) return true; } + else if ("backup_now".equals(preference.getKey())) + { + Toast.makeText(getActivity(), "Backed up to " + BackupHelper.backupJson().getPath(), Toast.LENGTH_SHORT).show(); + } + else if ("restore".equals(preference.getKey())) + { + class BackupData + { + Date date; + String plantsPath; + String gardenPath; + String schedulePath; + + @Override public String toString() + { + DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(getActivity()); + DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(getActivity()); + return dateFormat.format(date) + " " + timeFormat.format(date); + } + } + + // get list of backups + File backupPath = new File(Environment.getExternalStorageDirectory(), "/backups/GrowTracker/"); + String[] backupFiles = backupPath.list(); + Arrays.sort(backupFiles); + final ArrayList backups = new ArrayList(); + + BackupData current = null; + Date lastDate = new Date(); + for (String backup : backupFiles) + { + if (current == null) + { + current = new BackupData(); + } + + File backupFile = new File(backup); + String[] parts = backupFile.getName().split("\\."); + Date date = new Date(); + if (parts.length > 1) + { + try + { + date = new Date(Long.parseLong(parts[0])); + } + catch (NumberFormatException e) + { + date = new Date(backupFile.lastModified()); + } + + if (parts.length == 2) + { + BackupData backupData = new BackupData(); + backupData.plantsPath = backupPath.getPath() + "/" + backup; + backupData.date = date; + backups.add(backupData); + continue; + } + else + { + current.date = date; + } + } + else + { + continue; + } + + if (!current.date.equals(lastDate)) + { + lastDate = current.date; + backups.add(current); + current = new BackupData(); + } + + if (backup.contains("plants")) + { + current.plantsPath = backupPath.getPath() + "/" + backup; + } + + if (backup.contains("gardens")) + { + current.gardenPath = backupPath.getPath() + "/" + backup; + } + + if (backup.contains("schedules")) + { + current.schedulePath = backupPath.getPath() + "/" + backup; + } + } + + Collections.sort(backups, new Comparator() + { + @Override public int compare(BackupData o1, BackupData o2) + { + if (o1.date.before(o2.date)) return 1; + if (o1.date.after(o2.date)) return -1; + else return 0; + } + }); + CharSequence[] items = new CharSequence[backups.size()]; + for (int index = 0, count = backups.size(); index < count; index++) + { + items[index] = backups.get(index).toString(); + } + + new AlertDialog.Builder(getActivity()) + .setTitle("Select backup") + .setItems(items, new DialogInterface.OnClickListener() + { + @Override public void onClick(DialogInterface dialog, int which) + { + BackupData selectedBackup = backups.get(which); + + if ((MainApplication.isFailsafe())) + { + MainApplication.setFailsafe(false); + } + + FileManager.getInstance().copyFile(selectedBackup.plantsPath, PlantManager.FILES_DIR + "/plants.json"); + boolean loaded = PlantManager.getInstance().load(); + + if (selectedBackup.gardenPath != null) + { + FileManager.getInstance().copyFile(selectedBackup.gardenPath, GardenManager.FILES_DIR + "/gardens.json"); + GardenManager.getInstance().load(); + } + + if (selectedBackup.schedulePath != null) + { + FileManager.getInstance().copyFile(selectedBackup.schedulePath, ScheduleManager.FILES_DIR + "/schedules.json"); + ScheduleManager.instance.load(); + } + + if (!loaded) + { + SnackBar.show(getActivity(), "Could not restore from backup " + selectedBackup, null); + } + else + { + Toast.makeText(getActivity(), "Restore to " + selectedBackup + " completed", Toast.LENGTH_LONG).show(); + } + } + }) + .show(); + } return false; } diff --git a/app/src/main/java/me/anon/grow/fragment/StageDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/StageDialogFragment.java index 9b090bfb..f3f826eb 100644 --- a/app/src/main/java/me/anon/grow/fragment/StageDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/StageDialogFragment.java @@ -17,7 +17,6 @@ import java.util.Calendar; import java.util.Date; -import lombok.Setter; import me.anon.grow.R; import me.anon.lib.Views; import me.anon.model.PlantStage; @@ -34,7 +33,12 @@ public static interface OnStageUpdated @Views.InjectView(R.id.actions) private Spinner actionsSpinner; @Views.InjectView(R.id.date) private TextView date; - @Setter private OnStageUpdated onStageUpdated; + private OnStageUpdated onStageUpdated; + + public void setOnStageUpdated(OnStageUpdated onStageUpdated) + { + this.onStageUpdated = onStageUpdated; + } private StageChange action; private boolean edit = false; diff --git a/app/src/main/java/me/anon/grow/fragment/StatisticsFragment.java b/app/src/main/java/me/anon/grow/fragment/StatisticsFragment.java index 36def072..ee3dec64 100644 --- a/app/src/main/java/me/anon/grow/fragment/StatisticsFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/StatisticsFragment.java @@ -4,13 +4,19 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.PopupMenu; import android.widget.TextView; import com.github.mikephil.charting.charts.LineChart; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.SortedMap; import me.anon.grow.R; @@ -19,20 +25,15 @@ import me.anon.lib.helper.TimeHelper; import me.anon.lib.manager.PlantManager; import me.anon.model.Action; +import me.anon.model.Additive; import me.anon.model.EmptyAction; import me.anon.model.Plant; -import me.anon.model.PlantMedium; import me.anon.model.PlantStage; import me.anon.model.StageChange; import me.anon.model.Water; /** - * // TODO: Add class description - * - * TODO: Average time between feeds - * * @author 7LPdWcaW - * @documentation // TODO Reference flow doc * @project GrowTracker */ @Views.Injectable @@ -56,6 +57,8 @@ public static StatisticsFragment newInstance(int plantIndex) return fragment; } + @Views.InjectView(R.id.additives) private LineChart additives; + @Views.InjectView(R.id.additive_selector) private TextView additivesSpinner; @Views.InjectView(R.id.input_ph) private LineChart inputPh; @Views.InjectView(R.id.ppm) private LineChart ppm; @Views.InjectView(R.id.temp) private LineChart temp; @@ -92,6 +95,8 @@ public static StatisticsFragment newInstance(int plantIndex) @Views.InjectView(R.id.max_temp) private TextView maxtemp; @Views.InjectView(R.id.ave_temp) private TextView avetemp; + private Set checkedAdditives = null; + @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.statistics_view, container, false); @@ -104,6 +109,11 @@ public static StatisticsFragment newInstance(int plantIndex) { super.onActivityCreated(savedInstanceState); + if (savedInstanceState != null) + { + checkedAdditives = new HashSet<>(savedInstanceState.getStringArrayList("checked_additives")); + } + getActivity().setTitle("Plant statistics"); if (getArguments() != null) @@ -131,16 +141,95 @@ public static StatisticsFragment newInstance(int plantIndex) maxppm.setText(ppmAdditional[1].equals(String.valueOf(Long.MIN_VALUE)) ? "0" : ppmAdditional[1]); aveppm.setText(ppmAdditional[2]); - if (plant.getMedium() == PlantMedium.HYDRO) + setAdditiveStats(); + + tempContainer.setVisibility(View.VISIBLE); + + String[] tempAdditional = new String[3]; + StatsHelper.setTempData(plant, temp, tempAdditional); + mintemp.setText(tempAdditional[0].equals("100.0") ? "-" : tempAdditional[0]); + maxtemp.setText(tempAdditional[1].equals("-100.0") ? "-" : tempAdditional[1]); + avetemp.setText(tempAdditional[2]); + } + + @Override public void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putStringArrayList("checked_additives", new ArrayList(checkedAdditives)); + } + + private void setAdditiveStats() + { + final Set additiveNames = new HashSet<>(); + for (Action action : plant.getActions()) { - tempContainer.setVisibility(View.VISIBLE); + if (action instanceof Water) + { + List actionAdditives = ((Water)action).getAdditives(); + for (Additive additive : actionAdditives) + { + additiveNames.add(additive.getDescription()); + } + } + } - String[] tempAdditional = new String[3]; - StatsHelper.setTempData(plant, temp, tempAdditional); - minppm.setText(tempAdditional[0]); - maxppm.setText(tempAdditional[1]); - aveppm.setText(tempAdditional[2]); + if (checkedAdditives == null) + { + checkedAdditives = new HashSet<>(); + checkedAdditives.addAll(additiveNames); } + + StatsHelper.setAdditiveData(plant, additives, checkedAdditives); + additives.notifyDataSetChanged(); + additives.postInvalidate(); + + additivesSpinner.setOnClickListener(new View.OnClickListener() + { + @Override public void onClick(View v) + { + PopupMenu menu = new PopupMenu(v.getContext(), v); + menu.getMenu().add("All/None").setCheckable(false); + for (String additiveName : additiveNames) + { + menu.getMenu().add(additiveName).setCheckable(true).setChecked(checkedAdditives.contains(additiveName)); + } + + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() + { + @Override public boolean onMenuItemClick(MenuItem item) + { + if (!item.isCheckable()) + { + if (checkedAdditives.size() != additiveNames.size()) + { + checkedAdditives.clear(); + checkedAdditives.addAll(additiveNames); + } + else + { + checkedAdditives.clear(); + } + } + else + { + if (item.isChecked()) + { + checkedAdditives.remove(item.getTitle().toString()); + } + else + { + checkedAdditives.add(item.getTitle().toString()); + } + } + + setAdditiveStats(); + return true; + } + }); + + menu.show(); + } + }); } private void setStatistics() diff --git a/app/src/main/java/me/anon/grow/fragment/WateringFragment.java b/app/src/main/java/me/anon/grow/fragment/WateringFragment.java index 33fb8f10..510054de 100644 --- a/app/src/main/java/me/anon/grow/fragment/WateringFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/WateringFragment.java @@ -1,14 +1,22 @@ package me.anon.grow.fragment; import android.app.Activity; +import android.app.AlertDialog; import android.app.Fragment; import android.app.FragmentManager; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.Html; import android.text.TextUtils; +import android.text.TextWatcher; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -30,10 +38,12 @@ import me.anon.lib.Unit; import me.anon.lib.Views; import me.anon.lib.manager.PlantManager; +import me.anon.lib.manager.ScheduleManager; import me.anon.model.Action; import me.anon.model.Additive; +import me.anon.model.FeedingSchedule; +import me.anon.model.FeedingScheduleDate; import me.anon.model.Plant; -import me.anon.model.PlantMedium; import me.anon.model.Water; import static me.anon.lib.TempUnit.CELCIUS; @@ -69,6 +79,16 @@ public class WateringFragment extends Fragment private Unit selectedMeasurementUnit, selectedDeliveryUnit; private TempUnit selectedTemperatureUnit; private boolean usingEc = false; + private TextWatcher deliveryTextChangeListener = 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){} + + @Override public void afterTextChanged(Editable s) + { + populateAdditives(); + } + }; /** * @param plantIndex If -1, assume new plant @@ -86,6 +106,12 @@ public static WateringFragment newInstance(int[] plantIndex, int feedingIndex) return fragment; } + @Override public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.add_watering_view, container, false); @@ -150,6 +176,51 @@ public static WateringFragment newInstance(int[] plantIndex, int feedingIndex) } } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) + { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.feeding_menu, menu); + } + + @Override public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == R.id.action_populate_feeding) + { + ArrayList items = new ArrayList<>(); + for (FeedingSchedule feedingSchedule : ScheduleManager.instance.getSchedules()) + { + items.add(feedingSchedule.getName()); + } + + new AlertDialog.Builder(getActivity()) + .setTitle("Select schedule") + .setItems(items.toArray(new String[items.size()]), new DialogInterface.OnClickListener() + { + @Override public void onClick(DialogInterface dialog, int which) + { + showScheduleDialog(ScheduleManager.instance.getSchedules().get(which)); + } + }) + .show(); + } + + return super.onOptionsItemSelected(item); + } + + private void showScheduleDialog(FeedingSchedule schedule) + { + FeedingSelectDialogFragment feedingSelectDialogFragment = new FeedingSelectDialogFragment(schedule, plants.get(0)); + feedingSelectDialogFragment.setOnFeedingSelectedListener(new FeedingSelectDialogFragment.OnFeedingSelectedListener() + { + @Override public void onFeedingSelected(FeedingScheduleDate date) + { + water.setAdditives(date.getAdditives()); + populateAdditives(); + } + }); + feedingSelectDialogFragment.show(getFragmentManager(), "feeding"); + } + private void setHints() { amountLabel.setText("Amount (" + selectedDeliveryUnit.getLabel() + ")"); @@ -161,53 +232,103 @@ private void setHints() ((TextView)((ViewGroup)waterPpm.getParent()).findViewById(R.id.ppm_label)).setText("EC"); } - if (water != null && plants.size() == 1) + if (water != null) { - Water hintFeed = null; + ArrayList hintFeed = new ArrayList<>(); - for (int index = plants.get(0).getActions().size() - 1; index >= 0; index--) + for (Plant plant : plants) { - if (plants.get(0).getActions().get(index).getClass() == Water.class) + for (int index = plant.getActions().size() - 1; index >= 0; index--) { - hintFeed = (Water)plants.get(0).getActions().get(index); - break; + if (plant.getActions().get(index).getClass() == Water.class) + { + hintFeed.add((Water)plant.getActions().get(index)); + break; + } } } - if (hintFeed != null) + if (hintFeed.size() > 0) { - if (hintFeed.getPh() != null) + Double averagePh = 0.0; + int phCount = 0; + Double averagePpm = 0.0; + int ppmCount = 0; + Double averageRunoff = 0.0; + int runoffCount = 0; + Double averageAmount = 0.0; + int amountCount = 0; + Double averageTemp = 0.0; + int tempCount = 0; + + for (Water hint : hintFeed) { - waterPh.setHint(String.valueOf(hintFeed.getPh())); + if (hint.getPh() != null) + { + averagePh += hint.getPh(); + phCount++; + } + + if (hint.getPpm() != null) + { + averagePpm += hint.getPpm(); + ppmCount++; + } + + if (hint.getRunoff() != null) + { + averageRunoff += hint.getRunoff(); + runoffCount++; + } + + if (hint.getAmount() != null) + { + averageAmount += hint.getAmount(); + amountCount++; + } + + if (hint.getTemp() != null) + { + averageTemp += hint.getTemp(); + tempCount++; + } } - if (hintFeed.getPpm() != null) + averagePh = averagePh / phCount; + averagePpm = averagePpm / ppmCount; + averageRunoff = averageRunoff / runoffCount; + averageAmount = averageAmount / amountCount; + averageTemp = averageTemp / tempCount; + + if (!averagePh.isNaN()) { - waterPpm.setHint(String.valueOf(hintFeed.getPpm())); + waterPh.setHint(String.valueOf(averagePh)); } - if (hintFeed.getRunoff() != null) + if (!averagePpm.isNaN()) { - runoffPh.setHint(String.valueOf(hintFeed.getRunoff())); + waterPpm.setHint(String.valueOf(averagePpm)); } - if (hintFeed.getAmount() != null) + if (!averageRunoff.isNaN()) { - amount.setHint(String.valueOf(ML.to(selectedDeliveryUnit, hintFeed.getAmount())) + selectedDeliveryUnit.getLabel()); + runoffPh.setHint(String.valueOf(averageRunoff)); } - if (plants.get(0).getMedium() == PlantMedium.HYDRO || plants.get(0).getMedium() == PlantMedium.AERO) + if (!averageAmount.isNaN()) { - tempContainer.setVisibility(View.VISIBLE); - tempLabel.setText("Temp (º" + selectedTemperatureUnit.getLabel() + ")"); + amount.setHint(String.valueOf(ML.to(selectedDeliveryUnit, averageAmount)) + selectedDeliveryUnit.getLabel()); + } - if (hintFeed.getTemp() != null) - { - temp.setHint(String.valueOf(CELCIUS.to(selectedTemperatureUnit, hintFeed.getTemp())) + selectedTemperatureUnit.getLabel()); - } + tempContainer.setVisibility(View.VISIBLE); + tempLabel.setText("Temp (º" + selectedTemperatureUnit.getLabel() + ")"); + + if (!averageTemp.isNaN()) + { + temp.setHint(String.valueOf(CELCIUS.to(selectedTemperatureUnit, averageTemp)) + selectedTemperatureUnit.getLabel()); } - notes.setHint(hintFeed.getNotes()); + notes.setHint(hintFeed.get(0).getNotes()); } } } @@ -278,39 +399,82 @@ private void setUi() amount.setText(String.valueOf(ML.to(selectedDeliveryUnit, water.getAmount()))); } - if (plants.get(0).getMedium() == PlantMedium.HYDRO) - { - tempContainer.setVisibility(View.VISIBLE); + tempContainer.setVisibility(View.VISIBLE); - if (water.getTemp() != null) - { - temp.setHint(String.valueOf(CELCIUS.to(selectedTemperatureUnit, water.getTemp())) + selectedTemperatureUnit.getLabel()); - } + if (water.getTemp() != null) + { + temp.setHint(String.valueOf(CELCIUS.to(selectedTemperatureUnit, water.getTemp())) + selectedTemperatureUnit.getLabel()); } - for (Additive additive : water.getAdditives()) - { - if (additive == null || additive.getAmount() == null) continue; + populateAdditives(); + notes.setText(water.getNotes()); + } + } + + private void populateAdditives() + { + additiveContainer.removeViews(0, additiveContainer.getChildCount() - 1); + int maxChars = 0; + + for (Additive additive : water.getAdditives()) + { + if (additive == null || additive.getAmount() == null) continue; + + double converted = Unit.ML.to(selectedMeasurementUnit, additive.getAmount()); + String amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); + amountStr = additive.getDescription() + " - " + amountStr + selectedMeasurementUnit.getLabel() + "/" + selectedDeliveryUnit.getLabel(); + maxChars = Math.max(maxChars, amountStr.length()); + } + + for (Additive additive : water.getAdditives()) + { + if (additive == null || additive.getAmount() == null) continue; - double converted = Unit.ML.to(selectedMeasurementUnit, additive.getAmount()); - String amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); + double converted = Unit.ML.to(selectedMeasurementUnit, additive.getAmount()); + String amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); - View additiveStub = LayoutInflater.from(getActivity()).inflate(R.layout.additive_stub, additiveContainer, false); - ((TextView)additiveStub).setText(additive.getDescription() + " - " + amountStr + selectedMeasurementUnit.getLabel() + "/" + selectedDeliveryUnit.getLabel()); + View additiveStub = LayoutInflater.from(getActivity()).inflate(R.layout.additive_stub, additiveContainer, false); + amountStr = additive.getDescription() + "   -   " + amountStr + selectedMeasurementUnit.getLabel() + "/" + selectedDeliveryUnit.getLabel(); - additiveStub.setTag(additive); - additiveStub.setOnClickListener(new View.OnClickListener() + Double totalDelivery = water.getAmount(); + if (totalDelivery == null || !TextUtils.isEmpty(amount.getText().toString())) + { + if (totalDelivery == null || totalDelivery != Double.parseDouble(amount.getText().toString())) { - @Override public void onClick(View view) + try { - onNewAdditiveClick(view); + totalDelivery = selectedDeliveryUnit.to(Unit.ML, Double.parseDouble(amount.getText().toString())); } - }); - additiveContainer.addView(additiveStub, additiveContainer.getChildCount() - 1); + catch (NumberFormatException e) + { + totalDelivery = null; + } + } } - notes.setText(water.getNotes()); + if (totalDelivery != null) + { + totalDelivery = ML.to(selectedDeliveryUnit, totalDelivery); + Double additiveAmount = ML.to(selectedMeasurementUnit, additive.getAmount()); + + amountStr = amountStr + "  (" + Unit.toTwoDecimalPlaces(additiveAmount * totalDelivery) + selectedMeasurementUnit.getLabel() + " total)"; + } + + ((TextView)additiveStub).setText(Html.fromHtml(amountStr)); + + additiveStub.setTag(additive); + additiveStub.setOnClickListener(new View.OnClickListener() + { + @Override public void onClick(View view) + { + onNewAdditiveClick(view); + } + }); + additiveContainer.addView(additiveStub, additiveContainer.getChildCount() - 1); } + + amount.removeTextChangedListener(deliveryTextChangeListener); + amount.addTextChangedListener(deliveryTextChangeListener); } @Views.OnClick public void onNewAdditiveClick(View view) @@ -320,6 +484,7 @@ private void setUi() getActivity().getCurrentFocus().clearFocus(); } + final View focus = getActivity().getCurrentFocus(); final Object currentTag = view.getTag(); FragmentManager fm = getFragmentManager(); AddAdditiveDialogFragment addAdditiveDialogFragment = new AddAdditiveDialogFragment(view.getTag() instanceof Additive ? (Additive)view.getTag() : null); @@ -332,28 +497,9 @@ private void setUi() return; } - double converted = Unit.ML.to(selectedMeasurementUnit, additive.getAmount()); - String amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); - - View additiveStub = LayoutInflater.from(getActivity()).inflate(R.layout.additive_stub, additiveContainer, false); - ((TextView)additiveStub).setText(additive.getDescription() + " - " + amountStr + selectedMeasurementUnit.getLabel() + "/" + selectedDeliveryUnit.getLabel()); - - if (currentTag == null) + if (!water.getAdditives().contains(additive)) { - if (!water.getAdditives().contains(additive)) - { - water.getAdditives().add(additive); - - additiveStub.setTag(additive); - additiveStub.setOnClickListener(new View.OnClickListener() - { - @Override public void onClick(View view) - { - onNewAdditiveClick(view); - } - }); - additiveContainer.addView(additiveStub, additiveContainer.getChildCount() - 1); - } + water.getAdditives().add(additive); } else { @@ -363,29 +509,24 @@ private void setUi() if (tag == currentTag) { - converted = Unit.ML.to(selectedMeasurementUnit, additive.getAmount()); - amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); - water.getAdditives().set(childIndex, additive); - - ((TextView)additiveContainer.getChildAt(childIndex)).setText(additive.getDescription() + " - " + amountStr + selectedMeasurementUnit.getLabel() + "/" + selectedDeliveryUnit.getLabel()); - additiveContainer.getChildAt(childIndex).setTag(additive); - break; } } } - additiveStub.requestFocus(); - additiveStub.requestFocusFromTouch(); + populateAdditives(); + + if (focus != null) + { + focus.requestFocus(); + focus.requestFocusFromTouch(); + } } @Override public void onAdditiveDeleteRequested(Additive additive) { - if (water.getAdditives().contains(additive)) - { - water.getAdditives().remove(additive); - } + water.getAdditives().remove(additive); for (int childIndex = 0; childIndex < additiveContainer.getChildCount(); childIndex++) { @@ -398,8 +539,11 @@ private void setUi() } } - additiveContainer.getChildAt(additiveContainer.getChildCount() - 1).requestFocus(); - additiveContainer.getChildAt(additiveContainer.getChildCount() - 1).requestFocusFromTouch(); + if (focus != null) + { + focus.requestFocus(); + focus.requestFocusFromTouch(); + } } }); diff --git a/app/src/main/java/me/anon/lib/ClassExt.kt b/app/src/main/java/me/anon/lib/ClassExt.kt new file mode 100644 index 00000000..a724d782 --- /dev/null +++ b/app/src/main/java/me/anon/lib/ClassExt.kt @@ -0,0 +1,32 @@ +package me.anon.lib + +import android.app.Activity +import com.kenny.snackbar.SnackBar +import com.kenny.snackbar.SnackBarListener + +fun SnackBar.show( + context: Activity, + message: Int, action: Int, + startListener: () -> kotlin.Unit, + endListener: () -> kotlin.Unit, + actionListener: () -> kotlin.Unit +) +{ + SnackBar.show(context, message, action, object : SnackBarListener + { + override fun onSnackBarStarted(`object`: Any?) + { + startListener.invoke() + } + + override fun onSnackBarAction(`object`: Any?) + { + actionListener.invoke() + } + + override fun onSnackBarFinished(`object`: Any?) + { + endListener.invoke() + } + }) +} diff --git a/app/src/main/java/me/anon/lib/TempUnit.java b/app/src/main/java/me/anon/lib/TempUnit.java index ab63e6bb..7ab4bd1d 100644 --- a/app/src/main/java/me/anon/lib/TempUnit.java +++ b/app/src/main/java/me/anon/lib/TempUnit.java @@ -6,13 +6,9 @@ import java.math.BigDecimal; import java.math.RoundingMode; -import lombok.AllArgsConstructor; -import lombok.Getter; - /** * Unit class used for measurement input */ -@AllArgsConstructor public enum TempUnit { KELVIN("K") @@ -55,7 +51,17 @@ public enum TempUnit } }; - @Getter private String label; + private String label; + + private TempUnit(String label) + { + this.label = label; + } + + public String getLabel() + { + return label; + } private static Double toTwoDecimalPlaces(double input) { diff --git a/app/src/main/java/me/anon/lib/Unit.java b/app/src/main/java/me/anon/lib/Unit.java index 80e82217..cab6b05a 100644 --- a/app/src/main/java/me/anon/lib/Unit.java +++ b/app/src/main/java/me/anon/lib/Unit.java @@ -6,13 +6,9 @@ import java.math.BigDecimal; import java.math.RoundingMode; -import lombok.AllArgsConstructor; -import lombok.Getter; - /** * Unit class used for measurement input */ -@AllArgsConstructor public enum Unit { ML("ml") @@ -167,9 +163,19 @@ public enum Unit } }; - @Getter private String label; + private String label; + + private Unit(String label) + { + this.label = label; + } + + public String getLabel() + { + return label; + } - private static Double toTwoDecimalPlaces(double input) + public static Double toTwoDecimalPlaces(double input) { return new BigDecimal(input).setScale(2, RoundingMode.HALF_EVEN).doubleValue(); } diff --git a/app/src/main/java/me/anon/lib/handler/ExceptionHandler.java b/app/src/main/java/me/anon/lib/handler/ExceptionHandler.java index b714fb58..33c28554 100755 --- a/app/src/main/java/me/anon/lib/handler/ExceptionHandler.java +++ b/app/src/main/java/me/anon/lib/handler/ExceptionHandler.java @@ -8,15 +8,13 @@ import java.io.File; import java.io.FilenameFilter; -import lombok.Getter; - public class ExceptionHandler { public static String VERSION = ""; public static String VERSION_CODE = ""; public static String PACKAGE_NAME = ""; - @Getter private String filesPath = "/"; + private String filesPath = "/"; private String[] stackTraceFileList = null; private static ExceptionHandler instance; @@ -36,6 +34,12 @@ public static ExceptionHandler getInstance() return instance; } + + public String getFilesPath() + { + return filesPath; + } + /** * @param context */ diff --git a/app/src/main/java/me/anon/lib/helper/BackupHelper.kt b/app/src/main/java/me/anon/lib/helper/BackupHelper.kt new file mode 100644 index 00000000..6c8abb5c --- /dev/null +++ b/app/src/main/java/me/anon/lib/helper/BackupHelper.kt @@ -0,0 +1,28 @@ +package me.anon.lib.helper + +import android.os.Environment +import me.anon.lib.manager.FileManager +import me.anon.lib.manager.PlantManager +import java.io.File + +/** + * Helper class for backing up data files + */ +object BackupHelper +{ + @JvmField + public var FILES_PATH = Environment.getExternalStorageDirectory().absolutePath + "/backups/GrowTracker" + + @JvmStatic + public fun backupJson(): File + { + val time = System.currentTimeMillis() + val backupPath = File(FILES_PATH) + backupPath.mkdirs() + FileManager.getInstance().copyFile("${PlantManager.FILES_DIR}/plants.json", "$FILES_PATH/$time.plants.json.bak") + FileManager.getInstance().copyFile("${PlantManager.FILES_DIR}/schedules.json", "$FILES_PATH/$time.schedules.json.bak") + FileManager.getInstance().copyFile("${PlantManager.FILES_DIR}/gardens.json", "$FILES_PATH/$time.gardens.json.bak") + + return backupPath + } +} diff --git a/app/src/main/java/me/anon/lib/helper/ExportHelper.java b/app/src/main/java/me/anon/lib/helper/ExportHelper.java index 478ffba9..60e2739d 100644 --- a/app/src/main/java/me/anon/lib/helper/ExportHelper.java +++ b/app/src/main/java/me/anon/lib/helper/ExportHelper.java @@ -1,11 +1,13 @@ package me.anon.lib.helper; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.AsyncTask; +import android.os.Build; import android.os.Environment; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -40,7 +42,6 @@ import me.anon.model.EmptyAction; import me.anon.model.NoteAction; import me.anon.model.Plant; -import me.anon.model.PlantMedium; import me.anon.model.PlantStage; import me.anon.model.StageChange; import me.anon.model.Water; @@ -186,17 +187,14 @@ public class ExportHelper plantDetails.append(" - *Average input ppm*: ").append(avePpm[2]); plantDetails.append(NEW_LINE); - if (plant.getMedium() == PlantMedium.HYDRO) - { - String[] aveTemp = new String[3]; - StatsHelper.setTempData(plant, null, aveTemp); - plantDetails.append(" - *Minimum input temperature*: ").append(aveTemp[0]); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Maximum input temperature*: ").append(aveTemp[1]); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Average input temperature*: ").append(aveTemp[2]); - plantDetails.append(NEW_LINE); - } + String[] aveTemp = new String[3]; + StatsHelper.setTempData(plant, null, aveTemp); + plantDetails.append(" - *Minimum input temperature*: ").append(aveTemp[0]); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Maximum input temperature*: ").append(aveTemp[1]); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Average input temperature*: ").append(aveTemp[2]); + plantDetails.append(NEW_LINE); plantDetails.append("##Timeline"); plantDetails.append(NEW_LINE); @@ -355,6 +353,28 @@ else if (action instanceof StageChange) int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + LineChart additives = new LineChart(context); + additives.setLayoutParams(new ViewGroup.LayoutParams(width, height)); + additives.setMinimumWidth(width); + additives.setMinimumHeight(height); + additives.measure(widthMeasureSpec, heightMeasureSpec); + additives.requestLayout(); + additives.layout(0, 0, width, height); + StatsHelper.setAdditiveData(plant, additives, null); + additives.getData().setDrawValues(true); + + try + { + OutputStream stream = new FileOutputStream(tempFolder.getAbsolutePath() + "/additives.png"); + additives.getChartBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream); + + stream.close(); + } + catch (Exception e) + { + e.printStackTrace(); + } + LineChart inputPh = new LineChart(context); inputPh.setLayoutParams(new ViewGroup.LayoutParams(width, height)); inputPh.setMinimumWidth(width); @@ -363,6 +383,7 @@ else if (action instanceof StageChange) inputPh.requestLayout(); inputPh.layout(0, 0, width, height); StatsHelper.setInputData(plant, inputPh, null); + inputPh.getData().setDrawValues(true); try { @@ -384,6 +405,7 @@ else if (action instanceof StageChange) ppm.requestLayout(); ppm.layout(0, 0, width, height); StatsHelper.setPpmData(plant, ppm, null); + ppm.getData().setDrawValues(true); try { @@ -397,32 +419,35 @@ else if (action instanceof StageChange) e.printStackTrace(); } - if (plant.getMedium() == PlantMedium.HYDRO) + LineChart temp = new LineChart(context); + temp.setLayoutParams(new ViewGroup.LayoutParams(width, height)); + temp.setMinimumWidth(width); + temp.setMinimumHeight(height); + temp.measure(widthMeasureSpec, heightMeasureSpec); + temp.requestLayout(); + temp.layout(0, 0, width, height); + StatsHelper.setTempData(plant, temp, null); + temp.getData().setDrawValues(true); + + try { - LineChart temp = new LineChart(context); - temp.setLayoutParams(new ViewGroup.LayoutParams(width, height)); - temp.setMinimumWidth(width); - temp.setMinimumHeight(height); - temp.measure(widthMeasureSpec, heightMeasureSpec); - temp.requestLayout(); - temp.layout(0, 0, width, height); - StatsHelper.setTempData(plant, temp, null); - - try - { - OutputStream stream = new FileOutputStream(tempFolder.getAbsolutePath() + "/temp.png"); - temp.getChartBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream); + OutputStream stream = new FileOutputStream(tempFolder.getAbsolutePath() + "/temp.png"); + temp.getChartBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream); - stream.close(); - } - catch (Exception e) - { - e.printStackTrace(); - } + stream.close(); + } + catch (Exception e) + { + e.printStackTrace(); } try { + if (new File(tempFolder.getAbsolutePath() + "/additives.png").exists()) + { + outFile.addFile(new File(tempFolder.getAbsolutePath() + "/additives.png"), params); + } + if (new File(tempFolder.getAbsolutePath() + "/input-ph.png").exists()) { outFile.addFile(new File(tempFolder.getAbsolutePath() + "/input-ph.png"), params); @@ -465,7 +490,15 @@ protected static void copyImagesAndFinish(Context context, final Plant plant, fi plantIndex = PlantManager.getInstance().getPlants().indexOf(plant); notificationManager = (NotificationManager)appContext.getSystemService(Context.NOTIFICATION_SERVICE); - exportNotification = new NotificationCompat.Builder(appContext) + if (Build.VERSION.SDK_INT >= 26) + { + NotificationChannel channel = new NotificationChannel("export", "Export status", NotificationManager.IMPORTANCE_DEFAULT); + notificationManager.createNotificationChannel(channel); + } + + notificationManager = (NotificationManager)appContext.getSystemService(Context.NOTIFICATION_SERVICE); + + exportNotification = new NotificationCompat.Builder(appContext, "export") .setContentText("Exporting grow log for " + plant.getName()) .setContentTitle("Exporting") .setContentIntent(PendingIntent.getActivity(appContext, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT)) @@ -515,8 +548,6 @@ protected static void copyImagesAndFinish(Context context, final Plant plant, fi fos.write(buffer, 0, len); } - publishProgress(++count, total); - fis.close(); fos.flush(); fos.close(); @@ -525,6 +556,8 @@ protected static void copyImagesAndFinish(Context context, final Plant plant, fi { e.printStackTrace(); } + + publishProgress(++count, total); } deleteRecursive(tempFolder); diff --git a/app/src/main/java/me/anon/lib/helper/StatsHelper.java b/app/src/main/java/me/anon/lib/helper/StatsHelper.java index 3dbb9ec6..28b3e1e2 100644 --- a/app/src/main/java/me/anon/lib/helper/StatsHelper.java +++ b/app/src/main/java/me/anon/lib/helper/StatsHelper.java @@ -1,15 +1,35 @@ package me.anon.lib.helper; +import android.graphics.Color; +import android.support.v4.graphics.ColorUtils; +import android.support.v4.util.Pair; +import android.widget.TextView; + import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.MarkerView; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; -import com.github.mikephil.charting.utils.ValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; +import com.github.mikephil.charting.formatter.YAxisValueFormatter; +import com.github.mikephil.charting.highlight.Highlight; +import com.github.mikephil.charting.utils.ViewPortHandler; import java.util.ArrayList; - +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Random; +import java.util.Set; + +import me.anon.grow.R; import me.anon.model.Action; +import me.anon.model.Additive; import me.anon.model.Plant; +import me.anon.model.PlantStage; import me.anon.model.Water; /** @@ -17,14 +37,174 @@ */ public class StatsHelper { - private static ValueFormatter formatter = new ValueFormatter() + public static ValueFormatter formatter = new ValueFormatter() { - @Override public String getFormattedValue(float value) + @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { return String.format("%.2f", value); } }; + public static void styleGraph(LineChart chart) + { + chart.setDrawGridBackground(false); + chart.setGridBackgroundColor(0x00ffffff); + chart.getAxisLeft().setDrawGridLines(false); + chart.getXAxis().setDrawGridLines(false); + chart.getLegend().setTextColor(0xff666666); + chart.getLegend().setTextSize(12f); + chart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); + chart.getXAxis().setTextColor(0xff666666); + chart.getXAxis().setTextSize(12f); + chart.getAxisRight().setEnabled(false); + chart.getAxisLeft().setTextColor(0xff666666); + chart.getAxisLeft().setTextSize(12f); + chart.getAxisLeft().setValueFormatter(new YAxisValueFormatter() + { + @Override public String getFormattedValue(float value, YAxis yAxis) + { + if (value == (int)value) + { + return String.format("%s", value); + } + + return String.format("%.2f", value); + } + }); + chart.getAxisLeft().setStartAtZero(false); + chart.setScaleYEnabled(false); + chart.setDescription(""); + chart.getAxisLeft().setXOffset(8.0f); + chart.getLegend().setWordWrapEnabled(true); + chart.setTouchEnabled(true); + chart.setHighlightPerTapEnabled(true); + chart.setMarkerView(new MarkerView(chart.getContext(), R.layout.chart_marker) + { + @Override + public void refreshContent(Entry e, Highlight highlight) + { + ((TextView)findViewById(R.id.content)).setText("" + e.getVal()); + } + + @Override public int getXOffset(float xpos) + { + return -(getWidth() / 2); + } + + @Override public int getYOffset(float ypos) + { + return -getHeight(); + } + }); + } + + public static void styleDataset(LineDataSet data, int colour) + { + data.setDrawCubic(true); + data.setLineWidth(2.0f); + data.setDrawCircleHole(true); + data.setColor(colour); + data.setCircleColor(colour); + data.setCircleSize(4.0f); + data.setDrawHighlightIndicators(true); + data.setHighlightEnabled(true); + data.setHighlightLineWidth(2f); + data.setHighLightColor(ColorUtils.setAlphaComponent(colour, 96)); + data.setDrawValues(false); + data.setValueFormatter(formatter); + } + + public static void setAdditiveData(Plant plant, LineChart chart, Set checkedAdditives) + { + ArrayList actions = plant.getActions(); + ArrayList>> vals = new ArrayList<>(); + ArrayList xVals = new ArrayList<>(); + final Set additiveNames = new HashSet<>(); + LineData data = new LineData(); + LinkedHashMap stageTimes = plant.getStages(); + double min = Double.MAX_VALUE; + double max = Double.MIN_VALUE; + + for (Action action : actions) + { + if (action instanceof Water) + { + List actionAdditives = ((Water)action).getAdditives(); + for (Additive additive : actionAdditives) + { + additiveNames.add(additive.getDescription()); + min = Math.min(min, additive.getAmount()); + max = Math.max(max, additive.getAmount()); + } + + PlantStage stage = null; + long changeDate = 0; + ListIterator iterator = new ArrayList(stageTimes.keySet()).listIterator(stageTimes.size()); + while (iterator.hasPrevious()) + { + PlantStage key = iterator.previous(); + Action changeAction = stageTimes.get(key); + if (action.getDate() > changeAction.getDate()) + { + stage = key; + changeDate = changeAction.getDate(); + } + } + + long difference = action.getDate() - changeDate; + if (stage != null) + { + xVals.add(((int)TimeHelper.toDays(difference) + "" + stage.getPrintString().charAt(0)).toLowerCase()); + } + else + { + xVals.add(""); + } + } + } + + ArrayList dataSets = new ArrayList<>(); + for (String additiveName : checkedAdditives) + { + int index = 0; + ArrayList additiveValues = new ArrayList<>(); + for (Action action : actions) + { + if (action instanceof Water) + { + boolean found = false; + for (Additive additive : ((Water)action).getAdditives()) + { + if (additiveName.equals(additive.getDescription())) + { + found = true; + additiveValues.add(new Entry(additive.getAmount().floatValue(), index)); + } + } + + index++; + } + } + + LineDataSet dataSet = new LineDataSet(additiveValues, additiveName); + + String[] colours = chart.getResources().getStringArray(R.array.stats_colours); + ArrayList namesList = new ArrayList<>(additiveNames); + StatsHelper.styleDataset(dataSet, dataSets.size() < colours.length ? Color.parseColor(colours[namesList.indexOf(additiveName)]) : (additiveName.hashCode() + new Random().nextInt(0xffffff))); + + dataSet.setValueFormatter(StatsHelper.formatter); + dataSets.add(dataSet); + } + + LineData lineData = new LineData(xVals, dataSets); + lineData.setValueFormatter(StatsHelper.formatter); + lineData.setValueTextColor(0xff666666); + + styleGraph(chart); + + chart.setData(lineData); + } + /** * Generates and sets the input watering data from the given plant * @@ -39,6 +219,7 @@ public static void setInputData(Plant plant, LineChart chart, String[] additiona ArrayList averageVals = new ArrayList<>(); ArrayList xVals = new ArrayList<>(); LineData data = new LineData(); + LinkedHashMap stageTimes = plant.getStages(); float min = Float.MAX_VALUE; float max = Float.MIN_VALUE; float totalIn = 0; @@ -68,14 +249,28 @@ public static void setInputData(Plant plant, LineChart chart, String[] additiona totalOut += ((Water)action).getRunoff().floatValue(); } - xVals.add(""); + PlantStage stage = null; + long changeDate = 0; + ListIterator iterator = new ArrayList(stageTimes.keySet()).listIterator(stageTimes.size()); + while (iterator.hasPrevious()) + { + PlantStage key = iterator.previous(); + Action changeAction = stageTimes.get(key); + if (action.getDate() > changeAction.getDate()) + { + stage = key; + changeDate = changeAction.getDate(); + } + } - float aveIn = totalIn; - float aveOut = totalOut; - if (index > 0) + long difference = action.getDate() - changeDate; + if (stage != null) { - aveIn /= (float)index; - aveOut /= (float)index; + xVals.add(((int)TimeHelper.toDays(difference) + "" + stage.getPrintString().charAt(0)).toLowerCase()); + } + else + { + xVals.add(""); } index++; @@ -85,33 +280,13 @@ public static void setInputData(Plant plant, LineChart chart, String[] additiona if (chart != null) { LineDataSet dataSet = new LineDataSet(inputVals, "Input PH"); - dataSet.setDrawCubic(true); - dataSet.setLineWidth(1.0f); - dataSet.setDrawCircleHole(false); - dataSet.setCircleColor(0xffffffff); - dataSet.setValueTextColor(0xffffffff); - dataSet.setCircleSize(2.0f); - dataSet.setValueTextSize(8.0f); - dataSet.setValueFormatter(formatter); + styleDataset(dataSet, Color.parseColor(chart.getContext().getResources().getStringArray(R.array.stats_colours)[0])); LineDataSet runoffDataSet = new LineDataSet(runoffVals, "Runoff PH"); - runoffDataSet.setDrawCubic(true); - runoffDataSet.setLineWidth(1.0f); - runoffDataSet.setDrawCircleHole(false); - runoffDataSet.setColor(0xffFFF9C4); - runoffDataSet.setCircleColor(0xffFFF9C4); - runoffDataSet.setValueTextColor(0xffFFF9C4); - runoffDataSet.setCircleSize(2.0f); - runoffDataSet.setValueTextSize(8.0f); - runoffDataSet.setValueFormatter(formatter); + styleDataset(dataSet, Color.parseColor(chart.getContext().getResources().getStringArray(R.array.stats_colours)[1])); LineDataSet averageDataSet = new LineDataSet(averageVals, "Average PH"); - averageDataSet.setDrawCubic(true); - averageDataSet.setLineWidth(1.0f); - averageDataSet.setDrawCircleHole(false); - averageDataSet.setColor(0xffffffff); - averageDataSet.setCircleSize(0.0f); - averageDataSet.setValueTextSize(0.0f); + styleDataset(dataSet, Color.parseColor(chart.getContext().getResources().getStringArray(R.array.stats_colours)[2])); averageDataSet.setValueFormatter(null); ArrayList dataSets = new ArrayList(); @@ -121,21 +296,9 @@ public static void setInputData(Plant plant, LineChart chart, String[] additiona LineData lineData = new LineData(xVals, dataSets); lineData.setValueFormatter(formatter); - chart.setBackgroundColor(0xff006064); - chart.setGridBackgroundColor(0xff006064); - chart.setDrawGridBackground(false); - chart.setHighlightEnabled(false); - chart.getLegend().setTextColor(0xffffffff); - chart.getAxisLeft().setTextColor(0xffffffff); - chart.getAxisRight().setEnabled(false); - chart.getAxisLeft().setValueFormatter(formatter); + styleGraph(chart); chart.getAxisLeft().setAxisMinValue(min - 0.5f); chart.getAxisLeft().setAxisMaxValue(max + 0.5f); - chart.getAxisLeft().setStartAtZero(false); - chart.setScaleYEnabled(false); - chart.setDescription(""); - chart.setPinchZoom(false); - chart.setDoubleTapToZoomEnabled(false); chart.setData(lineData); } @@ -144,7 +307,7 @@ public static void setInputData(Plant plant, LineChart chart, String[] additiona { additionalRef[0] = String.valueOf(min); additionalRef[1] = String.valueOf(max); - additionalRef[2] = String.format("%1$,.2f", (totalIn / (double)index)); + additionalRef[2] = String.format("%1$,.2f", (totalIn / (double)inputVals.size())); } } @@ -160,10 +323,11 @@ public static void setPpmData(Plant plant, LineChart chart, String[] additionalR ArrayList vals = new ArrayList<>(); ArrayList xVals = new ArrayList<>(); LineData data = new LineData(); + LinkedHashMap stageTimes = plant.getStages(); long min = Long.MAX_VALUE; long max = Long.MIN_VALUE; - long ave = 0; + long total = 0; int index = 0; for (Action action : plant.getActions()) @@ -171,38 +335,90 @@ public static void setPpmData(Plant plant, LineChart chart, String[] additionalR if (action instanceof Water && ((Water)action).getPpm() != null) { vals.add(new Entry(((Water)action).getPpm().floatValue(), index++)); - xVals.add(""); + PlantStage stage = null; + long changeDate = 0; + ListIterator iterator = new ArrayList(stageTimes.keySet()).listIterator(stageTimes.size()); + while (iterator.hasPrevious()) + { + PlantStage key = iterator.previous(); + Action changeAction = stageTimes.get(key); + if (action.getDate() > changeAction.getDate()) + { + stage = key; + changeDate = changeAction.getDate(); + } + } + + long difference = action.getDate() - changeDate; + if (stage != null) + { + xVals.add(((int)TimeHelper.toDays(difference) + "" + stage.getPrintString().charAt(0)).toLowerCase()); + } + else + { + xVals.add(""); + } min = Math.min(min, ((Water)action).getPpm().longValue()); max = Math.max(max, ((Water)action).getPpm().longValue()); - ave += ((Water)action).getPpm(); + total += ((Water)action).getPpm(); } } if (chart != null) { LineDataSet dataSet = new LineDataSet(vals, "PPM"); - dataSet.setDrawCubic(true); - dataSet.setLineWidth(1.0f); - dataSet.setDrawCircleHole(false); - dataSet.setCircleColor(0xffffffff); - dataSet.setValueTextColor(0xffffffff); - dataSet.setCircleSize(2.0f); - dataSet.setValueTextSize(8.0f); - dataSet.setColor(0xffA7FFEB); - - chart.setBackgroundColor(0xff1B5E20); - chart.setGridBackgroundColor(0xff1B5E20); - chart.setDrawGridBackground(false); - chart.setHighlightEnabled(false); - chart.getLegend().setEnabled(false); - chart.getAxisLeft().setTextColor(0xffffffff); - chart.getAxisRight().setEnabled(false); - chart.getAxisLeft().setXOffset(8.0f); - chart.setScaleYEnabled(false); - chart.setDescription(""); - chart.setPinchZoom(false); - chart.setDoubleTapToZoomEnabled(false); + styleDataset(dataSet, Color.parseColor(chart.getContext().getResources().getStringArray(R.array.stats_colours)[0])); + styleGraph(chart); + chart.setMarkerView(new MarkerView(chart.getContext(), R.layout.chart_marker) + { + @Override + public void refreshContent(Entry e, Highlight highlight) + { + String val = String.format("%.2f", e.getVal()); + if (e.getVal() == (int)e.getVal()) + { + val = String.format("%s", (int)e.getVal()); + } + + ((TextView)findViewById(R.id.content)).setText(val); + } + + @Override public int getXOffset(float xpos) + { + return -(getWidth() / 2); + } + + @Override public int getYOffset(float ypos) + { + return -getHeight(); + } + }); + chart.getAxisLeft().setValueFormatter(new YAxisValueFormatter() + { + @Override public String getFormattedValue(float value, YAxis yAxis) + { + if (value == (int)value) + { + return String.format("%s", (int)value); + } + + return String.format("%.2f", value); + } + }); + dataSet.setValueFormatter(new ValueFormatter() + { + @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) + { + if (value == (int)value) + { + return String.format("%s", (int)value); + } + + return String.format("%.2f", value); + } + }); + chart.setData(new LineData(xVals, dataSet)); } @@ -210,7 +426,7 @@ public static void setPpmData(Plant plant, LineChart chart, String[] additionalR { additionalRef[0] = String.valueOf(min); additionalRef[1] = String.valueOf(max); - additionalRef[2] = String.format("%1$,.2f", (ave / (double)index)); + additionalRef[2] = String.format("%1$,.2f", (total / (double)vals.size())); } } @@ -226,9 +442,10 @@ public static void setTempData(Plant plant, LineChart chart, String[] additional ArrayList vals = new ArrayList<>(); ArrayList xVals = new ArrayList<>(); LineData data = new LineData(); - float min = -100f; - float max = 100f; - float ave = 0; + LinkedHashMap stageTimes = plant.getStages(); + float min = 100f; + float max = -100f; + float total = 0; int index = 0; for (Action action : plant.getActions()) @@ -236,45 +453,47 @@ public static void setTempData(Plant plant, LineChart chart, String[] additional if (action instanceof Water && ((Water)action).getTemp() != null) { vals.add(new Entry(((Water)action).getTemp().floatValue(), index++)); - xVals.add(""); + PlantStage stage = null; + long changeDate = 0; + ListIterator iterator = new ArrayList(stageTimes.keySet()).listIterator(stageTimes.size()); + while (iterator.hasPrevious()) + { + PlantStage key = iterator.previous(); + Action changeAction = stageTimes.get(key); + if (action.getDate() > changeAction.getDate()) + { + stage = key; + changeDate = changeAction.getDate(); + } + } + + long difference = action.getDate() - changeDate; + if (stage != null) + { + xVals.add(((int)TimeHelper.toDays(difference) + "" + stage.getPrintString().charAt(0)).toLowerCase()); + } + else + { + xVals.add(""); + } min = Math.min(min, ((Water)action).getTemp().floatValue()); max = Math.max(max, ((Water)action).getTemp().floatValue()); - ave += ((Water)action).getTemp().floatValue(); + total += ((Water)action).getTemp().floatValue(); } } if (chart != null) { LineDataSet dataSet = new LineDataSet(vals, "Temperature"); - dataSet.setDrawCubic(true); - dataSet.setLineWidth(1.0f); - dataSet.setDrawCircleHole(false); - dataSet.setCircleColor(0xffffffff); - dataSet.setValueTextColor(0xffffffff); - dataSet.setCircleSize(2.0f); - dataSet.setValueTextSize(8.0f); - dataSet.setValueFormatter(formatter); + styleDataset(dataSet, Color.parseColor(chart.getContext().getResources().getStringArray(R.array.stats_colours)[0])); LineData lineData = new LineData(xVals, dataSet); lineData.setValueFormatter(formatter); - chart.setBackgroundColor(0xff311B92); - chart.setGridBackgroundColor(0xff311B92); - chart.setDrawGridBackground(false); - chart.setHighlightEnabled(false); - chart.getLegend().setEnabled(false); - chart.getAxisLeft().setTextColor(0xffffffff); - chart.getAxisRight().setEnabled(false); - chart.getAxisLeft().setValueFormatter(formatter); - chart.getAxisLeft().setXOffset(8.0f); chart.getAxisLeft().setAxisMinValue(min - 5f); chart.getAxisLeft().setAxisMaxValue(max + 5f); - chart.getAxisLeft().setStartAtZero(false); - chart.setScaleYEnabled(false); - chart.setDescription(""); - chart.setPinchZoom(false); - chart.setDoubleTapToZoomEnabled(false); + styleGraph(chart); chart.setData(lineData); } @@ -282,7 +501,7 @@ public static void setTempData(Plant plant, LineChart chart, String[] additional { additionalRef[0] = String.valueOf(min); additionalRef[1] = String.valueOf(max); - additionalRef[2] = String.format("%1$,.2f", (ave / (double)index)); + additionalRef[2] = String.format("%1$,.2f", (total / (double)vals.size())); } } } diff --git a/app/src/main/java/me/anon/lib/manager/GardenManager.java b/app/src/main/java/me/anon/lib/manager/GardenManager.java index d982faf2..552e8615 100644 --- a/app/src/main/java/me/anon/lib/manager/GardenManager.java +++ b/app/src/main/java/me/anon/lib/manager/GardenManager.java @@ -11,9 +11,6 @@ import java.io.File; import java.util.ArrayList; -import lombok.Data; -import lombok.Getter; -import lombok.experimental.Accessors; import me.anon.grow.MainApplication; import me.anon.lib.helper.EncryptionHelper; import me.anon.lib.helper.GsonHelper; @@ -26,19 +23,27 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Data -@Accessors(prefix = {"m", ""}, chain = true) public class GardenManager { - @Getter(lazy = true) private static final GardenManager instance = new GardenManager(); + private static final GardenManager instance = new GardenManager(); - private static String FILES_DIR; + public static GardenManager getInstance() + { + return instance; + } + + public static String FILES_DIR; private final ArrayList mGardens = new ArrayList<>(); private Context context; private GardenManager(){} + public ArrayList getGardens() + { + return mGardens; + } + public void initialise(Context context) { this.context = context.getApplicationContext(); diff --git a/app/src/main/java/me/anon/lib/manager/PlantManager.java b/app/src/main/java/me/anon/lib/manager/PlantManager.java index d0d1061a..72191dd9 100644 --- a/app/src/main/java/me/anon/lib/manager/PlantManager.java +++ b/app/src/main/java/me/anon/lib/manager/PlantManager.java @@ -26,12 +26,10 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; -import lombok.Data; -import lombok.Getter; -import lombok.experimental.Accessors; import me.anon.grow.BootActivity; import me.anon.grow.MainApplication; import me.anon.lib.helper.AddonHelper; +import me.anon.lib.helper.BackupHelper; import me.anon.lib.helper.GsonHelper; import me.anon.lib.stream.DecryptInputStream; import me.anon.lib.stream.EncryptOutputStream; @@ -46,11 +44,14 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Data -@Accessors(prefix = {"m", ""}, chain = true) public class PlantManager { - @Getter(lazy = true) private static final PlantManager instance = new PlantManager(); + private static final PlantManager instance = new PlantManager(); + + public static PlantManager getInstance() + { + return instance; + } public static String FILES_DIR; @@ -59,6 +60,11 @@ public class PlantManager private PlantManager(){} + public ArrayList getPlants() + { + return mPlants; + } + public void initialise(Context context) { this.context = context.getApplicationContext(); @@ -176,11 +182,11 @@ public void upsert(int index, Plant plant) } } - public void load() + public boolean load() { if (MainApplication.isFailsafe()) { - return; + return false; } // redundancy check @@ -199,26 +205,30 @@ public void load() { if (TextUtils.isEmpty(MainApplication.getKey())) { - return; + return false; } DecryptInputStream stream = new DecryptInputStream(MainApplication.getKey(), new File(FILES_DIR, "/plants.json")); mPlants.clear(); mPlants.addAll((ArrayList)GsonHelper.parse(stream, new TypeToken>(){}.getType())); + MainApplication.setFailsafe(false); } else { mPlants.clear(); mPlants.addAll((ArrayList)GsonHelper.parse(new FileInputStream(new File(FILES_DIR, "/plants.json")), new TypeToken>(){}.getType())); + MainApplication.setFailsafe(false); } + + return true; } catch (final JsonSyntaxException e) { e.printStackTrace(); - FileManager.getInstance().copyFile(FILES_DIR + "/plants.json", FILES_DIR + "/plants_" + System.currentTimeMillis() + ".json"); - Toast.makeText(context, "There is a syntax error in your app data. Your data has been backed up to '" + FILES_DIR + ". Please fix before re-opening the app.", Toast.LENGTH_LONG).show(); + File backupPath = BackupHelper.backupJson(); + Toast.makeText(context, "There is a syntax error in your app data. Your data has been backed up to " + backupPath.getPath() + ". Please fix before re-opening the app.\n" + e.getMessage(), Toast.LENGTH_LONG).show(); // prevent save MainApplication.setFailsafe(true); @@ -227,13 +237,15 @@ public void load() { e.printStackTrace(); - FileManager.getInstance().copyFile(FILES_DIR + "/plants.json", FILES_DIR + "/plants_" + System.currentTimeMillis() + ".json"); - Toast.makeText(context, "There is a problem loading your app data.", Toast.LENGTH_LONG).show(); + File backupPath = BackupHelper.backupJson(); + Toast.makeText(context, "There is a problem loading your app data. Your data has been backed up to " + backupPath.getPath(), Toast.LENGTH_LONG).show(); // prevent save MainApplication.setFailsafe(true); } } + + return false; } public void save() @@ -257,14 +269,12 @@ public void save(final AsyncCallback callback) public void save(final AsyncCallback callback, boolean ignoreCheck) { -// synchronized (mPlants) + synchronized (mPlants) { if (MainApplication.isFailsafe()) return; if ((!ignoreCheck && mPlants.size() > 0) || ignoreCheck) { - AddonHelper.broadcastPlantList(context); - saveTask.add(new SaveAsyncTask(mPlants) { @Override protected Void doInBackground(Void... params) @@ -330,6 +340,8 @@ public void save(final AsyncCallback callback, boolean ignoreCheck) { isSaving.set(false); } + + AddonHelper.broadcastPlantList(context); } }); @@ -355,7 +367,7 @@ public abstract static class SaveAsyncTask extends AsyncTask public SaveAsyncTask(List plants) { - this.plants = plants; + this.plants = new ArrayList(plants); } } } diff --git a/app/src/main/java/me/anon/lib/manager/ScheduleManager.kt b/app/src/main/java/me/anon/lib/manager/ScheduleManager.kt new file mode 100644 index 00000000..0e527d5d --- /dev/null +++ b/app/src/main/java/me/anon/lib/manager/ScheduleManager.kt @@ -0,0 +1,103 @@ +package me.anon.lib.manager + +import android.content.Context +import android.content.Intent +import android.text.TextUtils +import android.widget.Toast +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import me.anon.grow.MainApplication +import me.anon.lib.helper.EncryptionHelper +import me.anon.lib.helper.GsonHelper +import me.anon.model.FeedingSchedule +import java.io.File +import java.util.* + +class ScheduleManager private constructor() +{ + public val schedules: ArrayList = arrayListOf() + private lateinit var context: Context + + fun initialise(context: Context) + { + this.context = context.applicationContext + FILES_DIR = this.context.getExternalFilesDir(null).absolutePath + + load() + } + + fun load() + { + if (FileManager.getInstance().fileExists("$FILES_DIR/schedules.json")) + { + val scheduleData = if (MainApplication.isEncrypted()) + { + if (TextUtils.isEmpty(MainApplication.getKey())) + { + return + } + + EncryptionHelper.decrypt(MainApplication.getKey(), FileManager.getInstance().readFile("$FILES_DIR/schedules.json")) + } + else + { + FileManager.getInstance().readFileAsString("$FILES_DIR/schedules.json") + } + + try + { + if (!TextUtils.isEmpty(scheduleData)) + { + schedules.clear() + schedules.addAll(GsonHelper.parse(scheduleData, object : TypeToken>(){}.type) as ArrayList) + } + } + catch (e: JsonSyntaxException) + { + e.printStackTrace() + } + + } + } + + fun save() + { + synchronized(schedules) { + if (MainApplication.isEncrypted()) + { + if (TextUtils.isEmpty(MainApplication.getKey())) + { + return + } + + FileManager.getInstance().writeFile("$FILES_DIR/schedules.json", EncryptionHelper.encrypt(MainApplication.getKey(), GsonHelper.parse(schedules))) + } + else + { + FileManager.getInstance().writeFile("$FILES_DIR/schedules.json", GsonHelper.parse(schedules)) + } + + if (File("$FILES_DIR/schedules.json").length() == 0L || !File("$FILES_DIR/schedules.json").exists()) + { + Toast.makeText(context, "There was a fatal problem saving the schedule data, please backup this data", Toast.LENGTH_LONG).show() + val sendData = GsonHelper.parse(schedules) + val share = Intent(Intent.ACTION_SEND) + share.type = "text/plain" + share.putExtra(Intent.EXTRA_TEXT, "== WARNING : PLEASE BACK UP THIS DATA == \r\n\r\n $sendData") + context.startActivity(share) + } + } + } + + fun insert(schedule: FeedingSchedule) + { + schedules.add(schedule) + save() + } + + companion object + { + @JvmField public val instance = ScheduleManager() + @JvmField public var FILES_DIR: String? = null + } +} diff --git a/app/src/main/java/me/anon/model/Action.java b/app/src/main/java/me/anon/model/Action.java index 31984940..99969a5b 100644 --- a/app/src/main/java/me/anon/model/Action.java +++ b/app/src/main/java/me/anon/model/Action.java @@ -1,9 +1,5 @@ package me.anon.model; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - /** * // TODO: Add class description * @@ -11,11 +7,30 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Accessors(prefix = {"m", ""}, chain = true) public abstract class Action { - @Getter @Setter private long date = System.currentTimeMillis(); - @Getter @Setter private String notes; + private long date = System.currentTimeMillis(); + private String notes; + + public long getDate() + { + return date; + } + + public void setDate(long date) + { + this.date = date; + } + + public String getNotes() + { + return notes; + } + + public void setNotes(String notes) + { + this.notes = notes; + } public enum ActionName { @@ -29,8 +44,8 @@ public enum ActionName TRANSPLANTED("Transplanted", 0x9AFFFF8D), TRIM("Trim", 0x9AFFAB91); - @Getter private String printString; - @Getter private int colour; + private String printString; + private int colour; private ActionName(String name, int colour) { @@ -48,6 +63,16 @@ public static String[] names() return names; } + + public String getPrintString() + { + return printString; + } + + public int getColour() + { + return colour; + } } @Override public boolean equals(Object o) diff --git a/app/src/main/java/me/anon/model/Additive.java b/app/src/main/java/me/anon/model/Additive.java index 9db0909a..4c04f4de 100644 --- a/app/src/main/java/me/anon/model/Additive.java +++ b/app/src/main/java/me/anon/model/Additive.java @@ -1,10 +1,5 @@ package me.anon.model; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - /** * // TODO: Add class description * @@ -12,11 +7,28 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Getter @Setter -@Accessors(prefix = {"m", ""}, chain = true) -@NoArgsConstructor public class Additive { private Double amount; private String description; + + public Double getAmount() + { + return amount; + } + + public void setAmount(Double amount) + { + this.amount = amount; + } + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } } diff --git a/app/src/main/java/me/anon/model/CrashReport.java b/app/src/main/java/me/anon/model/CrashReport.java index 7f1823b9..23af497b 100755 --- a/app/src/main/java/me/anon/model/CrashReport.java +++ b/app/src/main/java/me/anon/model/CrashReport.java @@ -2,9 +2,6 @@ import java.io.Serializable; -import lombok.Data; - -@Data public class CrashReport implements Serializable { // App information @@ -20,4 +17,94 @@ public class CrashReport implements Serializable private Throwable exception; private String additionalMessage = ""; private long timestamp = 0L; + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + this.packageName = packageName; + } + + public String getVersionCode() + { + return versionCode; + } + + public void setVersionCode(String versionCode) + { + this.versionCode = versionCode; + } + + public String getModel() + { + return model; + } + + public void setModel(String model) + { + this.model = model; + } + + public String getManufacturer() + { + return manufacturer; + } + + public void setManufacturer(String manufacturer) + { + this.manufacturer = manufacturer; + } + + public String getOsVersion() + { + return osVersion; + } + + public void setOsVersion(String osVersion) + { + this.osVersion = osVersion; + } + + public Throwable getException() + { + return exception; + } + + public void setException(Throwable exception) + { + this.exception = exception; + } + + public String getAdditionalMessage() + { + return additionalMessage; + } + + public void setAdditionalMessage(String additionalMessage) + { + this.additionalMessage = additionalMessage; + } + + public long getTimestamp() + { + return timestamp; + } + + public void setTimestamp(long timestamp) + { + this.timestamp = timestamp; + } } diff --git a/app/src/main/java/me/anon/model/EmptyAction.java b/app/src/main/java/me/anon/model/EmptyAction.java index 724cc75f..fe45a871 100644 --- a/app/src/main/java/me/anon/model/EmptyAction.java +++ b/app/src/main/java/me/anon/model/EmptyAction.java @@ -2,10 +2,6 @@ import android.support.annotation.Nullable; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - /** * // TODO: Add class description * @@ -13,15 +9,27 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Getter @Setter -@NoArgsConstructor public class EmptyAction extends Action { @Nullable private ActionName action; + public EmptyAction() + { + } + public EmptyAction(Action.ActionName action) { this.setDate(System.currentTimeMillis()); this.setAction(action); } + + public void setAction(@Nullable ActionName action) + { + this.action = action; + } + + @Nullable public ActionName getAction() + { + return action; + } } diff --git a/app/src/main/java/me/anon/model/Garden.java b/app/src/main/java/me/anon/model/Garden.java index 8d33b394..533eaf66 100644 --- a/app/src/main/java/me/anon/model/Garden.java +++ b/app/src/main/java/me/anon/model/Garden.java @@ -2,16 +2,28 @@ import java.util.ArrayList; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - -@Getter @Setter -@Accessors(prefix = {"m", ""}, chain = true) -@NoArgsConstructor public class Garden { protected String name; protected ArrayList plantIds; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public ArrayList getPlantIds() + { + return plantIds; + } + + public void setPlantIds(ArrayList plantIds) + { + this.plantIds = plantIds; + } } diff --git a/app/src/main/java/me/anon/model/Models.kt b/app/src/main/java/me/anon/model/Models.kt new file mode 100644 index 00000000..5e480311 --- /dev/null +++ b/app/src/main/java/me/anon/model/Models.kt @@ -0,0 +1,37 @@ +package me.anon.model + +import java.util.* + +/** + * Schedule root object holding list of feeding schedules + */ +class FeedingSchedule( + val id: String = UUID.randomUUID().toString(), + var name: String = "", + var description: String = "", + var schedules: ArrayList = arrayListOf() +) { + constructor() : this( + id = UUID.randomUUID().toString(), + name = "", + description = "", + schedules = arrayListOf() + ){} +} + +/** + * Feeding schedule for specific date + */ +class FeedingScheduleDate( + val id: String = UUID.randomUUID().toString(), + var dateRange: Array, + var stageRange: Array, + var additives: ArrayList = arrayListOf() +) { + constructor() : this( + id = UUID.randomUUID().toString(), + dateRange = arrayOf(), + stageRange = arrayOf(), + additives = arrayListOf() + ){} +} diff --git a/app/src/main/java/me/anon/model/NoteAction.java b/app/src/main/java/me/anon/model/NoteAction.java index e91c13be..57d1c16a 100644 --- a/app/src/main/java/me/anon/model/NoteAction.java +++ b/app/src/main/java/me/anon/model/NoteAction.java @@ -1,9 +1,5 @@ package me.anon.model; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - /** * // TODO: Add class description * @@ -11,10 +7,12 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Getter @Setter -@NoArgsConstructor public class NoteAction extends Action { + public NoteAction() + { + } + public NoteAction(String note) { this.setDate(System.currentTimeMillis()); diff --git a/app/src/main/java/me/anon/model/Nutrient.java b/app/src/main/java/me/anon/model/Nutrient.java index f3f7ff49..0a3412de 100644 --- a/app/src/main/java/me/anon/model/Nutrient.java +++ b/app/src/main/java/me/anon/model/Nutrient.java @@ -1,10 +1,5 @@ package me.anon.model; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - /** * // TODO: Add class description * @@ -13,9 +8,6 @@ * @project GrowTracker */ @Deprecated -@Getter @Setter -@Accessors(prefix = {"m", ""}, chain = true) -@NoArgsConstructor public class Nutrient { private Double npc; // nitrogen @@ -24,4 +16,64 @@ public class Nutrient private Double capc; // calcium private Double spc; // sulfur private Double mgpc; // magnesium + + public Double getNpc() + { + return npc; + } + + public void setNpc(Double npc) + { + this.npc = npc; + } + + public Double getPpc() + { + return ppc; + } + + public void setPpc(Double ppc) + { + this.ppc = ppc; + } + + public Double getKpc() + { + return kpc; + } + + public void setKpc(Double kpc) + { + this.kpc = kpc; + } + + public Double getCapc() + { + return capc; + } + + public void setCapc(Double capc) + { + this.capc = capc; + } + + public Double getSpc() + { + return spc; + } + + public void setSpc(Double spc) + { + this.spc = spc; + } + + public Double getMgpc() + { + return mgpc; + } + + public void setMgpc(Double mgpc) + { + this.mgpc = mgpc; + } } diff --git a/app/src/main/java/me/anon/model/Plant.java b/app/src/main/java/me/anon/model/Plant.java index d209fca9..084bca6f 100644 --- a/app/src/main/java/me/anon/model/Plant.java +++ b/app/src/main/java/me/anon/model/Plant.java @@ -6,16 +6,11 @@ import java.util.ArrayList; import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; +import java.util.LinkedHashMap; import java.util.SortedMap; import java.util.TreeMap; import java.util.UUID; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; import me.anon.lib.DateRenderer; import me.anon.lib.Unit; import me.anon.lib.helper.TimeHelper; @@ -29,9 +24,6 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Getter @Setter -@Accessors(prefix = {"m", ""}, chain = true) -@NoArgsConstructor public class Plant { private String id = UUID.randomUUID().toString(); @@ -58,6 +50,91 @@ public String getId() return id; } + public void setId(String id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getStrain() + { + return strain; + } + + public void setStrain(String strain) + { + this.strain = strain; + } + + public long getPlantDate() + { + return plantDate; + } + + public void setPlantDate(long plantDate) + { + this.plantDate = plantDate; + } + + public boolean isClone() + { + return clone; + } + + public void setClone(boolean clone) + { + this.clone = clone; + } + + public PlantMedium getMedium() + { + return medium; + } + + public void setMedium(PlantMedium medium) + { + this.medium = medium; + } + + public String getMediumDetails() + { + return mediumDetails; + } + + public void setMediumDetails(String mediumDetails) + { + this.mediumDetails = mediumDetails; + } + + public ArrayList getImages() + { + return images; + } + + public void setImages(ArrayList images) + { + this.images = images; + } + + public ArrayList getActions() + { + return actions; + } + + public void setActions(ArrayList actions) + { + this.actions = actions; + } + /** * Stage is now calculated via latest {@link StageChange} action */ @@ -234,9 +311,9 @@ public PlantStage getStage() * Returns a map of plant stages * @return */ - public Map getStages() + public LinkedHashMap getStages() { - HashMap stages = new HashMap<>(); + LinkedHashMap stages = new LinkedHashMap<>(); for (int index = actions.size() - 1; index >= 0; index--) { diff --git a/app/src/main/java/me/anon/model/PlantMedium.java b/app/src/main/java/me/anon/model/PlantMedium.java index 122c9067..f753f4a1 100644 --- a/app/src/main/java/me/anon/model/PlantMedium.java +++ b/app/src/main/java/me/anon/model/PlantMedium.java @@ -1,7 +1,5 @@ package me.anon.model; -import lombok.Getter; - /** * // TODO: Add class description * @@ -16,7 +14,12 @@ public enum PlantMedium COCO("Coco Coir"), AERO("Aeroponics"); - @Getter private String printString; + private String printString; + + public String getPrintString() + { + return printString; + } private PlantMedium(String name) { diff --git a/app/src/main/java/me/anon/model/PlantStage.java b/app/src/main/java/me/anon/model/PlantStage.java index f1ec1ab4..721a17f6 100644 --- a/app/src/main/java/me/anon/model/PlantStage.java +++ b/app/src/main/java/me/anon/model/PlantStage.java @@ -1,6 +1,6 @@ package me.anon.model; -import lombok.Getter; +import android.support.annotation.Nullable; /** * // TODO: Add class description @@ -20,7 +20,12 @@ public enum PlantStage CURING("Curing"), HARVESTED("Harvested"); - @Getter private String printString; + private String printString; + + public String getPrintString() + { + return printString; + } private PlantStage(String name) { @@ -37,4 +42,18 @@ public static String[] names() return names; } + + @Nullable + public static PlantStage valueOfPrintString(String printString) + { + for (PlantStage plantStage : values()) + { + if (plantStage.printString.equals(printString)) + { + return plantStage; + } + } + + return null; + } } diff --git a/app/src/main/java/me/anon/model/StageChange.java b/app/src/main/java/me/anon/model/StageChange.java index fe27b872..3412333e 100644 --- a/app/src/main/java/me/anon/model/StageChange.java +++ b/app/src/main/java/me/anon/model/StageChange.java @@ -1,10 +1,5 @@ package me.anon.model; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - /** * // TODO: Add class description * @@ -12,16 +7,27 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Getter @Setter -@Accessors(prefix = {"m", ""}, chain = true) -@NoArgsConstructor public class StageChange extends Action { private PlantStage newStage; + public StageChange() + { + } + public StageChange(PlantStage stage) { this.setDate(System.currentTimeMillis()); this.setNewStage(stage); } + + public PlantStage getNewStage() + { + return newStage; + } + + public void setNewStage(PlantStage newStage) + { + this.newStage = newStage; + } } diff --git a/app/src/main/java/me/anon/model/Water.java b/app/src/main/java/me/anon/model/Water.java index 4231c3cf..e045a3ce 100644 --- a/app/src/main/java/me/anon/model/Water.java +++ b/app/src/main/java/me/anon/model/Water.java @@ -1,12 +1,16 @@ package me.anon.model; +import android.content.Context; +import android.preference.PreferenceManager; + import java.util.ArrayList; import java.util.List; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; +import me.anon.lib.TempUnit; +import me.anon.lib.Unit; + +import static me.anon.lib.TempUnit.CELCIUS; +import static me.anon.lib.Unit.ML; /** * // TODO: Add class description @@ -15,9 +19,6 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Getter @Setter -@Accessors(prefix = {"m", ""}, chain = true) -@NoArgsConstructor public class Water extends Action { private Double ppm; @@ -29,4 +30,174 @@ public class Water extends Action @Deprecated private Nutrient nutrient; @Deprecated private Double mlpl; + + public void setNutrient(Nutrient nutrient) + { + this.nutrient = nutrient; + } + + public void setMlpl(Double mlpl) + { + this.mlpl = mlpl; + } + + public Nutrient getNutrient() + { + return nutrient; + } + + public Double getMlpl() + { + return mlpl; + } + + public Double getPpm() + { + return ppm; + } + + public void setPpm(Double ppm) + { + this.ppm = ppm; + } + + public Double getPh() + { + return ph; + } + + public void setPh(Double ph) + { + this.ph = ph; + } + + public Double getRunoff() + { + return runoff; + } + + public void setRunoff(Double runoff) + { + this.runoff = runoff; + } + + public Double getAmount() + { + return amount; + } + + public void setAmount(Double amount) + { + this.amount = amount; + } + + public Double getTemp() + { + return temp; + } + + public void setTemp(Double temp) + { + this.temp = temp; + } + + public List getAdditives() + { + return additives; + } + + public void setAdditives(List additives) + { + this.additives = additives; + } + + public String getSummary(Context context) + { + Unit measureUnit = Unit.getSelectedMeasurementUnit(context); + Unit deliveryUnit = Unit.getSelectedDeliveryUnit(context); + TempUnit tempUnit = TempUnit.getSelectedTemperatureUnit(context); + boolean usingEc = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("tds_ec", false); + + String summary = ""; + StringBuilder waterStr = new StringBuilder(); + + if (getPh() != null) + { + waterStr.append("In pH: "); + waterStr.append(getPh()); + waterStr.append(", "); + } + + if (getRunoff() != null) + { + waterStr.append("Out pH: "); + waterStr.append(getRunoff()); + waterStr.append(", "); + } + + summary += waterStr.toString().length() > 0 ? waterStr.toString().substring(0, waterStr.length() - 2) + "
" : ""; + + waterStr = new StringBuilder(); + + if (getPpm() != null) + { + String ppm = String.valueOf(getPpm().longValue()); + if (usingEc) + { + waterStr.append("EC: "); + ppm = String.valueOf((getPpm() * 2d) / 1000d); + } + else + { + waterStr.append("PPM: "); + } + + waterStr.append(ppm); + waterStr.append(", "); + } + + if (getAmount() != null) + { + waterStr.append("Amount: "); + waterStr.append(ML.to(deliveryUnit, getAmount())); + waterStr.append(deliveryUnit.getLabel()); + waterStr.append(", "); + } + + if (getTemp() != null) + { + waterStr.append("Temp: "); + waterStr.append(CELCIUS.to(tempUnit, getTemp())); + waterStr.append("º").append(tempUnit.getLabel()).append(", "); + } + + summary += waterStr.toString().length() > 0 ? waterStr.toString().substring(0, waterStr.length() - 2) + "
" : ""; + + waterStr = new StringBuilder(); + + if (getAdditives().size() > 0) + { + waterStr.append("Additives:"); + + for (Additive additive : getAdditives()) + { + if (additive == null || additive.getAmount() == null) continue; + + double converted = ML.to(measureUnit, additive.getAmount()); + String amountStr = converted == Math.floor(converted) ? String.valueOf((int)converted) : String.valueOf(converted); + + waterStr.append("
    • "); + waterStr.append(additive.getDescription()); + waterStr.append(" - "); + waterStr.append(amountStr); + waterStr.append(measureUnit.getLabel()); + waterStr.append("/"); + waterStr.append(deliveryUnit.getLabel()); + } + } + + summary += waterStr.toString(); + + return summary; + } } diff --git a/app/src/main/java/me/anon/view/ActionHolder.java b/app/src/main/java/me/anon/view/ActionHolder.java index 488d7fb5..3612fab2 100644 --- a/app/src/main/java/me/anon/view/ActionHolder.java +++ b/app/src/main/java/me/anon/view/ActionHolder.java @@ -6,9 +6,7 @@ import android.widget.ImageButton; import android.widget.TextView; -import lombok.Data; import me.anon.grow.R; -import me.anon.lib.Views; /** * // TODO: Add class description @@ -17,7 +15,6 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Data public class ActionHolder extends RecyclerView.ViewHolder { private CardView card; @@ -29,6 +26,46 @@ public class ActionHolder extends RecyclerView.ViewHolder private TextView summary; private ImageButton overflow; + public CardView getCard() + { + return card; + } + + public TextView getDate() + { + return date; + } + + public TextView getFullDate() + { + return fullDate; + } + + public TextView getDateDay() + { + return dateDay; + } + + public TextView getStageDay() + { + return stageDay; + } + + public TextView getName() + { + return name; + } + + public TextView getSummary() + { + return summary; + } + + public ImageButton getOverflow() + { + return overflow; + } + public ActionHolder(View itemView) { super(itemView); diff --git a/app/src/main/java/me/anon/view/FeedingDateHolder.kt b/app/src/main/java/me/anon/view/FeedingDateHolder.kt new file mode 100644 index 00000000..bc2005b2 --- /dev/null +++ b/app/src/main/java/me/anon/view/FeedingDateHolder.kt @@ -0,0 +1,66 @@ +package me.anon.view + +import android.support.v7.widget.RecyclerView +import android.text.Html +import android.view.View +import kotlinx.android.synthetic.main.feeding_date_stub.view.* +import me.anon.controller.adapter.FeedingDateAdapter +import me.anon.lib.Unit +import me.anon.lib.helper.TimeHelper +import me.anon.model.FeedingScheduleDate + +/** + * // TODO: Add class description + */ +class FeedingDateHolder(val adapter: FeedingDateAdapter, itemView: View) : RecyclerView.ViewHolder(itemView) +{ + private val title = itemView.title + private val additives = itemView.additives + private val delete = itemView.delete + private val copy = itemView.copy + + private val measureUnit: Unit by lazy { Unit.getSelectedMeasurementUnit(itemView.context); } + private val deliveryUnit: Unit by lazy { Unit.getSelectedDeliveryUnit(itemView.context); } + + public fun bind(feedingSchedule: FeedingScheduleDate) + { + delete.visibility = View.GONE + copy.visibility = View.GONE + itemView.setBackgroundColor(0x00FFFFFF.toInt()) + + val lastStage = adapter.plantStages.toSortedMap().lastKey() + val days = TimeHelper.toDays(adapter.plantStages[lastStage] ?: 0).toInt() + + if (lastStage.ordinal >= feedingSchedule.stageRange[0].ordinal) + { + if (days >= feedingSchedule.dateRange[0] + && ((days <= feedingSchedule.dateRange[1] && lastStage.ordinal == feedingSchedule.stageRange[0].ordinal) + || (lastStage.ordinal < feedingSchedule.stageRange[1].ordinal))) + { + itemView.setBackgroundColor(0x70BBDEFB.toInt()) + } + } + + title.text = "${feedingSchedule.dateRange[0]}${feedingSchedule.stageRange[0].printString[0]}" + if (feedingSchedule.dateRange[0] != feedingSchedule.dateRange[1]) + { + title.text = "${title.text} - ${feedingSchedule.dateRange[1]}${feedingSchedule.stageRange[1].printString[0]}" + } + + var additiveStr = "" + for (additive in feedingSchedule.additives) + { + val converted = Unit.ML.to(measureUnit, additive.amount!!) + val amountStr = if (converted == Math.floor(converted)) converted.toInt().toString() else converted.toString() + + if (additiveStr.isNotEmpty()) additiveStr += "
" + additiveStr += "• ${additive.description} - ${amountStr}${measureUnit.label}/${deliveryUnit.label}" + } + + additives.text = Html.fromHtml(additiveStr) + + itemView.setOnClickListener { + adapter.onItemSelectCallback.invoke(feedingSchedule) + } + } +} diff --git a/app/src/main/java/me/anon/view/ImageActionHolder.java b/app/src/main/java/me/anon/view/ImageActionHolder.java new file mode 100644 index 00000000..dbceafd2 --- /dev/null +++ b/app/src/main/java/me/anon/view/ImageActionHolder.java @@ -0,0 +1,99 @@ +package me.anon.view; + +import android.content.Intent; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.CardView; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.android.flexbox.FlexboxLayout; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.nostra13.universalimageloader.core.imageaware.ImageAware; +import com.nostra13.universalimageloader.core.imageaware.ImageViewAware; + +import java.util.ArrayList; +import java.util.Collections; + +import me.anon.controller.adapter.ActionAdapter; +import me.anon.grow.MainApplication; +import me.anon.grow.R; +import me.anon.grow.fragment.ImageLightboxDialog; + +/** + * // TODO: Add class description + * + * @author 7LPdWcaW + * @documentation // TODO Reference flow doc + * @project GrowTracker + */ +public class ImageActionHolder extends RecyclerView.ViewHolder +{ + private FlexboxLayout flexboxLayout; + private TextView dateDay; + private TextView stageDay; + private ActionAdapter adapter; + + public TextView getDateDay() + { + return dateDay; + } + + public TextView getStageDay() + { + return stageDay; + } + + public ImageActionHolder(ActionAdapter adapter, View itemView) + { + super(itemView); + + this.adapter = adapter; + flexboxLayout = (FlexboxLayout)itemView.findViewById(R.id.image_container); + dateDay = (TextView)itemView.findViewById(R.id.date_day); + stageDay = (TextView)itemView.findViewById(R.id.stage_day); + } + + public void bind(final ArrayList imageUrls) + { + flexboxLayout.removeAllViews(); + for (final String imageUrl : imageUrls) + { + CardView view = (CardView)LayoutInflater.from(itemView.getContext()).inflate(R.layout.action_image_item, flexboxLayout, false); + final ImageView image = (ImageView)view.getChildAt(0); + + ImageLoader.getInstance().cancelDisplayTask(image); + + image.post(new Runnable() + { + @Override public void run() + { + if (image != null && ViewCompat.isLaidOut(image)) + { + ImageAware imageAware = new ImageViewAware(image, true); + ImageLoader.getInstance().displayImage("file://" + imageUrl, imageAware, MainApplication.getDisplayImageOptions()); + } + } + }); + + flexboxLayout.addView(view); + + image.setOnClickListener(new View.OnClickListener() + { + @Override public void onClick(View v) + { + ArrayList images = new ArrayList<>(); + images.addAll(adapter.getPlant().getImages()); + Collections.reverse(images); + + Intent details = new Intent(v.getContext(), ImageLightboxDialog.class); + details.putExtra("images", (String[])images.toArray(new String[images.size()])); + details.putExtra("image_position", images.indexOf(imageUrl)); + v.getContext().startActivity(details); + } + }); + } + } +} diff --git a/app/src/main/java/me/anon/view/ImageHolder.java b/app/src/main/java/me/anon/view/ImageHolder.java index 8cd5101a..abd13e4d 100644 --- a/app/src/main/java/me/anon/view/ImageHolder.java +++ b/app/src/main/java/me/anon/view/ImageHolder.java @@ -5,10 +5,7 @@ import android.widget.CheckBox; import android.widget.ImageView; -import lombok.Data; -import lombok.Getter; import me.anon.grow.R; -import me.anon.lib.Views; /** * // TODO: Add class description @@ -17,12 +14,21 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Data public class ImageHolder extends RecyclerView.ViewHolder { private ImageView image; private CheckBox selection; + public ImageView getImage() + { + return image; + } + + public CheckBox getSelection() + { + return selection; + } + public ImageHolder(View itemView) { super(itemView); diff --git a/app/src/main/java/me/anon/view/PlantHolder.java b/app/src/main/java/me/anon/view/PlantHolder.java index 1527c0f7..52e9578c 100644 --- a/app/src/main/java/me/anon/view/PlantHolder.java +++ b/app/src/main/java/me/anon/view/PlantHolder.java @@ -5,7 +5,6 @@ import android.widget.ImageView; import android.widget.TextView; -import lombok.Data; import me.anon.grow.R; /** @@ -15,13 +14,27 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Data public class PlantHolder extends RecyclerView.ViewHolder { private ImageView image; private TextView name; private TextView summary; + public ImageView getImage() + { + return image; + } + + public TextView getName() + { + return name; + } + + public TextView getSummary() + { + return summary; + } + public PlantHolder(View itemView) { super(itemView); diff --git a/app/src/main/java/me/anon/view/PlantSelectHolder.java b/app/src/main/java/me/anon/view/PlantSelectHolder.java index 053a77ec..398ddd01 100644 --- a/app/src/main/java/me/anon/view/PlantSelectHolder.java +++ b/app/src/main/java/me/anon/view/PlantSelectHolder.java @@ -6,7 +6,6 @@ import android.widget.ImageView; import android.widget.TextView; -import lombok.Data; import me.anon.grow.R; /** @@ -16,13 +15,27 @@ * @documentation // TODO Reference flow doc * @project GrowTracker */ -@Data public class PlantSelectHolder extends RecyclerView.ViewHolder { private ImageView image; private CheckBox checkbox; private TextView name; + public ImageView getImage() + { + return image; + } + + public CheckBox getCheckbox() + { + return checkbox; + } + + public TextView getName() + { + return name; + } + public PlantSelectHolder(View itemView) { super(itemView); diff --git a/app/src/main/java/me/anon/view/ScheduleHolder.kt b/app/src/main/java/me/anon/view/ScheduleHolder.kt new file mode 100644 index 00000000..32f2984f --- /dev/null +++ b/app/src/main/java/me/anon/view/ScheduleHolder.kt @@ -0,0 +1,62 @@ +package me.anon.view + +import android.app.AlertDialog +import android.content.Intent +import android.support.v7.widget.RecyclerView +import android.view.View +import kotlinx.android.synthetic.main.schedule_item.view.* +import me.anon.controller.adapter.FeedingScheduleAdapter +import me.anon.grow.FeedingScheduleDetailsActivity +import me.anon.grow.R +import me.anon.lib.manager.ScheduleManager +import me.anon.model.FeedingSchedule + +/** + * Feeding schedule view holder class + */ +class ScheduleHolder(val adapter: FeedingScheduleAdapter, itemView: View) : RecyclerView.ViewHolder(itemView) +{ + private val title = itemView.title + private val summary = itemView.summary + private val delete = itemView.delete + private val copy = itemView.copy + + public fun bind(feedingSchedule: FeedingSchedule) + { + title.text = feedingSchedule.name + summary.text = feedingSchedule.description + + summary.visibility = when { + summary.text.isEmpty() -> View.GONE + else -> View.VISIBLE + } + + delete.setOnClickListener { + AlertDialog.Builder(it.context) + .setTitle(R.string.confirm_title) + .setMessage(R.string.confirm_delete_schedule) + .setPositiveButton(R.string.confirm_positive) { _, _ -> + adapter.onDeleteCallback.invoke(feedingSchedule) + } + .setNegativeButton(R.string.confirm_negative, null) + .show() + } + + copy.setOnClickListener { + AlertDialog.Builder(it.context) + .setTitle(R.string.confirm_title) + .setMessage(R.string.confirm_copy_schedule) + .setPositiveButton(R.string.confirm_positive) { _, _ -> + adapter.onCopyCallback.invoke(feedingSchedule) + } + .setNegativeButton(R.string.confirm_negative, null) + .show() + } + + itemView.setOnClickListener { + it.context.startActivity(Intent(it.context, FeedingScheduleDetailsActivity::class.java).also { + it.putExtra("schedule_index", ScheduleManager.instance.schedules.indexOf(feedingSchedule)) + }) + } + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_dropdown.png b/app/src/main/res/drawable-hdpi/ic_dropdown.png new file mode 100644 index 00000000..49fcb5ba Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_dropdown.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_dropdown.png b/app/src/main/res/drawable-mdpi/ic_dropdown.png new file mode 100644 index 00000000..133678a2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_dropdown.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_dropdown.png b/app/src/main/res/drawable-xhdpi/ic_dropdown.png new file mode 100644 index 00000000..003798f6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_dropdown.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_dropdown.png b/app/src/main/res/drawable-xxhdpi/ic_dropdown.png new file mode 100644 index 00000000..7551a41a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_dropdown.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_dropdown.png b/app/src/main/res/drawable-xxxhdpi/ic_dropdown.png new file mode 100644 index 00000000..56038748 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_dropdown.png differ diff --git a/app/src/main/res/drawable/divider_4dp.xml b/app/src/main/res/drawable/divider_4dp.xml new file mode 100644 index 00000000..6281382c --- /dev/null +++ b/app/src/main/res/drawable/divider_4dp.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_content_copy_black_24dp.xml b/app/src/main/res/drawable/ic_content_copy_black_24dp.xml new file mode 100644 index 00000000..8a894a3b --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-land/plant_details_view.xml b/app/src/main/res/layout-land/plant_details_view.xml index 4a00b855..46e39cf9 100644 --- a/app/src/main/res/layout-land/plant_details_view.xml +++ b/app/src/main/res/layout-land/plant_details_view.xml @@ -2,6 +2,7 @@ @@ -78,6 +79,89 @@ /> + + + + + + + + + + + + +