package org.commcare.android.resource.installers;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.Vector;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.logic.GlobalConstants;
import org.commcare.android.util.AndroidCommCarePlatform;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.odk.provider.FormsProviderAPI;
import org.commcare.resources.model.MissingMediaException;
import org.commcare.resources.model.Resource;
import org.commcare.resources.model.ResourceInitializationException;
import org.commcare.resources.model.ResourceTable;
import org.commcare.resources.model.UnresolvedResourceException;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.Reference;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localizer;
import org.javarosa.core.util.OrderedHashtable;
import org.javarosa.core.util.PrefixTreeNode;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.xform.parse.XFormParseException;
import org.javarosa.xform.parse.XFormParser;
import org.odk.collect.android.jr.extensions.IntentExtensionParser;
import org.odk.collect.android.jr.extensions.PollSensorExtensionParser;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
/**
* @author ctsims
*
*/
public class XFormAndroidInstaller extends FileSystemInstaller {
String namespace;
String contentUri;
public XFormAndroidInstaller() {
}
public XFormAndroidInstaller(String localDestination, String upgradeDestination) {
super(localDestination, upgradeDestination);
}
/* (non-Javadoc)
* @see org.commcare.resources.model.ResourceInstaller#initialize(org.commcare.util.CommCareInstance)
*/
public boolean initialize(AndroidCommCarePlatform instance) throws ResourceInitializationException {
instance.registerXmlns(namespace, contentUri);
return true;
}
/*
* (non-Javadoc)
* @see org.commcare.android.resource.installers.FileSystemInstaller#customInstall(org.commcare.resources.model.Resource, org.javarosa.core.reference.Reference, boolean)
*/
@Override
protected int customInstall(Resource r, Reference local, boolean upgrade) throws IOException, UnresolvedResourceException {
//Ugh. Really need to sync up the Xform libs between ccodk and odk.
XFormParser.registerHandler("intent", new IntentExtensionParser());
XFormParser.registerStructuredAction("pollsensor", new PollSensorExtensionParser());
FormDef formDef;
try {
formDef = new XFormParser(new InputStreamReader(local.getStream(), "UTF-8")).parse();
} catch(XFormParseException xfpe) {
throw new UnresolvedResourceException(r, xfpe.getMessage(), true);
}
this.namespace = formDef.getInstance().schema;
if(namespace == null) { throw new UnresolvedResourceException(r, "Invalid XForm, no namespace defined", true);}
//TODO: Where should this context be?
ContentResolver cr = CommCareApplication._().getContentResolver();
ContentProviderClient cpc = cr.acquireContentProviderClient(FormsProviderAPI.FormsColumns.CONTENT_URI);
ContentValues cv = new ContentValues();
cv.put(FormsProviderAPI.FormsColumns.DISPLAY_NAME, "NAME");
cv.put(FormsProviderAPI.FormsColumns.DESCRIPTION, "NAME"); //nullable
cv.put(FormsProviderAPI.FormsColumns.JR_FORM_ID, formDef.getMainInstance().schema); // ?
cv.put(FormsProviderAPI.FormsColumns.FORM_FILE_PATH, local.getLocalURI());
cv.put(FormsProviderAPI.FormsColumns.FORM_MEDIA_PATH, GlobalConstants.MEDIA_REF);
//cv.put(FormsProviderAPI.FormsColumns.SUBMISSION_URI, "NAME"); //nullable
//cv.put(FormsProviderAPI.FormsColumns.BASE64_RSA_PUBLIC_KEY, "NAME"); //nullable
try {
Cursor existingforms = cr.query(FormsProviderAPI.FormsColumns.CONTENT_URI,
new String[] { FormsProviderAPI.FormsColumns._ID} ,
FormsProviderAPI.FormsColumns.JR_FORM_ID + "=?",
new String[] { formDef.getMainInstance().schema}, null);
if(existingforms.moveToFirst()) {
//we already have one form. Hopefully this is during an upgrade...
if(!upgrade) {
//Hm, error out?
}
//So we know there's another form here. We should wait until it's time for
//the upgrade and replace the pointer to here.
Uri recordId = ContentUris.withAppendedId(FormsProviderAPI.FormsColumns.CONTENT_URI, existingforms.getLong(0));
//Grab the URI we should update
this.contentUri = recordId.toString();
//TODO: Check to see if there is more than one form, and deal
} else {
Uri result = cpc.insert(FormsProviderAPI.FormsColumns.CONTENT_URI, cv);
this.contentUri = result.toString();
}
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
throw new IOException("couldn't talk to form database to install form");
}
return upgrade ? Resource.RESOURCE_STATUS_UPGRADE : Resource.RESOURCE_STATUS_INSTALLED;
}
/* (non-Javadoc)
* @see org.commcare.android.resource.installers.FileSystemInstaller#upgrade(org.commcare.resources.model.Resource, org.commcare.resources.model.ResourceTable)
*/
@Override
public boolean upgrade(Resource r) {
boolean fileUpgrade = super.upgrade(r);
if(!fileUpgrade) { return false;}
return updateFilePath();
}
/**
* At some point hopefully soon we're not going to be shuffling our xforms around like crazy, so updates will mostly involve
* just changing where the provider points.
*
* @return
*/
private boolean updateFilePath() {
String localRawUri;
try {
localRawUri = ReferenceManager._().DeriveReference(this.localLocation).getLocalURI();
} catch (InvalidReferenceException e) {
Logger.log(AndroidLogger.TYPE_RESOURCES, "Installed resource wasn't able to be derived from " + localLocation);
return false;
}
//We're maintaining this whole Content setup now, so we've goota update things when we move them.
ContentResolver cr = CommCareApplication._().getContentResolver();
ContentValues cv = new ContentValues();
cv.put(FormsProviderAPI.FormsColumns.FORM_FILE_PATH, new File(localRawUri).getAbsolutePath());
//Update the form file path
int updatedRows = cr.update(Uri.parse(this.contentUri), cv, null, null);
if(updatedRows > 1) {
throw new RuntimeException("Bad URI stored for xforms installer: " + this.contentUri);
} if(updatedRows == 0) {
return false;
}
return true;
}
public boolean revert(Resource r, ResourceTable table) {
if(!super.revert(r, table)) {
return false;
}
return updateFilePath();
}
public int rollback(Resource r) {
int newStatus = super.rollback(r);
if(newStatus == Resource.RESOURCE_STATUS_INSTALLED) {
if(updateFilePath()) {
return newStatus;
} else {
//BOOO!
return -1;
}
} else {
return newStatus;
}
}
/* (non-Javadoc)
* @see org.commcare.resources.model.ResourceInstaller#requiresRuntimeInitialization()
*/
public boolean requiresRuntimeInitialization() {
return true;
}
/* (non-Javadoc)
* @see org.javarosa.core.util.externalizable.Externalizable#readExternal(java.io.DataInputStream, org.javarosa.core.util.externalizable.PrototypeFactory)
*/
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
super.readExternal(in, pf);
this.namespace = ExtUtil.nullIfEmpty(ExtUtil.readString(in));
this.contentUri = ExtUtil.nullIfEmpty(ExtUtil.readString(in));
}
/* (non-Javadoc)
* @see org.javarosa.core.util.externalizable.Externalizable#writeExternal(java.io.DataOutputStream)
*/
public void writeExternal(DataOutputStream out) throws IOException {
super.writeExternal(out);
ExtUtil.writeString(out, ExtUtil.emptyIfNull(namespace));
ExtUtil.writeString(out, ExtUtil.emptyIfNull(contentUri));
}
public boolean verifyInstallation(Resource r, Vector<MissingMediaException> problems) {
//Check to see whether the formDef exists and reads correctly
FormDef formDef;
try {
Reference local = ReferenceManager._().DeriveReference(localLocation);
formDef = new XFormParser(new InputStreamReader(local.getStream(), "UTF-8")).parse();
} catch(Exception e) {
// something weird/bad happened here. first make sure storage is available
if(!CommCareApplication._().isStorageAvailable()) {
problems.addElement(new MissingMediaException(r, "Couldn't access your persisent storage. Please make sure your SD card is connected properly"));
}
problems.addElement(new MissingMediaException(r, "Form did not properly save into persistent storage"));
return true;
}
if(formDef==null){
System.out.println("formdef is null");
}
//Otherwise, we want to figure out if the form has media, and we need to see whether it's properly
//available
Localizer localizer = formDef.getLocalizer();
//get this out of the memory ASAP!
formDef = null;
if(localizer == null) {
//things are fine
return false;
}
for(String locale : localizer.getAvailableLocales()) {
OrderedHashtable<String, PrefixTreeNode> localeData = localizer.getLocaleData(locale);
for(Enumeration en = localeData.keys(); en.hasMoreElements() ; ) {
String key = (String)en.nextElement();
if(key.indexOf(";") != -1) {
//got some forms here
String form = key.substring(key.indexOf(";") + 1, key.length());
if(form.equals(FormEntryCaption.TEXT_FORM_VIDEO) ||
form.equals(FormEntryCaption.TEXT_FORM_AUDIO) ||
form.equals(FormEntryCaption.TEXT_FORM_IMAGE)) {
try {
String externalMedia = localeData.get(key).render();
Reference ref = ReferenceManager._().DeriveReference(externalMedia);
String localName = ref.getLocalURI();
try {
if(!ref.doesBinaryExist()) {
problems.addElement(new MissingMediaException(r,"Missing external media: " + localName, externalMedia));
}
} catch (IOException e) {
problems.addElement(new MissingMediaException(r,"Problem reading external media: " + localName, externalMedia));
}
} catch (InvalidReferenceException e) {
//So the problem is that this might be a valid entry that depends on context
//in the form, so we'll ignore this situation for now.
}
}
}
}
}
if(problems.size() == 0 ) { return false;}
return true;
}
}