0.5.6α Wage Slave

* support primary (preferred) phone numbers/email addresses
* fix (crash) bug in URI sanitation
* don't set calendar properties not supported by device's Android version
* require already-set remote file name when finding updated records
* version bump to 0.5.6α
pull/2/head
rfc2822 11 years ago
parent 4dc65d0144
commit 4f1488ece5

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid"
android:versionCode="23"
android:versionName="0.5.5-alpha" >
android:versionCode="24"
android:versionName="0.5.6-alpha" >
<uses-sdk
android:minSdkVersion="14"

@ -46,13 +46,9 @@
<Type type="custom" />
</DataKind>
<DataKind
kind="photo"
maxOccurs="1" />
<DataKind kind="photo" maxOccurs="1" />
<DataKind
kind="organization"
maxOccurs="1" />
<DataKind kind="organization" maxOccurs="1" />
<DataKind kind="im" >
<Type type="aim" />
@ -66,17 +62,11 @@
<Type type="custom" />
</DataKind>
<DataKind
kind="nickname"
maxOccurs="1" />
<DataKind kind="nickname" maxOccurs="1" />
<DataKind
kind="note"
maxOccurs="1" />
<DataKind kind="note" maxOccurs="1" />
<DataKind
kind="postal"
needsStructured="true" >
<DataKind kind="postal" needsStructured="true" >
<Type type="home" />
<Type type="work" />
<Type type="other" />
@ -85,13 +75,8 @@
<DataKind kind="website" />
<DataKind
dateWithTime="false"
kind="event" >
<Type
maxOccurs="1"
type="birthday"
yearOptional="false" />
<DataKind kind="event" dateWithTime="false" >
<Type maxOccurs="1" type="birthday" yearOptional="false" />
<Type type="anniversary" />
</DataKind>

@ -12,7 +12,7 @@ package at.bitfire.davdroid;
public class Constants {
public static final String
APP_VERSION = "0.5.5-alpha",
APP_VERSION = "0.5.6-alpha",
ACCOUNT_TYPE = "bitfire.at.davdroid",

@ -23,6 +23,9 @@ public class URIUtils {
// handles invalid URLs/paths as good as possible
public static String sanitize(String original) {
if (original == null)
return null;
Pattern p = Pattern.compile("^((https?:)?//([^/]+))?(.*)", Pattern.CASE_INSENSITIVE);
// $1: "http://hostname" or "https://hostname" or "//hostname" or empty (hostname may end with":port")
// $2: "http:" or "https:" or empty
@ -30,7 +33,7 @@ public class URIUtils {
// $4: path or empty
Matcher m = p.matcher(original);
if (m.find()) {
if (m.matches()) {
String schema = m.group(2),
host = m.group(3),
path = m.group(4);

@ -193,7 +193,7 @@ public class LocalAddressBook extends LocalCollection<Contact> {
}
protected void populatePhoneNumbers(Contact c) throws RemoteException {
@Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER },
@Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER, Phone.IS_SUPER_PRIMARY },
Phone.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Phone.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
@ -265,12 +265,14 @@ public class LocalAddressBook extends LocalCollection<Contact> {
if (!StringUtils.isEmpty(customType))
number.addType(TelephoneType.get(labelToXName(customType)));
}
if (cursor.getInt(3) != 0) // IS_PRIMARY
number.addType(TelephoneType.PREF);
c.getPhoneNumbers().add(number);
}
}
protected void populateEmailAddresses(Contact c) throws RemoteException {
@Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL },
@Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL, Email.IS_SUPER_PRIMARY },
Email.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(c.getLocalID()), Email.CONTENT_ITEM_TYPE }, null);
while (cursor != null && cursor.moveToNext()) {
@ -290,6 +292,8 @@ public class LocalAddressBook extends LocalCollection<Contact> {
if (!StringUtils.isEmpty(customType))
email.addType(EmailType.get(labelToXName(customType)));
}
if (cursor.getInt(3) != 0) // IS_PRIMARY
email.addType(EmailType.PREF);
c.getEmails().add(email);
}
}
@ -572,7 +576,13 @@ public class LocalAddressBook extends LocalCollection<Contact> {
protected Builder buildPhoneNumber(Builder builder, Telephone number) {
int typeCode = Phone.TYPE_OTHER;
String typeLabel = null;
boolean is_primary = false;
Set<TelephoneType> types = number.getTypes();
// preferred number?
if (types.contains(TelephoneType.PREF))
is_primary = true;
// 1 Android type <-> 2 VCard types: fax, cell, pager
if (types.contains(TelephoneType.FAX)) {
if (types.contains(TelephoneType.HOME))
@ -625,7 +635,9 @@ public class LocalAddressBook extends LocalCollection<Contact> {
builder = builder
.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
.withValue(Phone.NUMBER, number.getText())
.withValue(Phone.TYPE, typeCode);
.withValue(Phone.TYPE, typeCode)
.withValue(Phone.IS_PRIMARY, is_primary ? 1 : 0)
.withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);
if (typeLabel != null)
builder = builder.withValue(Phone.LABEL, typeLabel);
return builder;
@ -634,9 +646,12 @@ public class LocalAddressBook extends LocalCollection<Contact> {
protected Builder buildEmail(Builder builder, ezvcard.property.Email email) {
int typeCode = 0;
String typeLabel = null;
boolean is_primary = false;
for (EmailType type : email.getTypes())
if (type == EmailType.HOME)
if (type == EmailType.PREF)
is_primary = true;
else if (type == EmailType.HOME)
typeCode = Email.TYPE_HOME;
else if (type == EmailType.WORK)
typeCode = Email.TYPE_WORK;
@ -654,7 +669,9 @@ public class LocalAddressBook extends LocalCollection<Contact> {
builder = builder
.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE)
.withValue(Email.ADDRESS, email.getValue())
.withValue(Email.TYPE, typeCode);
.withValue(Email.TYPE, typeCode)
.withValue(Email.IS_PRIMARY, is_primary ? 1 : 0)
.withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);;
if (typeLabel != null)
builder = builder.withValue(Email.LABEL, typeLabel);
return builder;

@ -95,7 +95,7 @@ public class LocalCalendar extends LocalCollection<Event> {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
protected String entryColumnUID() {
return (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) ?
return (android.os.Build.VERSION.SDK_INT >= 17) ?
Events.UID_2445 : Events.SYNC_DATA2;
}
@ -124,8 +124,6 @@ public class LocalCalendar extends LocalCollection<Event> {
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle());
values.put(Calendars.CALENDAR_COLOR, color);
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
@ -133,6 +131,11 @@ public class LocalCalendar extends LocalCollection<Event> {
values.put(Calendars.SYNC_EVENTS, 1);
values.put(Calendars.VISIBLE, 1);
if (android.os.Build.VERSION.SDK_INT >= 15) {
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
}
if (info.getTimezone() != null)
values.put(Calendars.CALENDAR_TIME_ZONE, info.getTimezone());

@ -66,9 +66,44 @@ 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";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() },
where, null, null);
if (cursor == null)
throw new LocalStorageException("Couldn't query new records");
long[] fresh = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++) {
long id = cursor.getLong(0);
// new record: generate UID + remote file name so that we can upload
T resource = findById(id, false);
resource.generateUID();
resource.generateName();
// write generated UID + remote file name into database
ContentValues values = new ContentValues(2);
values.put(entryColumnUID(), resource.getUid());
values.put(entryColumnRemoteName(), resource.getName());
providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null);
fresh[idx] = id;
}
return fresh;
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
public long[] findDirty() throws LocalStorageException {
String where = entryColumnDirty() + "=1";
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";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
try {
@ -106,40 +141,6 @@ public abstract class LocalCollection<T extends Resource> {
throw new LocalStorageException(ex);
}
}
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";
if (entryColumnParentID() != null)
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
try {
@Cleanup Cursor cursor = providerClient.query(entriesURI(),
new String[] { entryColumnID() },
where, null, null);
if (cursor == null)
throw new LocalStorageException("Couldn't query new records");
long[] fresh = new long[cursor.getCount()];
for (int idx = 0; cursor.moveToNext(); idx++) {
long id = cursor.getLong(0);
// new record: generate UID + remote file name so that we can upload
T resource = findById(id, false);
resource.generateUID();
resource.generateName();
// write generated UID + remote file name into database
ContentValues values = new ContentValues(2);
values.put(entryColumnUID(), resource.getUid());
values.put(entryColumnRemoteName(), resource.getName());
providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null);
fresh[idx] = id;
}
return fresh;
} catch(RemoteException ex) {
throw new LocalStorageException(ex);
}
}
public T findById(long localID, boolean populate) throws LocalStorageException {
try {

@ -146,7 +146,7 @@ public class SyncManager {
private int pushDirty() throws LocalStorageException, IOException, HttpException {
int count = 0;
long[] dirtyIDs = local.findDirty();
long[] dirtyIDs = local.findUpdated();
Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)");
try {
for (long id : dirtyIDs) {

@ -6,6 +6,8 @@ import at.bitfire.davdroid.URIUtils;
public class URIUtilsTest extends TestCase {
public void testSanitize() {
assertNull(URIUtils.sanitize(null));
// escape "@"
assertEquals("https://my%40server/my%40email.com/dir", URIUtils.sanitize("https://my@server/my@email.com/dir"));
assertEquals("http://my%40server/my%40email.com/dir", URIUtils.sanitize("http://my@server/my@email.com/dir"));

@ -206,7 +206,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS);
List<WebDavResource> members = dav.getMembers();
assertEquals(2, members.size());
assertEquals(ROBOHYDRA_BASE + "dav/addressbooks/user%40domain/My%20Contacts%3A1.vcf/", members.get(0).getLocation().toString());
assertEquals(ROBOHYDRA_BASE + "dav/addressbooks/user%40domain/My%20Contacts%3a1.vcf/", members.get(0).getLocation().toString());
assertEquals("HTTPS://example.com/user%40domain/absolute-url.vcf", members.get(1).getLocation().toString());
}

Loading…
Cancel
Save