1
0
mirror of https://github.com/etesync/android synced 2025-06-24 08:58:50 +00:00

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
This commit is contained in:
R Hirner 2015-01-18 20:17:52 +01:00
parent 1a796ade60
commit 6c998c31c3
13 changed files with 158 additions and 104 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -4,7 +4,7 @@ N:Gump;Forrest;Mr.
FN:Forrest Gump FN:Forrest Gump
ORG:Bubba Gump Shrimp Co. ORG:Bubba Gump Shrimp Co.
TITLE:Shrimp Man 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=WORK,VOICE:(111) 555-1212
TEL;TYPE=HOME,VOICE:(404) 555-1212 TEL;TYPE=HOME,VOICE:(404) 555-1212
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America

View File

@ -55,7 +55,7 @@ public class ContactTest extends InstrumentationTestCase {
@Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING); @Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING);
Contact c = new Contact(fileName, null); Contact c = new Contact(fileName, null);
c.parseEntity(in); c.parseEntity(in, null);
return c; return c;
} }

View File

@ -9,12 +9,20 @@ package at.bitfire.davdroid.resource;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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.Email;
import ezvcard.property.Telephone; import ezvcard.property.Telephone;
import lombok.Cleanup; import lombok.Cleanup;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import org.apache.commons.io.IOUtils;
import at.bitfire.davdroid.resource.Contact; import at.bitfire.davdroid.resource.Contact;
import at.bitfire.davdroid.resource.InvalidResourceException; import at.bitfire.davdroid.resource.InvalidResourceException;
@ -47,6 +55,10 @@ public class ContactTest extends InstrumentationTestCase {
assertEquals("forrestgump@example.com", email.getValue()); assertEquals("forrestgump@example.com", email.getValue());
assertEquals("PREF", email.getParameters("TYPE").get(0)); assertEquals("PREF", email.getParameters("TYPE").get(0));
assertEquals("INTERNET", email.getParameters("TYPE").get(1)); 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 { public void testParseInvalidUnknownProperties() throws IOException, InvalidResourceException {
@ -59,7 +71,12 @@ public class ContactTest extends InstrumentationTestCase {
protected Contact parseVCF(String fname) throws IOException, InvalidResourceException { protected Contact parseVCF(String fname) throws IOException, InvalidResourceException {
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING); @Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
Contact c = new Contact(fname, null); 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; return c;
} }
} }

View File

@ -125,7 +125,7 @@ public class EventTest extends InstrumentationTestCase {
protected Event parseCalendar(String fname) throws IOException, InvalidResourceException { protected Event parseCalendar(String fname) throws IOException, InvalidResourceException {
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING); @Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
Event e = new Event(fname, null); Event e = new Event(fname, null);
e.parseEntity(in); e.parseEntity(in, null);
return e; return e;
} }
} }

View File

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

View File

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

View File

@ -14,6 +14,8 @@ import org.apache.commons.lang.StringUtils;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -129,7 +131,7 @@ public class Contact extends Resource {
@SuppressWarnings("LoopStatementThatDoesntLoop") @SuppressWarnings("LoopStatementThatDoesntLoop")
@Override @Override
public void parseEntity(InputStream is) throws IOException { public void parseEntity(InputStream is, AssetDownloader downloader) throws IOException {
VCard vcard = Ezvcard.parse(is).first(); VCard vcard = Ezvcard.parse(is).first();
if (vcard == null) if (vcard == null)
return; return;
@ -204,6 +206,14 @@ public class Contact extends Resource {
// PHOTO // PHOTO
for (Photo photo : vcard.getPhotos()) { for (Photo photo : vcard.getPhotos()) {
this.photo = photo.getData(); 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); vcard.removeProperties(Photo.class);
break; break;
} }

View File

@ -133,7 +133,7 @@ public class Event extends Resource {
@Override @Override
@SuppressWarnings("unchecked") @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; net.fortuna.ical4j.model.Calendar ical;
try { try {
CalendarBuilder builder = new CalendarBuilder(); CalendarBuilder builder = new CalendarBuilder();

View File

@ -11,13 +11,17 @@ import android.util.Log;
import net.fortuna.ical4j.model.ValidationException; 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 org.apache.http.impl.client.CloseableHttpClient;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -43,16 +47,20 @@ public abstract class RemoteCollection<T extends Resource> {
private static final String TAG = "davdroid.RemoteCollection"; private static final String TAG = "davdroid.RemoteCollection";
CloseableHttpClient httpClient; CloseableHttpClient httpClient;
@Getter WebDavResource collection; URI baseURI;
@Getter WebDavResource collection;
abstract protected String memberContentType(); abstract protected String memberContentType();
abstract protected DavMultiget.Type multiGetType(); abstract protected DavMultiget.Type multiGetType();
abstract protected T newResourceSkeleton(String name, String ETag); abstract protected T newResourceSkeleton(String name, String ETag);
public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
this.httpClient = httpClient; 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,7 +68,7 @@ public abstract class RemoteCollection<T extends Resource> {
public String getCTag() throws URISyntaxException, IOException, HttpException { public String getCTag() throws URISyntaxException, IOException, HttpException {
try { 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); collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG);
} catch (DavException e) { } catch (DavException e) {
return null; return null;
@ -83,7 +91,7 @@ public abstract class RemoteCollection<T extends Resource> {
public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException { public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException {
try { try {
if (resources.length == 1) 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)"); Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)");
@ -101,7 +109,7 @@ public abstract class RemoteCollection<T extends Resource> {
try { try {
if (member.getContent() != null) { if (member.getContent() != null) {
@Cleanup InputStream is = new ByteArrayInputStream(member.getContent()); @Cleanup InputStream is = new ByteArrayInputStream(member.getContent());
resource.parseEntity(is); resource.parseEntity(is, getDownloader());
foundResources.add(resource); foundResources.add(resource);
} else } else
Log.e(TAG, "Ignoring entity without content"); Log.e(TAG, "Ignoring entity without content");
@ -139,8 +147,8 @@ public abstract class RemoteCollection<T extends Resource> {
@Cleanup InputStream is = new ByteArrayInputStream(data); @Cleanup InputStream is = new ByteArrayInputStream(data);
try { try {
resource.parseEntity(is); resource.parseEntity(is, getDownloader());
} catch(VCardParseException e) { } catch (VCardParseException e) {
throw new InvalidResourceException(e); throw new InvalidResourceException(e);
} }
return resource; return resource;
@ -180,4 +188,28 @@ public abstract class RemoteCollection<T extends Resource> {
return eTag; 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);
}
}
};
}
} }

View File

@ -10,7 +10,12 @@ package at.bitfire.davdroid.resource;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
@ -39,8 +44,17 @@ public abstract class Resource {
/** initializes UID and remote file name (required for first upload) */ /** initializes UID and remote file name (required for first upload) */
public abstract void initialize(); public abstract void initialize();
/** fills the resource data from an input stream (for instance, .vcf file for Contact) */ /** fills the resource data from an input stream (for instance, .vcf file for Contact)
public abstract void parseEntity(InputStream entity) throws IOException, InvalidResourceException; * @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) */ /** writes the resource data to an output stream (for instance, .vcf file for Contact) */
public abstract ByteArrayOutputStream toEntity() throws IOException; public abstract ByteArrayOutputStream toEntity() throws IOException;
public interface AssetDownloader {
public byte[] download(URI url) throws URISyntaxException, IOException, HttpException, DavException;
}
} }

View File

@ -45,7 +45,8 @@ public class DavRedirectStrategy implements RedirectStrategy {
String location = getLocation(request, response, context).toString(); String location = getLocation(request, response, context).toString();
Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location); Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location);
return RequestBuilder.copy(request) return RequestBuilder
.copy(request)
.setUri(location) .setUri(location)
.removeHeaders("Content-Length") // Content-Length will be set again automatically, if required; .removeHeaders("Content-Length") // Content-Length will be set again automatically, if required;
// remove it now to avoid duplicate header // remove it now to avoid duplicate header

View File

@ -111,7 +111,10 @@ public class WebDavResource {
public WebDavResource(CloseableHttpClient httpClient, URI baseURI, String username, String password, boolean preemptive) { public WebDavResource(CloseableHttpClient httpClient, URI baseURI, String username, String password, boolean preemptive) {
this(httpClient, baseURI); this(httpClient, baseURI);
context.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); context.getCredentialsProvider().setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(username, password)
);
if (preemptive) { if (preemptive) {
HttpHost host = new HttpHost(baseURI.getHost(), baseURI.getPort(), baseURI.getScheme()); 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 public WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings
// reuse httpClient, context and location (no deep copy)
httpClient = parent.httpClient; httpClient = parent.httpClient;
context = parent.context; context = parent.context;
location = parent.location; location = parent.location;
@ -152,25 +156,21 @@ public class WebDavResource {
} }
/* feature detection */ /* feature detection */
public void options() throws URISyntaxException, IOException, HttpException { public void options() throws URISyntaxException, IOException, HttpException {
HttpOptionsHC4 options = new HttpOptionsHC4(location); HttpOptionsHC4 options = new HttpOptionsHC4(location);
CloseableHttpResponse response = httpClient.execute(options, context);
try {
checkResponse(response);
Header[] allowHeaders = response.getHeaders("Allow"); @Cleanup CloseableHttpResponse response = httpClient.execute(options, context);
for (Header allowHeader : allowHeaders) checkResponse(response);
methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?")));
Header[] capHeaders = response.getHeaders("DAV"); Header[] allowHeaders = response.getHeaders("Allow");
for (Header capHeader : capHeaders) for (Header allowHeader : allowHeaders)
capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?"))); methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?")));
} finally {
response.close(); Header[] capHeaders = response.getHeaders("DAV");
} for (Header capHeader : capHeaders)
capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?")));
} }
public boolean supportsDAV(String capability) { public boolean supportsDAV(String capability) {
@ -263,7 +263,7 @@ public class WebDavResource {
/* collection operations */ /* collection operations */
public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException { 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, // processMultiStatus() requires knowledge of the actual content location,
// so we have to handle redirections manually and create a new request for the new 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) if (response == null)
throw new DavNoContentException(); throw new DavNoContentException();
try { checkResponse(response); // will also handle Content-Location
checkResponse(response); // will also handle Content-Location processMultiStatus(response);
processMultiStatus(response);
} finally {
response.close();
}
} }
public void multiGet(DavMultiget.Type type, String[] names) throws URISyntaxException, IOException, DavException, HttpException { 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, // processMultiStatus() requires knowledge of the actual content location,
// so we have to handle redirections manually and create a new request for the new 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) if (response == null)
throw new DavNoContentException(); throw new DavNoContentException();
try { checkResponse(response); // will also handle Content-Location
checkResponse(response); // will also handle Content-Location processMultiStatus(response);
processMultiStatus(response);
} finally {
response.close();
}
} }
@ -347,18 +339,14 @@ public class WebDavResource {
HttpGetHC4 get = new HttpGetHC4(location); HttpGetHC4 get = new HttpGetHC4(location);
get.addHeader("Accept", acceptedType); get.addHeader("Accept", acceptedType);
CloseableHttpResponse response = httpClient.execute(get, context); @Cleanup CloseableHttpResponse response = httpClient.execute(get, context);
try { checkResponse(response);
checkResponse(response);
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
if (entity == null) if (entity == null)
throw new DavNoContentException(); throw new DavNoContentException();
content = EntityUtilsHC4.toByteArray(entity); content = EntityUtilsHC4.toByteArray(entity);
} finally {
response.close();
}
} }
// returns the ETag of the created/updated resource, if available (null otherwise) // returns the ETag of the created/updated resource, if available (null otherwise)
@ -378,16 +366,12 @@ public class WebDavResource {
if (getContentType() != null) if (getContentType() != null)
put.addHeader("Content-Type", getContentType()); put.addHeader("Content-Type", getContentType());
CloseableHttpResponse response = httpClient.execute(put, context); @Cleanup CloseableHttpResponse response = httpClient.execute(put, context);
try { checkResponse(response);
checkResponse(response);
Header eTag = response.getLastHeader("ETag"); Header eTag = response.getLastHeader("ETag");
if (eTag != null) if (eTag != null)
return eTag.getValue(); return eTag.getValue();
} finally {
response.close();
}
return null; return null;
} }
@ -398,12 +382,8 @@ public class WebDavResource {
if (getETag() != null) if (getETag() != null)
delete.addHeader("If-Match", getETag()); delete.addHeader("If-Match", getETag());
CloseableHttpResponse response = httpClient.execute(delete, context); @Cleanup CloseableHttpResponse response = httpClient.execute(delete, context);
try { checkResponse(response);
checkResponse(response);
} finally {
response.close();
}
} }