diff --git a/README.md b/README.md index 075dfdbd..6d08fc50 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ 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) 0afd3816fd71d77282f2a48efa265b71 v2.4.1](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.4.1/v2.4.1-production.apk) +[Latest APK: (MD5) aa3496894c364a766145a576122e3ee6 v2.5](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.5/v2.5-production.apk) -[Latest APK (Discrete): (MD5) 075a079e1e7697db54cf86180fc56e58 v2.4.1](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.4.1/v2.4.1-discrete.apk) +[Latest APK (Discrete): (MD5) 0f68525415eb2fc3c77184cdb3c51443 v2.5](https://github.com/7LPdWcaW/GrowTracker-Android/releases/download/v2.5/v2.5-discrete.apk) [Get it on F-Droid with automatic updates](https://f-droid.org/packages/me.anon.grow/) @@ -32,27 +32,27 @@ You can either elect to update manually, or get notified on releases by installi # 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) -[![discrete plant list](screenshots/1c-thumb.png)](screenshots/1c.png) -[![discrete plant list](screenshots/1d-thumb.png)](screenshots/1d.png) -[![discrete plant list](screenshots/1e-thumb.png)](screenshots/1e.png) -[![plant details](screenshots/2-thumb.png)](screenshots/2.png) -[![feeding](screenshots/3-thumb.png)](screenshots/3.png) -[![nutrients](screenshots/4-thumb.png)](screenshots/4.png) -[![nutrients](screenshots/4b-thumb.png)](screenshots/4b.png) -[![actions](screenshots/5-thumb.png)](screenshots/5.png) -[![pictures](screenshots/6-thumb.png)](screenshots/6.png) -[![statistics](screenshots/7-thumb.png)](screenshots/7.png) -[![past actions](screenshots/8-thumb.png)](screenshots/8.png) -[![action filters](screenshots/9-thumb.png)](screenshots/9.png) -[![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) +[![install](metadata/images/phoneScreenshotsThumbs/install-thumb.png)](metadata/images/phoneScreenshots/install.png) +[![plant list](metadata/images/phoneScreenshotsThumbs/1-thumb.png)](metadata/images/phoneScreenshots/1.png) +[![discrete plant list](metadata/images/phoneScreenshotsThumbs/1b-thumb.png)](metadata/images/phoneScreenshots/1b.png) +[![discrete plant list](metadata/images/phoneScreenshotsThumbs/1c-thumb.png)](metadata/images/phoneScreenshots/1c.png) +[![discrete plant list](metadata/images/phoneScreenshotsThumbs/1d-thumb.png)](metadata/images/phoneScreenshots/1d.png) +[![discrete plant list](metadata/images/phoneScreenshotsThumbs/1e-thumb.png)](metadata/images/phoneScreenshots/1e.png) +[![plant details](metadata/images/phoneScreenshotsThumbs/2-thumb.png)](metadata/images/phoneScreenshots/2.png) +[![feeding](metadata/images/phoneScreenshotsThumbs/3-thumb.png)](metadata/images/phoneScreenshots/3.png) +[![nutrients](metadata/images/phoneScreenshotsThumbs/4-thumb.png)](metadata/images/phoneScreenshots/4.png) +[![nutrients](metadata/images/phoneScreenshotsThumbs/4b-thumb.png)](metadata/images/phoneScreenshots/4b.png) +[![actions](metadata/images/phoneScreenshotsThumbs/5-thumb.png)](metadata/images/phoneScreenshots/5.png) +[![pictures](metadata/images/phoneScreenshotsThumbs/6-thumb.png)](metadata/images/phoneScreenshots/6.png) +[![statistics](metadata/images/phoneScreenshotsThumbs/7-thumb.png)](metadata/images/phoneScreenshots/7.png) +[![past actions](metadata/images/phoneScreenshotsThumbs/8-thumb.png)](metadata/images/phoneScreenshots/8.png) +[![action filters](metadata/images/phoneScreenshotsThumbs/9-thumb.png)](metadata/images/phoneScreenshots/9.png) +[![action options](metadata/images/phoneScreenshotsThumbs/10-thumb.png)](metadata/images/phoneScreenshots/10.png) +[![settings](metadata/images/phoneScreenshotsThumbs/11-thumb.png)](metadata/images/phoneScreenshots/11.png) +[![measurements](metadata/images/phoneScreenshotsThumbs/12-thumb.png)](metadata/images/phoneScreenshots/12.png) +[![schedules](metadata/images/phoneScreenshotsThumbs/13-thumb.png)](metadata/images/phoneScreenshots/13.png) +[![schedule details](metadata/images/phoneScreenshotsThumbs/14-thumb.png)](metadata/images/phoneScreenshots/14.png) +[![schedule date](metadata/images/phoneScreenshotsThumbs/15-thumb.png)](metadata/images/phoneScreenshots/15.png) # About the app @@ -88,7 +88,7 @@ One of, One of, -`PLANTED`, `GERMINATION`, `CUTTING`, `VEGETATION`, `FLOWER`, `DRYING`, `CURING`, `HARVESTED` +`PLANTED`, `GERMINATION`, `SEEDLING`, `CUTTING`, `VEGETATION`, `FLOWER`, `DRYING`, `CURING`, `HARVESTED` ### Action object (feeding) diff --git a/app/build.gradle b/app/build.gradle index e54e8101..063d010a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId "me.anon.grow" minSdkVersion 17 targetSdkVersion 28 - versionCode 20 - versionName "2.4.1" + versionCode 21 + versionName "2.5" javaCompileOptions { annotationProcessorOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ba199b3..dbd7254a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ /> + diff --git a/app/src/main/assets/readme.html b/app/src/main/assets/readme.html index 71279a50..2ba3a884 100644 --- a/app/src/main/assets/readme.html +++ b/app/src/main/assets/readme.html @@ -33,7 +33,7 @@

Medium (ENUM)

One of,

SOIL, HYDRO

Plant Stage (ENUM)

-

One of,

PLANTED, GERMINATION, CUTTING, VEGETATION, FLOWER, CURING, HARVESTED

+

One of,

PLANTED, GERMINATION, SEEDLING, CUTTING, VEGETATION, FLOWER, CURING, HARVESTED

Action object (feeding)

Temperature measured in ºC

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 f6e96659..39cddbaf 100644 --- a/app/src/main/java/me/anon/controller/adapter/ActionAdapter.java +++ b/app/src/main/java/me/anon/controller/adapter/ActionAdapter.java @@ -37,6 +37,7 @@ import me.anon.model.EmptyAction; import me.anon.model.NoteAction; import me.anon.model.Plant; +import me.anon.model.PlantStage; import me.anon.model.StageChange; import me.anon.model.Water; import me.anon.view.ActionHolder; @@ -152,6 +153,12 @@ public void setActions(@Nullable Plant plant, ArrayList actions, ArrayLi Collections.reverse(actions); for (Action item : actions) { + // force planted stage to use plant date + if (item instanceof StageChange && ((StageChange)item).getNewStage() == PlantStage.PLANTED) + { + item.setDate(plant.getPlantDate()); + } + if (plant != null) { ArrayList groupedImages = new ArrayList<>(); @@ -451,47 +458,31 @@ else if (item.getItemId() == R.id.delete) dateDay.setText(Html.fromHtml(dateDayStr)); String stageDayStr = ""; - StageChange current = null; - StageChange previous = null; + + StageChange lastChange = null; + StageChange currentChange = new StageChange(); + currentChange.setDate(action.getDate()); for (int actionIndex = index; actionIndex < actions.size(); actionIndex++) { if (actions.get(actionIndex) instanceof StageChange) { - if (current == null) + if (lastChange == null) { - current = (StageChange)actions.get(actionIndex); - } - else if (previous == null) - { - previous = (StageChange)actions.get(actionIndex); + lastChange = (StageChange)actions.get(actionIndex); + break; } } } - if (plant != null) - { - int totalDays = (int)TimeHelper.toDays(Math.abs(action.getDate() - plant.getPlantDate())); - stageDayStr += totalDays; + int totalDays = (int)TimeHelper.toDays(Math.abs(action.getDate() - plant.getPlantDate())); + stageDayStr += (totalDays == 0 ? 1 : totalDays); - if (previous == null) - { - previous = current; - } - - 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(); - } - } + if (lastChange != null) + { + int currentDays = (int)TimeHelper.toDays(Math.abs(currentChange.getDate() - lastChange.getDate())); + currentDays = (currentDays == 0 ? 1 : currentDays); + stageDayStr += "/" + currentDays + lastChange.getNewStage().getPrintString().substring(0, 1).toLowerCase(); } stageDay.setText(stageDayStr); 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 be990c7b..fe98086e 100644 --- a/app/src/main/java/me/anon/controller/adapter/ImageAdapter.java +++ b/app/src/main/java/me/anon/controller/adapter/ImageAdapter.java @@ -17,6 +17,8 @@ import me.anon.grow.MainApplication; import me.anon.grow.R; import me.anon.grow.fragment.ImageLightboxDialog; +import me.anon.lib.manager.PlantManager; +import me.anon.model.Plant; import me.anon.view.ImageHolder; /** @@ -28,6 +30,7 @@ */ public class ImageAdapter extends RecyclerView.Adapter { + public Plant plant = null; private List images = new ArrayList<>(); private List selected = new ArrayList<>(); private View.OnLongClickListener onLongClickListener; @@ -75,6 +78,7 @@ public void setImages(List images) if (!inActionMode) { Intent details = new Intent(v.getContext(), ImageLightboxDialog.class); + details.putExtra("plant_index", PlantManager.getInstance().getPlants().indexOf(plant)); details.putExtra("images", (String[])images.toArray(new String[getItemCount()])); details.putExtra("image_position", position); v.getContext().startActivity(details); diff --git a/app/src/main/java/me/anon/grow/MainApplication.java b/app/src/main/java/me/anon/grow/MainApplication.java index 08cc957a..72f1dd11 100644 --- a/app/src/main/java/me/anon/grow/MainApplication.java +++ b/app/src/main/java/me/anon/grow/MainApplication.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import com.nostra13.universalimageloader.core.DisplayImageOptions; @@ -90,10 +91,17 @@ public static boolean isTablet() return isTablet; } + private static Context context; + public static SharedPreferences getDefaultPreferences() + { + return PreferenceManager.getDefaultSharedPreferences(context); + } + @Override public void onCreate() { super.onCreate(); + context = this; ExceptionHandler.getInstance().register(this); encrypted = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("encrypt", false); diff --git a/app/src/main/java/me/anon/grow/fragment/ActionSelectDialogFragment.java b/app/src/main/java/me/anon/grow/fragment/ActionSelectDialogFragment.java index ac18d56b..e2cd2679 100644 --- a/app/src/main/java/me/anon/grow/fragment/ActionSelectDialogFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/ActionSelectDialogFragment.java @@ -19,6 +19,7 @@ import me.anon.grow.R; import me.anon.lib.Views; import me.anon.model.Action; +import me.anon.view.ActionHolder; import me.anon.view.ImageActionHolder; /** @@ -57,7 +58,15 @@ public ActionSelectDialogFragment(ArrayList actions) { super.onBindViewHolder(vh, index); int padding = (int)getResources().getDimension(R.dimen.padding_8dp); - vh.itemView.setPadding(padding, padding, padding, padding); + vh.itemView.setPadding(0, 0, 0, 0); + vh.itemView.findViewById(R.id.date_container).setVisibility(View.GONE); + ((View)vh.itemView.findViewById(R.id.content_container).getParent()).setPadding(0, 0, 0, 0); + + if (vh instanceof ActionHolder) + { + ((ActionHolder)vh).getCard().setBackgroundResource(0); + ((ActionHolder)vh).getCard().setContentPadding(padding, padding, padding * 2, (int)(padding * 2.5)); + } } }; @@ -66,7 +75,6 @@ public ActionSelectDialogFragment(ArrayList actions) adapter.setActions(null, actions, exclude); } - @SuppressLint("ValidFragment") public ActionSelectDialogFragment() { 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 2648179a..2aff63cf 100644 --- a/app/src/main/java/me/anon/grow/fragment/EventListFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/EventListFragment.java @@ -40,6 +40,7 @@ import me.anon.model.EmptyAction; import me.anon.model.NoteAction; import me.anon.model.Plant; +import me.anon.model.PlantStage; import me.anon.model.StageChange; import me.anon.model.Water; import me.anon.view.ActionHolder; @@ -436,6 +437,11 @@ else if (action instanceof StageChange) { @Override public void onStageUpdated(final StageChange action) { + if (action.getNewStage() == PlantStage.PLANTED) + { + plant.setPlantDate(action.getDate()); + } + plant.getActions().set(originalIndex, action); PlantManager.getInstance().upsert(plantIndex, plant); setActions(); diff --git a/app/src/main/java/me/anon/grow/fragment/ImageLightboxDialog.java b/app/src/main/java/me/anon/grow/fragment/ImageLightboxDialog.java index 2c4dbb70..6bddd836 100644 --- a/app/src/main/java/me/anon/grow/fragment/ImageLightboxDialog.java +++ b/app/src/main/java/me/anon/grow/fragment/ImageLightboxDialog.java @@ -35,11 +35,17 @@ import me.anon.grow.R; import me.anon.lib.DateRenderer; import me.anon.lib.Views; +import me.anon.lib.helper.TimeHelper; +import me.anon.lib.manager.PlantManager; +import me.anon.model.Action; +import me.anon.model.Plant; +import me.anon.model.StageChange; @Views.Injectable public class ImageLightboxDialog extends Activity { private String[] imageUrls = {}; + private Plant plant; @Views.InjectView(R.id.pager) public ViewPager pager; private int pagerPosition = 0; @@ -61,6 +67,7 @@ public class ImageLightboxDialog extends Activity imageUrls = getIntent().getExtras().getStringArray("images"); } + plant = PlantManager.getInstance().getPlants().get(getIntent().getIntExtra("plant_index", -1)); pagerPosition = getIntent().getExtras().getInt("image_position", 0); } else @@ -71,6 +78,7 @@ public class ImageLightboxDialog extends Activity if (savedInstanceState != null) { + plant = PlantManager.getInstance().getPlants().get(getIntent().getIntExtra("plant_index", -1)); pagerPosition = savedInstanceState.getInt("image_position"); } @@ -82,6 +90,7 @@ public class ImageLightboxDialog extends Activity @Override protected void onSaveInstanceState(Bundle outState) { + outState.putInt("plant_index", PlantManager.getInstance().getPlants().indexOf(plant)); outState.putInt("image_position", pager.getCurrentItem()); super.onSaveInstanceState(outState); } @@ -169,10 +178,36 @@ private void forceHideKeyboard(ViewGroup view) DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(view.getContext()); DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(view.getContext()); String[] parts = images[position].split("/"); - String date = parts[parts.length - 1].split("\\.")[0]; + String fileDate = parts[parts.length - 1].split("\\.")[0]; + Date date = new Date(Long.parseLong(fileDate)); - String dateStr = dateFormat.format(new Date(Long.parseLong(date))) + " " + timeFormat.format(new Date(Long.parseLong(date))); - ((TextView)imageLayout.findViewById(R.id.taken)).setText(Html.fromHtml("Image taken: " + dateStr + " (" + new DateRenderer().timeAgo(Long.parseLong(date)).formattedDate + " ago)")); + StageChange lastChange = null; + StageChange currentChange = new StageChange(); + currentChange.setDate(date.getTime()); + + for (int index = plant.getActions().size() - 1; index >= 0; index--) + { + Action action = plant.getActions().get(index); + if (action instanceof StageChange) + { + if (action.getDate() < date.getTime() && lastChange == null) + { + lastChange = (StageChange)action; + break; + } + } + } + + String stageDayStr = ""; + if (lastChange != null) + { + int currentDays = (int)TimeHelper.toDays(Math.abs(currentChange.getDate() - lastChange.getDate())); + currentDays = (currentDays == 0 ? 1 : currentDays); + stageDayStr += " [" + currentDays + lastChange.getNewStage().getPrintString().substring(0, 1).toLowerCase() + "]"; + } + + String dateStr = dateFormat.format(date) + " " + timeFormat.format(date); + ((TextView)imageLayout.findViewById(R.id.taken)).setText(Html.fromHtml("Image taken: " + dateStr + stageDayStr + " (" + new DateRenderer().timeAgo(date.getTime()).formattedDate + " ago)")); try { 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 6edd47b3..2fba63ab 100644 --- a/app/src/main/java/me/anon/grow/fragment/PlantDetailsFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/PlantDetailsFragment.java @@ -4,10 +4,6 @@ import android.app.Activity; 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; import android.content.Context; import android.content.DialogInterface; @@ -24,7 +20,6 @@ import android.preference.PreferenceManager; import android.provider.MediaStore; 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; @@ -55,6 +50,7 @@ import java.io.OutputStream; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Locale; @@ -70,6 +66,7 @@ import me.anon.grow.R; import me.anon.grow.StatisticsActivity; import me.anon.grow.ViewPhotosActivity; +import me.anon.grow.service.ExportService; import me.anon.lib.DateRenderer; import me.anon.lib.ExportCallback; import me.anon.lib.SnackBar; @@ -78,11 +75,13 @@ import me.anon.lib.helper.AddonHelper; import me.anon.lib.helper.ExportHelper; import me.anon.lib.helper.FabAnimator; +import me.anon.lib.helper.NotificationHelper; import me.anon.lib.helper.PermissionHelper; import me.anon.lib.manager.GardenManager; import me.anon.lib.manager.PlantManager; import me.anon.lib.task.AsyncCallback; import me.anon.lib.task.EncryptTask; +import me.anon.model.Action; import me.anon.model.EmptyAction; import me.anon.model.NoteAction; import me.anon.model.Plant; @@ -279,6 +278,14 @@ private void setUi() { @Override public void onDateSelected(Calendar newDate) { + for (Action action : plant.getActions()) + { + if (action instanceof StageChange && ((StageChange)action).getNewStage() == PlantStage.PLANTED) + { + action.setDate(newDate.getTimeInMillis()); + } + } + plant.setPlantDate(newDate.getTimeInMillis()); String dateStr = dateFormat.format(new Date(plant.getPlantDate())) + " " + timeFormat.format(new Date(plant.getPlantDate())); date.setText(dateStr); @@ -730,64 +737,7 @@ else if (item.getItemId() == R.id.duplicate) 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(), "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()) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(R.drawable.ic_stat_name) - .build(); - - notificationManager.notify(plantIndex, exportNotification); - - ExportHelper.exportPlant(getActivity(), plant, new ExportCallback() - { - @Override public void onCallback(Context context, File file) - { - if (file != null && file.exists() && getActivity() != null) - { - 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, openIntent, PendingIntent.FLAG_UPDATE_CURRENT)) - .setSmallIcon(R.drawable.ic_stat_done) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setAutoCancel(true) - .build(); - notificationManager.notify(plantIndex, finishNotification); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) - { - new MediaScannerWrapper(getActivity(), file.getAbsolutePath(), "application/zip").scan(); - } - else - { - getActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(file))); - } - } - } - }); + ExportService.export(getActivity(),new ArrayList(Arrays.asList(plant)), plant.getName().replaceAll("[^a-zA-Z0-9]+", "-"), plant.getName()); return true; } @@ -928,7 +878,6 @@ else if (item.getItemId() == R.id.export) public void save() { name.setError(null); - strain.setError(null); if (!TextUtils.isEmpty(name.getText())) { @@ -944,11 +893,6 @@ public void save() { plant.setStrain(strain.getText().toString().trim()); } - else - { - strain.setError("strain can not be empty"); - return; - } plant.setMediumDetails(mediumDetails.getText().toString()); 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 2638ec50..399567b8 100644 --- a/app/src/main/java/me/anon/grow/fragment/PlantListFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/PlantListFragment.java @@ -21,6 +21,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import com.esotericsoftware.kryo.Kryo; @@ -34,12 +35,14 @@ import me.anon.grow.MainActivity; import me.anon.grow.MainApplication; import me.anon.grow.R; +import me.anon.grow.service.ExportService; import me.anon.lib.SnackBar; import me.anon.lib.SnackBarListener; import me.anon.lib.Views; import me.anon.lib.event.GardenChangeEvent; import me.anon.lib.helper.BusHelper; import me.anon.lib.helper.FabAnimator; +import me.anon.lib.helper.NotificationHelper; import me.anon.lib.manager.GardenManager; import me.anon.lib.manager.PlantManager; import me.anon.model.EmptyAction; @@ -131,7 +134,10 @@ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, Recycle } else { - recycler.setLayoutManager(new LinearLayoutManager(getActivity())); + boolean reverse = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("reverse_order", false); + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, reverse); + layoutManager.setStackFromEnd(reverse); + recycler.setLayoutManager(layoutManager); } recycler.setAdapter(adapter); @@ -321,6 +327,7 @@ private synchronized void saveCurrentState() { if (resultCode != Activity.RESULT_CANCELED) { + adapter.notifyDataSetChanged(); SnackBar.show(getActivity(), "Watering added", new SnackBarListener() { @Override public void onSnackBarStarted(Object o) @@ -356,6 +363,7 @@ private synchronized void saveCurrentState() if (garden != null) { + menu.findItem(R.id.export_garden).setVisible(true); menu.findItem(R.id.edit_garden).setVisible(true); menu.findItem(R.id.delete_garden).setVisible(true); } @@ -392,6 +400,15 @@ private synchronized void saveCurrentState() return true; } + else if (item.getItemId() == R.id.export_garden) + { + Toast.makeText(getActivity(), "Exporting grow log of garden...", Toast.LENGTH_SHORT).show(); + NotificationHelper.createExportChannel(getActivity()); + NotificationHelper.sendExportNotification(getActivity(), "Exporting garden grow log", "Exporting " + garden.getName()); + + ArrayList export = new ArrayList<>(adapter.getPlants()); + exportPlants(export); + } else if (item.getItemId() == R.id.delete_garden) { new AlertDialog.Builder(getActivity()) @@ -443,7 +460,7 @@ else if (item.getItemId() == R.id.delete_garden) saveCurrentState(); } - int[] ids = {R.id.filter_planted, R.id.filter_germination, R.id.filter_cutting, R.id.filter_vegetation, R.id.filter_flowering, R.id.filter_drying, R.id.filter_curing, R.id.filter_harvested}; + int[] ids = {R.id.filter_planted, R.id.filter_germination, R.id.filter_seedling, R.id.filter_cutting, R.id.filter_vegetation, R.id.filter_flowering, R.id.filter_drying, R.id.filter_curing, R.id.filter_harvested}; PlantStage[] stages = PlantStage.values(); for (int index = 0; index < ids.length; index++) @@ -473,6 +490,12 @@ else if (item.getItemId() == R.id.delete_garden) return super.onOptionsItemSelected(item); } + private void exportPlants(final ArrayList plants) + { + Toast.makeText(getActivity(), "Exporting grow log...", Toast.LENGTH_SHORT).show(); + ExportService.export(getActivity(), plants, garden.getName().replaceAll("[^a-zA-Z0-9]+", "-"), garden.getName()); + } + private void filter() { ArrayList plantList = PlantManager.getInstance().getSortedPlantList(garden); @@ -487,7 +510,7 @@ private void filter() } } - if ((garden == null && plants.size() < plantList.size()) || garden != null) + if (garden != null || plants.size() < plantList.size()) { adapter.setShowOnly(plants); } 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 ecd57895..ffd339ec 100644 --- a/app/src/main/java/me/anon/grow/fragment/SettingsFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/SettingsFragment.java @@ -15,11 +15,13 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; -import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.preference.SwitchPreference; +import android.support.design.widget.Snackbar; import android.support.v4.content.FileProvider; import android.text.Html; import android.text.TextUtils; @@ -29,6 +31,8 @@ import com.nostra13.universalimageloader.core.ImageLoader; +import org.jetbrains.annotations.NotNull; + import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; @@ -45,6 +49,7 @@ import me.anon.grow.MainApplication; import me.anon.grow.R; import me.anon.lib.SnackBar; +import me.anon.lib.SnackBarListener; import me.anon.lib.TempUnit; import me.anon.lib.Unit; import me.anon.lib.helper.AddonHelper; @@ -97,6 +102,10 @@ public class SettingsFragment extends PreferenceFragment implements Preference.O findPreference("encrypt").setOnPreferenceChangeListener(this); findPreference("failsafe").setOnPreferenceChangeListener(this); findPreference("auto_backup").setOnPreferenceChangeListener(this); + findPreference("backup_size").setOnPreferenceChangeListener(this); + String currentBackup = findPreference("backup_size").getSharedPreferences().getString("backup_size", "20"); + findPreference("backup_size").setSummary("Currently " + currentBackup + "mb / Using " + lengthToString(BackupHelper.backupSize(), false)); + findPreference("readme").setOnPreferenceClickListener(this); findPreference("export").setOnPreferenceClickListener(this); findPreference("default_garden").setOnPreferenceClickListener(this); @@ -106,14 +115,14 @@ public class SettingsFragment extends PreferenceFragment implements Preference.O findPreference("backup_now").setOnPreferenceClickListener(this); findPreference("restore").setOnPreferenceClickListener(this); - findPreference("failsafe").setEnabled(((CheckBoxPreference)findPreference("encrypt")).isChecked()); + findPreference("failsafe").setEnabled(((SwitchPreference)findPreference("encrypt")).isChecked()); if (MainApplication.isFailsafe()) { - findPreference("failsafe").setTitle(""); - findPreference("failsafe").setSummary(""); - findPreference("encrypt").setTitle(""); - findPreference("encrypt").setSummary(""); + findPreference("failsafe").setTitle("Redacted"); + findPreference("failsafe").setSummary("Redacted"); + findPreference("encrypt").setTitle("Redacted"); + findPreference("encrypt").setSummary("Redacted"); } else { @@ -229,7 +238,17 @@ private void populateAddons() @Override public boolean onPreferenceChange(final Preference preference, Object newValue) { - if ("encrypt".equals(preference.getKey())) + if ("backup_size".equals(preference.getKey())) + { + String currentBackup = (String)newValue; + PreferenceManager.getDefaultSharedPreferences(getContext()).edit() + .putString("backup_size", currentBackup) + .apply(); + ((EditTextPreference)preference).setText(currentBackup); + BackupHelper.limitBackups(currentBackup); + findPreference("backup_size").setSummary("Currently " + currentBackup + "mb / Using " + lengthToString(BackupHelper.backupSize(), false)); + } + else if ("encrypt".equals(preference.getKey())) { if ((Boolean)newValue == true) { @@ -286,7 +305,7 @@ private void populateAddons() } else { - ((CheckBoxPreference)preference).setChecked(false); + ((SwitchPreference)preference).setChecked(false); Toast.makeText(getActivity(), "Error - passphrases did not match up", Toast.LENGTH_SHORT).show(); } } @@ -299,14 +318,14 @@ private void populateAddons() { @Override public void onClick(DialogInterface dialog, int which) { - ((CheckBoxPreference)preference).setChecked(false); + ((SwitchPreference)preference).setChecked(false); } }) - .setOnDismissListener(new DialogInterface.OnDismissListener() + .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override public void onDismiss(DialogInterface dialog) + @Override public void onCancel(DialogInterface dialog) { - ((CheckBoxPreference)preference).setChecked(false); + ((SwitchPreference)preference).setChecked(false); } }) .show(); @@ -343,7 +362,7 @@ private void populateAddons() } else { - ((CheckBoxPreference)preference).setChecked(true); + ((SwitchPreference)preference).setChecked(true); Toast.makeText(getActivity(), "Error - incorrect passphrase", Toast.LENGTH_SHORT).show(); } } @@ -392,7 +411,7 @@ else if ("failsafe".equals(preference.getKey())) } else { - ((CheckBoxPreference)preference).setChecked(false); + ((SwitchPreference)preference).setChecked(false); Toast.makeText(getActivity(), "Error - passphrases did not match up", Toast.LENGTH_SHORT).show(); } } @@ -405,7 +424,7 @@ else if ("failsafe".equals(preference.getKey())) { @Override public void onClick(DialogInterface dialog, int which) { - ((CheckBoxPreference)preference).setChecked(false); + ((SwitchPreference)preference).setChecked(false); } }) .show(); @@ -607,18 +626,27 @@ class BackupData String plantsPath; String gardenPath; String schedulePath; + long size = 0; @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); + boolean encrypted = plantsPath != null && plantsPath.endsWith("dat"); + return dateFormat.format(date) + " " + timeFormat.format(date) + " (" + (encrypted ? "encrypted " : "") + lengthToString(size, false) + ")"; } } // get list of backups File backupPath = new File(Environment.getExternalStorageDirectory(), "/backups/GrowTracker/"); String[] backupFiles = backupPath.list(); + + if (backupFiles == null || backupFiles.length == 0) + { + Toast.makeText(getActivity(), "There are no backups to restore from", Toast.LENGTH_LONG).show(); + return false; + } + Arrays.sort(backupFiles); final ArrayList backups = new ArrayList(); @@ -650,6 +678,7 @@ class BackupData BackupData backupData = new BackupData(); backupData.plantsPath = backupPath.getPath() + "/" + backup; backupData.date = date; + backupData.size = backupPath.length(); backups.add(backupData); continue; } @@ -670,19 +699,23 @@ class BackupData current = new BackupData(); } + File file = new File(backupPath.getPath() + "/" + backup); if (backup.contains("plants")) { current.plantsPath = backupPath.getPath() + "/" + backup; + current.size += file.length(); } if (backup.contains("gardens")) { current.gardenPath = backupPath.getPath() + "/" + backup; + current.size += file.length(); } if (backup.contains("schedules")) { current.schedulePath = backupPath.getPath() + "/" + backup; + current.size += file.length(); } } @@ -714,28 +747,55 @@ class BackupData MainApplication.setFailsafe(false); } + if (selectedBackup.plantsPath.endsWith("dat") && !MainApplication.isEncrypted()) + { + SnackBar.show(getActivity(), "You must have encrpted mode enabled with the same password to restore this backup", "Enable", new SnackBarListener() + { + @Override public void onSnackBarStarted(@NotNull Object o){} + @Override public void onSnackBarFinished(@NotNull Object o){} + + @Override public void onSnackBarAction(@NotNull Object o) + { + ((SwitchPreference)findPreference("encrypt")).setChecked(true); + onPreferenceChange(findPreference("encrypt"), true); + } + }); + return; + } + + FileManager.getInstance().copyFile(PlantManager.FILES_DIR + "/plants.json", PlantManager.FILES_DIR + "/plants.temp"); FileManager.getInstance().copyFile(selectedBackup.plantsPath, PlantManager.FILES_DIR + "/plants.json"); - boolean loaded = PlantManager.getInstance().load(); + boolean loaded = PlantManager.getInstance().load(true); if (selectedBackup.gardenPath != null) { + FileManager.getInstance().copyFile(GardenManager.FILES_DIR + "/gardens.json", GardenManager.FILES_DIR + "/gardens.temp"); FileManager.getInstance().copyFile(selectedBackup.gardenPath, GardenManager.FILES_DIR + "/gardens.json"); GardenManager.getInstance().load(); } if (selectedBackup.schedulePath != null) { + FileManager.getInstance().copyFile(ScheduleManager.FILES_DIR + "/schedules.json", ScheduleManager.FILES_DIR + "/schedules.temp"); 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); + String errorEnd = MainApplication.isEncrypted() ? "unencrypted" : "encryped"; + SnackBar.show(getActivity(), "Could not restore from backup " + selectedBackup + ". File may be " + errorEnd, Snackbar.LENGTH_INDEFINITE, null); + FileManager.getInstance().copyFile(PlantManager.FILES_DIR + "/plants.temp", PlantManager.FILES_DIR + "/plants.json"); + FileManager.getInstance().copyFile(GardenManager.FILES_DIR + "/gardens.temp", GardenManager.FILES_DIR + "/gardens.json"); + FileManager.getInstance().copyFile(ScheduleManager.FILES_DIR + "/schedules.temp", ScheduleManager.FILES_DIR + "/schedules.json"); + PlantManager.getInstance().load(); + GardenManager.getInstance().load(); + ScheduleManager.instance.load(); } else { Toast.makeText(getActivity(), "Restore to " + selectedBackup + " completed", Toast.LENGTH_LONG).show(); + getActivity().recreate(); } } }) @@ -756,4 +816,17 @@ class BackupData populateAddons(); } } + + public String lengthToString(long bytes, boolean si) + { + int unit = si ? 1000 : 1024; + if (bytes < unit) + { + return bytes + " B"; + } + + int exp = (int)(Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); + } } 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 30030a1b..e221b83e 100644 --- a/app/src/main/java/me/anon/grow/fragment/StatisticsFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/StatisticsFragment.java @@ -72,6 +72,8 @@ public static StatisticsFragment newInstance(int plantIndex) @Views.InjectView(R.id.germ_time_container) private View germTimeContainer; @Views.InjectView(R.id.veg_time) private TextView vegTime; @Views.InjectView(R.id.veg_time_container) private View vegTimeContainer; + @Views.InjectView(R.id.seedling_time) private TextView seedlingTime; + @Views.InjectView(R.id.seedling_time_container) private View seedlingTimeContainer; @Views.InjectView(R.id.cutting_time) private TextView cuttingTime; @Views.InjectView(R.id.cutting_time_container) private View cuttingTimeContainer; @Views.InjectView(R.id.flower_time) private TextView flowerTime; @@ -299,6 +301,12 @@ private void setStatistics() vegTimeContainer.setVisibility(View.VISIBLE); } + if (stages.containsKey(PlantStage.SEEDLING)) + { + seedlingTime.setText((int)TimeHelper.toDays(stages.get(PlantStage.SEEDLING)) + " days"); + seedlingTimeContainer.setVisibility(View.VISIBLE); + } + if (stages.containsKey(PlantStage.CUTTING)) { cuttingTime.setText((int)TimeHelper.toDays(stages.get(PlantStage.CUTTING)) + " days"); diff --git a/app/src/main/java/me/anon/grow/fragment/ViewPhotosFragment.java b/app/src/main/java/me/anon/grow/fragment/ViewPhotosFragment.java index 05a5f65c..37300afa 100644 --- a/app/src/main/java/me/anon/grow/fragment/ViewPhotosFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/ViewPhotosFragment.java @@ -51,9 +51,12 @@ import me.anon.lib.helper.ExportHelper; import me.anon.lib.helper.FabAnimator; import me.anon.lib.helper.PermissionHelper; +import me.anon.lib.helper.TimeHelper; import me.anon.lib.manager.PlantManager; import me.anon.lib.task.EncryptTask; +import me.anon.model.Action; import me.anon.model.Plant; +import me.anon.model.StageChange; /** * // TODO: Add class description @@ -119,6 +122,7 @@ public static ViewPhotosFragment newInstance(int plantIndex) } adapter = new ImageAdapter(); + adapter.plant = plant; adapter.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) @@ -241,7 +245,33 @@ private void setAdapter() if (!lastFileDate.equalsIgnoreCase(printedFileDate)) { lastFileDate = printedFileDate; - sections.add(new SectionedGridRecyclerViewAdapter.Section(index, printedFileDate)); + + StageChange lastChange = null; + StageChange currentChange = new StageChange(); + currentChange.setDate(fileDate); + + for (int actionIndex = plant.getActions().size() - 1; actionIndex >= 0; actionIndex--) + { + Action action = plant.getActions().get(actionIndex); + if (action instanceof StageChange) + { + if (action.getDate() < fileDate && lastChange == null) + { + lastChange = (StageChange)action; + break; + } + } + } + + String stageDayStr = ""; + if (lastChange != null) + { + int currentDays = (int)TimeHelper.toDays(Math.abs(currentChange.getDate() - lastChange.getDate())); + currentDays = (currentDays == 0 ? 1 : currentDays); + stageDayStr += " ~" + currentDays + lastChange.getNewStage().getPrintString().substring(0, 1).toLowerCase(); + } + + sections.add(new SectionedGridRecyclerViewAdapter.Section(index, printedFileDate + stageDayStr)); } } 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 d6604aa2..a585b69d 100644 --- a/app/src/main/java/me/anon/grow/fragment/WateringFragment.java +++ b/app/src/main/java/me/anon/grow/fragment/WateringFragment.java @@ -29,6 +29,8 @@ 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; @@ -217,12 +219,24 @@ else if (item.getItemId() == R.id.action_populate_previous) } } + Collections.sort(items, new Comparator() + { + @Override + public int compare(Action o1, Action o2) + { + if (o1.getDate() < o2.getDate()) return 1; + if (o1.getDate() > o2.getDate()) return -1; + return 0; + } + }); + ActionSelectDialogFragment actionSelectDialogFragment = new ActionSelectDialogFragment(items); actionSelectDialogFragment.setOnActionSelectedListener(new ActionSelectDialogFragment.OnActionSelectedListener() { @Override public void onActionSelected(Action action) { water = (Water)new Kryo().copy(action); + water.setDate(System.currentTimeMillis()); setUi(); } }); @@ -409,7 +423,8 @@ private void setUi() } }); - if (plants.size() == 1) +// if (plants.size() == 1) + if (water != null) { if (water.getPh() != null) { diff --git a/app/src/main/java/me/anon/grow/service/ExportService.kt b/app/src/main/java/me/anon/grow/service/ExportService.kt new file mode 100644 index 00000000..a0d21935 --- /dev/null +++ b/app/src/main/java/me/anon/grow/service/ExportService.kt @@ -0,0 +1,70 @@ +package me.anon.grow.service + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.IBinder +import com.google.gson.reflect.TypeToken +import me.anon.grow.fragment.PlantDetailsFragment +import me.anon.lib.ExportCallback +import me.anon.lib.helper.ExportHelper +import me.anon.lib.helper.GsonHelper +import me.anon.lib.helper.NotificationHelper +import me.anon.model.Plant +import java.io.File + +/** + * Service for exporting + */ +class ExportService : Service() +{ + companion object + { + @JvmStatic + public fun export(context: Context, plants: ArrayList, title: String, name: String) + { + val plantStr = GsonHelper.getGson().toJson(plants, object : TypeToken>(){}.type) + val intent = Intent(context, ExportService::class.java) + intent.putExtra("plants", plantStr) + intent.putExtra("title", title) + intent.putExtra("name", name) + context.startService(intent) + } + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int + { + intent?.let { + val plants = GsonHelper.parse>(it.extras.getString("plants", "[]"), object : TypeToken>(){}.type) as ArrayList + val title = it.getStringExtra("title") + val name = it.getStringExtra("name") + + NotificationHelper.createExportChannel(this) + NotificationHelper.sendExportNotification(this, "Exporting grow log for $name", "Exporting grow log for $name") + + ExportHelper.exportPlants(this, plants, title, object : ExportCallback() + { + override fun onCallback(context: Context, file: File) + { + super.onCallback(context, file) + NotificationHelper.sendExportCompleteNotification(context, "Export of $name complete", "Exported $name to ${file.absolutePath}", file) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + { + PlantDetailsFragment.MediaScannerWrapper(context, file.parent, "application/zip").scan() + } + else + { + context.sendBroadcast(Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(file))) + } + } + }) + } + + return START_NOT_STICKY + } +} diff --git a/app/src/main/java/me/anon/lib/SnackBar.kt b/app/src/main/java/me/anon/lib/SnackBar.kt index 237a1e2c..fa0d36ba 100644 --- a/app/src/main/java/me/anon/lib/SnackBar.kt +++ b/app/src/main/java/me/anon/lib/SnackBar.kt @@ -20,32 +20,47 @@ class SnackBar { @JvmStatic public fun show(context: Activity, @StringRes messageRes: Int, @StringRes actionTextRes: Int = -1, - listener: SnackBarListener + listener: SnackBarListener? ) { show(context, context.getString(messageRes), if (actionTextRes != -1) context.getString(actionTextRes) else "", + Snackbar.LENGTH_LONG, listener ) } @JvmStatic - public fun show(context: Activity, message: String, listener: SnackBarListener) + public fun show(context: Activity, message: String, listener: SnackBarListener?) { - show(context, message, "", listener) + show(context, message, "", Snackbar.LENGTH_LONG, listener) + } + + @JvmStatic + public fun show(context: Activity, message: String, length: Int, listener: SnackBarListener?) + { + show(context, message, "", length, listener) } @JvmStatic public fun show(context: Activity, message: String, actionText: String = "", - listener: SnackBarListener + listener: SnackBarListener? + ) + { + show(context, message, actionText, Snackbar.LENGTH_LONG, listener) + } + + @JvmStatic + public fun show(context: Activity, message: String, actionText: String = "", length: Int = Snackbar.LENGTH_LONG, + listener: SnackBarListener? ) { - SnackBar().show(context, message, actionText, { - listener.onSnackBarStarted(0) + SnackBar().show(context, message, actionText, Snackbar.LENGTH_LONG, { + listener?.onSnackBarStarted(0) }, { - listener.onSnackBarFinished(0) + listener?.onSnackBarFinished(0) }, { - listener.onSnackBarAction(0) + listener?.onSnackBarAction(0) }) } } @@ -63,17 +78,28 @@ class SnackBar { show(context, context.getString(messageRes), if (actionTextRes != -1) context.getString(actionTextRes) else "", + Snackbar.LENGTH_LONG, start, end, action ) } - public fun show(context: Activity, message: String, actionText: String = "", + public fun show(context: Activity, message: String, actionText: String = "", length: Int = Snackbar.LENGTH_LONG, start: () -> kotlin.Unit = {}, end: () -> kotlin.Unit = {}, action: () -> kotlin.Unit = {} ) { - val snackbar = Snackbar.make(context.findViewById(android.R.id.content), message, Snackbar.LENGTH_SHORT) + val snackbar = Snackbar.make(context.findViewById(android.R.id.content), message, length) + var actionText = actionText + var action = action + + if (length == Snackbar.LENGTH_INDEFINITE) + { + actionText = "Dismiss" + action = { + snackbar.dismiss() + } + } if (actionText.isNotEmpty()) { @@ -95,6 +121,7 @@ class SnackBar } }) + snackbar.show() } } diff --git a/app/src/main/java/me/anon/lib/ext/BooleanUtils.kt b/app/src/main/java/me/anon/lib/ext/BooleanUtils.kt new file mode 100644 index 00000000..dc5e2387 --- /dev/null +++ b/app/src/main/java/me/anon/lib/ext/BooleanUtils.kt @@ -0,0 +1,28 @@ +package me.anon.lib.ext + +import android.view.View + +public fun Boolean?.asVisibility(): Int +{ + return when (this) + { + true -> View.VISIBLE + else -> View.GONE + } +} + +public fun Boolean?.toValue(trueValue: Any?, falseValue: Any?): R +{ + return when (this) + { + true -> trueValue as R + false, null -> falseValue as R + } +} + +/** + * Ternary implementation + * Usage: t ?: + */ +@Suppress("FunctionName") +public infix fun Boolean?.T(value: T): T? = if (this == true) value else null diff --git a/app/src/main/java/me/anon/lib/helper/BackupHelper.kt b/app/src/main/java/me/anon/lib/helper/BackupHelper.kt index 6c8abb5c..28babaff 100644 --- a/app/src/main/java/me/anon/lib/helper/BackupHelper.kt +++ b/app/src/main/java/me/anon/lib/helper/BackupHelper.kt @@ -1,6 +1,9 @@ package me.anon.lib.helper +import android.content.Context import android.os.Environment +import me.anon.grow.MainApplication +import me.anon.lib.ext.T import me.anon.lib.manager.FileManager import me.anon.lib.manager.PlantManager import java.io.File @@ -14,15 +17,46 @@ object BackupHelper public var FILES_PATH = Environment.getExternalStorageDirectory().absolutePath + "/backups/GrowTracker" @JvmStatic - public fun backupJson(): File + public fun backupJson(): File? { + if (MainApplication.isFailsafe()) return null + + limitBackups() + val isEncrypted = MainApplication.isEncrypted() val time = System.currentTimeMillis() val backupPath = File(FILES_PATH) + val ext = isEncrypted T "dat" ?: "bak" 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") + FileManager.getInstance().copyFile("${PlantManager.FILES_DIR}/plants.json", "$FILES_PATH/$time.plants.json.$ext") + FileManager.getInstance().copyFile("${PlantManager.FILES_DIR}/schedules.json", "$FILES_PATH/$time.schedules.json.$ext") + FileManager.getInstance().copyFile("${PlantManager.FILES_DIR}/gardens.json", "$FILES_PATH/$time.gardens.json.$ext") return backupPath } + + @JvmStatic + public fun backupSize(): Long + { + val path = File(BackupHelper.FILES_PATH) + return path.listFiles().fold(0L, { acc, file -> acc + file.length() }) + } + + @JvmStatic + public fun limitBackups(size: String = MainApplication.getDefaultPreferences().getString("backup_size", "20")!!) + { + val files = File(BackupHelper.FILES_PATH).listFiles() + val sorted = ArrayList(files.sortedBy { it.lastModified() }) + val limit = size.toInt() * 1_048_576 + + var currentSize = backupSize() + while (currentSize > limit) + { + val remove = sorted.removeAt(0) + val len = remove.length() + if (remove.delete()) + { + currentSize -= len + } + } + } } 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 732024e9..f1490406 100644 --- a/app/src/main/java/me/anon/lib/helper/ExportHelper.java +++ b/app/src/main/java/me/anon/lib/helper/ExportHelper.java @@ -1,13 +1,11 @@ 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.preference.PreferenceManager; import android.support.annotation.NonNull; @@ -24,14 +22,17 @@ import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.util.Zip4jConstants; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.OutputStream; import java.text.DateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.SortedMap; import me.anon.grow.R; @@ -58,15 +59,10 @@ public class ExportHelper /** * Creates a grow log for given plant and zips them into a compressed file - * - * @param context - * @param plant - * @param callback - * - * @return */ - @Nullable public static void exportPlant(final Context context, @NonNull final Plant plant, final ExportCallback callback) + @Nullable public static void exportPlants(final Context context, @NonNull final ArrayList plants, String name, final ExportCallback callback) { + String exportInt = "" + System.currentTimeMillis(); String folderPath = ""; Unit measureUnit = Unit.getSelectedMeasurementUnit(context); Unit deliveryUnit = Unit.getSelectedDeliveryUnit(context); @@ -81,401 +77,415 @@ public class ExportHelper folderPath = Environment.DIRECTORY_DOWNLOADS; } - File exportFolder = new File(folderPath + "/GrowLogs/"); + File exportFolder = new File(folderPath + "/GrowLogs/" + exportInt); exportFolder.mkdirs(); - // temp folder to write to - final File tempFolder = new File(exportFolder.getAbsolutePath() + "/" + plant.getId()); + final File finalFile = new File(folderPath + "/GrowLogs/" + name + ".zip"); - if (tempFolder.exists()) + if (finalFile.exists()) { - deleteRecursive(tempFolder); + finalFile.delete(); } - tempFolder.mkdir(); - - long startDate = plant.getPlantDate(); - long endDate = System.currentTimeMillis(); - long feedDifference = 0L; - long waterDifference = 0L; - long lastWater = 0L; - int totalWater = 0, totalFlush = 0; - - for (Action action : plant.getActions()) + try { - if (action instanceof StageChange) - { - if (((StageChange)action).getNewStage() == PlantStage.HARVESTED) - { - endDate = action.getDate(); - } - } + final ZipFile outFile = new ZipFile(finalFile); + final ZipParameters params = new ZipParameters(); + params.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + params.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); - if (action.getClass() == Water.class) + for (Plant plant : plants) { - if (lastWater != 0) + // temp folder to write to + final String zipPathPrefix = plants.size() == 1 ? "" : plant.getName() + "/"; + + long startDate = plant.getPlantDate(); + long endDate = System.currentTimeMillis(); + long feedDifference = 0L; + long waterDifference = 0L; + long lastWater = 0L; + int totalWater = 0, totalFlush = 0; + final Set additiveNames = new HashSet<>(); + + for (Action action : plant.getActions()) { - waterDifference += Math.abs(action.getDate() - lastWater); - } + if (action instanceof StageChange) + { + if (((StageChange)action).getNewStage() == PlantStage.HARVESTED) + { + endDate = action.getDate(); + } + } - totalWater++; - lastWater = action.getDate(); - } + if (action.getClass() == Water.class) + { + if (lastWater != 0) + { + waterDifference += Math.abs(action.getDate() - lastWater); + } - if (action instanceof EmptyAction && ((EmptyAction)action).getAction() == Action.ActionName.FLUSH) - { - totalFlush++; - } - } + List actionAdditives = ((Water)action).getAdditives(); + for (Additive additive : actionAdditives) + { + additiveNames.add(additive.getDescription()); + } - long seconds = ((endDate - startDate) / 1000); - double days = (double)seconds * 0.0000115741d; + totalWater++; + lastWater = action.getDate(); + } - StringBuffer plantDetails = new StringBuffer(1000); - plantDetails.append("#").append(plant.getName()).append(" Grow Log"); - plantDetails.append(NEW_LINE); - plantDetails.append("*Strain*: ").append(plant.getStrain()); - plantDetails.append(NEW_LINE); - plantDetails.append("*Is clone?*: ").append(plant.isClone()); - plantDetails.append(NEW_LINE); - plantDetails.append("*Medium*: ").append(plant.getMedium().getPrintString()); - plantDetails.append(NEW_LINE); + if (action instanceof EmptyAction && ((EmptyAction)action).getAction() == Action.ActionName.FLUSH) + { + totalFlush++; + } + } - plantDetails.append("##Stages"); - plantDetails.append(NEW_LINE); + long seconds = ((endDate - startDate) / 1000); + double days = (double)seconds * 0.0000115741d; - SortedMap stages = plant.calculateStageTime(); - Map plantStages = plant.getStages(); + StringBuffer plantDetails = new StringBuffer(1000); + plantDetails.append("#").append(plant.getName()).append(" Grow Log"); + plantDetails.append(NEW_LINE); + plantDetails.append("*Strain*: ").append(plant.getStrain()); + plantDetails.append(NEW_LINE); + plantDetails.append("*Is clone?*: ").append(plant.isClone()); + plantDetails.append(NEW_LINE); + plantDetails.append("*Medium*: ").append(plant.getMedium().getPrintString()); + plantDetails.append(NEW_LINE); - for (PlantStage plantStage : stages.keySet()) - { - plantDetails.append("- *").append(plantStage.getPrintString()).append("*: "); - plantDetails.append(printableDate(context, plantStages.get(plantStage).getDate())); + plantDetails.append("##Stages"); + plantDetails.append(NEW_LINE); - if (plantStage != PlantStage.PLANTED && plantStage != PlantStage.HARVESTED) - { - plantDetails.append(" (").append((int)TimeHelper.toDays(stages.get(plantStage))).append(" days)"); - } + SortedMap stages = plant.calculateStageTime(); + Map plantStages = plant.getStages(); - plantDetails.append(NEW_LINE); - } + for (PlantStage plantStage : stages.keySet()) + { + plantDetails.append("- *").append(plantStage.getPrintString()).append("*: "); + plantDetails.append(printableDate(context, plantStages.get(plantStage).getDate())); - plantDetails.append("##General stats"); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Total grow time*: ").append(String.format("%1$,.2f days", days)); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Total waters*: ").append(String.valueOf(totalWater)); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Total flushes*: ").append(String.valueOf(totalFlush)); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Average time between waterings*: ").append(String.format("%1$,.2f days", (TimeHelper.toDays(waterDifference) / (double)totalWater))); - plantDetails.append(NEW_LINE); - - String[] avePh = new String[3]; - StatsHelper.setInputData(plant, null, avePh); - plantDetails.append(" - *Minimum input pH*: ").append(avePh[0]); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Maximum input pH*: ").append(avePh[1]); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Average input pH*: ").append(avePh[2]); - plantDetails.append(NEW_LINE); - - String[] avePpm = new String[3]; - StatsHelper.setPpmData(plant, null, avePpm, usingEc); - plantDetails.append(" - *Minimum input " + (usingEc ? "EC" : "ppm") + "*: ").append(avePpm[0]); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Maximum input " + (usingEc ? "EC" : "ppm") + "*: ").append(avePpm[1]); - plantDetails.append(NEW_LINE); - plantDetails.append(" - *Average input " + (usingEc ? "EC" : "ppm") + "*: ").append(avePpm[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); - - for (Action action : plant.getActions()) - { - plantDetails.append("###").append(printableDate(context, action.getDate())); - plantDetails.append(NEW_LINE); + if (plantStage != PlantStage.PLANTED && plantStage != PlantStage.HARVESTED) + { + plantDetails.append(" (").append((int)TimeHelper.toDays(stages.get(plantStage))).append(" days)"); + } - if (action.getClass() == Water.class) - { - plantDetails.append("*Type*: Watering"); + plantDetails.append(NEW_LINE); + } + + plantDetails.append("##General stats"); plantDetails.append(NEW_LINE); - } - else if (action instanceof EmptyAction && ((EmptyAction)action).getAction() != null) - { - plantDetails.append(((EmptyAction)action).getAction().getPrintString()); + plantDetails.append(" - *Total grow time*: ").append(String.format("%1$,.2f days", days)); plantDetails.append(NEW_LINE); - } - else if (action instanceof NoteAction) - { - plantDetails.append("*Note*"); + plantDetails.append(" - *Total waters*: ").append(String.valueOf(totalWater)); plantDetails.append(NEW_LINE); - } - else if (action instanceof StageChange) - { - plantDetails.append("*Changed state to*: ").append(((StageChange)action).getNewStage().getPrintString()); + plantDetails.append(" - *Total flushes*: ").append(String.valueOf(totalFlush)); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Average time between waterings*: ").append(String.format("%1$,.2f days", (TimeHelper.toDays(waterDifference) / (double)totalWater))); plantDetails.append(NEW_LINE); - } - - if (Water.class.isAssignableFrom(action.getClass())) - { - boolean newLine = false; - if (((Water)action).getPh() != null) - { - plantDetails.append("*In pH*: "); - plantDetails.append(((Water)action).getPh()); - plantDetails.append(", "); - newLine = true; - } + String[] avePh = new String[3]; + StatsHelper.setInputData(plant, null, avePh); + plantDetails.append(" - *Minimum input pH*: ").append(avePh[0]); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Maximum input pH*: ").append(avePh[1]); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Average input pH*: ").append(avePh[2]); + plantDetails.append(NEW_LINE); - if (((Water)action).getRunoff() != null) - { - plantDetails.append("*Out pH*: "); - plantDetails.append(((Water)action).getRunoff()); - plantDetails.append(", "); - newLine = true; - } + String[] avePpm = new String[3]; + StatsHelper.setPpmData(plant, null, avePpm, usingEc); + plantDetails.append(" - *Minimum input " + (usingEc ? "EC" : "ppm") + "*: ").append(avePpm[0]); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Maximum input " + (usingEc ? "EC" : "ppm") + "*: ").append(avePpm[1]); + plantDetails.append(NEW_LINE); + plantDetails.append(" - *Average input " + (usingEc ? "EC" : "ppm") + "*: ").append(avePpm[2]); + plantDetails.append(NEW_LINE); - if (((Water)action).getPpm() != null) - { - plantDetails.append("*PPM*: "); - plantDetails.append(((Water)action).getPpm()); - plantDetails.append(", "); - newLine = true; - } + 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); - if (((Water)action).getAmount() != null) + if (!additiveNames.isEmpty()) { - plantDetails.append("*Amount*: "); - plantDetails.append(ML.to(deliveryUnit, ((Water)action).getAmount())); - plantDetails.append(deliveryUnit.getLabel()); - plantDetails.append(", "); - newLine = true; + plantDetails.append("##Additives used"); + plantDetails.append(NEW_LINE); + plantDetails.append(" - "); + plantDetails.append(TextUtils.join(NEW_LINE + " - ", additiveNames)); + plantDetails.append(NEW_LINE); } - if (((Water)action).getTemp() != null) - { - plantDetails.append("*Temp*: "); - plantDetails.append(((Water)action).getTemp()); - plantDetails.append("ºC, "); - newLine = true; - } + plantDetails.append("##Timeline"); + plantDetails.append(NEW_LINE); - if (((Water)action).getAdditives().size() > 0) + for (Action action : plant.getActions()) { + plantDetails.append("###").append(printableDate(context, action.getDate())); plantDetails.append(NEW_LINE); - plantDetails.append("*Additives:*"); - plantDetails.append("\r\n"); - for (Additive additive : ((Water)action).getAdditives()) + if (action.getClass() == Water.class) { - 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); - - plantDetails.append("\r\n"); - plantDetails.append(" - "); - plantDetails.append(additive.getDescription()); - plantDetails.append(" - "); - plantDetails.append(amountStr); - plantDetails.append(measureUnit.getLabel()); - plantDetails.append("/"); - plantDetails.append(deliveryUnit.getLabel()); + plantDetails.append("*Type*: Watering"); + plantDetails.append(NEW_LINE); + } + else if (action instanceof EmptyAction && ((EmptyAction)action).getAction() != null) + { + plantDetails.append(((EmptyAction)action).getAction().getPrintString()); + plantDetails.append(NEW_LINE); + } + else if (action instanceof NoteAction) + { + plantDetails.append("*Note*"); + plantDetails.append(NEW_LINE); + } + else if (action instanceof StageChange) + { + plantDetails.append("*Changed state to*: ").append(((StageChange)action).getNewStage().getPrintString()); + plantDetails.append(NEW_LINE); } - } - if (newLine) - { - plantDetails.delete(plantDetails.length() - 2, plantDetails.length() - 1); - plantDetails.append(NEW_LINE); + if (Water.class.isAssignableFrom(action.getClass())) + { + boolean newLine = false; + + if (((Water)action).getPh() != null) + { + plantDetails.append("*In pH*: "); + plantDetails.append(((Water)action).getPh()); + plantDetails.append(", "); + newLine = true; + } + + if (((Water)action).getRunoff() != null) + { + plantDetails.append("*Out pH*: "); + plantDetails.append(((Water)action).getRunoff()); + plantDetails.append(", "); + newLine = true; + } + + if (((Water)action).getPpm() != null) + { + plantDetails.append("*PPM*: "); + plantDetails.append(((Water)action).getPpm()); + plantDetails.append(", "); + newLine = true; + } + + if (((Water)action).getAmount() != null) + { + plantDetails.append("*Amount*: "); + plantDetails.append(ML.to(deliveryUnit, ((Water)action).getAmount())); + plantDetails.append(deliveryUnit.getLabel()); + plantDetails.append(", "); + newLine = true; + } + + if (((Water)action).getTemp() != null) + { + plantDetails.append("*Temp*: "); + plantDetails.append(((Water)action).getTemp()); + plantDetails.append("ºC, "); + newLine = true; + } + + if (((Water)action).getAdditives().size() > 0) + { + plantDetails.append(NEW_LINE); + plantDetails.append("*Additives:*"); + plantDetails.append("\r\n"); + + for (Additive additive : ((Water)action).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); + + plantDetails.append("\r\n"); + plantDetails.append(" - "); + plantDetails.append(additive.getDescription()); + plantDetails.append(" - "); + plantDetails.append(amountStr); + plantDetails.append(measureUnit.getLabel()); + plantDetails.append("/"); + plantDetails.append(deliveryUnit.getLabel()); + } + } + + if (newLine) + { + plantDetails.delete(plantDetails.length() - 2, plantDetails.length() - 1); + plantDetails.append(NEW_LINE); + } + } + + if (!TextUtils.isEmpty(action.getNotes())) + { + plantDetails.append(action.getNotes()); + plantDetails.append(NEW_LINE); + } } - } - if (!TextUtils.isEmpty(action.getNotes())) - { - plantDetails.append(action.getNotes()); + plantDetails.append("##Raw plant data"); plantDetails.append(NEW_LINE); - } - } + plantDetails.append("```").append("\r\n").append(GsonHelper.parse(plant)).append("\r\n").append("```"); + plantDetails.append(NEW_LINE); + plantDetails.append("Generated using [Grow Tracker](https://github.com/7LPdWcaW/GrowTracker-Android)"); - plantDetails.append("##Raw plant data"); - plantDetails.append(NEW_LINE); - plantDetails.append("```").append("\r\n").append(GsonHelper.parse(plant)).append("\r\n").append("```"); - plantDetails.append(NEW_LINE); - plantDetails.append("Generated using [Grow Tracker](https://github.com/7LPdWcaW/GrowTracker-Android)"); + // Write the log + try + { + ZipParameters parameters = new ZipParameters(); + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + parameters.setFileNameInZip(zipPathPrefix + "growlog.md"); + parameters.setSourceExternalStream(true); - // Write the log - try - { - FileWriter fileWriter = new FileWriter(tempFolder.getAbsolutePath() + "/growlog.md", false); - fileWriter.write(plantDetails.toString()); - fileWriter.flush(); - fileWriter.close(); - } - catch (Exception e) - { - e.printStackTrace(); - } + outFile.addStream(new ByteArrayInputStream(plantDetails.toString().getBytes("UTF-8")), parameters); + } + catch (Exception e) + { + e.printStackTrace(); + } - try - { - // Create stats charts and save images - final File finalFile = new File(exportFolder.getAbsolutePath() + "/" + plant.getName().replaceAll("[^a-zA-Z0-9]+", "-") + ".zip"); + try + { + int width = 1024 + (totalWater * 20); + int height = 512; + int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + + LineChart additives = new LineChart(context); + additives.setPadding(100, 100, 100, 100); + 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, additiveNames); + additives.getData().setDrawValues(true); - if (finalFile.exists()) - { - finalFile.delete(); - } + try + { + ZipParameters parameters = new ZipParameters(); + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + parameters.setFileNameInZip(zipPathPrefix + "additives.jpg"); + parameters.setSourceExternalStream(true); - final ZipFile outFile = new ZipFile(finalFile); - final ZipParameters params = new ZipParameters(); - params.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); - params.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL); - outFile.addFile(new File(tempFolder.getAbsolutePath() + "/growlog.md"), params); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + additives.getChartBitmap().compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + ByteArrayInputStream stream = new ByteArrayInputStream(outputStream.toByteArray()); + outFile.addStream(stream, parameters); - if (new File(tempFolder.getAbsolutePath() + "/images/").exists()) - { - outFile.addFolder(new File(tempFolder.getAbsolutePath() + "/images/"), params); - } + stream.close(); + } + catch (Exception e) + { + e.printStackTrace(); + } - int width = 512 + (totalWater * 10); - int height = 512; - 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); + LineChart inputPh = new LineChart(context); + inputPh.setPadding(100, 100, 100, 100); + inputPh.setLayoutParams(new ViewGroup.LayoutParams(width, height)); + inputPh.setMinimumWidth(width); + inputPh.setMinimumHeight(height); + inputPh.measure(widthMeasureSpec, heightMeasureSpec); + inputPh.requestLayout(); + inputPh.layout(0, 0, width, height); + StatsHelper.setInputData(plant, inputPh, null); + inputPh.getData().setDrawValues(true); - stream.close(); - } - catch (Exception e) - { - e.printStackTrace(); - } + try + { + ZipParameters parameters = new ZipParameters(); + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + parameters.setFileNameInZip(zipPathPrefix + "input-ph.jpg"); + parameters.setSourceExternalStream(true); - LineChart inputPh = new LineChart(context); - inputPh.setLayoutParams(new ViewGroup.LayoutParams(width, height)); - inputPh.setMinimumWidth(width); - inputPh.setMinimumHeight(height); - inputPh.measure(widthMeasureSpec, heightMeasureSpec); - inputPh.requestLayout(); - inputPh.layout(0, 0, width, height); - StatsHelper.setInputData(plant, inputPh, null); - inputPh.getData().setDrawValues(true); - - try - { - OutputStream stream = new FileOutputStream(tempFolder.getAbsolutePath() + "/input-ph.png"); - inputPh.getChartBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + inputPh.getChartBitmap().compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + ByteArrayInputStream stream = new ByteArrayInputStream(outputStream.toByteArray()); + outFile.addStream(stream, parameters); - stream.close(); - } - catch (Exception e) - { - e.printStackTrace(); - } + stream.close(); + } + catch (Exception e) + { + e.printStackTrace(); + } - LineChart ppm = new LineChart(context); - ppm.setLayoutParams(new ViewGroup.LayoutParams(width, height)); - ppm.setMinimumWidth(width); - ppm.setMinimumHeight(height); - ppm.measure(widthMeasureSpec, heightMeasureSpec); - ppm.requestLayout(); - ppm.layout(0, 0, width, height); - StatsHelper.setPpmData(plant, ppm, null, usingEc); - ppm.getData().setDrawValues(true); - - try - { - OutputStream stream = new FileOutputStream(tempFolder.getAbsolutePath() + "/" + (usingEc ? "ec" : "ppm") + ".png"); - ppm.getChartBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream); + LineChart ppm = new LineChart(context); + ppm.setPadding(100, 100, 100, 100); + ppm.setLayoutParams(new ViewGroup.LayoutParams(width, height)); + ppm.setMinimumWidth(width); + ppm.setMinimumHeight(height); + ppm.measure(widthMeasureSpec, heightMeasureSpec); + ppm.requestLayout(); + ppm.layout(0, 0, width, height); + StatsHelper.setPpmData(plant, ppm, null, usingEc); + ppm.getData().setDrawValues(true); - stream.close(); - } - catch (Exception e) - { - e.printStackTrace(); - } + try + { + ZipParameters parameters = new ZipParameters(); + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + parameters.setFileNameInZip(zipPathPrefix + (usingEc ? "ec" : "ppm") + ".jpg"); + parameters.setSourceExternalStream(true); - 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 - { - OutputStream stream = new FileOutputStream(tempFolder.getAbsolutePath() + "/temp.png"); - temp.getChartBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ppm.getChartBitmap().compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + ByteArrayInputStream stream = new ByteArrayInputStream(outputStream.toByteArray()); + outFile.addStream(stream, parameters); - 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); - } + LineChart temp = new LineChart(context); + temp.setPadding(100, 100, 100, 100); + 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); - if (new File(tempFolder.getAbsolutePath() + "/input-ph.png").exists()) - { - outFile.addFile(new File(tempFolder.getAbsolutePath() + "/input-ph.png"), params); - } + try + { + ZipParameters parameters = new ZipParameters(); + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + parameters.setFileNameInZip(zipPathPrefix + "temp.jpg"); + parameters.setSourceExternalStream(true); - if (new File(tempFolder.getAbsolutePath() + "/ppm.png").exists()) - { - outFile.addFile(new File(tempFolder.getAbsolutePath() + "/ppm.png"), params); - } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + temp.getChartBitmap().compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + ByteArrayInputStream stream = new ByteArrayInputStream(outputStream.toByteArray()); + outFile.addStream(stream, parameters); - if (new File(tempFolder.getAbsolutePath() + "/ec.png").exists()) - { - outFile.addFile(new File(tempFolder.getAbsolutePath() + "/ec.png"), params); + stream.close(); + } + catch (Exception e) + { + e.printStackTrace(); + } } - - if (new File(tempFolder.getAbsolutePath() + "/temp.png").exists()) + catch (Exception e) { - outFile.addFile(new File(tempFolder.getAbsolutePath() + "/temp.png"), params); + e.printStackTrace(); } } - catch (Exception e) - { - e.printStackTrace(); - } - copyImagesAndFinish(context, plant, tempFolder, finalFile, callback); + copyImagesAndFinish(context, plants, outFile, callback); } catch (ZipException e) { @@ -483,7 +493,7 @@ else if (action instanceof StageChange) } } - protected static void copyImagesAndFinish(Context context, final Plant plant, final File tempFolder, final File finalFile, final ExportCallback callback) + protected static void copyImagesAndFinish(Context context, final ArrayList plant, final ZipFile finalFile, final ExportCallback callback) { final Context appContext = context.getApplicationContext(); new AsyncTask() @@ -495,84 +505,76 @@ protected static void copyImagesAndFinish(Context context, final Plant plant, fi @Override protected void onPreExecute() { plantIndex = PlantManager.getInstance().getPlants().indexOf(plant); - notificationManager = (NotificationManager)appContext.getSystemService(Context.NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= 26) - { - NotificationChannel channel = new NotificationChannel("export", "Export status", NotificationManager.IMPORTANCE_DEFAULT); - notificationManager.createNotificationChannel(channel); - } + NotificationHelper.createExportChannel(appContext); notificationManager = (NotificationManager)appContext.getSystemService(Context.NOTIFICATION_SERVICE); exportNotification = new NotificationCompat.Builder(appContext, "export") - .setContentText("Exporting grow log for " + plant.getName()) + .setContentText("Exporting grow log for " + (plant.size() == 1 ? plant.get(0).getName() : "multiple plants")) .setContentTitle("Exporting") .setContentIntent(PendingIntent.getActivity(appContext, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT)) - .setTicker("Exporting grow log for " + plant.getName()) + .setTicker("Exporting grow log for " + (plant.size() == 1 ? plant.get(0).getName() : "multiple plants")) + .setSmallIcon(R.drawable.ic_stat_name) .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(R.drawable.ic_stat_name); + .setSound(null); - notificationManager.notify(plantIndex, exportNotification.build()); + notificationManager.notify(0, exportNotification.build()); } @Override protected void onProgressUpdate(Integer... values) { exportNotification.setProgress(values[1], values[0], false); - notificationManager.notify(plantIndex, exportNotification.build()); + notificationManager.notify(0, exportNotification.build()); } @Override protected File doInBackground(Plant... params) { - Plant plant = params[0]; + int total = 0; + for (Plant plant : plant) + { + total += plant.getImages().size(); + } - // Copy images to dir - int count = 0, total = plant.getImages().size(); - for (String filePath : plant.getImages()) + int count = 0; + for (int index = 0; index < params.length; index++) { - try - { - File currentImage = new File(filePath); - long fileDate = Long.parseLong(currentImage.getName().replaceAll("[^0-9]", "")); + Plant plant = params[index]; + final String zipPathPrefix = params.length == 1 ? "" : plant.getName() + "/"; - if (fileDate == 0) + // Copy images to dir + for (String filePath : plant.getImages()) + { + try { - fileDate = currentImage.lastModified(); - } - - File imageFolderPath = new File(tempFolder.getAbsolutePath() + "/images/" + dateFolder(appContext, fileDate) + "/"); - imageFolderPath.mkdirs(); + File currentImage = new File(filePath); + long fileDate = Long.parseLong(currentImage.getName().replaceAll("[^0-9]", "")); - FileInputStream fis = new FileInputStream(currentImage); - FileOutputStream fos = new FileOutputStream(new File(imageFolderPath.getAbsolutePath() + "/" + fileDate + ".jpg")); + if (fileDate == 0) + { + fileDate = currentImage.lastModified(); + } - byte[] buffer = new byte[8192]; - int len = 0; + ZipParameters parameters = new ZipParameters(); + parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); + parameters.setFileNameInZip(zipPathPrefix + "images/" + dateFolder(appContext, fileDate) + "/" + fileDate + ".jpg"); + parameters.setSourceExternalStream(true); - while ((len = fis.read(buffer)) != -1) + FileInputStream fis = new FileInputStream(currentImage); + finalFile.addStream(fis, parameters); + } + catch (Exception e) { - fos.write(buffer, 0, len); + e.printStackTrace(); } - fis.close(); - fos.flush(); - fos.close(); + publishProgress(++count, total); } - catch (Exception e) - { - e.printStackTrace(); - } - - publishProgress(++count, total); } - deleteRecursive(tempFolder); - callback.onCallback(appContext, finalFile); - + callback.onCallback(appContext, finalFile.getFile()); return null; } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, plant); + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, plant.toArray(new Plant[0])); } /** diff --git a/app/src/main/java/me/anon/lib/helper/NotificationHelper.kt b/app/src/main/java/me/anon/lib/helper/NotificationHelper.kt new file mode 100644 index 00000000..0c7726f3 --- /dev/null +++ b/app/src/main/java/me/anon/lib/helper/NotificationHelper.kt @@ -0,0 +1,83 @@ +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.Color +import android.os.Build +import android.support.v4.app.NotificationCompat +import android.support.v4.content.FileProvider +import me.anon.grow.R +import java.io.File + +/** + * Helper class for sending notification + */ +object NotificationHelper +{ + @JvmStatic + public fun createExportChannel(context: Context) + { + if (Build.VERSION.SDK_INT >= 26) + { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel("export", "Export status", NotificationManager.IMPORTANCE_HIGH) + channel.setSound(null, null) + channel.enableLights(false) + channel.enableVibration(false) + channel.lightColor = Color.GREEN + + notificationManager.createNotificationChannel(channel) + } + } + + @JvmStatic + public fun sendExportNotification(context: Context, title: String, message: String) + { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(0) + + val exportNotification = NotificationCompat.Builder(context, "export") + .setContentText(title) + .setContentTitle("Exporting") + .setContentIntent(PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT)) + .setTicker(message) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setSmallIcon(R.drawable.ic_stat_name) + .setSound(null) + .build() + + notificationManager.notify(0, exportNotification) + } + + @JvmStatic + public fun sendExportCompleteNotification(context: Context, title: String, message: String, file: File) + { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(0) + + val openIntent = Intent(Intent.ACTION_VIEW) + val apkURI = FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", file) + openIntent.setDataAndType(apkURI, "application/zip") + openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + val finishNotification = NotificationCompat.Builder(context, "export") + .setContentText(message) + .setTicker(title) + .setContentTitle("Export Complete") + .setContentIntent(PendingIntent.getActivity(context, 0, openIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + .setStyle(NotificationCompat.BigTextStyle() + .bigText(message) + ) + .setSmallIcon(R.drawable.ic_stat_done) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setAutoCancel(true) + .setSound(null) + .build() + + notificationManager.notify(0, finishNotification) + } +} 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 34a2bca9..b4474cc5 100644 --- a/app/src/main/java/me/anon/lib/helper/StatsHelper.java +++ b/app/src/main/java/me/anon/lib/helper/StatsHelper.java @@ -1,6 +1,7 @@ package me.anon.lib.helper; import android.graphics.Color; +import android.support.annotation.NonNull; import android.support.v4.graphics.ColorUtils; import android.support.v4.util.Pair; import android.widget.TextView; @@ -114,7 +115,7 @@ public static void styleDataset(LineDataSet data, int colour) data.setValueFormatter(formatter); } - public static void setAdditiveData(Plant plant, LineChart chart, Set checkedAdditives) + public static void setAdditiveData(Plant plant, LineChart chart, @NonNull Set checkedAdditives) { ArrayList actions = plant.getActions(); ArrayList>> vals = new ArrayList<>(); 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 552e8615..db305754 100644 --- a/app/src/main/java/me/anon/lib/manager/GardenManager.java +++ b/app/src/main/java/me/anon/lib/manager/GardenManager.java @@ -56,7 +56,7 @@ public void load() { if (FileManager.getInstance().fileExists(FILES_DIR + "/gardens.json")) { - String plantData; + String gardenData; if (MainApplication.isEncrypted()) { @@ -65,19 +65,19 @@ public void load() return; } - plantData = EncryptionHelper.decrypt(MainApplication.getKey(), FileManager.getInstance().readFile(FILES_DIR + "/gardens.json")); + gardenData = EncryptionHelper.decrypt(MainApplication.getKey(), FileManager.getInstance().readFile(FILES_DIR + "/gardens.json")); } else { - plantData = FileManager.getInstance().readFileAsString(FILES_DIR + "/gardens.json"); + gardenData = FileManager.getInstance().readFileAsString(FILES_DIR + "/gardens.json"); } try { - if (!TextUtils.isEmpty(plantData)) + if (!TextUtils.isEmpty(gardenData)) { mGardens.clear(); - mGardens.addAll((ArrayList)GsonHelper.parse(plantData, new TypeToken>(){}.getType())); + mGardens.addAll((ArrayList)GsonHelper.parse(gardenData, new TypeToken>(){}.getType())); } } catch (JsonSyntaxException e) 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 72191dd9..2609a014 100644 --- a/app/src/main/java/me/anon/lib/manager/PlantManager.java +++ b/app/src/main/java/me/anon/lib/manager/PlantManager.java @@ -183,6 +183,11 @@ public void upsert(int index, Plant plant) } public boolean load() + { + return load(false); + } + + public boolean load(boolean fromRestore) { if (MainApplication.isFailsafe()) { @@ -227,21 +232,27 @@ public boolean load() { e.printStackTrace(); - 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(); + if (!fromRestore) + { + 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); + // prevent save + MainApplication.setFailsafe(true); + } } catch (Exception e) { e.printStackTrace(); - 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(); + if (!fromRestore) + { + 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); + // prevent save + MainApplication.setFailsafe(true); + } } } diff --git a/app/src/main/java/me/anon/lib/stream/DecryptInputStream.java b/app/src/main/java/me/anon/lib/stream/DecryptInputStream.java index 8373e892..f6a261dc 100644 --- a/app/src/main/java/me/anon/lib/stream/DecryptInputStream.java +++ b/app/src/main/java/me/anon/lib/stream/DecryptInputStream.java @@ -84,7 +84,14 @@ public DecryptInputStream(String key, File file) throws FileNotFoundException @Override public void close() throws IOException { - cis.close(); - fis.close(); + try + { + cis.close(); + fis.close(); + } + catch (NullPointerException e) + { + e.printStackTrace(); + } } } diff --git a/app/src/main/java/me/anon/model/Plant.java b/app/src/main/java/me/anon/model/Plant.java index 084bca6f..70eef4da 100644 --- a/app/src/main/java/me/anon/model/Plant.java +++ b/app/src/main/java/me/anon/model/Plant.java @@ -28,11 +28,11 @@ public class Plant { private String id = UUID.randomUUID().toString(); private String name; - private String strain; + private String strain = null; private long plantDate = System.currentTimeMillis(); private boolean clone = false; private PlantMedium medium = PlantMedium.SOIL; - private String mediumDetails; + private String mediumDetails = null; private ArrayList images = new ArrayList<>(); private ArrayList actions = new ArrayList<>(); @@ -216,7 +216,11 @@ public String generateLongSummary(Context context) Unit deliveryUnit = Unit.getSelectedDeliveryUnit(context); String summary = ""; - summary += getStrain() + " - "; + + if (getStrain() != null) + { + summary += getStrain() + " - "; + } if (getStage() == PlantStage.HARVESTED) { diff --git a/app/src/main/java/me/anon/model/PlantStage.java b/app/src/main/java/me/anon/model/PlantStage.java index 721a17f6..84dc6f9d 100644 --- a/app/src/main/java/me/anon/model/PlantStage.java +++ b/app/src/main/java/me/anon/model/PlantStage.java @@ -13,6 +13,7 @@ public enum PlantStage { PLANTED("Planted"), GERMINATION("Germination"), + SEEDLING("Seedling"), CUTTING("Cutting"), VEGETATION("Vegetation"), FLOWER("Flower"), diff --git a/app/src/main/java/me/anon/view/ImageActionHolder.java b/app/src/main/java/me/anon/view/ImageActionHolder.java index dbceafd2..9267da1b 100644 --- a/app/src/main/java/me/anon/view/ImageActionHolder.java +++ b/app/src/main/java/me/anon/view/ImageActionHolder.java @@ -21,6 +21,7 @@ import me.anon.grow.MainApplication; import me.anon.grow.R; import me.anon.grow.fragment.ImageLightboxDialog; +import me.anon.lib.manager.PlantManager; /** * // TODO: Add class description @@ -89,6 +90,7 @@ public void bind(final ArrayList imageUrls) Collections.reverse(images); Intent details = new Intent(v.getContext(), ImageLightboxDialog.class); + details.putExtra("plant_index", PlantManager.getInstance().getPlants().indexOf(adapter.getPlant())); 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/res/layout-land/statistics_view.xml b/app/src/main/res/layout-land/statistics_view.xml index d9a0dbd8..5f94a862 100644 --- a/app/src/main/res/layout-land/statistics_view.xml +++ b/app/src/main/res/layout-land/statistics_view.xml @@ -65,6 +65,66 @@ /> + + + + + + + + + + + + - - - + - - + + + - + @@ -65,6 +67,36 @@ /> + + + + + + @@ -102,17 +136,20 @@ android:visibility="gone" > + + + + - + + - - - + + - - diff --git a/screenshots/1.png b/metadata/images/phoneScreenshots/1.png similarity index 100% rename from screenshots/1.png rename to metadata/images/phoneScreenshots/1.png diff --git a/screenshots/10.png b/metadata/images/phoneScreenshots/10.png similarity index 100% rename from screenshots/10.png rename to metadata/images/phoneScreenshots/10.png diff --git a/screenshots/11.png b/metadata/images/phoneScreenshots/11.png similarity index 100% rename from screenshots/11.png rename to metadata/images/phoneScreenshots/11.png diff --git a/screenshots/12.png b/metadata/images/phoneScreenshots/12.png similarity index 100% rename from screenshots/12.png rename to metadata/images/phoneScreenshots/12.png diff --git a/screenshots/13.png b/metadata/images/phoneScreenshots/13.png similarity index 100% rename from screenshots/13.png rename to metadata/images/phoneScreenshots/13.png diff --git a/screenshots/14.png b/metadata/images/phoneScreenshots/14.png similarity index 100% rename from screenshots/14.png rename to metadata/images/phoneScreenshots/14.png diff --git a/screenshots/15.png b/metadata/images/phoneScreenshots/15.png similarity index 100% rename from screenshots/15.png rename to metadata/images/phoneScreenshots/15.png diff --git a/screenshots/1b.png b/metadata/images/phoneScreenshots/1b.png similarity index 100% rename from screenshots/1b.png rename to metadata/images/phoneScreenshots/1b.png diff --git a/screenshots/1c.png b/metadata/images/phoneScreenshots/1c.png similarity index 100% rename from screenshots/1c.png rename to metadata/images/phoneScreenshots/1c.png diff --git a/screenshots/1d.png b/metadata/images/phoneScreenshots/1d.png similarity index 100% rename from screenshots/1d.png rename to metadata/images/phoneScreenshots/1d.png diff --git a/screenshots/1e.png b/metadata/images/phoneScreenshots/1e.png similarity index 100% rename from screenshots/1e.png rename to metadata/images/phoneScreenshots/1e.png diff --git a/screenshots/2.png b/metadata/images/phoneScreenshots/2.png similarity index 100% rename from screenshots/2.png rename to metadata/images/phoneScreenshots/2.png diff --git a/screenshots/3.png b/metadata/images/phoneScreenshots/3.png similarity index 100% rename from screenshots/3.png rename to metadata/images/phoneScreenshots/3.png diff --git a/screenshots/4.png b/metadata/images/phoneScreenshots/4.png similarity index 100% rename from screenshots/4.png rename to metadata/images/phoneScreenshots/4.png diff --git a/screenshots/4b.png b/metadata/images/phoneScreenshots/4b.png similarity index 100% rename from screenshots/4b.png rename to metadata/images/phoneScreenshots/4b.png diff --git a/screenshots/5.png b/metadata/images/phoneScreenshots/5.png similarity index 100% rename from screenshots/5.png rename to metadata/images/phoneScreenshots/5.png diff --git a/screenshots/6.png b/metadata/images/phoneScreenshots/6.png similarity index 100% rename from screenshots/6.png rename to metadata/images/phoneScreenshots/6.png diff --git a/screenshots/7.png b/metadata/images/phoneScreenshots/7.png similarity index 100% rename from screenshots/7.png rename to metadata/images/phoneScreenshots/7.png diff --git a/screenshots/8.png b/metadata/images/phoneScreenshots/8.png similarity index 100% rename from screenshots/8.png rename to metadata/images/phoneScreenshots/8.png diff --git a/screenshots/9.png b/metadata/images/phoneScreenshots/9.png similarity index 100% rename from screenshots/9.png rename to metadata/images/phoneScreenshots/9.png diff --git a/screenshots/install.png b/metadata/images/phoneScreenshots/install.png similarity index 100% rename from screenshots/install.png rename to metadata/images/phoneScreenshots/install.png diff --git a/screenshots/1-thumb.png b/metadata/images/phoneScreenshotsThumbs/1-thumb.png similarity index 100% rename from screenshots/1-thumb.png rename to metadata/images/phoneScreenshotsThumbs/1-thumb.png diff --git a/screenshots/10-thumb.png b/metadata/images/phoneScreenshotsThumbs/10-thumb.png similarity index 100% rename from screenshots/10-thumb.png rename to metadata/images/phoneScreenshotsThumbs/10-thumb.png diff --git a/screenshots/11-thumb.png b/metadata/images/phoneScreenshotsThumbs/11-thumb.png similarity index 100% rename from screenshots/11-thumb.png rename to metadata/images/phoneScreenshotsThumbs/11-thumb.png diff --git a/screenshots/12-thumb.png b/metadata/images/phoneScreenshotsThumbs/12-thumb.png similarity index 100% rename from screenshots/12-thumb.png rename to metadata/images/phoneScreenshotsThumbs/12-thumb.png diff --git a/screenshots/13-thumb.png b/metadata/images/phoneScreenshotsThumbs/13-thumb.png similarity index 100% rename from screenshots/13-thumb.png rename to metadata/images/phoneScreenshotsThumbs/13-thumb.png diff --git a/screenshots/14-thumb.png b/metadata/images/phoneScreenshotsThumbs/14-thumb.png similarity index 100% rename from screenshots/14-thumb.png rename to metadata/images/phoneScreenshotsThumbs/14-thumb.png diff --git a/screenshots/15-thumb.png b/metadata/images/phoneScreenshotsThumbs/15-thumb.png similarity index 100% rename from screenshots/15-thumb.png rename to metadata/images/phoneScreenshotsThumbs/15-thumb.png diff --git a/screenshots/1b-thumb.png b/metadata/images/phoneScreenshotsThumbs/1b-thumb.png similarity index 100% rename from screenshots/1b-thumb.png rename to metadata/images/phoneScreenshotsThumbs/1b-thumb.png diff --git a/screenshots/1c-thumb.png b/metadata/images/phoneScreenshotsThumbs/1c-thumb.png similarity index 100% rename from screenshots/1c-thumb.png rename to metadata/images/phoneScreenshotsThumbs/1c-thumb.png diff --git a/screenshots/1d-thumb.png b/metadata/images/phoneScreenshotsThumbs/1d-thumb.png similarity index 100% rename from screenshots/1d-thumb.png rename to metadata/images/phoneScreenshotsThumbs/1d-thumb.png diff --git a/screenshots/1e-thumb.png b/metadata/images/phoneScreenshotsThumbs/1e-thumb.png similarity index 100% rename from screenshots/1e-thumb.png rename to metadata/images/phoneScreenshotsThumbs/1e-thumb.png diff --git a/screenshots/2-thumb.png b/metadata/images/phoneScreenshotsThumbs/2-thumb.png similarity index 100% rename from screenshots/2-thumb.png rename to metadata/images/phoneScreenshotsThumbs/2-thumb.png diff --git a/screenshots/3-thumb.png b/metadata/images/phoneScreenshotsThumbs/3-thumb.png similarity index 100% rename from screenshots/3-thumb.png rename to metadata/images/phoneScreenshotsThumbs/3-thumb.png diff --git a/screenshots/4-thumb.png b/metadata/images/phoneScreenshotsThumbs/4-thumb.png similarity index 100% rename from screenshots/4-thumb.png rename to metadata/images/phoneScreenshotsThumbs/4-thumb.png diff --git a/screenshots/4b-thumb.png b/metadata/images/phoneScreenshotsThumbs/4b-thumb.png similarity index 100% rename from screenshots/4b-thumb.png rename to metadata/images/phoneScreenshotsThumbs/4b-thumb.png diff --git a/screenshots/5-thumb.png b/metadata/images/phoneScreenshotsThumbs/5-thumb.png similarity index 100% rename from screenshots/5-thumb.png rename to metadata/images/phoneScreenshotsThumbs/5-thumb.png diff --git a/screenshots/6-thumb.png b/metadata/images/phoneScreenshotsThumbs/6-thumb.png similarity index 100% rename from screenshots/6-thumb.png rename to metadata/images/phoneScreenshotsThumbs/6-thumb.png diff --git a/screenshots/7-thumb.png b/metadata/images/phoneScreenshotsThumbs/7-thumb.png similarity index 100% rename from screenshots/7-thumb.png rename to metadata/images/phoneScreenshotsThumbs/7-thumb.png diff --git a/screenshots/8-thumb.png b/metadata/images/phoneScreenshotsThumbs/8-thumb.png similarity index 100% rename from screenshots/8-thumb.png rename to metadata/images/phoneScreenshotsThumbs/8-thumb.png diff --git a/screenshots/9-thumb.png b/metadata/images/phoneScreenshotsThumbs/9-thumb.png similarity index 100% rename from screenshots/9-thumb.png rename to metadata/images/phoneScreenshotsThumbs/9-thumb.png diff --git a/screenshots/install-thumb.png b/metadata/images/phoneScreenshotsThumbs/install-thumb.png similarity index 100% rename from screenshots/install-thumb.png rename to metadata/images/phoneScreenshotsThumbs/install-thumb.png