package org.activityinfo.ui.client.local; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * 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/gpl-3.0.html>. * #L% */ import com.extjs.gxt.ui.client.event.BaseEvent; import com.extjs.gxt.ui.client.event.Listener; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.RunAsyncCallback; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.inject.Inject; import com.google.inject.Singleton; import org.activityinfo.i18n.shared.UiConstants; import org.activityinfo.legacy.client.Dispatcher; import org.activityinfo.legacy.client.remote.AbstractDispatcher; import org.activityinfo.legacy.client.remote.Remote; import org.activityinfo.legacy.shared.Log; import org.activityinfo.legacy.shared.command.Command; import org.activityinfo.legacy.shared.command.result.CommandResult; import org.activityinfo.ui.client.AppEvents; import org.activityinfo.ui.client.EventBus; import org.activityinfo.ui.client.inject.ClientSideAuthProvider; import org.activityinfo.ui.client.local.LocalStateChangeEvent.State; import org.activityinfo.ui.client.local.capability.LocalCapabilityProfile; import org.activityinfo.ui.client.local.capability.PermissionRefusedException; import org.activityinfo.ui.client.local.sync.*; import javax.inject.Provider; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; /** * This class keeps as much of the offline functionality behind a runAsync * clause to defer downloading the related JavaScript until the user actually * goes into offline mode. */ @Singleton public class LocalController extends AbstractDispatcher { public interface PromptConnectCallback { void onCancel(); void onTryToConnect(); } private final EventBus eventBus; private final Provider<Synchronizer> synchronizerProvider; private UiConstants uiConstants; private final Dispatcher remoteDispatcher; private final LocalCapabilityProfile capabilityProfile; private final Provider<SyncHistoryTable> historyTable; private Strategy activeStrategy; private Date lastSynced = null; @Inject public LocalController(EventBus eventBus, @Remote Dispatcher remoteDispatcher, Provider<Synchronizer> gateway, LocalCapabilityProfile capabilityProfile, UiConstants uiConstants, Provider<SyncHistoryTable> historyTable) { this.eventBus = eventBus; this.remoteDispatcher = remoteDispatcher; this.synchronizerProvider = gateway; this.capabilityProfile = capabilityProfile; this.uiConstants = uiConstants; this.historyTable = historyTable; Log.trace("OfflineManager: starting"); if (capabilityProfile.isOfflineModeSupported()) { activateStrategy(new LoadingLocalStrategy()); } else { activateStrategy(new NotInstalledStrategy()); } eventBus.addListener(AppEvents.INIT, new Listener<BaseEvent>() { @Override public void handleEvent(BaseEvent be) { fireStatus(); } }); } public Date getLastSyncTime() { return lastSynced; } public void install() { if (activeStrategy instanceof NotInstalledStrategy) { ((NotInstalledStrategy) activeStrategy).enableOffline(); } } public void synchronize() { if (activeStrategy instanceof LocalStrategy) { ((LocalStrategy) activeStrategy).synchronize(); } } public State getState() { return activeStrategy.getState(); } @Override public <T extends CommandResult> void execute(Command<T> command, AsyncCallback<T> callback) { activeStrategy.dispatch(command, callback); } private void activateStrategy(Strategy strategy) { try { this.activeStrategy = strategy; this.activeStrategy.activate(); fireStatus(); } catch (Exception caught) { // errors really ought to be handled by the strategy that is passing // control to us // but we can't afford to let an uncaught exception go as it could // leave the app // in a state of limbo Log.error("Uncaught exception while activatign strategy, defaulting to Not INstalled"); activateStrategy(new NotInstalledStrategy()); } } private void fireStatus() { eventBus.fireEvent(new LocalStateChangeEvent(this.activeStrategy.getState())); } private void loadSynchronizerImpl(final AsyncCallback<Synchronizer> callback) { Log.trace("loadSynchronizerImpl() starting..."); GWT.runAsync(new RunAsyncCallback() { @Override public void onFailure(Throwable throwable) { Log.trace("loadSynchronizerImpl() failed"); callback.onFailure(throwable); } @Override public void onSuccess() { Log.trace("loadSynchronizerImpl() succeeded"); Synchronizer impl = null; try { impl = synchronizerProvider.get(); } catch (Exception caught) { Log.error("SynchronizationImpl constructor threw exception", caught); callback.onFailure(caught); return; } callback.onSuccess(impl); } }); } private void reportFailure(Throwable throwable) { Log.error("Exception in offline controller", throwable); eventBus.fireEvent(new SyncErrorEvent(SyncErrorType.fromException(throwable))); } private abstract class Strategy { Strategy activate() { return this; } void dispatch(Command command, AsyncCallback callback) { // by default, we send to the server remoteDispatcher.execute(command, callback); } abstract State getState(); } /** * Strategy for handling the state in which offline mode is not at all * available. * <p/> * The only thing the user can do from here is start installation. */ private class NotInstalledStrategy extends Strategy { @Override public NotInstalledStrategy activate() { return this; } @Override State getState() { return State.UNINSTALLED; } public void enableOffline() { Log.trace("enablingOffline() started"); capabilityProfile.acquirePermission(new AsyncCallback<Void>() { @Override public void onSuccess(Void result) { activateStrategy(new InstallingStrategy()); } @Override public void onFailure(Throwable caught) { if (!(caught instanceof PermissionRefusedException)) { reportFailure(caught); } } }); } } /** * Strategy for handling the state in which installation is in progress. * Commands continue to be handled by the remote dispatcher during * installation */ private class InstallingStrategy extends Strategy { @Override State getState() { return State.INSTALLING; } @Override Strategy activate() { eventBus.fireEvent(new SyncStatusEvent(uiConstants.starting(), 0)); loadSynchronizerImpl(new AsyncCallback<Synchronizer>() { @Override public void onFailure(Throwable caught) { activateStrategy(new NotInstalledStrategy()); reportFailure(caught); } @Override public void onSuccess(final Synchronizer gateway) { gateway.install(new AsyncCallback<Void>() { @Override public void onFailure(Throwable caught) { activateStrategy(new NotInstalledStrategy()); LocalController.this.reportFailure(caught); } @Override public void onSuccess(Void result) { activateStrategy(new LocalStrategy(gateway)); } }); } }); return this; } } /** * This is a sort of purgatory state that occurs immediately after while * we're determining whether offline mode has been enabled and then if so, * while we'ere loading the offline module async fragment. */ private class LoadingLocalStrategy extends Strategy { /** * Commands cannot be executed until everything is loaded... */ private List<CommandRequest> pendingRequests; @Override State getState() { return State.CHECKING; } @Override Strategy activate() { pendingRequests = new ArrayList<CommandRequest>(); try { historyTable.get().get(new AsyncCallback<Date>() { @Override public void onSuccess(Date result) { loadModule(); } @Override public void onFailure(Throwable caught) { abandonShip(); } }); } catch (Exception e) { abandonShip(); } return this; } private void loadModule() { loadSynchronizerImpl(new AsyncCallback<Synchronizer>() { @Override public void onFailure(Throwable caught) { abandonShip(caught); } @Override public void onSuccess(final Synchronizer gateway) { gateway.validateOfflineInstalled(new AsyncCallback<Void>() { @Override public void onFailure(Throwable caught) { abandonShip(caught); } @Override public void onSuccess(Void result) { activateStrategy(new LocalStrategy(gateway)); doDispatch(pendingRequests); } }); } }); } @Override void dispatch(Command command, AsyncCallback callback) { pendingRequests.add(new CommandRequest(command, callback)); } void abandonShip(Throwable caught) { reportFailure(caught); abandonShip(); } // something went wrong while loading the async fragment or // in the boot up, revert to the uninstalled state. the user // can always reinstall. (not ideal, obviously) void abandonShip() { activateStrategy(new NotInstalledStrategy()); doDispatch(pendingRequests); } } /** * Strategy for handling the state during which the user is offline. We try * to handle commands locally if possible. When unsupported commands are * encountered, we offer the user the chance to connect. */ private final class LocalStrategy extends Strategy { private Synchronizer localManager; private LocalStrategy(Synchronizer localManager) { this.localManager = localManager; } public void synchronize() { localManager.synchronize(); } @Override State getState() { return State.INSTALLED; } @Override public LocalStrategy activate() { // ensure that's the user's authentication is persisted across sessions! ClientSideAuthProvider.persistAuthentication(); localManager.getLastSyncTime(new AsyncCallback<Date>() { @Override public void onSuccess(Date result) { lastSynced = result; eventBus.fireEvent(new SyncCompleteEvent(result)); // do an initial synchronization attempt localManager.synchronize(); } @Override public void onFailure(Throwable caught) { localManager.synchronize(); } }); return this; } @Override void dispatch(Command command, AsyncCallback callback) { localManager.execute(command, callback); } } private static class CommandRequest { private final Command command; private final AsyncCallback callback; public CommandRequest(Command command, AsyncCallback callback) { super(); this.command = command; this.callback = callback; } public void dispatch(Strategy strategy) { strategy.dispatch(command, callback); } } private void doDispatch(final Collection<CommandRequest> requests) { if (!requests.isEmpty()) { // wait until everything's been switched around Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { for (CommandRequest request : requests) { request.dispatch(activeStrategy); } } }); } } }