package org.commcare.tasks; import android.content.Context; import android.support.v4.util.Pair; import org.commcare.CommCareApp; import org.commcare.CommCareApplication; import org.commcare.dalvik.R; import org.commcare.engine.resource.AndroidResourceManager; import org.commcare.engine.resource.AppInstallStatus; import org.commcare.engine.resource.ResourceInstallUtils; import org.commcare.engine.resource.installers.LocalStorageUnavailableException; import org.commcare.logging.AndroidLogger; import org.commcare.resources.model.InstallCancelled; import org.commcare.resources.model.InvalidResourceException; import org.commcare.resources.model.Resource; import org.commcare.resources.model.ResourceTable; import org.commcare.resources.model.TableStateListener; import org.commcare.resources.model.UnresolvedResourceException; import org.commcare.utils.AndroidCommCarePlatform; import org.commcare.views.dialogs.PinnedNotificationWithProgress; import org.javarosa.core.services.Logger; import org.javarosa.xml.util.UnfullfilledRequirementsException; import java.util.Vector; /** * Stages an update for the seated app in the background. Does not perform * actual update. If the user opens the Update activity, this task will report * its progress to that activity. Enforces the constraint that only one * instance is ever running. * * Will be cancelled on user logout, but can still run if no user is logged in. * * @author Phillip Mates (pmates@dimagi.com) */ public class UpdateTask extends SingletonTask<String, Integer, ResultAndError<AppInstallStatus>> implements TableStateListener, InstallCancelled { private static UpdateTask singletonRunningInstance = null; private static final Object lock = new Object(); private final AndroidResourceManager resourceManager; private final CommCareApp app; private PinnedNotificationWithProgress pinnedNotificationProgress = null; private Context ctx; private String profileRef; private boolean wasTriggeredByAutoUpdate = false; private boolean taskWasCancelledByUser = false; private int currentProgress = 0; private int maxProgress = 0; private int authority; private UpdateTask() { TAG = UpdateTask.class.getSimpleName(); app = CommCareApplication.instance().getCurrentApp(); AndroidCommCarePlatform platform = app.getCommCarePlatform(); authority = Resource.RESOURCE_AUTHORITY_REMOTE; resourceManager = new AndroidResourceManager(platform); resourceManager.setUpgradeListeners(this, this); } public static UpdateTask getNewInstance() { synchronized (lock) { if (singletonRunningInstance == null) { singletonRunningInstance = new UpdateTask(); return singletonRunningInstance; } else { throw new IllegalStateException("An instance of " + TAG + " already exists."); } } } public static UpdateTask getRunningInstance() { synchronized (lock) { if (singletonRunningInstance != null && singletonRunningInstance.getStatus() == Status.RUNNING) { return singletonRunningInstance; } return null; } } /** * Attaches pinned notification with a progress bar the task, which will * report updates to and close down the notification. * * @param ctx For launching notification and localizing text. */ public void startPinnedNotification(Context ctx) { this.ctx = ctx; pinnedNotificationProgress = new PinnedNotificationWithProgress(ctx, "updates.pinned.download", "updates.pinned.progress", R.drawable.update_download_icon); } @Override protected final ResultAndError<AppInstallStatus> doInBackground(String... params) { profileRef = params[0]; setupUpdate(); try { return new ResultAndError<>(stageUpdate()); } catch (InvalidResourceException e) { ResourceInstallUtils.logInstallError(e, "Structure error ocurred during install|"); return new ResultAndError<>(AppInstallStatus.UnknownFailure, buildCombinedErrorMessage(e.resourceName, e.getMessage())); } catch (LocalStorageUnavailableException e) { ResourceInstallUtils.logInstallError(e, "Couldn't install file to local storage|"); return new ResultAndError<>(AppInstallStatus.NoLocalStorage, e.getMessage()); } catch (UnfullfilledRequirementsException e) { ResourceInstallUtils.logInstallError(e, "App resources are incompatible with this device|"); return new ResultAndError<>(AppInstallStatus.IncompatibleReqs, e.getMessage()); } catch (UnresolvedResourceException e) { return new ResultAndError<>(ResourceInstallUtils.processUnresolvedResource(e), e.getMessage()); } catch (Exception e) { ResourceInstallUtils.logInstallError(e, "Unknown error ocurred during install|"); return new ResultAndError<>(AppInstallStatus.UnknownFailure, e.getMessage()); } } private void setupUpdate() { ResourceInstallUtils.recordUpdateAttemptTime(app); if (wasTriggeredByAutoUpdate) { ResourceInstallUtils.recordAutoUpdateStart(app); } resourceManager.incrementUpdateAttempts(); Logger.log(AndroidLogger.TYPE_RESOURCES, "Beginning install attempt for profile " + profileRef); } private AppInstallStatus stageUpdate() throws UnfullfilledRequirementsException, UnresolvedResourceException { Resource profile = resourceManager.getMasterProfile(); boolean appInstalled = (profile != null && profile.getStatus() == Resource.RESOURCE_STATUS_INSTALLED); if (!appInstalled) { return AppInstallStatus.UnknownFailure; } String profileRefWithParams = ResourceInstallUtils.addParamsToProfileReference(profileRef); return resourceManager.checkAndPrepareUpgradeResources(profileRefWithParams, authority); } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); if (pinnedNotificationProgress != null) { pinnedNotificationProgress.handleTaskUpdate(values); } } @Override protected void onPostExecute(ResultAndError<AppInstallStatus> resultAndError) { super.onPostExecute(resultAndError); if (!resultAndError.data.isUpdateInCompletedState()) { resourceManager.processUpdateFailure(resultAndError.data, ctx, wasTriggeredByAutoUpdate); } else if (wasTriggeredByAutoUpdate) { // auto-update was successful or app was up-to-date. ResourceInstallUtils.recordAutoUpdateCompletion(app); } if (pinnedNotificationProgress != null) { pinnedNotificationProgress.handleTaskCompletion(resultAndError); } } @Override protected void onCancelled(ResultAndError<AppInstallStatus> resultAndError) { super.onCancelled(resultAndError); if (taskWasCancelledByUser && wasTriggeredByAutoUpdate) { // task may have been cancelled by logout, in which case we want // to keep trying to auto-update upon logging in again. ResourceInstallUtils.recordAutoUpdateCompletion(app); } taskWasCancelledByUser = false; if (pinnedNotificationProgress != null) { pinnedNotificationProgress.handleTaskCancellation(); } resourceManager.upgradeCancelled(); } @Override public void clearTaskInstance() { synchronized (lock) { singletonRunningInstance = null; } } /** * Calculate and report the resource install progress a table has made. */ @Override public void compoundResourceAdded(ResourceTable table) { Vector<Resource> resources = AndroidResourceManager.getResourceListFromProfile(table); currentProgress = 0; for (Resource r : resources) { int resourceStatus = r.getStatus(); if (resourceStatus == Resource.RESOURCE_STATUS_UPGRADE || resourceStatus == Resource.RESOURCE_STATUS_INSTALLED) { currentProgress += 1; } } maxProgress = resources.size(); incrementProgress(currentProgress, maxProgress); } @Override public void simpleResourceAdded() { incrementProgress(++currentProgress, maxProgress); } @Override public void incrementProgress(int complete, int total) { this.publishProgress(complete, total); } /** * Allows resource installation process to check if this task was cancelled */ @Override public boolean wasInstallCancelled() { return isCancelled(); } public int getProgress() { return currentProgress; } public int getMaxProgress() { return maxProgress; } /** * Register task as triggered by auto update; used to determine retry behaviour. */ public void setAsAutoUpdate() { wasTriggeredByAutoUpdate = true; } public void setLocalAuthority() { authority = Resource.RESOURCE_AUTHORITY_LOCAL; } /** * Record that task cancellation was triggered by user, not the app logging * out. Useful for knowing if an auto-update should resume or not upon next * login. */ public void cancelWasUserTriggered() { taskWasCancelledByUser = true; } public static boolean isCombinedErrorMessage(String message) { return message != null && message.startsWith("||"); } /** * Put both the resource in question and a detailed error message into one string for * ease of transport, which will be split out later and formatted into a user-readable * pinned notification */ private static String buildCombinedErrorMessage(String head, String tail) { return "||" + head + "==" + tail; } public static Pair<String, String> splitCombinedErrorMessage(String message) { String[] splitMessage = message.split("==", 2); return Pair.create(splitMessage[0].substring(2), splitMessage[1]); } }