/** * Copyright (C) 2016 eBusiness Information * * This file is part of OSM Contributor. * * OSM Contributor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OSM Contributor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OSM Contributor. If not, see <http://www.gnu.org/licenses/>. */ package io.jawg.osmcontributor.rest.managers; import android.app.Application; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import io.jawg.osmcontributor.ui.managers.PoiManager; import io.jawg.osmcontributor.database.dao.PoiDao; import io.jawg.osmcontributor.database.dao.PoiTypeDao; import io.jawg.osmcontributor.model.events.PoiTypesLoaded; import io.jawg.osmcontributor.model.events.PoisAndNotesDownloadedEvent; import io.jawg.osmcontributor.model.entities.Note; import io.jawg.osmcontributor.model.entities.Poi; import io.jawg.osmcontributor.model.entities.PoiType; import io.jawg.osmcontributor.rest.Backend; import io.jawg.osmcontributor.ui.managers.LoginManager; import io.jawg.osmcontributor.ui.managers.NoteManager; import io.jawg.osmcontributor.database.events.DbInitializedEvent; import io.jawg.osmcontributor.database.events.InitDbEvent; import io.jawg.osmcontributor.rest.events.PleaseUploadPoiChangesByIdsEvent; import io.jawg.osmcontributor.rest.events.SyncDownloadPoisAndNotesEvent; import io.jawg.osmcontributor.rest.events.SyncDownloadWayEvent; import io.jawg.osmcontributor.rest.events.SyncFinishUploadPoiEvent; import io.jawg.osmcontributor.rest.events.error.SyncConflictingNodeErrorEvent; import io.jawg.osmcontributor.rest.events.error.SyncNewNodeErrorEvent; import io.jawg.osmcontributor.rest.events.error.SyncUnauthorizedEvent; import io.jawg.osmcontributor.rest.events.error.SyncUploadRetrofitErrorEvent; import io.jawg.osmcontributor.utils.Box; import io.jawg.osmcontributor.utils.FlavorUtils; import io.jawg.osmcontributor.utils.OsmAnswers; import timber.log.Timber; /** * Manage the synchronisation of POIs and Notes between the backend and the application. */ public class SyncManager { Application application; PoiManager poiManager; NoteManager noteManager; PoiDao poiDao; PoiTypeDao poiTypeDao; EventBus bus; Backend backend; SyncWayManager syncWayManager; SyncNoteManager syncNoteManager; LoginManager loginManager; @Inject public SyncManager(Application application, PoiManager poiManager, LoginManager loginManager, NoteManager noteManager, PoiDao poiDao, PoiTypeDao poiTypeDao, EventBus bus, Backend backend, SyncWayManager syncWayManager, SyncNoteManager syncNoteManager) { this.application = application; this.poiManager = poiManager; this.noteManager = noteManager; this.loginManager = loginManager; this.poiDao = poiDao; this.poiTypeDao = poiTypeDao; this.bus = bus; this.backend = backend; this.syncWayManager = syncWayManager; this.syncNoteManager = syncNoteManager; } // ******************************** // ************ Events ************ // ******************************** @Subscribe(threadMode = ThreadMode.ASYNC) public void onSyncDownloadPoisAndNotesEvent(SyncDownloadPoisAndNotesEvent event) { syncDownloadPoiBox(event.getBox()); List<Note> notes = syncNoteManager.syncDownloadNotesInBox(event.getBox()); if (notes != null && notes.size() > 0) { noteManager.mergeBackendNotes(notes); } bus.post(new PoisAndNotesDownloadedEvent()); } @Subscribe(threadMode = ThreadMode.ASYNC) public void onSyncDownloadWayEvent(SyncDownloadWayEvent event) { syncWayManager.syncDownloadWay(event.getBox()); } @Subscribe(threadMode = ThreadMode.ASYNC) public void onPleaseUploadPoiChangesByIdsEvent(PleaseUploadPoiChangesByIdsEvent event) { if (loginManager.checkCredentials()) { remoteAddOrUpdateOrDeletePois(event.getComment(), event.getPoiIds(), event.getPoiNodeRefIds()); } else { bus.post(new SyncUnauthorizedEvent()); } } /** * Initialize the database if the Flavor is PoiStorage. * * @param event The initialization event. */ @Subscribe(threadMode = ThreadMode.ASYNC) public void onInitDbEvent(InitDbEvent event) { if (FlavorUtils.isPoiStorage()) { Timber.d("Initializing database ..."); syncDownloadPoiTypes(); bus.postSticky(new DbInitializedEvent()); } } // ******************************** // ************ public ************ // ******************************** /** * Download the list of PoiType from the backend and actualize the database. * <br/> * If there was new, modified or a deleted PoiType, send a * {@link PoiTypesLoaded} event containing the new list of PoiTypes. */ public void syncDownloadPoiTypes() { List<PoiType> poiTypes = backend.getPoiTypes(); PoiType dbPoiType; boolean modified = false; List<PoiType> oldTypes = poiTypeDao.queryForAll(); // Create and update the PoiTypes in database with the result from backend for (PoiType type : poiTypes) { dbPoiType = type.getBackendId() != null ? poiTypeDao.findByBackendId(type.getBackendId()) : null; if (dbPoiType != null) { type.setId(dbPoiType.getId()); } type = poiManager.savePoiType(type); if (!modified && !type.equals(dbPoiType)) { modified = true; } } //Delete from database the PoiTypes who aren't in the new list of the backend. for (PoiType type : oldTypes) { if (!poiTypes.contains(type)) { poiManager.deletePoiType(type); modified = true; } } if (modified) { bus.postSticky(new PoiTypesLoaded(poiManager.getPoiTypesSortedByName())); } } /** * Download from backend the list of Poi contained in the box. * Update the database with the obtained list. * * @param box The Box to synchronize with the database. */ public void syncDownloadPoiBox(final Box box) { if (FlavorUtils.isPoiStorage()) { // Sync PoiTypes before the Poi to be sure we have all PoiTypes syncDownloadPoiTypes(); } List<Poi> pois = backend.getPoisInBox(box); if (pois.size() > 0) { Timber.d("Updating %d nodes", pois.size()); poiManager.mergeFromOsmPois(pois, box); } else { Timber.d("No new node found in the area"); } } /** * Send in a unique changeSet all the new POIs, modified and suppressed ones from ids send in params. * <p/> * Send a {@link io.jawg.osmcontributor.rest.events.SyncFinishUploadPoiEvent} with the counts. * * @param comment The comment of the changeSet. * @param poisId Pois to upload * @param poiNodeRefsId PoisNodeRef to upload */ public void remoteAddOrUpdateOrDeletePois(String comment, List<Long> poisId, List<Long> poiNodeRefsId) { final List<Poi> pois = poiDao.queryForIds(poisId); final List<Poi> updatedPois = new ArrayList<>(); final List<Poi> newPois = new ArrayList<>(); final List<Poi> toDeletePois = new ArrayList<>(); updatedPois.addAll(syncWayManager.downloadPoiForWayEdition(poiNodeRefsId)); for (Poi p : pois) { if (p.getBackendId() == null) { newPois.add(p); continue; } if (p.getToDelete()) { toDeletePois.add(p); continue; } updatedPois.add(p); } remoteAddOrUpdateOrDeletePois(comment, poiDao.queryForAllUpdated(), poiDao.queryForAllNew(), poiDao.queryToDelete()); } private void remoteAddOrUpdateOrDeletePois(String comment, List<Poi> updatedPois, List<Poi> newPois, List<Poi> toDeletePois) { int successfullyAddedPoisCount = 0; int successfullyUpdatedPoisCount = 0; int successfullyDeletedPoisCount = 0; if (updatedPois.size() == 0 && newPois.size() == 0 && toDeletePois.size() == 0) { Timber.i("No new or updatable or to delete POIs to send to osm"); } else { Timber.i("Found %d new, %d updated and %d to delete POIs to send to osm", newPois.size(), updatedPois.size(), toDeletePois.size()); final String changeSetId = backend.initializeTransaction(comment); if (changeSetId != null) { successfullyAddedPoisCount = remoteAddPois(newPois, changeSetId); successfullyUpdatedPoisCount = remoteUpdatePois(updatedPois, changeSetId); successfullyDeletedPoisCount = remoteDeletePois(toDeletePois, changeSetId); } } bus.post(new SyncFinishUploadPoiEvent(successfullyAddedPoisCount, successfullyUpdatedPoisCount, successfullyDeletedPoisCount)); } // ********************************* // ************ private ************ // ********************************* /** * Add a List of POIs to the backend. * * @param pois The List of POIs to add to the backend. * @param changeSetId The changeSet in which the POIs are sent. * @return The number of POIs who where successfully added. */ private int remoteAddPois(List<Poi> pois, String changeSetId) { int count = 0; for (Poi poi : pois) { if (remoteAddPoi(poi, changeSetId)) { count++; } } return count; } /** * Add a Poi to the backend. * * @param poi The Poi to add to the backend. * @param changeSetId The changeSet in which the Poi is sent. * @return Whether the addition was a success or not. */ private boolean remoteAddPoi(final Poi poi, String changeSetId) { Backend.CreationResult creationResult = backend.addPoi(poi, changeSetId); switch (creationResult.getStatus()) { case SUCCESS: poi.setBackendId(creationResult.getBackendId()); poi.setUpdated(false); poiManager.savePoi(poi); OsmAnswers.remotePoiAction(poi.getType().getTechnicalName(), "add"); return true; case FAILURE_UNKNOWN: default: poiManager.deletePoi(poi); bus.post(new SyncNewNodeErrorEvent(poi.getName(), poi.getId())); return false; } } /** * Update a List of POIs to the backend. * * @param pois The List of POIs to update to the backend. * @param changeSetId The changeSet in which the POIs are sent. * @return The number of POIs who where successfully updated. */ private int remoteUpdatePois(List<Poi> pois, String changeSetId) { int count = 0; for (Poi poi : pois) { if (remoteUpdatePoi(poi, changeSetId)) { count++; } } return count; } /** * Update a Poi of the backend. * * @param poi The Poi to update. * @param changeSetId The changeSet in which the Poi is sent. * @return Whether the update was a success or not. */ private boolean remoteUpdatePoi(final Poi poi, String changeSetId) { Backend.UpdateResult updateResult = backend.updatePoi(poi, changeSetId); poiManager.deleteOldPoiAssociated(poi); switch (updateResult.getStatus()) { case SUCCESS: poi.setVersion(updateResult.getVersion()); poi.setUpdated(false); poiManager.savePoi(poi); OsmAnswers.remotePoiAction(poi.getType().getTechnicalName(), "update"); return true; case FAILURE_CONFLICT: bus.post(new SyncConflictingNodeErrorEvent(poi.getName(), poi.getId())); Timber.e("Couldn't update poi %s: conflict, redownloading last version of poi", poi); deleteAndRetrieveUnmodifiedPoi(poi); return false; case FAILURE_NOT_EXISTING: Timber.e("Couldn't update poi %s, it didn't exist. Deleting the incriminated poi", poi); poiManager.deletePoi(poi); bus.post(new SyncConflictingNodeErrorEvent(poi.getName(), poi.getId())); return false; case FAILURE_UNKNOWN: default: Timber.e("Couldn't update poi %s. Deleting the incriminated poi", poi); poiManager.deletePoi(poi); bus.post(new SyncUploadRetrofitErrorEvent(poi.getId())); return false; } } /** * Delete a List of POIs to the backend. * * @param pois The List of POIs to delete to the backend. * @param changeSetId The changeSet in which the POIs are sent. * @return The number of POIs who where successfully deleted. */ private int remoteDeletePois(List<Poi> pois, String changeSetId) { int count = 0; for (Poi poi : pois) { if (remoteDeletePoi(poi, changeSetId)) { count++; } } return count; } /** * Delete a Poi of the backend. * * @param poi The Poi to delete. * @param changeSetId The changeSet in which the Poi is sent. * @return Whether the delete was a success or not. */ private boolean remoteDeletePoi(final Poi poi, String changeSetId) { Backend.ModificationStatus modificationStatus = backend.deletePoi(poi, changeSetId); poiManager.deleteOldPoiAssociated(poi); switch (modificationStatus) { case SUCCESS: case FAILURE_NOT_EXISTING: OsmAnswers.remotePoiAction(poi.getType().getTechnicalName(), "delete"); poiManager.deletePoi(poi); return true; case FAILURE_CONFLICT: bus.post(new SyncConflictingNodeErrorEvent(poi.getName(), poi.getId())); Timber.e("Couldn't update poi %s: conflict, redownloading last version of poi", poi); deleteAndRetrieveUnmodifiedPoi(poi); return false; case FAILURE_UNKNOWN: default: Timber.e("Couldn't delete poi %s", poi); bus.post(new SyncUploadRetrofitErrorEvent(poi.getId())); return false; } } /** * Delete the Poi from the database and re-download it from the backend. * * @param poi The Poi to delete and re-download. */ // TODO manage way pois private void deleteAndRetrieveUnmodifiedPoi(final Poi poi) { poiManager.deletePoi(poi); if (poi.getBackendId() != null) { Poi backendPoi = backend.getPoiById(poi.getBackendId()); if (backendPoi != null) { poiManager.savePoi(backendPoi); } else { Timber.w("The poi with id %s couldn't be found ", poi.getBackendId()); } } } }