/** AirCasting - Share your Air! Copyright (C) 2011-2012 HabitatMap, Inc. This program 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. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. You can contact the authors by email at <info@habitatmap.org> */ package pl.llp.aircasting.sync; import pl.llp.aircasting.Intents; import pl.llp.aircasting.R; import pl.llp.aircasting.android.Logger; import pl.llp.aircasting.api.SessionDriver; import pl.llp.aircasting.api.SyncDriver; import pl.llp.aircasting.api.data.CreateSessionResponse; import pl.llp.aircasting.api.data.DeleteSessionResponse; import pl.llp.aircasting.api.data.SyncResponse; import pl.llp.aircasting.event.SyncStateChangedEvent; import pl.llp.aircasting.helper.SettingsHelper; import pl.llp.aircasting.model.MeasurementStream; import pl.llp.aircasting.model.Note; import pl.llp.aircasting.model.Session; import pl.llp.aircasting.storage.repository.RepositoryException; import pl.llp.aircasting.storage.repository.SessionRepository; import pl.llp.aircasting.util.SyncState; import pl.llp.aircasting.util.http.HttpResult; import pl.llp.aircasting.util.http.Status; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.widget.Toast; import com.google.common.base.Predicate; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; import roboguice.inject.InjectResource; import roboguice.service.RoboIntentService; import java.util.Collection; import java.util.List; import java.util.UUID; import static com.google.common.collect.Iterables.find; import static com.google.common.collect.Lists.newArrayList; public class SyncService extends RoboIntentService { @Inject ConnectivityManager connectivityManager; @Inject SessionRepository sessionRepository; @Inject SyncDriver syncDriver; @Inject SettingsHelper settingsHelper; @Inject SessionDriver sessionDriver; @Inject SyncState syncState; @Inject Context context; @Inject EventBus events; @InjectResource(R.string.account_reminder) String accountReminder; @Inject SessionTimeFixer sessionTimes; public SyncService() { super(SyncService.class.getSimpleName()); } @Override protected void onHandleIntent(Intent intent) { try { syncState.startSync(); events.post(new SyncStateChangedEvent()); if (canUpload()) { sync(); } else if (!settingsHelper.hasCredentials()) { Intents.notifySyncUpdate(context, accountReminder); } } catch (SessionSyncException exception) { Toast.makeText(getBaseContext(), R.string.session_sync_failed, Toast.LENGTH_LONG); } finally { syncState.markSyncComplete(); events.post(new SyncStateChangedEvent()); } } private void sync() throws SessionSyncException { Iterable<Session> sessions = prepareSessions(sessionRepository.allCompleteSessions()); HttpResult<SyncResponse> result = syncDriver.sync(sessions); Status status = result.getStatus(); if(status == Status.ERROR || status == Status.FAILURE) { throw new SessionSyncException("Initial sync failed"); } SyncResponse syncResponse = result.getContent(); if (syncResponse != null) { sessionRepository.deleteSubmitted(); UUID[] upload = syncResponse.getUpload(); UUID[] deleted = syncResponse.getDeleted(); long[] download = syncResponse.getDownload(); deleteMarked(deleted); uploadSessions(upload); downloadSessions(download); } } private void deleteMarked(UUID[] deleted) { if(deleted.length == 0) return; for (UUID uuid : deleted) { sessionRepository.markSessionForRemoval(uuid); } Intents.notifySyncUpdate(context); sessionRepository.deleteSubmitted(); } List<Session> prepareSessions(List<Session> sessions) { List<Session> result = newArrayList(); for (Session session : sessions) { if(session.isLocationless() && !session.isFixed()) { continue; } boolean ignoreSession = deletedAnything(session); if (ignoreSession) { continue; } result.add(session); } if(sessions.size() > result.size()) { sessionRepository.deleteSubmitted(); } return result; } private boolean deletedAnything(Session session) { if(session.isMarkedForRemoval()) { DeleteSessionResponse response = sessionDriver.deleteSession(session).getContent(); if(response != null && (response.isSuccess() || response.noSuchSession())) { markSessionSubmittedForRemoval(session); } return true; } else { Collection<MeasurementStream> streams = session.getMeasurementStreams(); for (MeasurementStream stream : streams) { if(stream.isMarkedForRemoval()) { DeleteSessionResponse response = sessionDriver.deleteStreams(session).getContent(); if(response != null && (response.isSuccess() || response.noSuchSession())) { markStreamsSubmittedForRemoval(session); } } } } return false; } private void markStreamsSubmittedForRemoval(Session session) { Collection<MeasurementStream> streams = session.getMeasurementStreams(); for (MeasurementStream stream : streams) { if (stream.isMarkedForRemoval()) { stream.setSubmittedForRemoval(true); sessionRepository.updateStream(stream); } } } private void markSessionSubmittedForRemoval(Session session) { session.setSubmittedForRemoval(true); sessionRepository.update(session); } private boolean canUpload() { NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected() && (!settingsHelper.isSyncOnlyWifi() || networkInfo.getType() == ConnectivityManager.TYPE_WIFI) && settingsHelper.hasCredentials(); } private void uploadSessions(UUID[] onServer) { for (UUID uuid : onServer) { Session session = sessionRepository.loadFully(uuid); if (session != null && skipUpload(session)) { continue; } else { HttpResult<CreateSessionResponse> result = sessionDriver.create(session); if (result.getStatus() == Status.SUCCESS) { updateSession(session, result.getContent()); } } Intents.notifySyncUpdate(context); } } private boolean skipUpload(Session session) { return session == null || session.isMarkedForRemoval() || (session.isLocationless() && !session.isFixed()); } private void updateSession(Session session, CreateSessionResponse sessionResponse) { session.setLocation(sessionResponse.getLocation()); for (CreateSessionResponse.Note responseNote : sessionResponse.getNotes()) { final int number = responseNote.getNumber(); Note note = find(session.getNotes(), new Predicate<Note>() { @Override public boolean apply(Note note) { return note.getNumber() == number; } }); note.setPhotoPath(responseNote.getPhotoLocation()); } sessionRepository.update(session); } private void downloadSessions(long[] ids) { for (long id : ids) { HttpResult<Session> result = sessionDriver.show(id); if (result.getStatus() == Status.SUCCESS) { Session session = result.getContent(); if (session == null) { Logger.w("Session [" + id + "] couldn't "); } else if (!session.isFixed() && session.isIncomplete()) { Logger.w(String.format("Session [%s] lacks of some measurements.", id)); } else { try { fixTimesFromUTC(session); sessionRepository.save(session); } catch (RepositoryException e) { Logger.e("Error saving session [" + id + "]", e); } } } Intents.notifySyncUpdate(context); } } private void fixTimesFromUTC(Session session) { sessionTimes.fromUTCtoLocal(session); } }