diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 89223042e..3a1e053f7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -9,7 +9,6 @@
android:largeScreens="true"
android:anyDensity="true"
/>
-
+
-
\ No newline at end of file
+
+
+
diff --git a/res/drawable/btn_black.xml b/res/drawable/btn_black.xml
new file mode 100644
index 000000000..9a2f8ab97
--- /dev/null
+++ b/res/drawable/btn_black.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/drawable/btn_blue.xml b/res/drawable/btn_blue.xml
new file mode 100644
index 000000000..32f9c6380
--- /dev/null
+++ b/res/drawable/btn_blue.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/drawable/btn_brown.xml b/res/drawable/btn_brown.xml
new file mode 100644
index 000000000..6d8d59031
--- /dev/null
+++ b/res/drawable/btn_brown.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/drawable/btn_green.xml b/res/drawable/btn_green.xml
new file mode 100644
index 000000000..d6f7855cb
--- /dev/null
+++ b/res/drawable/btn_green.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/drawable/btn_purple.xml b/res/drawable/btn_purple.xml
new file mode 100644
index 000000000..94e1c1437
--- /dev/null
+++ b/res/drawable/btn_purple.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/drawable/btn_red.xml b/res/drawable/btn_red.xml
new file mode 100644
index 000000000..216d100a4
--- /dev/null
+++ b/res/drawable/btn_red.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/drawable/btn_yellow.xml b/res/drawable/btn_yellow.xml
new file mode 100644
index 000000000..85439c88d
--- /dev/null
+++ b/res/drawable/btn_yellow.xml
@@ -0,0 +1,37 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/res/layout/password.xml b/res/layout/password.xml
index 02268f9fc..fe0456ce7 100644
--- a/res/layout/password.xml
+++ b/res/layout/password.xml
@@ -85,4 +85,40 @@
android:layout_alignTop="@id/pass_ok"
android:layout_toRightOf="@id/pass_ok"
android:text="@string/show_password"/>
-
\ No newline at end of file
+
+
+
+
+
diff --git a/res/layout/webdav_prefs.xml b/res/layout/webdav_prefs.xml
new file mode 100644
index 000000000..8c30eb510
--- /dev/null
+++ b/res/layout/webdav_prefs.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d68b6d4ed..bd55b1e1e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -60,4 +60,27 @@
-
\ No newline at end of file
+
+
+
+
diff --git a/src/com/keepassdroid/PasswordActivity.java b/src/com/keepassdroid/PasswordActivity.java
index 1f29b7d34..ccf5c95d5 100644
--- a/src/com/keepassdroid/PasswordActivity.java
+++ b/src/com/keepassdroid/PasswordActivity.java
@@ -21,14 +21,24 @@
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.net.URLDecoder;
+import net.temerity.davsync.DAVException;
+import net.temerity.davsync.DAVNetwork;
+import net.temerity.davsync.DAVProfile;
+
import android.app.Activity;
+import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -59,6 +69,7 @@
import com.keepassdroid.utils.Interaction;
import com.keepassdroid.utils.Util;
+
public class PasswordActivity extends LockingActivity {
private static final int MENU_ABOUT = Menu.FIRST;
@@ -130,7 +141,71 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
}
+
+ /* added by mcrawford */
+ protected void processSyncButtons()
+ {
+ FileDbHelper dbHelp = App.fileDbHelper; // database for recent files
+ Button syncbutton = (Button) findViewById(R.id.sync_button);
+ try {
+ final AlertDialog worked = new AlertDialog.Builder(this).setTitle("Success!").setMessage("File was synchronized successfully!").create();
+ final AlertDialog failed = new AlertDialog.Builder(this).setTitle("Failure!").setMessage("Unable to synchronize file!").create();
+
+ final DAVProfile prof = dbHelp.getDavProfile(mFileName);
+ syncbutton.setOnClickListener(new View.OnClickListener() {
+
+ public void onClick(View v) {
+ DAVNetwork net = new DAVNetwork(prof, new File(mFileName));
+ try {
+ if(net.sync())
+ worked.show();
+ else
+ failed.show();
+ } catch (Exception e) {
+ // FIXME: do something here
+ }
+ }
+ });
+ syncbutton.setEnabled(true);
+ syncbutton.setBackgroundDrawable(getResources().getDrawable(R.drawable.btn_green));
+ syncbutton.setText("Sync");
+ syncbutton.setTextColor(Color.WHITE);
+ ///TODO come back to this after webdav is configured.
+ } catch( DAVException ce ) {
+ syncbutton.setEnabled(false);
+ syncbutton.setBackgroundDrawable(null);
+ syncbutton.setText("No WebDAV Configuration Found");
+ syncbutton.setTextColor(Color.RED);
+ }
+
+ Button wdcbutton = (Button) findViewById(R.id.webdav_configure_button);
+ wdcbutton.setOnClickListener(new View.OnClickListener() {
+
+ public void onClick(View v) {
+ Intent x = new Intent(PasswordActivity.this, net.temerity.davsync.davsync.class);
+ x.putExtra("kdbfile",mFileName);
+ startActivityForResult(x, 0);
+ }
+ });
+ }
+
+
+ @Override
+ protected void onResume()
+ {
+ super.onResume();
+ processSyncButtons();
+ }
+
+ /* end added by mcrawford */
+
+ private boolean checkNetworkPermission() {
+ String permission = "android.permission.INTERNET";
+ int res = getBaseContext().checkCallingOrSelfPermission(permission);
+ return res == PackageManager.PERMISSION_GRANTED;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -179,7 +254,7 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.password);
populateView();
-
+
Button confirmButton = (Button) findViewById(R.id.pass_ok);
confirmButton.setOnClickListener(new OkClickHandler());
@@ -226,6 +301,9 @@ public void onClick(View v) {
}
});
+ /* added by mcrawford */
+ processSyncButtons();
+ /* end added by mcrawford */
retrieveSettings();
diff --git a/src/com/keepassdroid/fileselect/FileDbHelper.java b/src/com/keepassdroid/fileselect/FileDbHelper.java
index 6d39c5638..31d5d56e7 100644
--- a/src/com/keepassdroid/fileselect/FileDbHelper.java
+++ b/src/com/keepassdroid/fileselect/FileDbHelper.java
@@ -19,6 +19,8 @@
*/
package com.keepassdroid.fileselect;
+import net.temerity.davsync.DAVException;
+import net.temerity.davsync.DAVProfile;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
@@ -27,6 +29,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+
public class FileDbHelper {
public static final String LAST_FILENAME = "lastFile";
@@ -34,7 +37,7 @@ public class FileDbHelper {
private static final String DATABASE_NAME = "keepassdroid";
private static final String FILE_TABLE = "files";
- private static final int DATABASE_VERSION = 1;
+ private static final int DATABASE_VERSION = 2;
private static final int MAX_FILES = 5;
@@ -47,6 +50,8 @@ public class FileDbHelper {
"create table " + FILE_TABLE + " ( " + KEY_FILE_ID + " integer primary key autoincrement, "
+ KEY_FILE_FILENAME + " text not null, " + KEY_FILE_KEYFILE + " text, "
+ KEY_FILE_UPDATED + " integer not null);";
+ private static final String DATABASE_CREATE_DAV =
+ "CREATE TABLE IF NOT EXISTS dav_profiles ( filename TEXT PRIMARY KEY, hostname TEXT, resource TEXT, username TEXT, password TEXT );";
private final Context mCtx;
private DatabaseHelper mDbHelper;
@@ -63,6 +68,7 @@ private static class DatabaseHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
+ db.execSQL(DATABASE_CREATE_DAV);
// Migrate preference to database if it is set.
SharedPreferences settings = mCtx.getSharedPreferences("PasswordActivity", Context.MODE_PRIVATE);
@@ -88,7 +94,10 @@ public void onCreate(SQLiteDatabase db) {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- // Only one database version so far
+ // this new database will also contain DAV profiles
+ if( oldVersion == 1 && newVersion == 2 ) {
+ db.execSQL(DATABASE_CREATE_DAV);
+ }
}
private void deletePrefs(SharedPreferences prefs) {
@@ -104,6 +113,36 @@ private void deletePrefs(SharedPreferences prefs) {
}
}
+ public void addDavProfile(DAVProfile p) {
+ if( ! isOpen() ) {
+ open();
+ }
+ String q = "INSERT OR REPLACE INTO dav_profiles VALUES('"
+ + p.getFilename() + "','"
+ + p.getHostname() + "','"
+ + p.getResource() + "','"
+ + p.getUsername() + "','"
+ + p.getPassword() + "');";
+ mDb.execSQL(q);
+ }
+
+ public void delDavProfile(String filename) {
+ mDb.execSQL("DELETE FROM dav_profiles WHERE filename LIKE '" + filename + "';");
+ }
+
+ public DAVProfile getDavProfile(String filename) throws DAVException {
+ if( ! isOpen() ) {
+ open();
+ }
+ Cursor c = mDb.rawQuery("SELECT * FROM dav_profiles WHERE filename = '" + filename + "';", null);
+ if( c.getCount() == 0 ) {
+ throw new DAVException();
+ } else {
+ c.moveToFirst();
+ return new DAVProfile(c.getString(0), c.getString(1), c.getString(2), c.getString(3), c.getString(4));
+ }
+ }
+
public FileDbHelper(Context ctx) {
mCtx = ctx;
}
diff --git a/src/net/temerity/davsync/DAVException.java b/src/net/temerity/davsync/DAVException.java
new file mode 100644
index 000000000..3a8f637f1
--- /dev/null
+++ b/src/net/temerity/davsync/DAVException.java
@@ -0,0 +1,32 @@
+package net.temerity.davsync;
+
+import java.lang.Exception;
+import java.lang.Throwable;
+
+public class DAVException extends Exception {
+
+ private static final long serialVersionUID = -4879636486257695816L;
+ private String error = "unknown error";
+
+ public DAVException() {
+ super();
+ }
+
+ public DAVException(String msg) {
+ super(msg);
+ error = msg;
+ }
+
+ public DAVException(String msg, Throwable cause) {
+ super(msg, cause);
+ error = msg;
+ }
+
+ public DAVException(Throwable cause) {
+ super(cause);
+ }
+
+ public String toString() {
+ return new String(error);
+ }
+}
diff --git a/src/net/temerity/davsync/DAVNetwork.java b/src/net/temerity/davsync/DAVNetwork.java
new file mode 100644
index 000000000..392e9e8fc
--- /dev/null
+++ b/src/net/temerity/davsync/DAVNetwork.java
@@ -0,0 +1,336 @@
+package net.temerity.davsync;
+
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.File;
+import java.io.InputStream;
+import java.lang.IllegalArgumentException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.FileEntity;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.UsernamePasswordCredentials;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+public class DAVNetwork {
+ private DateFormat lmfmt; // last modified formatter
+ private File path;
+ private String url;
+ private Credentials creds;
+ private DefaultHttpClient client;
+ private XPathExpression expr = null;
+
+ public DAVNetwork(DAVProfile profile, File kpfile) {
+ url = "https://" + profile.getHostname() + profile.getResource();
+ //File sdcard = Environment.getExternalStorageDirectory();
+ path = kpfile;
+ //no need to create dirs since the file MUST already exist...
+ //path.getParentFile().mkdirs();
+ client = new DefaultHttpClient();
+
+ HttpParams params = client.getParams();
+ HttpConnectionParams.setConnectionTimeout(params, 5000);
+ HttpConnectionParams.setSoTimeout(params, 5000);
+
+ creds = new UsernamePasswordCredentials(profile.getUsername(), profile.getPassword());
+ AuthScope as = new AuthScope(profile.getHostname(), 443);
+ client.getCredentialsProvider().setCredentials(as, creds); // also: AuthScope.ANY
+
+ //CredentialsProvider cp = new BasicCredentialsProvider();
+ //cp.setCredentials(AuthScope.ANY, creds);
+ //client.setCredentialsProvider(cp);
+ lmfmt = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss Z");
+ }
+
+ private class DAVNamespaceContext implements NamespaceContext {
+
+ @Override public String getNamespaceURI(String prefix) {
+ if( prefix.equals("D") ) {
+ return "DAV:";
+ } else if( prefix.equals("lp1") ) {
+ return "DAV:";
+ } else if( prefix.equals("lp2") ) {
+ return "http://apache.org/dav/props/";
+ } else if( prefix.equals("lp3") ) {
+ return "http://subversion.tigris.org/xmlns/dav/";
+ }
+ return XMLConstants.NULL_NS_URI;
+ }
+
+ @Override public String getPrefix(String namespaceURI) {
+ return null;
+ }
+
+ @Override public Iterator getPrefixes(String namespaceURI) {
+ return null;
+ }
+
+ }
+
+ private Date getRemoteTimestamp() throws ClientProtocolException, IOException, ParserConfigurationException, SAXException {
+ final String TAG = "DAVNetwork::getRemoteTimestamp";
+
+ HttpPropFind hpf = new HttpPropFind(url);
+ HttpResponse hr = client.execute(hpf);
+ HttpEntity he = hr.getEntity();
+ if( he == null ) {
+ throw new SAXException("Caught illegal entity after executing PROPFIND");
+ }
+ InputStream xmlStream = he.getContent();
+
+ long xmlLength = he.getContentLength();
+ if (xmlLength < 0) {
+ Log.d(TAG, "Could not get content length for PROPFIND: " + xmlLength);
+ throw new IOException("Unknown MultiStatus length from PROPFIND");
+ }
+
+ byte[] xmlData = new byte[(int) xmlLength];
+ int offset = 0, bytesRead = 0;
+ while (offset < xmlLength && bytesRead >= 0) {
+ bytesRead = xmlStream.read(xmlData, offset, xmlData.length - offset);
+ offset += bytesRead;
+ }
+
+ xmlStream.close();
+ he.consumeContent();
+
+ if( offset < xmlData.length ) {
+ Log.d(TAG, "FATAL: got " + offset + " bytes, expecting " + xmlLength + " bytes");
+ throw new IOException("Unable to determine remote properties");
+ }
+
+ // parse the XML
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ Document doc = db.parse(new ByteArrayInputStream(xmlData));
+
+ if( expr == null ) {
+ XPathFactory factory = XPathFactory.newInstance();
+ XPath xpath = factory.newXPath();
+ xpath.setNamespaceContext(new DAVNamespaceContext());
+ try {
+ //expr = xpath.compile("/D:multistatus/D:response/D:propstat/D:prop/*[starts-with(name(), 'lp1:')]");
+ expr = xpath.compile("/D:multistatus/D:response/D:propstat/D:prop/lp1:getlastmodified");
+ } catch (XPathExpressionException e) {
+ Log.d(TAG, "Unable to compile XPath expression: " + e);
+ throw new SAXException("Unable to compile XPath expression");
+ }
+ }
+
+ NodeList nodes;
+ try {
+ nodes = (NodeList)expr.evaluate(doc, XPathConstants.NODESET);
+ } catch (XPathExpressionException e) {
+ Log.d(TAG, "Unable to compile XPath expression");
+ throw new SAXException("Unable to compile XPath expression");
+ }
+
+ if( nodes.getLength() != 1 ) {
+ Log.d(TAG, "timestamp count for target != 1");
+ throw new SAXException("Received incorrect PROPFIND XML");
+ }
+
+ try {
+ Date lastModified = (Date)lmfmt.parse(nodes.item(0).getTextContent());
+ Log.d(TAG, "Success: timestamp = " + lastModified);
+ return lastModified;
+ } catch (DOMException e) {
+ Log.d(TAG, "Date content string too long");
+ } catch (ParseException e) {
+ Log.d(TAG, "Failed to parse date from WebDAV server");
+ }
+ throw new SAXException(); // should never get here
+
+ /*
+ SAXParserFactory spf = SAXParserFactory.newInstance();
+ SAXParser sp = spf.newSAXParser();
+ PropFindHandler pfh = new PropFindHandler();
+ XMLReader xr = sp.getXMLReader();
+ xr.setContentHandler(pfh);
+ xr.parse(new InputSource(new ByteArrayInputStream(xmlData)));
+ return pfh.lastModified;
+ */
+ }
+
+ public boolean testRemote() {
+ final String TAG = "DAVNetwork::testRemote";
+ try {
+ HttpPropFind hpf = new HttpPropFind(url);
+ HttpResponse hr = client.execute(hpf);
+ int ret = hr.getStatusLine().getStatusCode();
+
+ HttpEntity he = hr.getEntity();
+ if( he != null ) {
+ he.consumeContent();
+ }
+
+ if( ret == HttpStatus.SC_MULTI_STATUS || ret == HttpStatus.SC_NOT_FOUND ) {
+ Log.d(TAG, "PROPFIND succeeded on remote resource and returned " + ret);
+ return true;
+ } else {
+ Log.d(TAG, "PROPFIND failed on remote resource and returned " + ret);
+ return false;
+ }
+ } catch (ClientProtocolException e) {
+ Log.d(TAG, "Caught ClientProtocolException: " + e);
+ return false;
+ } catch (IOException e) {
+ Log.d(TAG, "Caught IOException: " + e);
+ return false;
+ }
+ }
+
+ public boolean sync() throws IOException, IllegalArgumentException {
+ final String TAG = "DAVNetwork::sync";
+ Date date_remote, date_local;
+ boolean has_remote, has_local;
+
+ // get local & remote info
+ try {
+ date_remote = getRemoteTimestamp();
+ has_remote = true;
+ } catch( Exception e ) {
+ date_remote = new Date(1900, 1, 1);
+ has_remote = false;
+ }
+ if( path.exists() ) {
+ date_local = new Date(path.lastModified());
+ has_local = true;
+ } else {
+ date_local = new Date(1900, 1, 1);
+ has_local = false;
+ }
+
+ // do the sync
+ if( has_local == true && has_remote == false ) {
+ Log.d(TAG, "Uploading local file");
+ return upload(date_local);
+ } else if( has_local == false && has_remote == true ) {
+ Log.d(TAG, "Downloading remote file");
+ return download(date_remote);
+ } else if( has_local == false && has_remote == false ) {
+ // this should never happen
+ Log.d(TAG, "New KDB file creation unimplemented");
+ return false;
+ } else {
+ Log.d(TAG, date_remote.toString() + " <=> " + date_local.toString() );
+
+ int comparator = date_local.compareTo(date_remote);
+ if( comparator < 0 ) {
+ Log.d(TAG, "Final sync decision: download");
+ return download(date_remote);
+ } else if( comparator > 0 ) {
+ Log.d(TAG, "Final sync decision: upload");
+ return upload(date_local);
+ } else {
+ // the files are already synced, we do nothing
+ Log.d(TAG, "Final sync decision: the files are equal");
+ return true;
+ }
+ }
+ }
+
+ // FIXME: is it possible to set the date on the uploaded file?
+ private boolean upload(Date modified) {
+ int ret = -1;
+ final String TAG = "DAVNetwork::upload";
+
+ HttpPut pm = new HttpPut(url);
+ pm.setEntity(new FileEntity(path, "binary/octet-stream"));
+
+ try {
+ HttpResponse hr = client.execute(pm);
+ ret = hr.getStatusLine().getStatusCode();
+
+ HttpEntity he = hr.getEntity();
+ if( he != null ) {
+ he.consumeContent();
+ }
+
+ if( ret >= 200 && ret <= 226 ) {
+ Log.d(TAG, "Put method successfully completed");
+ /* hack, hack, hack: set the local file's modification date equal to the remote's */
+ try {
+ Date d = getRemoteTimestamp();
+ if( ! path.setLastModified( d.getTime() ) )
+ Log.d(TAG, "Could not set local timestamp [1]");
+ } catch( Exception e ) {
+ Log.d(TAG, "Could not set local timestamp [2]");
+ }
+ return true;
+ } else {
+ Log.d(TAG, "Failed to execute Put method: " + ret);
+ return false;
+ }
+ } catch( IOException ioe ) {
+ Log.d(TAG, "Caught IOException while uploading file");
+ return false;
+ }
+ // common: HttpStatus.SC_NO_CONTENT HttpStatus.SC_CREATED HttpStatus.SC_OK
+ }
+
+ private boolean download(Date modified) {
+ int ret = -1;
+ final String TAG = "DAVNetwork::upload";
+ HttpGet gm = new HttpGet(url);
+ try {
+ HttpResponse hr = client.execute(gm);
+ ret = hr.getStatusLine().getStatusCode();
+ HttpEntity entity = hr.getEntity();
+ if (ret == HttpStatus.SC_OK && entity != null ) {
+ // long rlen = entity.getContentLength();
+ InputStream input = entity.getContent();
+ FileOutputStream output = new FileOutputStream(path, false);
+ int count = -1;
+ byte[] buffer = new byte[8192];
+ while( (count = input.read(buffer)) != -1 ) {
+ output.write(buffer, 0, count);
+ }
+ output.flush();
+ output.close();
+ input.close();
+ if( ! path.setLastModified(modified.getTime()) ) {
+ Log.w(TAG, "Failed to set local last-modified time equal to remote");
+ }
+ }
+ entity.consumeContent();
+ return true;
+ } catch( IOException ioe ) {
+ Log.d(TAG, "Caught IOException while downloading file");
+ return false;
+ }
+ }
+}
diff --git a/src/net/temerity/davsync/DAVProfile.java b/src/net/temerity/davsync/DAVProfile.java
new file mode 100644
index 000000000..5f65093f1
--- /dev/null
+++ b/src/net/temerity/davsync/DAVProfile.java
@@ -0,0 +1,38 @@
+package net.temerity.davsync;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.Properties;
+
+public class DAVProfile {
+ private String filename, username, password, hostname, resource;
+
+ public DAVProfile(String filename, String hostname, String resource, String username, String password) {
+ this.filename = new String(filename);
+ this.username = new String(username);
+ this.password = new String(password);
+ this.hostname = new String(hostname);
+ this.resource = new String(resource);
+ }
+
+ public String getFilename() {
+ return new String(filename);
+ }
+
+ public String getHostname() {
+ return new String(hostname);
+ }
+
+ public String getResource() {
+ return new String(resource);
+ }
+
+ public String getUsername() {
+ return new String(username);
+ }
+
+ public String getPassword() {
+ return new String(password);
+ }
+}
+
diff --git a/src/net/temerity/davsync/HttpPropFind.java b/src/net/temerity/davsync/HttpPropFind.java
new file mode 100644
index 000000000..a235a9fe4
--- /dev/null
+++ b/src/net/temerity/davsync/HttpPropFind.java
@@ -0,0 +1,45 @@
+package net.temerity.davsync;
+
+import java.net.URI;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+
+
+public class HttpPropFind extends HttpEntityEnclosingRequestBase implements HttpEntityEnclosingRequest {
+
+ private final int DEPTH = 1;
+ public final static String METHOD_NAME = "PROPFIND";
+
+ public HttpPropFind() {
+ super();
+ this.setHeader("Content-Type", "text/xml");
+ this.setDepth(DEPTH);
+ }
+
+ public HttpPropFind(final URI uri) {
+ super();
+ this.setHeader("Content-Type", "text/xml");
+ this.setDepth(DEPTH);
+ this.setURI(uri);
+ }
+
+ public void setDepth(int val) {
+ this.setHeader("Depth", String.valueOf(val));
+ }
+
+ /**
+ * @throws IllegalArgumentException if the uri is invalid.
+ */
+ public HttpPropFind(final String uri) {
+ super();
+ this.setHeader("Content-Type", "text/xml");
+ this.setDepth(DEPTH);
+ setURI(URI.create(uri));
+ }
+
+ @Override
+ public String getMethod() {
+ return METHOD_NAME;
+ }
+
+}
diff --git a/src/net/temerity/davsync/PropFindHandler.java b/src/net/temerity/davsync/PropFindHandler.java
new file mode 100644
index 000000000..67dc78e5d
--- /dev/null
+++ b/src/net/temerity/davsync/PropFindHandler.java
@@ -0,0 +1,54 @@
+package net.temerity.davsync;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class PropFindHandler extends DefaultHandler {
+ private int level = 0;
+ private Boolean ele_getlastmodified = false, gotResponse = false;
+ public Date lastModified;
+
+ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ level++;
+
+ /*
+ * We only support a single response element - this indicates a regular file
+ * If multiple response elements are returned, we have a directory listing
+ */
+ if( qName.equals("D:response") ) {
+ if( gotResponse == false )
+ gotResponse = true;
+ else
+ throw new SAXException("Found multiple response elements");
+ }
+
+ if( qName.equals("lp1:getlastmodified") ) {
+ ele_getlastmodified = true;
+ }
+ }
+
+ @Override public void endElement(String uri, String localName, String qName) throws SAXException {
+ level--;
+ if( qName.equals("lp1:getlastmodified") ) {
+ ele_getlastmodified = false;
+ }
+ }
+
+ @Override public void characters(char[] ch, int start, int length) throws SAXException {
+ if( ele_getlastmodified == true ) {
+ String dateString = new String(ch, start, length);
+ DateFormat fmt = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss Z");
+ try {
+ lastModified = (Date)fmt.parse(dateString);
+ } catch( ParseException pe ) {
+ throw new SAXException("Unable to parse remote timestamp");
+ }
+ }
+ }
+}
diff --git a/src/net/temerity/davsync/davsync.java b/src/net/temerity/davsync/davsync.java
new file mode 100644
index 000000000..7d7d41711
--- /dev/null
+++ b/src/net/temerity/davsync/davsync.java
@@ -0,0 +1,207 @@
+package net.temerity.davsync;
+
+import java.io.File;
+import java.util.Properties;
+
+import com.android.keepass.R;
+import com.keepassdroid.app.App;
+import com.keepassdroid.fileselect.FileDbHelper;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.content.Context;
+
+public class davsync extends Activity {
+
+ private Context context;
+ private File kdbfile;
+ protected boolean changed;
+ FileDbHelper dbHelp = App.fileDbHelper; // database for recent files
+ private EditText[] field = new EditText[4]; // hostname, resource, username, password
+ private Button[] button = new Button[4]; // save, clear, test, sync
+
+ private enum ButtonType {
+ SAVE, CLEAR, TEST, SYNC
+ }
+
+ private class ButtonListener implements OnClickListener {
+ ButtonType type;
+
+ public ButtonListener(ButtonType type) {
+ this.type = type;
+ }
+ public void onClick(View v) {
+ switch( type ) {
+ case SAVE:
+ save();
+ break;
+ case CLEAR:
+ clear();
+ break;
+ case TEST:
+ test();
+ break;
+ case SYNC:
+ sync();
+ break;
+ }
+ }
+ }
+
+ // TODO: delete the database entry
+ private void clear() {
+ for(int i = 0; i < field.length; i++)
+ field[i].setText("");
+ dbHelp.delDavProfile(kdbfile.getAbsolutePath());
+ }
+
+ private void save() {
+ try {
+ dbHelp.addDavProfile(getCurrentProfile());
+ Toast.makeText(context, "Saved", Toast.LENGTH_SHORT).show();
+ finish();
+ } catch (DAVException e1) {
+ Toast.makeText(context, "Invalid", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // TODO: add a dialog during the test
+ // http://developer.android.com/guide/topics/ui/dialogs.html
+ private void test() {
+ Toast toast = Toast.makeText(context, "Unspecified test failure", Toast.LENGTH_SHORT);
+ try {
+ DAVNetwork net = new DAVNetwork(getCurrentProfile(), kdbfile);
+ if( net.testRemote() ) {
+ toast = Toast.makeText(context, "Test succeeded", Toast.LENGTH_SHORT);
+ } else {
+ toast = Toast.makeText(context, "Test failed", Toast.LENGTH_SHORT);
+ }
+ } catch (DAVException ce) {
+ toast = Toast.makeText(context, ce.toString(), Toast.LENGTH_SHORT);
+ } finally {
+ toast.show();
+ }
+ }
+
+ private void sync() {
+ Toast toast = Toast.makeText(context, "Unspecified test failure", Toast.LENGTH_SHORT);
+ try {
+ DAVNetwork net = new DAVNetwork(getCurrentProfile(), kdbfile);
+ if( net.sync() == true ) {
+ toast = Toast.makeText(context, "Sync succeeded", Toast.LENGTH_SHORT);
+ } else {
+ toast = Toast.makeText(context, "Sync failed", Toast.LENGTH_SHORT);
+ }
+ } catch( Exception e ) {
+ // TODO
+ } finally {
+ toast.show();
+ }
+ }
+
+ // read the state of all fields from memory and return a Profile object
+ private DAVProfile getCurrentProfile() throws DAVException {
+ String host = field[0].getText().toString();
+ if( ! host.matches("[a-zA-Z0-9.]+") ) {
+ throw new DAVException("Please input a valid hostname");
+ }
+ String rsrc = field[1].getText().toString();
+ if( ! rsrc.matches("[a-zA-Z0-9./]+") ) {
+ throw new DAVException("Please input a valid resource");
+ }
+ String user = field[2].getText().toString();
+ if( user.length() == 0 ) {
+ throw new DAVException("Please input a valid username");
+ }
+ String pass = field[3].getText().toString();
+ if( pass.length() == 0 ) {
+ throw new DAVException("Please input a valid password");
+ }
+
+ return new DAVProfile(kdbfile.getAbsolutePath(), host, rsrc, user, pass);
+ }
+
+ /** Called when the activity is first created. */
+ @Override public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ changed = false;
+ setContentView(R.layout.webdav_prefs);
+ context = getApplicationContext();
+ String kdbfilepath = getIntent().getStringExtra("kdbfile");
+ if( kdbfilepath == null ) {
+ // FIXME: this shouldn't ever happen
+ Log.w("davsync::onCreate","kdbfilepath not present, things will go pear-shaped soon!");
+ }
+ kdbfile = new File(kdbfilepath);
+
+ /*
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(filepath)
+ .setCancelable(true);
+ AlertDialog alert = builder.create();
+ alert.show();
+ */
+
+
+ // handle button events
+ button[0] = (Button)this.findViewById(R.id.wdc_save);
+ button[1] = (Button)this.findViewById(R.id.wdc_clear);
+ button[2] = (Button)this.findViewById(R.id.wdc_test);
+
+ button[0].setOnClickListener(new ButtonListener(ButtonType.SAVE));
+ button[1].setOnClickListener(new ButtonListener(ButtonType.CLEAR));
+ button[2].setOnClickListener(new ButtonListener(ButtonType.TEST));
+
+ /*field[0].setOnEditorActionListener(new EditActionListener());
+ field[1].setOnEditorActionListener(new EditActionListener());
+ field[2].setOnEditorActionListener(new EditActionListener());
+ field[3].setOnEditorActionListener(new EditActionListener());
+ */
+
+ // access to text fields
+ field[0] = (EditText)findViewById(R.id.hostname);
+ field[1] = (EditText)findViewById(R.id.resource);
+ field[2] = (EditText)findViewById(R.id.username);
+ field[3] = (EditText)findViewById(R.id.password);
+
+ try {
+ DAVProfile p = dbHelp.getDavProfile(kdbfilepath);
+ field[0].setText(p.getHostname());
+ field[1].setText(p.getResource());
+ field[2].setText(p.getUsername());
+ field[3].setText(p.getPassword());
+ } catch( DAVException ce ) {
+ // configuration doesn't exist, but thats okay - just don't set field values
+ }
+ }
+
+ public String getString(Properties props, String key)
+ {
+ String temp = (String)props.get(key);
+ if(temp == null)
+ temp = "";
+
+ return temp;
+ }
+
+ class EditActionListener implements OnEditorActionListener
+ {
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ // TODO Auto-generated method stub
+ changed = true;
+ return false;
+ }
+
+ }
+}