Commit fa766906 authored by Ricki Hirner's avatar Ricki Hirner 🐑

Tasks with new sync logic

parent 65c15258
......@@ -11,15 +11,14 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion '23.0.1'
useLibrary 'org.apache.http.legacy'
defaultConfig {
applicationId "at.bitfire.davdroid"
minSdkVersion 14
targetSdkVersion 23
versionCode 74
versionName "0.9-alpha2"
versionCode 75
versionName "0.9-alpha3"
buildConfigField "java.util.Date", "buildTime", "new java.util.Date()"
}
......
......@@ -22,9 +22,13 @@ import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.text.TextUtils;
import com.google.common.base.Joiner;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.model.component.VTimeZone;
import java.io.FileNotFoundException;
import java.util.LinkedList;
import java.util.List;
......@@ -34,6 +38,7 @@ import at.bitfire.ical4android.AndroidCalendar;
import at.bitfire.ical4android.AndroidCalendarFactory;
import at.bitfire.ical4android.BatchOperation;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.DateUtils;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
......@@ -84,9 +89,10 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection {
values.put(Calendars.OWNER_ACCOUNT, account.name);
values.put(Calendars.SYNC_EVENTS, 1);
values.put(Calendars.VISIBLE, 1);
if (info.timezone != null) {
// TODO parse VTIMEZONE
// values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.timezone));
if (!TextUtils.isEmpty(info.timezone)) {
VTimeZone timeZone = DateUtils.parseVTimeZone(info.timezone);
if (timeZone != null && timeZone.getTimeZoneId() != null)
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.getTimeZoneId().getValue()));
}
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
if (Build.VERSION.SDK_INT >= 15) {
......
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.os.RemoteException;
import android.provider.CalendarContract.Events;
import net.fortuna.ical4j.model.property.ProdId;
import org.dmfs.provider.tasks.TaskContract.Tasks;
import java.io.FileNotFoundException;
import java.text.ParseException;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.ical4android.AndroidTask;
import at.bitfire.ical4android.AndroidTaskFactory;
import at.bitfire.ical4android.AndroidTaskList;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Task;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
public class LocalTask extends AndroidTask implements LocalResource {
static {
Task.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x");
}
static final String COLUMN_ETAG = Tasks.SYNC1,
COLUMN_UID = Tasks._UID,
COLUMN_SEQUENCE = Tasks.SYNC2;
@Getter protected String fileName;
@Getter @Setter protected String eTag;
public LocalTask(@NonNull AndroidTaskList taskList, Task task, String fileName, String eTag) {
super(taskList, task);
this.fileName = fileName;
this.eTag = eTag;
}
protected LocalTask(@NonNull AndroidTaskList taskList, long id, ContentValues baseInfo) {
super(taskList, id);
if (baseInfo != null) {
fileName = baseInfo.getAsString(Events._SYNC_ID);
eTag = baseInfo.getAsString(COLUMN_ETAG);
}
}
/* process LocalTask-specific fields */
@Override
protected void populateTask(ContentValues values) throws FileNotFoundException, RemoteException, ParseException {
super.populateTask(values);
fileName = values.getAsString(Events._SYNC_ID);
eTag = values.getAsString(COLUMN_ETAG);
task.uid = values.getAsString(COLUMN_UID);
if (values.containsKey(COLUMN_SEQUENCE))
task.sequence = values.getAsInteger(COLUMN_SEQUENCE);
}
@Override
protected void buildTask(ContentProviderOperation.Builder builder, boolean update) {
super.buildTask(builder, update);
builder .withValue(Tasks._SYNC_ID, fileName)
.withValue(COLUMN_UID, task.uid)
.withValue(COLUMN_SEQUENCE, task.sequence)
.withValue(COLUMN_ETAG, eTag);
}
/* custom queries */
public void updateFileNameAndUID(String uid) throws CalendarStorageException {
try {
String newFileName = uid + ".ics";
ContentValues values = new ContentValues(2);
values.put(Tasks._SYNC_ID, newFileName);
values.put(COLUMN_UID, uid);
taskList.provider.client.update(taskSyncURI(), values, null, null);
fileName = newFileName;
if (task != null)
task.uid = uid;
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
@Override
public void clearDirty(String eTag) throws CalendarStorageException {
try {
ContentValues values = new ContentValues(2);
values.put(Tasks._DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
if (task != null)
values.put(COLUMN_SEQUENCE, task.sequence);
taskList.provider.client.update(taskSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e);
}
}
static class Factory implements AndroidTaskFactory {
static final Factory INSTANCE = new Factory();
@Override
public LocalTask newInstance(AndroidTaskList taskList, long id, ContentValues baseInfo) {
return new LocalTask(taskList, id, baseInfo);
}
@Override
public LocalTask newInstance(AndroidTaskList taskList, Task task) {
return new LocalTask(taskList, task, null, null);
}
@Override
public LocalTask[] newArray(int size) {
return new LocalTask[size];
}
}
}
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import org.dmfs.provider.tasks.TaskContract.TaskLists;
import org.dmfs.provider.tasks.TaskContract.Tasks;
import java.io.FileNotFoundException;
import at.bitfire.ical4android.AndroidTaskList;
import at.bitfire.ical4android.AndroidTaskListFactory;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
import lombok.NonNull;
public class LocalTaskList extends AndroidTaskList implements LocalCollection {
public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green"
public static final String COLUMN_CTAG = TaskLists.SYNC_VERSION;
static String[] BASE_INFO_COLUMNS = new String[] {
Tasks._ID,
Tasks._SYNC_ID,
LocalTask.COLUMN_ETAG
};
private static Boolean tasksProviderAvailable;
@Override
protected String[] taskBaseInfoColumns() {
return BASE_INFO_COLUMNS;
}
protected LocalTaskList(Account account, TaskProvider provider, long id) {
super(account, provider, LocalTask.Factory.INSTANCE, id);
}
public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws CalendarStorageException {
TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks);
if (provider == null)
throw new CalendarStorageException("Couldn't access OpenTasks provider");
ContentValues values = new ContentValues();
values.put(TaskLists._SYNC_ID, info.getURL());
values.put(TaskLists.LIST_NAME, info.getTitle());
values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor);
values.put(TaskLists.OWNER, account.name);
values.put(TaskLists.SYNC_ENABLED, 1);
values.put(TaskLists.VISIBLE, 1);
return create(account, provider, values);
}
@Override
public LocalTask[] getAll() throws CalendarStorageException {
return (LocalTask[])queryTasks(null, null);
}
@Override
public LocalTask[] getDeleted() throws CalendarStorageException {
return (LocalTask[])queryTasks(Tasks._DELETED + "!=0", null);
}
@Override
public LocalTask[] getWithoutFileName() throws CalendarStorageException {
return (LocalTask[])queryTasks(Tasks._SYNC_ID + " IS NULL", null);
}
@Override
public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException {
LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0", null);
if (tasks != null)
for (LocalTask task : tasks)
task.getTask().sequence++;
return tasks;
}
@Override
public String getCTag() throws CalendarStorageException {
try {
@Cleanup Cursor cursor = provider.client.query(taskListSyncUri(), new String[] { COLUMN_CTAG }, null, null, null);
if (cursor != null && cursor.moveToNext())
return cursor.getString(0);
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't read local (last known) CTag", e);
}
return null;
}
@Override
public void setCTag(String cTag) throws CalendarStorageException {
try {
ContentValues values = new ContentValues(1);
values.put(COLUMN_CTAG, cTag);
provider.client.update(taskListSyncUri(), values, null, null);
} catch (RemoteException e) {
throw new CalendarStorageException("Couldn't write local (last known) CTag", e);
}
}
// helpers
public static boolean tasksProviderAvailable(@NonNull ContentResolver resolver) {
if (tasksProviderAvailable != null)
return tasksProviderAvailable;
else {
TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks);
return tasksProviderAvailable = (provider != null);
}
}
public static class Factory implements AndroidTaskListFactory {
public static final Factory INSTANCE = new Factory();
@Override
public AndroidTaskList newInstance(Account account, TaskProvider provider, long id) {
return new LocalTaskList(account, provider, id);
}
@Override
public AndroidTaskList[] newArray(int size) {
return new LocalTaskList[size];
}
}
}
......@@ -9,7 +9,6 @@
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
......@@ -36,7 +35,6 @@ import at.bitfire.dav4android.DavCalendar;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.AddressData;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarData;
import at.bitfire.dav4android.property.DisplayName;
......@@ -46,14 +44,11 @@ import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.davdroid.resource.LocalEvent;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.ical4android.AndroidHostInfo;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
......@@ -63,20 +58,16 @@ public class CalendarSyncManager extends SyncManager {
MAX_MULTIGET = 30,
NOTIFICATION_ID = 2;
protected AndroidHostInfo hostInfo;
public CalendarSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result, LocalCalendar calendar) {
super(NOTIFICATION_ID, context, account, extras, provider, result);
public CalendarSyncManager(Context context, Account account, Bundle extras, SyncResult result, LocalCalendar calendar) {
super(NOTIFICATION_ID, context, account, extras, result);
localCollection = calendar;
}
@Override
protected void prepare() {
Thread.currentThread().setContextClassLoader(context.getClassLoader());
hostInfo = new AndroidHostInfo(context.getContentResolver());
Thread.currentThread().setContextClassLoader(context.getClassLoader()); // required for ical4j
collectionURL = HttpUrl.parse(localCalendar().getName());
davCollection = new DavCalendar(httpClient, collectionURL);
......@@ -191,13 +182,13 @@ public class CalendarSyncManager extends SyncManager {
private void processVEvent(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
Event[] events;
try {
events = Event.fromStream(stream, charset, hostInfo);
events = Event.fromStream(stream, charset);
} catch (InvalidCalendarException e) {
Constants.log.error("Received invalid iCalendar, ignoring");
return;
}
if (events.length == 1) {
if (events != null && events.length == 1) {
Event newData = events[0];
// delete local event, if it exists
......
......@@ -55,11 +55,11 @@ public class CalendarsSyncAdapterService extends Service {
try {
for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
Constants.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName());
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, provider, syncResult, calendar);
CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, syncResult, calendar);
syncManager.performSync();
}
} catch (CalendarStorageException e) {
Constants.log.error("Couldn't get list of local calendars", e);
Constants.log.error("Couldn't enumerate local calendars", e);
}
Constants.log.info("Calendar sync complete");
......
......@@ -60,11 +60,13 @@ public class ContactsSyncManager extends SyncManager {
MAX_MULTIGET = 10,
NOTIFICATION_ID = 1;
final protected ContentProviderClient provider;
protected boolean hasVCard4;
public ContactsSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result) {
super(NOTIFICATION_ID, context, account, extras, provider, result);
super(NOTIFICATION_ID, context, account, extras, result);
this.provider = provider;
}
......@@ -175,10 +177,10 @@ public class ContactsSyncManager extends SyncManager {
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
if (contacts.length == 1) {
if (contacts != null && contacts.length == 1) {
Contact newData = contacts[0];
// delete local contact, if it exists
// update local contact, if it exists
LocalContact localContact = (LocalContact)localResources.get(fileName);
if (localContact != null) {
Constants.log.info("Updating " + fileName + " in local address book");
......
......@@ -11,7 +11,6 @@ import android.accounts.Account;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
......@@ -67,7 +66,6 @@ abstract public class SyncManager {
protected final Context context;
protected final Account account;
protected final Bundle extras;
protected final ContentProviderClient provider;
protected final SyncResult syncResult;
protected final AccountSettings settings;
......@@ -92,11 +90,10 @@ abstract public class SyncManager {
public SyncManager(int notificationId, Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult syncResult) {
public SyncManager(int notificationId, Context context, Account account, Bundle extras, SyncResult syncResult) {
this.context = context;
this.account = account;
this.extras = extras;
this.provider = provider;
this.syncResult = syncResult;
// get account settings and generate httpClient
......@@ -167,9 +164,14 @@ abstract public class SyncManager {
}
}
} catch(HttpException|DavException e) {
Constants.log.error("HTTP/DAV Exception during sync", e);
syncResult.stats.numParseExceptions++;
} catch(Exception e) {
if (e instanceof HttpException || e instanceof DavException) {
Constants.log.error("HTTP/DAV Exception during sync", e);
syncResult.stats.numParseExceptions++;
} else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) {
Constants.log.error("Couldn't access local storage", e);
syncResult.databaseError = true;
}
Intent detailsIntent = new Intent(context, DebugInfoActivity.class);
detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e);
......@@ -195,9 +197,6 @@ abstract public class SyncManager {
}
notificationManager.notify(account.name, notificationId, notification);
} catch(CalendarStorageException|ContactsStorageException e) {
Constants.log.error("Couldn't access local storage", e);
syncResult.databaseError = true;
}
}
......
......@@ -16,6 +16,14 @@ import android.content.Intent;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.IBinder;
import android.provider.CalendarContract;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
public class TasksSyncAdapterService extends Service {
private static SyncAdapter syncAdapter;
......@@ -43,8 +51,24 @@ public class TasksSyncAdapterService extends Service {
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient, SyncResult syncResult) {
Constants.log.info("Starting task sync (" + authority + ")");
try {
@Cleanup TaskProvider provider = TaskProvider.acquire(getContext().getContentResolver(), TaskProvider.ProviderName.OpenTasks);
if (provider == null)
throw new CalendarStorageException("Couldn't access OpenTasks provider");
for (LocalTaskList taskList : (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null)) {
Constants.log.info("Synchronizing task list #" + taskList.getId() + ", URL: " + taskList.getName());
TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, extras, provider, syncResult, taskList);
syncManager.performSync();
}
} catch (CalendarStorageException e) {
Constants.log.error("Couldn't enumerate local task lists", e);
}
Constants.log.info("Calendar sync complete");
}
}
......
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.content.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.text.TextUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ResponseBody;
import org.dmfs.provider.tasks.TaskContract.TaskLists;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.dav4android.DavCalendar;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarData;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.davdroid.resource.LocalTask;
import at.bitfire.davdroid.resource.LocalTaskList;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.InvalidCalendarException;
import at.bitfire.ical4android.Task;
import at.bitfire.ical4android.TaskProvider;
import lombok.Cleanup;
public class TasksSyncManager extends SyncManager {
protected static final int
MAX_MULTIGET = 30,
NOTIFICATION_ID = 3;
final protected TaskProvider provider;
public TasksSyncManager(Context context, Account account, Bundle extras, TaskProvider provider, SyncResult result, LocalTaskList taskList) {
super(NOTIFICATION_ID, context, account, extras, result);
this.provider = provider;
localCollection = taskList;
}
@Override
protected void prepare() {
Thread.currentThread().setContextClassLoader(context.getClassLoader()); // required for ical4j
collectionURL = HttpUrl.parse(localTaskList().getSyncId());
davCollection = new DavCalendar(httpClient, collectionURL);
}
@Override
protected void queryCapabilities() throws DavException, IOException, HttpException, CalendarStorageException {
davCollection.propfind(0, DisplayName.NAME, CalendarColor.NAME, GetCTag.NAME);
// update name and color
DisplayName pDisplayName = (DisplayName)davCollection.properties.get(DisplayName.NAME);
String displayName = (pDisplayName != null && !TextUtils.isEmpty(pDisplayName.displayName)) ?
pDisplayName.displayName : collectionURL.toString();
CalendarColor pColor = (CalendarColor)davCollection.properties.get(CalendarColor.NAME);
int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor;
ContentValues values = new ContentValues(2);
Constants.log.info("Setting new calendar name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
values.put(TaskLists.LIST_NAME, displayName);
values.put(TaskLists.LIST_COLOR, color);
localTaskList().update(values);
}
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
LocalTask local = (LocalTask)resource;
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
local.getTask().toStream().toByteArray()
);
}
@Override
protected void listRemote() throws IOException, HttpException, DavException {
// fetch list of remote VTODOs and build hash table to index file name
davCalendar().calendarQuery("VTODO");
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
Constants.log.debug("Found remote VTODO: " + fileName);
remoteResources.put(fileName, vCard);
}
}
@Override
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
Constants.log.info("Downloading " + toDownload.size() + " tasks (" + MAX_MULTIGET + " at once)");
// download new/updated iCalendars from server
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
Constants.log.info("Downloading " + Joiner.on