From d6086ca4f533678e65018297fa969ee15bb07bae Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Wed, 28 Dec 2016 19:47:45 +0100 Subject: [PATCH 01/32] Implement delete recording #134 --- app/src/main/res/menu/recording_item_menu.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/src/main/res/menu/recording_item_menu.xml diff --git a/app/src/main/res/menu/recording_item_menu.xml b/app/src/main/res/menu/recording_item_menu.xml new file mode 100644 index 00000000..dc7e4475 --- /dev/null +++ b/app/src/main/res/menu/recording_item_menu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file From 8b23f8c0908d59188ade972a5a855c0243058a97 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Wed, 28 Dec 2016 20:31:40 +0100 Subject: [PATCH 02/32] Implement delete hive #134 --- .../gobees/apiary/ApiaryContract.java | 17 +++++ .../gobees/apiary/ApiaryHivesFragment.java | 19 +++++- .../gobees/apiary/ApiaryPresenter.java | 62 ++++++++++++----- .../gobees/apiary/HivesAdapter.java | 60 ++++++++++++++++- .../gobees/data/source/GoBeesDataSource.java | 19 +++++- .../data/source/cache/GoBeesRepository.java | 15 +++++ .../source/local/GoBeesLocalDataSource.java | 44 ++++++++++++- .../davidmiguel/gobees/hive/HiveContract.java | 17 +++++ .../gobees/hive/HivePresenter.java | 66 ++++++++++++++----- .../gobees/hive/HiveRecordingsFragment.java | 20 +++++- .../gobees/hive/RecordingsAdapter.java | 58 +++++++++++++++- app/src/main/res/menu/hive_item_menu.xml | 13 ++++ app/src/main/res/menu/recording_item_menu.xml | 8 ++- app/src/main/res/values/strings.xml | 12 ++++ 14 files changed, 386 insertions(+), 44 deletions(-) create mode 100644 app/src/main/res/menu/hive_item_menu.xml diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java index 10b5c508..67b0d76c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java @@ -54,6 +54,16 @@ interface View extends BaseView { */ void showSuccessfullySavedMessage(); + /** + * Shows successfully deleted message. + */ + void showSuccessfullyDeletedMessage(); + + /** + * Shows error while deleting hive message. + */ + void showDeletedErrorMessage(); + /** * Sets the title in the action bar. * @param title title. @@ -86,5 +96,12 @@ interface Presenter extends BasePresenter { * @param requestedHive hive to show. */ void openHiveDetail(@NonNull Hive requestedHive); + + /** + * Deletes given hive. + * + * @param hive hive to delete. + */ + void deleteHive(@NonNull Hive hive); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java index e67d82bb..cedc30dd 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java @@ -59,7 +59,7 @@ public static ApiaryHivesFragment newInstance(long apiaryId) { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - listAdapter = new HivesAdapter(new ArrayList(0), this); + listAdapter = new HivesAdapter(getActivity().getMenuInflater(), new ArrayList(0), this); } @Nullable @@ -198,6 +198,16 @@ public void showSuccessfullySavedMessage() { showMessage(getString(R.string.successfully_saved_hive_message)); } + @Override + public void showSuccessfullyDeletedMessage() { + showMessage(getString(R.string.successfully_deleted_hive_message)); + } + + @Override + public void showDeletedErrorMessage() { + showMessage(getString(R.string.deleted_hive_error_message)); + } + @Override public void showTitle(@NonNull String title) { ActionBar ab = ((ApiaryActivity) getActivity()).getSupportActionBar(); @@ -223,7 +233,12 @@ public void onHiveClick(Hive clickedHive) { @Override public void onHiveDelete(Hive clickedHive) { - // TODO delete hive + presenter.deleteHive(clickedHive); + } + + @Override + public void onOpenMenuClick(View view) { + getActivity().openContextMenu(view); } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index aeecd7a8..43f77a85 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -16,7 +16,7 @@ class ApiaryPresenter implements ApiaryContract.Presenter { private GoBeesRepository goBeesRepository; - private ApiaryContract.View apiaryView; + private ApiaryContract.View view; /** * Force update the first time. @@ -24,11 +24,11 @@ class ApiaryPresenter implements ApiaryContract.Presenter { private boolean firstLoad = true; private long apiaryId; - ApiaryPresenter(GoBeesRepository goBeesRepository, ApiaryContract.View apiaryView, + ApiaryPresenter(GoBeesRepository goBeesRepository, ApiaryContract.View view, long apiaryId) { this.goBeesRepository = goBeesRepository; - this.apiaryView = apiaryView; - this.apiaryView.setPresenter(this); + this.view = view; + this.view.setPresenter(this); this.apiaryId = apiaryId; } @@ -36,7 +36,7 @@ class ApiaryPresenter implements ApiaryContract.Presenter { public void result(int requestCode, int resultCode) { // If a hive was successfully added, show snackbar if (AddEditApiaryActivity.REQUEST_ADD_APIARY == requestCode && Activity.RESULT_OK == resultCode) { - apiaryView.showSuccessfullySavedMessage(); + view.showSuccessfullySavedMessage(); } // TODO show error message if it fails } @@ -47,7 +47,7 @@ public void loadHives(boolean forceUpdate) { forceUpdate = forceUpdate || firstLoad; firstLoad = false; // Show progress indicator - apiaryView.setLoadingIndicator(true); + view.setLoadingIndicator(true); // Refresh data if needed if (forceUpdate) { goBeesRepository.refreshHives(apiaryId); @@ -57,42 +57,74 @@ public void loadHives(boolean forceUpdate) { @Override public void onApiaryLoaded(Apiary apiary) { // The view may not be able to handle UI updates anymore - if (!apiaryView.isActive()) { + if (!view.isActive()) { return; } // Hide progress indicator - apiaryView.setLoadingIndicator(false); + view.setLoadingIndicator(false); // Set apiary name as title - apiaryView.showTitle(apiary.getName()); + view.showTitle(apiary.getName()); // Process hives if (apiary.getHives() == null || apiary.getHives().isEmpty()) { // Show a message indicating there are no hives - apiaryView.showNoHives(); + view.showNoHives(); } else { // Show the list of hives - apiaryView.showHives(apiary.getHives()); + view.showHives(apiary.getHives()); } } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore - if (!apiaryView.isActive()) { + if (!view.isActive()) { return; } - apiaryView.showLoadingHivesError(); + view.showLoadingHivesError(); } }); } @Override public void addEditHive() { - apiaryView.showAddEditHive(apiaryId); + view.showAddEditHive(apiaryId); } @Override public void openHiveDetail(@NonNull Hive requestedHive) { - apiaryView.showHiveDetail(requestedHive.getId()); + view.showHiveDetail(requestedHive.getId()); + } + + @Override + public void deleteHive(@NonNull Hive hive) { + // Show progress indicator + view.setLoadingIndicator(true); + // Delete hive + goBeesRepository.deleteHive(apiaryId, hive, new GoBeesDataSource.TaskCallback() { + @Override + public void onSuccess() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Refresh recordings + loadHives(true); + // Show success message + view.showSuccessfullyDeletedMessage(); + } + + @Override + public void onFailure() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Hide progress indicator + view.setLoadingIndicator(false); + // Show error + view.showDeletedErrorMessage(); + } + }); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java index 037c13ba..24344caf 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java @@ -5,9 +5,13 @@ import android.support.annotation.NonNull; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.view.ContextMenu; import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import com.davidmiguel.gobees.R; @@ -25,9 +29,12 @@ class HivesAdapter extends RecyclerView.Adapter { private List hives; + private MenuInflater menuInflater; private HivesAdapter.HiveItemListener listener; - HivesAdapter(List hives, HivesAdapter.HiveItemListener listener) { + HivesAdapter(MenuInflater menuInflater, List hives, + HivesAdapter.HiveItemListener listener) { + this.menuInflater = menuInflater; this.hives = checkNotNull(hives); this.listener = listener; } @@ -58,20 +65,42 @@ interface HiveItemListener { void onHiveClick(Hive clickedHive); void onHiveDelete(Hive clickedHive); + + void onOpenMenuClick(View view); } class ViewHolder extends RecyclerView.ViewHolder - implements BaseViewHolder, View.OnClickListener, ItemTouchHelperViewHolder { + implements BaseViewHolder, View.OnClickListener, + View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener, + ItemTouchHelperViewHolder { + private View viewHolder; private CardView card; private TextView hiveName; + private ImageView moreIcon; + private Drawable background; ViewHolder(View itemView) { super(itemView); - itemView.setOnClickListener(this); + + // Get views + viewHolder = itemView; card = (CardView) itemView.findViewById(R.id.card); hiveName = (TextView) itemView.findViewById(R.id.hive_name); + moreIcon = (ImageView) itemView.findViewById(R.id.more_icon); + + // Set listeners + viewHolder.setOnClickListener(this); + viewHolder.setOnCreateContextMenuListener(this); + moreIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Open Menu + listener.onOpenMenuClick(viewHolder); + } + }); + background = card.getBackground(); } @@ -79,11 +108,36 @@ public void bind(@NonNull Hive hive) { hiveName.setText(hive.getName()); } + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + // Inflate menu + menuInflater.inflate(R.menu.hive_item_menu, contextMenu); + // Set click listener + for (int i = 0; i < contextMenu.size(); i++) { + contextMenu.getItem(i).setOnMenuItemClickListener(this); + } + } + @Override public void onClick(View view) { listener.onHiveClick(hives.get(getAdapterPosition())); } + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.menu_edit: + // TODO + return true; + case R.id.menu_delete: + listener.onHiveDelete(hives.get(getAdapterPosition())); + return true; + default: + return false; + } + } + @Override public void onItemSelected() { card.setBackgroundColor(Color.LTGRAY); diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 5f2a5702..8eef69f5 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -125,6 +125,14 @@ public interface GoBeesDataSource { */ void saveHive(long apiaryId, @NonNull Hive hive, @NonNull TaskCallback callback); + /** + * Deletes given hive. + * + * @param hive hive to delete. + * @param callback TaskCallback. + */ + void deleteHive(@NonNull Hive hive, @NonNull TaskCallback callback); + /** * Returns the next hive id. * (Realm does not support auto-increment at the moment). @@ -153,7 +161,7 @@ public interface GoBeesDataSource { void saveRecords(long hiveId, @NonNull List records, @NonNull TaskCallback callback); /** - * Get recording with records of given period. + * Gets recording with records of given period. * * @param hiveId hive id. * @param start start of the period (00:00 of that date). @@ -162,6 +170,15 @@ public interface GoBeesDataSource { */ void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback); + /** + * Deletes the records contained in the given recording. + * + * @param hiveId hive id. + * @param recording recording to delete. + * @param callback TaskCallback. + */ + void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback); + /** * Force to update recordings cache. */ diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index e5e1db6b..d258aa41 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -5,6 +5,7 @@ import com.davidmiguel.gobees.data.model.Apiary; import com.davidmiguel.gobees.data.model.Hive; import com.davidmiguel.gobees.data.model.Record; +import com.davidmiguel.gobees.data.model.Recording; import com.davidmiguel.gobees.data.source.GoBeesDataSource; import java.util.ArrayList; @@ -222,6 +223,13 @@ public void saveHive(long apiaryId, @NonNull Hive hive, @NonNull TaskCallback ca // cachedApiaries.put(apiary.getId(), apiary); } + @Override + public void deleteHive(@NonNull Hive hive, @NonNull TaskCallback callback) { + checkNotNull(callback); + // Delete hive + goBeesDataSource.deleteHive(hive, callback); + } + @Override public void getNextHiveId(@NonNull GetNextHiveIdCallback callback) { checkNotNull(callback); @@ -250,6 +258,13 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin goBeesDataSource.getRecording(hiveId, start, end, callback); } + @Override + public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { + checkNotNull(callback); + // Delete recording + goBeesDataSource.deleteRecording(hiveId, recording, callback); + } + @Override public void refreshRecordings(long hiveId) { // TODO diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 19a55f67..20c41405 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -225,6 +225,24 @@ public void execute(Realm realm) { } } + @Override + public void deleteHive(@NonNull final Hive hive, @NonNull TaskCallback callback) { + if (hive.getRecords() == null) { + callback.onFailure(); + return; + } + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Delete records of the hive + hive.getRecords().where().findAll().deleteAllFromRealm(); + // Delete hive + hive.deleteFromRealm(); + } + }); + callback.onSuccess(); + } + @Override public void getNextHiveId(@NonNull GetNextHiveIdCallback callback) { Number nextId = realm.where(Hive.class).max("id"); @@ -292,7 +310,7 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin RealmResults records = hive.getRecords() .where() .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(start, 0, 0, 0, 0)) - .lessThan("timestamp", DateTimeUtils.setTime(end, 23, 59, 59, 999)) + .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(end, 23, 59, 59, 999)) .findAll() .sort("timestamp"); // Create recording @@ -300,6 +318,30 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin callback.onRecordingLoaded(recording); } + @Override + public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { + // Get hive + Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); + if (hive == null || hive.getRecords() == null) { + callback.onFailure(); + return; + } + // Get records to delete + final RealmResults records = hive.getRecords() + .where() + .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) + .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) + .findAll(); + // Delete records + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + records.deleteAllFromRealm(); + } + }); + callback.onSuccess(); + } + @Override public void refreshRecordings(long hiveId) { // Not required because the GoBeesRepository handles the logic of refreshing the diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java index 29e2f3e0..79954ab6 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java @@ -58,6 +58,16 @@ interface View extends BaseView { */ void showSuccessfullySavedMessage(); + /** + * Shows successfully deleted message. + */ + void showSuccessfullyDeletedMessage(); + + /** + * Shows error while deleting recording message. + */ + void showDeletedErrorMessage(); + /** * Sets the title in the action bar. * @@ -94,5 +104,12 @@ interface Presenter extends BasePresenter { * @param recording recording. */ void openRecordingsDetail(@NonNull Recording recording); + + /** + * Deletes given recording. + * + * @param recording recording to delete. + */ + void deleteRecording(@NonNull Recording recording); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java index 2794a18f..fe99abab 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java @@ -16,7 +16,7 @@ class HivePresenter implements HiveContract.Presenter { private GoBeesRepository goBeesRepository; - private HiveContract.View hiveView; + private HiveContract.View view; /** * Force update the first time. @@ -24,11 +24,11 @@ class HivePresenter implements HiveContract.Presenter { private boolean firstLoad = true; private long hiveId; - HivePresenter(GoBeesRepository goBeesRepository, HiveContract.View hiveView, + HivePresenter(GoBeesRepository goBeesRepository, HiveContract.View view, long hiveId) { this.goBeesRepository = goBeesRepository; - this.hiveView = hiveView; - this.hiveView.setPresenter(this); + this.view = view; + this.view.setPresenter(this); this.hiveId = hiveId; } @@ -39,16 +39,17 @@ public void result(int requestCode, int resultCode) { // Refresh recordings loadRecordings(true); // Show message - hiveView.showSuccessfullySavedMessage(); + view.showSuccessfullySavedMessage(); } } + @Override public void loadRecordings(boolean forceUpdate) { // Force update the first time forceUpdate = forceUpdate || firstLoad; firstLoad = false; // Show progress indicator - hiveView.setLoadingIndicator(true); + view.setLoadingIndicator(true); // Refresh data if needed if (forceUpdate) { goBeesRepository.refreshRecordings(hiveId); @@ -59,42 +60,77 @@ public void loadRecordings(boolean forceUpdate) { @Override public void onHiveLoaded(Hive hive) { // The view may not be able to handle UI updates anymore - if (!hiveView.isActive()) { + if (!view.isActive()) { return; } // Hide progress indicator - hiveView.setLoadingIndicator(false); + view.setLoadingIndicator(false); // Set hive name as title - hiveView.showTitle(hive.getName()); + view.showTitle(hive.getName()); // Process recordings if (hive.getRecordings().isEmpty()) { // Show a message indicating there are no recordings - hiveView.showNoRecordings(); + view.showNoRecordings(); } else { // Show the list of recordings - hiveView.showRecordings(hive.getRecordings()); + view.showRecordings(hive.getRecordings()); } } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore - if (!hiveView.isActive()) { + if (!view.isActive()) { return; } - hiveView.showLoadingRecordingsError(); + // Hide progress indicator + view.setLoadingIndicator(false); + // Show error + view.showLoadingRecordingsError(); } }); } @Override public void startNewRecording() { - hiveView.startNewRecording(hiveId); + view.startNewRecording(hiveId); } @Override public void openRecordingsDetail(@NonNull Recording recording) { - hiveView.showRecordingDetail(hiveId, recording.getDate()); + view.showRecordingDetail(hiveId, recording.getDate()); + } + + @Override + public void deleteRecording(@NonNull Recording recording) { + // Show progress indicator + view.setLoadingIndicator(true); + // Delete recording + goBeesRepository.deleteRecording(hiveId, recording, new GoBeesDataSource.TaskCallback() { + @Override + public void onSuccess() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Refresh recordings + loadRecordings(true); + // Show success message + view.showSuccessfullyDeletedMessage(); + } + + @Override + public void onFailure() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Hide progress indicator + view.setLoadingIndicator(false); + // Show error + view.showDeletedErrorMessage(); + } + }); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java index 554f9bb1..f3a6a2c6 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java @@ -60,7 +60,8 @@ public static HiveRecordingsFragment newInstance() { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - listAdapter = new RecordingsAdapter(getContext(), new ArrayList(0), this); + listAdapter = new RecordingsAdapter(getContext(), getActivity().getMenuInflater(), + new ArrayList(0), this); } @Nullable @@ -196,6 +197,16 @@ public void showSuccessfullySavedMessage() { showMessage(getString(R.string.successfully_saved_recording_message)); } + @Override + public void showSuccessfullyDeletedMessage() { + showMessage(getString(R.string.successfully_deleted_recording_message)); + } + + @Override + public void showDeletedErrorMessage() { + showMessage(getString(R.string.deleted_recording_error_message)); + } + @Override public void showTitle(@NonNull String title) { ActionBar ab = ((HiveActivity) getActivity()).getSupportActionBar(); @@ -226,7 +237,12 @@ public void onRecordingClick(Recording clickedRecording) { @Override public void onRecordingDelete(Recording clickedRecording) { - // TODO delete recording + presenter.deleteRecording(clickedRecording); + } + + @Override + public void onOpenMenuClick(View view) { + getActivity().openContextMenu(view); } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java index 24bc9b56..e5b4200d 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java @@ -7,9 +7,13 @@ import android.support.v4.content.ContextCompat; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.view.ContextMenu; import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import com.davidmiguel.gobees.R; @@ -40,11 +44,14 @@ class RecordingsAdapter extends RecyclerView.Adapter { private Context context; + private MenuInflater menuInflater; private List recordings; private RecordingItemListener listener; - RecordingsAdapter(Context context, List recordings, RecordingItemListener listener) { + RecordingsAdapter(Context context, MenuInflater menuInflater, + List recordings, RecordingItemListener listener) { this.context = context; + this.menuInflater = menuInflater; this.recordings = checkNotNull(recordings); this.listener = listener; } @@ -75,24 +82,44 @@ interface RecordingItemListener { void onRecordingClick(Recording clickedRecording); void onRecordingDelete(Recording clickedRecording); + + void onOpenMenuClick(View view); } class ViewHolder extends RecyclerView.ViewHolder - implements BaseViewHolder, View.OnClickListener, ItemTouchHelperViewHolder { + implements BaseViewHolder, View.OnClickListener, + View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener, + ItemTouchHelperViewHolder { + private View viewHolder; private CardView card; private TextView recordingDate; private LineChart chart; + private ImageView moreIcon; private Drawable background; private SimpleDateFormat formatter; ViewHolder(View itemView) { super(itemView); - itemView.setOnClickListener(this); + + // Get views + viewHolder = itemView; card = (CardView) itemView.findViewById(R.id.card); recordingDate = (TextView) itemView.findViewById(R.id.recording_date); chart = (LineChart) itemView.findViewById(R.id.chart); + moreIcon = (ImageView) itemView.findViewById(R.id.more_icon); + + // Set listeners + viewHolder.setOnClickListener(this); + viewHolder.setOnCreateContextMenuListener(this); + moreIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Open Menu + listener.onOpenMenuClick(viewHolder); + } + }); background = card.getBackground(); formatter = new SimpleDateFormat( @@ -113,11 +140,36 @@ public void bind(@NonNull final Recording recording) { } } + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + // Inflate menu + menuInflater.inflate(R.menu.recording_item_menu, contextMenu); + // Set click listener + for (int i = 0; i < contextMenu.size(); i++) { + contextMenu.getItem(i).setOnMenuItemClickListener(this); + } + } + @Override public void onClick(View view) { listener.onRecordingClick(recordings.get(getAdapterPosition())); } + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.menu_edit: + // TODO + return true; + case R.id.menu_delete: + listener.onRecordingDelete(recordings.get(getAdapterPosition())); + return true; + default: + return false; + } + } + @Override public void onItemSelected() { card.setBackgroundColor(Color.LTGRAY); diff --git a/app/src/main/res/menu/hive_item_menu.xml b/app/src/main/res/menu/hive_item_menu.xml new file mode 100644 index 00000000..b378c3a8 --- /dev/null +++ b/app/src/main/res/menu/hive_item_menu.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/recording_item_menu.xml b/app/src/main/res/menu/recording_item_menu.xml index dc7e4475..b378c3a8 100644 --- a/app/src/main/res/menu/recording_item_menu.xml +++ b/app/src/main/res/menu/recording_item_menu.xml @@ -3,7 +3,11 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae17c326..2ccc6251 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,10 @@ Rain Wind + + Edit + + Delete @@ -93,6 +97,10 @@ You have no hives! Hive saved! + + Hive deleted! + + Error while deleting hive. Last revision icon @@ -129,6 +137,10 @@ You have no recordings! Recording saved! + + Recording deleted! + + Error while deleting recording. EEE d MMM, yyyy From 57f50c487e41e31d3b1a804ed37a50e56adc3974 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 29 Dec 2016 00:05:11 +0100 Subject: [PATCH 03/32] Implement delete apiary #134 --- .../gobees/apiaries/ApiariesAdapter.java | 59 ++++++++++++- .../gobees/apiaries/ApiariesContract.java | 26 +++++- .../gobees/apiaries/ApiariesFragment.java | 19 ++++- .../gobees/apiaries/ApiariesPresenter.java | 60 ++++++++++---- .../gobees/apiary/ApiaryPresenter.java | 2 +- .../gobees/apiary/HivesAdapter.java | 2 +- .../gobees/data/source/GoBeesDataSource.java | 4 +- .../data/source/cache/GoBeesRepository.java | 9 +- .../source/local/GoBeesLocalDataSource.java | 82 +++++++++++-------- app/src/main/res/menu/apiary_item_menu.xml | 13 +++ app/src/main/res/values/strings.xml | 4 + 11 files changed, 217 insertions(+), 63 deletions(-) create mode 100644 app/src/main/res/menu/apiary_item_menu.xml diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java index e3ec024e..8ce854bf 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java @@ -5,9 +5,13 @@ import android.support.annotation.NonNull; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.view.ContextMenu; import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import com.davidmiguel.gobees.R; @@ -24,10 +28,12 @@ */ class ApiariesAdapter extends RecyclerView.Adapter { + private MenuInflater menuInflater; private List apiaries; private ApiaryItemListener listener; - ApiariesAdapter(List apiaries, ApiaryItemListener listener) { + ApiariesAdapter(MenuInflater menuInflater, List apiaries, ApiaryItemListener listener) { + this.menuInflater = menuInflater; this.apiaries = checkNotNull(apiaries); this.listener = listener; } @@ -58,22 +64,44 @@ interface ApiaryItemListener { void onApiaryClick(Apiary clickedApiary); void onApiaryDelete(Apiary clickedApiary); + + void onOpenMenuClick(View view); } class ViewHolder extends RecyclerView.ViewHolder - implements BaseViewHolder, View.OnClickListener, ItemTouchHelperViewHolder { + implements BaseViewHolder, View.OnClickListener, + View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener, + ItemTouchHelperViewHolder { + private View viewHolder; private CardView card; private TextView apiaryName; private TextView numHives; + private ImageView moreIcon; + private Drawable background; ViewHolder(View itemView) { super(itemView); - itemView.setOnClickListener(this); + + // Get views + viewHolder = itemView; card = (CardView) itemView.findViewById(R.id.card); apiaryName = (TextView) itemView.findViewById(R.id.apiary_name); numHives = (TextView) itemView.findViewById(R.id.num_hives); + moreIcon = (ImageView) itemView.findViewById(R.id.more_icon); + + // Set listeners + viewHolder.setOnClickListener(this); + viewHolder.setOnCreateContextMenuListener(this); + moreIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Open Menu + listener.onOpenMenuClick(viewHolder); + } + }); + background = card.getBackground(); } @@ -84,11 +112,36 @@ public void bind(@NonNull Apiary apiary) { } } + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + // Inflate menu + menuInflater.inflate(R.menu.apiary_item_menu, contextMenu); + // Set click listener + for (int i = 0; i < contextMenu.size(); i++) { + contextMenu.getItem(i).setOnMenuItemClickListener(this); + } + } + @Override public void onClick(View view) { listener.onApiaryClick(apiaries.get(getAdapterPosition())); } + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.menu_edit: + // TODO + return true; + case R.id.menu_delete: + listener.onApiaryDelete(apiaries.get(getAdapterPosition())); + return true; + default: + return false; + } + } + @Override public void onItemSelected() { card.setBackgroundColor(Color.LTGRAY); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java index e0fc59e3..2432a27e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java @@ -17,12 +17,14 @@ interface View extends BaseView { /** * Displays or hide loading indicator. + * * @param active true or false. */ void setLoadingIndicator(final boolean active); /** * Shows list of apiaries. + * * @param apiaries apiaries to show (list cannot be empty). */ void showApiaries(@NonNull List apiaries); @@ -34,6 +36,7 @@ interface View extends BaseView { /** * Opens activity to show the details of the given apiary. + * * @param apiaryId apiary to show. */ void showApiaryDetail(long apiaryId); @@ -52,19 +55,31 @@ interface View extends BaseView { * Shows successfully saved message. */ void showSuccessfullySavedMessage(); + + /** + * Shows successfully deleted message. + */ + void showSuccessfullyDeletedMessage(); + + /** + * Shows error while deleting apiary message. + */ + void showDeletedErrorMessage(); } interface Presenter extends BasePresenter { /** * Shows a snackbar showing whether an apiary was successfully added or not. + * * @param requestCode request code from the intent. - * @param resultCode result code from the intent. + * @param resultCode result code from the intent. */ void result(int requestCode, int resultCode); /** * Load apiaries from repository. + * * @param forceUpdate force cache update. */ void loadApiaries(boolean forceUpdate); @@ -76,12 +91,21 @@ interface Presenter extends BasePresenter { /** * Opens activity to show the details of the given apiary. + * * @param requestedApiary apiary to show. */ void openApiaryDetail(@NonNull Apiary requestedApiary); + /** + * Deletes given apiary. + * + * @param apiary apiary to delete. + */ + void deleteApiary(@NonNull Apiary apiary); + // TODO eliminar generar y eliminar datos void generateData(); + void deleteData(); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index c2d44b2f..67ba238c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -54,7 +54,7 @@ public static ApiariesFragment newInstance() { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - listAdapter = new ApiariesAdapter(new ArrayList(0), this); + listAdapter = new ApiariesAdapter(getActivity().getMenuInflater(), new ArrayList(0), this); } @Nullable @@ -192,6 +192,16 @@ public void showSuccessfullySavedMessage() { showMessage(getString(R.string.successfully_saved_apiary_message)); } + @Override + public void showSuccessfullyDeletedMessage() { + showMessage(getString(R.string.successfully_deleted_apiary_message)); + } + + @Override + public void showDeletedErrorMessage() { + showMessage(getString(R.string.deleted_apiary_error_message)); + } + @Override public boolean isActive() { return isAdded(); @@ -209,7 +219,12 @@ public void onApiaryClick(Apiary clickedApiary) { @Override public void onApiaryDelete(Apiary clickedApiary) { - // TODO delete apiary + presenter.deleteApiary(clickedApiary); + } + + @Override + public void onOpenMenuClick(View view) { + getActivity().openContextMenu(view); } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index 0e6c4ed8..b5d2031e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -18,24 +18,24 @@ class ApiariesPresenter implements ApiariesContract.Presenter { private GoBeesRepository goBeesRepository; - private ApiariesContract.View apiariesView; + private ApiariesContract.View view; /** * Force update the first time. */ private boolean firstLoad = true; - ApiariesPresenter(GoBeesRepository goBeesRepository, ApiariesContract.View apiariesView) { + ApiariesPresenter(GoBeesRepository goBeesRepository, ApiariesContract.View view) { this.goBeesRepository = goBeesRepository; - this.apiariesView = apiariesView; - this.apiariesView.setPresenter(this); + this.view = view; + this.view.setPresenter(this); } @Override public void result(int requestCode, int resultCode) { // If a apiary was successfully added, show snackbar if (AddEditApiaryActivity.REQUEST_ADD_APIARY == requestCode && Activity.RESULT_OK == resultCode) { - apiariesView.showSuccessfullySavedMessage(); + view.showSuccessfullySavedMessage(); } } @@ -45,7 +45,7 @@ public void loadApiaries(boolean forceUpdate) { forceUpdate = forceUpdate || firstLoad; firstLoad = false; // Show progress indicator - apiariesView.setLoadingIndicator(true); + view.setLoadingIndicator(true); // Refresh data if needed if (forceUpdate) { goBeesRepository.refreshApiaries(); @@ -56,40 +56,72 @@ public void loadApiaries(boolean forceUpdate) { @Override public void onApiariesLoaded(List apiaries) { // The view may not be able to handle UI updates anymore - if (!apiariesView.isActive()) { + if (!view.isActive()) { return; } // Hide progress indicator - apiariesView.setLoadingIndicator(false); + view.setLoadingIndicator(false); // Process apiaries if (apiaries.isEmpty()) { // Show a message indicating there are no apiaries - apiariesView.showNoApiaries(); + view.showNoApiaries(); } else { // Show the list of apiaries - apiariesView.showApiaries(apiaries); + view.showApiaries(apiaries); } } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore - if (!apiariesView.isActive()) { + if (!view.isActive()) { return; } - apiariesView.showLoadingApiariesError(); + view.showLoadingApiariesError(); } }); } @Override public void addEditApiary() { - apiariesView.showAddEditApiary(); + view.showAddEditApiary(); } @Override public void openApiaryDetail(@NonNull Apiary requestedApiary) { - apiariesView.showApiaryDetail(requestedApiary.getId()); + view.showApiaryDetail(requestedApiary.getId()); + } + + @Override + public void deleteApiary(@NonNull Apiary apiary) { + // Show progress indicator + view.setLoadingIndicator(true); + // Delete apiary + goBeesRepository.deleteApiary(apiary, new GoBeesDataSource.TaskCallback() { + @Override + public void onSuccess() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Refresh recordings + loadApiaries(true); + // Show success message + view.showSuccessfullyDeletedMessage(); + } + + @Override + public void onFailure() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Hide progress indicator + view.setLoadingIndicator(false); + // Show error + view.showDeletedErrorMessage(); + } + }); } // TODO eliminar generar y eliminar datos diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index 43f77a85..e91cce87 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -100,7 +100,7 @@ public void deleteHive(@NonNull Hive hive) { // Show progress indicator view.setLoadingIndicator(true); // Delete hive - goBeesRepository.deleteHive(apiaryId, hive, new GoBeesDataSource.TaskCallback() { + goBeesRepository.deleteHive(hive, new GoBeesDataSource.TaskCallback() { @Override public void onSuccess() { // The view may not be able to handle UI updates anymore diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java index 24344caf..5a614750 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java @@ -28,8 +28,8 @@ */ class HivesAdapter extends RecyclerView.Adapter { - private List hives; private MenuInflater menuInflater; + private List hives; private HivesAdapter.HiveItemListener listener; HivesAdapter(MenuInflater menuInflater, List hives, diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 8eef69f5..8614dfe7 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -64,10 +64,10 @@ public interface GoBeesDataSource { /** * Delete apiary. * - * @param apiaryId apiary id. + * @param apiary apiary to delete. * @param callback TaskCallback. */ - void deleteApiary(long apiaryId, @NonNull TaskCallback callback); + void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback); /** * Delete all apiaries. diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index d258aa41..09c312e0 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -133,15 +133,10 @@ public void refreshApiaries() { } @Override - public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { + public void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback) { checkNotNull(callback); // Delete apiary - goBeesDataSource.deleteApiary(apiaryId, callback); - // Do in memory cache update to keep the app UI up to date - if (cachedApiaries == null) { - cachedApiaries = new LinkedHashMap<>(); - } - cachedApiaries.remove(apiaryId); + goBeesDataSource.deleteApiary(apiary, callback); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 20c41405..1c8fd5c2 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -98,12 +98,21 @@ public void refreshApiaries() { } @Override - public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { + public void deleteApiary(@NonNull final Apiary apiary, @NonNull TaskCallback callback) { try { - final Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { + if (apiary.getHives() != null) { + for (Hive hive : apiary.getHives()) { + // Delete records of the hives + if (hive.getRecords() != null) { + hive.getRecords().where().findAll().deleteAllFromRealm(); + } + // Delete hives + hive.deleteFromRealm(); + } + } // Delete apiary apiary.deleteFromRealm(); } @@ -227,20 +236,22 @@ public void execute(Realm realm) { @Override public void deleteHive(@NonNull final Hive hive, @NonNull TaskCallback callback) { - if (hive.getRecords() == null) { + try { + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Delete records of the hive + if (hive.getRecords() != null) { + hive.getRecords().where().findAll().deleteAllFromRealm(); + } + // Delete hive + hive.deleteFromRealm(); + } + }); + callback.onSuccess(); + } catch (Exception e) { callback.onFailure(); - return; } - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - // Delete records of the hive - hive.getRecords().where().findAll().deleteAllFromRealm(); - // Delete hive - hive.deleteFromRealm(); - } - }); - callback.onSuccess(); } @Override @@ -320,26 +331,33 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin @Override public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { - // Get hive - Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); - if (hive == null || hive.getRecords() == null) { + try { + // Get hive + Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); + if (hive == null) { + callback.onFailure(); + return; + } + if (hive.getRecords() != null) { + // Get records to delete + final RealmResults records; + records = hive.getRecords() + .where() + .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) + .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) + .findAll(); + // Delete records + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + records.deleteAllFromRealm(); + } + }); + } + callback.onSuccess(); + } catch (Exception e) { callback.onFailure(); - return; } - // Get records to delete - final RealmResults records = hive.getRecords() - .where() - .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) - .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) - .findAll(); - // Delete records - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - records.deleteAllFromRealm(); - } - }); - callback.onSuccess(); } @Override diff --git a/app/src/main/res/menu/apiary_item_menu.xml b/app/src/main/res/menu/apiary_item_menu.xml new file mode 100644 index 00000000..b378c3a8 --- /dev/null +++ b/app/src/main/res/menu/apiary_item_menu.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ccc6251..f4af44eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,10 @@ You have no apiaries! Apiary saved! + + Apiary deleted! + + Error while deleting apiary. From 6d823cfbc98205d5833469945317e3ebcdd11b12 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 29 Dec 2016 00:05:11 +0100 Subject: [PATCH 04/32] Revert "Implement delete apiary #134" This reverts commit 57f50c487e41e31d3b1a804ed37a50e56adc3974. --- .../gobees/apiaries/ApiariesAdapter.java | 59 +------------ .../gobees/apiaries/ApiariesContract.java | 26 +----- .../gobees/apiaries/ApiariesFragment.java | 19 +---- .../gobees/apiaries/ApiariesPresenter.java | 60 ++++---------- .../gobees/apiary/ApiaryPresenter.java | 2 +- .../gobees/apiary/HivesAdapter.java | 2 +- .../gobees/data/source/GoBeesDataSource.java | 4 +- .../data/source/cache/GoBeesRepository.java | 9 +- .../source/local/GoBeesLocalDataSource.java | 82 ++++++++----------- app/src/main/res/menu/apiary_item_menu.xml | 13 --- app/src/main/res/values/strings.xml | 4 - 11 files changed, 63 insertions(+), 217 deletions(-) delete mode 100644 app/src/main/res/menu/apiary_item_menu.xml diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java index 8ce854bf..e3ec024e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java @@ -5,13 +5,9 @@ import android.support.annotation.NonNull; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; -import android.view.ContextMenu; import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; import com.davidmiguel.gobees.R; @@ -28,12 +24,10 @@ */ class ApiariesAdapter extends RecyclerView.Adapter { - private MenuInflater menuInflater; private List apiaries; private ApiaryItemListener listener; - ApiariesAdapter(MenuInflater menuInflater, List apiaries, ApiaryItemListener listener) { - this.menuInflater = menuInflater; + ApiariesAdapter(List apiaries, ApiaryItemListener listener) { this.apiaries = checkNotNull(apiaries); this.listener = listener; } @@ -64,44 +58,22 @@ interface ApiaryItemListener { void onApiaryClick(Apiary clickedApiary); void onApiaryDelete(Apiary clickedApiary); - - void onOpenMenuClick(View view); } class ViewHolder extends RecyclerView.ViewHolder - implements BaseViewHolder, View.OnClickListener, - View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener, - ItemTouchHelperViewHolder { + implements BaseViewHolder, View.OnClickListener, ItemTouchHelperViewHolder { - private View viewHolder; private CardView card; private TextView apiaryName; private TextView numHives; - private ImageView moreIcon; - private Drawable background; ViewHolder(View itemView) { super(itemView); - - // Get views - viewHolder = itemView; + itemView.setOnClickListener(this); card = (CardView) itemView.findViewById(R.id.card); apiaryName = (TextView) itemView.findViewById(R.id.apiary_name); numHives = (TextView) itemView.findViewById(R.id.num_hives); - moreIcon = (ImageView) itemView.findViewById(R.id.more_icon); - - // Set listeners - viewHolder.setOnClickListener(this); - viewHolder.setOnCreateContextMenuListener(this); - moreIcon.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // Open Menu - listener.onOpenMenuClick(viewHolder); - } - }); - background = card.getBackground(); } @@ -112,36 +84,11 @@ public void bind(@NonNull Apiary apiary) { } } - @Override - public void onCreateContextMenu(ContextMenu contextMenu, View view, - ContextMenu.ContextMenuInfo contextMenuInfo) { - // Inflate menu - menuInflater.inflate(R.menu.apiary_item_menu, contextMenu); - // Set click listener - for (int i = 0; i < contextMenu.size(); i++) { - contextMenu.getItem(i).setOnMenuItemClickListener(this); - } - } - @Override public void onClick(View view) { listener.onApiaryClick(apiaries.get(getAdapterPosition())); } - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.menu_edit: - // TODO - return true; - case R.id.menu_delete: - listener.onApiaryDelete(apiaries.get(getAdapterPosition())); - return true; - default: - return false; - } - } - @Override public void onItemSelected() { card.setBackgroundColor(Color.LTGRAY); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java index 2432a27e..e0fc59e3 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java @@ -17,14 +17,12 @@ interface View extends BaseView { /** * Displays or hide loading indicator. - * * @param active true or false. */ void setLoadingIndicator(final boolean active); /** * Shows list of apiaries. - * * @param apiaries apiaries to show (list cannot be empty). */ void showApiaries(@NonNull List apiaries); @@ -36,7 +34,6 @@ interface View extends BaseView { /** * Opens activity to show the details of the given apiary. - * * @param apiaryId apiary to show. */ void showApiaryDetail(long apiaryId); @@ -55,31 +52,19 @@ interface View extends BaseView { * Shows successfully saved message. */ void showSuccessfullySavedMessage(); - - /** - * Shows successfully deleted message. - */ - void showSuccessfullyDeletedMessage(); - - /** - * Shows error while deleting apiary message. - */ - void showDeletedErrorMessage(); } interface Presenter extends BasePresenter { /** * Shows a snackbar showing whether an apiary was successfully added or not. - * * @param requestCode request code from the intent. - * @param resultCode result code from the intent. + * @param resultCode result code from the intent. */ void result(int requestCode, int resultCode); /** * Load apiaries from repository. - * * @param forceUpdate force cache update. */ void loadApiaries(boolean forceUpdate); @@ -91,21 +76,12 @@ interface Presenter extends BasePresenter { /** * Opens activity to show the details of the given apiary. - * * @param requestedApiary apiary to show. */ void openApiaryDetail(@NonNull Apiary requestedApiary); - /** - * Deletes given apiary. - * - * @param apiary apiary to delete. - */ - void deleteApiary(@NonNull Apiary apiary); - // TODO eliminar generar y eliminar datos void generateData(); - void deleteData(); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index 67ba238c..c2d44b2f 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -54,7 +54,7 @@ public static ApiariesFragment newInstance() { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - listAdapter = new ApiariesAdapter(getActivity().getMenuInflater(), new ArrayList(0), this); + listAdapter = new ApiariesAdapter(new ArrayList(0), this); } @Nullable @@ -192,16 +192,6 @@ public void showSuccessfullySavedMessage() { showMessage(getString(R.string.successfully_saved_apiary_message)); } - @Override - public void showSuccessfullyDeletedMessage() { - showMessage(getString(R.string.successfully_deleted_apiary_message)); - } - - @Override - public void showDeletedErrorMessage() { - showMessage(getString(R.string.deleted_apiary_error_message)); - } - @Override public boolean isActive() { return isAdded(); @@ -219,12 +209,7 @@ public void onApiaryClick(Apiary clickedApiary) { @Override public void onApiaryDelete(Apiary clickedApiary) { - presenter.deleteApiary(clickedApiary); - } - - @Override - public void onOpenMenuClick(View view) { - getActivity().openContextMenu(view); + // TODO delete apiary } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index b5d2031e..0e6c4ed8 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -18,24 +18,24 @@ class ApiariesPresenter implements ApiariesContract.Presenter { private GoBeesRepository goBeesRepository; - private ApiariesContract.View view; + private ApiariesContract.View apiariesView; /** * Force update the first time. */ private boolean firstLoad = true; - ApiariesPresenter(GoBeesRepository goBeesRepository, ApiariesContract.View view) { + ApiariesPresenter(GoBeesRepository goBeesRepository, ApiariesContract.View apiariesView) { this.goBeesRepository = goBeesRepository; - this.view = view; - this.view.setPresenter(this); + this.apiariesView = apiariesView; + this.apiariesView.setPresenter(this); } @Override public void result(int requestCode, int resultCode) { // If a apiary was successfully added, show snackbar if (AddEditApiaryActivity.REQUEST_ADD_APIARY == requestCode && Activity.RESULT_OK == resultCode) { - view.showSuccessfullySavedMessage(); + apiariesView.showSuccessfullySavedMessage(); } } @@ -45,7 +45,7 @@ public void loadApiaries(boolean forceUpdate) { forceUpdate = forceUpdate || firstLoad; firstLoad = false; // Show progress indicator - view.setLoadingIndicator(true); + apiariesView.setLoadingIndicator(true); // Refresh data if needed if (forceUpdate) { goBeesRepository.refreshApiaries(); @@ -56,72 +56,40 @@ public void loadApiaries(boolean forceUpdate) { @Override public void onApiariesLoaded(List apiaries) { // The view may not be able to handle UI updates anymore - if (!view.isActive()) { + if (!apiariesView.isActive()) { return; } // Hide progress indicator - view.setLoadingIndicator(false); + apiariesView.setLoadingIndicator(false); // Process apiaries if (apiaries.isEmpty()) { // Show a message indicating there are no apiaries - view.showNoApiaries(); + apiariesView.showNoApiaries(); } else { // Show the list of apiaries - view.showApiaries(apiaries); + apiariesView.showApiaries(apiaries); } } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore - if (!view.isActive()) { + if (!apiariesView.isActive()) { return; } - view.showLoadingApiariesError(); + apiariesView.showLoadingApiariesError(); } }); } @Override public void addEditApiary() { - view.showAddEditApiary(); + apiariesView.showAddEditApiary(); } @Override public void openApiaryDetail(@NonNull Apiary requestedApiary) { - view.showApiaryDetail(requestedApiary.getId()); - } - - @Override - public void deleteApiary(@NonNull Apiary apiary) { - // Show progress indicator - view.setLoadingIndicator(true); - // Delete apiary - goBeesRepository.deleteApiary(apiary, new GoBeesDataSource.TaskCallback() { - @Override - public void onSuccess() { - // The view may not be able to handle UI updates anymore - if (!view.isActive()) { - return; - } - // Refresh recordings - loadApiaries(true); - // Show success message - view.showSuccessfullyDeletedMessage(); - } - - @Override - public void onFailure() { - // The view may not be able to handle UI updates anymore - if (!view.isActive()) { - return; - } - // Hide progress indicator - view.setLoadingIndicator(false); - // Show error - view.showDeletedErrorMessage(); - } - }); + apiariesView.showApiaryDetail(requestedApiary.getId()); } // TODO eliminar generar y eliminar datos diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index e91cce87..43f77a85 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -100,7 +100,7 @@ public void deleteHive(@NonNull Hive hive) { // Show progress indicator view.setLoadingIndicator(true); // Delete hive - goBeesRepository.deleteHive(hive, new GoBeesDataSource.TaskCallback() { + goBeesRepository.deleteHive(apiaryId, hive, new GoBeesDataSource.TaskCallback() { @Override public void onSuccess() { // The view may not be able to handle UI updates anymore diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java index 5a614750..24344caf 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java @@ -28,8 +28,8 @@ */ class HivesAdapter extends RecyclerView.Adapter { - private MenuInflater menuInflater; private List hives; + private MenuInflater menuInflater; private HivesAdapter.HiveItemListener listener; HivesAdapter(MenuInflater menuInflater, List hives, diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 8614dfe7..8eef69f5 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -64,10 +64,10 @@ public interface GoBeesDataSource { /** * Delete apiary. * - * @param apiary apiary to delete. + * @param apiaryId apiary id. * @param callback TaskCallback. */ - void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback); + void deleteApiary(long apiaryId, @NonNull TaskCallback callback); /** * Delete all apiaries. diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index 09c312e0..d258aa41 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -133,10 +133,15 @@ public void refreshApiaries() { } @Override - public void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback) { + public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { checkNotNull(callback); // Delete apiary - goBeesDataSource.deleteApiary(apiary, callback); + goBeesDataSource.deleteApiary(apiaryId, callback); + // Do in memory cache update to keep the app UI up to date + if (cachedApiaries == null) { + cachedApiaries = new LinkedHashMap<>(); + } + cachedApiaries.remove(apiaryId); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 1c8fd5c2..20c41405 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -98,21 +98,12 @@ public void refreshApiaries() { } @Override - public void deleteApiary(@NonNull final Apiary apiary, @NonNull TaskCallback callback) { + public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { try { + final Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { - if (apiary.getHives() != null) { - for (Hive hive : apiary.getHives()) { - // Delete records of the hives - if (hive.getRecords() != null) { - hive.getRecords().where().findAll().deleteAllFromRealm(); - } - // Delete hives - hive.deleteFromRealm(); - } - } // Delete apiary apiary.deleteFromRealm(); } @@ -236,22 +227,20 @@ public void execute(Realm realm) { @Override public void deleteHive(@NonNull final Hive hive, @NonNull TaskCallback callback) { - try { - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - // Delete records of the hive - if (hive.getRecords() != null) { - hive.getRecords().where().findAll().deleteAllFromRealm(); - } - // Delete hive - hive.deleteFromRealm(); - } - }); - callback.onSuccess(); - } catch (Exception e) { + if (hive.getRecords() == null) { callback.onFailure(); + return; } + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Delete records of the hive + hive.getRecords().where().findAll().deleteAllFromRealm(); + // Delete hive + hive.deleteFromRealm(); + } + }); + callback.onSuccess(); } @Override @@ -331,33 +320,26 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin @Override public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { - try { - // Get hive - Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); - if (hive == null) { - callback.onFailure(); - return; - } - if (hive.getRecords() != null) { - // Get records to delete - final RealmResults records; - records = hive.getRecords() - .where() - .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) - .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) - .findAll(); - // Delete records - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - records.deleteAllFromRealm(); - } - }); - } - callback.onSuccess(); - } catch (Exception e) { + // Get hive + Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); + if (hive == null || hive.getRecords() == null) { callback.onFailure(); + return; } + // Get records to delete + final RealmResults records = hive.getRecords() + .where() + .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) + .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) + .findAll(); + // Delete records + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + records.deleteAllFromRealm(); + } + }); + callback.onSuccess(); } @Override diff --git a/app/src/main/res/menu/apiary_item_menu.xml b/app/src/main/res/menu/apiary_item_menu.xml deleted file mode 100644 index b378c3a8..00000000 --- a/app/src/main/res/menu/apiary_item_menu.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4af44eb..2ccc6251 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,10 +57,6 @@ You have no apiaries! Apiary saved! - - Apiary deleted! - - Error while deleting apiary. From 7912346622aef34916a9f85dc80c28082a053439 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 29 Dec 2016 00:05:11 +0100 Subject: [PATCH 05/32] Revert "Revert "Implement delete apiary #134"" This reverts commit 6d823cfbc98205d5833469945317e3ebcdd11b12. --- .../gobees/apiaries/ApiariesAdapter.java | 59 ++++++++++++- .../gobees/apiaries/ApiariesContract.java | 26 +++++- .../gobees/apiaries/ApiariesFragment.java | 19 ++++- .../gobees/apiaries/ApiariesPresenter.java | 60 ++++++++++---- .../gobees/apiary/ApiaryPresenter.java | 2 +- .../gobees/apiary/HivesAdapter.java | 2 +- .../gobees/data/source/GoBeesDataSource.java | 4 +- .../data/source/cache/GoBeesRepository.java | 9 +- .../source/local/GoBeesLocalDataSource.java | 82 +++++++++++-------- app/src/main/res/menu/apiary_item_menu.xml | 13 +++ app/src/main/res/values/strings.xml | 4 + 11 files changed, 217 insertions(+), 63 deletions(-) create mode 100644 app/src/main/res/menu/apiary_item_menu.xml diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java index e3ec024e..8ce854bf 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java @@ -5,9 +5,13 @@ import android.support.annotation.NonNull; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.view.ContextMenu; import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import com.davidmiguel.gobees.R; @@ -24,10 +28,12 @@ */ class ApiariesAdapter extends RecyclerView.Adapter { + private MenuInflater menuInflater; private List apiaries; private ApiaryItemListener listener; - ApiariesAdapter(List apiaries, ApiaryItemListener listener) { + ApiariesAdapter(MenuInflater menuInflater, List apiaries, ApiaryItemListener listener) { + this.menuInflater = menuInflater; this.apiaries = checkNotNull(apiaries); this.listener = listener; } @@ -58,22 +64,44 @@ interface ApiaryItemListener { void onApiaryClick(Apiary clickedApiary); void onApiaryDelete(Apiary clickedApiary); + + void onOpenMenuClick(View view); } class ViewHolder extends RecyclerView.ViewHolder - implements BaseViewHolder, View.OnClickListener, ItemTouchHelperViewHolder { + implements BaseViewHolder, View.OnClickListener, + View.OnCreateContextMenuListener, MenuItem.OnMenuItemClickListener, + ItemTouchHelperViewHolder { + private View viewHolder; private CardView card; private TextView apiaryName; private TextView numHives; + private ImageView moreIcon; + private Drawable background; ViewHolder(View itemView) { super(itemView); - itemView.setOnClickListener(this); + + // Get views + viewHolder = itemView; card = (CardView) itemView.findViewById(R.id.card); apiaryName = (TextView) itemView.findViewById(R.id.apiary_name); numHives = (TextView) itemView.findViewById(R.id.num_hives); + moreIcon = (ImageView) itemView.findViewById(R.id.more_icon); + + // Set listeners + viewHolder.setOnClickListener(this); + viewHolder.setOnCreateContextMenuListener(this); + moreIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Open Menu + listener.onOpenMenuClick(viewHolder); + } + }); + background = card.getBackground(); } @@ -84,11 +112,36 @@ public void bind(@NonNull Apiary apiary) { } } + @Override + public void onCreateContextMenu(ContextMenu contextMenu, View view, + ContextMenu.ContextMenuInfo contextMenuInfo) { + // Inflate menu + menuInflater.inflate(R.menu.apiary_item_menu, contextMenu); + // Set click listener + for (int i = 0; i < contextMenu.size(); i++) { + contextMenu.getItem(i).setOnMenuItemClickListener(this); + } + } + @Override public void onClick(View view) { listener.onApiaryClick(apiaries.get(getAdapterPosition())); } + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.menu_edit: + // TODO + return true; + case R.id.menu_delete: + listener.onApiaryDelete(apiaries.get(getAdapterPosition())); + return true; + default: + return false; + } + } + @Override public void onItemSelected() { card.setBackgroundColor(Color.LTGRAY); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java index e0fc59e3..2432a27e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java @@ -17,12 +17,14 @@ interface View extends BaseView { /** * Displays or hide loading indicator. + * * @param active true or false. */ void setLoadingIndicator(final boolean active); /** * Shows list of apiaries. + * * @param apiaries apiaries to show (list cannot be empty). */ void showApiaries(@NonNull List apiaries); @@ -34,6 +36,7 @@ interface View extends BaseView { /** * Opens activity to show the details of the given apiary. + * * @param apiaryId apiary to show. */ void showApiaryDetail(long apiaryId); @@ -52,19 +55,31 @@ interface View extends BaseView { * Shows successfully saved message. */ void showSuccessfullySavedMessage(); + + /** + * Shows successfully deleted message. + */ + void showSuccessfullyDeletedMessage(); + + /** + * Shows error while deleting apiary message. + */ + void showDeletedErrorMessage(); } interface Presenter extends BasePresenter { /** * Shows a snackbar showing whether an apiary was successfully added or not. + * * @param requestCode request code from the intent. - * @param resultCode result code from the intent. + * @param resultCode result code from the intent. */ void result(int requestCode, int resultCode); /** * Load apiaries from repository. + * * @param forceUpdate force cache update. */ void loadApiaries(boolean forceUpdate); @@ -76,12 +91,21 @@ interface Presenter extends BasePresenter { /** * Opens activity to show the details of the given apiary. + * * @param requestedApiary apiary to show. */ void openApiaryDetail(@NonNull Apiary requestedApiary); + /** + * Deletes given apiary. + * + * @param apiary apiary to delete. + */ + void deleteApiary(@NonNull Apiary apiary); + // TODO eliminar generar y eliminar datos void generateData(); + void deleteData(); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index c2d44b2f..67ba238c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -54,7 +54,7 @@ public static ApiariesFragment newInstance() { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - listAdapter = new ApiariesAdapter(new ArrayList(0), this); + listAdapter = new ApiariesAdapter(getActivity().getMenuInflater(), new ArrayList(0), this); } @Nullable @@ -192,6 +192,16 @@ public void showSuccessfullySavedMessage() { showMessage(getString(R.string.successfully_saved_apiary_message)); } + @Override + public void showSuccessfullyDeletedMessage() { + showMessage(getString(R.string.successfully_deleted_apiary_message)); + } + + @Override + public void showDeletedErrorMessage() { + showMessage(getString(R.string.deleted_apiary_error_message)); + } + @Override public boolean isActive() { return isAdded(); @@ -209,7 +219,12 @@ public void onApiaryClick(Apiary clickedApiary) { @Override public void onApiaryDelete(Apiary clickedApiary) { - // TODO delete apiary + presenter.deleteApiary(clickedApiary); + } + + @Override + public void onOpenMenuClick(View view) { + getActivity().openContextMenu(view); } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index 0e6c4ed8..b5d2031e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -18,24 +18,24 @@ class ApiariesPresenter implements ApiariesContract.Presenter { private GoBeesRepository goBeesRepository; - private ApiariesContract.View apiariesView; + private ApiariesContract.View view; /** * Force update the first time. */ private boolean firstLoad = true; - ApiariesPresenter(GoBeesRepository goBeesRepository, ApiariesContract.View apiariesView) { + ApiariesPresenter(GoBeesRepository goBeesRepository, ApiariesContract.View view) { this.goBeesRepository = goBeesRepository; - this.apiariesView = apiariesView; - this.apiariesView.setPresenter(this); + this.view = view; + this.view.setPresenter(this); } @Override public void result(int requestCode, int resultCode) { // If a apiary was successfully added, show snackbar if (AddEditApiaryActivity.REQUEST_ADD_APIARY == requestCode && Activity.RESULT_OK == resultCode) { - apiariesView.showSuccessfullySavedMessage(); + view.showSuccessfullySavedMessage(); } } @@ -45,7 +45,7 @@ public void loadApiaries(boolean forceUpdate) { forceUpdate = forceUpdate || firstLoad; firstLoad = false; // Show progress indicator - apiariesView.setLoadingIndicator(true); + view.setLoadingIndicator(true); // Refresh data if needed if (forceUpdate) { goBeesRepository.refreshApiaries(); @@ -56,40 +56,72 @@ public void loadApiaries(boolean forceUpdate) { @Override public void onApiariesLoaded(List apiaries) { // The view may not be able to handle UI updates anymore - if (!apiariesView.isActive()) { + if (!view.isActive()) { return; } // Hide progress indicator - apiariesView.setLoadingIndicator(false); + view.setLoadingIndicator(false); // Process apiaries if (apiaries.isEmpty()) { // Show a message indicating there are no apiaries - apiariesView.showNoApiaries(); + view.showNoApiaries(); } else { // Show the list of apiaries - apiariesView.showApiaries(apiaries); + view.showApiaries(apiaries); } } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore - if (!apiariesView.isActive()) { + if (!view.isActive()) { return; } - apiariesView.showLoadingApiariesError(); + view.showLoadingApiariesError(); } }); } @Override public void addEditApiary() { - apiariesView.showAddEditApiary(); + view.showAddEditApiary(); } @Override public void openApiaryDetail(@NonNull Apiary requestedApiary) { - apiariesView.showApiaryDetail(requestedApiary.getId()); + view.showApiaryDetail(requestedApiary.getId()); + } + + @Override + public void deleteApiary(@NonNull Apiary apiary) { + // Show progress indicator + view.setLoadingIndicator(true); + // Delete apiary + goBeesRepository.deleteApiary(apiary, new GoBeesDataSource.TaskCallback() { + @Override + public void onSuccess() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Refresh recordings + loadApiaries(true); + // Show success message + view.showSuccessfullyDeletedMessage(); + } + + @Override + public void onFailure() { + // The view may not be able to handle UI updates anymore + if (!view.isActive()) { + return; + } + // Hide progress indicator + view.setLoadingIndicator(false); + // Show error + view.showDeletedErrorMessage(); + } + }); } // TODO eliminar generar y eliminar datos diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index 43f77a85..e91cce87 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -100,7 +100,7 @@ public void deleteHive(@NonNull Hive hive) { // Show progress indicator view.setLoadingIndicator(true); // Delete hive - goBeesRepository.deleteHive(apiaryId, hive, new GoBeesDataSource.TaskCallback() { + goBeesRepository.deleteHive(hive, new GoBeesDataSource.TaskCallback() { @Override public void onSuccess() { // The view may not be able to handle UI updates anymore diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java index 24344caf..5a614750 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java @@ -28,8 +28,8 @@ */ class HivesAdapter extends RecyclerView.Adapter { - private List hives; private MenuInflater menuInflater; + private List hives; private HivesAdapter.HiveItemListener listener; HivesAdapter(MenuInflater menuInflater, List hives, diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 8eef69f5..8614dfe7 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -64,10 +64,10 @@ public interface GoBeesDataSource { /** * Delete apiary. * - * @param apiaryId apiary id. + * @param apiary apiary to delete. * @param callback TaskCallback. */ - void deleteApiary(long apiaryId, @NonNull TaskCallback callback); + void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback); /** * Delete all apiaries. diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index d258aa41..09c312e0 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -133,15 +133,10 @@ public void refreshApiaries() { } @Override - public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { + public void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback) { checkNotNull(callback); // Delete apiary - goBeesDataSource.deleteApiary(apiaryId, callback); - // Do in memory cache update to keep the app UI up to date - if (cachedApiaries == null) { - cachedApiaries = new LinkedHashMap<>(); - } - cachedApiaries.remove(apiaryId); + goBeesDataSource.deleteApiary(apiary, callback); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 20c41405..1c8fd5c2 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -98,12 +98,21 @@ public void refreshApiaries() { } @Override - public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { + public void deleteApiary(@NonNull final Apiary apiary, @NonNull TaskCallback callback) { try { - final Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { + if (apiary.getHives() != null) { + for (Hive hive : apiary.getHives()) { + // Delete records of the hives + if (hive.getRecords() != null) { + hive.getRecords().where().findAll().deleteAllFromRealm(); + } + // Delete hives + hive.deleteFromRealm(); + } + } // Delete apiary apiary.deleteFromRealm(); } @@ -227,20 +236,22 @@ public void execute(Realm realm) { @Override public void deleteHive(@NonNull final Hive hive, @NonNull TaskCallback callback) { - if (hive.getRecords() == null) { + try { + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Delete records of the hive + if (hive.getRecords() != null) { + hive.getRecords().where().findAll().deleteAllFromRealm(); + } + // Delete hive + hive.deleteFromRealm(); + } + }); + callback.onSuccess(); + } catch (Exception e) { callback.onFailure(); - return; } - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - // Delete records of the hive - hive.getRecords().where().findAll().deleteAllFromRealm(); - // Delete hive - hive.deleteFromRealm(); - } - }); - callback.onSuccess(); } @Override @@ -320,26 +331,33 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin @Override public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { - // Get hive - Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); - if (hive == null || hive.getRecords() == null) { + try { + // Get hive + Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); + if (hive == null) { + callback.onFailure(); + return; + } + if (hive.getRecords() != null) { + // Get records to delete + final RealmResults records; + records = hive.getRecords() + .where() + .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) + .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) + .findAll(); + // Delete records + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + records.deleteAllFromRealm(); + } + }); + } + callback.onSuccess(); + } catch (Exception e) { callback.onFailure(); - return; } - // Get records to delete - final RealmResults records = hive.getRecords() - .where() - .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) - .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) - .findAll(); - // Delete records - realm.executeTransaction(new Realm.Transaction() { - @Override - public void execute(Realm realm) { - records.deleteAllFromRealm(); - } - }); - callback.onSuccess(); } @Override diff --git a/app/src/main/res/menu/apiary_item_menu.xml b/app/src/main/res/menu/apiary_item_menu.xml new file mode 100644 index 00000000..b378c3a8 --- /dev/null +++ b/app/src/main/res/menu/apiary_item_menu.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ccc6251..f4af44eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,10 @@ You have no apiaries! Apiary saved! + + Apiary deleted! + + Error while deleting apiary. From 951d7e41ecccd8c28dd29c22a8f6bac2c1224c6d Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 29 Dec 2016 00:36:56 +0100 Subject: [PATCH 06/32] Minor fix in delete apiary #134 --- .../gobees/data/source/local/GoBeesLocalDataSource.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 1c8fd5c2..e7860f6b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -104,14 +104,14 @@ public void deleteApiary(@NonNull final Apiary apiary, @NonNull TaskCallback cal @Override public void execute(Realm realm) { if (apiary.getHives() != null) { + // Delete records of the hives for (Hive hive : apiary.getHives()) { - // Delete records of the hives if (hive.getRecords() != null) { hive.getRecords().where().findAll().deleteAllFromRealm(); } - // Delete hives - hive.deleteFromRealm(); } + // Delete hives + apiary.getHives().where().findAll().deleteAllFromRealm(); } // Delete apiary apiary.deleteFromRealm(); @@ -119,6 +119,7 @@ public void execute(Realm realm) { }); callback.onSuccess(); } catch (Exception e) { + e.printStackTrace(); callback.onFailure(); } } From b4b49d467c32ddb1eada5532d691051ea9844eda Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 29 Dec 2016 00:49:08 +0100 Subject: [PATCH 07/32] Implement edit apiary #131 --- .../gobees/apiaries/ApiariesAdapter.java | 8 ++++--- .../gobees/apiaries/ApiariesContract.java | 8 +++++-- .../gobees/apiaries/ApiariesFragment.java | 21 +++++++++++++------ .../gobees/apiaries/ApiariesPresenter.java | 4 ++-- .../source/local/GoBeesLocalDataSource.java | 1 - 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java index 8ce854bf..1e5eb149 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java @@ -61,9 +61,11 @@ void replaceData(List apiaries) { } interface ApiaryItemListener { - void onApiaryClick(Apiary clickedApiary); + void onApiaryClick(Apiary apiary); - void onApiaryDelete(Apiary clickedApiary); + void onApiaryDelete(Apiary apiary); + + void onApiaryEdit(Apiary apiary); void onOpenMenuClick(View view); } @@ -132,7 +134,7 @@ public void onClick(View view) { public boolean onMenuItemClick(MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_edit: - // TODO + listener.onApiaryEdit(apiaries.get(getAdapterPosition())); return true; case R.id.menu_delete: listener.onApiaryDelete(apiaries.get(getAdapterPosition())); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java index 2432a27e..956d6c58 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesContract.java @@ -31,8 +31,10 @@ interface View extends BaseView { /** * Opens activity to add or edit an apiary. + * + * @param apiaryId apiary id (or -1 for creating a new one). */ - void showAddEditApiary(); + void showAddEditApiary(long apiaryId); /** * Opens activity to show the details of the given apiary. @@ -86,8 +88,10 @@ interface Presenter extends BasePresenter { /** * Orders to open activity to add or edit an apiary. + * + * @param apiaryId apiary id (or -1 for creating a new one). */ - void addEditApiary(); + void addEditApiary(long apiaryId); /** * Opens activity to show the details of the given apiary. diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index 67ba238c..4a6f7cbb 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -21,6 +21,7 @@ import com.davidmiguel.gobees.R; import com.davidmiguel.gobees.addeditapiary.AddEditApiaryActivity; +import com.davidmiguel.gobees.addeditapiary.AddEditApiaryFragment; import com.davidmiguel.gobees.apiaries.ApiariesAdapter.ApiaryItemListener; import com.davidmiguel.gobees.apiary.ApiaryActivity; import com.davidmiguel.gobees.apiary.ApiaryHivesFragment; @@ -80,7 +81,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - presenter.addEditApiary(); + presenter.addEditApiary(AddEditApiaryActivity.NEW_APIARY); } }); @@ -165,8 +166,11 @@ public void showApiaries(@NonNull List apiaries) { } @Override - public void showAddEditApiary() { + public void showAddEditApiary(long apiaryId) { Intent intent = new Intent(getContext(), AddEditApiaryActivity.class); + if(apiaryId != AddEditApiaryActivity.NEW_APIARY) { + intent.putExtra(AddEditApiaryFragment.ARGUMENT_EDIT_APIARY_ID, apiaryId); + } startActivityForResult(intent, AddEditApiaryActivity.REQUEST_ADD_APIARY); } @@ -213,13 +217,18 @@ public void setPresenter(@NonNull ApiariesContract.Presenter presenter) { } @Override - public void onApiaryClick(Apiary clickedApiary) { - presenter.openApiaryDetail(clickedApiary); + public void onApiaryClick(Apiary apiary) { + presenter.openApiaryDetail(apiary); + } + + @Override + public void onApiaryDelete(Apiary apiary) { + presenter.deleteApiary(apiary); } @Override - public void onApiaryDelete(Apiary clickedApiary) { - presenter.deleteApiary(clickedApiary); + public void onApiaryEdit(Apiary apiary) { + presenter.addEditApiary(apiary.getId()); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index b5d2031e..19a24a88 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -83,8 +83,8 @@ public void onDataNotAvailable() { } @Override - public void addEditApiary() { - view.showAddEditApiary(); + public void addEditApiary(long apiaryId) { + view.showAddEditApiary(apiaryId); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index e7860f6b..4d1035f5 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -119,7 +119,6 @@ public void execute(Realm realm) { }); callback.onSuccess(); } catch (Exception e) { - e.printStackTrace(); callback.onFailure(); } } From 867485408c0ddc88325ef34e5fe6eb43cc5fa38b Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 29 Dec 2016 01:06:11 +0100 Subject: [PATCH 08/32] Implement edit hive #133 --- .../addedithive/AddEditHiveFragment.java | 5 ++-- .../addedithive/AddEditHivePresenter.java | 24 +++++++++---------- .../gobees/apiary/ApiaryContract.java | 17 ++++++++++--- .../gobees/apiary/ApiaryHivesFragment.java | 20 +++++++++++----- .../gobees/apiary/ApiaryPresenter.java | 4 ++-- .../gobees/apiary/HivesAdapter.java | 8 ++++--- .../gobees/hive/RecordingsAdapter.java | 3 --- app/src/main/res/menu/recording_item_menu.xml | 4 ---- 8 files changed, 50 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java index 32558d0d..72b61d62 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java @@ -97,7 +97,7 @@ public void setName(String name) { @Override public void setNotes(String notes) { - nameTextView.setText(notes); + notesTextView.setText(notes); } @Override @@ -112,7 +112,8 @@ public boolean isActive() { /** * Shows a snackbar with the given message. - * @param view view. + * + * @param view view. * @param message message to show. */ @SuppressWarnings("ConstantConditions") diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java index dd8b56bb..7e274bf8 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java @@ -14,19 +14,19 @@ class AddEditHivePresenter implements AddEditHiveContract.Presenter, GetHiveCallback, TaskCallback { private GoBeesRepository goBeesRepository; - private AddEditHiveContract.View addEditHiveView; + private AddEditHiveContract.View view; private long apiaryId; private long hiveId; AddEditHivePresenter(GoBeesRepository goBeesRepository, - AddEditHiveContract.View addEditHiveView, + AddEditHiveContract.View view, long apiaryId, long hiveId) { this.goBeesRepository = goBeesRepository; - this.addEditHiveView = addEditHiveView; + this.view = view; this.apiaryId = apiaryId; this.hiveId = hiveId; - addEditHiveView.setPresenter(this); + view.setPresenter(this); } @Override @@ -56,30 +56,30 @@ public void start() { @Override public void onHiveLoaded(Hive hive) { // Show hive data on view - if (addEditHiveView.isActive()) { - addEditHiveView.setName(hive.getName()); - addEditHiveView.setNotes(hive.getNotes()); + if (view.isActive()) { + view.setName(hive.getName()); + view.setNotes(hive.getNotes()); } } @Override public void onDataNotAvailable() { // Show error message - if (addEditHiveView.isActive()) { - addEditHiveView.showEmptyHiveError(); + if (view.isActive()) { + view.showEmptyHiveError(); } } @Override public void onSuccess() { // Apiary saved successfully -> go back to apiaries activity - addEditHiveView.showHivesList(); + view.showHivesList(); } @Override public void onFailure() { // Error saving apiaries - addEditHiveView.showSaveHiveError(); + view.showSaveHiveError(); } private boolean isNewHive() { @@ -97,7 +97,7 @@ public void onNextHiveIdLoaded(long hiveId) { if (newHive.isValidHive()) { goBeesRepository.saveHive(apiaryId, newHive, listener); } else { - addEditHiveView.showEmptyHiveError(); + view.showEmptyHiveError(); } } }); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java index 67b0d76c..65ec12b7 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java @@ -17,24 +17,29 @@ interface View extends BaseView { /** * Displays or hide loading indicator. + * * @param active true or false. */ void setLoadingIndicator(final boolean active); /** * Shows list of hives. + * * @param hives hives to show (list cannot be empty). */ void showHives(@NonNull List hives); /** * Opens activity to add or edit a hive. + * * @param apiaryId apiary id. + * @param hiveId hive id (or -1 for creating a new one). */ - void showAddEditHive(long apiaryId); + void showAddEditHive(long apiaryId, long hiveId); /** * Opens activity to show the details of the given hive. + * * @param hiveId hive to show. */ void showHiveDetail(long hiveId); @@ -66,6 +71,7 @@ interface View extends BaseView { /** * Sets the title in the action bar. + * * @param title title. */ void showTitle(@NonNull String title); @@ -75,24 +81,29 @@ interface Presenter extends BasePresenter { /** * Shows a snackbar showing whether a hive was successfully added or not. + * * @param requestCode request code from the intent. - * @param resultCode result code from the intent. + * @param resultCode result code from the intent. */ void result(int requestCode, int resultCode); /** * Load hives from repository. + * * @param forceUpdate force cache update. */ void loadHives(boolean forceUpdate); /** * Orders to open activity to add or edit a hive. + * + * @param hiveId hive id (or -1 for creating a new one). */ - void addEditHive(); + void addEditHive(long hiveId); /** * Opens activity to show the details of the given hive. + * * @param requestedHive hive to show. */ void openHiveDetail(@NonNull Hive requestedHive); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java index cedc30dd..7916b711 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java @@ -84,7 +84,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - presenter.addEditHive(); + presenter.addEditHive(AddEditHiveActivity.NEW_HIVE); } }); @@ -170,9 +170,12 @@ public void showHives(@NonNull List hives) { } @Override - public void showAddEditHive(long apiaryId) { + public void showAddEditHive(long apiaryId, long hiveId) { Intent intent = new Intent(getContext(), AddEditHiveActivity.class); intent.putExtra(AddEditHiveFragment.ARGUMENT_EDIT_APIARY_ID, apiaryId); + if (hiveId != AddEditHiveActivity.NEW_HIVE) { + intent.putExtra(AddEditHiveFragment.ARGUMENT_EDIT_HIVE_ID, hiveId); + } startActivityForResult(intent, AddEditHiveActivity.REQUEST_ADD_HIVE); } @@ -227,13 +230,18 @@ public boolean isActive() { } @Override - public void onHiveClick(Hive clickedHive) { - presenter.openHiveDetail(clickedHive); + public void onHiveClick(Hive hive) { + presenter.openHiveDetail(hive); + } + + @Override + public void onHiveDelete(Hive hive) { + presenter.deleteHive(hive); } @Override - public void onHiveDelete(Hive clickedHive) { - presenter.deleteHive(clickedHive); + public void onHiveEdit(Hive hive) { + presenter.addEditHive(hive.getId()); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index e91cce87..da9f8061 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -86,8 +86,8 @@ public void onDataNotAvailable() { } @Override - public void addEditHive() { - view.showAddEditHive(apiaryId); + public void addEditHive(long hiveId) { + view.showAddEditHive(apiaryId, hiveId); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java index 5a614750..d7023358 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/HivesAdapter.java @@ -62,9 +62,11 @@ void replaceData(List hives) { } interface HiveItemListener { - void onHiveClick(Hive clickedHive); + void onHiveClick(Hive hive); - void onHiveDelete(Hive clickedHive); + void onHiveDelete(Hive hive); + + void onHiveEdit(Hive hive); void onOpenMenuClick(View view); } @@ -128,7 +130,7 @@ public void onClick(View view) { public boolean onMenuItemClick(MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.menu_edit: - // TODO + listener.onHiveEdit(hives.get(getAdapterPosition())); return true; case R.id.menu_delete: listener.onHiveDelete(hives.get(getAdapterPosition())); diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java index e5b4200d..ee984f89 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java @@ -159,9 +159,6 @@ public void onClick(View view) { @Override public boolean onMenuItemClick(MenuItem menuItem) { switch (menuItem.getItemId()) { - case R.id.menu_edit: - // TODO - return true; case R.id.menu_delete: listener.onRecordingDelete(recordings.get(getAdapterPosition())); return true; diff --git a/app/src/main/res/menu/recording_item_menu.xml b/app/src/main/res/menu/recording_item_menu.xml index b378c3a8..58bbdeec 100644 --- a/app/src/main/res/menu/recording_item_menu.xml +++ b/app/src/main/res/menu/recording_item_menu.xml @@ -2,10 +2,6 @@ - Date: Fri, 30 Dec 2016 01:12:32 +0100 Subject: [PATCH 09/32] Add openweathermap key #92 --- app/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 2454fa70..e605be7a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,6 +33,11 @@ android { } } + buildTypes.each { + // OpenWeatherMap key (stored in [USER_HOME]/.gradle/gradle.properties) + it.buildConfigField 'String', 'OPEN_WEATHER_MAP_API_KEY', OpenWeatherMapApiKey + } + // Mock: stubs out the service layer completely and returns a fake dataset // Prod: production version productFlavors { From d7d75c2f9043139610d59eaf5f47d13a40685e1b Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Fri, 30 Dec 2016 16:56:49 +0100 Subject: [PATCH 10/32] Add network call and json parser #92 --- app/build.gradle | 1 + .../davidmiguel/gobees/data/model/Apiary.java | 31 +--- .../gobees/data/model/MeteoDetail.java | 153 ------------------ .../model/{MeteoDay.java => MeteoRecord.java} | 150 ++++++++++------- .../gobees/data/model/Recording.java | 6 +- .../data/model/mothers/ApiaryMother.java | 8 +- .../data/source/network/NetworkUtils.java | 85 ++++++++++ .../source/network/OpenWeatherMapUtils.java | 140 ++++++++++++++++ .../source/network/WeatherDataSource.java | 32 ++++ .../source/cache/GoBeesRepositoryTest.java | 4 +- .../network/OpenWeatherMapUtilsTest.java | 72 +++++++++ 11 files changed, 438 insertions(+), 244 deletions(-) delete mode 100644 app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDetail.java rename app/src/main/java/com/davidmiguel/gobees/data/model/{MeteoDay.java => MeteoRecord.java} (58%) create mode 100644 app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java create mode 100644 app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java create mode 100644 app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java create mode 100644 app/src/test/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtilsTest.java diff --git a/app/build.gradle b/app/build.gradle index e605be7a..5f64a746 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,7 @@ dependencies { testCompile 'org.slf4j:slf4j-api:1.7.21' testCompile 'org.slf4j:slf4j-log4j12:1.7.21' testCompile 'log4j:log4j:1.2.17' + testCompile 'org.json:json:20160810' // Android Testing Support Library's runner and rules androidTestCompile 'com.android.support.test:runner:0.5' diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java index 6ceed60a..581a79fa 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java @@ -60,13 +60,7 @@ public class Apiary extends RealmObject { * List of meteorological data from days. */ @Nullable - private RealmList meteoDays; - - /** - * List of meteorological data from specific moments in time. - */ - @Nullable - private RealmList meteoDetails; + private RealmList meteoRecords; public Apiary() { // Needed by Realm @@ -74,8 +68,7 @@ public Apiary() { public Apiary(long id, String name, @Nullable String imageUrl, @Nullable Double locationLong, @Nullable Double locationLat, @Nullable String notes, - @Nullable RealmList hives, @Nullable RealmList meteoDays, - @Nullable RealmList meteoDetails) { + @Nullable RealmList hives, @Nullable RealmList meteoRecords) { this.id = id; this.name = name; this.imageUrl = imageUrl; @@ -83,8 +76,7 @@ public Apiary(long id, String name, @Nullable String imageUrl, @Nullable Double this.locationLat = locationLat; this.notes = notes; this.hives = hives; - this.meteoDays = meteoDays; - this.meteoDetails = meteoDetails; + this.meteoRecords = meteoRecords; } public long getId() { @@ -149,21 +141,12 @@ public void setHives(@Nullable RealmList hives) { } @Nullable - public RealmList getMeteoDays() { - return meteoDays; - } - - public void setMeteoDays(@Nullable RealmList meteoDays) { - this.meteoDays = meteoDays; - } - - @Nullable - public RealmList getMeteoDetails() { - return meteoDetails; + public RealmList getMeteoRecords() { + return meteoRecords; } - public void setMeteoDetails(@Nullable RealmList meteoDetails) { - this.meteoDetails = meteoDetails; + public void setMeteoRecords(@Nullable RealmList meteoRecords) { + this.meteoRecords = meteoRecords; } public void addHive(@NonNull Hive hive){ diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDetail.java b/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDetail.java deleted file mode 100644 index 37569433..00000000 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDetail.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.davidmiguel.gobees.data.model; - -import android.support.annotation.Nullable; - -import com.google.common.base.Objects; - -import java.util.Date; - -import io.realm.RealmObject; -import io.realm.annotations.PrimaryKey; -import io.realm.annotations.Required; - -/** - * Model class for saving the meteorology of a specific moment in time. - */ -@SuppressWarnings("unused") -public class MeteoDetail extends RealmObject { - - @PrimaryKey - private long id; - - /** - * Time of data calculation. - */ - @Required - private Date timestamp; - - /** - * Weather icon id, e.g: "04n". - */ - @Nullable - private String icon; - - /** - * Current temperature in Kelvin, e.g: 285.95. - */ - private double temperature; - - /** - * Percentage of cloudiness, e.g: 56. - */ - private int clouds; - - /** - * Rain volume (mm) for the last 3 hours, e.g: 3.25. - */ - private double rain; - - /** - * Snow volume (mm) for the last 3 hours, e.g: 2.3. - */ - private double snow; - - /** - * Percentage of humidity, e.g: 18. - */ - private int humidity; - - /** - * Wind speed in meter/sec, e.g: 6.17. - */ - private double windSpeed; - - /** - * Wind direction in degrees (meteorological), e.g: 209.5. - */ - private double windDegrees; - - public MeteoDetail() { - // Needed by Realm - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public Date getTimestamp() { - return timestamp; - } - - public void setTimestamp(Date timestamp) { - this.timestamp = timestamp; - } - - @Nullable - public String getIcon() { - return icon; - } - - public void setIcon(@Nullable String icon) { - this.icon = icon; - } - - public double getTemperature() { - return temperature; - } - - public void setTemperature(double temperature) { - this.temperature = temperature; - } - - public int getClouds() { - return clouds; - } - - public void setClouds(int clouds) { - this.clouds = clouds; - } - - public double getRain() { - return rain; - } - - public void setRain(double rain) { - this.rain = rain; - } - - public double getSnow() { - return snow; - } - - public void setSnow(double snow) { - this.snow = snow; - } - - public int getHumidity() { - return humidity; - } - - public void setHumidity(int humidity) { - this.humidity = humidity; - } - - public double getWindSpeed() { - return windSpeed; - } - - public void setWindSpeed(double windSpeed) { - this.windSpeed = windSpeed; - } - - public double getWindDegrees() { - return windDegrees; - } - - public void setWindDegrees(double windDegrees) { - this.windDegrees = windDegrees; - } -} diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDay.java b/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoRecord.java similarity index 58% rename from app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDay.java rename to app/src/main/java/com/davidmiguel/gobees/data/model/MeteoRecord.java index 7f83b770..a0fbefc3 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoDay.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/MeteoRecord.java @@ -2,8 +2,6 @@ import android.support.annotation.Nullable; -import com.google.common.base.Objects; - import java.util.Date; import io.realm.RealmObject; @@ -11,10 +9,10 @@ import io.realm.annotations.Required; /** - * Model class for saving the general meteorology of a specific day. + * Model class for saving the a meteorology record. */ @SuppressWarnings("unused") -public class MeteoDay extends RealmObject { +public class MeteoRecord extends RealmObject { @PrimaryKey private long id; @@ -26,16 +24,21 @@ public class MeteoDay extends RealmObject { private Date timestamp; /** - * Weather icon id, e.g: "04n". + * City name. */ @Nullable - private String icon; + private String cityName; + + /** + * Id of the weather condition (http://openweathermap.org/weather-conditions). + */ + private int weatherCondition; /** - * Weather condition description, e.g: "overcast clouds". + * Weather icon id, e.g: "04n". */ @Nullable - private String description; + private String weatherConditionIcon; /** * Temperature in Kelvin, e.g: 274.03. @@ -53,19 +56,9 @@ public class MeteoDay extends RealmObject { private double temperatureMax; /** - * Percentage of cloudiness, e.g: 56. + * Pressure, e.g: 1023. */ - private int clouds; - - /** - * Rain volume (mm) for the last 3 hours, e.g: 3.25. - */ - private double rain; - - /** - * Snow volume (mm) for the last 3 hours, e.g: 2.3. - */ - private double snow; + private int pressure; /** * Percentage of humidity, e.g: 18. @@ -82,26 +75,53 @@ public class MeteoDay extends RealmObject { */ private double windDegrees; - public MeteoDay() { + /** + * Percentage of cloudiness, e.g: 56. + */ + private int clouds; + + /** + * Rain volume (mm) for the last 3 hours, e.g: 3.25. + */ + private double rain; + + /** + * Snow volume (mm) for the last 3 hours, e.g: 2.3. + */ + private double snow; + + public MeteoRecord() { // Needed by Realm } - public MeteoDay(long id, Date timestamp, @Nullable String icon, @Nullable String description, double temperature, - double temperatureMin, double temperatureMax, int clouds, double rain, - double snow, int humidity, double windSpeed, double windDegrees) { + private MeteoRecord(long id, Date timestamp, @Nullable String cityName, int weatherCondition, + @Nullable String weatherConditionIcon, double temperature, double temperatureMin, + double temperatureMax, int pressure, int humidity, double windSpeed, + double windDegrees, int clouds, double rain, double snow) { this.id = id; this.timestamp = timestamp; - this.icon = icon; - this.description = description; + this.cityName = cityName; + this.weatherCondition = weatherCondition; + this.weatherConditionIcon = weatherConditionIcon; this.temperature = temperature; this.temperatureMin = temperatureMin; this.temperatureMax = temperatureMax; - this.clouds = clouds; - this.rain = rain; - this.snow = snow; + this.pressure = pressure; this.humidity = humidity; this.windSpeed = windSpeed; this.windDegrees = windDegrees; + this.clouds = clouds; + this.rain = rain; + this.snow = snow; + } + + public MeteoRecord(Date timestamp, String cityName, int weatherCondition, + String weatherConditionIcon, double temperature, double temperatureMin, + double temperatureMax, int pressure, int humidity, double windSpeed, + double windDegrees, int clouds, double rain, double snow) { + this(-1, timestamp, cityName, weatherCondition, weatherConditionIcon, temperature, + temperatureMin, temperatureMax, pressure, humidity, windSpeed, windDegrees, + clouds, rain, snow); } public long getId() { @@ -121,21 +141,29 @@ public void setTimestamp(Date timestamp) { } @Nullable - public String getIcon() { - return icon; + public String getCityName() { + return cityName; + } + + public void setCityName(@Nullable String cityName) { + this.cityName = cityName; + } + + public int getWeatherCondition() { + return weatherCondition; } - public void setIcon(@Nullable String icon) { - this.icon = icon; + public void setWeatherCondition(int weatherCondition) { + this.weatherCondition = weatherCondition; } @Nullable - public String getDescription() { - return description; + public String getWeatherConditionIcon() { + return weatherConditionIcon; } - public void setDescription(@Nullable String description) { - this.description = description; + public void setWeatherConditionIcon(@Nullable String weatherConditionIcon) { + this.weatherConditionIcon = weatherConditionIcon; } public double getTemperature() { @@ -162,28 +190,12 @@ public void setTemperatureMax(double temperatureMax) { this.temperatureMax = temperatureMax; } - public int getClouds() { - return clouds; - } - - public void setClouds(int clouds) { - this.clouds = clouds; - } - - public double getRain() { - return rain; - } - - public void setRain(double rain) { - this.rain = rain; + public int getPressure() { + return pressure; } - public double getSnow() { - return snow; - } - - public void setSnow(double snow) { - this.snow = snow; + public void setPressure(int pressure) { + this.pressure = pressure; } public int getHumidity() { @@ -209,4 +221,28 @@ public double getWindDegrees() { public void setWindDegrees(double windDegrees) { this.windDegrees = windDegrees; } + + public int getClouds() { + return clouds; + } + + public void setClouds(int clouds) { + this.clouds = clouds; + } + + public double getRain() { + return rain; + } + + public void setRain(double rain) { + this.rain = rain; + } + + public double getSnow() { + return snow; + } + + public void setSnow(double snow) { + this.snow = snow; + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/Recording.java b/app/src/main/java/com/davidmiguel/gobees/data/model/Recording.java index e73b0b11..107b219c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/Recording.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/Recording.java @@ -22,9 +22,9 @@ public class Recording { /** * List of meteorological records. */ - private List meteo; + private List meteo; - public Recording(Date date, List records, List meteo) { + public Recording(Date date, List records, List meteo) { this.date = date; this.records = records; this.meteo = meteo; @@ -43,7 +43,7 @@ public List getRecords() { return records; } - public List getMeteo() { + public List getMeteo() { return meteo; } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java b/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java index c623847c..24a81678 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java @@ -2,8 +2,7 @@ import com.davidmiguel.gobees.data.model.Apiary; import com.davidmiguel.gobees.data.model.Hive; -import com.davidmiguel.gobees.data.model.MeteoDay; -import com.davidmiguel.gobees.data.model.MeteoDetail; +import com.davidmiguel.gobees.data.model.MeteoRecord; import java.util.ArrayList; import java.util.List; @@ -29,8 +28,7 @@ public class ApiaryMother { private Double locationLat; private String notes; private RealmList hives; - private RealmList meteoDays; - private RealmList meteoDetails; + private RealmList meteoRecords; private ApiaryMother() { setValues(NUM_HIVES); @@ -87,7 +85,7 @@ private ApiaryMother withNotes(String notes) { private Apiary build() { return new Apiary(id, name, imageUrl, locationLong, - locationLat, notes, hives, meteoDays, meteoDetails); + locationLat, notes, hives, meteoRecords); } private List generateHives(int num) { diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java new file mode 100644 index 00000000..d8f61dc5 --- /dev/null +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java @@ -0,0 +1,85 @@ +package com.davidmiguel.gobees.data.source.network; + +import android.net.Uri; + +import com.davidmiguel.gobees.BuildConfig; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Scanner; + +/** + * These utilities will be used to communicate with the weather servers. + */ +public class NetworkUtils { + + /* Current weather API (http://openweathermap.org/current) */ + private static final String CURRENT_WEATHER_URL = + "http://api.openweathermap.org/data/2.5/weather"; + + /* Query parameters */ + private static final String LAT_PARAM = "lat"; + private static final String LON_PARAM = "lon"; + private static final String UNITS_PARAM = "units"; + private static final String APPID_PARAM = "appid"; + + /* The units we want our API to return */ + private static final String UNITS = "metric"; + + /** + * Builds the URL to get current weather data. + * + * @param latitude the latitude of the location. + * @param longitude the longitude of the location. + * @return url to use to query the weather server. + */ + public static URL getCurrentWeatherUrl(double latitude, double longitude) { + Uri weatherQueryUri = Uri.parse(CURRENT_WEATHER_URL).buildUpon() + .appendQueryParameter(LAT_PARAM, String.valueOf(latitude)) + .appendQueryParameter(LON_PARAM, String.valueOf(longitude)) + .appendQueryParameter(UNITS_PARAM, UNITS) + .appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_WEATHER_MAP_API_KEY) + .build(); + + try { + return new URL(weatherQueryUri.toString()); + } catch (MalformedURLException e) { + return null; + } + } + + /** + * This method returns the entire result from the HTTP response. + * + * @param url The URL to fetch the HTTP response from. + * @return The contents of the HTTP response, null if no response. + * @throws IOException Related to network and stream reading. + */ + public static String getResponseFromHttpUrl(URL url) throws IOException { + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.connect(); + InputStream in = urlConnection.getInputStream(); + + Scanner scanner = new Scanner(in); + scanner.useDelimiter("\\A"); + + boolean hasInput = scanner.hasNext(); + String response = null; + if (hasInput) { + response = scanner.next(); + } + scanner.close(); + return response; + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } +} diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java new file mode 100644 index 00000000..1c2db557 --- /dev/null +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java @@ -0,0 +1,140 @@ +package com.davidmiguel.gobees.data.source.network; + +import com.davidmiguel.gobees.data.model.MeteoRecord; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.util.Date; + +/** + * Utility functions to handle OpenWeatherMap JSON data. + */ +public class OpenWeatherMapUtils { + + /* Result code */ + private static final String OWM_MESSAGE_CODE = "cod"; + + /* Location information */ + private static final String OWM_CITY = "name"; + + /* Weather condition */ + private static final String OWM_WEATHER = "weather"; + private static final String OWM_WEATHER_ID = "id"; + private static final String OWM_WEATHER_ICON = "icon"; + + /* Weather main information */ + private static final String OWM_MAIN = "main"; + private static final String OWM_MAIN_TEMPERATURE = "temp"; + private static final String OWM_MAIN_TEMPERATURE_MIN = "temp_min"; + private static final String OWM_MAIN_TEMPERATURE_MAX = "temp_max"; + private static final String OWM_MAIN_PRESSURE = "pressure"; + private static final String OWM_MAIN_HUMIDITY = "humidity"; + + /* Wind */ + private static final String OWM_WIND = "wind"; + private static final String OWM_WIND_SPEED = "speed"; + private static final String OWM_WIND_DIRECTION = "deg"; + + /* Clouds */ + private static final String OWM_CLOUDS = "clouds"; + private static final String OWM_CLOUDS_CLOUDINESS = "all"; + + /* Rain */ + private static final String OWM_RAIN = "rain"; + private static final String OWM_RAIN_3H = "3h"; + + /* Snow */ + private static final String OWM_SNOW = "snow"; + private static final String OWM_SNOW_3H = "3h"; + + public static MeteoRecord parseCurrentWeatherJson(String weatherJson) throws JSONException { + // Get JSON + JSONObject jsonObject = new JSONObject(weatherJson); + + // Check errors + if (jsonObject.has(OWM_MESSAGE_CODE)) { + int errorCode = jsonObject.getInt(OWM_MESSAGE_CODE); + switch (errorCode) { + case HttpURLConnection.HTTP_OK: + break; + case HttpURLConnection.HTTP_NOT_FOUND: + /* Location invalid */ + return null; + default: + /* Server probably down */ + return null; + } + } + + // Parse JSON + Date timestamp = new Date(); + String cityName = null; + int weatherCondition = 0; + String weatherConditionIcon = null; + double temperature = 0; + double temperatureMin = 0; + double temperatureMax = 0; + int pressure = 0; + int humidity = 0; + double windSpeed = 0; + double windDegrees = 0; + int clouds = 0; + double rain = 0; + double snow = 0; + + // Get city + if (jsonObject.has(OWM_CITY)) { + cityName = jsonObject.getString(OWM_CITY); + } + + // Get weather condition + if (jsonObject.has(OWM_WEATHER)) { + JSONArray jsonWeatherArray = jsonObject.getJSONArray(OWM_WEATHER); + JSONObject jsonWeatherObject = jsonWeatherArray.getJSONObject(0); + weatherCondition = jsonWeatherObject.getInt(OWM_WEATHER_ID); + weatherConditionIcon = jsonWeatherObject.getString(OWM_WEATHER_ICON); + } + + // Get main info + if (jsonObject.has(OWM_MAIN)) { + JSONObject jsonMainObject = jsonObject.getJSONObject(OWM_MAIN); + temperature = jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE); + temperatureMin = jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE_MIN); + temperatureMax = jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE_MAX); + pressure = jsonMainObject.getInt(OWM_MAIN_PRESSURE); + humidity = jsonMainObject.getInt(OWM_MAIN_HUMIDITY); + } + + // Get wind + if (jsonObject.has(OWM_WIND)) { + JSONObject jsonWindObject = jsonObject.getJSONObject(OWM_WIND); + windSpeed = jsonWindObject.getDouble(OWM_WIND_SPEED); + windDegrees = jsonWindObject.getDouble(OWM_WIND_DIRECTION); + } + + // Get clouds + if (jsonObject.has(OWM_CLOUDS)) { + JSONObject jsonCloudsObject = jsonObject.getJSONObject(OWM_CLOUDS); + clouds = jsonCloudsObject.getInt(OWM_CLOUDS_CLOUDINESS); + } + + // Get rain + if (jsonObject.has(OWM_RAIN)) { + JSONObject jsonRainObject = jsonObject.getJSONObject(OWM_RAIN); + rain = jsonRainObject.getDouble(OWM_RAIN_3H); + } + + // Get snow + if (jsonObject.has(OWM_SNOW)) { + JSONObject jsonSnowObject = jsonObject.getJSONObject(OWM_SNOW); + snow = jsonSnowObject.getDouble(OWM_SNOW_3H); + } + + return new MeteoRecord(timestamp, cityName, weatherCondition, weatherConditionIcon, + temperature, temperatureMin, temperatureMax, pressure, humidity, windSpeed, + windDegrees, clouds, rain, snow); + } +} diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java new file mode 100644 index 00000000..158ad4ec --- /dev/null +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java @@ -0,0 +1,32 @@ +package com.davidmiguel.gobees.data.source.network; + +import com.davidmiguel.gobees.data.model.MeteoRecord; + +import org.json.JSONException; + +import java.io.IOException; +import java.net.URL; + +/** + * Created by davidmigloz on 30/12/2016. + */ +public class WeatherDataSource { + + /** + * Get current weather data. + * + * @param latitude the latitude of the location. + * @param longitude the longitude of the location. + * @return current weather data. + */ + MeteoRecord getCurrentWeather(double latitude, double longitude) { + try { + URL weatherRequestUrl = NetworkUtils.getCurrentWeatherUrl(latitude, longitude); + String jsonWeatherResponse = NetworkUtils.getResponseFromHttpUrl(weatherRequestUrl); + return OpenWeatherMapUtils.parseCurrentWeatherJson(jsonWeatherResponse); + } catch (JSONException | IOException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java b/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java index e4199091..eadfdd21 100644 --- a/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java @@ -155,10 +155,10 @@ public void deleteApiary_deleteApiaryFromDbAndCache() { assertThat(goBeesRepository.cachedApiaries.containsKey(apiary.getId()), is(true)); // When deleted - goBeesRepository.deleteApiary(apiary.getId(), taskCallback); + goBeesRepository.deleteApiary(apiary, taskCallback); // Verify the data source were called - verify(goBeesLocalDataSource).deleteApiary(apiary.getId(), taskCallback); + verify(goBeesLocalDataSource).deleteApiary(apiary, taskCallback); // Verify it's removed from repository assertThat(goBeesRepository.cachedApiaries.containsKey(apiary.getId()), is(false)); diff --git a/app/src/test/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtilsTest.java b/app/src/test/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtilsTest.java new file mode 100644 index 00000000..c5d82c39 --- /dev/null +++ b/app/src/test/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtilsTest.java @@ -0,0 +1,72 @@ +package com.davidmiguel.gobees.data.source.network; + +import com.davidmiguel.gobees.data.model.MeteoRecord; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * OpenWeatherMapUtilsTest unit tests. + */ +public class OpenWeatherMapUtilsTest { + @Test + public void parseCurrentWeatherJson() throws Exception { + String jsonResponse = "{\n" + + " \"coord\":{\n" + + " \"lon\":139,\n" + + " \"lat\":35\n" + + " },\n" + + " \"sys\":{\n" + + " \"country\":\"JP\",\n" + + " \"sunrise\":1369769524,\n" + + " \"sunset\":1369821049\n" + + " },\n" + + " \"weather\":[\n" + + " {\n" + + " \"id\":804,\n" + + " \"main\":\"clouds\",\n" + + " \"description\":\"overcast clouds\",\n" + + " \"icon\":\"04n\"\n" + + " }\n" + + " ],\n" + + " \"main\":{\n" + + " \"temp\":289.5,\n" + + " \"humidity\":89,\n" + + " \"pressure\":1013,\n" + + " \"temp_min\":287.04,\n" + + " \"temp_max\":292.04\n" + + " },\n" + + " \"wind\":{\n" + + " \"speed\":7.31,\n" + + " \"deg\":187.002\n" + + " },\n" + + " \"rain\":{\n" + + " \"3h\":0\n" + + " },\n" + + " \"clouds\":{\n" + + " \"all\":92\n" + + " },\n" + + " \"dt\":1369824698,\n" + + " \"id\":1851632,\n" + + " \"name\":\"Shuzenji\",\n" + + " \"cod\":200\n" + + "}"; + MeteoRecord mr = OpenWeatherMapUtils.parseCurrentWeatherJson(jsonResponse); + assertNotNull(mr); + assertEquals("city", "Shuzenji", mr.getCityName()); + assertEquals("weatherCondition", 804, mr.getWeatherCondition()); + assertEquals("weatherConditionIcon", "04n", mr.getWeatherConditionIcon()); + assertEquals("temperature", 289.5, mr.getTemperature(), 0); + assertEquals("temperatureMin", 287.04, mr.getTemperatureMin(), 0); + assertEquals("temperatureMax", 292.04, mr.getTemperatureMax(), 0); + assertEquals("pressure", 1013, mr.getPressure()); + assertEquals("humidity", 89, mr.getHumidity()); + assertEquals("windSpeed", 7.31, mr.getWindSpeed(), 0); + assertEquals("windDegrees", 187.002, mr.getWindDegrees(), 0); + assertEquals("clouds", 92, mr.getClouds()); + assertEquals("rain", 0, mr.getRain(), 0); + assertEquals("snow", 0, mr.getSnow(), 0); + } + +} \ No newline at end of file From 09e68791d9d32f599d8665b57879a0afc759a233 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Fri, 30 Dec 2016 19:26:08 +0100 Subject: [PATCH 11/32] Add current weather in apiary #92 --- .../davidmiguel/gobees/data/model/Apiary.java | 21 +++++++- .../data/model/mothers/ApiaryMother.java | 3 +- .../data/source/network/NetworkUtils.java | 16 +++--- .../source/network/OpenWeatherMapUtils.java | 4 +- .../source/network/WeatherDataSource.java | 49 ++++++++++++++----- 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java index 581a79fa..0d31b72a 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java @@ -57,7 +57,13 @@ public class Apiary extends RealmObject { private RealmList hives; /** - * List of meteorological data from days. + * Current weather in the apiary. + */ + @Nullable + private MeteoRecord currentWeather; + + /** + * List of meteorological data from recordings. */ @Nullable private RealmList meteoRecords; @@ -68,7 +74,8 @@ public Apiary() { public Apiary(long id, String name, @Nullable String imageUrl, @Nullable Double locationLong, @Nullable Double locationLat, @Nullable String notes, - @Nullable RealmList hives, @Nullable RealmList meteoRecords) { + @Nullable RealmList hives, @Nullable MeteoRecord currentWeather, + @Nullable RealmList meteoRecords) { this.id = id; this.name = name; this.imageUrl = imageUrl; @@ -76,6 +83,7 @@ public Apiary(long id, String name, @Nullable String imageUrl, @Nullable Double this.locationLat = locationLat; this.notes = notes; this.hives = hives; + this.currentWeather = currentWeather; this.meteoRecords = meteoRecords; } @@ -140,6 +148,15 @@ public void setHives(@Nullable RealmList hives) { this.hives = hives; } + @Nullable + public MeteoRecord getCurrentWeather() { + return currentWeather; + } + + public void setCurrentWeather(@Nullable MeteoRecord currentWeather) { + this.currentWeather = currentWeather; + } + @Nullable public RealmList getMeteoRecords() { return meteoRecords; diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java b/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java index 24a81678..463f2354 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/mothers/ApiaryMother.java @@ -28,6 +28,7 @@ public class ApiaryMother { private Double locationLat; private String notes; private RealmList hives; + private MeteoRecord currentWeather; private RealmList meteoRecords; private ApiaryMother() { @@ -85,7 +86,7 @@ private ApiaryMother withNotes(String notes) { private Apiary build() { return new Apiary(id, name, imageUrl, locationLong, - locationLat, notes, hives, meteoRecords); + locationLat, notes, hives, currentWeather, meteoRecords); } private List generateHives(int num) { diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java index d8f61dc5..501116c9 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/NetworkUtils.java @@ -14,7 +14,7 @@ /** * These utilities will be used to communicate with the weather servers. */ -public class NetworkUtils { +class NetworkUtils { /* Current weather API (http://openweathermap.org/current) */ private static final String CURRENT_WEATHER_URL = @@ -36,7 +36,7 @@ public class NetworkUtils { * @param longitude the longitude of the location. * @return url to use to query the weather server. */ - public static URL getCurrentWeatherUrl(double latitude, double longitude) { + static URL getCurrentWeatherUrl(double latitude, double longitude) { Uri weatherQueryUri = Uri.parse(CURRENT_WEATHER_URL).buildUpon() .appendQueryParameter(LAT_PARAM, String.valueOf(latitude)) .appendQueryParameter(LON_PARAM, String.valueOf(longitude)) @@ -54,11 +54,15 @@ public static URL getCurrentWeatherUrl(double latitude, double longitude) { /** * This method returns the entire result from the HTTP response. * - * @param url The URL to fetch the HTTP response from. - * @return The contents of the HTTP response, null if no response. - * @throws IOException Related to network and stream reading. + * @param url URL to fetch the HTTP response from. + * @return contents of the HTTP response, null if no response. + * @throws IOException related to network and stream reading. */ - public static String getResponseFromHttpUrl(URL url) throws IOException { + static String getResponseFromHttpUrl(URL url) throws IOException { + if (url == null) { + return null; + } + // Make the call to the api HttpURLConnection urlConnection = null; try { urlConnection = (HttpURLConnection) url.openConnection(); diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java index 1c2db557..904dc616 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java @@ -12,7 +12,7 @@ /** * Utility functions to handle OpenWeatherMap JSON data. */ -public class OpenWeatherMapUtils { +class OpenWeatherMapUtils { /* Result code */ private static final String OWM_MESSAGE_CODE = "cod"; @@ -50,7 +50,7 @@ public class OpenWeatherMapUtils { private static final String OWM_SNOW = "snow"; private static final String OWM_SNOW_3H = "3h"; - public static MeteoRecord parseCurrentWeatherJson(String weatherJson) throws JSONException { + static MeteoRecord parseCurrentWeatherJson(String weatherJson) throws JSONException { // Get JSON JSONObject jsonObject = new JSONObject(weatherJson); diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java index 158ad4ec..bfd4b7ed 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java @@ -1,5 +1,7 @@ package com.davidmiguel.gobees.data.source.network; +import android.os.AsyncTask; + import com.davidmiguel.gobees.data.model.MeteoRecord; import org.json.JSONException; @@ -8,25 +10,50 @@ import java.net.URL; /** - * Created by davidmigloz on 30/12/2016. + * Provides access to the weather server. */ public class WeatherDataSource { + private GetWeatherCallback callback; + /** * Get current weather data. * - * @param latitude the latitude of the location. + * @param latitude the latitude of the location. * @param longitude the longitude of the location. - * @return current weather data. */ - MeteoRecord getCurrentWeather(double latitude, double longitude) { - try { - URL weatherRequestUrl = NetworkUtils.getCurrentWeatherUrl(latitude, longitude); - String jsonWeatherResponse = NetworkUtils.getResponseFromHttpUrl(weatherRequestUrl); - return OpenWeatherMapUtils.parseCurrentWeatherJson(jsonWeatherResponse); - } catch (JSONException | IOException e) { - e.printStackTrace(); - return null; + void getCurrentWeather(double latitude, double longitude, GetWeatherCallback getWeatherCallback) { + this.callback = getWeatherCallback; + new GetWeatherTask().execute(NetworkUtils.getCurrentWeatherUrl(latitude, longitude)); + } + + interface GetWeatherCallback { + void onWeatherLoaded(MeteoRecord meteoRecord); + + void onDataNotAvailable(); + } + + /** + * Background task to connect to the weather api, get the data and parse it. + */ + private class GetWeatherTask extends AsyncTask { + @Override + protected MeteoRecord doInBackground(URL... urls) { + URL weatherRequestUrl = urls[0]; + try { + String json = NetworkUtils.getResponseFromHttpUrl(weatherRequestUrl); + return OpenWeatherMapUtils.parseCurrentWeatherJson(json); + } catch (IOException | JSONException e) { + return null; + } + } + + @Override + protected void onPostExecute(MeteoRecord meteoRecord) { + if (meteoRecord == null) { + callback.onDataNotAvailable(); + } + callback.onWeatherLoaded(meteoRecord); } } } From 6914f56f21961506884eb64dfef8ec02d06e8535 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 01:27:44 +0100 Subject: [PATCH 12/32] Get current weather in apiaries list #92 --- app/src/main/AndroidManifest.xml | 1 + .../gobees/apiaries/ApiariesAdapter.java | 15 ++++ .../gobees/apiaries/ApiariesContract.java | 15 ++++ .../gobees/apiaries/ApiariesFragment.java | 17 +++- .../gobees/apiaries/ApiariesPresenter.java | 51 ++++++++++++ .../davidmiguel/gobees/data/model/Apiary.java | 34 ++++---- .../gobees/data/source/GoBeesDataSource.java | 8 ++ .../data/source/cache/GoBeesRepository.java | 60 +++++++++++++- .../source/local/GoBeesLocalDataSource.java | 47 ++++++++++- .../source/network/WeatherDataSource.java | 80 +++++++++++++++---- .../main/res/layout/apiaries_list_item.xml | 8 +- app/src/main/res/values/strings.xml | 6 ++ .../com/davidmiguel/gobees/Injection.java | 4 +- 13 files changed, 305 insertions(+), 41 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 57d2a169..13a6c10e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + { */ void showApiaries(@NonNull List apiaries); + /** + * Notify that the apiaries data has changed to update the list. + */ + void notifyApiariesUpdated(); + /** * Opens activity to add or edit an apiary. * @@ -67,6 +72,16 @@ interface View extends BaseView { * Shows error while deleting apiary message. */ void showDeletedErrorMessage(); + + /** + * Shows successfully current weather updated message. + */ + void showSuccessfullyWeatherUpdatedMessage(); + + /** + * Shows error while updating current weather message. + */ + void showWeatherUpdateErrorMessage(); } interface Presenter extends BasePresenter { diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index 4a6f7cbb..da23466b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -165,10 +165,15 @@ public void showApiaries(@NonNull List apiaries) { noApiariesView.setVisibility(View.GONE); } + @Override + public void notifyApiariesUpdated() { + listAdapter.notifyDataSetChanged(); + } + @Override public void showAddEditApiary(long apiaryId) { Intent intent = new Intent(getContext(), AddEditApiaryActivity.class); - if(apiaryId != AddEditApiaryActivity.NEW_APIARY) { + if (apiaryId != AddEditApiaryActivity.NEW_APIARY) { intent.putExtra(AddEditApiaryFragment.ARGUMENT_EDIT_APIARY_ID, apiaryId); } startActivityForResult(intent, AddEditApiaryActivity.REQUEST_ADD_APIARY); @@ -206,6 +211,16 @@ public void showDeletedErrorMessage() { showMessage(getString(R.string.deleted_apiary_error_message)); } + @Override + public void showSuccessfullyWeatherUpdatedMessage() { + showMessage(getString(R.string.successfully_updated_weather_message)); + } + + @Override + public void showWeatherUpdateErrorMessage() { + showMessage(getString(R.string.weather_update_error_message)); + } + @Override public boolean isActive() { return isAdded(); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index 19a24a88..51f6019c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -9,14 +9,20 @@ import com.davidmiguel.gobees.data.source.cache.GoBeesRepository; import com.davidmiguel.gobees.data.source.local.DataGenerator; +import java.util.ArrayList; +import java.util.Date; import java.util.List; +import io.realm.Realm; + /** * Listens to user actions from the UI ApiariesFragment, retrieves the data and updates the * UI as required. */ class ApiariesPresenter implements ApiariesContract.Presenter { + private static final int MIN_UPDATE_WEATHER = 15; + private GoBeesRepository goBeesRepository; private ApiariesContract.View view; @@ -68,6 +74,8 @@ public void onApiariesLoaded(List apiaries) { } else { // Show the list of apiaries view.showApiaries(apiaries); + // Check whether current weather is up to date + checkCurrentWeather(apiaries); } } @@ -142,4 +150,47 @@ public void deleteData() { public void start() { loadApiaries(false); } + + /** + * Checks whether current weather is up to date (less than 15min old). + * If not, orders to update the current weather in all apiaries. + * + * @param apiaries list of apiaries. + */ + private void checkCurrentWeather(List apiaries) { + List apiariesToUpdate = new ArrayList<>(); + Date now = new Date(); + // Check dates + for (Apiary apiary : apiaries) { + if(apiary.hasLocation()) { + if (apiary.getCurrentWeather() != null) { + // Check time + Date weatherDate = apiary.getCurrentWeather().getTimestamp(); + long minFromLastUpdate = (now.getTime() - weatherDate.getTime()) / 60000; + if(minFromLastUpdate >= MIN_UPDATE_WEATHER) { + apiariesToUpdate.add(apiary); + } + } else { + apiariesToUpdate.add(apiary); + } + + } + } + // Update weather if needed + if(apiariesToUpdate.size() > 0) { + goBeesRepository.updateApiariesCurrentWeather(apiariesToUpdate, new GoBeesDataSource.TaskCallback() { + @Override + public void onSuccess() { + view.notifyApiariesUpdated(); + view.showSuccessfullyWeatherUpdatedMessage(); + } + + @Override + public void onFailure() { + view.showWeatherUpdateErrorMessage(); + } + }); + } + + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java index 0d31b72a..7f3abd09 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java @@ -33,16 +33,16 @@ public class Apiary extends RealmObject { private String imageUrl; /** - * Apiary location longitude. + * Apiary location latitude. */ @Nullable - private Double locationLong; + private Double locationLat; /** - * Apiary location latitude. + * Apiary location longitude. */ @Nullable - private Double locationLat; + private Double locationLong; /** * Apiary notes. @@ -72,15 +72,15 @@ public Apiary() { // Needed by Realm } - public Apiary(long id, String name, @Nullable String imageUrl, @Nullable Double locationLong, - @Nullable Double locationLat, @Nullable String notes, + public Apiary(long id, String name, @Nullable String imageUrl, @Nullable Double locationLat, + @Nullable Double locationLong, @Nullable String notes, @Nullable RealmList hives, @Nullable MeteoRecord currentWeather, @Nullable RealmList meteoRecords) { this.id = id; this.name = name; this.imageUrl = imageUrl; - this.locationLong = locationLong; this.locationLat = locationLat; + this.locationLong = locationLong; this.notes = notes; this.hives = hives; this.currentWeather = currentWeather; @@ -113,21 +113,21 @@ public void setImageUrl(@Nullable String imageUrl) { } @Nullable - public Double getLocationLong() { - return locationLong; + public Double getLocationLat() { + return locationLat; } - public void setLocationLong(@Nullable Double locationLong) { - this.locationLong = locationLong; + public void setLocationLat(@Nullable Double locationLat) { + this.locationLat = locationLat; } @Nullable - public Double getLocationLat() { - return locationLat; + public Double getLocationLong() { + return locationLong; } - public void setLocationLat(@Nullable Double locationLat) { - this.locationLat = locationLat; + public void setLocationLong(@Nullable Double locationLong) { + this.locationLong = locationLong; } @Nullable @@ -175,4 +175,8 @@ public void addHive(@NonNull Hive hive){ public boolean isValidApiary() { return !Strings.isNullOrEmpty(name); } + + public boolean hasLocation() { + return locationLat != null && locationLong != null; + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 8614dfe7..c0303612 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -179,6 +179,14 @@ public interface GoBeesDataSource { */ void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback); + /** + * Updates the current weather of the apiaries in the list. + * + * @param apiariesToUpdate apiaries to update weather. + * @param callback TaskCallback. + */ + void updateApiariesCurrentWeather(List apiariesToUpdate, @NonNull TaskCallback callback); + /** * Force to update recordings cache. */ diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index 09c312e0..99f84531 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -4,15 +4,20 @@ import com.davidmiguel.gobees.data.model.Apiary; import com.davidmiguel.gobees.data.model.Hive; +import com.davidmiguel.gobees.data.model.MeteoRecord; import com.davidmiguel.gobees.data.model.Record; import com.davidmiguel.gobees.data.model.Recording; import com.davidmiguel.gobees.data.source.GoBeesDataSource; +import com.davidmiguel.gobees.data.source.network.WeatherDataSource; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static com.google.common.base.Preconditions.checkNotNull; @@ -31,6 +36,11 @@ public class GoBeesRepository implements GoBeesDataSource { */ private final GoBeesDataSource goBeesDataSource; + /** + * Weather server. + */ + private final WeatherDataSource weatherDataSource; + /** * This variable has package local visibility so it can be accessed from tests. */ @@ -42,13 +52,15 @@ public class GoBeesRepository implements GoBeesDataSource { */ boolean cacheIsDirty = false; - private GoBeesRepository(GoBeesDataSource goBeesDataSource) { + private GoBeesRepository(GoBeesDataSource goBeesDataSource, WeatherDataSource weatherDataSource) { this.goBeesDataSource = goBeesDataSource; + this.weatherDataSource = weatherDataSource; } - public static GoBeesRepository getInstance(GoBeesDataSource apiariesLocalDataSource) { + public static GoBeesRepository getInstance(GoBeesDataSource apiariesLocalDataSource, + WeatherDataSource weatherDataSource) { if (INSTANCE == null) { - INSTANCE = new GoBeesRepository(apiariesLocalDataSource); + INSTANCE = new GoBeesRepository(apiariesLocalDataSource, weatherDataSource); } return INSTANCE; } @@ -260,6 +272,48 @@ public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull goBeesDataSource.deleteRecording(hiveId, recording, callback); } + @SuppressWarnings("ConstantConditions") + @Override + public void updateApiariesCurrentWeather(final List apiariesToUpdate, @NonNull final TaskCallback callback) { + checkNotNull(callback); + // Prepare callback + final List apiaries = Collections.synchronizedList(apiariesToUpdate); + final AtomicBoolean error = new AtomicBoolean(false); + final AtomicInteger counter = new AtomicInteger(0); + WeatherDataSource.GetWeatherCallback getWeatherCallback = new WeatherDataSource.GetWeatherCallback() { + @Override + public void onWeatherLoaded(int id, MeteoRecord meteoRecord) { + // Set weather + apiaries.get(id).setCurrentWeather(meteoRecord); + // Check if all apiaries have finished + int value = counter.incrementAndGet(); + if (value >= apiaries.size()) { + if (!error.get()) { + // Update weather in database and finish + goBeesDataSource.updateApiariesCurrentWeather(apiariesToUpdate, callback); + } else { + callback.onFailure(); + } + } + } + + @Override + public void onDataNotAvailable() { + error.set(true); + // Check if all apiaries have finished + int value = counter.incrementAndGet(); + if (value >= apiaries.size()) { + callback.onFailure(); + } + } + }; + // Update weather + for (int i = 0; i < apiariesToUpdate.size(); i++) { + weatherDataSource.getCurrentWeather(i, apiariesToUpdate.get(i).getLocationLat(), + apiaries.get(i).getLocationLong(), getWeatherCallback); + } + } + @Override public void refreshRecordings(long hiveId) { // TODO diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 4d1035f5..28f89835 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -4,6 +4,7 @@ import com.davidmiguel.gobees.data.model.Apiary; import com.davidmiguel.gobees.data.model.Hive; +import com.davidmiguel.gobees.data.model.MeteoRecord; import com.davidmiguel.gobees.data.model.Record; import com.davidmiguel.gobees.data.model.Recording; import com.davidmiguel.gobees.data.source.GoBeesDataSource; @@ -59,7 +60,7 @@ public void execute(Realm realm) { public void getApiaries(@NonNull GetApiariesCallback callback) { try { RealmResults apiaries = realm.where(Apiary.class).findAll(); - callback.onApiariesLoaded(new ArrayList<>(apiaries)); + callback.onApiariesLoaded(realm.copyFromRealm(apiaries)); } catch (Exception e) { callback.onDataNotAvailable(); } @@ -69,7 +70,7 @@ public void getApiaries(@NonNull GetApiariesCallback callback) { public void getApiary(long apiaryId, @NonNull GetApiaryCallback callback) { try { Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); - callback.onApiaryLoaded(apiary); + callback.onApiaryLoaded(realm.copyFromRealm(apiary)); } catch (Exception e) { callback.onDataNotAvailable(); } @@ -151,7 +152,7 @@ public void getNextApiaryId(@NonNull GetNextApiaryIdCallback callback) { public void getHives(long apiaryId, @NonNull GetHivesCallback callback) { try { Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); - callback.onHivesLoaded(new ArrayList<>(apiary.getHives())); + callback.onHivesLoaded(realm.copyFromRealm(apiary.getHives())); } catch (Exception e) { callback.onDataNotAvailable(); } @@ -161,7 +162,7 @@ public void getHives(long apiaryId, @NonNull GetHivesCallback callback) { public void getHive(long hiveId, @NonNull GetHiveCallback callback) { try { Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); - callback.onHiveLoaded(hive); + callback.onHiveLoaded(realm.copyFromRealm(hive)); } catch (Exception e) { callback.onDataNotAvailable(); } @@ -360,6 +361,44 @@ public void execute(Realm realm) { } } + @SuppressWarnings("ConstantConditions") + @Override + public void updateApiariesCurrentWeather(final List apiariesToUpdate, @NonNull TaskCallback callback) { + try { + // Save meteo records + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Get next id + Number n = realm.where(MeteoRecord.class).max("id"); + long nextId = (n != null ? n.longValue() + 1 : 0); + // Save records + for (Apiary apiary : apiariesToUpdate) { + MeteoRecord meteoRecord = apiary.getCurrentWeather(); + meteoRecord.setId(nextId++); + realm.copyToRealmOrUpdate(meteoRecord); + } + } + }); + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Save apiaries with current weather updated + for (Apiary apiary : apiariesToUpdate) { + Apiary requestedApiary = realm.where(Apiary.class) + .equalTo("id", apiary.getId()).findFirst(); + MeteoRecord meteoRecord = realm.where(MeteoRecord.class) + .equalTo("id", apiary.getCurrentWeather().getId()).findFirst(); + requestedApiary.setCurrentWeather(meteoRecord); + } + } + }); + callback.onSuccess(); + } catch (Exception e) { + callback.onFailure(); + } + } + @Override public void refreshRecordings(long hiveId) { // Not required because the GoBeesRepository handles the logic of refreshing the diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java index bfd4b7ed..c3726a2b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java @@ -14,21 +14,34 @@ */ public class WeatherDataSource { - private GetWeatherCallback callback; + private static WeatherDataSource INSTANCE; + + private WeatherDataSource() { + } + + public static WeatherDataSource getInstance() { + if (INSTANCE == null) { + INSTANCE = new WeatherDataSource(); + } + return INSTANCE; + } /** * Get current weather data. * + * @param id identifier of the operation. * @param latitude the latitude of the location. * @param longitude the longitude of the location. */ - void getCurrentWeather(double latitude, double longitude, GetWeatherCallback getWeatherCallback) { - this.callback = getWeatherCallback; - new GetWeatherTask().execute(NetworkUtils.getCurrentWeatherUrl(latitude, longitude)); + public void getCurrentWeather(int id, double latitude, double longitude, + GetWeatherCallback getWeatherCallback) { + URL weatherRequestUrl = NetworkUtils.getCurrentWeatherUrl(latitude, longitude); + DataHolder data = new DataHolder(id, weatherRequestUrl, getWeatherCallback); + new GetWeatherTask().execute(data); } - interface GetWeatherCallback { - void onWeatherLoaded(MeteoRecord meteoRecord); + public interface GetWeatherCallback { + void onWeatherLoaded(int id, MeteoRecord meteoRecord); void onDataNotAvailable(); } @@ -36,24 +49,63 @@ interface GetWeatherCallback { /** * Background task to connect to the weather api, get the data and parse it. */ - private class GetWeatherTask extends AsyncTask { + private class GetWeatherTask extends AsyncTask { @Override - protected MeteoRecord doInBackground(URL... urls) { - URL weatherRequestUrl = urls[0]; + protected DataHolder doInBackground(DataHolder... dataArray) { + DataHolder data = dataArray[0]; + URL weatherRequestUrl = data.getUrl(); try { String json = NetworkUtils.getResponseFromHttpUrl(weatherRequestUrl); - return OpenWeatherMapUtils.parseCurrentWeatherJson(json); + MeteoRecord meteoRecord = OpenWeatherMapUtils.parseCurrentWeatherJson(json); + data.setMeteoRecord(meteoRecord); + return data; } catch (IOException | JSONException e) { return null; } } @Override - protected void onPostExecute(MeteoRecord meteoRecord) { - if (meteoRecord == null) { - callback.onDataNotAvailable(); + protected void onPostExecute(DataHolder data) { + if (data.getMeteoRecord() == null) { + data.getGetWeatherCallback().onDataNotAvailable(); } - callback.onWeatherLoaded(meteoRecord); + data.getGetWeatherCallback().onWeatherLoaded(data.getId(), data.getMeteoRecord()); + } + } + + /** + * Class to pass data to the async task. + */ + private class DataHolder { + GetWeatherCallback getWeatherCallback; + private int id; + private URL url; + private MeteoRecord meteoRecord; + + DataHolder(int id, URL url, GetWeatherCallback getWeatherCallback) { + this.id = id; + this.url = url; + this.getWeatherCallback = getWeatherCallback; + } + + int getId() { + return id; + } + + URL getUrl() { + return url; + } + + GetWeatherCallback getGetWeatherCallback() { + return getWeatherCallback; + } + + MeteoRecord getMeteoRecord() { + return meteoRecord; + } + + void setMeteoRecord(MeteoRecord meteoRecord) { + this.meteoRecord = meteoRecord; } } } diff --git a/app/src/main/res/layout/apiaries_list_item.xml b/app/src/main/res/layout/apiaries_list_item.xml index bf2c3433..c5ea6453 100644 --- a/app/src/main/res/layout/apiaries_list_item.xml +++ b/app/src/main/res/layout/apiaries_list_item.xml @@ -78,6 +78,7 @@ android:layout_marginStart="4dp" android:layout_toEndOf="@id/hive_icon" android:gravity="bottom" + android:text="@string/default_num_hives" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textColor="@color/colorSecondaryText"/> @@ -92,7 +93,8 @@ android:adjustViewBounds="false" android:contentDescription="@string/weather_icon" android:src="@drawable/ic_filter_drama" - android:tint="@color/colorSecondaryText"/> + android:tint="@color/colorSecondaryText" + android:visibility="gone"/> + android:textColor="@color/colorSecondaryText" + android:visibility="gone"/> Number of hives + + 0 Error while loading apiaries. @@ -61,6 +63,10 @@ Apiary deleted! Error while deleting apiary. + + Current weather updated! + + Error while updating current weather. diff --git a/app/src/mock/java/com/davidmiguel/gobees/Injection.java b/app/src/mock/java/com/davidmiguel/gobees/Injection.java index 24ea7df3..70edd504 100644 --- a/app/src/mock/java/com/davidmiguel/gobees/Injection.java +++ b/app/src/mock/java/com/davidmiguel/gobees/Injection.java @@ -2,6 +2,7 @@ import com.davidmiguel.gobees.data.source.cache.GoBeesRepository; import com.davidmiguel.gobees.data.source.local.GoBeesLocalDataSource; +import com.davidmiguel.gobees.data.source.network.WeatherDataSource; /** * Enables injection of mock implementations for GoBeesDataSource at compile time. @@ -11,6 +12,7 @@ public class Injection { public static GoBeesRepository provideApiariesRepository() { - return GoBeesRepository.getInstance(GoBeesLocalDataSource.getInstance()); + return GoBeesRepository.getInstance(GoBeesLocalDataSource.getInstance(), + WeatherDataSource.getInstance()); } } \ No newline at end of file From 381f223bb15b5ce6aa5514fae287b176cb6edad9 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 13:34:43 +0100 Subject: [PATCH 13/32] Add weather condition icons #92 --- .../gobees/apiaries/ApiariesAdapter.java | 18 ++-- .../gobees/apiaries/ApiariesFragment.java | 9 +- .../source/preferences/GoBeesPreferences.java | 30 +++++++ .../gobees/utils/WeatherUtils.java | 88 +++++++++++++++++++ .../ic_weather_day_clear_sky.xml | 46 ++++++++++ .../ic_weather_day_few_clouds.xml | 41 +++++++++ .../ic_weather_day_night_broken_clouds.xml | 22 +++++ .../ic_weather_day_night_mist.xml | 56 ++++++++++++ .../ic_weather_day_night_scattered_clouds.xml | 17 ++++ .../ic_weather_day_night_shower_rain.xml | 37 ++++++++ .../ic_weather_day_night_snow.xml | 57 ++++++++++++ .../ic_weather_day_night_thunderstorm.xml | 23 +++++ .../drawable-anydpi/ic_weather_day_rain.xml | 69 +++++++++++++++ .../ic_weather_night_clear_sky.xml | 15 ++++ .../ic_weather_night_few_clouds.xml | 21 +++++ .../drawable-anydpi/ic_weather_night_rain.xml | 42 +++++++++ .../main/res/layout/apiaries_list_item.xml | 5 +- app/src/main/res/values/strings.xml | 13 ++- .../source/cache/GoBeesRepositoryTest.java | 6 +- 19 files changed, 600 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/davidmiguel/gobees/data/source/preferences/GoBeesPreferences.java create mode 100644 app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_night_scattered_clouds.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_night_shower_rain.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_night_snow.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_night_thunderstorm.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_day_rain.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_night_clear_sky.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_night_few_clouds.xml create mode 100644 app/src/main/res/drawable-anydpi/ic_weather_night_rain.xml diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java index 85b06166..adcd3ffe 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesAdapter.java @@ -1,5 +1,6 @@ package com.davidmiguel.gobees.apiaries; +import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; @@ -18,6 +19,7 @@ import com.davidmiguel.gobees.data.model.Apiary; import com.davidmiguel.gobees.utils.BaseViewHolder; import com.davidmiguel.gobees.utils.ItemTouchHelperViewHolder; +import com.davidmiguel.gobees.utils.WeatherUtils; import java.util.List; @@ -28,11 +30,13 @@ */ class ApiariesAdapter extends RecyclerView.Adapter { + private final Context context; private MenuInflater menuInflater; private List apiaries; private ApiaryItemListener listener; - ApiariesAdapter(MenuInflater menuInflater, List apiaries, ApiaryItemListener listener) { + ApiariesAdapter(Context context, MenuInflater menuInflater, List apiaries, ApiaryItemListener listener) { + this.context = context; this.menuInflater = menuInflater; this.apiaries = checkNotNull(apiaries); this.listener = listener; @@ -93,7 +97,7 @@ class ViewHolder extends RecyclerView.ViewHolder card = (CardView) itemView.findViewById(R.id.card); apiaryName = (TextView) itemView.findViewById(R.id.apiary_name); numHives = (TextView) itemView.findViewById(R.id.num_hives); - moreIcon = (ImageView) itemView.findViewById(R.id.weather_icon); + weatherIcon = (ImageView) itemView.findViewById(R.id.weather_icon); temp = (TextView) itemView.findViewById(R.id.temp); moreIcon = (ImageView) itemView.findViewById(R.id.more_icon); @@ -115,14 +119,16 @@ public void bind(@NonNull Apiary apiary) { // Set apiary name apiaryName.setText(apiary.getName()); // Set number of hives - if(apiary.getHives() != null) { + if (apiary.getHives() != null) { numHives.setText(Integer.toString(apiary.getHives().size())); } - if(apiary.getCurrentWeather() != null) { + if (apiary.getCurrentWeather() != null) { // Set weather icon - // TODO + String iconId = apiary.getCurrentWeather().getWeatherConditionIcon(); + weatherIcon.setImageResource(WeatherUtils.getWeatherIconResourceId(iconId)); // Set temperature - temp.setText(Double.toString(apiary.getCurrentWeather().getTemperature()) + "ºC"); + double temperature = apiary.getCurrentWeather().getTemperature(); + temp.setText(WeatherUtils.formatTemperature(context, temperature)); // Show weatherIcon.setVisibility(View.VISIBLE); temp.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index da23466b..9c67e481 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -18,6 +18,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import android.widget.Toast; import com.davidmiguel.gobees.R; import com.davidmiguel.gobees.addeditapiary.AddEditApiaryActivity; @@ -55,7 +56,7 @@ public static ApiariesFragment newInstance() { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - listAdapter = new ApiariesAdapter(getActivity().getMenuInflater(), new ArrayList(0), this); + listAdapter = new ApiariesAdapter(getContext(), getActivity().getMenuInflater(), new ArrayList(0), this); } @Nullable @@ -213,12 +214,14 @@ public void showDeletedErrorMessage() { @Override public void showSuccessfullyWeatherUpdatedMessage() { - showMessage(getString(R.string.successfully_updated_weather_message)); + Toast.makeText(getActivity(), getString(R.string.successfully_updated_weather_message), + Toast.LENGTH_SHORT).show(); } @Override public void showWeatherUpdateErrorMessage() { - showMessage(getString(R.string.weather_update_error_message)); + Toast.makeText(getActivity(), getString(R.string.weather_update_error_message), + Toast.LENGTH_SHORT).show(); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/preferences/GoBeesPreferences.java b/app/src/main/java/com/davidmiguel/gobees/data/source/preferences/GoBeesPreferences.java new file mode 100644 index 00000000..77856896 --- /dev/null +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/preferences/GoBeesPreferences.java @@ -0,0 +1,30 @@ +package com.davidmiguel.gobees.data.source.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import com.davidmiguel.gobees.R; + +/** + * Access to data stored in shared preferences. + */ +public class GoBeesPreferences { + + /** + * Returns true if the user has selected metric temperature display. + * + * @param context Context used to get the SharedPreferences. + * @return true if metric display should be used, false if imperial display should be used. + */ + public static boolean isMetric(Context context) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + + String keyForUnits = context.getString(R.string.pref_temp_units_key); + String defaultUnits = context.getString(R.string.pref_temp_units_metric); + String preferredUnits = sp.getString(keyForUnits, defaultUnits); + String metric = context.getString(R.string.pref_temp_units_metric); + + return metric.equals(preferredUnits); + } +} diff --git a/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java b/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java new file mode 100644 index 00000000..9e80dc5f --- /dev/null +++ b/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java @@ -0,0 +1,88 @@ +package com.davidmiguel.gobees.utils; + +import android.content.Context; + +import com.davidmiguel.gobees.R; +import com.davidmiguel.gobees.data.source.preferences.GoBeesPreferences; + +/** + * Contains useful utilities for a weather app. Such as conversion between Celsius and Fahrenheit, + * from kph to mph, and from degrees to NSEW. It also contains the mapping of weather condition + * codes in OpenWeatherMap to strings. + */ +public class WeatherUtils { + + /** + * This method will convert a temperature from Celsius to Fahrenheit. + * + * @param temperatureInCelsius Temperature in degrees Celsius(°C). + * @return Temperature in degrees Fahrenheit (°F). + */ + private static double celsiusToFahrenheit(double temperatureInCelsius) { + return (temperatureInCelsius * 1.8) + 32; + } + + /** + * Temperature data is stored in Celsius. Depending on the user's preference, + * the app may need to display the temperature in Fahrenheit. This method will perform that + * temperature conversion if necessary. It will also format the temperature so that no + * decimal points show. Temperatures will be formatted to the following form: "21°C". + * + * @param context Android Context to access preferences and resources. + * @param temperature Temperature in degrees Celsius (°C). + * @return Formatted temperature String in the following form: "21°C" + */ + public static String formatTemperature(Context context, double temperature) { + int temperatureFormatResourceId = R.string.format_temperature_celsius; + if (!GoBeesPreferences.isMetric(context)) { + temperature = celsiusToFahrenheit(temperature); + temperatureFormatResourceId = R.string.format_temperature_fahrenheit; + } + return String.format(context.getString(temperatureFormatResourceId), temperature); + } + + /** + * Helper method to provide the icon resource id according to the weather condition id returned + * by the OpenWeatherMap call. + * + * @param weatherIconId from OpenWeatherMap API response + * See http://openweathermap.org/weather-conditions for a list of all IDs. + * @return resource id for the corresponding icon. day_clear_sky if no relation is found. + */ + public static int getWeatherIconResourceId(String weatherIconId) { + switch (weatherIconId) { + case "01d": // clear sky day + return R.drawable.ic_weather_day_clear_sky; + case "01n": // clear sky night + return R.drawable.ic_weather_night_clear_sky; + case "02d": // few clouds day + return R.drawable.ic_weather_day_few_clouds; + case "02n": // clear sky night + return R.drawable.ic_weather_night_few_clouds; + case "03d": // scattered clouds day + case "03n": // scattered clouds night + return R.drawable.ic_weather_day_night_scattered_clouds; + case "04d": // broken clouds day + case "04n": // broken clouds night + return R.drawable.ic_weather_day_night_broken_clouds; + case "09d": // shower rain day + case "09n": // shower rain night + return R.drawable.ic_weather_day_night_shower_rain; + case "10d": // rain day + return R.drawable.ic_weather_day_rain; + case "10n": // rain night + return R.drawable.ic_weather_night_rain; + case "11d": // thunderstorm day + case "11n": // thunderstorm night + return R.drawable.ic_weather_day_night_thunderstorm; + case "13d": // snow day + case "13n": // snow night + return R.drawable.ic_weather_day_night_snow; + case "50d": // mist day + case "50n": // mist night + return R.drawable.ic_weather_day_night_mist; + default: // Not found + return R.drawable.ic_weather_day_clear_sky; + } + } +} diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml new file mode 100644 index 00000000..3cb6ddd1 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml new file mode 100644 index 00000000..9986e107 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml new file mode 100644 index 00000000..66f8e7fc --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml new file mode 100644 index 00000000..21cf2365 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_scattered_clouds.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_scattered_clouds.xml new file mode 100644 index 00000000..e6b0828d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_scattered_clouds.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_shower_rain.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_shower_rain.xml new file mode 100644 index 00000000..f75ddeab --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_shower_rain.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_snow.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_snow.xml new file mode 100644 index 00000000..bb754b2a --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_snow.xml @@ -0,0 +1,57 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_thunderstorm.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_thunderstorm.xml new file mode 100644 index 00000000..767f9051 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_thunderstorm.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_rain.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_rain.xml new file mode 100644 index 00000000..30a59dd0 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_rain.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_night_clear_sky.xml b/app/src/main/res/drawable-anydpi/ic_weather_night_clear_sky.xml new file mode 100644 index 00000000..0be4ef08 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_night_clear_sky.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_night_few_clouds.xml b/app/src/main/res/drawable-anydpi/ic_weather_night_few_clouds.xml new file mode 100644 index 00000000..57d16df3 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_night_few_clouds.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_night_rain.xml b/app/src/main/res/drawable-anydpi/ic_weather_night_rain.xml new file mode 100644 index 00000000..b5d7013c --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_weather_night_rain.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/apiaries_list_item.xml b/app/src/main/res/layout/apiaries_list_item.xml index c5ea6453..b4f262ed 100644 --- a/app/src/main/res/layout/apiaries_list_item.xml +++ b/app/src/main/res/layout/apiaries_list_item.xml @@ -90,9 +90,9 @@ android:layout_marginBottom="16dp" android:layout_marginStart="@dimen/activity_horizontal_margin" android:layout_toEndOf="@id/num_hives" - android:adjustViewBounds="false" + android:adjustViewBounds="true" android:contentDescription="@string/weather_icon" - android:src="@drawable/ic_filter_drama" + android:src="@drawable/ic_weather_day_clear_sky" android:tint="@color/colorSecondaryText" android:visibility="gone"/> @@ -103,6 +103,7 @@ android:layout_alignParentBottom="true" android:layout_marginBottom="16dp" android:layout_marginStart="4dp" + android:text="@string/default_temperature" android:layout_toEndOf="@id/weather_icon" android:gravity="bottom" android:textAppearance="@style/TextAppearance.AppCompat.Small" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de8ebb0c..0445f5d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ - + + @@ -20,6 +21,10 @@ Edit Delete + + %1.0f\u00B0C + + %1.0f\u00B0F @@ -53,6 +58,8 @@ Number of hives 0 + + 10\u00B0C Error while loading apiaries. @@ -270,9 +277,9 @@ Temperature Units - Metric + Metric (\u00B0C) - Imperial + Imperial (\u00B0F) units diff --git a/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java b/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java index eadfdd21..296c3f1a 100644 --- a/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java @@ -8,6 +8,7 @@ import com.davidmiguel.gobees.data.source.GoBeesDataSource.GetHiveCallback; import com.davidmiguel.gobees.data.source.GoBeesDataSource.TaskCallback; import com.davidmiguel.gobees.data.source.local.GoBeesLocalDataSource; +import com.davidmiguel.gobees.data.source.network.WeatherDataSource; import com.google.common.collect.Lists; import org.junit.After; @@ -43,6 +44,9 @@ public class GoBeesRepositoryTest { @Mock private GoBeesLocalDataSource goBeesLocalDataSource; + @Mock + private WeatherDataSource weatherDataSource; + @Mock private GetApiariesCallback getApiariesCallback; @@ -67,7 +71,7 @@ public void setupTasksRepository() { MockitoAnnotations.initMocks(this); // Get a reference to the class under test - goBeesRepository = GoBeesRepository.getInstance(goBeesLocalDataSource); + goBeesRepository = GoBeesRepository.getInstance(goBeesLocalDataSource, weatherDataSource); // We start the apiaries to 3 APIARIES = Lists.newArrayList( From f891c2788b88df2a403fcda73315c1741f4f1844 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 14:03:38 +0100 Subject: [PATCH 14/32] Minor changes #92 --- app/src/main/res/layout/apiaries_list_item.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/apiaries_list_item.xml b/app/src/main/res/layout/apiaries_list_item.xml index b4f262ed..a2f1cfd4 100644 --- a/app/src/main/res/layout/apiaries_list_item.xml +++ b/app/src/main/res/layout/apiaries_list_item.xml @@ -94,7 +94,7 @@ android:contentDescription="@string/weather_icon" android:src="@drawable/ic_weather_day_clear_sky" android:tint="@color/colorSecondaryText" - android:visibility="gone"/> + android:visibility="visible"/> + android:visibility="visible"/> Date: Sat, 31 Dec 2016 14:04:02 +0100 Subject: [PATCH 15/32] Minor changes #92 --- app/src/main/res/layout/apiaries_list_item.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/apiaries_list_item.xml b/app/src/main/res/layout/apiaries_list_item.xml index a2f1cfd4..b4f262ed 100644 --- a/app/src/main/res/layout/apiaries_list_item.xml +++ b/app/src/main/res/layout/apiaries_list_item.xml @@ -94,7 +94,7 @@ android:contentDescription="@string/weather_icon" android:src="@drawable/ic_weather_day_clear_sky" android:tint="@color/colorSecondaryText" - android:visibility="visible"/> + android:visibility="gone"/> + android:visibility="gone"/> Date: Sat, 31 Dec 2016 15:05:09 +0100 Subject: [PATCH 16/32] Fix delete apiaries and hives #136 --- .../davidmiguel/gobees/apiaries/ApiariesPresenter.java | 8 ++++---- .../com/davidmiguel/gobees/apiary/ApiaryPresenter.java | 2 +- .../gobees/data/source/GoBeesDataSource.java | 10 +++++----- .../gobees/data/source/cache/GoBeesRepository.java | 8 ++++---- .../data/source/local/GoBeesLocalDataSource.java | 8 ++++++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index 51f6019c..b57ba1ad 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -105,7 +105,7 @@ public void deleteApiary(@NonNull Apiary apiary) { // Show progress indicator view.setLoadingIndicator(true); // Delete apiary - goBeesRepository.deleteApiary(apiary, new GoBeesDataSource.TaskCallback() { + goBeesRepository.deleteApiary(apiary.getId(), new GoBeesDataSource.TaskCallback() { @Override public void onSuccess() { // The view may not be able to handle UI updates anymore @@ -162,12 +162,12 @@ private void checkCurrentWeather(List apiaries) { Date now = new Date(); // Check dates for (Apiary apiary : apiaries) { - if(apiary.hasLocation()) { + if (apiary.hasLocation()) { if (apiary.getCurrentWeather() != null) { // Check time Date weatherDate = apiary.getCurrentWeather().getTimestamp(); long minFromLastUpdate = (now.getTime() - weatherDate.getTime()) / 60000; - if(minFromLastUpdate >= MIN_UPDATE_WEATHER) { + if (minFromLastUpdate >= MIN_UPDATE_WEATHER) { apiariesToUpdate.add(apiary); } } else { @@ -177,7 +177,7 @@ private void checkCurrentWeather(List apiaries) { } } // Update weather if needed - if(apiariesToUpdate.size() > 0) { + if (apiariesToUpdate.size() > 0) { goBeesRepository.updateApiariesCurrentWeather(apiariesToUpdate, new GoBeesDataSource.TaskCallback() { @Override public void onSuccess() { diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index da9f8061..631e16db 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -100,7 +100,7 @@ public void deleteHive(@NonNull Hive hive) { // Show progress indicator view.setLoadingIndicator(true); // Delete hive - goBeesRepository.deleteHive(hive, new GoBeesDataSource.TaskCallback() { + goBeesRepository.deleteHive(hive.getId(), new GoBeesDataSource.TaskCallback() { @Override public void onSuccess() { // The view may not be able to handle UI updates anymore diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index c0303612..2b4dec49 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -64,10 +64,10 @@ public interface GoBeesDataSource { /** * Delete apiary. * - * @param apiary apiary to delete. + * @param apiaryId apiary id. * @param callback TaskCallback. */ - void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback); + void deleteApiary(long apiaryId, @NonNull TaskCallback callback); /** * Delete all apiaries. @@ -128,10 +128,10 @@ public interface GoBeesDataSource { /** * Deletes given hive. * - * @param hive hive to delete. + * @param hiveId hive id. * @param callback TaskCallback. */ - void deleteHive(@NonNull Hive hive, @NonNull TaskCallback callback); + void deleteHive(long hiveId, @NonNull TaskCallback callback); /** * Returns the next hive id. @@ -183,7 +183,7 @@ public interface GoBeesDataSource { * Updates the current weather of the apiaries in the list. * * @param apiariesToUpdate apiaries to update weather. - * @param callback TaskCallback. + * @param callback TaskCallback. */ void updateApiariesCurrentWeather(List apiariesToUpdate, @NonNull TaskCallback callback); diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index 99f84531..b6eaeda3 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -145,10 +145,10 @@ public void refreshApiaries() { } @Override - public void deleteApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback) { + public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { checkNotNull(callback); // Delete apiary - goBeesDataSource.deleteApiary(apiary, callback); + goBeesDataSource.deleteApiary(apiaryId, callback); } @Override @@ -231,10 +231,10 @@ public void saveHive(long apiaryId, @NonNull Hive hive, @NonNull TaskCallback ca } @Override - public void deleteHive(@NonNull Hive hive, @NonNull TaskCallback callback) { + public void deleteHive(long hiveId, @NonNull TaskCallback callback) { checkNotNull(callback); // Delete hive - goBeesDataSource.deleteHive(hive, callback); + goBeesDataSource.deleteHive(hiveId, callback); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 28f89835..0b6642b6 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -99,8 +99,11 @@ public void refreshApiaries() { } @Override - public void deleteApiary(@NonNull final Apiary apiary, @NonNull TaskCallback callback) { + public void deleteApiary(long apiaryId, @NonNull TaskCallback callback) { try { + // Get apiary + final Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); + // Delete realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { @@ -236,8 +239,9 @@ public void execute(Realm realm) { } @Override - public void deleteHive(@NonNull final Hive hive, @NonNull TaskCallback callback) { + public void deleteHive(long hiveId, @NonNull TaskCallback callback) { try { + final Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { From 88d1b8915d648c5cf0c05e4f6e1ceec0ab253a3a Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 15:15:07 +0100 Subject: [PATCH 17/32] Fix delete tests #136 --- app/src/prod/java/com/davidmiguel/gobees/Injection.java | 4 +++- .../gobees/data/source/cache/GoBeesRepositoryTest.java | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/prod/java/com/davidmiguel/gobees/Injection.java b/app/src/prod/java/com/davidmiguel/gobees/Injection.java index 712b0b4a..ed0fb191 100644 --- a/app/src/prod/java/com/davidmiguel/gobees/Injection.java +++ b/app/src/prod/java/com/davidmiguel/gobees/Injection.java @@ -2,6 +2,7 @@ import com.davidmiguel.gobees.data.source.cache.GoBeesRepository; import com.davidmiguel.gobees.data.source.local.GoBeesLocalDataSource; +import com.davidmiguel.gobees.data.source.network.WeatherDataSource; /** * Enables injection of production implementations for TasksDataSource at compile time. @@ -9,6 +10,7 @@ public class Injection { public static GoBeesRepository provideApiariesRepository() { - return GoBeesRepository.getInstance(GoBeesLocalDataSource.getInstance()); + return GoBeesRepository.getInstance(GoBeesLocalDataSource.getInstance(), + WeatherDataSource.getInstance()); } } \ No newline at end of file diff --git a/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java b/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java index 296c3f1a..9623838b 100644 --- a/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepositoryTest.java @@ -159,13 +159,10 @@ public void deleteApiary_deleteApiaryFromDbAndCache() { assertThat(goBeesRepository.cachedApiaries.containsKey(apiary.getId()), is(true)); // When deleted - goBeesRepository.deleteApiary(apiary, taskCallback); + goBeesRepository.deleteApiary(apiary.getId(), taskCallback); // Verify the data source were called - verify(goBeesLocalDataSource).deleteApiary(apiary, taskCallback); - - // Verify it's removed from repository - assertThat(goBeesRepository.cachedApiaries.containsKey(apiary.getId()), is(false)); + verify(goBeesLocalDataSource).deleteApiary(apiary.getId(), taskCallback); } @Test From 7ec955b04d89e67f53bf0ff9ff7459fa501e4ddf Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 16:46:35 +0100 Subject: [PATCH 18/32] Fix travis.yml #136 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 250aacff..968faddd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,8 @@ before_script: - cp opencv/build/lib/libopencv_java310.so opencv_lib/libopencv_java310.so - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/opencv_lib/ - chmod +x gradlew + # Create gradle.properties with fake credentials + - echo "OpenWeatherMapApiKey=\"c75f70c4717eb95847d378bba3bdb275\"" > ~/.gradle/gradle.properties script: # Compile and run unit tests From 01b70843e728eec4ca50600b2de2158bb79fce4b Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 17:58:27 +0100 Subject: [PATCH 19/32] Remove unused drawables --- .../addeditapiary/AddEditApiaryFragment.java | 1 - .../addedithive/AddEditHiveFragment.java | 1 - .../gobees/apiaries/ApiariesFragment.java | 1 - .../source/network/WeatherDataSource.java | 2 +- .../gobees/hive/RecordingsAdapter.java | 2 +- .../{fade_green.xml => chart_fade_green.xml} | 0 app/src/main/res/drawable-anydpi/ic_add.xml | 10 +++--- .../drawable-anydpi/ic_add_circle_outline.xml | 9 ----- .../{ic_show_chart.xml => ic_chart_line.xml} | 0 .../{ic_rain.xml => ic_chart_rain.xml} | 0 ...mperature.xml => ic_chart_temperature.xml} | 0 .../{ic_wind.xml => ic_chart_wind.xml} | 0 .../{ic_event.xml => ic_date.xml} | 0 .../{ic_chevron_right.xml => ic_enter.xml} | 4 +-- .../res/drawable-anydpi/ic_filter_drama.xml | 9 ----- .../{ic_cached.xml => ic_last_revision.xml} | 4 +-- .../drawable-anydpi/ic_power_settings_new.xml | 9 ----- .../{ic_videocam.xml => ic_recording.xml} | 0 ...nual_record.xml => ic_start_recording.xml} | 0 .../res/drawable-anydpi/ic_statistics.xml | 9 ----- .../ic_weather_day_clear_sky.xml | 26 +++++++------- .../ic_weather_day_few_clouds.xml | 20 +++++------ .../ic_weather_day_night_broken_clouds.xml | 10 +++--- .../ic_weather_day_night_mist.xml | 32 +++++++++--------- .../res/drawable-anydpi/touch_feedback.xml | 4 --- .../{marker.png => chart_marker.png} | Bin app/src/main/res/layout/addeditapiary_act.xml | 2 +- .../main/res/layout/addeditapiary_frag.xml | 2 +- app/src/main/res/layout/addedithive_act.xml | 2 +- .../main/res/layout/apiaries_list_item.xml | 2 +- .../res/layout/apiary_hives_list_item.xml | 4 +-- app/src/main/res/layout/hive_act.xml | 2 +- .../res/layout/hive_recordings_list_item.xml | 2 +- app/src/main/res/layout/monitoring_frag.xml | 2 +- .../res/layout/recording_bees_marker_vew.xml | 2 +- app/src/main/res/layout/recording_frag.xml | 8 ++--- app/src/main/res/values/strings.xml | 6 ++-- 37 files changed, 73 insertions(+), 114 deletions(-) rename app/src/main/res/drawable-anydpi/{fade_green.xml => chart_fade_green.xml} (100%) delete mode 100644 app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml rename app/src/main/res/drawable-anydpi/{ic_show_chart.xml => ic_chart_line.xml} (100%) rename app/src/main/res/drawable-anydpi/{ic_rain.xml => ic_chart_rain.xml} (100%) rename app/src/main/res/drawable-anydpi/{ic_temperature.xml => ic_chart_temperature.xml} (100%) rename app/src/main/res/drawable-anydpi/{ic_wind.xml => ic_chart_wind.xml} (100%) rename app/src/main/res/drawable-anydpi/{ic_event.xml => ic_date.xml} (100%) rename app/src/main/res/drawable-anydpi/{ic_chevron_right.xml => ic_enter.xml} (77%) delete mode 100644 app/src/main/res/drawable-anydpi/ic_filter_drama.xml rename app/src/main/res/drawable-anydpi/{ic_cached.xml => ic_last_revision.xml} (85%) delete mode 100644 app/src/main/res/drawable-anydpi/ic_power_settings_new.xml rename app/src/main/res/drawable-anydpi/{ic_videocam.xml => ic_recording.xml} (100%) rename app/src/main/res/drawable-anydpi/{ic_fiber_manual_record.xml => ic_start_recording.xml} (100%) delete mode 100644 app/src/main/res/drawable-anydpi/ic_statistics.xml delete mode 100644 app/src/main/res/drawable-anydpi/touch_feedback.xml rename app/src/main/res/drawable-nodpi/{marker.png => chart_marker.png} (100%) diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java index 1674aa5f..562db10b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java @@ -72,7 +72,6 @@ public void onClick(View view) { // Configure floating action button FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab_add_apiary); - fab.setImageResource(R.drawable.ic_done); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java index 72b61d62..244a8054 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java @@ -58,7 +58,6 @@ public void onActivityCreated(Bundle savedInstanceState) { // Configure floating action button FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab_add_hive); - fab.setImageResource(R.drawable.ic_done); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index 9c67e481..cb16f4f9 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -78,7 +78,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // Set up floating action button FloatingActionButton fab = (FloatingActionButton) getActivity().findViewById(R.id.fab_add_apiary); - fab.setImageResource(R.drawable.ic_add); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java index c3726a2b..9fdc0e12 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java @@ -60,7 +60,7 @@ protected DataHolder doInBackground(DataHolder... dataArray) { data.setMeteoRecord(meteoRecord); return data; } catch (IOException | JSONException e) { - return null; + return data; } } diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java index ee984f89..3b9c6d2d 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java @@ -214,7 +214,7 @@ private LineData styleChartLines(List entries) { lineDataSet.setColor(color); lineDataSet.setDrawFilled(true); lineDataSet.setFillAlpha(255); - Drawable drawable = ContextCompat.getDrawable(context, R.drawable.fade_green); + Drawable drawable = ContextCompat.getDrawable(context, R.drawable.chart_fade_green); lineDataSet.setFillDrawable(drawable); return new LineData(lineDataSet); } diff --git a/app/src/main/res/drawable-anydpi/fade_green.xml b/app/src/main/res/drawable-anydpi/chart_fade_green.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/fade_green.xml rename to app/src/main/res/drawable-anydpi/chart_fade_green.xml diff --git a/app/src/main/res/drawable-anydpi/ic_add.xml b/app/src/main/res/drawable-anydpi/ic_add.xml index 87545cde..c8f60180 100644 --- a/app/src/main/res/drawable-anydpi/ic_add.xml +++ b/app/src/main/res/drawable-anydpi/ic_add.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> diff --git a/app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml b/app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml deleted file mode 100644 index 03511a76..00000000 --- a/app/src/main/res/drawable-anydpi/ic_add_circle_outline.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-anydpi/ic_show_chart.xml b/app/src/main/res/drawable-anydpi/ic_chart_line.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_show_chart.xml rename to app/src/main/res/drawable-anydpi/ic_chart_line.xml diff --git a/app/src/main/res/drawable-anydpi/ic_rain.xml b/app/src/main/res/drawable-anydpi/ic_chart_rain.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_rain.xml rename to app/src/main/res/drawable-anydpi/ic_chart_rain.xml diff --git a/app/src/main/res/drawable-anydpi/ic_temperature.xml b/app/src/main/res/drawable-anydpi/ic_chart_temperature.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_temperature.xml rename to app/src/main/res/drawable-anydpi/ic_chart_temperature.xml diff --git a/app/src/main/res/drawable-anydpi/ic_wind.xml b/app/src/main/res/drawable-anydpi/ic_chart_wind.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_wind.xml rename to app/src/main/res/drawable-anydpi/ic_chart_wind.xml diff --git a/app/src/main/res/drawable-anydpi/ic_event.xml b/app/src/main/res/drawable-anydpi/ic_date.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_event.xml rename to app/src/main/res/drawable-anydpi/ic_date.xml diff --git a/app/src/main/res/drawable-anydpi/ic_chevron_right.xml b/app/src/main/res/drawable-anydpi/ic_enter.xml similarity index 77% rename from app/src/main/res/drawable-anydpi/ic_chevron_right.xml rename to app/src/main/res/drawable-anydpi/ic_enter.xml index 24835127..87372cc9 100644 --- a/app/src/main/res/drawable-anydpi/ic_chevron_right.xml +++ b/app/src/main/res/drawable-anydpi/ic_enter.xml @@ -1,8 +1,8 @@ + android:viewportHeight="24.0" + android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable-anydpi/ic_filter_drama.xml b/app/src/main/res/drawable-anydpi/ic_filter_drama.xml deleted file mode 100644 index 6185cf37..00000000 --- a/app/src/main/res/drawable-anydpi/ic_filter_drama.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-anydpi/ic_cached.xml b/app/src/main/res/drawable-anydpi/ic_last_revision.xml similarity index 85% rename from app/src/main/res/drawable-anydpi/ic_cached.xml rename to app/src/main/res/drawable-anydpi/ic_last_revision.xml index 5e776beb..3acbbb59 100644 --- a/app/src/main/res/drawable-anydpi/ic_cached.xml +++ b/app/src/main/res/drawable-anydpi/ic_last_revision.xml @@ -1,8 +1,8 @@ + android:viewportHeight="24.0" + android:viewportWidth="24.0"> diff --git a/app/src/main/res/drawable-anydpi/ic_power_settings_new.xml b/app/src/main/res/drawable-anydpi/ic_power_settings_new.xml deleted file mode 100644 index d3dbc7eb..00000000 --- a/app/src/main/res/drawable-anydpi/ic_power_settings_new.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-anydpi/ic_videocam.xml b/app/src/main/res/drawable-anydpi/ic_recording.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_videocam.xml rename to app/src/main/res/drawable-anydpi/ic_recording.xml diff --git a/app/src/main/res/drawable-anydpi/ic_fiber_manual_record.xml b/app/src/main/res/drawable-anydpi/ic_start_recording.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_fiber_manual_record.xml rename to app/src/main/res/drawable-anydpi/ic_start_recording.xml diff --git a/app/src/main/res/drawable-anydpi/ic_statistics.xml b/app/src/main/res/drawable-anydpi/ic_statistics.xml deleted file mode 100644 index cc07bc2a..00000000 --- a/app/src/main/res/drawable-anydpi/ic_statistics.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml index 3cb6ddd1..c9ee6ba2 100644 --- a/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_clear_sky.xml @@ -1,46 +1,46 @@ + android:width="40.367dp" + android:height="40.973dp" + android:viewportHeight="40.973" + android:viewportWidth="40.367"> +c0,0.988,0.802,1.791,1.79,1.791c0.988,0,1.789-0.803,1.789-1.791V1.79z"/> +c0.7,0.699,1.833,0.699,2.532,0c0.699-0.698,0.699-1.832,0-2.53L8.632,6.078z"/> +c0.988,0,1.79-0.802,1.79-1.791c0-0.987-0.802-1.789-1.79-1.789H1.791z"/> +c0.7-0.699,0.7-1.833,0-2.531c-0.698-0.697-1.833-0.697-2.531,0.001L6.078,32.944z"/> +c0-0.988-0.803-1.79-1.791-1.79c-0.987,0-1.79,0.803-1.79,1.789V39.181z"/> +c-0.699-0.698-1.832-0.698-2.531,0c-0.699,0.699-0.699,1.832,0,2.53L32.945,35.499z"/> +c-0.988,0-1.791,0.801-1.791,1.791c0,0.987,0.803,1.79,1.791,1.788L38.576,22.595z"/> +c-0.699,0.699-0.699,1.831,0,2.531c0.699,0.699,1.832,0.699,2.531,0L35.498,8.632z"/> +s3.392-7.576,7.576-7.576c4.185,0,7.576,3.394,7.576,7.576S24.988,28.401,20.805,28.401z"/> \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml index 9986e107..04fa2458 100644 --- a/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_few_clouds.xml @@ -1,30 +1,30 @@ + android:width="54.682dp" + android:height="33.525dp" + android:viewportHeight="33.525" + android:viewportWidth="54.682"> +c-0.828,0-1.502,0.673-1.502,1.502v3.336C35.75,5.668,36.424,6.341,37.252,6.341z"/> +c-0.586-0.587-1.539-0.587-2.125,0s-0.586,1.538,0,2.124L27.273,9.583z"/> +c0.586,0.588,1.537,0.588,2.123,0c0.586-0.586,0.586-1.537,0-2.123L47.203,25.305z"/> +c0.828,0,1.502-0.672,1.502-1.502C54.682,16.628,54.008,15.955,53.18,15.955z"/> +L45.1,7.478c-0.586,0.586-0.586,1.538,0,2.125C45.686,10.189,46.637,10.189,47.223,9.603z"/> +C39.859,18.056,35.467,18.371,35.467,18.371z"/> \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml index 66f8e7fc..153d1963 100644 --- a/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_broken_clouds.xml @@ -1,9 +1,9 @@ + android:width="52.045dp" + android:height="26.648dp" + android:viewportHeight="26.648" + android:viewportWidth="52.045"> +C49.83,15.467,48.287,17.009,46.385,17.009z"/> \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml b/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml index 21cf2365..e0a854ca 100644 --- a/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml +++ b/app/src/main/res/drawable-anydpi/ic_weather_day_night_mist.xml @@ -1,56 +1,56 @@ + android:width="47.707dp" + android:height="24.756dp" + android:viewportHeight="24.756" + android:viewportWidth="47.707"> +C0.881,13.866,0,14.747,0,15.833s0.881,1.967,1.969,1.966L9.246,17.8z"/> +c-1.088,0-1.969,0.881-1.969,1.967s0.881,1.967,1.969,1.966L23.855,17.8z"/> +c-1.086,0-1.965,0.881-1.965,1.967s0.879,1.967,1.965,1.966L38.461,17.8z"/> +C0.881-0.046,0,0.834,0,1.921c0,1.086,0.881,1.965,1.969,1.965H9.246z"/> +c-1.088,0-1.969,0.881-1.969,1.967s0.881,1.965,1.969,1.965H23.855z"/> +c-1.086,0-1.965,0.881-1.965,1.967s0.879,1.965,1.965,1.965H38.461z"/> +c-1.088,0-1.967,0.881-1.967,1.966c0,1.087,0.879,1.967,1.967,1.966L16.525,24.756z"/> +c-1.09,0-1.967,0.881-1.967,1.966c0,1.087,0.877,1.967,1.967,1.966L31.133,24.756z"/> +c-1.086,0-1.965,0.881-1.965,1.966c0,1.087,0.879,1.967,1.965,1.966L45.74,24.756z"/> +c-1.088,0-1.967,0.881-1.967,1.966c0,1.087,0.879,1.967,1.967,1.966L16.525,10.843z"/> +c-1.09,0-1.967,0.881-1.967,1.966c0,1.087,0.877,1.967,1.967,1.966L31.133,10.843z"/> +c-1.086,0-1.965,0.881-1.965,1.966c0,1.087,0.879,1.967,1.965,1.966L45.74,10.843z"/> \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/touch_feedback.xml b/app/src/main/res/drawable-anydpi/touch_feedback.xml deleted file mode 100644 index fde661e5..00000000 --- a/app/src/main/res/drawable-anydpi/touch_feedback.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-nodpi/marker.png b/app/src/main/res/drawable-nodpi/chart_marker.png similarity index 100% rename from app/src/main/res/drawable-nodpi/marker.png rename to app/src/main/res/drawable-nodpi/chart_marker.png diff --git a/app/src/main/res/layout/addeditapiary_act.xml b/app/src/main/res/layout/addeditapiary_act.xml index da43be9b..d6d5feff 100644 --- a/app/src/main/res/layout/addeditapiary_act.xml +++ b/app/src/main/res/layout/addeditapiary_act.xml @@ -37,7 +37,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" - android:src="@drawable/ic_add" + android:src="@drawable/ic_done" app:fabSize="normal" app:layout_anchor="@id/contentFrame" app:layout_anchorGravity="bottom|right|end"/> diff --git a/app/src/main/res/layout/addeditapiary_frag.xml b/app/src/main/res/layout/addeditapiary_frag.xml index a0c301cd..490b1fb2 100644 --- a/app/src/main/res/layout/addeditapiary_frag.xml +++ b/app/src/main/res/layout/addeditapiary_frag.xml @@ -45,7 +45,7 @@ android:layout_centerVertical="true" android:layout_gravity="center" android:adjustViewBounds="false" - android:contentDescription="@string/date_icon" + android:contentDescription="@string/gps_location_icon" android:src="@drawable/ic_my_location" android:tint="@color/colorAccent"/> diff --git a/app/src/main/res/layout/addedithive_act.xml b/app/src/main/res/layout/addedithive_act.xml index 07c6a509..a8af73db 100644 --- a/app/src/main/res/layout/addedithive_act.xml +++ b/app/src/main/res/layout/addedithive_act.xml @@ -37,7 +37,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" - android:src="@drawable/ic_add" + android:src="@drawable/ic_done" app:fabSize="normal" app:layout_anchor="@id/contentFrame" app:layout_anchorGravity="bottom|right|end"/> diff --git a/app/src/main/res/layout/apiaries_list_item.xml b/app/src/main/res/layout/apiaries_list_item.xml index b4f262ed..914a6dd1 100644 --- a/app/src/main/res/layout/apiaries_list_item.xml +++ b/app/src/main/res/layout/apiaries_list_item.xml @@ -121,7 +121,7 @@ android:layout_marginEnd="8dp" android:adjustViewBounds="false" android:contentDescription="@string/enter_icon" - android:src="@drawable/ic_chevron_right" + android:src="@drawable/ic_enter" android:tint="@color/colorAccent"/> diff --git a/app/src/main/res/layout/apiary_hives_list_item.xml b/app/src/main/res/layout/apiary_hives_list_item.xml index 5f0a9b25..56cc4698 100644 --- a/app/src/main/res/layout/apiary_hives_list_item.xml +++ b/app/src/main/res/layout/apiary_hives_list_item.xml @@ -66,7 +66,7 @@ android:layout_toEndOf="@id/hive_image" android:adjustViewBounds="false" android:contentDescription="@string/last_revision_icon" - android:src="@drawable/ic_cached" + android:src="@drawable/ic_last_revision" android:tint="@color/colorSecondaryText"/> diff --git a/app/src/main/res/layout/hive_act.xml b/app/src/main/res/layout/hive_act.xml index 273ffe31..fb52272a 100644 --- a/app/src/main/res/layout/hive_act.xml +++ b/app/src/main/res/layout/hive_act.xml @@ -52,7 +52,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/fab_margin" - android:src="@drawable/ic_videocam" + android:src="@drawable/ic_recording" app:fabSize="normal" app:layout_anchor="@id/contentFrame" app:layout_anchorGravity="bottom|right|end"/> diff --git a/app/src/main/res/layout/hive_recordings_list_item.xml b/app/src/main/res/layout/hive_recordings_list_item.xml index b4ca4a48..7899a578 100644 --- a/app/src/main/res/layout/hive_recordings_list_item.xml +++ b/app/src/main/res/layout/hive_recordings_list_item.xml @@ -25,7 +25,7 @@ android:layout_marginTop="18dp" android:adjustViewBounds="false" android:contentDescription="@string/date_icon" - android:src="@drawable/ic_event" + android:src="@drawable/ic_date" android:tint="@color/colorAccent"/> diff --git a/app/src/main/res/layout/recording_bees_marker_vew.xml b/app/src/main/res/layout/recording_bees_marker_vew.xml index 592ccfeb..9235e53f 100644 --- a/app/src/main/res/layout/recording_bees_marker_vew.xml +++ b/app/src/main/res/layout/recording_bees_marker_vew.xml @@ -2,7 +2,7 @@ + android:background="@drawable/chart_marker"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0445f5d6..05aef78c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,9 +32,9 @@ More… - + Enter… - + Date Weather icon @@ -42,6 +42,8 @@ Chart line Settings icon + + Get GPS location From 5c6efe7a7faa8d8c4ed4dcdfad6fa0216ccb0b4d Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 18:40:43 +0100 Subject: [PATCH 20/32] Fix long names overlaping #138 --- app/src/main/res/layout/addedithive_frag.xml | 4 +++- app/src/main/res/layout/apiaries_list_item.xml | 5 ++++- app/src/main/res/layout/apiary_hives_list_item.xml | 7 ++++++- app/src/main/res/layout/hive_recordings_list_item.xml | 7 +++++-- app/src/main/res/values/strings.xml | 8 ++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/addedithive_frag.xml b/app/src/main/res/layout/addedithive_frag.xml index b157fb4c..b4dd24c7 100644 --- a/app/src/main/res/layout/addedithive_frag.xml +++ b/app/src/main/res/layout/addedithive_frag.xml @@ -18,6 +18,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/hive_name_hint" + android:inputType="textCapWords" android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Title"/> @@ -26,7 +27,8 @@ android:layout_width="match_parent" android:layout_height="350dp" android:gravity="top" - android:hint="@string/hive_notes_hint"/> + android:hint="@string/hive_notes_hint" + android:inputType="textCapSentences"/> diff --git a/app/src/main/res/layout/apiaries_list_item.xml b/app/src/main/res/layout/apiaries_list_item.xml index 914a6dd1..b97449bd 100644 --- a/app/src/main/res/layout/apiaries_list_item.xml +++ b/app/src/main/res/layout/apiaries_list_item.xml @@ -40,6 +40,9 @@ android:layout_marginTop="24dp" android:layout_toEndOf="@id/apiary_image" android:layout_toStartOf="@+id/more_icon" + android:ellipsize="end" + android:maxLines="2" + android:text="@string/default_apiary_name" android:textAppearance="@style/TextAppearance.AppCompat.Title" android:textColor="@color/colorPrimaryText"/> @@ -103,9 +106,9 @@ android:layout_alignParentBottom="true" android:layout_marginBottom="16dp" android:layout_marginStart="4dp" - android:text="@string/default_temperature" android:layout_toEndOf="@id/weather_icon" android:gravity="bottom" + android:text="@string/default_temperature" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textColor="@color/colorSecondaryText" android:visibility="gone"/> diff --git a/app/src/main/res/layout/apiary_hives_list_item.xml b/app/src/main/res/layout/apiary_hives_list_item.xml index 56cc4698..46a7a7a6 100644 --- a/app/src/main/res/layout/apiary_hives_list_item.xml +++ b/app/src/main/res/layout/apiary_hives_list_item.xml @@ -40,6 +40,9 @@ android:layout_marginTop="24dp" android:layout_toEndOf="@id/hive_image" android:layout_toStartOf="@+id/more_icon" + android:ellipsize="end" + android:maxLines="2" + android:text="@string/default_hive_name" android:textAppearance="@style/TextAppearance.AppCompat.Title" android:textColor="@color/colorPrimaryText"/> @@ -79,8 +82,10 @@ android:layout_marginStart="4dp" android:layout_toEndOf="@id/last_revision_icon" android:layout_toStartOf="@+id/enter_icon" + android:ellipsize="end" android:gravity="bottom" - android:text="10 days ago" + android:maxLines="1" + android:text="@string/default_last_revision" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textColor="@color/colorSecondaryText"/> diff --git a/app/src/main/res/layout/hive_recordings_list_item.xml b/app/src/main/res/layout/hive_recordings_list_item.xml index 7899a578..e2638a20 100644 --- a/app/src/main/res/layout/hive_recordings_list_item.xml +++ b/app/src/main/res/layout/hive_recordings_list_item.xml @@ -35,7 +35,10 @@ android:layout_alignParentTop="true" android:layout_marginStart="8dp" android:layout_marginTop="16dp" + android:text="@string/default_recording_date" android:layout_toEndOf="@id/date_icon" + android:ellipsize="end" + android:maxLines="2" android:textAppearance="@style/TextAppearance.AppCompat.Title" android:textColor="@color/colorPrimaryText"/> @@ -56,8 +59,8 @@ android:id="@+id/chart" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="8dp" - android:layout_below="@id/recording_date"/> + android:layout_below="@id/recording_date" + android:layout_marginTop="8dp"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05aef78c..80395728 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,8 @@ Number of hives + + My apiary 0 @@ -110,6 +112,10 @@ Hives Info + + My hive + + 10 days ago Error while loading hives. @@ -150,6 +156,8 @@ Recordings Info + + Wen. 18 jan., 2017 Error while loading recordings. From dfeea6d4b1150a28c8d8347f64031ad81af8dba6 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sat, 31 Dec 2016 20:07:55 +0100 Subject: [PATCH 21/32] Fix edit apiary and hive #139 --- .../addeditapiary/AddEditApiaryContract.java | 2 +- .../addeditapiary/AddEditApiaryFragment.java | 2 +- .../addeditapiary/AddEditApiaryPresenter.java | 73 +++++++++--------- .../addedithive/AddEditHiveContract.java | 4 +- .../addedithive/AddEditHiveFragment.java | 2 +- .../addedithive/AddEditHivePresenter.java | 62 +++++++++++---- .../davidmiguel/gobees/data/model/Hive.java | 18 +++++ .../source/local/GoBeesLocalDataSource.java | 8 +- .../AddEditApiaryPresenterTest.java | 6 +- .../addedithive/AddEditHivePresenterTest.java | 6 +- docs/img/board.png | Bin 0 -> 170975 bytes 11 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 docs/img/board.png diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java index 9a0a49f8..86919f0e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java @@ -75,7 +75,7 @@ interface Presenter extends BasePresenter { * @param name apiary name. * @param notes apiary notes. */ - void saveApiary(String name, String notes); + void save(String name, String notes); /** * Fill apiary data (the apiary must already exist in the repository). diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java index 562db10b..c647c75c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java @@ -75,7 +75,7 @@ public void onClick(View view) { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - presenter.saveApiary(nameTextView.getText().toString(), + presenter.save(nameTextView.getText().toString(), notesTextView.getText().toString()); } }); diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java index b6ebe3b9..022b67cb 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java @@ -30,6 +30,7 @@ class AddEditApiaryPresenter implements AddEditApiaryContract.Presenter, private LocationService locationService; private long apiaryId; + private Apiary apiary; private Location apiaryLocation; AddEditApiaryPresenter(GoBeesRepository goBeesRepository, @@ -42,9 +43,9 @@ class AddEditApiaryPresenter implements AddEditApiaryContract.Presenter, } @Override - public void saveApiary(String name, String notes) { + public void save(String name, String notes) { if (isNewApiary()) { - createApiary(name, notes, this); + createApiary(name, notes); } else { updateApiary(name, notes); } @@ -61,14 +62,14 @@ public void populateApiary() { @Override public void toogleLocation(Context context) { if (locationService == null) { - // Conect GPS service + // Connect GPS service // TODO check permissions locationService = new LocationService(context, this, this); locationService.connect(); view.setLocationIcon(true); view.showSearchingGpsMsg(); } else { - // Disconect GPS service + // Disconnect GPS service stopLocation(); } @@ -88,11 +89,15 @@ public void stopLocation() { public void start() { if (!isNewApiary()) { populateApiary(); + } else { + apiary = new Apiary(); } } + @SuppressWarnings("ConstantConditions") @Override public void onApiaryLoaded(Apiary apiary) { + this.apiary = apiary; // Show apiary data on view if (view.isActive()) { // Set name @@ -100,7 +105,7 @@ public void onApiaryLoaded(Apiary apiary) { // Set notes view.setNotes(apiary.getNotes()); // Set location - if (apiary.getLocationLat() != null && apiary.getLocationLong() != null) { + if (apiary.hasLocation()) { apiaryLocation = new Location(""); apiaryLocation.setLatitude(apiary.getLocationLat()); apiaryLocation.setLongitude(apiary.getLocationLong()); @@ -176,34 +181,15 @@ private boolean isNewApiary() { /** * Create an save a new apiary. * - * @param name apiary name. - * @param notes apiary notes. - * @param listener TaskCallback. + * @param name apiary name. + * @param notes apiary notes. */ - private void createApiary(final String name, final String notes, final TaskCallback listener) { + private void createApiary(final String name, final String notes) { // Get next id goBeesRepository.getNextApiaryId(new GoBeesDataSource.GetNextApiaryIdCallback() { @Override public void onNextApiaryIdLoaded(long apiaryId) { - // Create apiary - Apiary newApiary = new Apiary(); - // Set id - newApiary.setId(apiaryId); - // Set name - newApiary.setName(name); - // Set location - if (apiaryLocation != null) { - newApiary.setLocationLat(apiaryLocation.getLatitude()); - newApiary.setLocationLong(apiaryLocation.getLongitude()); - } - // Set notes - newApiary.setNotes(notes); - // Save it if it is correct - if (newApiary.isValidApiary()) { - goBeesRepository.saveApiary(newApiary, listener); - } else { - view.showEmptyApiaryError(); - } + saveApiary(apiaryId, name, notes); } }); } @@ -218,21 +204,34 @@ private void updateApiary(String name, String notes) { if (isNewApiary()) { throw new RuntimeException("updateApiary() was called but apiary is new."); } - // TODO refactor createApiary() and updateApiary() (almost same code) - // Create new apiary with the modifications - Apiary editedApiary = new Apiary(); + saveApiary(apiaryId, name, notes); + } + + /** + * Saves (or update) the apiary. + * + * @param apiaryId apiary id. + * @param name apiary name. + * @param notes apiary notes. + */ + private void saveApiary(long apiaryId, String name, String notes) { // Set id - editedApiary.setId(apiaryId); + apiary.setId(apiaryId); // Set name - editedApiary.setName(name); + apiary.setName(name); // Set location if (apiaryLocation != null) { - editedApiary.setLocationLat(apiaryLocation.getLatitude()); - editedApiary.setLocationLong(apiaryLocation.getLongitude()); + apiary.setLocationLat(apiaryLocation.getLatitude()); + apiary.setLocationLong(apiaryLocation.getLongitude()); } // Set notes - editedApiary.setNotes(notes); - goBeesRepository.saveApiary(editedApiary, this); + apiary.setNotes(notes); + // Save it if it is correct + if (apiary.isValidApiary()) { + goBeesRepository.saveApiary(apiary, this); + } else { + view.showEmptyApiaryError(); + } } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveContract.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveContract.java index b544a593..6fb4146f 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveContract.java @@ -6,7 +6,7 @@ /** * This specifies the contract between the view and the presenter. */ -public class AddEditHiveContract { +interface AddEditHiveContract { interface View extends BaseView { @@ -48,7 +48,7 @@ interface Presenter extends BasePresenter { * @param name hive name. * @param notes hive notes. */ - void saveHive(String name, String notes); + void save(String name, String notes); /** * Fill hive data (the hive must already exist in the repository). diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java index 244a8054..cc6ba955 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHiveFragment.java @@ -61,7 +61,7 @@ public void onActivityCreated(Bundle savedInstanceState) { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - presenter.saveHive(nameTextView.getText().toString(), + presenter.save(nameTextView.getText().toString(), notesTextView.getText().toString()); } }); diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java index 7e274bf8..e6544f00 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java @@ -18,6 +18,7 @@ class AddEditHivePresenter implements AddEditHiveContract.Presenter, private long apiaryId; private long hiveId; + private Hive hive; AddEditHivePresenter(GoBeesRepository goBeesRepository, AddEditHiveContract.View view, @@ -30,9 +31,9 @@ class AddEditHivePresenter implements AddEditHiveContract.Presenter, } @Override - public void saveHive(String name, String notes) { + public void save(String name, String notes) { if (isNewHive()) { - createHive(name, notes, this); + createHive(name, notes); } else { updateHive(name, notes); } @@ -50,11 +51,14 @@ public void populateHive() { public void start() { if (!isNewHive()) { populateHive(); + } else { + hive = new Hive(); } } @Override public void onHiveLoaded(Hive hive) { + this.hive = hive; // Show hive data on view if (view.isActive()) { view.setName(hive.getName()); @@ -82,33 +86,63 @@ public void onFailure() { view.showSaveHiveError(); } + /** + * Checks whether a hive is new or not. + * + * @return true/false. + */ private boolean isNewHive() { return hiveId == AddEditHiveActivity.NEW_HIVE; } - private void createHive(final String name, final String notes, final TaskCallback listener) { + /** + * Create an save a new hive. + * + * @param name hive name. + * @param notes hive notes. + */ + private void createHive(final String name, final String notes) { // Get next id goBeesRepository.getNextHiveId(new GetNextHiveIdCallback() { @Override public void onNextHiveIdLoaded(long hiveId) { - // Create hive - Hive newHive = new Hive(hiveId, name, null, notes, null); - // Save it if it is correct - if (newHive.isValidHive()) { - goBeesRepository.saveHive(apiaryId, newHive, listener); - } else { - view.showEmptyHiveError(); - } + saveHive(hiveId, name, notes); } }); } + /** + * Update and save a hive. + * + * @param name hive name + * @param notes hive notes + */ private void updateHive(String name, String notes) { if (isNewHive()) { throw new RuntimeException("updateHive() was called but hive is new."); } - // Create new hive with the modifications - Hive editedHive = new Hive(hiveId, name, null, notes, null); - goBeesRepository.saveHive(apiaryId, editedHive, this); + saveHive(hiveId, name, notes); + } + + /** + * aves (or update) the hive. + * + * @param hiveId hive id. + * @param name hive name. + * @param notes hive notes. + */ + private void saveHive(long hiveId, String name, String notes) { + // Set id + hive.setId(hiveId); + // Set name + hive.setName(name); + // Set notes + hive.setNotes(notes); + // Save it if it is correct + if (hive.isValidHive()) { + goBeesRepository.saveHive(apiaryId, hive, this); + } else { + view.showEmptyHiveError(); + } } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/Hive.java b/app/src/main/java/com/davidmiguel/gobees/data/model/Hive.java index 61dae9fb..21856212 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/Hive.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/Hive.java @@ -3,6 +3,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.google.common.base.Objects; import com.google.common.base.Strings; import java.util.List; @@ -133,4 +134,21 @@ public void addRecords(@NonNull List recordsList) { records.addAll(recordsList); } } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Hive hive = (Hive) o; + return id == hive.id; + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 0b6642b6..b745c2f2 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -229,7 +229,13 @@ public void execute(Realm realm) { realm.copyToRealmOrUpdate(hive); // Add to apiary Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); - apiary.addHive(hive); + if (apiary.getHives() != null) { + Hive h = apiary.getHives().where().equalTo("id", hive.getId()).findFirst(); + if (h == null) { + // New hive + apiary.addHive(hive); + } + } } }); callback.onSuccess(); diff --git a/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java b/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java index 6e8d5f76..54a659bd 100644 --- a/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java @@ -57,7 +57,7 @@ public void saveNewApiaryToRepository_showsSuccessMessage() { new AddEditApiaryPresenter(apiariesRepository, addeditapiaryView, AddEditApiaryActivity.NEW_APIARY); // When the presenter is asked to save an apiary - addEditApiaryPresenter.saveApiary("Apiary 1", "Some notes about it...."); + addEditApiaryPresenter.save("Apiary 1", "Some notes about it...."); // Then a new id is requested verify(apiariesRepository).getNextApiaryId(getNextApiaryIdCallbackArgumentCaptor.capture()); getNextApiaryIdCallbackArgumentCaptor.getValue().onNextApiaryIdLoaded(1); @@ -76,7 +76,7 @@ public void saveEmptyApiary_showsErrorUi() { new AddEditApiaryPresenter(apiariesRepository, addeditapiaryView, AddEditApiaryActivity.NEW_APIARY); // When the presenter is asked to save an empty apiary - addEditApiaryPresenter.saveApiary("", ""); + addEditApiaryPresenter.save("", ""); // Then a new id is requested verify(apiariesRepository).getNextApiaryId(getNextApiaryIdCallbackArgumentCaptor.capture()); getNextApiaryIdCallbackArgumentCaptor.getValue().onNextApiaryIdLoaded(1); @@ -90,7 +90,7 @@ public void saveExistingApiaryToRepository_showsSuccessMessageUi() { addEditApiaryPresenter = new AddEditApiaryPresenter(apiariesRepository, addeditapiaryView, 1); // When the presenter is asked to save an apiary - addEditApiaryPresenter.saveApiary("Apiary 1", "Some more notes about it...."); + addEditApiaryPresenter.save("Apiary 1", "Some more notes about it...."); // Then an apiary is saved in the repository verify(apiariesRepository) .saveApiary(any(Apiary.class), taskCallbackArgumentCaptor.capture()); diff --git a/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java b/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java index 8601802c..fa4e91df 100644 --- a/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java @@ -58,7 +58,7 @@ public void saveNewHiveToRepository_showsSuccessMessage() { new AddEditHivePresenter(apiariesRepository, addEditHiveView, APIARY_ID, AddEditHiveActivity.NEW_HIVE); // When the presenter is asked to save a hive - addEditHivePresenter.saveHive("Hive 1", "Some notes about it...."); + addEditHivePresenter.save("Hive 1", "Some notes about it...."); // Then a new id is requested verify(apiariesRepository).getNextHiveId(getNextHiveIdCallbackArgumentCaptor.capture()); getNextHiveIdCallbackArgumentCaptor.getValue().onNextHiveIdLoaded(1); @@ -77,7 +77,7 @@ public void saveEmptyApiary_showsErrorUi() { new AddEditHivePresenter(apiariesRepository, addEditHiveView, APIARY_ID, AddEditHiveActivity.NEW_HIVE); // When the presenter is asked to save an empty hive - addEditHivePresenter.saveHive("", ""); + addEditHivePresenter.save("", ""); // Then a new id is requested verify(apiariesRepository).getNextHiveId(getNextHiveIdCallbackArgumentCaptor.capture()); getNextHiveIdCallbackArgumentCaptor.getValue().onNextHiveIdLoaded(1); @@ -91,7 +91,7 @@ public void saveExistingApiaryToRepository_showsSuccessMessageUi() { addEditHivePresenter = new AddEditHivePresenter(apiariesRepository, addEditHiveView, APIARY_ID, 1); // When the presenter is asked to save a hive - addEditHivePresenter.saveHive("Apiary 1", "Some more notes about it...."); + addEditHivePresenter.save("Apiary 1", "Some more notes about it...."); // Then a hive is saved in the repository verify(apiariesRepository) .saveHive(anyLong(), any(Hive.class), taskCallbackArgumentCaptor.capture()); diff --git a/docs/img/board.png b/docs/img/board.png new file mode 100644 index 0000000000000000000000000000000000000000..834f03e569b2cb63814584997c4548e429e870f7 GIT binary patch literal 170975 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

)e-cxQe8CkWcs?rM5tO)n}uus?SR{ z?Y;u3dTmMODa>otRj0IeYNB`3wW}AYWnMi@Ub^#1$w}6^5%7EJ|MhR>?}CBYBza)5 z%;7x2Ya_*(r%T&S@ochM%P=Nq30tH;(RPn`zBxs8UTgFL?VdU$^~>Bo+D?Jweq#XpRDKF zWex|)yg_1oJ&)ch_)5NcM84dMV56^ehrwW0+Y}9CVd2s*h~fTNWGbYmBDZ58j>>em znWym&f=6^l(@~9cbj3ad!%N7by$*;)UO7MD>ia|!DEZP|z<(Fyqt67f5p0*}x?SvM zkUa<7!zNS6+uhhZ+1?YomLrnM5)^LRzCMbNn>qT|ZfTKy#16_}~-fp8}G$C1U35qRXwq3{ys zXY%pks+4TUTI%Y?T6nexYwBRa7q9%Dgc`8&;I`X~EfA=Zp5okB0?#Q6BZJ#~1*}N* zeo#&N3grFihpOV!Q5DL4`?dVfI{Z5aRW7|3^bZei>2nQ@i;{S0kuv|xz;oo!H!wYF zr*ZQ(_vlZ>C1)U&S#+vOn?cqg7x|L0ND`xud#(e-3;aizyr5y10ptaNxbcs{fW_=S z;zm5@%w1FFE$iQ>N0ylDdh#YKjZ{ownb8VfTZQ|In+F#+2ilsCyA5po3YtsnXjX!< zB_AvwVSi<}+~3b;f_@#fRhYml&UUwOO7y$ zDU8v2c1c0$aQdwBi=m&zhv5bhFS*lw^BB^mykip>#}w{zC`+z3uBL7^Jq5iR-A#RH zP)fWH6qbikE88#sVZ{$Xsg72?zpzDzQ|%NQ1tiM6P7=|#w>w@qAj-l~LDFo^OuJTo zBKxQmt7yCutGu&L=@$EXSQvy3K4L9Qa9%xldj$9Jnkbk{XT7~@hJ_Ey!DZPMf3~6`MdJ(SUbjinBZ@h=$V=|@X?;y*fNReAOY)5H)+4#%7C82 za`gQ4dikJp$l%x7%x+>YUe%-%<)A19rdkiWy|zp<~|8d%@pcSZ}gMG5Gc* z{@q=fQ3DBI_N1}N-NQv+PSCrfv*LBnksms$F@y-~SNcABD)H4J3e(kfYV+ z1e2Nl-O64<)oGqHZfp4QIk>B~$@Lz1$e^Y^*jxrDa^nXw-T(58$F-ySwH@M©4 zP_15yL(*pVce{P?Qw=q@ zb+8yam{jPa+1*MhcY_0{Ft43R_zK3bqx>qC&H%0jQ--(XzgSoV$-aK%NZZgodEoZw zJF~rmOTlbLZekh+D7@v_r?^Hb9A2>&+S7Rqr2Ei^ZXmJ$+wQneh(B(tjjBsNk+lcK zvEF8)Z^VggPK{#&(XchAtUS+??8WY=Fq@n;i0u@P(o?TrEs#`p(5t@lIDTlctlwIwn*!_bQF!26%6kOrr3))iCC zAoi?#sbf}|0{7gB+`to}doib-Gx<>-^nE_dWhJaBDJ|I$t%L`OQga3`;#|So@VNo9 zdRt9aat0qX<0Md=Y&dAp4g)%I!UkgiTG=_=HD^YN|jRod2qI({#ZL6I0uNPw1vO8&fE7*eEqwUnH#&`{Gv-{72;cDuSf$i#C5{ zEo89QELnky>|T|z{_JC*##{FC;y$4BRTeNL;ktrjUw;-vs+c-SBNdDDz@+a0I1h zl^%Wm)JuzEsf;?G+r=H$9IiHB$f#}e#$VpiHjFPJ9>c2M9Chw@uc}+(af4C3Xj>n} zlKf}bG^Df~6N|9nGw*_aC$%mlX3r`;Dd~+IDmR7z>&ay8M=gPvv;Z`cT%-S8L9g9v z;sj3>eM|C*2-^cIvmp4aa(jLFU-rlUz&Eyf1dTf1I3au2&WB(B0pnfH%%04`F6FN; zaRvhT{BN}TUtbb(2kbRq)Jqh$*|}MUUCl^E6eT#9t@dbDtQsE`;_Az7M5c@^{U&m> z9V-YSPJ|Pga>GyP)PJJxzaBaibUkxlV^37xt4 znCl9I_XzmEgAB_*K*Cd!#7E)haj55PFcb{i|CZyz2=auW@QCxJZf4_vrEx{D@9!~CN* z>2c}N9m8lPg40*8sD76pcLAUX{t1A|458mv?-kYW2ZRQnLE`Oh$!j14NDR{+Zp74v zEk2wmL6584YfO|+tZg9dRjIJq1lTh zo%zi6&F9(K7B4Eri@ydrXnUVjYWY*I4vN5Q8iZF*i@uiaug<=5svdhl{wyXVXek31 z$C(7Yi*nF=4zlk%B9F;9=xIO|ALi5r$kI=iF=->~;l>&*3{gZ144q%7-t4ZUdgk`G zGta9CX)oF^rQ|o(e|P(wyZ%(oeMH%htJ3n%QmlKk&8)qIIB5V?zD^q-9w38#uo-VI zmYNGw1d&itRh35B%U~SVB`QUc9^rPxwPRzuX;`&+lE3YLd4jKXPIl>)?|#x?^|j&R z>BTO!P@A0l9@de^<*pREQ!c5YeLdC#_{5*?eZwNDiyqB?wl#=XPQbq(#6Ke7dYAKi zx!3?T`bA*G5O<}MtMS~BJwn^e1vxk9@PxkZ1cTsPS1Lq;bCv~9I(l4lPVTB)qKnL{ z6WqV(KzpG(Z}(D?R@ohTlS@>XMZ;})EmzA$nBQYz2uefX9_{MrO>&J8<{*+^S(3m0t zO_NXPU7ESOFPj*94qwrNurO zd^f3`znh@Ak9N?-5mwuz)^xIfK~dq$iDTuRGjBELbD{aQg9ai8m!WPFog^ZPSrLGo zT@^&@U2JNJdtd|tQRTRurXGYHDe*z?6nFu(>nisrC>_uxs{`;?^SoR&VxyZ#qD3TXet11^z+{c~ zl|Eq3bs-L+{A#t83j_kO1{(Y8lUV%9>3A7H!n$^oj{-(V_wr+IK@wkv=@?n$zQkkj zRsT=hI03d%pX+79vG4rwhxDt(A8E!f&p9zn-iSuQd&+a4O)mOLD#X$E-9n^~s_BHd z712~A`49mF{ThghCOso@BoT5a#>2xiWE6QtMIO5TfLfL&%#HjPlt(NA8YVqvXk$tF zx|J>>h^MrIy|=)Tq+K=Sd3GDdw^}FCdaxkTba9-w(SdVGE$yJ6}*kV*0$pv632_}ON7EAHr!fz1vcB8#x(;CRQL{rHy7 zBXn<33aP{lT{fdps$7j$<_9r6x}UbXqP0Ly`%A&X+I0;D)T-?&k)>p9>lw<8KOVSxY}e-T)%C7V zS`L8V z49APm({fr9fA$Lq0<5L*q7u>a-q;eKc=Q{NKUYJ*X60dHe7WehRvq6~)2g{b^{frC zCL58sk%!mQ0jjs+vozA{DPffw<*j9QWX;M#r#L&BOEYud1$ms-WRgIm4@Tj5ODc6f z>t*9_wcO3ivRi55zu}PZy;PQ0ddN-_0pol0P*@1edXJM*jOKI2G7ovFN*1x$U6pv7 zKQd_P<8_Phtt$wHwdxe@%0sm0BuVxD#3h_1Kq9i4Nz*XOCOBR^@BuhC#c!C3bF$k^ zD#8}!h<|X0{7NMA&>73E3S+fZ%*SN^Y;$N;M#)$5sb6}K{jzxaCi5qYKD(j`zvS;0 z{?w=S=~AvnkO%ZzxDIv!sdJ}L`vVj2uO27lvV}T^!-15EOMXeCu(VwfYu%UqST~)M z%<;am%3}xTfKDrgu(X`L*sAfdxP>7f?Z4P@f$ILU%4K4m!1j5O>iZR##i&j{1|EG% z`*+I8?`Ic6ddoQ%qx^@8!K=r}S^J&sp49`ah@PS!r&{!1oaXNHAJ2$ZopgDK3TM~; zS@u-k$pnQdg&=UV@=JSsby%AI8t~T5v8AEh1uZk(XAGra17nt125w}Y2XFlz;J%fA zkUMWSP#Ch)G1IOX|4x-PJV010APVhiYNUk@L;=HO0+|;Y=-=@ZFkNXr6e%reE-0P+ zQ1IvjpPy=ZZdnCgOF@0VL;7Z_ptidByXJ@c9u7Z6i;l~cd(*kls1BNRjXx`DM$=0!a2;SbHv!AmL@7)-6J9Zy=7Y#HV%Wvqr| zDGV$=cm@#4OH>xa9I>F?@KI1WIn@lVDm_l`-btF z8-vnvd?;@Q{oY|p8`O$XfujiYD9rUXy`P}pkoRLf_-Wl|i^2Cw44Q#;58B@zjz;j5 z=wdqaPu>aHbSEGo^*}aXk{NRD^P z>|g<(RsKDgaGv8uPD(m<5(jl-i8T_5c5R&uf9Ht(&=Z(>3(;DaIrZhk0;`48z+Z zUmMXOp86J891+{*BlZtdx7vcz2heVWH_H~FtG>DaOR?bww!y>o(v_Gt(Z6&(s>Gw` zy;LuxxN}KHYvk8h|iv!&8VJGyv%yogP)~T)YQ-LXxJ`PFG}80Fnwv+L;QZU?{^}Fx;JqbuuI(P=P;2z-z_-ya}$gMi|QzlGTOsWmoA&TJwr+y4CKr-#_q#a%iAl|``!wt!dcIU;4H`Gtqlu< z#}fv6>W^6nU;+HuV`Eu{>QwHhx2=@JPhYnYaD;*Q&F%&(Cifs_=|iu5M~%GrMw5U(P< zat}Nk2!w76R~VABir&r;y~{Pm!L6hbJePN$yqyFLJaZiPM{owxZ*l`v>o-@atr<7x zmk4lG!VXuOcxoJQL)TLMw28#uOy|9cn>r*kqgexvIX3u5!4`FQJA`RSxCr1bSj``6 z`aXUQv18$FxTi5(N5sqx#X-Qc<`zogdEBgVWy$DYh#fnwsgFPKC9!Dxfp5v}BI)zv zkER+KR}qOdJhryyEew!*JSN@iK~(Q5SnmmTSy!^1o_HNWPO9c;w{Ue#5?bi(KagB? zORmL;zp_#B31;!5fxbUXg^D#cY9EaXxz!YgX1)Mo3FXtnPl_B#r9x9I;f+$?A&URv zV=jXCc&ZHpa4go>hF%gHYM(6fA3z82f-d(&(~J1j1-bQmZz=1{ETDIGw-Y4Hm49FM z|4oPi54(LI+KMl@3!9*&vsu`OP2BzEYih+S1BDln0fi;*63h&m{bZu|I9_z7T@Z|I z-|F6gKSo2Y4@KIgdb8xesk^k*q&7CsXj^-DpQHL1jP6=OLV5zT{9f*+-L1aGbb{rE z85C5bw_Xa4@#WUs20 z)fvMtEU{lnz#CzlNgQ^HKRv7SfJXRfbZK+JhqiieXoqQfD4y)t3eRn3_&jD|ZS$Zf zL{q2DRN}vFmfhkSic;D-sY$=t@VFKk8vJxui1nElQ;BT!atqU4fLRdw-LO$4CbZS3 z3Z({sfk8p|%n9Z9$(Mt9V(IDVg7s=&KXAU8(i+}uKY<>~n)1guAnl~oV8XHw8$VPy z%%$URd>viH(p~lWRJHfjHHkjKGxAWV@!OJ46HJb)DJV=ZaD3$Kwzl31^BtEj~%P`4Pw=SHrkaM`40$E)4lYewzgFdjl0~f^$5&NzO7MU;}ZOd4=`K& zP1m|ITW1JI{O0dCvcQnESrJvDUiQwXSPIa4xp$fCi>k_aI@l;2~&TtG9Fp0Gl#71bFMP zuCq<~M<-?bT3T#2w0dhMyG}k?I+@2ZR`7{Xo^@EmxM;8iI$^V|QQfr-bOaKn3swq? zpJ$;aiFxJAdG#a0LiNdbdkGR|U&Tx4Db6DRcm3J-jPF6xLwEb3A=w*$ zdmE!Ba;;eo-<-cX-ZBhLj2Lm4D^_x9qo?|@JU+qbM?=H-dmRXK!@dLqUc^;AvC<|k zrf)sBb}&pE(UCL2AAg5Iks7)OOJ7v@H!|A7m)*eb#!SQTd> zMi@`*nd~p#&7=3QcoAEQ$P7!dGp*o=U-So}1rTxdJQI{3k;VBOJ+et8OzZf4<&aMr z+hvr*7U+y#w5ETcAB@h8Z(YW*Ed5*}O{DR^^$W0n4c=*1UyLzI2< ziK4N8`&j7JFfu^|*5u&}j4F91jDw{nu$^BhBV10_anxoU)y}xi5L2EZ zW(6no<4L<_(7i+QrM<7AOYo&Kr|kl>r{__tTZRdmFSHJLU${?|IhA_|lQF_*SWvUk zenQp@L$-8oe791iiJq?=D#p~6qH5KR2O7|a&8Z*huEpi?}-NogPse@nyumXYDAyu;D0Bz1TgG+4*l@v#I4_ zmUcfOky>d}EbY3Y^9!_B)|9oOCu9@VBpcOdrYAFXXV=vLmFVf5q%$k+ZSZ-(`AJ*& zb|mHL)w8Mdlbhn89RJva&Bi7k-j7oyWxbh4N$elDH~Ux)97^3YuKQu#Hib)|%FU?L_|k^L{*-#zKvez?%S!Sn{)_R4Pl_mVvupdq@nYIzi&uXAOzs7dP((mFFE2 z%JaV5dk_~*vekfbKD!R1m)Sz{iI9u{ONv<}jiHbH^p&ZBt6ND10d}`1VJk0IYr0bI zE5k9eQjaxNp%Ykc-`)mkUnxA_X*P=V)!6w7;Sk84z{9qgpkkKtoG7t&-}J;?LVWEW zlyWTN{->P=M4{nl_n7f&#$(4-U*5?jeE?>A#GY*MXOL4rN!x?V8*|WVGKlSfvL#9w zrO?CmrD22}s;-AUZnaz;X91>w$I5;9+GH>bsa&C5y;x_w2ug=)^mFGnP)HmE-O95H z6Q5|~)ARw+T*7t-$HMMJ3{+o`jz!x)Hh4&{{VTt*f=4#RorRW=yHITQc^6ACFp#eu z{hWmax{*rSW=VNVZ#cZrjc|reM+nx`QV%WkkHCC@WZ?Wp3b;V~{9Q$)1^n5%73$+V z7GCe--*x(Z8|D_f@pZ4SgaubuO*bsM#e@1uGkKmgr6o`FW`Ha@DSNLA(m{i{8&RG+ zg!8gjS&-WGSOm~pK}mQ|c8CrqdOZGezJ4&*;Ai}1{liCd6&mx{(Lb0(R{lzei_+^_ zk?6Wd;B#l|9*ac5%PRdZV?0Hy?X#fJJ0Yykx)D5q-cH8p-$MM+e*7jYsA0-6N^gDe z*}FA?fc*4@;c`dOc5L$c4R=H#?33b}pxaGX9LiRBvG;Jr!5^yTOpSZ*$JmX#P2IG$ zUvJy088k`DDe@Vx(0SC7We~i$lSJ*t`?NCen zbiOG8I88BD=dJZyYOcOTNj#eYS*9>K#Tc`EwLPmcgFT{Z1;XyXy49mZ?l@273U?Vy z`R&k{(J)L&uX!||=EWz4lj`G|ml@=)o@rzqXN`Jd3^~vYx2LVot_QC>rau@m36de| zX)C~R{R=RV(Cy7O1{YEjlAPWLgMPZc;T{W6ciPG%t+TD$j!dBrQ6GgWR zJ^RQD5Afd<;e7EJfn9<20Cau175A2xg0B&-D$mD=%&;ZT9$yog%G}uv$~Q||wRYXK zboajuS_2H496r7)t%K#QUhmmvKaY0vmCV|c>2hC5xXt@ubJ6+G<;dKvP9M8ggB>f~ zV6u7ib#2T=;OzSZe+>{PM<_YTfcB7(&qaGN#i;N zdbbOGX!_Gqw2&Ibzp^3bsVW&m-~PZpYEgiR>i5D(w+B!A5H9F;G?4uHk8DiZ>W)HM zPSjBfjY&n-uyq8s!S06lFcgM6Xg=mY8|qs4>Xf2WyX-0@+bD4#VMsy^g+T_m#mAYH z!{hA6<5M?`@^=Xm3qTu);^xC+|CcAbN+$+fVbVuspYl6bc^0i%P9u5Gj?~EKx*e#E zRCUHY1Q4%E4M?~6M>zr}YCkNAzDi}Jd6#o$zJ{P6Ata0wK3p1`^s^OR>(~}$%3hC| zN_{*pyr2fb-z>?an>=eZQ!5kQE-yMD3bsUieZ!gzD%<6 z6(-YQTX^74S#5f8nfcFFx;RdI4~?bRG{X4G4SB-4&#p;lJZTDRP>dNBrnD+E4K!I5 z@yYdo)67G<>zh;EU+oOX@!1yA7ebi2^Ig!!Ov;)oPDYIt0u?!vQ#qVzlmWyYU%hRC z93$vsPjTm`+%(kdgFNK)5`*pUQnNnYv-LChQ2Z8c(S^z+4lhJ@_VdG!5qb^A&I249 z+JwX?@*+EuHs(xs?$&B#f5Rp`gz2Hf=Xxz?29YSl1N-A0;}C1BF|oYJ^W~heZ4g}m zrl!rr3VM@3rl{;yj9p4cz4;{PveT?lyt`i@T9pNl?+j*(FnQ(Fl5QUtJ$>(-56-2J z{LI4{Xg-%@w;jJ$RS4Tb0a1FM6yYhOec(1TzNSs6`WNk>)@QKfQ)6GFD~iTtk^$dV zk>7gu+P!->YtUnHZyGlhv4mL<0JOy45Sd^;l&D2c^JMRp6ZJgXIB8rb{>-S=!+f^6 z!MZ7aDzAb9OHfNCbE=t=Z-K=CTtDIdfnj65s7{>6WM%=mSCWm_xMRhSY67CRG-aOt>-DrtDNUVAsuZey^A;xNl!gC*d15kD1ed14AN7gEXudO8Fw9W5b z45Z(h_r{r7VLHdUp0IJHJM6(iHdTd_K11!)7YdU_k)XFlIQp8fx>(_(Ola08LVc2s zXT9^xybWM94#h|xR4BE5yhT-+B7iw47^Ub)ekSnV^S6?xP@}w>!mQuE__&yZ%2IGT zb>XNP?&70v3;dDN?Uq^3(_wDaU#}3yjr*3j_1DS-+ zX+vVYH;S4LJD2m$e9Whn(}OASJ}C=0*q7m z-%s&0lj{V&tE9rdw)=^2krZDuRxir>uVJiD?4&ezxK;szFZSg`s3CYvl&wgf)ppR9 zso#Po^&b8zGfr>WTv=ye&SN#v*LMEdE)i6bGE+GROcBwMBZFRB&xdkr?~gHdGzqkg z&4HfKMGVqI+2|4xm{GvdW7i#~X<)`<*HE8{B5=xXUoaxjcjYI>r}_Y89>u*G-~o-w z7Gou#PP;i1?Q}oc4H6~E^<f7!r@ef?aM& zrbC(xFhi?(mB<{>0?(8K_H&MZH96V3ZKs`JU-c8lgkC==VjbCa7!t5NMI_+gTv};= zQI2uq-ZBQtE1As2hBzo&X!3Q~b>uiS2_$*azeEn5e5b|$3--iw^JORL&`rs?4+h(x z=?nFj4Ah0QILd3LFjl3oq??P=hXt*f9m2^2@i7)Zw)Q{iJ>2l`o7pz$E%&%OeuQ9R z-xwJ4(y6gFMws!HWE0$jO@#)<;^=n;R2`C$_X4;ps)J+z(e?gC&V~FX6gl1tEK}biWpI3pAYSaqwWWAw1RFM-FKH6))LI9eAdCbBom&4|RAM4^1$ zan1b2w^9y)JCp$#3@Pd-!3Jkd{b&4$KDM#3H+|;Odx*V4g@R2L|NH(arv00U`w`L2 zX0C!8b^fN%P0xxSn9R3pbPfTVlNk|uXOR?gYUXD!mKI%<=SV}AAasr1dL0fW=B3iK`FE(LfW1G?X2RcJk{G+dQ+-GRBiu?o+#zRGLJ z!1)}7eX&x4o?dz89ke@N``|^r(^ytJi=7eY#=eiN1bticLsLna~DIl;1{p zRG(8MG>x4*t+yKuss?TZ`+iGh@Fy``Q>fnJ>+sP&&5u-kA1qkbBtWIO@eZ}v&WLbO zfJr+NZzgJ=cj;XSYPFAHp2b81s}}>ly;HOr99hAAZxG@EI9S$)cNc9!&>Czs2jf9a z4-KmF>RVweqT#WK3=N@;q!LOt;1Jn=%UYzZJ;Cx#~F+S#u!au@hGl~0P6nnUi(L`>r;pi;c^_UDZtgt=)0hm(|WASeAc1|yqSfl&iw*|Q5 zR0G-{D>v1{dF*B}p?QT&pa@`I1qm|%!xP93B$C;kLkA!sz@W(6O2Z-T@Zav6b&xQz z^nrZCo74V6vpI$}EpE(ygornmZ%yI;+`%iG5i)ZMe$@2F8wReM4~dlzNJ=Y;u%%!J z4?3zG=N9~UQ{2Xs0(Gf!AvAftvdn@V`J2Nw;^v#zwO zhi)bNH3yz~+p6SWbe?rYZ6e44!|$0@3UE89L|$OCXl>fWP8v6niS5n`6)<`c_1HXg zU(Q8uSS63uD12<|I?$gE zUf=4~^$0FlU)Aq^?bzqJWUH?ImE0_Pzaei~r_fMJq{T7U`_1|Whb#JDJJh#cP-@7? zz;9kUtm4JwH$bssRZj91Y-@ocmc3QarG)N1#fDy^)-G;em~O|aB&X`D#jjK_iObLcYAR^`Z z10~&Pp%d<;YbJMMd-b2r3OfD^{jSd?#ss(fFb?$1cTo!M8=h((>x*;~bUkQ8P(EMU z1y3)~$4xg&Q%bIzd3f=#Y(ALTbL5z9^ib!nrhK#rZw`?a<}eU5!L)F^dZQHDyvC@} zSzR+L@+=}^?e66bj{BAb3E%Yl3ab`Mpay=wu2EI~J)0Ya5!q4!sU-bG$iGhp{aUpk z<63_atFlI=Cln7(rELmdxv7P?oMB^DZ4MD1$*|;kf^m4lNnbbaiZ*}PHt2)-I%GMn zCR9KM%bXZsb#J0wg^$CR{DySRyuuM>Opf~C?My_a%KnEH@i}Usz_SD<_u1E|o5;VVK{Ojz@brKw%w62p za=sRozY)BCXBEq8Oz&?!@od)N2RcN8=RtVaCpdN+>z8_`RWUzT<42B)di679&J72R zsBh1phOD3609hXLQB)t@n4fS*`5^iXh2_Jfck-!L^2d%jV!;Z2zdaW+aaWNrj+#U% znBP#UV+W$^-#q(8QwhoU1X4GejX!1*bbJiyW`_yGd6mf#zg5La!1nt+Ae7ae@O!u} zoYzmCxnQ}>LmEHV3-{)C^~dZD=Ef-XUe6rrYOdT5er35pt*u^jrCHoR+2GX0DZ9SW zscK{5MPDsiRoy;1=;gt!8wc(b-U#l-X%S~9uo_Ki?Iz^K{?o7vO6uim28mSKZIdC% zb;ci&bBO;6Uj9y;2C~EbwecHPHAkE-drsHg5%!H0MVm7nU1r-0D@N)fMndxDny>rK zf?qkb328I1w)iL=g;*`mB!|r96 zl;bJhlZ~Uu|0@sqshk7<5?~M z9o!YaH;T!28O5HVRqi_)?ju+6uX)Q|i@1g*X9Np!)?Wd7njube+ zO8vIY+ly~mTSeYHe)yeD`z;I!!0LhyMBOMmhVtvnzqk*VgE6Z+uxI9%(aHGTu8|Hg zHy%sw7TM2SVW!|BOsW+o!=EJa)f1PRPL0LrL!>S&iEdy(g^l_BLNtQHiOgHx`L1rC z9rir!bwr(XDheG@n!N>+#+0+a7EMU4OLmlzP3sXs&qYPP*tYiK&eEx^Y_se3=;Fef zAlGH^4UH}P*tM;oPW-Jf@(w@n70#A`(`^f^(aW_ty(FI0Ek4I!9dea%sPh*81}Yz& ztb zPjveHw1ISkg0*M1WWuGy$u4st_{~3)wSBB>8>SN7#M4wfcQ)X-#+M3eJqn5@U#>cz8Lkh1nbx)6A zGjJ7pUF9sg_ICI=hQgE!_nAlvl>@YVc;k2fZ6omTHRkXqRR=_Lxe zphHuluxlr+7^nZ`hFD`DF<5h#0*@q;uA3tdYF-N$%B0~5BFAq{2;LL6TDn_Kj))Jp zO}ZK1dRHUxw^{*>)eS*jd$NN2$l-mmp(9E8n($xiZx+an7KUp#*?Bfa0=itW^}5R4 z873=GForhkJ5>a?wo^j!Eb50xRymB1O&`ZV@aAAvpT71$p39BMDkq)~u2CsA)tgeJ zwfIEzKcGg*DeGp(T>TVo9?dpT_C7Gf#=_W;SuNJfoH$J69Um55xqIWa+;NGrsb`h2 zgEu+uyGo*jrzWezlW@V%b*+|3OU9t!F15s|@VVVD9vfi}`tbe{`KNW@`JWdUzzD{d zpRXgLtdnb<3Oggl9a9~8uXrAcK*W8az~P{HBx8fT)lM1``?ISG>4U7vf(?XI#!uXq z4O<0jjW%!cMPW^4O=hg(E!SLsr)ml63Sh`ojzS z?-v@aE!7;g#|N$x@xbQxK1}kJy`-kYTKD7GH#Io>i*=Y|MkkL%_Lw0&+n`4W!(d5;bk`XL&> z^07)!zV=XtZ%LRIA}Y2i7d$#C#@atnvh}509cDju&BmSHazEd`+vdnIXLPV3zcXKS z0AW2iz#`aThgjB*>MJ7=`WevM$$Dh>97#40$iUju<&(0U?)<{*9+1=AR=4x+ApYw>`UrNVJkUZzq zv6R3zWqIM?CZg7K=kC}@K@r)ZNi;EXnC79gd~-b>UwOGOcAgua^q@maECG1#Fb8kx zYsH@~ygiLW9VT-K0^6;w*Do2!cUcDs_nOxzCi*fJDH1E^cuuC0&i-&8{R-f!f;02A zJbXH%f5ITp5b=)c>4qZO!{aUelFV1HDzAdWMbC(>pxb`iRT?G6~TT!iO z1$=zWif^A^F7b*kN)lD7IP$j>9_*f%rwdmy#si0`n}m8NELd8gpG38-j)O3$4bFTP zIb_Q(6&rb&&?eb3x3Us<4bM+f3cWP+Gb(R!>|kvb(EKqo-gx_yQnp^U)hT~y0=mkW z$H5b|slaIX!olo5&@-SClbseX)ev)cra)Q&BjL=`}j9o{LM9 zjh@iePEB4l8N_b3-QW0x0)|K~`1M$ux0#bAoT7z2jte07O=T~06`3?0xY;x4j;eOL zCjHqe!8=$QF+E?zYrXhf;klJsyw?8B@*dM*>!Y%+#G3|N)Adp^wDT-z&r9TTHNUE6%tu|vfJPQu{2=QY4A7t)ae?z_LbOCi=q4-WB*jRx`*^mnPu;oHdm3+MY_~?NE8Wjt| zgHnEWpB8r&x)JxFV6!Q|woG2d@{LTamS@hvTwbo0LsZ*~a=GJ!_rK;5+nIZ0=aGjA za4ZRc==k_RJJ+4)?y4{Gx6#Nu>=D7U&ZY#yEl!Iez2hTNhEHnu7{u{C|AgY7dT1W5 zHd5Sl@dS7GsFYdt@#L^Qew8wsw6ea3z_rqyJDpUqk6##Y=Z`+!UpMCs=wanRqA{;` z;{W@tC}3oZ`gw6-0}IpEv-cex_nP1~>~v!Hoi$3S?&6+0u_*a!-;;PwE5?YYt}*le zF|r|WCchwkCulvv(do?^?d7a}E9G`%6$^r~BtT6%ucW1|+vcT)VC!6yf~d!6-vI5% zB|yKJZ7Yc5Pp_EaLivx}D0v2BOM&k8I*2*7&V?MCN*y*jT&>m@3M-;;Yc?DAu41_R z*QVO$T*}~*arLNCyD8AQ$J&^Rr+;+OWIw{X9IOkkP5NT<<+o_o;w0ZtCx~KRok4qFRF_r7}H4IBd2Ls|D^i^T0Bm{UpIr!bN{0XN857FwT3#qDhTWmI~a11Ti z={R!tr=W3cf8+9o`sIY}<3?Wn#ZQo}fxMnAk2pf&{gV;~tu2z#!Ce1Ed&)+xvQ;TN z(X*}GB!ewE-Wi1VsdX}xh;?K07RT2ce09@*njdcAb*{N&0))}|R~=#?dQgX>M&Q2c zmSIi#eXB_!gBmAt@RLgIYcD_@@s8fvg<_0HeSVx>WM0v3CNU>Y;uv@?8Vh%;f16Z= z^0g-R9TBcxscnMUeGMn#U6C;`MeuxplN(qgJoxd(+SXSP)=B4mYdCG1muduifJ^!` zL5&atlZzeu`NW^3+&rs>v~Uc_`ohs0{G=5d$k2uaR=Xv$Uti36wE{D$T}RcyjM3Cr z`|*RWO;^Qag!y%SjR6q!V9tCoNw-2Yd};8vbhyxnJN_9 zpZJu&Ms{Db!B)$XZ;A8g(NN-1^Y{5GT^SG{QK_s@lFP%txgZ~SM^4RHFTbRLn*`5N zCDe-S?aPeHM!7u5271YBc9Aa;>ONhUMutGQ3~V_zxLa?ubu9Mk7uCAE zj=@iD^etfmu+){zs`Ztss#B`V=|}O7AQY8q@+Y!PAPlpqe~4ZQ?X`U?9)D+uq27Q2BV#Cd8B!$GswP>u5~$6fI{wtn>{Q6bZiDkP|R1g?X5$mRyYrSU2gx~22sw;Lt_h|9=B#j2 zXn%(Cz4#8vwYztzxY30D({LKtRKh1{mVJOOH^4%&o%w1LFIaMufL^3E9Y1($Kck3Y zCDOlU4rO{C%E))ntREl0|4-8}TaPvjKA-M->Mee18Llp-=gD95Si zCloI<4lJSLTT<_=;({Vujc51TWcl3la0wRXicDTA+;XTON!5i;tTA#l&E1fcp}k8% zz<>g}g2$?{VDoChZ#*pG#Z=&UX2}zk*kMYugx>kK$k6@3X-ud@0Igh0)m+ET&5d@; z|NQ;1sYZYW^r67BBf%enRx{y$ztaEf-Ht~TOzPA=O_$RhkxT4$s0%8fxEK)i^I8NX z^V;N>0F|;2NF*I#C64^}8x_t?xp8%0HIu^tig`d8mfPj63z}=-yrU< zq49Iv3^7@m43F16y<%TbeZ#rT>S4d%kimUk%Fz)wW4JL3IYyRXENCJ}%g2@1N*YxIhh-4=2EU4P1pjq7UjDQBc%3#so zyDzyupE$yip03REN~W58YY{<7(QG3g@z^P#rn6-&Ok@ML4t@3oN}Ebr5$!ZvWqZzERyXvbvg`^Jy#3nj6A9V|w$` z6ib7PFCV+Cs&q0gz3yLKoE<-w#hpMKDLl_Ufm%Ei;1I?yV0VQ5_o#C`kd^1)V!viF?Q55To?{>9?@r$bZR zZ0qz)N`~+I$#!wD^oC5jkqEm$U)P>i>5UH`Y%MI79op=3GAD6T(DSg=FPBO3RU1;U z-Xbz+tlmsENtIf9cW~?W*BYWwfm**Il-f`T`VGC$gx|%y4J=v6tnG}@uxm+`! zZUJE&)-8G z5DH}JT)H=EberSdDwnt37%Zv+*X?%__(-8MY3;Ztu@;={)A4%xu&T>Xa{9*J{|Q1q z)Bn=8Gx0?K1_*DVC5;^u`r6-1t?HNdBA}Q(NKN%? z8gM`y7?@-TYB$dg`=yyN2!`qc@^cx=Ka`rXV0VL8nmWW(9I{RVG#&vT;~R9-?MTEn zvnkk7=wQb9$br0R@QaQ4ST&`Qf7tVbTK8ewX$CmCNMMbBxPj})L-TRhKbXqj>m9dS z5ObMui?FiRA6Dja%%-L@#1|3bi%x~1F_r5dS0%($J5*a|s_U!&8FCoweWGt$_RyS9 zDV`Fm6?0?*ookhlhbcl2I2I7fHF!F(b<{^YYJhP1PaXgWh@*~-{~+(zr9D)Wx( zXBK8VsPq#`^fe3FBq?Z5oB9ll3%_DLPEL@pyhm!+6?Ms$aC3q%G05VbBW}iTikt|& z)0TV=m_ln<(*1dD-C}2yd-qadZ_t!LKCi>b`}&@Rrm0X9Gd)nOtXz6@{{hph{Lg=d zer5hKzpI7=sI*>I>q!B`C=(1*V0;QtA7*8v8FZ-ATM155zKSc{J%%Q)+xWP)h?g1a{@8O3A}TiT`DFSHL)`-rIdY$P@6beUd4raMJg)a zhKabS-NHg}MXC@&uEkooucjHGt>y0i*=WWMr=@t0VgeD+JgItt|5}UE=ZcCZ#il02 z9Bi7mjuzbVJsLphv&r?rn2)nu>iXdNn-q_AFJG_+jcrOMA-l9}0-k52wz=0h8dzPo zltIe3_{AS_Z(jxPq+GFEfL3;*RxAQ^-wURiw)m@Y^G^|EiC?xM7Z+_CNn(B4;u^*|UAXc2O~*WL zV}k}OBo)An&}IR56&?9K?5unIW8nqSmaH~+R+?OKszeNxEzqEQ$u1Y1RZB#M6}<~l zT=e?2Di1&{VI#>lO|@2obpy9w<+1qXZ?8bOI$J-@D6Qy4pvsR26NPimX=j-7mydgN z&oi*aK96UTRPdfn($dJfq<}5)t5f<9=|mf#>N_G!0v5a5MD$&t`0EUzOMtyitd=m- zxOLQO)M+`uQBz9JB?9Y!-BHI^s`iszth%eJj7(xfj0Lxy9*)sq%#~%YO4G>;?-uz{ zn|BmKne2X%Hcky?b*2fLs~CEOp~X+*1)W*LA^EolD&M14=GkLHWuE-d!E)aJ$Nd2` z88*^F)TnOGq9Bh{p_BZ|D>T-SO_$=S2?IAs5am zFv*-sfKSs!r+_V(u<*%={GonCfD4_^eQG@-PNA8XCpFi{UCULa;Ahr&(^tvhh_r zFutQX%4<3Oz^vRoQc;V&hp257J=xijoFHGB0E%x5gH5&UhYICn-$YONHCZ95@yx-J zfM1~qMY4N0DU1WgG0>5-OEYm4S)IbA--vnIGZf2aFGNThFKIq|Z z6-$Q@C4JfpH3@aUdl&y#gGyZpHhWh5?wXE#!f|-gto|~nL`|$X+I%}6c zOL`qB`Wr*GZ(Ey8>`TxWv3XT@D-C13%fe;FX&dAB;M(Z|LYzIGK}`meT{~%s1$w$+ zpo*W;)@Z-J(^vIr6-DPil}s!olj@saj4vvU!+T%MWvVA~I11K^oabvO^~8eN{5Y3v zOds@F#|t?2jhg{te?RLsKnMO_qKg>G({wQt8h5<6>uI^LLs}zxBj90ib&<_2;4e^R ziIl?|I`96zqF=9r1fWUhxNlj05p&X{u7EFO%j8Q!&`fv(Z4}uklD-SIDGViDs80R@ zDF6*}uClX_n3>O#xrR>r*4{^~(UJ?(<2`eH)`GmyE~ci`0nPfGzd-^D3d%5fKvMfx z7?gFM=05QrXm2y-4W)o|k3Z}BT|)b3;Q~k^DqdI0zC|@6U*#;HvUN{FticO;ZT{1* zI9W;tTte^D?r(jlGbA$QWl)yqtX@t$HuygD{#`ozXUd6`I45|N{>;ZvaCx488AYKT z^+LD&-2{+^TL1f66*=Q^AzuBg^IyfQf00e~w#if&-akUF6>uTZ16S#P{NIs6O7u~P z0&XS=q+UjGSg#M`3Y%2`)_da=P}rgV7}@wwl`8q4bh+2t@yjvHYN8b+Ecz=FfY@32 zgO!Xc+6~cso{^=j&%Zs$cSSZTplFr2Oquytq8w7$H9^@DLV{5At8LkVsrz!pjCU)x zDsMh)21JkJy}s-|v5nZB7F?QF-+V$Eaq!&Kn-kDH`MGDW&+7{__XusoBw3ODn!m_gTL9#kY7)+zBKllX1jOcsIi@|B$ttR8$;n_zEMCFQml- z)FUMC58!@D^jN17AMsZ*Fmn_2zqT7U0UPePVh~-9^xsw9e{naTJz*fdtc*iON1_*f z#3!EYa{bNz^@KG0ge|BdXGpQMziY*Sj%Iz++({HXe60HMB zbNuY2qPN&eOshPhPBg$s%lnk;d-#$Q^0G9qdF}J-TYmgeJpTt0}AWq0#W01NeNzW$?n^`JQ;VV-6(%+=usEFW63_{ zrDh*nz-FL@lz;j8y#R_Wu>X`?WXo?jZ^~uQBu-zd? zQl|WpzX?#H2kqwe1lpv3l>jA0YN2LJpsRNdHT!<%UR?~<)3Ej!C~vVLKo$n0@5OIN zJ20og_l%C-L@D!$Q4PV#5L7i*2$=E@@0plGJYyAe97lZ}TEw%F_`8d!!C+s3&?{TmT!{7d{~F8e{%^k`+qZgLPH zn-Ih^|LI?jOtSe_JTPs{D8wjfU3Ao54w}F7X16@YM;u$dX;mgxWeELoUR8^YeU`pi zW+`qZlC57<&LpYlv0ppf(c}=;D-{Us*&dCAH#QwP6L&oOeCvF7rz(8KWzv)KY%;D! zhjMJRz4#KRMPFD_F_vWj(7#8H(cB$95Np4}25B>~R#PMRG69mWd?F%vm3A!kTIkbu z^b+dZ2<#ndaHxPPpHc1{D%3n&uyg}m2?b>7P6DO{^xiIWG}o8}eTf9Rid|)^yN0cz z#BCkben{~pl55>D68X@JTG3RDXBr!R?|w&KD`8?B)1%I(as@<;b2M0J*7PAr^7Rj0 zY;=gn#EL%=Kf@cve4^F)t;Tzf7ioUe1PBBSo|G+1ajrLO!Bh*5p>`DmavAQGo%sn8 z(dA85 z0aA*`IF|PE-3QDF)Son87%F^G8@E$9<%m%#?Kh_)1m0~jc7I(PPhBz_y^cD_Pg}7z z!K`jhjXX35e6;-YDy$OI&Ilb^cu=|p;}<|d&BpEnMWuD zemAZptjXt*o=Uiee-~1j_E(&wPNX3|t2a%D{1B*aCHGRkW8<7QQxt&O=J2Hp>9r7w z49|fTFT)kT!`L#9uChJsjqJIsEB)&tySY#1h1%7eka91)2yl#>=4pbF zep5*)9$babd|abjzPZOqdrWjYp7C5$K(( zp66*5-wbNTNiJh8_gF9|DU8fsf)a%y*D4DCZ4HnO0@~2{$fSC0EB-~HzHoy)wtfhE zlS^sBEuZ1WFsFwBrWykUmVyY8MMA?>Sdbeee{;w^gi@X>`i zjnf_KzE)rIu`Uo=8>n~!*iPI?TOV9u{|bQesl7mh(fHWGPx;~hkduX82R)<~1LIuQ z{y3fMV$gopUKnhM`7^MfN5TSn&2(%03l~H^{oYcZ-g#<)Sn$-FGS~-(5A?-=&GGdv z^6c%ArL;itAJvkxvOJ%D&wP1pGvCUgz*BdY>8)4VlbG1V$fgk(21c#PgfU%Cf^&FTw3C7g|dlYc7h<00K z&$xyu=o#KQ=tckKrB;0Tno7FIy1^zV+q>{J`}~lRHEM&~mG-Xd|DvRvuw`{uYWc{d zqIteqVV6js(iDa~u*1UsxX(&1|M$(bl$|SpK}wK_cW_@tS7yu5Y``~rox@}3sn(Z? zETx_Nr&jffmY(*ZB1=cPXZ_kJpa zny|lDCXzqUi&?7IX#l#kV{;+;FNxZAx2eeea7%9i>F4rI2_3m0lzMxBB?{**(%c8! z%_+wEtr{ZY)lJ1V&3a&Gy<2u6P!2H=Wk_#8-gI)8+Z%*{Jx{1qzG(d*xpXJDsl&Rk zrM5O7(yJ%*whH@2`v0Ts+7@b}UjDRtxILGZmBns`C&r0pZJxwuE#{1rw)$rE0i@+4 z(!@x8b6-A2rR&<3w}nBT&ocf8Qyt(>AB>OCHn4&fT5$Iz+=~)ObDrLN58QM=c7z=T zb)?<23gBHcZfy;iAWYgn{N`P=%^bEK^qHeT2u|}uS#S?%A^5Wcx43&f(p)F=VKV2i zP5;Ec=%;V~3t3lg-&4n}556Ky6Hj0wmPJKAE5+FL8P!lzw(Ec6af% z;J?%%KfVZ3F6QWy9C8?VRqU~H;J0C5n&u`Q!m2=%AF|D~gSp4|foZ#}kY_Q#mpQ8~ zRr^3Cc2 zzyj)34cj=C=Hx9}R735OZ)~$9)s0!(nbnIij@$`9$T^sM-2$4q&%%CiKmRVFNr2Qi z`(+bzhBOlrV~(3-4ZdGR!lG=1w@*>})x0eXu={zYi2^$J!J%k-2>70-F+?t8wSEdt zi@7j*iC;^}c+CKKbu3;DOgYUlGw^V91*y0iw1>n2nLQ5Mt0Ex%iE3))eK0Y zvyGGL(CiFjmeGqbIO-hJy*+R}da>I9s~boXel)EBo!;N2W|x3reSX^SN}HDZNv;cA zMiDBscux7J+B$@a9dNErz!~bwhkHf_)9m2~u{hO-hvbs7Z(Ditv{@se^!d*q_!q$o!si21qHY|&=1jS~dRWoP5 z^5d#@J);1$&iw#m4Kvqi?&Bd9BdJObFwS9F+*xCJZ*E18i zNK^4ZZT_aNLD5%}=)`AJEj)d=xjAPvYWk3oB=>}rM6P*T*Pa6RxG`C33A%x~(2qj& zYqwx(l+l&XUf%`xw6XXo-^7Xw!gymAMH)`QoDbqr-WO^BhIKSo7b4oz$-RdkbkrvK zj@ls3Zr;Sa9I93~wg3;L=<4cg`hvQSPAb<`JnN5iKBYs>Q?Q^Re|4a@y<1lfjxd+L zq&5FPehq@>BX>8p#7K^F+8<{Z`f{lTa0yhAVbyhRwJI9xhWW`3==Js#4jW9X5P&r5 z-xAzzvk$H9birfsn9MWT5Wf$#F;A_w?I_<2bCHVmKge4jV-+(5)OS(aZ5tlH)GN4! z{XV5)*v&}G%1>71JC+BYmfPuuJF9F_FE6%mF1&0=Zx-8hS_?C7(Jg?@9eE+1R)pVoTe8h-AbD(=d5;uO#mk6e8F}MPLNi@_ zg^Y^vB#<%Eo0?`V8#^s$9@ERbV~`wV97}eeKWd@6J4e}`H%jhKIw~~e<6~2CjhgB%?bkz{w= zo?i3=bKaLLbhd3_ED@ZPPhMQp)*nGF>y#$=U^Hle(bGjhYn(+p6#Q=cjh|1&k9b|_ z91xjym5qa+mM#GhKl%shPp&^%E`>=PJvb01k(dx#k)`TI(=qyK?LMK zQ3LTGQHK>TaKb3HKpogWY#0R^$Y@Lij1NKqU3KcZ<968935?MBo8f5HEoouyw^!{*ZOi< zMg$~0#%|F=L$EsTp(VU;vAp}iJ=}|h`_c1Q7LuS_{v@Xz+EvYr+V&VwwV~k7_$59H z1c8V(nh@aP@|-$Ta+(>?XlG|i)U=QBN~q2SgWg&HUo!=gk16sTQrUNOB3ad`!|xzr zUv8v7*zDh#3@gDh7}4n7+@fau4PguhT#lqt2a}$!hS4nE=Du)x2xZnbC{VN=$CIJW z$l%kpr&r%|tuvomHd;}a0C3sw(6OQrPLYhj*3Kv%)m`DAb?L>c-c1lPK{`eCe|$<} zTDc532R)bXlP~dAXLZ9P?R{Fm9QPR}7Z8HXbt3@9h9*bTz%7P*{4_y+k22EO8K0$~ zU^$=U(WP6XTw;wV;a0RnOnaQr1QO63Mm4e z-0GMWtd^+U=w=P389M7Gd<48k;gl^8DeTt1gzm35cteDP_YZ`JyAwjv+P+!nfc2`) znkHcV^dA}yIa1d~$_ZT&sp}D&FOM;gm;Pohis)vgN30Gtqe=1+lzyuY;g4kCKei5X z0Qv@qsGFks&B)cKI}c6vmPN_db=Un0Le1uC)JXN!tNoUSRyazX=Lv<}O<06%9a&1N z4tktY>z^!beJXQWNz_f=uc?!cthQ{faTM_pI2aFe2(@e#6ds!y?rSv00fbtAfm+C1 zR#w0{%op&663XZ$v244!P$gK+9G0K!Z>5&oc>5lIV9);Wacif-hX@8_&6T|c;I_Xz z#N+@@n!M%bdr8(?t)i$)+kOs#t;7@oGJDo~tq#cU4bhLp?PBi!_`g06+3~j_Y2+0W z2>{OYDr!`^N_+vU6fuP83EeyuJ+~r{25-769y*E_a+)Th? zZ7l1mOrd#2d)(iOMpXL1(hpHkciZz2>{&l>zRgy#l6g+5cCsouE9$vpa567_{@3|| zXkAl8^opJ_48dO1g|&2G&a(INj2`pFiK|^l4Y@_qOD{cdzYiCDuHP%z9ylhBTMh{r}ov_Wjl zAuI4ENctSWgaqO|>YK~??!OGOs~H`=iXR&wCvvo}_w733tI6P%xA+hsD_))aq-Hcm z^dpADV<#drKb%i%6jsme#(0(z$8Z%UowzIy7ufzk-rh2<%BB1NKL#ow(j_I`EiG`Q z8|en=Zs`)FLAoUbM7kR`pmdjXcXz`ke%HprQ}_LS{r~5F;v;omGqYyRnl)>^KReY) z7r*VpY_)j7brFm+Js-^y7-1;ACGR|Ni-U!_A#M<;-Kqcm0ZEeRFX}qTQc>Y=uz4#( z0{r)L4?lJ?-(dBjataEzatgkPH{VCD9&Y85^{-d0q{3^`gKm=mnkE4*HRQnzNTDDQ z{6ks|c*^pBLug@Q>V&O$N)VK@8OTVf-m)u4*qdp2=fEt{CqKHU`IvHP<{i|R7wk)F zIn*lwqnuT#lBv(0S+|&;)Nx5)KY*kov440#6;Jxe+cvu5b@H>Awbj*Rrp* z)Og;aPSrD?yn|RvPQ{(;YVLRE;P@}EO}fLYn+LSRCC!h7jk2Un4e28bq7lGk(WOEYRi|v@$}U z<&lyS)23Ys(teJ3u;0G4U@eRYlVi#*;hPf)sl||6iq9B|h!mq6hGiQMd|B<(K{Fo& zTMTl{Q&MLpjb4m3;=#V5OXr`MN(f5S_FO}hr01cHo}qXQ%HCe+Hx(>`09k=o%Qn@y zgb1i>VH=3q0{`>|lfnA@#8OsPHl`G?2v62B$#~Y}X8@}Fhj!opidV;+Y(ga(UfXS@ zP2kAWS(5J=;~X?#S++VP;D8A8EYi;w6}g+)h~ku?n`FS;kwucT8O%Y% zH*<~u{+1(=sB)A*QL|W7N2?)ZTH5s`$_6w&eQ#kwrb6t;RB$T|UAP3`^RzAk`iVaY z;79T8BJ&>g#ri#aDLh|QkM=1nO%(WoEEfy$La#Oa-Yv5NZ#g#h4o<2gV|S)ER^IH* z*|I(@Mtvl)9XO0vi^zDt)+=An#rt@?|%|Dr5ou>mp^I8Bp?oT=DPzFs)@L}X!#uSleZ8P7>~w3R#X#&GO#of z6=G@$2V-UnJSv;0#cRoG*zUo%RlQFENDY6V3U;R;#oR}wDaq9rh`s_?dOLXr{yLbz z6)+Ky*m1DuB!}gJ$mO{fnT%C6AmA_T%|TK(-*D)Fpl@hlbj26Zzy>}}|CD}}IX}#1 zuX@Yw>}&hrb5j>mnqj)ANvAhg6-9Y#9MnXZ^vORtSo83G^T?4aZ4MR_Hfl$d)^=mz z2&XNP1Xau%TuGMa^VN6)J}pVl$Isv_mbddiRVbL>w>@l*-OQiu{3xFh@5Vx*$K}gJ zxHcCa#PW{wlwWLxefy<_1j_og4orCKAo7ru139v6bhg$N&)Y;9FuXI)>!5EMaRP_wXKJQqkwdA%L|qLx#hPo)DeM;17W_w|%zju5k4!DLBccBLRZ+&2951ZCIRXZal&ErDPMZ&V~jf z_BcsRpl%J;?8*W@E1NM4g*;7D8{3zi&52c3?(zqz?`k~DY;bu$i{{ToPLS@}=W5L4 zlDfWmi=0$H#7|wIErmPu_5^ka4usnQ=6+4z0|3-d`I)4s@~tWDB!oI{h`DUV_;TGA zbL@-}-mqIVz{PC-6hPJsC;RP+ipojK$=OnmX%C1Q!(Z~?wL9LtPaHQyZB}RDY8n_W z=SlN+A3BM2*dJ5iGYJ7wxZez*4`>0}qxdSI)ME*}c&*oGF0$I`5mQJw2Z@rR&8~}U zOAV01JGH=nYIBgW*a~Q3Wm`v%2kQ{jzLWC|Rt9+t4S96+;ocJ#kSODWsp43YLPZd(Z)Ka7cYswg_TMEQ~2o5V%nl4WzA>AH3cTZ zZVz#K5r_lMv~^nU(^Z=rQ(>I2Kk9tvnSfXOCZXZJwr+5F@8%R~@4}K3{Z5mu{R(Xz z|L~Cwz5eIw748EzDw?&R(hf(>p#vI4212~~>cP*gw#PA?P_He+J~}yL;Ex9UR35&5 zCApcy0?#gFGP26LHima6T{TZoJP)LQ%V|$ROOfSHFLC}U8h~CrV`> zcl1oQe;O4Q8hpLec1g301Iv@`%=H3iNBBSkCsHoQS@l5E%iqc*F#JU|I$J7JFv%RQ z?mTq4LWQrcCU2U3dL30B z!$}^*}1+r-t|SW$5O0vP}z0)t&G*?J&oet`}K+wRgm7XIiApN(k>O6^>&y zkS5th_`MLtw7)Jc{dG)$Vsd6i4xr6U`a)gZY#^14qQ(Yz*qDz&YbjfcDgXLff3^?2 z*5{|hZYTGsZ0^X0-+yrxicN(*jhdQRcjwI(m`Q+38g_kp{Y}@r_sq_@WTD=<;(

^fdzUh^}hrAu&xLS@@ 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 literal 0 HcmV?d00001 From f42103640d06cff6d2398c20b96837f90dc32038 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Sun, 1 Jan 2017 14:55:50 +0100 Subject: [PATCH 22/32] Fix add edit apiary hive tests #139 --- .../gobees/addeditapiary/AddEditApiaryPresenter.java | 5 +++-- .../davidmiguel/gobees/addedithive/AddEditHivePresenter.java | 5 +++-- .../gobees/addeditapiary/AddEditApiaryPresenterTest.java | 1 + .../gobees/addedithive/AddEditHivePresenterTest.java | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java index 022b67cb..227620c8 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java @@ -39,6 +39,9 @@ class AddEditApiaryPresenter implements AddEditApiaryContract.Presenter, this.goBeesRepository = goBeesRepository; this.view = view; this.apiaryId = apiaryId; + if (isNewApiary()) { + apiary = new Apiary(); + } view.setPresenter(this); } @@ -89,8 +92,6 @@ public void stopLocation() { public void start() { if (!isNewApiary()) { populateApiary(); - } else { - apiary = new Apiary(); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java index e6544f00..3c7d5d89 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenter.java @@ -27,6 +27,9 @@ class AddEditHivePresenter implements AddEditHiveContract.Presenter, this.view = view; this.apiaryId = apiaryId; this.hiveId = hiveId; + if (isNewHive()) { + hive = new Hive(); + } view.setPresenter(this); } @@ -51,8 +54,6 @@ public void populateHive() { public void start() { if (!isNewHive()) { populateHive(); - } else { - hive = new Hive(); } } diff --git a/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java b/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java index 54a659bd..00c8f593 100644 --- a/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenterTest.java @@ -89,6 +89,7 @@ public void saveExistingApiaryToRepository_showsSuccessMessageUi() { // Get a reference to the class under test for apiary with id=1 addEditApiaryPresenter = new AddEditApiaryPresenter(apiariesRepository, addeditapiaryView, 1); + addEditApiaryPresenter.onApiaryLoaded(ApiaryMother.newDefaultApiary()); // When the presenter is asked to save an apiary addEditApiaryPresenter.save("Apiary 1", "Some more notes about it...."); // Then an apiary is saved in the repository diff --git a/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java b/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java index fa4e91df..1c718ff7 100644 --- a/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/addedithive/AddEditHivePresenterTest.java @@ -90,6 +90,7 @@ public void saveExistingApiaryToRepository_showsSuccessMessageUi() { // Get a reference to the class under test for hive with id=1 addEditHivePresenter = new AddEditHivePresenter(apiariesRepository, addEditHiveView, APIARY_ID, 1); + addEditHivePresenter.onHiveLoaded(HiveMother.newDefaultHive()); // When the presenter is asked to save a hive addEditHivePresenter.save("Apiary 1", "Some more notes about it...."); // Then a hive is saved in the repository From 0eeba788e8e66166ee292dd973c10692bd9b05c6 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Mon, 2 Jan 2017 19:59:57 +0100 Subject: [PATCH 23/32] Request ACCESS_FINE_LOCATION permission #123 --- app/build.gradle | 1 + .../addeditapiary/AddEditApiaryActivity.java | 18 +++-- .../addeditapiary/AddEditApiaryContract.java | 7 ++ .../addeditapiary/AddEditApiaryFragment.java | 65 +++++++++++++++++++ .../addeditapiary/AddEditApiaryPresenter.java | 22 +++---- .../source/local/GoBeesLocalDataSource.java | 2 + app/src/main/res/values/strings.xml | 11 ++++ build.gradle | 1 + 8 files changed, 112 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5f64a746..3b5a8eed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,6 +89,7 @@ dependencies { compile 'com.makeramen:roundedimageview:2.3.0' compile 'com.github.PhilJay:MPAndroidChart:v3.0.1' compile 'com.vanniktech:vntnumberpickerpreference:1.0.0' + compile 'rebus:permission-utils:1.0.6' // Dependencies for local unit tests testCompile 'junit:junit:4.12' diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryActivity.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryActivity.java index 9ac6d2ff..5a9ccb8a 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryActivity.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryActivity.java @@ -1,6 +1,8 @@ package com.davidmiguel.gobees.addeditapiary; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -18,6 +20,7 @@ public class AddEditApiaryActivity extends AppCompatActivity { public static final int REQUEST_ADD_APIARY = 1; public static final int NEW_APIARY = -1; + private Fragment addEditApiaryFragment; private GoBeesRepository goBeesRepository; @Override @@ -39,9 +42,7 @@ protected void onCreate(Bundle savedInstanceState) { .getLongExtra(AddEditApiaryFragment.ARGUMENT_EDIT_APIARY_ID, NEW_APIARY); // Add fragment to the activity and set title - AddEditApiaryFragment addEditApiaryFragment = - (AddEditApiaryFragment) getSupportFragmentManager() - .findFragmentById(R.id.contentFrame); + addEditApiaryFragment = getSupportFragmentManager().findFragmentById(R.id.contentFrame); if (addEditApiaryFragment == null) { addEditApiaryFragment = AddEditApiaryFragment.newInstance(); if (getIntent().hasExtra(AddEditApiaryFragment.ARGUMENT_EDIT_APIARY_ID)) { @@ -67,7 +68,8 @@ protected void onCreate(Bundle savedInstanceState) { goBeesRepository.openDb(); // Create the presenter - new AddEditApiaryPresenter(goBeesRepository, addEditApiaryFragment, apiaryId); + new AddEditApiaryPresenter(goBeesRepository, + (AddEditApiaryContract.View) addEditApiaryFragment, apiaryId); } @Override @@ -82,4 +84,12 @@ public boolean onSupportNavigateUp() { onBackPressed(); return true; } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (addEditApiaryFragment != null) { + addEditApiaryFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java index 86919f0e..541e331c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryContract.java @@ -65,6 +65,13 @@ interface View extends BaseView { * Shows save error message. */ void showSaveApiaryError(); + + /** + * Checks whether ACCESS_FINE_LOCATION permission is granted. If not, asks for it. + * + * @return if the permission is granted. + */ + boolean checkLocationPermission(); } interface Presenter extends BasePresenter { diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java index c647c75c..082adfd1 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java @@ -1,6 +1,7 @@ package com.davidmiguel.gobees.addeditapiary; import android.app.Activity; +import android.content.DialogInterface; import android.location.Location; import android.os.Bundle; import android.support.annotation.NonNull; @@ -9,6 +10,7 @@ import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -18,6 +20,12 @@ import com.davidmiguel.gobees.R; +import rebus.permissionutils.AskagainCallback; +import rebus.permissionutils.PermissionEnum; +import rebus.permissionutils.PermissionManager; +import rebus.permissionutils.PermissionUtils; +import rebus.permissionutils.SimpleCallback; + import static com.google.common.base.Preconditions.checkNotNull; /** @@ -149,6 +157,57 @@ public void showSaveApiaryError() { showMessage(getView(), getString(R.string.save_apiary_error_message)); } + @Override + public boolean checkLocationPermission() { + // Check location permission + if (PermissionUtils.isGranted(getActivity(), PermissionEnum.ACCESS_FINE_LOCATION)) { + return true; + } + // Ask permission + PermissionManager.with(getActivity()) + .permission(PermissionEnum.ACCESS_FINE_LOCATION) + .askagain(true) + .askagainCallback(new AskagainCallback() { + @Override + public void showRequestPermission(final UserResponse response) { + new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.permission_request_title)) + .setMessage(getString(R.string.permission_request_body)) + .setPositiveButton(getString(R.string.permission_request_ok_button), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + response.result(true); + } + }) + .setNegativeButton(getString(R.string.permission_request_no_button), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + response.result(false); + } + }) + .setCancelable(false) + .show(); + } + }) + .callback(new SimpleCallback() { + @Override + public void result(boolean allPermissionsGranted) { + if (allPermissionsGranted) { + // Launch the featuer + presenter.toogleLocation(getContext()); + } else { + // Warn the user that it's not possible to use the feature + Toast.makeText(getActivity(), getString(R.string.permission_request_denied), + Toast.LENGTH_LONG).show(); + } + } + }) + .ask(); + return false; + } + @Override public boolean isActive() { return isAdded(); @@ -159,6 +218,12 @@ public void setPresenter(@NonNull AddEditApiaryContract.Presenter presenter) { this.presenter = checkNotNull(presenter); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + PermissionManager.handleResult(requestCode, permissions, grantResults); + } + /** * Shows a snackbar with the given message. * diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java index 227620c8..4f4fae09 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryPresenter.java @@ -64,18 +64,18 @@ public void populateApiary() { @Override public void toogleLocation(Context context) { - if (locationService == null) { - // Connect GPS service - // TODO check permissions - locationService = new LocationService(context, this, this); - locationService.connect(); - view.setLocationIcon(true); - view.showSearchingGpsMsg(); - } else { - // Disconnect GPS service - stopLocation(); + if (view.checkLocationPermission()) { + if (locationService == null) { + // Connect GPS service + locationService = new LocationService(context, this, this); + locationService.connect(); + view.setLocationIcon(true); + view.showSearchingGpsMsg(); + } else { + // Disconnect GPS service + stopLocation(); + } } - } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index b745c2f2..1c413679 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -312,6 +312,8 @@ public void execute(Realm realm) { // Add to hive Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); hive.addRecords(records); + // TODO remove + RealmResults recordsAfter = realm.where(Record.class).findAll(); } }); callback.onSuccess(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80395728..2ebd78c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,6 +103,17 @@ Error while saving apiary. Apiary logo + + Are you sure? + + Precise location permission is needed if you want to + receive weather information or locate your apiaries in a map. Do you want to authorize it? + + ALLOW + + DENY + + The app does not have permissions to use this feature. diff --git a/build.gradle b/build.gradle index 47501a96..b87f9163 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ allprojects { jcenter() mavenCentral() maven { url "https://jitpack.io" } + maven { url "http://dl.bintray.com/raphaelbussa/maven" } } } From 3eb61aa90c5fc2a0d75aeb17f957eb4f16492d22 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Mon, 2 Jan 2017 20:35:40 +0100 Subject: [PATCH 24/32] Fix error while parsing weather JSON #140 --- .../source/network/OpenWeatherMapUtils.java | 36 ++++++++++++------- .../source/network/WeatherDataSource.java | 1 + 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java index 904dc616..7b5b440c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/OpenWeatherMapUtils.java @@ -94,43 +94,55 @@ static MeteoRecord parseCurrentWeatherJson(String weatherJson) throws JSONExcept if (jsonObject.has(OWM_WEATHER)) { JSONArray jsonWeatherArray = jsonObject.getJSONArray(OWM_WEATHER); JSONObject jsonWeatherObject = jsonWeatherArray.getJSONObject(0); - weatherCondition = jsonWeatherObject.getInt(OWM_WEATHER_ID); - weatherConditionIcon = jsonWeatherObject.getString(OWM_WEATHER_ICON); + weatherCondition = jsonWeatherObject.has(OWM_WEATHER_ID) ? + jsonWeatherObject.getInt(OWM_WEATHER_ID) : -1; + weatherConditionIcon = jsonWeatherObject.has(OWM_WEATHER_ICON) ? + jsonWeatherObject.getString(OWM_WEATHER_ICON) : ""; } // Get main info if (jsonObject.has(OWM_MAIN)) { JSONObject jsonMainObject = jsonObject.getJSONObject(OWM_MAIN); - temperature = jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE); - temperatureMin = jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE_MIN); - temperatureMax = jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE_MAX); - pressure = jsonMainObject.getInt(OWM_MAIN_PRESSURE); - humidity = jsonMainObject.getInt(OWM_MAIN_HUMIDITY); + temperature = jsonMainObject.has(OWM_MAIN_TEMPERATURE) ? + jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE) : 0; + temperatureMin = jsonMainObject.has(OWM_MAIN_TEMPERATURE_MIN) ? + jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE_MIN) : 0; + temperatureMax = jsonMainObject.has(OWM_MAIN_TEMPERATURE_MAX) ? + jsonMainObject.getDouble(OWM_MAIN_TEMPERATURE_MAX) : 0; + pressure = jsonMainObject.has(OWM_MAIN_PRESSURE) ? + jsonMainObject.getInt(OWM_MAIN_PRESSURE) : 0; + humidity = jsonMainObject.has(OWM_MAIN_HUMIDITY) ? + jsonMainObject.getInt(OWM_MAIN_HUMIDITY) : 0; } // Get wind if (jsonObject.has(OWM_WIND)) { JSONObject jsonWindObject = jsonObject.getJSONObject(OWM_WIND); - windSpeed = jsonWindObject.getDouble(OWM_WIND_SPEED); - windDegrees = jsonWindObject.getDouble(OWM_WIND_DIRECTION); + windSpeed = jsonWindObject.has(OWM_WIND_SPEED) ? + jsonWindObject.getDouble(OWM_WIND_SPEED) : 0; + windDegrees = jsonWindObject.has(OWM_WIND_DIRECTION) ? + jsonWindObject.getDouble(OWM_WIND_DIRECTION) : 0; } // Get clouds if (jsonObject.has(OWM_CLOUDS)) { JSONObject jsonCloudsObject = jsonObject.getJSONObject(OWM_CLOUDS); - clouds = jsonCloudsObject.getInt(OWM_CLOUDS_CLOUDINESS); + clouds = jsonCloudsObject.has(OWM_CLOUDS_CLOUDINESS) ? + jsonCloudsObject.getInt(OWM_CLOUDS_CLOUDINESS) : 0; } // Get rain if (jsonObject.has(OWM_RAIN)) { JSONObject jsonRainObject = jsonObject.getJSONObject(OWM_RAIN); - rain = jsonRainObject.getDouble(OWM_RAIN_3H); + rain = jsonRainObject.has(OWM_RAIN_3H) ? + jsonRainObject.getDouble(OWM_RAIN_3H) : 0; } // Get snow if (jsonObject.has(OWM_SNOW)) { JSONObject jsonSnowObject = jsonObject.getJSONObject(OWM_SNOW); - snow = jsonSnowObject.getDouble(OWM_SNOW_3H); + snow = jsonSnowObject.has(OWM_SNOW_3H) ? + jsonSnowObject.getDouble(OWM_SNOW_3H) : 0; } return new MeteoRecord(timestamp, cityName, weatherCondition, weatherConditionIcon, diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java index 9fdc0e12..63911b99 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java @@ -1,6 +1,7 @@ package com.davidmiguel.gobees.data.source.network; import android.os.AsyncTask; +import android.util.Log; import com.davidmiguel.gobees.data.model.MeteoRecord; From 3ac77e132a53fb9e342bfe5db353dd523827f320 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Mon, 2 Jan 2017 23:41:47 +0100 Subject: [PATCH 25/32] Request CAMERA permission #123 --- .../addeditapiary/AddEditApiaryFragment.java | 22 +++---- .../davidmiguel/gobees/hive/HiveActivity.java | 18 ++++- .../davidmiguel/gobees/hive/HiveContract.java | 7 ++ .../gobees/hive/HivePresenter.java | 4 +- .../gobees/hive/HiveRecordingsFragment.java | 66 +++++++++++++++++++ app/src/main/res/values/strings.xml | 27 +++++--- 6 files changed, 119 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java index 082adfd1..87bf80e5 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java @@ -163,7 +163,7 @@ public boolean checkLocationPermission() { if (PermissionUtils.isGranted(getActivity(), PermissionEnum.ACCESS_FINE_LOCATION)) { return true; } - // Ask permission + // Ask for permission PermissionManager.with(getActivity()) .permission(PermissionEnum.ACCESS_FINE_LOCATION) .askagain(true) @@ -172,15 +172,15 @@ public boolean checkLocationPermission() { public void showRequestPermission(final UserResponse response) { new AlertDialog.Builder(getActivity()) .setTitle(getString(R.string.permission_request_title)) - .setMessage(getString(R.string.permission_request_body)) - .setPositiveButton(getString(R.string.permission_request_ok_button), + .setMessage(getString(R.string.location_permission_request_body)) + .setPositiveButton(getString(R.string.permission_request_allow_button), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { response.result(true); } }) - .setNegativeButton(getString(R.string.permission_request_no_button), + .setNegativeButton(getString(R.string.permission_request_deny_button), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { @@ -195,7 +195,7 @@ public void onClick(DialogInterface dialogInterface, int i) { @Override public void result(boolean allPermissionsGranted) { if (allPermissionsGranted) { - // Launch the featuer + // Launch the feature presenter.toogleLocation(getContext()); } else { // Warn the user that it's not possible to use the feature @@ -208,6 +208,12 @@ public void result(boolean allPermissionsGranted) { return false; } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + PermissionManager.handleResult(requestCode, permissions, grantResults); + } + @Override public boolean isActive() { return isAdded(); @@ -218,12 +224,6 @@ public void setPresenter(@NonNull AddEditApiaryContract.Presenter presenter) { this.presenter = checkNotNull(presenter); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - PermissionManager.handleResult(requestCode, permissions, grantResults); - } - /** * Shows a snackbar with the given message. * diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java index 45592bf3..f0834ac7 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java @@ -1,7 +1,9 @@ package com.davidmiguel.gobees.hive; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; @@ -21,6 +23,7 @@ public class HiveActivity extends AppCompatActivity { public static final int NO_HIVE = -1; + private Fragment hiveRecordingsFragment; private GoBeesRepository goBeesRepository; @Override @@ -44,7 +47,7 @@ protected void onCreate(Bundle savedInstanceState) { } // Create recordings fragment - HiveRecordingsFragment hiveRecordingsFragment = HiveRecordingsFragment.newInstance(); + hiveRecordingsFragment = HiveRecordingsFragment.newInstance(); // Create hive info fragment HiveInfoFragment hiveInfoFragment = HiveInfoFragment.newInstance(); @@ -54,7 +57,8 @@ protected void onCreate(Bundle savedInstanceState) { TabsFragmentPagerAdapter adapter = new TabsFragmentPagerAdapter( getSupportFragmentManager(), HiveActivity.this, - Lists.newArrayList(hiveRecordingsFragment, hiveInfoFragment) + Lists.newArrayList((HiveRecordingsFragment) hiveRecordingsFragment, + hiveInfoFragment) ); viewPager.setAdapter(adapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); @@ -65,7 +69,7 @@ protected void onCreate(Bundle savedInstanceState) { goBeesRepository.openDb(); // Create the presenter - new HivePresenter(goBeesRepository, hiveRecordingsFragment, hiveId); + new HivePresenter(goBeesRepository, (HiveContract.View) hiveRecordingsFragment, hiveId); } @Override @@ -80,4 +84,12 @@ public boolean onSupportNavigateUp() { onBackPressed(); return true; } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (hiveRecordingsFragment != null) { + hiveRecordingsFragment.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java index 79954ab6..f5ad3afa 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java @@ -74,6 +74,13 @@ interface View extends BaseView { * @param title title. */ void showTitle(@NonNull String title); + + /** + * Checks whether ACCESS_FINE_LOCATION permission is granted. If not, asks for it. + * + * @return if the permission is granted. + */ + boolean checkCameraPermission(); } interface Presenter extends BasePresenter { diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java index fe99abab..706a341a 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java @@ -93,7 +93,9 @@ public void onDataNotAvailable() { @Override public void startNewRecording() { - view.startNewRecording(hiveId); + if (view.checkCameraPermission()) { + view.startNewRecording(hiveId); + } } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java index f3a6a2c6..3313660b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java @@ -1,5 +1,6 @@ package com.davidmiguel.gobees.hive; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; @@ -11,6 +12,7 @@ import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -20,6 +22,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import android.widget.Toast; import com.davidmiguel.gobees.R; import com.davidmiguel.gobees.data.model.Recording; @@ -34,6 +37,12 @@ import java.util.Date; import java.util.List; +import rebus.permissionutils.AskagainCallback; +import rebus.permissionutils.PermissionEnum; +import rebus.permissionutils.PermissionManager; +import rebus.permissionutils.PermissionUtils; +import rebus.permissionutils.SimpleCallback; + import static com.google.common.base.Preconditions.checkNotNull; /** @@ -215,6 +224,63 @@ public void showTitle(@NonNull String title) { } } + @Override + public boolean checkCameraPermission() { + // Check camera permission + if (PermissionUtils.isGranted(getActivity(), PermissionEnum.CAMERA)) { + return true; + } + // Ask for permission + PermissionManager.with(getActivity()) + .permission(PermissionEnum.CAMERA) + .askagain(true) + .askagainCallback(new AskagainCallback() { + @Override + public void showRequestPermission(final UserResponse response) { + new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.permission_request_title)) + .setMessage(getString(R.string.camera_permission_request_body)) + .setPositiveButton(getString(R.string.permission_request_allow_button), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + response.result(true); + } + }) + .setNegativeButton(getString(R.string.permission_request_deny_button), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + response.result(false); + } + }) + .setCancelable(false) + .show(); + } + }) + .callback(new SimpleCallback() { + @Override + public void result(boolean allPermissionsGranted) { + if (allPermissionsGranted) { + // Launch the feature + presenter.startNewRecording(); + } else { + // Warn the user that it's not possible to use the feature + Toast.makeText(getActivity(), getString(R.string.permission_request_denied), + Toast.LENGTH_LONG).show(); + } + } + }) + .ask(); + return false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + PermissionManager.handleResult(requestCode, permissions, grantResults); + } + @Override public void setPresenter(@NonNull HiveContract.Presenter presenter) { this.presenter = checkNotNull(presenter); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ebd78c9..eafbff06 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,14 @@ %1.0f\u00B0C %1.0f\u00B0F + + Permission denied + + ALLOW + + DENY + + The app does not have permissions to use this feature. @@ -103,17 +111,11 @@ Error while saving apiary. Apiary logo - - Are you sure? - Precise location permission is needed if you want to - receive weather information or locate your apiaries in a map. Do you want to authorize it? - - ALLOW - - DENY - - The app does not have permissions to use this feature. + + Without this permission the app is unable to receive weather information or to locate + your apiaries in a map. Are you sure you want to deny this permission? + @@ -181,6 +183,11 @@ Error while deleting recording. EEE d MMM, yyyy + + + Without this permission the app is unable to monitor the flight activity of your hive. + Are you sure you want to deny this permission? + From d67abd70c1cceaf9b51c50c4f0d867b6768916c9 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Tue, 3 Jan 2017 16:41:13 +0100 Subject: [PATCH 26/32] Fix error while saving recording #141 --- .../gobees/data/source/GoBeesDataSource.java | 8 +++-- .../data/source/cache/GoBeesRepository.java | 2 +- .../source/local/GoBeesLocalDataSource.java | 9 +++-- .../davidmiguel/gobees/hive/HiveContract.java | 14 +++++++- .../gobees/hive/HivePresenter.java | 29 +++++++++++---- .../gobees/hive/HiveRecordingsFragment.java | 15 +++++++- .../gobees/monitoring/MonitoringFragment.java | 35 ++++++++++++++++--- .../gobees/monitoring/MonitoringService.java | 18 +++++++--- .../gobees/recording/RecordingFragment.java | 2 +- .../gobees/recording/RecordingPresenter.java | 2 +- app/src/main/res/values/strings.xml | 6 ++++ 11 files changed, 116 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 2b4dec49..eae1e804 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -156,9 +156,9 @@ public interface GoBeesDataSource { * Note: record must be a new unmanaged object (don't modify managed objects). * * @param records list of record unmanaged objects. - * @param callback TaskCallback. + * @param callback SaveRecordingCallback. */ - void saveRecords(long hiveId, @NonNull List records, @NonNull TaskCallback callback); + void saveRecords(long hiveId, @NonNull List records, @NonNull SaveRecordingCallback callback); /** * Gets recording with records of given period. @@ -230,6 +230,10 @@ interface GetRecordingCallback { void onDataNotAvailable(); } + interface SaveRecordingCallback extends TaskCallback { + void onRecordingTooShort(); + } + interface TaskCallback { void onSuccess(); diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index b6eaeda3..d6906ac0 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -252,7 +252,7 @@ public void saveRecord(long hiveId, @NonNull Record record, @NonNull TaskCallbac } @Override - public void saveRecords(long hiveId, @NonNull List records, @NonNull TaskCallback callback) { + public void saveRecords(long hiveId, @NonNull List records, @NonNull SaveRecordingCallback callback) { checkNotNull(callback); // Save record goBeesDataSource.saveRecords(hiveId, records, callback); diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 1c413679..8716e562 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -292,7 +292,12 @@ public void execute(Realm realm) { @Override public void saveRecords(final long hiveId, @NonNull final List records, - @NonNull TaskCallback callback) { + @NonNull SaveRecordingCallback callback) { + if (records.size() < 10) { + // Recording too short + callback.onRecordingTooShort(); + return; + } try { // Get first id Number n = realm.where(Record.class).max("id"); @@ -312,8 +317,6 @@ public void execute(Realm realm) { // Add to hive Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); hive.addRecords(records); - // TODO remove - RealmResults recordsAfter = realm.where(Record.class).findAll(); } }); callback.onSuccess(); diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java index f5ad3afa..bbf4b051 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java @@ -1,5 +1,6 @@ package com.davidmiguel.gobees.hive; +import android.content.Intent; import android.support.annotation.NonNull; import com.davidmiguel.gobees.data.model.Recording; @@ -58,6 +59,16 @@ interface View extends BaseView { */ void showSuccessfullySavedMessage(); + /** + * Shows error while saving message. + */ + void showSaveErrorMessage(); + + /** + * Shows recording too short error message. + */ + void showRecordingTooShortErrorMessage(); + /** * Shows successfully deleted message. */ @@ -90,8 +101,9 @@ interface Presenter extends BasePresenter { * * @param requestCode request code from the intent. * @param resultCode result code from the intent. + * @param data intent data. */ - void result(int requestCode, int resultCode); + void result(int requestCode, int resultCode, Intent data); /** * Load recordings from repository. diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java index 706a341a..a5dd8681 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java @@ -1,6 +1,7 @@ package com.davidmiguel.gobees.hive; import android.app.Activity; +import android.content.Intent; import android.support.annotation.NonNull; import com.davidmiguel.gobees.data.model.Hive; @@ -33,13 +34,29 @@ class HivePresenter implements HiveContract.Presenter { } @Override - public void result(int requestCode, int resultCode) { + public void result(int requestCode, int resultCode, Intent data) { // If a recording was successfully saved, show snackbar - if (MonitoringActivity.REQUEST_MONITORING == requestCode && Activity.RESULT_OK == resultCode) { - // Refresh recordings - loadRecordings(true); - // Show message - view.showSuccessfullySavedMessage(); + if (MonitoringActivity.REQUEST_MONITORING == requestCode) { + if (resultCode == Activity.RESULT_OK) { + // Refresh recordings + loadRecordings(true); + // Show message + view.showSuccessfullySavedMessage(); + } else if (resultCode == Activity.RESULT_CANCELED) { + // Get error type + int error = data.getIntExtra(HiveRecordingsFragment.ARGUMENT_MONITORING_ERROR, -1); + // Show error message + switch (error) { + case HiveRecordingsFragment.ERROR_RECORDING_TOO_SHORT: + view.showRecordingTooShortErrorMessage(); + break; + case HiveRecordingsFragment.ERROR_SAVING_RECORDING: + view.showSaveErrorMessage(); + break; + default: + view.showSaveErrorMessage(); + } + } } } diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java index 3313660b..5ff6aba2 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java @@ -52,6 +52,9 @@ public class HiveRecordingsFragment extends Fragment implements BaseTabFragment, HiveContract.View, RecordingsAdapter.RecordingItemListener { public static final String ARGUMENT_HIVE_ID = "HIVE_ID"; + public static final String ARGUMENT_MONITORING_ERROR = "MONITORING_ERROR"; + public static final int ERROR_SAVING_RECORDING = 0; + public static final int ERROR_RECORDING_TOO_SHORT = 2; private HiveContract.Presenter presenter; private RecordingsAdapter listAdapter; @@ -149,7 +152,7 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - presenter.result(requestCode, resultCode); + presenter.result(requestCode, resultCode, data); } @Override @@ -206,6 +209,16 @@ public void showSuccessfullySavedMessage() { showMessage(getString(R.string.successfully_saved_recording_message)); } + @Override + public void showSaveErrorMessage() { + showMessage(getString(R.string.save_recording_error_message)); + } + + @Override + public void showRecordingTooShortErrorMessage() { + showMessage(getString(R.string.recording_too_short_error_message)); + } + @Override public void showSuccessfullyDeletedMessage() { showMessage(getString(R.string.successfully_deleted_recording_message)); diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java index f4ca3789..2cf781ba 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java @@ -22,6 +22,8 @@ import com.davidmiguel.gobees.R; import com.davidmiguel.gobees.camera.CameraView; +import com.davidmiguel.gobees.data.source.GoBeesDataSource; +import com.davidmiguel.gobees.hive.HiveRecordingsFragment; import com.davidmiguel.gobees.monitoring.MonitoringService.MonitoringBinder; import com.davidmiguel.gobees.utils.BackClickHelperFragment; @@ -113,7 +115,35 @@ public void onClick(View view) { @Override public void onServiceConnected(ComponentName componentName, IBinder service) { MonitoringBinder binder = (MonitoringBinder) service; - mService = binder.getService(); + mService = binder.getService(new GoBeesDataSource.SaveRecordingCallback() { + @Override + public void onRecordingTooShort() { + // Finish activity with error + Intent intent = new Intent(); + intent.putExtra(HiveRecordingsFragment.ARGUMENT_MONITORING_ERROR, + HiveRecordingsFragment.ERROR_RECORDING_TOO_SHORT); + getActivity().setResult(Activity.RESULT_CANCELED, intent); + getActivity().finish(); + + } + + @Override + public void onSuccess() { + // Finish activity with OK + getActivity().setResult(Activity.RESULT_OK); + getActivity().finish(); + } + + @Override + public void onFailure() { + // Finish activity with error + Intent intent = new Intent(); + intent.putExtra(HiveRecordingsFragment.ARGUMENT_MONITORING_ERROR, + HiveRecordingsFragment.ERROR_SAVING_RECORDING); + getActivity().setResult(Activity.RESULT_CANCELED, intent); + getActivity().finish(); + } + }); // Set chronometer chronometer.setBase(mService.getStartTime()); chronometer.start(); @@ -205,9 +235,6 @@ public void stopRecordingService() { Intent stopIntent = new Intent(getActivity(), MonitoringService.class); stopIntent.setAction(MonitoringService.STOP_ACTION); getActivity().startService(stopIntent); - // Finish activity - getActivity().setResult(Activity.RESULT_OK); - getActivity().finish(); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java index dc51d7af..fdc41950 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java @@ -18,7 +18,7 @@ import com.davidmiguel.gobees.camera.AndroidCameraListener; import com.davidmiguel.gobees.camera.CameraFrame; import com.davidmiguel.gobees.data.model.Record; -import com.davidmiguel.gobees.data.source.GoBeesDataSource; +import com.davidmiguel.gobees.data.source.GoBeesDataSource.SaveRecordingCallback; import com.davidmiguel.gobees.data.source.cache.GoBeesRepository; import com.davidmiguel.gobees.video.BeesCounter; import com.davidmiguel.gobees.video.ContourBeesCounter; @@ -46,6 +46,7 @@ public class MonitoringService extends Service implements AndroidCameraListener private static MonitoringService INSTANCE = null; private final IBinder mBinder = new MonitoringBinder(); + private SaveRecordingCallback callback; private GoBeesRepository goBeesRepository; private LinkedList records; private AndroidCamera androidCamera; @@ -90,22 +91,30 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Start service in foreground startForeground(NOTIFICATION_ID, n); - // STOP action + // STOP action } else if (intent.getAction().equals(STOP_ACTION)) { // Release camera androidCamera.release(); // Clean records cleanRecords(); // Save records - goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records, new GoBeesDataSource.TaskCallback() { + goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records, new SaveRecordingCallback() { + @Override + public void onRecordingTooShort() { + stopService(); + callback.onRecordingTooShort(); + } + @Override public void onSuccess() { stopService(); + callback.onSuccess(); } @Override public void onFailure() { stopService(); + callback.onFailure(); } }); } @@ -269,7 +278,8 @@ private void cleanRecords() { * runs in the same process as its clients, we don't need to deal with IPC. */ public class MonitoringBinder extends Binder { - MonitoringService getService() { + MonitoringService getService(SaveRecordingCallback c) { + callback = c; // Return this INSTANCE of MonitoringService so clients can call public methods return MonitoringService.this; } diff --git a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java index c0931d82..011fb04c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java @@ -194,7 +194,7 @@ public void showLoadingRecordingError() { @Override public void showNoRecords() { - // TODO + showMessage(getString(R.string.no_records_error)); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java index f5f3d55c..7a7ae116 100644 --- a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java @@ -47,7 +47,7 @@ public void openWindChart() { @Override public void deleteRecording() { - // TODO + // TODO } @Override diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eafbff06..bffba219 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,6 +177,10 @@ You have no recordings! Recording saved! + + Error while saving recording. + + Error: Recording too short! Recording deleted! @@ -194,6 +198,8 @@ Error while loading recording. + + Error: No records found. EEE d MMM, yyyy From 715ca54ff0142d10c8c9118afdaf1d7844c64db9 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Tue, 3 Jan 2017 20:13:49 +0100 Subject: [PATCH 27/32] Fetch weather data while monitoring #93 --- .../gobees/apiary/ApiaryContract.java | 5 +- .../gobees/apiary/ApiaryHivesFragment.java | 3 +- .../gobees/apiary/ApiaryPresenter.java | 2 +- .../davidmiguel/gobees/data/model/Apiary.java | 8 ++- .../gobees/data/source/GoBeesDataSource.java | 18 ++++++- .../data/source/cache/GoBeesRepository.java | 32 ++++++++++++ .../source/local/GoBeesLocalDataSource.java | 33 +++++++++++++ .../davidmiguel/gobees/hive/HiveActivity.java | 10 +++- .../davidmiguel/gobees/hive/HiveContract.java | 5 +- .../gobees/hive/HivePresenter.java | 6 ++- .../gobees/hive/HiveRecordingsFragment.java | 4 +- .../gobees/monitoring/MonitoringActivity.java | 9 +++- .../gobees/monitoring/MonitoringFragment.java | 1 + .../monitoring/MonitoringPresenter.java | 5 +- .../gobees/monitoring/MonitoringService.java | 49 +++++++++++++++++-- .../gobees/monitoring/MonitoringSettings.java | 13 +++++ 16 files changed, 185 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java index 65ec12b7..960fcbbe 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryContract.java @@ -40,9 +40,10 @@ interface View extends BaseView { /** * Opens activity to show the details of the given hive. * - * @param hiveId hive to show. + * @param apiaryId apiary id. + * @param hiveId hive to show. */ - void showHiveDetail(long hiveId); + void showHiveDetail(long apiaryId, long hiveId); /** * Shows loading hives error message. diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java index 7916b711..310ff06a 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java @@ -180,8 +180,9 @@ public void showAddEditHive(long apiaryId, long hiveId) { } @Override - public void showHiveDetail(long hiveId) { + public void showHiveDetail(long apiaryId, long hiveId) { Intent intent = new Intent(getActivity(), HiveActivity.class); + intent.putExtra(HiveRecordingsFragment.ARGUMENT_APIARY_ID, apiaryId); intent.putExtra(HiveRecordingsFragment.ARGUMENT_HIVE_ID, hiveId); getActivity().startActivity(intent); } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java index 631e16db..b8863e43 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryPresenter.java @@ -92,7 +92,7 @@ public void addEditHive(long hiveId) { @Override public void openHiveDetail(@NonNull Hive requestedHive) { - view.showHiveDetail(requestedHive.getId()); + view.showHiveDetail(apiaryId, requestedHive.getId()); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java index 7f3abd09..14f640cd 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/model/Apiary.java @@ -166,12 +166,18 @@ public void setMeteoRecords(@Nullable RealmList meteoRecords) { this.meteoRecords = meteoRecords; } - public void addHive(@NonNull Hive hive){ + public void addHive(@NonNull Hive hive) { if (hives != null) { hives.add(hive); } } + public void addMeteoRecord(@NonNull MeteoRecord meteoRecord) { + if (meteoRecords != null) { + meteoRecords.add(meteoRecord); + } + } + public boolean isValidApiary() { return !Strings.isNullOrEmpty(name); } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index eae1e804..9c59cca4 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -32,7 +32,6 @@ public interface GoBeesDataSource { /** * Gets all apiaries. - * Note: don't modify the Apiary objects. * * @param callback GetApiariesCallback. */ @@ -40,13 +39,20 @@ public interface GoBeesDataSource { /** * Gets apiary with given id. - * Note: don't modify the Apiary object. * * @param apiaryId apiary id. * @param callback GetApiaryCallback */ void getApiary(long apiaryId, @NonNull GetApiaryCallback callback); + /** + * Gets apiary with given id. Blocking function. + * + * @param apiaryId apiary id. + * @return requested apiary. + */ + Apiary getApiaryBlocking(long apiaryId); + /** * Saves given apiary. If it already exists, is updated. * Note: apiary must be a new unmanaged object (don't modify managed objects). @@ -187,6 +193,14 @@ public interface GoBeesDataSource { */ void updateApiariesCurrentWeather(List apiariesToUpdate, @NonNull TaskCallback callback); + /** + * Save a meteo record from an apiary. + * + * @param apiary corresponding apiary. + * @param callback TaskCallback. + */ + void saveMeteoRecord(Apiary apiary, @NonNull TaskCallback callback); + /** * Force to update recordings cache. */ diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index d6906ac0..7f1c0356 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -19,6 +19,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import io.realm.RealmList; + import static com.google.common.base.Preconditions.checkNotNull; /** @@ -126,6 +128,11 @@ public void getApiary(long apiaryId, @NonNull final GetApiaryCallback callback) goBeesDataSource.getApiary(apiaryId, callback); } + @Override + public Apiary getApiaryBlocking(long apiaryId) { + return goBeesDataSource.getApiaryBlocking(apiaryId); + } + @Override public void saveApiary(@NonNull Apiary apiary, @NonNull TaskCallback callback) { checkNotNull(apiary); @@ -314,6 +321,31 @@ public void onDataNotAvailable() { } } + @SuppressWarnings("ConstantConditions") + @Override + public void saveMeteoRecord(@NonNull final Apiary apiary, @NonNull final TaskCallback callback) { + checkNotNull(apiary); + checkNotNull(callback); + weatherDataSource.getCurrentWeather(1, apiary.getLocationLat(), apiary.getLocationLong(), + new WeatherDataSource.GetWeatherCallback() { + @Override + public void onWeatherLoaded(int id, MeteoRecord meteoRecord) { + // Fill apiary with just this meteo record to store it on db + RealmList meteoRecords = new RealmList<>(); + meteoRecords.add(meteoRecord); + apiary.setMeteoRecords(meteoRecords); + // Save data + goBeesDataSource.saveMeteoRecord(apiary, callback); + callback.onSuccess(); + } + + @Override + public void onDataNotAvailable() { + callback.onFailure(); + } + }); + } + @Override public void refreshRecordings(long hiveId) { // TODO diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 8716e562..35b85ab7 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -76,6 +76,11 @@ public void getApiary(long apiaryId, @NonNull GetApiaryCallback callback) { } } + @Override + public Apiary getApiaryBlocking(long apiaryId) { + return realm.copyFromRealm(realm.where(Apiary.class).equalTo("id", apiaryId).findFirst()); + } + @Override public void saveApiary(@NonNull final Apiary apiary, @NonNull TaskCallback callback) { try { @@ -414,6 +419,34 @@ public void execute(Realm realm) { } } + @Override + public void saveMeteoRecord(@NonNull final Apiary apiary, @NonNull TaskCallback callback) { + try { + if (apiary.getMeteoRecords() == null || apiary.getMeteoRecords().size() != 1) { + callback.onFailure(); + } + final MeteoRecord meteoRecord = apiary.getMeteoRecords().first(); + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + // Get next id + Number n = realm.where(MeteoRecord.class).max("id"); + long nextId = (n != null ? n.longValue() + 1 : 0); + // Save meteo records + meteoRecord.setId(nextId); + realm.copyToRealmOrUpdate(meteoRecord); + // Add meteo record to apiary + Apiary requestedApiary = realm.where(Apiary.class) + .equalTo("id", apiary.getId()).findFirst(); + requestedApiary.addMeteoRecord(meteoRecord); + } + }); + callback.onSuccess(); + } catch (Exception e) { + callback.onFailure(); + } + } + @Override public void refreshRecordings(long hiveId) { // Not required because the GoBeesRepository handles the logic of refreshing the diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java index f0834ac7..7ca59d6a 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveActivity.java @@ -21,6 +21,7 @@ */ public class HiveActivity extends AppCompatActivity { + public static final int NO_APIARY = -1; public static final int NO_HIVE = -1; private Fragment hiveRecordingsFragment; @@ -31,6 +32,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.hive_act); + // Get apiary id + long apiaryId = getIntent().getLongExtra(HiveRecordingsFragment.ARGUMENT_APIARY_ID, NO_APIARY); + if (apiaryId == NO_APIARY) { + throw new IllegalArgumentException("No apiary id passed!"); + } + // Get hive id long hiveId = getIntent().getLongExtra(HiveRecordingsFragment.ARGUMENT_HIVE_ID, NO_HIVE); if (hiveId == NO_HIVE) { @@ -69,7 +76,8 @@ protected void onCreate(Bundle savedInstanceState) { goBeesRepository.openDb(); // Create the presenter - new HivePresenter(goBeesRepository, (HiveContract.View) hiveRecordingsFragment, hiveId); + new HivePresenter(goBeesRepository, (HiveContract.View) hiveRecordingsFragment, + apiaryId, hiveId); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java index bbf4b051..9284e28f 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java @@ -33,8 +33,11 @@ interface View extends BaseView { /** * Opens activity to record a hive. + * + * @param apiaryId apiary id. + * @param hiveId hive id. */ - void startNewRecording(long hiveId); + void startNewRecording(long apiaryId, long hiveId); /** * Opens activity to show the details of the given recording. diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java index a5dd8681..d11f6f0b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java @@ -23,13 +23,15 @@ class HivePresenter implements HiveContract.Presenter { * Force update the first time. */ private boolean firstLoad = true; + private long apiaryId; private long hiveId; HivePresenter(GoBeesRepository goBeesRepository, HiveContract.View view, - long hiveId) { + long apiaryId, long hiveId) { this.goBeesRepository = goBeesRepository; this.view = view; this.view.setPresenter(this); + this.apiaryId = apiaryId; this.hiveId = hiveId; } @@ -111,7 +113,7 @@ public void onDataNotAvailable() { @Override public void startNewRecording() { if (view.checkCameraPermission()) { - view.startNewRecording(hiveId); + view.startNewRecording(apiaryId, hiveId); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java index 5ff6aba2..26b7d123 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java @@ -51,6 +51,7 @@ public class HiveRecordingsFragment extends Fragment implements BaseTabFragment, HiveContract.View, RecordingsAdapter.RecordingItemListener { + public static final String ARGUMENT_APIARY_ID = "APIARY_ID"; public static final String ARGUMENT_HIVE_ID = "HIVE_ID"; public static final String ARGUMENT_MONITORING_ERROR = "MONITORING_ERROR"; public static final int ERROR_SAVING_RECORDING = 0; @@ -179,8 +180,9 @@ public void showRecordings(@NonNull List recordings) { } @Override - public void startNewRecording(long hiveId) { + public void startNewRecording(long apiaryId, long hiveId) { Intent intent = new Intent(getContext(), MonitoringActivity.class); + intent.putExtra(MonitoringFragment.ARGUMENT_APIARY_ID, apiaryId); intent.putExtra(MonitoringFragment.ARGUMENT_HIVE_ID, hiveId); startActivityForResult(intent, MonitoringActivity.REQUEST_MONITORING); } diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringActivity.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringActivity.java index 857e3597..026135b9 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringActivity.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringActivity.java @@ -12,6 +12,7 @@ public class MonitoringActivity extends AppCompatActivity { public static final int REQUEST_MONITORING = 1; + public static final int NO_APIARY = -1; public static final int NO_HIVE = -1; private MonitoringFragment monitoringFragment; @@ -21,6 +22,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.monitoring_act); + // Get apiary id + long apiaryId = getIntent().getLongExtra(MonitoringFragment.ARGUMENT_APIARY_ID, NO_APIARY); + if (apiaryId == NO_APIARY) { + throw new IllegalArgumentException("No apiary id passed!"); + } + // Get hive id long hiveId = getIntent().getLongExtra(MonitoringFragment.ARGUMENT_HIVE_ID, NO_HIVE); if (hiveId == NO_HIVE) { @@ -44,7 +51,7 @@ protected void onCreate(Bundle savedInstanceState) { .commit(); // Create the presenter - new MonitoringPresenter(monitoringFragment, monitoringSettingsFragment, hiveId); + new MonitoringPresenter(monitoringFragment, monitoringSettingsFragment, apiaryId, hiveId); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java index 2cf781ba..35cc51ef 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringFragment.java @@ -40,6 +40,7 @@ public class MonitoringFragment extends Fragment implements MonitoringContract.View, BackClickHelperFragment { + public static final String ARGUMENT_APIARY_ID = "APIARY_ID"; public static final String ARGUMENT_HIVE_ID = "HIVE_ID"; private MonitoringContract.Presenter presenter; diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringPresenter.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringPresenter.java index c38d65ff..d1143790 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringPresenter.java @@ -16,17 +16,19 @@ class MonitoringPresenter implements MonitoringContract.Presenter, CvCameraViewL private MonitoringContract.View view; private MonitoringContract.SettingsView settingsView; + private long apiaryId; private long hiveId; private BeesCounter bc; private Mat processedFrame; private boolean showAlgoOutput; MonitoringPresenter(MonitoringContract.View view, MonitoringContract.SettingsView settingsView, - long hiveId) { + long apiaryId, long hiveId) { this.view = view; this.view.setPresenter(this); this.settingsView = settingsView; this.settingsView.setPresenter(this); + this.apiaryId = apiaryId; this.hiveId = hiveId; } @@ -44,6 +46,7 @@ public void openSettings() { public void startMonitoring() { // Get settings and set hive id MonitoringSettings ms = settingsView.getMonitoringSettings(); + ms.setApiaryId(apiaryId); ms.setHiveId(hiveId); // Hide camera view view.hideCameraView(); diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java index fdc41950..aa6f8fea 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java @@ -17,7 +17,9 @@ import com.davidmiguel.gobees.camera.AndroidCameraImpl; import com.davidmiguel.gobees.camera.AndroidCameraListener; import com.davidmiguel.gobees.camera.CameraFrame; +import com.davidmiguel.gobees.data.model.Apiary; import com.davidmiguel.gobees.data.model.Record; +import com.davidmiguel.gobees.data.source.GoBeesDataSource; import com.davidmiguel.gobees.data.source.GoBeesDataSource.SaveRecordingCallback; import com.davidmiguel.gobees.data.source.cache.GoBeesRepository; import com.davidmiguel.gobees.video.BeesCounter; @@ -29,6 +31,8 @@ import java.util.Date; import java.util.LinkedList; +import java.util.Timer; +import java.util.TimerTask; /** * Monitoring service. @@ -40,18 +44,22 @@ public class MonitoringService extends Service implements AndroidCameraListener public static final String ARGUMENT_MON_SETTINGS = "MONITORING_SETTINGS"; public static final String START_ACTION = "start_action"; public static final String STOP_ACTION = "stop_action"; - private static final int NOTIFICATION_ID = 101; private static final int T_5_SECONDS = 5000; + private static final int T_15_MINUTES = 900000; private static MonitoringService INSTANCE = null; private final IBinder mBinder = new MonitoringBinder(); private SaveRecordingCallback callback; + private GoBeesRepository goBeesRepository; + private Apiary apiary; private LinkedList records; private AndroidCamera androidCamera; private BeesCounter bc; private MonitoringSettings monitoringSettings; + private Timer timer; + private FetchWeatherTask fetchWeatherTask; private boolean openCVLoaded = false; private long startTime; @@ -73,6 +81,9 @@ public void onCreate() { // Init db goBeesRepository = Injection.provideApiariesRepository(); goBeesRepository.openDb(); + // Create fetch weather task + fetchWeatherTask = new FetchWeatherTask(); + timer = new Timer(); } @Override @@ -83,6 +94,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getAction().equals(START_ACTION)) { // Get monitoring config monitoringSettings = (MonitoringSettings) intent.getSerializableExtra(ARGUMENT_MON_SETTINGS); + // Get apiary + apiary = goBeesRepository.getApiaryBlocking(monitoringSettings.getApiaryId()); // Configurations configOpenCV(); configBeeCounter(); @@ -130,6 +143,12 @@ public void onDestroy() { androidCamera.release(); androidCamera = null; } + // Stop fetching weather data + if (timer != null) { + timer.cancel(); + timer = null; + fetchWeatherTask = null; + } // Close database goBeesRepository.closeDb(); } @@ -182,7 +201,7 @@ public void onManagerConnected(final int status) { switch (status) { case LoaderCallbackInterface.SUCCESS: openCVLoaded = true; - startRecording(); + startMonitoring(); break; default: super.onManagerConnected(status); @@ -235,9 +254,14 @@ private Notification configNotification() { } /** - * Start recording (frames will be received via onPreviewFrame()). + * Start monitoring (frames will be received via onPreviewFrame()). */ - private void startRecording() { + private void startMonitoring() { + // If apiary has location -> Start fetching weather data (each 15min) + if (apiary.hasLocation()) { + timer.scheduleAtFixedRate(fetchWeatherTask, 0, T_15_MINUTES); + } + // Start camera if (!androidCamera.isConnected()) { androidCamera.connect(); } @@ -284,4 +308,21 @@ MonitoringService getService(SaveRecordingCallback c) { return MonitoringService.this; } } + + private class FetchWeatherTask extends TimerTask { + @Override + public void run() { + goBeesRepository.saveMeteoRecord(apiary, new GoBeesDataSource.TaskCallback() { + @Override + public void onSuccess() { + // Don't do anything + } + + @Override + public void onFailure() { + // Don't do anything + } + }); + } + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java index 18df42dd..a5e44f02 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java @@ -9,6 +9,11 @@ */ class MonitoringSettings implements Serializable { + /** + * Apiary id. + */ + private long apiaryId; + /** * Hive id. */ @@ -44,6 +49,14 @@ class MonitoringSettings implements Serializable { */ private int zoomRatio; + public long getApiaryId() { + return apiaryId; + } + + public void setApiaryId(long apiaryId) { + this.apiaryId = apiaryId; + } + long getHiveId() { return hiveId; } From 875c59fd0330c839a455096cb2df6daa0b7d814a Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Wed, 4 Jan 2017 13:56:26 +0100 Subject: [PATCH 28/32] Show weather data in recording charts #93 --- .../gobees/data/source/GoBeesDataSource.java | 5 +- .../data/source/cache/GoBeesRepository.java | 4 +- .../source/local/GoBeesLocalDataSource.java | 22 ++- .../davidmiguel/gobees/hive/HiveContract.java | 7 +- .../gobees/hive/HivePresenter.java | 2 +- .../gobees/hive/HiveRecordingsFragment.java | 3 +- .../gobees/hive/RecordingsAdapter.java | 3 +- .../gobees/monitoring/MonitoringService.java | 4 +- .../gobees/recording/RecordingActivity.java | 9 +- .../gobees/recording/RecordingFragment.java | 156 ++++++++++++++---- .../gobees/recording/RecordingPresenter.java | 6 +- .../gobees/utils/TempValueFormatter.java | 22 ++- .../gobees/utils/WeatherUtils.java | 17 +- app/src/main/res/values/strings.xml | 4 + 14 files changed, 215 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java index 9c59cca4..cd7bebbd 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/GoBeesDataSource.java @@ -167,14 +167,15 @@ public interface GoBeesDataSource { void saveRecords(long hiveId, @NonNull List records, @NonNull SaveRecordingCallback callback); /** - * Gets recording with records of given period. + * Gets recording with records and weather data of given period. * + * @param apiaryId apiary id. * @param hiveId hive id. * @param start start of the period (00:00 of that date). * @param end end of the period (23:59 of that date). * @param callback GetRecordingCallback. */ - void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback); + void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback); /** * Deletes the records contained in the given recording. diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index 7f1c0356..b291dd7e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -266,10 +266,10 @@ public void saveRecords(long hiveId, @NonNull List records, @NonNull Sav } @Override - public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback) { + public void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback) { checkNotNull(callback); // Save record - goBeesDataSource.getRecording(hiveId, start, end, callback); + goBeesDataSource.getRecording(apiaryId, hiveId, start, end, callback); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index 35b85ab7..c464bcf8 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -331,7 +331,13 @@ public void execute(Realm realm) { } @Override - public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback) { + public void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback) { + // Get apiary + Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); + if (apiary == null || apiary.getMeteoRecords() == null) { + callback.onDataNotAvailable(); + return; + } // Get hive Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); if (hive == null || hive.getRecords() == null) { @@ -345,8 +351,20 @@ public void getRecording(long hiveId, Date start, Date end, @NonNull GetRecordin .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(end, 23, 59, 59, 999)) .findAll() .sort("timestamp"); + if (records.size() == 0) { + callback.onDataNotAvailable(); + return; + } + // Get weather data + RealmResults meteoRecords1 = apiary.getMeteoRecords().where().findAll(); + RealmResults meteoRecords = apiary.getMeteoRecords() + .where() + .greaterThanOrEqualTo("timestamp", records.first().getTimestamp()) + .lessThanOrEqualTo("timestamp", records.last().getTimestamp()) + .findAll() + .sort("timestamp"); // Create recording - Recording recording = new Recording(start, new ArrayList<>(records)); + Recording recording = new Recording(start, new ArrayList<>(records), meteoRecords); callback.onRecordingLoaded(recording); } diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java index 9284e28f..2f03c6cf 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveContract.java @@ -42,10 +42,11 @@ interface View extends BaseView { /** * Opens activity to show the details of the given recording. * - * @param hiveId hive id. - * @param date recording date. + * @param apiaryId apiary id. + * @param hiveId hive id. + * @param date recording date. */ - void showRecordingDetail(long hiveId, Date date); + void showRecordingDetail(long apiaryId, long hiveId, Date date); /** * Shows loading recordings error message. diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java index d11f6f0b..29745d1f 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java @@ -119,7 +119,7 @@ public void startNewRecording() { @Override public void openRecordingsDetail(@NonNull Recording recording) { - view.showRecordingDetail(hiveId, recording.getDate()); + view.showRecordingDetail(apiaryId, hiveId, recording.getDate()); } @Override diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java index 26b7d123..ffee444e 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HiveRecordingsFragment.java @@ -188,8 +188,9 @@ public void startNewRecording(long apiaryId, long hiveId) { } @Override - public void showRecordingDetail(long hiveId, Date date) { + public void showRecordingDetail(long apiaryId, long hiveId, Date date) { Intent intent = new Intent(getActivity(), RecordingActivity.class); + intent.putExtra(RecordingFragment.ARGUMENT_APIARY_ID, apiaryId); intent.putExtra(RecordingFragment.ARGUMENT_HIVE_ID, hiveId); intent.putExtra(RecordingFragment.ARGUMENT_START_DATE, date.getTime()); intent.putExtra(RecordingFragment.ARGUMENT_END_DATE, date.getTime()); diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java index 3b9c6d2d..cc28c640 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/RecordingsAdapter.java @@ -206,7 +206,7 @@ private LineData styleChartLines(List entries) { int color = ContextCompat.getColor(context, R.color.colorAccent); // Set styles LineDataSet lineDataSet = new LineDataSet(entries, "Recording"); - lineDataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER); + lineDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); lineDataSet.setCubicIntensity(0.2f); lineDataSet.setDrawValues(false); lineDataSet.setDrawCircles(false); @@ -234,6 +234,7 @@ private void setupChart(LineChart lineChart, LineData data, long firstTimestamp) lineChart.getDescription().setEnabled(false); lineChart.getLegend().setEnabled(false); lineChart.setTouchEnabled(false); + lineChart.setNoDataText(context.getString(R.string.no_flight_act_data_available)); // X axis setup IAxisValueFormatter xAxisFormatter = new HourAxisValueFormatter(firstTimestamp); XAxis xAxis = lineChart.getXAxis(); diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java index aa6f8fea..c9f26ecb 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java @@ -46,6 +46,7 @@ public class MonitoringService extends Service implements AndroidCameraListener public static final String STOP_ACTION = "stop_action"; private static final int NOTIFICATION_ID = 101; private static final int T_5_SECONDS = 5000; + private static final int T_10_SECONDS = 10000; private static final int T_15_MINUTES = 900000; private static MonitoringService INSTANCE = null; @@ -258,8 +259,9 @@ private Notification configNotification() { */ private void startMonitoring() { // If apiary has location -> Start fetching weather data (each 15min) + // With a delay of 10 seconds (because first 5 seconds are ignored) if (apiary.hasLocation()) { - timer.scheduleAtFixedRate(fetchWeatherTask, 0, T_15_MINUTES); + timer.scheduleAtFixedRate(fetchWeatherTask, T_10_SECONDS, T_15_MINUTES); } // Start camera if (!androidCamera.isConnected()) { diff --git a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingActivity.java b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingActivity.java index 73cebc10..7cdd618b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingActivity.java +++ b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingActivity.java @@ -17,6 +17,7 @@ */ public class RecordingActivity extends AppCompatActivity { + public static final int NO_APIARY = -1; public static final int NO_HIVE = -1; public static final int NO_DATE = -1; @@ -36,6 +37,12 @@ protected void onCreate(Bundle savedInstanceState) { actionBar.setDisplayShowHomeEnabled(true); } + // Get apiary id + long apiaryId = getIntent().getLongExtra(RecordingFragment.ARGUMENT_APIARY_ID, NO_APIARY); + if (apiaryId == NO_APIARY) { + throw new IllegalArgumentException("No apiary id passed!"); + } + // Get hive id long hiveId = getIntent().getLongExtra(RecordingFragment.ARGUMENT_HIVE_ID, NO_HIVE); if (hiveId == NO_HIVE) { @@ -67,7 +74,7 @@ protected void onCreate(Bundle savedInstanceState) { goBeesRepository.openDb(); // Create the presenter - new RecordingPresenter(goBeesRepository, recordingFragment, hiveId, + new RecordingPresenter(goBeesRepository, recordingFragment, apiaryId, hiveId, new Date(startDate), new Date(endDate)); } diff --git a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java index 011fb04c..b82f2e8c 100644 --- a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java @@ -1,5 +1,6 @@ package com.davidmiguel.gobees.recording; +import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.NonNull; @@ -20,14 +21,18 @@ import android.widget.LinearLayout; import com.davidmiguel.gobees.R; +import com.davidmiguel.gobees.data.model.MeteoRecord; import com.davidmiguel.gobees.data.model.Record; import com.davidmiguel.gobees.data.model.Recording; +import com.davidmiguel.gobees.data.source.preferences.GoBeesPreferences; import com.davidmiguel.gobees.utils.HourAxisValueFormatter; import com.davidmiguel.gobees.utils.RainValueFormatter; import com.davidmiguel.gobees.utils.ScrollChildSwipeRefreshLayout; import com.davidmiguel.gobees.utils.StringUtils; import com.davidmiguel.gobees.utils.TempValueFormatter; +import com.davidmiguel.gobees.utils.WeatherUtils; import com.davidmiguel.gobees.utils.WindValueFormatter; +import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; @@ -49,6 +54,7 @@ */ public class RecordingFragment extends Fragment implements RecordingContract.View { + public static final String ARGUMENT_APIARY_ID = "APIARY_ID"; public static final String ARGUMENT_HIVE_ID = "HIVE_ID"; public static final String ARGUMENT_START_DATE = "START_DATE"; public static final String ARGUMENT_END_DATE = "END_DATE"; @@ -62,6 +68,9 @@ public class RecordingFragment extends Fragment implements RecordingContract.Vie private ImageView rainIcon; private ImageView windIcon; + private long referenceTimestamp; + private long lastTimestamp; + public RecordingFragment() { // Requires empty public constructor } @@ -180,9 +189,13 @@ public void run() { public void showRecording(@NonNull Recording recording) { // Setup charts setupBeesChart(recording.getRecords()); - setupTempChart(); - setupRainChart(); - setupWindChart(); + if (recording.getMeteo() != null && recording.getMeteo().size() > 0) { + setupTempChart(recording.getMeteo()); + setupRainChart(recording.getMeteo()); + setupWindChart(recording.getMeteo()); + } else { + showNoWeatherData(); + } // Show temp chart by default showTempChart(); } @@ -281,13 +294,13 @@ private void highlightChartIcon(String chartVisible) { */ private void setupBeesChart(List recordsList) { // Setup data - long firstTimestamp = recordsList.get(0).getTimestamp().getTime() / 1000; + referenceTimestamp = recordsList.get(0).getTimestamp().getTime() / 1000; Record[] records = recordsList.toArray(new Record[recordsList.size()]); List entries = new ArrayList<>(); int maxNumBees = 0; for (Record record : records) { // Convert timestamp to seconds and relative to first timestamp - long timestamp = (record.getTimestamp().getTime() / 1000 - firstTimestamp); + long timestamp = (record.getTimestamp().getTime() / 1000 - referenceTimestamp); int numBees = record.getNumBees(); entries.add(new Entry(timestamp, numBees)); // Get max num of bees @@ -295,9 +308,10 @@ private void setupBeesChart(List recordsList) { maxNumBees = numBees; } } + lastTimestamp = (long) entries.get(entries.size() - 1).getX(); // Style char lines (type, color, etc.) LineDataSet lineDataSet = new LineDataSet(entries, getString(R.string.num_bees)); - lineDataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER); + lineDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); lineDataSet.setCubicIntensity(0.2f); lineDataSet.setDrawValues(false); lineDataSet.setDrawCircles(false); @@ -317,8 +331,9 @@ private void setupBeesChart(List recordsList) { BeesMarkerView mv = new BeesMarkerView(getContext(), R.layout.recording_bees_marker_vew); mv.setChartView(beesChart); beesChart.setMarker(mv); + beesChart.setNoDataText(getString(R.string.no_flight_act_data_available)); // X axis setup - IAxisValueFormatter xAxisFormatter = new HourAxisValueFormatter(firstTimestamp); + IAxisValueFormatter xAxisFormatter = new HourAxisValueFormatter(referenceTimestamp); XAxis xAxis = beesChart.getXAxis(); xAxis.setValueFormatter(xAxisFormatter); xAxis.setDrawGridLines(false); @@ -343,22 +358,46 @@ private void setupBeesChart(List recordsList) { /** * Configure temperature chart and the data. + * + * @param meteo meteo records. */ - private void setupTempChart() { + private void setupTempChart(List meteo) { // Setup data - int[] degrees = {16, 20, 30, 33, 25}; List entries = new ArrayList<>(); - for (int i = 0; i < degrees.length; i++) { - entries.add(new Entry(i, degrees[i])); + // Add as first entry a copy of the first temperature record + // First relative timestamp is 0 (-5 to don't show the value in the chart) + entries.add(new Entry(-5, (float) meteo.get(0).getTemperature())); + // Add all temperature records + float maxTemp = Float.MIN_VALUE; + float minTemp = Float.MAX_VALUE; + for (MeteoRecord meteoRecord : meteo) { + // Convert timestamp to seconds and relative to first timestamp + long timestamp = (meteoRecord.getTimestamp().getTime() / 1000 - referenceTimestamp); + float temperature = (float) meteoRecord.getTemperature(); + entries.add(new Entry(timestamp, temperature)); + // Get max and min temperature + if (temperature > maxTemp) { + maxTemp = temperature; + } + if (temperature < minTemp) { + minTemp = temperature; + } } + // Add as last entry a copy of the last temperature record (+5 to don't show the value in the chart) + entries.add(new Entry(lastTimestamp + 5, (float) meteo.get(meteo.size() - 1).getTemperature())); // Style char lines (type, color, etc.) + TempValueFormatter tempValueFormatter = new TempValueFormatter( + GoBeesPreferences.isMetric(getContext()) ? + TempValueFormatter.Unit.CELSIUS : TempValueFormatter.Unit.FAHRENHEIT); LineDataSet lineDataSet = new LineDataSet(entries, getString(R.string.temperature)); lineDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); lineDataSet.setDrawValues(true); - lineDataSet.setValueFormatter(new TempValueFormatter(TempValueFormatter.Unit.CELSIUS)); + lineDataSet.setValueTextSize(10f); + lineDataSet.setValueFormatter(tempValueFormatter); lineDataSet.setDrawCircles(false); lineDataSet.setLineWidth(1.8f); lineDataSet.setColor(ContextCompat.getColor(getContext(), R.color.colorLineTempChart)); + lineDataSet.setLineWidth(2f); lineDataSet.setDrawFilled(true); lineDataSet.setFillColor(ContextCompat.getColor(getContext(), R.color.colorFillTempChart)); lineDataSet.setFillAlpha(255); @@ -371,10 +410,15 @@ private void setupTempChart() { tempChart.getLegend().setEnabled(false); tempChart.setTouchEnabled(false); // X axis setup - tempChart.getXAxis().setEnabled(false); + XAxis xAxis = tempChart.getXAxis(); + xAxis.setEnabled(false); + xAxis.setAxisMinimum(0); + xAxis.setAxisMaximum(lastTimestamp); // Y axis setup - tempChart.getAxisLeft().setAxisMinimum(0); - tempChart.getAxisLeft().setEnabled(false); + YAxis leftAxis = tempChart.getAxisLeft(); + leftAxis.setEnabled(false); + leftAxis.setAxisMaximum(maxTemp + 5); + leftAxis.setAxisMinimum(minTemp - 5); tempChart.getAxisRight().setEnabled(false); // Add data tempChart.setData(data); @@ -383,22 +427,39 @@ private void setupTempChart() { /** * Configure rain chart and the data. + * + * @param meteo meteo records. */ - private void setupRainChart() { + private void setupRainChart(List meteo) { // Setup data - int[] degrees = {1, 1, 10, 3, 2}; List entries = new ArrayList<>(); - for (int i = 0; i < degrees.length; i++) { - entries.add(new Entry(i, degrees[i])); + // Add as first entry a copy of the first rain record + // First relative timestamp is 0 (-5 to don't show the value in the chart) + entries.add(new Entry(-5, (float) meteo.get(0).getRain())); + // Add all rain records + float maxRain = Float.MIN_VALUE; + for (MeteoRecord meteoRecord : meteo) { + // Convert timestamp to seconds and relative to first timestamp + long timestamp = (meteoRecord.getTimestamp().getTime() / 1000 - referenceTimestamp); + float rain = (float) meteoRecord.getRain(); + entries.add(new Entry(timestamp, rain)); + // Get max and min temperature + if (rain > maxRain) { + maxRain = rain; + } } + // Add as last entry a copy of the last rain record (+5 to don't show the value in the chart) + entries.add(new Entry(lastTimestamp + 5, (float) meteo.get(meteo.size() - 1).getRain())); // Style char lines (type, color, etc.) LineDataSet lineDataSet = new LineDataSet(entries, getString(R.string.rain)); lineDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); lineDataSet.setDrawValues(true); + lineDataSet.setValueTextSize(10f); lineDataSet.setValueFormatter(new RainValueFormatter(RainValueFormatter.Unit.MM)); lineDataSet.setDrawCircles(false); lineDataSet.setLineWidth(1.8f); lineDataSet.setColor(ContextCompat.getColor(getContext(), R.color.colorLineRainChart)); + lineDataSet.setLineWidth(2f); lineDataSet.setDrawFilled(true); lineDataSet.setFillColor(ContextCompat.getColor(getContext(), R.color.colorFillRainChart)); lineDataSet.setFillAlpha(255); @@ -411,10 +472,15 @@ private void setupRainChart() { rainChart.getLegend().setEnabled(false); rainChart.setTouchEnabled(false); // X axis setup - rainChart.getXAxis().setEnabled(false); + XAxis xAxis = rainChart.getXAxis(); + xAxis.setEnabled(false); + xAxis.setAxisMinimum(0); + xAxis.setAxisMaximum(lastTimestamp); // Y axis setup - rainChart.getAxisLeft().setAxisMinimum(0); - rainChart.getAxisLeft().setEnabled(false); + YAxis leftAxis = rainChart.getAxisLeft(); + leftAxis.setEnabled(false); + leftAxis.setAxisMaximum(maxRain + 1); + leftAxis.setAxisMinimum(0); rainChart.getAxisRight().setEnabled(false); // Add data rainChart.setData(data); @@ -423,22 +489,39 @@ private void setupRainChart() { /** * Configure wind chart and the data. + * + * @param meteo meteo records. */ - private void setupWindChart() { + private void setupWindChart(List meteo) { // Setup data - int[] degrees = {5, 5, 5, 1, 1}; List entries = new ArrayList<>(); - for (int i = 0; i < degrees.length; i++) { - entries.add(new Entry(i, degrees[i])); + // Add as first entry a copy of the first wind record + // First relative timestamp is 0 (-5 to don't show the value in the chart) + entries.add(new Entry(-5, (float) meteo.get(0).getWindSpeed())); + // Add all wind records + float maxWind = Float.MIN_VALUE; + for (MeteoRecord meteoRecord : meteo) { + // Convert timestamp to seconds and relative to first timestamp + long timestamp = (meteoRecord.getTimestamp().getTime() / 1000 - referenceTimestamp); + float wind = (float) meteoRecord.getWindSpeed(); + entries.add(new Entry(timestamp, wind)); + // Get max and min temperature + if (wind > maxWind) { + maxWind = wind; + } } + // Add as last entry a copy of the last wind record (+5 to don't show the value in the chart) + entries.add(new Entry(lastTimestamp + 5, (float) meteo.get(meteo.size() - 1).getWindSpeed())); // Style char lines (type, color, etc.) LineDataSet lineDataSet = new LineDataSet(entries, getString(R.string.wind)); lineDataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); lineDataSet.setDrawValues(true); + lineDataSet.setValueTextSize(10f); lineDataSet.setValueFormatter(new WindValueFormatter(WindValueFormatter.Unit.MS)); lineDataSet.setDrawCircles(false); lineDataSet.setLineWidth(1.8f); lineDataSet.setColor(ContextCompat.getColor(getContext(), R.color.colorLineWindChart)); + lineDataSet.setLineWidth(2f); lineDataSet.setDrawFilled(true); lineDataSet.setFillColor(ContextCompat.getColor(getContext(), R.color.colorFillWindChart)); lineDataSet.setFillAlpha(255); @@ -451,12 +534,27 @@ private void setupWindChart() { windChart.getLegend().setEnabled(false); windChart.setTouchEnabled(false); // X axis setup - windChart.getXAxis().setEnabled(false); + XAxis xAxis = windChart.getXAxis(); + xAxis.setEnabled(false); + xAxis.setAxisMinimum(0); + xAxis.setAxisMaximum(lastTimestamp); // Y axis setup - windChart.getAxisLeft().setAxisMinimum(0); - windChart.getAxisLeft().setEnabled(false); + YAxis leftAxis = windChart.getAxisLeft(); + leftAxis.setEnabled(false); + leftAxis.setAxisMaximum(maxWind + 1); + leftAxis.setAxisMinimum(0); windChart.getAxisRight().setEnabled(false); // Add data windChart.setData(data); } + + private void showNoWeatherData() { + float textSize = WeatherUtils.convertDpToPixel(getResources(), 12); + tempChart.setNoDataText(getString(R.string.no_weather_data_available)); + tempChart.getPaint(Chart.PAINT_INFO).setTextSize(textSize); + rainChart.setNoDataText(getString(R.string.no_weather_data_available)); + rainChart.getPaint(Chart.PAINT_INFO).setTextSize(textSize); + windChart.setNoDataText(getString(R.string.no_weather_data_available)); + windChart.getPaint(Chart.PAINT_INFO).setTextSize(textSize); + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java index 7a7ae116..4a925c7b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingPresenter.java @@ -15,16 +15,18 @@ class RecordingPresenter implements RecordingContract.Presenter { private GoBeesRepository goBeesRepository; private RecordingContract.View view; + private long apiaryId; private long hiveId; private Date start; private Date end; private Recording recording; RecordingPresenter(GoBeesRepository goBeesRepository, RecordingContract.View view, - long hiveId, Date start, Date end) { + long apiaryId, long hiveId, Date start, Date end) { this.goBeesRepository = goBeesRepository; this.view = view; this.view.setPresenter(this); + this.apiaryId = apiaryId; this.hiveId = hiveId; this.start = start; this.end = end; @@ -61,7 +63,7 @@ private void loadRecording(final Date start, Date end) { // Show loading indicator view.setLoadingIndicator(true); // Get recording - goBeesRepository.getRecording(hiveId, start, end, new GetRecordingCallback() { + goBeesRepository.getRecording(apiaryId, hiveId, start, end, new GetRecordingCallback() { @Override public void onRecordingLoaded(Recording r) { recording = r; diff --git a/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java b/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java index 6741493e..5bcd4b00 100644 --- a/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java +++ b/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java @@ -14,7 +14,7 @@ /** * Format label in xºC format. */ -public class TempValueFormatter implements IValueFormatter { +public class TempValueFormatter implements IValueFormatter, IAxisValueFormatter { private Unit unit; @@ -25,9 +25,25 @@ public TempValueFormatter(Unit unit) { @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + return Math.round(convertValue(value)) + unit.toString(); + } + + @Override + public String getFormattedValue(float value, AxisBase axis) { return Math.round(value) + unit.toString(); } + private float convertValue(float value) { + switch (unit) { + case CELSIUS: + return value; + case FAHRENHEIT: + return (float) WeatherUtils.celsiusToFahrenheit(value); + default: + throw new IllegalArgumentException(); + } + } + public enum Unit { CELSIUS, FAHRENHEIT; @@ -35,9 +51,9 @@ public enum Unit { public String toString() { switch (this) { case CELSIUS: - return "ºC"; + return "\u00B0C"; case FAHRENHEIT: - return "ºF"; + return "\u00B0F"; default: throw new IllegalArgumentException(); } diff --git a/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java b/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java index 9e80dc5f..1e522c61 100644 --- a/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java +++ b/app/src/main/java/com/davidmiguel/gobees/utils/WeatherUtils.java @@ -1,6 +1,8 @@ package com.davidmiguel.gobees.utils; import android.content.Context; +import android.content.res.Resources; +import android.util.DisplayMetrics; import com.davidmiguel.gobees.R; import com.davidmiguel.gobees.data.source.preferences.GoBeesPreferences; @@ -18,7 +20,7 @@ public class WeatherUtils { * @param temperatureInCelsius Temperature in degrees Celsius(°C). * @return Temperature in degrees Fahrenheit (°F). */ - private static double celsiusToFahrenheit(double temperatureInCelsius) { + static double celsiusToFahrenheit(double temperatureInCelsius) { return (temperatureInCelsius * 1.8) + 32; } @@ -85,4 +87,17 @@ public static int getWeatherIconResourceId(String weatherIconId) { return R.drawable.ic_weather_day_clear_sky; } } + + /** + * Convert dp to pixels. + * + * @param resources android resources. + * @param dp dps to convert. + * @return pixels. + */ + public static float convertDpToPixel(Resources resources, float dp) { + DisplayMetrics metrics = resources.getDisplayMetrics(); + float px = dp * (metrics.densityDpi / 160f); + return Math.round(px); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bffba219..0dd3d317 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -200,6 +200,10 @@ Error while loading recording. Error: No records found. + + No flight activity data available. + + No weather data available. EEE d MMM, yyyy From 83e1f9d944a0a87139e34867d78516adcc090d0c Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Wed, 4 Jan 2017 14:47:18 +0100 Subject: [PATCH 29/32] Fix tests #93 --- .../java/com/davidmiguel/gobees/hive/HivePresenterTest.java | 2 +- .../davidmiguel/gobees/recording/RecordingPresenterTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/davidmiguel/gobees/hive/HivePresenterTest.java b/app/src/test/java/com/davidmiguel/gobees/hive/HivePresenterTest.java index 55c4f985..58d14f57 100644 --- a/app/src/test/java/com/davidmiguel/gobees/hive/HivePresenterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/hive/HivePresenterTest.java @@ -54,7 +54,7 @@ public void setupHivesPresenter() { RecordingMother.newDefaultRecording())); // Get a reference to the class under test - hivePresenter = new HivePresenter(goBeesRepository, hiveView, HIVE.getId()); + hivePresenter = new HivePresenter(goBeesRepository, hiveView, 0,HIVE.getId()); // The presenter won't update the view unless it's active when(hiveView.isActive()).thenReturn(true); diff --git a/app/src/test/java/com/davidmiguel/gobees/recording/RecordingPresenterTest.java b/app/src/test/java/com/davidmiguel/gobees/recording/RecordingPresenterTest.java index d66f3753..8ce4d753 100644 --- a/app/src/test/java/com/davidmiguel/gobees/recording/RecordingPresenterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/recording/RecordingPresenterTest.java @@ -48,7 +48,7 @@ public void setupHivesPresenter() { MockitoAnnotations.initMocks(this); // Get a reference to the class under test - presenter = new RecordingPresenter(goBeesRepository, view, HIVE_ID, DATE, DATE); + presenter = new RecordingPresenter(goBeesRepository, view, 0, HIVE_ID, DATE, DATE); // The presenter won't update the view unless it's active when(view.isActive()).thenReturn(true); @@ -62,7 +62,7 @@ public void start_showRecordingIntoView() { presenter.start(); // Callback is captured and invoked with stubbed recording - verify(goBeesRepository).getRecording(anyLong(), any(Date.class), any(Date.class), + verify(goBeesRepository).getRecording(anyLong(), anyLong(), any(Date.class), any(Date.class), getRecordingCallbackArgumentCaptor.capture()); getRecordingCallbackArgumentCaptor.getValue().onRecordingLoaded(RECORDING); From f4a4b19cd8d27b562707f7da1588e2dc05e84f3f Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Wed, 4 Jan 2017 18:25:56 +0100 Subject: [PATCH 30/32] Implement monitoring frame rate #135 --- .../gobees/camera/AndroidCameraImpl.java | 22 +- .../source/local/GoBeesLocalDataSource.java | 3 +- .../gobees/hive/HivePresenter.java | 2 +- .../gobees/monitoring/MonitoringService.java | 193 ++++++++++++------ .../gobees/monitoring/MonitoringSettings.java | 13 ++ .../MonitoringSettingsFragment.java | 15 ++ .../gobees/utils/DateTimeUtils.java | 3 + app/src/main/res/values/arrays.xml | 19 ++ app/src/main/res/values/strings.xml | 33 +++ app/src/main/res/xml/monitoring_settings.xml | 7 + 10 files changed, 234 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java b/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java index 257106e5..03f1d0fb 100644 --- a/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java +++ b/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java @@ -38,8 +38,8 @@ public class AndroidCameraImpl implements AndroidCamera, Camera.PreviewCallback private int mMaxFrameWidth; private int mMaxFrameHeight; private int mZoomRatio; - private long delay; - private long period; + private long initialDelay; + private long frameRate; private TakePhotoTask takePhotoTask; private Timer timer; private CameraFrame mCameraFrame; @@ -56,14 +56,15 @@ public class AndroidCameraImpl implements AndroidCamera, Camera.PreviewCallback * @param zoomRatio zoom ratio of the desired zoom value. */ public AndroidCameraImpl(AndroidCameraListener user, int cameraIndex, - int maxFrameWidth, int maxFrameHeight, int zoomRatio) { + int maxFrameWidth, int maxFrameHeight, int zoomRatio, + long initialDelay, long frameRate) { this.user = user; this.cameraIndex = cameraIndex; this.mMaxFrameWidth = maxFrameWidth; this.mMaxFrameHeight = maxFrameHeight; this.mZoomRatio = zoomRatio; - this.delay = 1000; - this.period = 1000; + this.initialDelay = initialDelay; + this.frameRate = frameRate; this.mThread = new CameraHandlerThread(this); this.takePhotoTask = new TakePhotoTask(); this.timer = new Timer(); @@ -124,9 +125,12 @@ public boolean isConnected() { @Override public void updateFrameRate(long delay, long period) { - this.delay = delay; - this.period = period; - timer.scheduleAtFixedRate(takePhotoTask, delay, delay); + this.timer.cancel(); + this.timer = new Timer(); + this.takePhotoTask = new TakePhotoTask(); + this.initialDelay = delay; + this.frameRate = period; + timer.scheduleAtFixedRate(takePhotoTask, delay, period); } /** @@ -157,7 +161,7 @@ void initCamera() { try { mCamera.setPreviewTexture(texture); mCamera.startPreview(); - timer.scheduleAtFixedRate(takePhotoTask, delay, period); + timer.scheduleAtFixedRate(takePhotoTask, initialDelay, frameRate); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index c464bcf8..cebd8361 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -298,7 +298,7 @@ public void execute(Realm realm) { @Override public void saveRecords(final long hiveId, @NonNull final List records, @NonNull SaveRecordingCallback callback) { - if (records.size() < 10) { + if (records.size() < 5) { // Recording too short callback.onRecordingTooShort(); return; @@ -356,7 +356,6 @@ public void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonN return; } // Get weather data - RealmResults meteoRecords1 = apiary.getMeteoRecords().where().findAll(); RealmResults meteoRecords = apiary.getMeteoRecords() .where() .greaterThanOrEqualTo("timestamp", records.first().getTimestamp()) diff --git a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java index 29745d1f..40789f78 100644 --- a/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/hive/HivePresenter.java @@ -44,7 +44,7 @@ public void result(int requestCode, int resultCode, Intent data) { loadRecordings(true); // Show message view.showSuccessfullySavedMessage(); - } else if (resultCode == Activity.RESULT_CANCELED) { + } else if (resultCode == Activity.RESULT_CANCELED && data != null) { // Get error type int error = data.getIntExtra(HiveRecordingsFragment.ARGUMENT_MONITORING_ERROR, -1); // Show error message diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java index c9f26ecb..0ff6c157 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java @@ -22,6 +22,7 @@ import com.davidmiguel.gobees.data.source.GoBeesDataSource; import com.davidmiguel.gobees.data.source.GoBeesDataSource.SaveRecordingCallback; import com.davidmiguel.gobees.data.source.cache.GoBeesRepository; +import com.davidmiguel.gobees.utils.DateTimeUtils; import com.davidmiguel.gobees.video.BeesCounter; import com.davidmiguel.gobees.video.ContourBeesCounter; @@ -37,32 +38,66 @@ /** * Monitoring service. * It reads the camera feed and run the bee counter algorithm in background. + * It also gets weather information periodically. + * Notes: + * - Intent with START action: starts monitoring. + * - Intent with STOP action: stops monitoring. + * - There is a delay of INITIAL_DELAY before the camera is opened (to avoid trepidations when the + * user manipulates the phone). + * - The first INITIAL_NUM_FRAMES frames are used to create a background model (they are not used + * to count bees). During this time, the frame rate is INITIAL_FRAME_RATE. + * - After the background model is created, the frame rate is set to the one configured by the user. + * - The recording must have more than 5 records, if not, it is ignored. + * - The first and last record of a recording always have numBees = -1 (this is used to know + * when the recording starts and ends). + * - The service gets and saves apiary weather data every WEATHER_REFRESH_RATE. + * - When the service ends, it calls the appropriate callback: */ @SuppressWarnings("deprecation") public class MonitoringService extends Service implements AndroidCameraListener { + // Intent argument public static final String ARGUMENT_MON_SETTINGS = "MONITORING_SETTINGS"; + // Intent actions public static final String START_ACTION = "start_action"; public static final String STOP_ACTION = "stop_action"; + // Notification id private static final int NOTIFICATION_ID = 101; - private static final int T_5_SECONDS = 5000; - private static final int T_10_SECONDS = 10000; - private static final int T_15_MINUTES = 900000; + // Delay before start recording + private static final int INITIAL_DELAY = DateTimeUtils.T_5_SECONDS; + // Frame rate while creating background model + private static final int INITIAL_FRAME_RATE = 300; + // Number of frames to create background model + private static final int INITIAL_NUM_FRAMES = 10; + // Number of last recording seconds to delete (they usually contains noise) + private static final int NUM_LAST_SEC_TO_DELETE = DateTimeUtils.T_5_SECONDS; + // Weather refresh rate + private static final int WEATHER_REFRESH_RATE = DateTimeUtils.T_15_MINUTES; + + // Service stuff private static MonitoringService INSTANCE = null; private final IBinder mBinder = new MonitoringBinder(); - private SaveRecordingCallback callback; + // Persistence private GoBeesRepository goBeesRepository; - private Apiary apiary; + private SaveRecordingCallback callback; private LinkedList records; + + // Camera and algorithm private AndroidCamera androidCamera; + private boolean openCVLoaded = false; private BeesCounter bc; - private MonitoringSettings monitoringSettings; + private int initialNumFrames; + private long startTime; + + // Weather private Timer timer; private FetchWeatherTask fetchWeatherTask; - private boolean openCVLoaded = false; - private long startTime; + + // Model info + private Apiary apiary; + private MonitoringSettings monitoringSettings; /** * Checks whether the service is running. @@ -98,10 +133,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Get apiary apiary = goBeesRepository.getApiaryBlocking(monitoringSettings.getApiaryId()); // Configurations - configOpenCV(); configBeeCounter(); configCamera(); Notification n = configNotification(); + configOpenCV(); // Start service in foreground startForeground(NOTIFICATION_ID, n); @@ -109,28 +144,34 @@ public int onStartCommand(Intent intent, int flags, int startId) { } else if (intent.getAction().equals(STOP_ACTION)) { // Release camera androidCamera.release(); - // Clean records - cleanRecords(); // Save records - goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records, new SaveRecordingCallback() { - @Override - public void onRecordingTooShort() { - stopService(); - callback.onRecordingTooShort(); - } + if (records.size() > 0) { + // Clean records + cleanRecords(); + // Save records on db + goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records, new SaveRecordingCallback() { + @Override + public void onRecordingTooShort() { + stopService(); + callback.onRecordingTooShort(); + } - @Override - public void onSuccess() { - stopService(); - callback.onSuccess(); - } + @Override + public void onSuccess() { + stopService(); + callback.onSuccess(); + } - @Override - public void onFailure() { - stopService(); - callback.onFailure(); - } - }); + @Override + public void onFailure() { + stopService(); + callback.onFailure(); + } + }); + } else { + stopService(); + callback.onRecordingTooShort(); + } } return START_STICKY; } @@ -167,6 +208,8 @@ public boolean isOpenCVLoaded() { @Override public void onCameraStarted(int width, int height) { + // Counter for creating background model with the first frames + initialNumFrames = 0; // Calculate start time (to be use in chronometer) Date now = new Date(); long elapsedRealtimeOffset = System.currentTimeMillis() - SystemClock.elapsedRealtime(); @@ -175,6 +218,17 @@ public void onCameraStarted(int width, int height) { @Override public void onPreviewFrame(CameraFrame cameraFrame) { + if (initialNumFrames < INITIAL_NUM_FRAMES) { + // To create background model + bc.countBees(cameraFrame.gray()); + bc.getProcessedFrame().release(); + initialNumFrames++; + return; + } else if (initialNumFrames == INITIAL_NUM_FRAMES) { + // After creating background model, set real configuration + androidCamera.updateFrameRate(0, monitoringSettings.getFrameRate()); + initialNumFrames++; + } // Process frame int numBees = bc.countBees(cameraFrame.gray()); bc.getProcessedFrame().release(); @@ -188,30 +242,7 @@ public void onPreviewFrame(CameraFrame cameraFrame) { * @return start time. */ public long getStartTime() { - return startTime; - } - - /** - * Config OpenCV (config callback and init OpenCV). - */ - private void configOpenCV() { - // OpenCV callback - BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) { - @Override - public void onManagerConnected(final int status) { - switch (status) { - case LoaderCallbackInterface.SUCCESS: - openCVLoaded = true; - startMonitoring(); - break; - default: - super.onManagerConnected(status); - break; - } - } - }; - // Init openCV - OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, loaderCallback); + return startTime + INITIAL_DELAY; } /** @@ -232,7 +263,9 @@ private void configCamera() { Camera.CameraInfo.CAMERA_FACING_BACK, monitoringSettings.getMaxFrameWidth(), monitoringSettings.getMaxFrameHeight(), - monitoringSettings.getZoomRatio()); + monitoringSettings.getZoomRatio(), + INITIAL_DELAY, + INITIAL_FRAME_RATE); } /** @@ -254,14 +287,38 @@ private Notification configNotification() { .setOngoing(true).build(); } + + /** + * Config OpenCV (config callback and init OpenCV). + * When OpenCV is ready, it starts monitoring. + */ + private void configOpenCV() { + // OpenCV callback + BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) { + @Override + public void onManagerConnected(final int status) { + switch (status) { + case LoaderCallbackInterface.SUCCESS: + openCVLoaded = true; + startMonitoring(); + break; + default: + super.onManagerConnected(status); + break; + } + } + }; + // Init openCV + OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, loaderCallback); + } + /** * Start monitoring (frames will be received via onPreviewFrame()). */ private void startMonitoring() { - // If apiary has location -> Start fetching weather data (each 15min) - // With a delay of 10 seconds (because first 5 seconds are ignored) + // If apiary has location -> Start fetching weather data (each WEATHER_REFRESH_RATE) if (apiary.hasLocation()) { - timer.scheduleAtFixedRate(fetchWeatherTask, T_10_SECONDS, T_15_MINUTES); + timer.scheduleAtFixedRate(fetchWeatherTask, getTotalInitialDelay(), WEATHER_REFRESH_RATE); } // Start camera if (!androidCamera.isConnected()) { @@ -278,18 +335,14 @@ private void stopService() { } /** - * Delete first and last records that usually contain noise and add two special records + * Delete last records that usually contain noise and add two special records * at the beginning and ending to know the limits of the recording. */ private void cleanRecords() { - long initTime = records.getFirst().getTimestamp().getTime(); - long endTime = records.getLast().getTimestamp().getTime(); - // Delete first seconds - while (records.size() > 0 && records.getFirst().getTimestamp().getTime() - initTime < T_5_SECONDS) { - records.removeFirst(); - } // Delete last seconds - while (records.size() > 0 && endTime - records.getLast().getTimestamp().getTime() < T_5_SECONDS) { + long endTime = records.getLast().getTimestamp().getTime(); + while (records.size() > 0 + && endTime - records.getLast().getTimestamp().getTime() < NUM_LAST_SEC_TO_DELETE) { records.removeLast(); } // Save initial and last record (to know the beginning and ending of the recording) @@ -299,6 +352,15 @@ private void cleanRecords() { } } + /** + * Get total initial delay: camera delay + creation of background model + 5 sec (of margin). + * + * @return total initial delay + */ + private long getTotalInitialDelay() { + return INITIAL_DELAY + INITIAL_FRAME_RATE * INITIAL_FRAME_RATE + DateTimeUtils.T_5_SECONDS; + } + /** * Class used for the client Binder. Because we know this service always * runs in the same process as its clients, we don't need to deal with IPC. @@ -311,6 +373,9 @@ MonitoringService getService(SaveRecordingCallback c) { } } + /** + * Task that makes a request to weather server and stores the received weather data. + */ private class FetchWeatherTask extends TimerTask { @Override public void run() { diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java index a5e44f02..fcd32121 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettings.java @@ -49,6 +49,11 @@ class MonitoringSettings implements Serializable { */ private int zoomRatio; + /** + * Frame rate (1000 = 1 frame per second...). + */ + private long frameRate; + public long getApiaryId() { return apiaryId; } @@ -112,4 +117,12 @@ int getZoomRatio() { void setZoomRatio(int zoomRatio) { this.zoomRatio = zoomRatio; } + + long getFrameRate() { + return frameRate; + } + + void setFrameRate(long frameRate) { + this.frameRate = frameRate; + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettingsFragment.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettingsFragment.java index e7e26458..bcfa25c8 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettingsFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringSettingsFragment.java @@ -69,6 +69,7 @@ public void initSettings() { bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_max_area_key))); bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_zoom_key))); bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_show_algo_output_key))); + bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_frame_rate_key))); } @Override @@ -107,6 +108,7 @@ public MonitoringSettings getMonitoringSettings() { monitoringSettings.setMinArea(getMinArea()); monitoringSettings.setMaxArea(getMaxArea()); monitoringSettings.setZoomRatio(getZoomRatio()); + monitoringSettings.setFrameRate(getFrameRate()); monitoringSettings.setMaxFrameWidth(640); monitoringSettings.setMaxFrameHeight(480); return monitoringSettings; @@ -285,4 +287,17 @@ private int getZoomRatio() { // Convert return Integer.parseInt(value); } + + /** + * Get frame ratio. + * + * @return frame ratio. + */ + private long getFrameRate() { + // Get value + String value = PreferenceManager.getDefaultSharedPreferences(getActivity()) + .getString(getString(R.string.pref_frame_rate_key), getString(R.string.pref_frame_rate_1min)); + // Convert + return Long.parseLong(value); + } } diff --git a/app/src/main/java/com/davidmiguel/gobees/utils/DateTimeUtils.java b/app/src/main/java/com/davidmiguel/gobees/utils/DateTimeUtils.java index a303dcbc..a390ba9b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/utils/DateTimeUtils.java +++ b/app/src/main/java/com/davidmiguel/gobees/utils/DateTimeUtils.java @@ -10,6 +10,9 @@ @SuppressWarnings("WeakerAccess") public class DateTimeUtils { + public static final int T_5_SECONDS = 5000; + public static final int T_15_MINUTES = 900000; + private static final long ONE_HOUR_IN_MS = 3600000; private static final long ONE_MIN_IN_MS = 60000; private static final long ONE_SEC_IN_MS = 1000; diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 987885ab..5fa37cec 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -40,4 +40,23 @@ @string/pref_zoom_x2 + + @string/pref_frame_rate_label_0_5sec + @string/pref_frame_rate_label_1sec + @string/pref_frame_rate_label_5sec + @string/pref_frame_rate_label_15sec + @string/pref_frame_rate_label_30sec + @string/pref_frame_rate_label_1min + @string/pref_frame_rate_label_5min + + + @string/pref_frame_rate_0_5sec + @string/pref_frame_rate_1sec + @string/pref_frame_rate_5sec + @string/pref_frame_rate_15sec + @string/pref_frame_rate_30sec + @string/pref_frame_rate_1min + @string/pref_frame_rate_5min + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0dd3d317..9d7ce2f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -301,6 +301,39 @@ 200 + + Frame rate + + 0.5 sec. + + 1 sec. + + 5 sec. + + 15 sec. + + 30 sec. + + 1 min. + + 5 min. + + framerate + + 500 + + 1000 + + 5000 + + 15000 + + 30000 + + 60000 + + 300000 + diff --git a/app/src/main/res/xml/monitoring_settings.xml b/app/src/main/res/xml/monitoring_settings.xml index 2f97d8b6..285807d6 100644 --- a/app/src/main/res/xml/monitoring_settings.xml +++ b/app/src/main/res/xml/monitoring_settings.xml @@ -47,5 +47,12 @@ android:key="@string/pref_zoom_key" android:title="@string/pref_zoom_label"/> + + \ No newline at end of file From 03e63ca64f1c8ca7129dbf9a46fb3588118ec27c Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 5 Jan 2017 02:07:40 +0100 Subject: [PATCH 31/32] Minor changes --- .../addeditapiary/AddEditApiaryFragment.java | 20 ++--- .../gobees/apiaries/ApiariesFragment.java | 4 +- .../gobees/apiaries/ApiariesPresenter.java | 2 - .../gobees/apiary/ApiaryHivesFragment.java | 8 +- .../gobees/camera/AndroidCameraImpl.java | 69 ++++++++--------- .../gobees/camera/AndroidCameraListener.java | 2 +- .../gobees/camera/CameraFrame.java | 34 ++++----- .../data/source/cache/GoBeesRepository.java | 76 +++++++++++-------- .../source/local/GoBeesLocalDataSource.java | 26 +++++-- .../source/network/WeatherDataSource.java | 1 - .../gobees/monitoring/MonitoringService.java | 71 ++++++++--------- .../gobees/recording/RecordingFragment.java | 27 +++---- .../gobees/utils/BaseViewHolder.java | 2 +- .../gobees/utils/TempValueFormatter.java | 5 -- app/src/main/res/layout/monitoring_act.xml | 15 ++-- app/src/main/res/values/colors.xml | 2 - .../gobees/video/ContourBeesCounterTest.java | 9 ++- 17 files changed, 200 insertions(+), 173 deletions(-) diff --git a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java index 87bf80e5..855b7a7d 100644 --- a/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/addeditapiary/AddEditApiaryFragment.java @@ -82,7 +82,7 @@ public void onClick(View view) { (FloatingActionButton) getActivity().findViewById(R.id.fab_add_apiary); fab.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(View view) { presenter.save(nameTextView.getText().toString(), notesTextView.getText().toString()); } @@ -108,13 +108,13 @@ public void setName(String name) { @Override public void setLocation(Location location) { - String sb = "(" + - String.valueOf(location.getLatitude()) + - ", " + - String.valueOf(location.getLongitude()) + - ") ±" + - Math.round(location.getAccuracy()) + - "m"; + String sb = "(" + + String.valueOf(location.getLatitude()) + + ", " + + String.valueOf(location.getLongitude()) + + ") ±" + + Math.round(location.getAccuracy()) + + "m"; locationTextView.setText(sb); } @@ -125,8 +125,8 @@ public void setNotes(String notes) { @Override public void setLocationIcon(boolean active) { - getLocationIcon.setColorFilter(active ? - ContextCompat.getColor(getContext(), R.color.colorPrimaryDark) : + getLocationIcon.setColorFilter(active + ? ContextCompat.getColor(getContext(), R.color.colorPrimaryDark) : ContextCompat.getColor(getContext(), R.color.colorAccent)); } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java index cb16f4f9..53a905de 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesFragment.java @@ -80,7 +80,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, (FloatingActionButton) getActivity().findViewById(R.id.fab_add_apiary); fab.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(View view) { presenter.addEditApiary(AddEditApiaryActivity.NEW_APIARY); } }); @@ -133,6 +133,8 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.menu_delete: presenter.deleteData(); break; + default: + return false; } return true; } diff --git a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java index b57ba1ad..0543475b 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiaries/ApiariesPresenter.java @@ -13,8 +13,6 @@ import java.util.Date; import java.util.List; -import io.realm.Realm; - /** * Listens to user actions from the UI ApiariesFragment, retrieves the data and updates the * UI as required. diff --git a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java index 310ff06a..1398e610 100644 --- a/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/apiary/ApiaryHivesFragment.java @@ -48,6 +48,12 @@ public class ApiaryHivesFragment extends Fragment private View noHivesView; private LinearLayout hivesView; + /** + * Get ApiaryHivesFragment instance. + * + * @param apiaryId apiary id. + * @return ApiaryHivesFragment instance. + */ public static ApiaryHivesFragment newInstance(long apiaryId) { Bundle arguments = new Bundle(); arguments.putLong(ARGUMENT_APIARY_ID, apiaryId); @@ -83,7 +89,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, (FloatingActionButton) getActivity().findViewById(R.id.fab_add_hive); fab.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(View view) { presenter.addEditHive(AddEditHiveActivity.NEW_HIVE); } }); diff --git a/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java b/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java index 03f1d0fb..1f1cef61 100644 --- a/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java +++ b/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraImpl.java @@ -29,20 +29,20 @@ @SuppressWarnings("deprecation") public class AndroidCameraImpl implements AndroidCamera, Camera.PreviewCallback { - private final static String TAG = "AndroidCameraImpl"; + private static final String TAG = "AndroidCameraImpl"; private final AndroidCameraListener user; - private final CameraHandlerThread mThread; + private final CameraHandlerThread cameraHandlerThread; private final int cameraIndex; - private android.hardware.Camera mCamera; - private int mMaxFrameWidth; - private int mMaxFrameHeight; - private int mZoomRatio; + private android.hardware.Camera camera; + private int maxframewidth; + private int maxFrameHeight; + private int zoomRatio; private long initialDelay; private long frameRate; private TakePhotoTask takePhotoTask; private Timer timer; - private CameraFrame mCameraFrame; + private CameraFrame cameraFrame; private SurfaceTexture texture = new SurfaceTexture(0); /** @@ -60,30 +60,30 @@ public AndroidCameraImpl(AndroidCameraListener user, int cameraIndex, long initialDelay, long frameRate) { this.user = user; this.cameraIndex = cameraIndex; - this.mMaxFrameWidth = maxFrameWidth; - this.mMaxFrameHeight = maxFrameHeight; - this.mZoomRatio = zoomRatio; + this.maxframewidth = maxFrameWidth; + this.maxFrameHeight = maxFrameHeight; + this.zoomRatio = zoomRatio; this.initialDelay = initialDelay; this.frameRate = frameRate; - this.mThread = new CameraHandlerThread(this); + this.cameraHandlerThread = new CameraHandlerThread(this); this.takePhotoTask = new TakePhotoTask(); this.timer = new Timer(); } @Override public void onPreviewFrame(byte[] frame, android.hardware.Camera camera) { - mCameraFrame.putFrameData(frame); - user.onPreviewFrame(mCameraFrame); + cameraFrame.putFrameData(frame); + user.onPreviewFrame(cameraFrame); } @Override public synchronized void connect() { - if (!user.isOpenCVLoaded()) { + if (!user.isOpenCvLoaded()) { return; } // Start camera - synchronized (mThread) { - mThread.openCamera(); + synchronized (cameraHandlerThread) { + cameraHandlerThread.openCamera(); } } @@ -95,32 +95,33 @@ public void release() { timer = null; takePhotoTask = null; // Release thread - mThread.interrupt(); + cameraHandlerThread.interrupt(); // Release camera - if (mCamera != null) { - mCamera.stopPreview(); - mCamera.setPreviewCallback(null); + if (camera != null) { + camera.stopPreview(); + camera.setPreviewCallback(null); try { - mCamera.setPreviewTexture(null); + camera.setPreviewTexture(null); } catch (IOException e) { Log.e(TAG, "Could not release preview-texture from camera."); } - mCamera.release(); - mCamera = null; + camera.release(); + camera = null; } // Release camera frame - if (mCameraFrame != null) { - mCameraFrame.release(); + if (cameraFrame != null) { + cameraFrame.release(); } // Release texture - if (texture != null) + if (texture != null) { texture.release(); + } } } @Override public boolean isConnected() { - return mCamera != null; + return camera != null; } @Override @@ -139,17 +140,17 @@ public void updateFrameRate(long delay, long period) { @SuppressWarnings("ConstantConditions") void initCamera() { // Get camera instance - mCamera = getCameraInstance(cameraIndex, mMaxFrameWidth, mMaxFrameHeight, mZoomRatio); - if (mCamera == null) { + camera = getCameraInstance(cameraIndex, maxframewidth, maxFrameHeight, zoomRatio); + if (camera == null) { return; } // Save frame size - Camera.Parameters params = mCamera.getParameters(); + Camera.Parameters params = camera.getParameters(); int mFrameWidth = params.getPreviewSize().width; int mFrameHeight = params.getPreviewSize().height; // Create frame mat Mat mFrame = new Mat(mFrameHeight + (mFrameHeight / 2), mFrameWidth, CvType.CV_8UC1); - mCameraFrame = new CameraFrame(mFrame, mFrameWidth, mFrameHeight); + cameraFrame = new CameraFrame(mFrame, mFrameWidth, mFrameHeight); // Config texture if (this.texture != null) { this.texture.release(); @@ -159,8 +160,8 @@ void initCamera() { user.onCameraStarted(mFrameWidth, mFrameHeight); // Set camera callbacks and start capturing try { - mCamera.setPreviewTexture(texture); - mCamera.startPreview(); + camera.setPreviewTexture(texture); + camera.startPreview(); timer.scheduleAtFixedRate(takePhotoTask, initialDelay, frameRate); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); @@ -284,7 +285,7 @@ private void setAutofocus(Camera camera) { private class TakePhotoTask extends TimerTask { @Override public void run() { - mCamera.setOneShotPreviewCallback(AndroidCameraImpl.this); + camera.setOneShotPreviewCallback(AndroidCameraImpl.this); } } } diff --git a/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraListener.java b/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraListener.java index dc18876e..47bf8be6 100644 --- a/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraListener.java +++ b/app/src/main/java/com/davidmiguel/gobees/camera/AndroidCameraListener.java @@ -10,7 +10,7 @@ public interface AndroidCameraListener { * * @return true / false. */ - boolean isOpenCVLoaded(); + boolean isOpenCvLoaded(); /** * This method is invoked when camera preview has started. After this method is invoked diff --git a/app/src/main/java/com/davidmiguel/gobees/camera/CameraFrame.java b/app/src/main/java/com/davidmiguel/gobees/camera/CameraFrame.java index 297de4ff..4c7810aa 100644 --- a/app/src/main/java/com/davidmiguel/gobees/camera/CameraFrame.java +++ b/app/src/main/java/com/davidmiguel/gobees/camera/CameraFrame.java @@ -9,11 +9,11 @@ */ public class CameraFrame { - private Mat mYuvFrameData; - private Mat mRgba; - private int mWidth; - private int mHeight; - private boolean mRgbaConverted; + private Mat yuvFrameData; + private Mat rgba; + private int width; + private int height; + private boolean rgbaConverted; /** * CameraFrame constructor. @@ -24,10 +24,10 @@ public class CameraFrame { */ CameraFrame(Mat frame, int width, int height) { super(); - mWidth = width; - mHeight = height; - mYuvFrameData = frame; - mRgba = new Mat(); + this.width = width; + this.height = height; + yuvFrameData = frame; + rgba = new Mat(); } /** @@ -36,7 +36,7 @@ public class CameraFrame { * @param frameData byte array with the data. */ synchronized void putFrameData(byte[] frameData) { - mYuvFrameData.put(0, 0, frameData); + yuvFrameData.put(0, 0, frameData); invalidate(); } @@ -46,14 +46,14 @@ synchronized void putFrameData(byte[] frameData) { * @return gray Mat. */ public Mat gray() { - return mYuvFrameData.submat(0, mHeight, 0, mWidth); + return yuvFrameData.submat(0, height, 0, width); } /** * Invalidates cached mat. */ private void invalidate() { - mRgbaConverted = false; + rgbaConverted = false; } /** @@ -62,17 +62,17 @@ private void invalidate() { * @return RGBA Mat. */ public Mat rgba() { - if (!mRgbaConverted) { - Imgproc.cvtColor(mYuvFrameData, mRgba, Imgproc.COLOR_YUV2BGR_NV12, 4); - mRgbaConverted = true; + if (!rgbaConverted) { + Imgproc.cvtColor(yuvFrameData, rgba, Imgproc.COLOR_YUV2BGR_NV12, 4); + rgbaConverted = true; } - return mRgba; + return rgba; } /** * Deallocates frame data. */ public void release() { - mRgba.release(); + rgba.release(); } } diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java index b291dd7e..bf36f231 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/cache/GoBeesRepository.java @@ -54,11 +54,18 @@ public class GoBeesRepository implements GoBeesDataSource { */ boolean cacheIsDirty = false; - private GoBeesRepository(GoBeesDataSource goBeesDataSource, WeatherDataSource weatherDataSource) { + private GoBeesRepository(GoBeesDataSource goBeesDataSource, + WeatherDataSource weatherDataSource) { this.goBeesDataSource = goBeesDataSource; this.weatherDataSource = weatherDataSource; } + /** + * Get GoBeesRepository instance. + * @param apiariesLocalDataSource local data source. + * @param weatherDataSource weather data source. + * @return GoBeesRepository instace. + */ public static GoBeesRepository getInstance(GoBeesDataSource apiariesLocalDataSource, WeatherDataSource weatherDataSource) { if (INSTANCE == null) { @@ -259,21 +266,24 @@ public void saveRecord(long hiveId, @NonNull Record record, @NonNull TaskCallbac } @Override - public void saveRecords(long hiveId, @NonNull List records, @NonNull SaveRecordingCallback callback) { + public void saveRecords(long hiveId, @NonNull List records, + @NonNull SaveRecordingCallback callback) { checkNotNull(callback); // Save record goBeesDataSource.saveRecords(hiveId, records, callback); } @Override - public void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback) { + public void getRecording(long apiaryId, long hiveId, Date start, Date end, + @NonNull GetRecordingCallback callback) { checkNotNull(callback); // Save record goBeesDataSource.getRecording(apiaryId, hiveId, start, end, callback); } @Override - public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { + public void deleteRecording(long hiveId, @NonNull Recording recording, + @NonNull TaskCallback callback) { checkNotNull(callback); // Delete recording goBeesDataSource.deleteRecording(hiveId, recording, callback); @@ -281,39 +291,42 @@ public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull @SuppressWarnings("ConstantConditions") @Override - public void updateApiariesCurrentWeather(final List apiariesToUpdate, @NonNull final TaskCallback callback) { + public void updateApiariesCurrentWeather(final List apiariesToUpdate, + @NonNull final TaskCallback callback) { checkNotNull(callback); // Prepare callback final List apiaries = Collections.synchronizedList(apiariesToUpdate); final AtomicBoolean error = new AtomicBoolean(false); final AtomicInteger counter = new AtomicInteger(0); - WeatherDataSource.GetWeatherCallback getWeatherCallback = new WeatherDataSource.GetWeatherCallback() { - @Override - public void onWeatherLoaded(int id, MeteoRecord meteoRecord) { - // Set weather - apiaries.get(id).setCurrentWeather(meteoRecord); - // Check if all apiaries have finished - int value = counter.incrementAndGet(); - if (value >= apiaries.size()) { - if (!error.get()) { - // Update weather in database and finish - goBeesDataSource.updateApiariesCurrentWeather(apiariesToUpdate, callback); - } else { - callback.onFailure(); + WeatherDataSource.GetWeatherCallback getWeatherCallback = + new WeatherDataSource.GetWeatherCallback() { + @Override + public void onWeatherLoaded(int id, MeteoRecord meteoRecord) { + // Set weather + apiaries.get(id).setCurrentWeather(meteoRecord); + // Check if all apiaries have finished + int value = counter.incrementAndGet(); + if (value >= apiaries.size()) { + if (!error.get()) { + // Update weather in database and finish + goBeesDataSource.updateApiariesCurrentWeather( + apiariesToUpdate, callback); + } else { + callback.onFailure(); + } + } } - } - } - @Override - public void onDataNotAvailable() { - error.set(true); - // Check if all apiaries have finished - int value = counter.incrementAndGet(); - if (value >= apiaries.size()) { - callback.onFailure(); - } - } - }; + @Override + public void onDataNotAvailable() { + error.set(true); + // Check if all apiaries have finished + int value = counter.incrementAndGet(); + if (value >= apiaries.size()) { + callback.onFailure(); + } + } + }; // Update weather for (int i = 0; i < apiariesToUpdate.size(); i++) { weatherDataSource.getCurrentWeather(i, apiariesToUpdate.get(i).getLocationLat(), @@ -323,7 +336,8 @@ public void onDataNotAvailable() { @SuppressWarnings("ConstantConditions") @Override - public void saveMeteoRecord(@NonNull final Apiary apiary, @NonNull final TaskCallback callback) { + public void saveMeteoRecord(@NonNull final Apiary apiary, + @NonNull final TaskCallback callback) { checkNotNull(apiary); checkNotNull(callback); weatherDataSource.getCurrentWeather(1, apiary.getLocationLat(), apiary.getLocationLong(), diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java index cebd8361..a5890df1 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/local/GoBeesLocalDataSource.java @@ -29,6 +29,11 @@ public class GoBeesLocalDataSource implements GoBeesDataSource { private GoBeesLocalDataSource() { } + /** + * Get GoBeesLocalDataSource instance. + * + * @return instance. + */ public static GoBeesLocalDataSource getInstance() { if (INSTANCE == null) { INSTANCE = new GoBeesLocalDataSource(); @@ -225,7 +230,8 @@ public void refreshHives(long apiaryId) { } @Override - public void saveHive(final long apiaryId, @NonNull final Hive hive, @NonNull TaskCallback callback) { + public void saveHive(final long apiaryId, @NonNull final Hive hive, + @NonNull TaskCallback callback) { try { realm.executeTransaction(new Realm.Transaction() { @Override @@ -277,7 +283,8 @@ public void getNextHiveId(@NonNull GetNextHiveIdCallback callback) { } @Override - public void saveRecord(final long hiveId, @NonNull final Record record, @NonNull TaskCallback callback) { + public void saveRecord(final long hiveId, @NonNull final Record record, + @NonNull TaskCallback callback) { try { realm.executeTransaction(new Realm.Transaction() { @Override @@ -331,7 +338,8 @@ public void execute(Realm realm) { } @Override - public void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonNull GetRecordingCallback callback) { + public void getRecording(long apiaryId, long hiveId, Date start, Date end, + @NonNull GetRecordingCallback callback) { // Get apiary Apiary apiary = realm.where(Apiary.class).equalTo("id", apiaryId).findFirst(); if (apiary == null || apiary.getMeteoRecords() == null) { @@ -368,7 +376,8 @@ public void getRecording(long apiaryId, long hiveId, Date start, Date end, @NonN } @Override - public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull TaskCallback callback) { + public void deleteRecording(long hiveId, @NonNull Recording recording, + @NonNull TaskCallback callback) { try { // Get hive Hive hive = realm.where(Hive.class).equalTo("id", hiveId).findFirst(); @@ -381,8 +390,10 @@ public void deleteRecording(long hiveId, @NonNull Recording recording, @NonNull final RealmResults records; records = hive.getRecords() .where() - .greaterThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) - .lessThanOrEqualTo("timestamp", DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) + .greaterThanOrEqualTo("timestamp", + DateTimeUtils.setTime(recording.getDate(), 0, 0, 0, 0)) + .lessThanOrEqualTo("timestamp", + DateTimeUtils.setTime(recording.getDate(), 23, 59, 59, 999)) .findAll(); // Delete records realm.executeTransaction(new Realm.Transaction() { @@ -400,7 +411,8 @@ public void execute(Realm realm) { @SuppressWarnings("ConstantConditions") @Override - public void updateApiariesCurrentWeather(final List apiariesToUpdate, @NonNull TaskCallback callback) { + public void updateApiariesCurrentWeather(final List apiariesToUpdate, + @NonNull TaskCallback callback) { try { // Save meteo records realm.executeTransaction(new Realm.Transaction() { diff --git a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java index 63911b99..9fdc0e12 100644 --- a/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java +++ b/app/src/main/java/com/davidmiguel/gobees/data/source/network/WeatherDataSource.java @@ -1,7 +1,6 @@ package com.davidmiguel.gobees.data.source.network; import android.os.AsyncTask; -import android.util.Log; import com.davidmiguel.gobees.data.model.MeteoRecord; diff --git a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java index 0ff6c157..9f865748 100644 --- a/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java +++ b/app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java @@ -77,7 +77,7 @@ public class MonitoringService extends Service implements AndroidCameraListener // Service stuff private static MonitoringService INSTANCE = null; - private final IBinder mBinder = new MonitoringBinder(); + private final IBinder binder = new MonitoringBinder(); // Persistence private GoBeesRepository goBeesRepository; @@ -86,7 +86,7 @@ public class MonitoringService extends Service implements AndroidCameraListener // Camera and algorithm private AndroidCamera androidCamera; - private boolean openCVLoaded = false; + private boolean openCvLoaded = false; private BeesCounter bc; private int initialNumFrames; private long startTime; @@ -129,16 +129,17 @@ public int onStartCommand(Intent intent, int flags, int startId) { // START action if (intent.getAction().equals(START_ACTION)) { // Get monitoring config - monitoringSettings = (MonitoringSettings) intent.getSerializableExtra(ARGUMENT_MON_SETTINGS); + monitoringSettings = + (MonitoringSettings) intent.getSerializableExtra(ARGUMENT_MON_SETTINGS); // Get apiary apiary = goBeesRepository.getApiaryBlocking(monitoringSettings.getApiaryId()); // Configurations configBeeCounter(); configCamera(); - Notification n = configNotification(); - configOpenCV(); + Notification not = configNotification(); + configOpenCv(); // Start service in foreground - startForeground(NOTIFICATION_ID, n); + startForeground(NOTIFICATION_ID, not); // STOP action } else if (intent.getAction().equals(STOP_ACTION)) { @@ -149,25 +150,26 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Clean records cleanRecords(); // Save records on db - goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records, new SaveRecordingCallback() { - @Override - public void onRecordingTooShort() { - stopService(); - callback.onRecordingTooShort(); - } - - @Override - public void onSuccess() { - stopService(); - callback.onSuccess(); - } - - @Override - public void onFailure() { - stopService(); - callback.onFailure(); - } - }); + goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records, + new SaveRecordingCallback() { + @Override + public void onRecordingTooShort() { + stopService(); + callback.onRecordingTooShort(); + } + + @Override + public void onSuccess() { + stopService(); + callback.onSuccess(); + } + + @Override + public void onFailure() { + stopService(); + callback.onFailure(); + } + }); } else { stopService(); callback.onRecordingTooShort(); @@ -198,12 +200,11 @@ public void onDestroy() { @Nullable @Override public IBinder onBind(Intent intent) { - return mBinder; + return binder; } - @Override - public boolean isOpenCVLoaded() { - return openCVLoaded; + public boolean isOpenCvLoaded() { + return openCvLoaded; } @Override @@ -274,7 +275,8 @@ private void configCamera() { private Notification configNotification() { // Intent to the monitoring activity (when the notification is clicked) Intent monitoringIntent = new Intent(this, MonitoringActivity.class); - monitoringIntent.putExtra(MonitoringFragment.ARGUMENT_HIVE_ID, monitoringSettings.getHiveId()); + monitoringIntent.putExtra(MonitoringFragment.ARGUMENT_HIVE_ID, + monitoringSettings.getHiveId()); PendingIntent pMonitoringIntent = PendingIntent.getActivity(this, 0, monitoringIntent, PendingIntent.FLAG_UPDATE_CURRENT); // Create notification @@ -292,14 +294,14 @@ private Notification configNotification() { * Config OpenCV (config callback and init OpenCV). * When OpenCV is ready, it starts monitoring. */ - private void configOpenCV() { + private void configOpenCv() { // OpenCV callback BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(final int status) { switch (status) { case LoaderCallbackInterface.SUCCESS: - openCVLoaded = true; + openCvLoaded = true; startMonitoring(); break; default: @@ -318,7 +320,8 @@ public void onManagerConnected(final int status) { private void startMonitoring() { // If apiary has location -> Start fetching weather data (each WEATHER_REFRESH_RATE) if (apiary.hasLocation()) { - timer.scheduleAtFixedRate(fetchWeatherTask, getTotalInitialDelay(), WEATHER_REFRESH_RATE); + timer.scheduleAtFixedRate(fetchWeatherTask, + getTotalInitialDelay(), WEATHER_REFRESH_RATE); } // Start camera if (!androidCamera.isConnected()) { @@ -358,7 +361,7 @@ private void cleanRecords() { * @return total initial delay */ private long getTotalInitialDelay() { - return INITIAL_DELAY + INITIAL_FRAME_RATE * INITIAL_FRAME_RATE + DateTimeUtils.T_5_SECONDS; + return INITIAL_DELAY + INITIAL_FRAME_RATE * INITIAL_NUM_FRAMES + DateTimeUtils.T_5_SECONDS; } /** diff --git a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java index b82f2e8c..34dbd480 100644 --- a/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java +++ b/app/src/main/java/com/davidmiguel/gobees/recording/RecordingFragment.java @@ -1,6 +1,5 @@ package com.davidmiguel.gobees.recording; -import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.NonNull; @@ -164,9 +163,10 @@ public boolean onOptionsItemSelected(MenuItem item) { NavUtils.navigateUpFromSameTask(getActivity()); return true; case R.id.menu_refresh: - break; + return true; + default: + return false; } - return true; } @Override @@ -279,12 +279,12 @@ private void setChartsVisibility(String chartVisible) { */ private void highlightChartIcon(String chartVisible) { int defaultColor = ContextCompat.getColor(getContext(), R.color.colorDivider); - tempIcon.setColorFilter(chartVisible.equals("temp") ? - ContextCompat.getColor(getContext(), R.color.colorTempIcon) : defaultColor); - rainIcon.setColorFilter(chartVisible.equals("rain") ? - ContextCompat.getColor(getContext(), R.color.colorRainIcon) : defaultColor); - windIcon.setColorFilter(chartVisible.equals("wind") ? - ContextCompat.getColor(getContext(), R.color.colorWindIcon) : defaultColor); + tempIcon.setColorFilter(chartVisible.equals("temp") + ? ContextCompat.getColor(getContext(), R.color.colorTempIcon) : defaultColor); + rainIcon.setColorFilter(chartVisible.equals("rain") + ? ContextCompat.getColor(getContext(), R.color.colorRainIcon) : defaultColor); + windIcon.setColorFilter(chartVisible.equals("wind") + ? ContextCompat.getColor(getContext(), R.color.colorWindIcon) : defaultColor); } /** @@ -317,7 +317,6 @@ private void setupBeesChart(List recordsList) { lineDataSet.setDrawCircles(false); lineDataSet.setLineWidth(1.8f); lineDataSet.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent)); - LineData data = new LineData(lineDataSet); // General setup beesChart.setDrawGridBackground(false); beesChart.setDrawBorders(false); @@ -353,7 +352,7 @@ private void setupBeesChart(List recordsList) { rightAxis.setDrawGridLines(true); rightAxis.setDrawAxisLine(false); // Add data - beesChart.setData(data); + beesChart.setData(new LineData(lineDataSet)); } /** @@ -401,7 +400,6 @@ private void setupTempChart(List meteo) { lineDataSet.setDrawFilled(true); lineDataSet.setFillColor(ContextCompat.getColor(getContext(), R.color.colorFillTempChart)); lineDataSet.setFillAlpha(255); - LineData data = new LineData(lineDataSet); // General setup tempChart.setDrawGridBackground(false); tempChart.setDrawBorders(false); @@ -421,7 +419,7 @@ private void setupTempChart(List meteo) { leftAxis.setAxisMinimum(minTemp - 5); tempChart.getAxisRight().setEnabled(false); // Add data - tempChart.setData(data); + tempChart.setData(new LineData(lineDataSet)); } @@ -463,7 +461,6 @@ private void setupRainChart(List meteo) { lineDataSet.setDrawFilled(true); lineDataSet.setFillColor(ContextCompat.getColor(getContext(), R.color.colorFillRainChart)); lineDataSet.setFillAlpha(255); - LineData data = new LineData(lineDataSet); // General setup rainChart.setDrawGridBackground(false); rainChart.setDrawBorders(false); @@ -483,7 +480,7 @@ private void setupRainChart(List meteo) { leftAxis.setAxisMinimum(0); rainChart.getAxisRight().setEnabled(false); // Add data - rainChart.setData(data); + rainChart.setData(new LineData(lineDataSet)); } diff --git a/app/src/main/java/com/davidmiguel/gobees/utils/BaseViewHolder.java b/app/src/main/java/com/davidmiguel/gobees/utils/BaseViewHolder.java index 975700a9..d943c3cd 100644 --- a/app/src/main/java/com/davidmiguel/gobees/utils/BaseViewHolder.java +++ b/app/src/main/java/com/davidmiguel/gobees/utils/BaseViewHolder.java @@ -8,6 +8,6 @@ public interface BaseViewHolder { * Load data from obj into the view holder. * @param obj object from which load data. */ - public void bind(@NonNull T obj); + void bind(@NonNull T obj); } diff --git a/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java b/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java index 5bcd4b00..8b6910fe 100644 --- a/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java +++ b/app/src/main/java/com/davidmiguel/gobees/utils/TempValueFormatter.java @@ -6,11 +6,6 @@ import com.github.mikephil.charting.formatter.IValueFormatter; import com.github.mikephil.charting.utils.ViewPortHandler; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - /** * Format label in xºC format. */ diff --git a/app/src/main/res/layout/monitoring_act.xml b/app/src/main/res/layout/monitoring_act.xml index 2bdb8747..c55660d0 100644 --- a/app/src/main/res/layout/monitoring_act.xml +++ b/app/src/main/res/layout/monitoring_act.xml @@ -1,11 +1,11 @@ - + - - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 88ea5e48..dcc319db 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -30,6 +30,4 @@ #80000000 #ffffff #d50000 - - #CFD8DC diff --git a/app/src/test/java/com/davidmiguel/gobees/video/ContourBeesCounterTest.java b/app/src/test/java/com/davidmiguel/gobees/video/ContourBeesCounterTest.java index 4b9a1cab..8013a199 100644 --- a/app/src/test/java/com/davidmiguel/gobees/video/ContourBeesCounterTest.java +++ b/app/src/test/java/com/davidmiguel/gobees/video/ContourBeesCounterTest.java @@ -97,14 +97,16 @@ private double calculateRelativeError(BeesCounter bc, String dataset) throws Exc long totalAbsoluteError = 0; long expectedNumBeesTotal = 0; - File expectedOutputs = TestUtils.getFileFromPath(this, "res/img/" + dataset + "/numBees.txt"); + File expectedOutputs = + TestUtils.getFileFromPath(this, "res/img/" + dataset + "/numBees.txt"); try (BufferedReader br = new BufferedReader(new FileReader(expectedOutputs))) { // Process NUM_FRAMES_SKIP frames to create background model for (String line; i <= NUM_FRAMES_SKIP && (line = br.readLine()) != null; i++) { bc.countBees(readFreame(i, dataset)); } // Compare beesCounter output with the expected output - int expectedNumBees, numBees; + int expectedNumBees; + int numBees; for (String line; (line = br.readLine()) != null; i++) { // Get number of bees in the frame (expected and output) expectedNumBeesTotal += expectedNumBees = Integer.parseInt(line); @@ -134,7 +136,8 @@ private double calculateRelativeError(BeesCounter bc, String dataset) throws Exc * @param numBees output number of bees. */ private void saveFrames(Mat frame, int id, int expectedNumBees, int numBees) { - TestUtils.saveMatToFile(frame, String.format("/img/%03d_e%d_o%d", id, expectedNumBees, numBees)); + TestUtils.saveMatToFile(frame, + String.format("/img/%03d_e%d_o%d", id, expectedNumBees, numBees)); } /** From ef178d5fef5cf2a9e5b5b9d932e0fa2c36e18b11 Mon Sep 17 00:00:00 2001 From: davidmigloz Date: Thu, 5 Jan 2017 02:09:39 +0100 Subject: [PATCH 32/32] Bumped version number to 0.5 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3b5a8eed..07e9d400 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.davidmiguel.gobees" minSdkVersion 19 targetSdkVersion 25 - versionCode 4 - versionName "v0.4" + versionCode 5 + versionName "v0.5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" }