diff --git a/app/src/androidTest/assets/davdroid-logo-192.png b/app/src/androidTest/assets/davdroid-logo-192.png new file mode 100644 index 00000000..488e6fd1 Binary files /dev/null and b/app/src/androidTest/assets/davdroid-logo-192.png differ diff --git a/app/src/androidTest/assets/reference.vcf b/app/src/androidTest/assets/reference.vcf index ba6a6b4d..401d0791 100644 --- a/app/src/androidTest/assets/reference.vcf +++ b/app/src/androidTest/assets/reference.vcf @@ -4,7 +4,7 @@ N:Gump;Forrest;Mr. FN:Forrest Gump ORG:Bubba Gump Shrimp Co. TITLE:Shrimp Man -PHOTO;VALUE=URL;TYPE=GIF:http://www.example.com/dir_photos/my_photo.gif +PHOTO;VALUE=URL;TYPE=PNG:http://10.0.0.11:3000/assets/davdroid-logo-192.png TEL;TYPE=WORK,VOICE:(111) 555-1212 TEL;TYPE=HOME,VOICE:(404) 555-1212 ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America @@ -13,4 +13,4 @@ ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com REV:2008-04-24T19:52:43Z -END:VCARD \ No newline at end of file +END:VCARD diff --git a/app/src/androidTest/java/at/bitfire/davdroid/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/ContactTest.java index f9a9f71a..f292e2d8 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/ContactTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/ContactTest.java @@ -55,7 +55,7 @@ public class ContactTest extends InstrumentationTestCase { @Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING); Contact c = new Contact(fileName, null); - c.parseEntity(in); + c.parseEntity(in, null); return c; } diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java index 18eda122..89841223 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/ContactTest.java @@ -9,12 +9,20 @@ package at.bitfire.davdroid.resource; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.HttpException; import ezvcard.property.Email; import ezvcard.property.Telephone; import lombok.Cleanup; import android.content.res.AssetManager; import android.test.InstrumentationTestCase; + +import org.apache.commons.io.IOUtils; + import at.bitfire.davdroid.resource.Contact; import at.bitfire.davdroid.resource.InvalidResourceException; @@ -47,8 +55,12 @@ public class ContactTest extends InstrumentationTestCase { assertEquals("forrestgump@example.com", email.getValue()); assertEquals("PREF", email.getParameters("TYPE").get(0)); assertEquals("INTERNET", email.getParameters("TYPE").get(1)); + + @Cleanup InputStream photoStream = assetMgr.open("davdroid-logo-192.png", AssetManager.ACCESS_STREAMING); + byte[] expectedPhoto = IOUtils.toByteArray(photoStream); + assertTrue(Arrays.equals(c.getPhoto(), expectedPhoto)); } - + public void testParseInvalidUnknownProperties() throws IOException, InvalidResourceException { Contact c = parseVCF("invalid-unknown-properties.vcf"); assertEquals("VCard with invalid unknown properties", c.getDisplayName()); @@ -59,7 +71,12 @@ public class ContactTest extends InstrumentationTestCase { protected Contact parseVCF(String fname) throws IOException, InvalidResourceException { @Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING); Contact c = new Contact(fname, null); - c.parseEntity(in); + c.parseEntity(in, new Resource.AssetDownloader() { + @Override + public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException { + return IOUtils.toByteArray(uri); + } + }); return c; } } diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java index 96da4c34..b24453d5 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/EventTest.java @@ -125,7 +125,7 @@ public class EventTest extends InstrumentationTestCase { protected Event parseCalendar(String fname) throws IOException, InvalidResourceException { @Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING); Event e = new Event(fname, null); - e.parseEntity(in); + e.parseEntity(in, null); return e; } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7d0f8a01..b22c4e28 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ { private static final String TAG = "davdroid.RemoteCollection"; - + CloseableHttpClient httpClient; - @Getter WebDavResource collection; + URI baseURI; + @Getter WebDavResource collection; abstract protected String memberContentType(); + abstract protected DavMultiget.Type multiGetType(); + abstract protected T newResourceSkeleton(String name, String ETag); - + public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { this.httpClient = httpClient; - - collection = new WebDavResource(httpClient, URIUtils.parseURI(baseURL, false), user, password, preemptiveAuth); + + baseURI = URIUtils.parseURI(baseURL, false); + collection = new WebDavResource(httpClient, baseURI, user, password, preemptiveAuth); } @@ -60,17 +68,17 @@ public abstract class RemoteCollection { public String getCTag() throws URISyntaxException, IOException, HttpException { try { - if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched + if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG); } catch (DavException e) { return null; } return collection.getCTag(); } - + public Resource[] getMemberETags() throws URISyntaxException, IOException, DavException, HttpException { collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG); - + List resources = new LinkedList(); if (collection.getMembers() != null) { for (WebDavResource member : collection.getMembers()) @@ -78,30 +86,30 @@ public abstract class RemoteCollection { } return resources.toArray(new Resource[0]); } - + @SuppressWarnings("unchecked") public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException { try { if (resources.length == 1) - return (T[]) new Resource[] { get(resources[0]) }; - + return (T[]) new Resource[]{get(resources[0])}; + Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)"); - + LinkedList names = new LinkedList(); for (Resource resource : resources) names.add(resource.getName()); - + LinkedList foundResources = new LinkedList(); collection.multiGet(multiGetType(), names.toArray(new String[0])); if (collection.getMembers() == null) throw new DavNoContentException(); - + for (WebDavResource member : collection.getMembers()) { T resource = newResourceSkeleton(member.getName(), member.getETag()); try { if (member.getContent() != null) { @Cleanup InputStream is = new ByteArrayInputStream(member.getContent()); - resource.parseEntity(is); + resource.parseEntity(is, getDownloader()); foundResources.add(resource); } else Log.e(TAG, "Ignoring entity without content"); @@ -109,12 +117,12 @@ public abstract class RemoteCollection { Log.e(TAG, "Ignoring unparseable entity in multi-response", e); } } - + return foundResources.toArray(new Resource[0]); } catch (InvalidResourceException e) { Log.e(TAG, "Couldn't parse entity from GET", e); } - + return new Resource[0]; } @@ -123,7 +131,7 @@ public abstract class RemoteCollection { public Resource get(Resource resource) throws URISyntaxException, IOException, HttpException, DavException, InvalidResourceException { WebDavResource member = new WebDavResource(collection, resource.getName()); - + if (resource instanceof Contact) member.get(Contact.MIME_TYPE); else if (resource instanceof Event) @@ -132,52 +140,76 @@ public abstract class RemoteCollection { Log.wtf(TAG, "Should fetch something, but neither contact nor calendar"); throw new InvalidResourceException("Didn't now which MIME type to accept"); } - + byte[] data = member.getContent(); if (data == null) throw new DavNoContentException(); - + @Cleanup InputStream is = new ByteArrayInputStream(data); try { - resource.parseEntity(is); - } catch(VCardParseException e) { + resource.parseEntity(is, getDownloader()); + } catch (VCardParseException e) { throw new InvalidResourceException(e); } return resource; } - + // returns ETag of the created resource, if returned by server public String add(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException { WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); member.setContentType(memberContentType()); - + @Cleanup ByteArrayOutputStream os = res.toEntity(); String eTag = member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE); - + // after a successful upload, the collection has implicitely changed, too collection.invalidateCTag(); - + return eTag; } public void delete(Resource res) throws URISyntaxException, IOException, HttpException { WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); member.delete(); - + collection.invalidateCTag(); } - + // returns ETag of the updated resource, if returned by server public String update(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException { WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); member.setContentType(memberContentType()); - + @Cleanup ByteArrayOutputStream os = res.toEntity(); String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE); - + // after a successful upload, the collection has implicitely changed, too collection.invalidateCTag(); - + return eTag; } + + + // helpers + + Resource.AssetDownloader getDownloader() { + return new Resource.AssetDownloader() { + @Override + public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException { + if (!uri.isAbsolute()) + throw new URISyntaxException(uri.toString(), "URI referenced from entity must be absolute"); + + if (uri.getScheme().equalsIgnoreCase(baseURI.getScheme()) && + uri.getAuthority().equalsIgnoreCase(baseURI.getAuthority())) { + // resource is on same server, send Authorization + WebDavResource file = new WebDavResource(collection, uri); + file.get("image/*"); + return file.getContent(); + } else { + // resource is on an external server, don't send Authorization + return IOUtils.toByteArray(uri); + } + } + }; + } } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Resource.java b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java index bda3e2e5..2bd87529 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/Resource.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java @@ -10,7 +10,12 @@ package at.bitfire.davdroid.resource; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.HttpException; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -39,8 +44,17 @@ public abstract class Resource { /** initializes UID and remote file name (required for first upload) */ public abstract void initialize(); - /** fills the resource data from an input stream (for instance, .vcf file for Contact) */ - public abstract void parseEntity(InputStream entity) throws IOException, InvalidResourceException; + /** fills the resource data from an input stream (for instance, .vcf file for Contact) + * @param entity entity to parse + * @param downloader will be used to fetch additional resources like contact images + **/ + public abstract void parseEntity(InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException; + /** writes the resource data to an output stream (for instance, .vcf file for Contact) */ public abstract ByteArrayOutputStream toEntity() throws IOException; + + + public interface AssetDownloader { + public byte[] download(URI url) throws URISyntaxException, IOException, HttpException, DavException; + } } diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java index 124ee644..3dc547d5 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java @@ -44,8 +44,9 @@ public class DavRedirectStrategy implements RedirectStrategy { String location = getLocation(request, response, context).toString(); Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location); - - return RequestBuilder.copy(request) + + return RequestBuilder + .copy(request) .setUri(location) .removeHeaders("Content-Length") // Content-Length will be set again automatically, if required; // remove it now to avoid duplicate header diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java index 84d0a875..1ad7d2c6 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java +++ b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java @@ -110,8 +110,11 @@ public class WebDavResource { public WebDavResource(CloseableHttpClient httpClient, URI baseURI, String username, String password, boolean preemptive) { this(httpClient, baseURI); - - context.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + + context.getCredentialsProvider().setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials(username, password) + ); if (preemptive) { HttpHost host = new HttpHost(baseURI.getHost(), baseURI.getPort(), baseURI.getScheme()); @@ -125,6 +128,7 @@ public class WebDavResource { } public WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings + // reuse httpClient, context and location (no deep copy) httpClient = parent.httpClient; context = parent.context; location = parent.location; @@ -151,26 +155,22 @@ public class WebDavResource { properties.put(Property.ETAG, ETag); } - /* feature detection */ public void options() throws URISyntaxException, IOException, HttpException { HttpOptionsHC4 options = new HttpOptionsHC4(location); - CloseableHttpResponse response = httpClient.execute(options, context); - try { - checkResponse(response); - - Header[] allowHeaders = response.getHeaders("Allow"); - for (Header allowHeader : allowHeaders) - methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?"))); - - Header[] capHeaders = response.getHeaders("DAV"); - for (Header capHeader : capHeaders) - capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?"))); - } finally { - response.close(); - } + + @Cleanup CloseableHttpResponse response = httpClient.execute(options, context); + checkResponse(response); + + Header[] allowHeaders = response.getHeaders("Allow"); + for (Header allowHeader : allowHeaders) + methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?"))); + + Header[] capHeaders = response.getHeaders("DAV"); + for (Header capHeader : capHeaders) + capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?"))); } public boolean supportsDAV(String capability) { @@ -263,7 +263,7 @@ public class WebDavResource { /* collection operations */ public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException { - CloseableHttpResponse response = null; + @Cleanup CloseableHttpResponse response = null; // processMultiStatus() requires knowledge of the actual content location, // so we have to handle redirections manually and create a new request for the new location @@ -283,16 +283,12 @@ public class WebDavResource { if (response == null) throw new DavNoContentException(); - try { - checkResponse(response); // will also handle Content-Location - processMultiStatus(response); - } finally { - response.close(); - } + checkResponse(response); // will also handle Content-Location + processMultiStatus(response); } public void multiGet(DavMultiget.Type type, String[] names) throws URISyntaxException, IOException, DavException, HttpException { - CloseableHttpResponse response = null; + @Cleanup CloseableHttpResponse response = null; // processMultiStatus() requires knowledge of the actual content location, // so we have to handle redirections manually and create a new request for the new location @@ -332,12 +328,8 @@ public class WebDavResource { if (response == null) throw new DavNoContentException(); - try { - checkResponse(response); // will also handle Content-Location - processMultiStatus(response); - } finally { - response.close(); - } + checkResponse(response); // will also handle Content-Location + processMultiStatus(response); } @@ -347,18 +339,14 @@ public class WebDavResource { HttpGetHC4 get = new HttpGetHC4(location); get.addHeader("Accept", acceptedType); - CloseableHttpResponse response = httpClient.execute(get, context); - try { - checkResponse(response); - - HttpEntity entity = response.getEntity(); - if (entity == null) - throw new DavNoContentException(); - - content = EntityUtilsHC4.toByteArray(entity); - } finally { - response.close(); - } + @Cleanup CloseableHttpResponse response = httpClient.execute(get, context); + checkResponse(response); + + HttpEntity entity = response.getEntity(); + if (entity == null) + throw new DavNoContentException(); + + content = EntityUtilsHC4.toByteArray(entity); } // returns the ETag of the created/updated resource, if available (null otherwise) @@ -378,17 +366,13 @@ public class WebDavResource { if (getContentType() != null) put.addHeader("Content-Type", getContentType()); - CloseableHttpResponse response = httpClient.execute(put, context); - try { - checkResponse(response); + @Cleanup CloseableHttpResponse response = httpClient.execute(put, context); + checkResponse(response); + + Header eTag = response.getLastHeader("ETag"); + if (eTag != null) + return eTag.getValue(); - Header eTag = response.getLastHeader("ETag"); - if (eTag != null) - return eTag.getValue(); - } finally { - response.close(); - } - return null; } @@ -398,12 +382,8 @@ public class WebDavResource { if (getETag() != null) delete.addHeader("If-Match", getETag()); - CloseableHttpResponse response = httpClient.execute(delete, context); - try { - checkResponse(response); - } finally { - response.close(); - } + @Cleanup CloseableHttpResponse response = httpClient.execute(delete, context); + checkResponse(response); }