package org.commcare.android.resource.installers;
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;
import android.util.Log;
import org.commcare.CommCareApplication;
import org.commcare.engine.extensions.IntentExtensionParser;
import org.commcare.android.javarosa.PollSensorAction;
import org.commcare.engine.extensions.PollSensorExtensionParser;
import org.commcare.engine.extensions.XFormExtensionUtils;
import org.commcare.logging.AndroidLogger;
import org.commcare.provider.FormsProviderAPI;
import org.commcare.resources.model.MissingMediaException;
import org.commcare.resources.model.Resource;
import org.commcare.resources.model.ResourceTable;
import org.commcare.resources.model.UnresolvedResourceException;
import org.commcare.utils.AndroidCommCarePlatform;
import org.commcare.utils.GlobalConstants;
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.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 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.Hashtable;
import java.util.Vector;
/**
* @author ctsims
*/
public class XFormAndroidInstaller extends FileSystemInstaller {
private static final String TAG = XFormAndroidInstaller.class.getSimpleName();
private String namespace;
private String contentUri;
@SuppressWarnings("unused")
public XFormAndroidInstaller() {
// for externalization
}
public XFormAndroidInstaller(String localDestination, String upgradeDestination) {
super(localDestination, upgradeDestination);
}
@Override
public boolean initialize(AndroidCommCarePlatform instance, boolean isUpgrade) {
instance.registerXmlns(namespace, contentUri);
return true;
}
@Override
protected int customInstall(Resource r, Reference local, boolean upgrade) throws IOException, UnresolvedResourceException {
registerAndroidLevelFormParsers();
FormDef formDef;
try {
formDef = XFormExtensionUtils.getFormFromInputStream(local.getStream());
} 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.instance().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
Cursor existingforms = null;
try {
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 != null && 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");
} finally {
if (existingforms != null) {
existingforms.close();
}
}
if (cpc != null) {
cpc.release();
}
return upgrade ? Resource.RESOURCE_STATUS_UPGRADE : Resource.RESOURCE_STATUS_INSTALLED;
}
public static void registerAndroidLevelFormParsers() {
//Ugh. Really need to sync up the Xform libs between ccodk and odk.
XFormParser.registerHandler("intent", new IntentExtensionParser());
XFormParser.registerActionHandler(PollSensorAction.ELEMENT_NAME, new PollSensorExtensionParser());
}
@Override
public boolean upgrade(Resource r) {
boolean fileUpgrade = super.upgrade(r);
return fileUpgrade && 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.
*/
private boolean updateFilePath() {
String localRawUri;
try {
localRawUri = ReferenceManager.instance().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.instance().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);
}
return updatedRows != 0;
}
@Override
public boolean revert(Resource r, ResourceTable table) {
return super.revert(r, table) && updateFilePath();
}
@Override
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;
}
}
@Override
public boolean requiresRuntimeInitialization() {
return true;
}
@Override
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));
}
@Override
public void writeExternal(DataOutputStream out) throws IOException {
super.writeExternal(out);
ExtUtil.writeString(out, ExtUtil.emptyIfNull(namespace));
ExtUtil.writeString(out, ExtUtil.emptyIfNull(contentUri));
}
@Override
public boolean verifyInstallation(Resource r, Vector<MissingMediaException> problems) {
//Check to see whether the formDef exists and reads correctly
FormDef formDef;
try {
Reference local = ReferenceManager.instance().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.instance().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) {
Log.d(TAG, "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!
if (localizer == null) {
//things are fine
return false;
}
for (String locale : localizer.getAvailableLocales()) {
Hashtable<String, String> localeData = localizer.getLocaleData(locale);
for (Enumeration en = localeData.keys(); en.hasMoreElements(); ) {
String key = (String)en.nextElement();
if (key.contains(";")) {
//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);
Reference ref = ReferenceManager.instance().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.
}
}
}
}
}
return problems.size() != 0;
}
}