mirror of
https://github.com/etesync/android
synced 2024-11-22 16:08:13 +00:00
Various upload changes
* fix issue when first upload fails (fixes #233) * show debug settings menu item in main activity * update ETag in local database when a PUT request returns an updated ETag, meaning that locally created/updated records won't be downloaded from the server immediately after the upload
This commit is contained in:
parent
7e7d36584c
commit
3cab688782
@ -3,4 +3,5 @@
|
||||
<item android:onClick="addAccount" android:title="@string/add_account" android:showAsAction="always" android:icon="@drawable/ic_action_new_account"></item>
|
||||
<item android:onClick="showSyncSettings" android:title="@string/show_sync_settings" android:showAsAction="always" android:icon="@drawable/show_sync_settings"></item>
|
||||
<item android:onClick="showWebsite" android:title="@string/show_website" android:showAsAction="always" android:icon="@drawable/view_website"></item>
|
||||
<item android:showAsAction="never" android:title="@string/debug_settings" android:onClick="showDebugSettings"></item>
|
||||
</menu>
|
||||
|
@ -22,6 +22,7 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import at.bitfire.davdroid.syncadapter.GeneralSettingsActivity;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
@ -55,6 +56,11 @@ public class MainActivity extends Activity {
|
||||
Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void showDebugSettings(MenuItem item) {
|
||||
Intent intent = new Intent(this, GeneralSettingsActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void showSyncSettings(MenuItem item) {
|
||||
Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
|
||||
|
@ -158,7 +158,7 @@ public class LocalCalendar extends LocalCollection<Event> {
|
||||
return calendars.toArray(new LocalCalendar[0]);
|
||||
}
|
||||
|
||||
public LocalCalendar(Account account, ContentProviderClient providerClient, int id, String url, String cTag) throws RemoteException {
|
||||
public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url, String cTag) throws RemoteException {
|
||||
super(account, providerClient);
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
|
@ -68,8 +68,8 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
// content provider (= database) querying
|
||||
|
||||
public long[] findNew() throws LocalStorageException {
|
||||
// new records are 1) dirty, and 2) don't have a remote file name yet
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnRemoteName() + " IS NULL";
|
||||
// new records are 1) dirty, and 2) don't have an ETag yet
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
try {
|
||||
@ -101,8 +101,8 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
}
|
||||
|
||||
public long[] findUpdated() throws LocalStorageException {
|
||||
// updated records are 1) dirty, and 2) already have a remote file name
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnRemoteName() + " IS NOT NULL";
|
||||
// updated records are 1) dirty, and 2) already have an ETag
|
||||
String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL";
|
||||
if (entryColumnParentID() != null)
|
||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||
try {
|
||||
@ -110,12 +110,12 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||
where, null, null);
|
||||
if (cursor == null)
|
||||
throw new LocalStorageException("Couldn't query dirty records");
|
||||
throw new LocalStorageException("Couldn't query updated records");
|
||||
|
||||
long[] dirty = new long[cursor.getCount()];
|
||||
long[] updated = new long[cursor.getCount()];
|
||||
for (int idx = 0; cursor.moveToNext(); idx++)
|
||||
dirty[idx] = cursor.getLong(0);
|
||||
return dirty;
|
||||
updated[idx] = cursor.getLong(0);
|
||||
return updated;
|
||||
} catch(RemoteException ex) {
|
||||
throw new LocalStorageException(ex);
|
||||
}
|
||||
@ -218,6 +218,18 @@ public abstract class LocalCollection<T extends Resource> {
|
||||
|
||||
public abstract void deleteAllExceptRemoteNames(Resource[] remoteResources);
|
||||
|
||||
public void updateETag(Resource res, String eTag) throws LocalStorageException {
|
||||
Log.d(TAG, "Setting ETag of local resource " + res + " to " + eTag);
|
||||
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(entryColumnETag(), eTag);
|
||||
try {
|
||||
providerClient.update(ContentUris.withAppendedId(entriesURI(), res.getLocalID()), values, null, new String[] {});
|
||||
} catch (RemoteException e) {
|
||||
throw new LocalStorageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearDirty(Resource resource) {
|
||||
pendingOperations.add(ContentProviderOperation
|
||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||
|
@ -127,14 +127,18 @@ public abstract class RemoteCollection<T extends Resource> {
|
||||
return resource;
|
||||
}
|
||||
|
||||
public void add(Resource res) throws IOException, HttpException, ValidationException {
|
||||
// returns ETag of the created resource, if returned by server
|
||||
public String add(Resource res) throws IOException, HttpException, ValidationException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.setContentType(memberContentType());
|
||||
|
||||
@Cleanup ByteArrayOutputStream os = res.toEntity();
|
||||
member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE);
|
||||
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 IOException, HttpException {
|
||||
@ -144,13 +148,17 @@ public abstract class RemoteCollection<T extends Resource> {
|
||||
collection.invalidateCTag();
|
||||
}
|
||||
|
||||
public void update(Resource res) throws IOException, HttpException, ValidationException {
|
||||
// returns ETag of the updated resource, if returned by server
|
||||
public String update(Resource res) throws IOException, HttpException, ValidationException {
|
||||
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
|
||||
member.setContentType(memberContentType());
|
||||
|
||||
@Cleanup ByteArrayOutputStream os = res.toEntity();
|
||||
member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE);
|
||||
String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE);
|
||||
|
||||
// after a successful upload, the collection has implicitely changed, too
|
||||
collection.invalidateCTag();
|
||||
|
||||
return eTag;
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import lombok.ToString;
|
||||
|
||||
@ToString
|
||||
public abstract class Resource {
|
||||
@Getter protected String name, ETag;
|
||||
@Getter @Setter protected String name, ETag;
|
||||
@Getter @Setter protected String uid;
|
||||
@Getter protected long localID;
|
||||
|
||||
|
@ -138,7 +138,9 @@ public class SyncManager {
|
||||
for (long id : newIDs)
|
||||
try {
|
||||
Resource res = local.findById(id, true);
|
||||
remote.add(res);
|
||||
String eTag = remote.add(res);
|
||||
if (eTag != null)
|
||||
local.updateETag(res, eTag);
|
||||
local.clearDirty(res);
|
||||
count++;
|
||||
} catch(PreconditionFailedException e) {
|
||||
@ -162,7 +164,9 @@ public class SyncManager {
|
||||
for (long id : dirtyIDs) {
|
||||
try {
|
||||
Resource res = local.findById(id, true);
|
||||
remote.update(res);
|
||||
String eTag = remote.update(res);
|
||||
if (eTag != null)
|
||||
local.updateETag(res, eTag);
|
||||
local.clearDirty(res);
|
||||
count++;
|
||||
} catch(PreconditionFailedException e) {
|
||||
|
@ -338,7 +338,8 @@ public class WebDavResource {
|
||||
}
|
||||
}
|
||||
|
||||
public void put(byte[] data, PutMode mode) throws IOException, HttpException {
|
||||
// returns the ETag of the created/updated resource, if available (null otherwise)
|
||||
public String put(byte[] data, PutMode mode) throws IOException, HttpException {
|
||||
HttpPut put = new HttpPut(location);
|
||||
put.setEntity(new ByteArrayEntity(data));
|
||||
|
||||
@ -357,9 +358,15 @@ public class WebDavResource {
|
||||
CloseableHttpResponse response = httpClient.execute(put, context);
|
||||
try {
|
||||
checkResponse(response);
|
||||
|
||||
Header eTag = response.getLastHeader("ETag");
|
||||
if (eTag != null)
|
||||
return eTag.getValue();
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void delete() throws IOException, HttpException {
|
||||
|
@ -134,8 +134,10 @@ exports.getBodyParts = function(conf) {
|
||||
if (req.method == "PUT") {
|
||||
if (req.headers['if-match']) /* can't overwrite new file */
|
||||
res.statusCode = 412;
|
||||
else
|
||||
else {
|
||||
res.statusCode = 201;
|
||||
res.headers["ETag"] = "has-just-been-created";
|
||||
}
|
||||
|
||||
} else if (req.method == "DELETE")
|
||||
res.statusCode = 404;
|
||||
@ -149,8 +151,10 @@ exports.getBodyParts = function(conf) {
|
||||
if (req.method == "PUT") {
|
||||
if (req.headers['if-none-match']) /* requested "don't overwrite", but this file exists */
|
||||
res.statusCode = 412;
|
||||
else
|
||||
else {
|
||||
res.statusCode = 204;
|
||||
res.headers["ETag"] = "has-just-been-updated";
|
||||
}
|
||||
|
||||
} else if (req.method == "DELETE")
|
||||
res.statusCode = 204;
|
||||
|
@ -1,6 +1,9 @@
|
||||
package at.bitfire.davdroid.resource.test;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import lombok.Cleanup;
|
||||
import android.accounts.Account;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
@ -9,40 +12,51 @@ import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.CalendarContract;
|
||||
import android.provider.CalendarContract.Attendees;
|
||||
import android.provider.CalendarContract.Calendars;
|
||||
import android.provider.CalendarContract.Events;
|
||||
import android.provider.CalendarContract.Reminders;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.util.Log;
|
||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||
|
||||
public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String calendarName = "DavdroidTest";
|
||||
private static final String
|
||||
TAG = "davroid.LocalCalendarTest",
|
||||
calendarName = "DAVdroid_Test";
|
||||
|
||||
ContentProviderClient client;
|
||||
long calendarID;
|
||||
ContentProviderClient providerClient;
|
||||
Account testAccount = new Account(calendarName, CalendarContract.ACCOUNT_TYPE_LOCAL);
|
||||
LocalCalendar testCalendar;
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
|
||||
protected void setUp() throws Exception {
|
||||
// get content resolver
|
||||
ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
|
||||
client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
providerClient = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||
|
||||
@Cleanup Cursor cursor = client.query(Calendars.CONTENT_URI,
|
||||
long id;
|
||||
|
||||
@Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI,
|
||||
new String[] { Calendars._ID },
|
||||
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.NAME + "=?",
|
||||
new String[] { CalendarContract.ACCOUNT_TYPE_LOCAL, calendarName },
|
||||
null);
|
||||
if (cursor.moveToNext()) {
|
||||
// found local test calendar
|
||||
calendarID = cursor.getLong(0);
|
||||
id = cursor.getLong(0);
|
||||
Log.i(TAG, "Found test calendar with ID " + id);
|
||||
|
||||
} else {
|
||||
// no local test calendar found, create
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Calendars.ACCOUNT_NAME, calendarName);
|
||||
values.put(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL);
|
||||
values.put(Calendars.ACCOUNT_NAME, testAccount.name);
|
||||
values.put(Calendars.ACCOUNT_TYPE, testAccount.type);
|
||||
values.put(Calendars.NAME, calendarName);
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, calendarName);
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
|
||||
@ -55,21 +69,39 @@ public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
|
||||
}
|
||||
|
||||
Uri calendarURI = client.insert(calendarsURI(), values);
|
||||
calendarID = ContentUris.parseId(calendarURI);
|
||||
Uri calendarURI = providerClient.insert(calendarsURI(), values);
|
||||
|
||||
id = ContentUris.parseId(calendarURI);
|
||||
Log.i(TAG, "Created test calendar with ID " + id);
|
||||
}
|
||||
|
||||
testCalendar = new LocalCalendar(testAccount, providerClient, id, null, null);
|
||||
}
|
||||
|
||||
protected void tearDown() throws Exception {
|
||||
Uri uri = ContentUris.withAppendedId(calendarsURI(), calendarID);
|
||||
client.delete(uri, null, null);
|
||||
Uri uri = ContentUris.withAppendedId(calendarsURI(), testCalendar.getId());
|
||||
providerClient.delete(uri, null, null);
|
||||
}
|
||||
|
||||
|
||||
// tests
|
||||
|
||||
public void testNothing() {
|
||||
assert(true);
|
||||
public void testFindNew() throws LocalStorageException, RemoteException {
|
||||
// at the beginning, there are no dirty events
|
||||
assertTrue(testCalendar.findNew().length == 0);
|
||||
assertTrue(testCalendar.findUpdated().length == 0);
|
||||
|
||||
// insert a "new" event
|
||||
insertNewEvent();
|
||||
|
||||
// there must be one "new" event now
|
||||
assertTrue(testCalendar.findNew().length == 1);
|
||||
assertTrue(testCalendar.findUpdated().length == 0);
|
||||
|
||||
// nothing has changed, the record must still be "new"
|
||||
// see issue #233
|
||||
assertTrue(testCalendar.findNew().length == 1);
|
||||
assertTrue(testCalendar.findUpdated().length == 0);
|
||||
}
|
||||
|
||||
|
||||
@ -82,5 +114,23 @@ public class LocalCalendarTest extends InstrumentationTestCase {
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").
|
||||
build();
|
||||
}
|
||||
|
||||
protected long insertNewEvent() throws LocalStorageException, RemoteException {
|
||||
Uri uri = Events.CONTENT_URI.buildUpon()
|
||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, testAccount.type)
|
||||
.appendQueryParameter(Calendars.ACCOUNT_NAME, testAccount.name)
|
||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||
.build();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Events.CALENDAR_ID, testCalendar.getId());
|
||||
values.put(Events.TITLE, "Test Event");
|
||||
values.put(Events.ALL_DAY, 0);
|
||||
values.put(Events.DTSTART, Calendar.getInstance().getTimeInMillis());
|
||||
values.put(Events.DTEND, Calendar.getInstance().getTimeInMillis());
|
||||
values.put(Events.EVENT_TIMEZONE, "UTC");
|
||||
values.put(Events.DIRTY, 1);
|
||||
return ContentUris.parseId(providerClient.insert(uri, values));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
|
||||
|
||||
public void testPutAddDontOverwrite() throws IOException, HttpException {
|
||||
// should succeed on a non-existing file
|
||||
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE);
|
||||
assertEquals("has-just-been-created", davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE));
|
||||
|
||||
// should fail on an existing file
|
||||
try {
|
||||
@ -195,7 +195,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
|
||||
|
||||
public void testPutUpdateDontOverwrite() throws IOException, HttpException {
|
||||
// should succeed on an existing file
|
||||
davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
|
||||
assertEquals("has-just-been-updated", davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE));
|
||||
|
||||
// should fail on a non-existing file
|
||||
try {
|
||||
|
Loading…
Reference in New Issue
Block a user