package org.commcare.android.tasks;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.Vector;
import javax.net.ssl.SSLHandshakeException;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.models.notifications.MessageTag;
import org.commcare.android.resource.installers.LocalStorageUnavailableException;
import org.commcare.android.tasks.templates.CommCareTask;
import org.commcare.android.util.AndroidCommCarePlatform;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.dalvik.application.CommCareApp;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.preferences.CommCarePreferences;
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.util.CommCarePlatform;
import org.commcare.xml.util.UnfullfilledRequirementsException;
import org.javarosa.core.services.Logger;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.SystemClock;
/**
* This task is responsible for
*
* @author ctsims
*
*/
public abstract class ResourceEngineTask<R> extends CommCareTask<String, int[], org.commcare.android.tasks.ResourceEngineTask.ResourceEngineOutcomes, R> implements TableStateListener {
public enum ResourceEngineOutcomes implements MessageTag {
/** App installed Succesfully **/
StatusInstalled("notification.install.installed"),
/** Missing resources could not be found during install **/
StatusMissing("notification.install.missing"),
/** Missing resources could not be found during install **/
StatusMissingDetails("notification.install.missing.withmessage"),
/** App is not compatible with current installation **/
StatusBadReqs("notification.install.badreqs"),
/** Unknown Error **/
StatusFailUnknown("notification.install.unknown"),
/** There's already an app installed **/
StatusFailState("notification.install.badstate"),
/** There's already an app installed **/
StatusNoLocalStorage("notification.install.nolocal"),
/** Install is fine **/
StatusUpToDate("notification.install.uptodate"),
/** Certificate was bad **/
StatusBadCertificate("notification.install.badcert");
ResourceEngineOutcomes(String root) {this.root = root;}
private final String root;
public String getLocaleKeyBase() { return root;}
public String getCategory() { return "install_update"; }
}
Context c;
CommCareApp app;
public static final int PHASE_CHECKING = 0;
public static final int PHASE_DOWNLOAD = 1;
public static final int PHASE_COMMIT = 2;
public static final long STATUS_UPDATE_WAIT_TIME = 1000; // wait time between dialog updates in milliseconds
protected UnresolvedResourceException missingResourceException = null;
protected int badReqCode = -1;
protected int phase = -1;
boolean upgradeMode = false;
boolean partialMode = false;
boolean startOverUpgrade;
//This boolean is set from CommCareSetupActivity -- If we are in keep trying mode for installation,
//we want to sleep in between attempts to launch this task
boolean shouldSleep;
protected String vAvailable;
protected String vRequired;
protected boolean majorIsProblem;
public ResourceEngineTask(Context c, boolean upgradeMode, boolean partialMode, CommCareApp app,
boolean startOverUpgrade, int taskId, boolean shouldSleep)
throws SessionUnavailableException{
this.partialMode = partialMode;
this.c = c;
this.upgradeMode = upgradeMode;
this.app = app;
this.startOverUpgrade = startOverUpgrade;
this.taskId = taskId;
this.shouldSleep = shouldSleep;
}
/* (non-Javadoc)
* @see android.os.AsyncTask#doInBackground(Params[])
*/
protected ResourceEngineOutcomes doTaskBackground(String... profileRefs) {
String profileRef = profileRefs[0];
AndroidCommCarePlatform platform = app.getCommCarePlatform();
SharedPreferences prefs =app.getAppPreferences();
//First of all, make sure we record that an attempt was started.
Editor editor = prefs.edit();
editor.putLong(CommCarePreferences.LAST_UPDATE_ATTEMPT, new Date().getTime());
editor.commit();
app.setupSandbox();
Logger.log(AndroidLogger.TYPE_RESOURCES, "Beginning install attempt for profile " + profileRefs[0]);
if (shouldSleep) SystemClock.sleep(2000);
try {
//This is replicated in the application in a few places.
ResourceTable global = platform.getGlobalResourceTable();
//Ok, should figure out what the state of this bad boy is.
Resource profile = global.getResourceWithId("commcare-application-profile");
boolean sanityTest1 = (profile != null && profile.getStatus() == Resource.RESOURCE_STATUS_INSTALLED);
if(upgradeMode){
if(!sanityTest1) return ResourceEngineOutcomes.StatusFailState;
global.setStateListener(this);
/* temporary is the upgrade table, which starts out in the state that it was left
* after the last install- partially populated if it stopped in middle, empty
* if the install was successful
*/
ResourceTable temporary = platform.getUpgradeResourceTable();
ResourceTable recovery = platform.getRecoveryTable();
temporary.setStateListener(this);
/*this populates the upgrade table with resources based on binary files,
* starting with the profile file. If the new profile is not a newer version,
* statgeUpgradeTable doesn't actually pull in all the new references
*/
platform.stageUpgradeTable(global, temporary, recovery, profileRef, startOverUpgrade);
Resource newProfile = temporary.getResourceWithId("commcare-application-profile");
if(!newProfile.isNewer(profile)) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "App Resources up to Date");
return ResourceEngineOutcomes.StatusUpToDate;
}
phase = PHASE_CHECKING;
//Replaces global table with temporary, or w/ recovery if something goes wrong
platform.upgrade(global, temporary, recovery);
//And see where we ended up to see whether an upgrade actually occurred
} else if(partialMode){
global.setStateListener(this);
platform.init(profileRef, global, false);
app.writeInstalled();
} else {
//this is a standard, clean install
if(sanityTest1) return ResourceEngineOutcomes.StatusFailState;
global.setStateListener(this);
platform.init(profileRef, global, false);
app.writeInstalled();
}
//Initialize them now that they're installed
CommCareApplication._().initializeGlobalResources(app);
//Alll goood, we need to set our current profile ref to either the one
//just used, or the auth ref, if one is available.
String authRef = platform.getCurrentProfile().getAuthReference() == null ? profileRef : platform.getCurrentProfile().getAuthReference();
prefs = app.getAppPreferences();
Editor edit = prefs.edit();
edit.putString("default_app_server", authRef);
edit.commit();
return ResourceEngineOutcomes.StatusInstalled;
} catch (LocalStorageUnavailableException e) {
e.printStackTrace();
tryToClearApp();
Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "Couldn't install file to local storage|" + e.getMessage());
return ResourceEngineOutcomes.StatusNoLocalStorage;
}catch (UnfullfilledRequirementsException e) {
e.printStackTrace();
badReqCode = e.getRequirementCode();
vAvailable = e.getAvailableVesionString();
vRequired= e.getRequiredVersionString();
majorIsProblem = e.getRequirementCode() == UnfullfilledRequirementsException.REQUIREMENT_MAJOR_APP_VERSION;
tryToClearApp();
Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "App resources are incompatible with this device|" + e.getMessage());
return ResourceEngineOutcomes.StatusBadReqs;
} catch (UnresolvedResourceException e) {
//couldn't find a resource, which isn't good.
e.printStackTrace();
tryToClearApp();
Throwable mExceptionCause = e.getCause();
if(mExceptionCause instanceof SSLHandshakeException){
Throwable mSecondExceptionCause = mExceptionCause.getCause();
if(mSecondExceptionCause instanceof CertificateException){
return ResourceEngineOutcomes.StatusBadCertificate;
}
}
missingResourceException = e;
Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "A resource couldn't be found, almost certainly due to the network|" + e.getMessage());
if(e.isMessageUseful()) {
return ResourceEngineOutcomes.StatusMissingDetails;
} else {
return ResourceEngineOutcomes.StatusMissing;
}
} catch(Exception e) {
e.printStackTrace();
tryToClearApp();
Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "Unknown error ocurred during install|" + e.getMessage());
return ResourceEngineOutcomes.StatusFailUnknown;
}
}
/**
* For now, never clear automatically - just let user choose when to retry vs. resume
*/
protected void tryToClearApp(){
//if(partialMode == false && upgradeMode == false){}
}
/* (non-Javadoc)
* @see android.os.AsyncTask#onProgressUpdate(Progress[])
*/
@Override
protected void onProgressUpdate(int[]... values) {
super.onProgressUpdate(values);
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTask#onPostExecute(java.lang.Object)
*/
@Override
protected void onPostExecute(ResourceEngineOutcomes result) {
super.onPostExecute(result);
//remove all references
c = null;
}
// last time in system millis that we updated the status dialog
long lastTime = 0;
public void resourceStateUpdated(ResourceTable table) {
// if last time isn't set or is less than our spacing count, do not perform status update
if(System.currentTimeMillis() - lastTime < ResourceEngineTask.STATUS_UPDATE_WAIT_TIME){
return;
}
Vector<Resource> resources = CommCarePlatform.getResourceListFromProfile(table);
//TODO: Better reflect upgrade status process
int score = 0;
for(Resource r : resources) {
switch(r.getStatus()) {
case Resource.RESOURCE_STATUS_UPGRADE:
//If we spot an upgrade after we've started the upgrade process,
//something now needs to be updated
if(phase == PHASE_CHECKING) {
this.phase = PHASE_DOWNLOAD;
}
score += 1;
break;
case Resource.RESOURCE_STATUS_INSTALLED:
score += 1;
break;
default:
score += 0;
break;
}
}
lastTime = System.currentTimeMillis();
incrementProgress(score, resources.size());
}
public void incrementProgress(int complete, int total) {
this.publishProgress(new int[] {complete, total, phase});
}
}