Download photo URIs in VCards

* download photo URIs in VCards (closes #430); send authorization only when on same server
* version bump to 0.6.11
* minor code optimizations
pull/2/head
R Hirner 10 years ago
parent 1a796ade60
commit 6c998c31c3

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@ -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
END:VCARD

@ -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;
}

@ -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;
}
}

@ -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;
}
}

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid"
android:versionCode="53" android:versionName="0.6.10.2"
android:versionCode="53" android:versionName="0.6.11"
android:installLocation="internalOnly">
<uses-sdk

@ -9,7 +9,7 @@ package at.bitfire.davdroid;
public class Constants {
public static final String
APP_VERSION = "0.6.10.2",
APP_VERSION = "0.6.11",
ACCOUNT_TYPE = "bitfire.at.davdroid",
WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app",
WEB_URL_VIEW_LOGS = "https://github.com/bitfireAT/davdroid/wiki/How-to-view-the-logs";

@ -14,6 +14,8 @@ import org.apache.commons.lang.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@ -129,7 +131,7 @@ public class Contact extends Resource {
@SuppressWarnings("LoopStatementThatDoesntLoop")
@Override
public void parseEntity(InputStream is) throws IOException {
public void parseEntity(InputStream is, AssetDownloader downloader) throws IOException {
VCard vcard = Ezvcard.parse(is).first();
if (vcard == null)
return;
@ -204,6 +206,14 @@ public class Contact extends Resource {
// PHOTO
for (Photo photo : vcard.getPhotos()) {
this.photo = photo.getData();
if (this.photo == null && photo.getUrl() != null)
try {
URI uri = new URI(photo.getUrl());
Log.i(TAG, "Downloading contact photo from " + uri);
this.photo = downloader.download(uri);
} catch(Exception e) {
Log.w(TAG, "Couldn't fetch contact photo", e);
}
vcard.removeProperties(Photo.class);
break;
}

@ -133,7 +133,7 @@ public class Event extends Resource {
@Override
@SuppressWarnings("unchecked")
public void parseEntity(@NonNull InputStream entity) throws IOException, InvalidResourceException {
public void parseEntity(@NonNull InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException {
net.fortuna.ical4j.model.Calendar ical;
try {
CalendarBuilder builder = new CalendarBuilder();

@ -11,13 +11,17 @@ import android.util.Log;
import net.fortuna.ical4j.model.ValidationException;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.utils.URIUtilsHC4;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.ByteArrayInputStream;
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 java.util.LinkedList;
import java.util.List;
@ -41,18 +45,22 @@ import lombok.Getter;
*/
public abstract class RemoteCollection<T extends Resource> {
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<T extends Resource> {
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<T> resources = new LinkedList<T>();
if (collection.getMembers() != null) {
for (WebDavResource member : collection.getMembers())
@ -78,30 +86,30 @@ public abstract class RemoteCollection<T extends Resource> {
}
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<String> names = new LinkedList<String>();
for (Resource resource : resources)
names.add(resource.getName());
LinkedList<T> foundResources = new LinkedList<T>();
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<T extends Resource> {
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<T extends Resource> {
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<T extends Resource> {
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);
}
}
};
}
}

@ -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;
}
}

@ -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

@ -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);
}

Loading…
Cancel
Save