From 20bc5af4a32ca3039f08245b562ae0758208a969 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 15 Oct 2015 13:46:19 +0200 Subject: [PATCH] Resource detection, bug fixes * resource detection is subject to change yet * don't use UID_2445 for Android <= 4.1 * more useful sync error notification messages * handle 401 Unauthorized and show account info when notification is tapped --- .../java/at/bitfire/davdroid/HttpClient.java | 5 +- .../davdroid/resource/DavResourceFinder.java | 199 +++++++++--------- .../bitfire/davdroid/resource/LocalEvent.java | 5 +- .../bitfire/davdroid/resource/LocalTask.java | 4 +- .../davdroid/syncadapter/SyncManager.java | 40 +++- .../syncadapter/TasksSyncManager.java | 4 +- .../davdroid/ui/DebugInfoActivity.java | 9 +- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values/strings.xml | 6 +- 9 files changed, 161 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/at/bitfire/davdroid/HttpClient.java b/app/src/main/java/at/bitfire/davdroid/HttpClient.java index 931ba661..117bcaac 100644 --- a/app/src/main/java/at/bitfire/davdroid/HttpClient.java +++ b/app/src/main/java/at/bitfire/davdroid/HttpClient.java @@ -126,8 +126,9 @@ public class HttpClient extends OkHttpClient { // add User-Agent to every request networkInterceptors().add(userAgentInterceptor); - // enable logs - enableLogs(); + // enable verbose logs, if requested + if (Constants.log.isTraceEnabled()) + enableLogs(); } protected void enableLogs() { diff --git a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java index 9845f197..7766bf6b 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java @@ -58,117 +58,126 @@ public class DavResourceFinder { final HttpClient httpClient = new HttpClient(context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive); // CardDAV - Constants.log.info("*** CardDAV resource detection ***"); - HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "carddav"); - - DavResource principal = new DavResource(httpClient, principalUrl); - principal.propfind(0, AddressbookHomeSet.NAME); - AddressbookHomeSet addrHomeSet = (AddressbookHomeSet)principal.properties.get(AddressbookHomeSet.NAME); - if (addrHomeSet != null && !addrHomeSet.hrefs.isEmpty()) { - Constants.log.info("Found addressbook home set(s): " + addrHomeSet); - - // enumerate address books - List addressBooks = new LinkedList<>(); - for (String href : addrHomeSet.hrefs) { - DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href)); - homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME, AddressbookDescription.NAME); - for (DavResource member : homeSet.members) { - ResourceType type = (ResourceType)member.properties.get(ResourceType.NAME); - if (type != null && type.types.contains(ResourceType.ADDRESSBOOK)) { - Constants.log.info("Found address book: " + member.location); - - CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet)member.properties.get(CurrentUserPrivilegeSet.NAME); - if (privs != null && (!privs.mayRead || !privs.mayWriteContent)) { - Constants.log.info("Only read/write address books are supported, ignoring this one"); - continue; - } + try { + Constants.log.info("*** CardDAV resource detection ***"); + HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "carddav"); + + DavResource principal = new DavResource(httpClient, principalUrl); + principal.propfind(0, AddressbookHomeSet.NAME); + AddressbookHomeSet addrHomeSet = (AddressbookHomeSet) principal.properties.get(AddressbookHomeSet.NAME); + if (addrHomeSet != null && !addrHomeSet.hrefs.isEmpty()) { + Constants.log.info("Found addressbook home set(s): " + addrHomeSet); + + // enumerate address books + List addressBooks = new LinkedList<>(); + for (String href : addrHomeSet.hrefs) { + DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href)); + homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME, AddressbookDescription.NAME); + for (DavResource member : homeSet.members) { + ResourceType type = (ResourceType) member.properties.get(ResourceType.NAME); + if (type != null && type.types.contains(ResourceType.ADDRESSBOOK)) { + Constants.log.info("Found address book: " + member.location); + + CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet) member.properties.get(CurrentUserPrivilegeSet.NAME); + if (privs != null && (!privs.mayRead || !privs.mayWriteContent)) { + Constants.log.info("Only read/write address books are supported, ignoring this one"); + continue; + } - DisplayName displayName = (DisplayName)member.properties.get(DisplayName.NAME); - AddressbookDescription description = (AddressbookDescription)member.properties.get(AddressbookDescription.NAME); - - addressBooks.add(new ServerInfo.ResourceInfo( - ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, - false, - member.location.toString(), - displayName != null ? displayName.displayName : null, - description != null ? description.description : null, - null - )); + DisplayName displayName = (DisplayName) member.properties.get(DisplayName.NAME); + AddressbookDescription description = (AddressbookDescription) member.properties.get(AddressbookDescription.NAME); + + addressBooks.add(new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, + false, + member.location.toString(), + displayName != null ? displayName.displayName : null, + description != null ? description.description : null, + null + )); + } } } + serverInfo.setAddressBooks(addressBooks); } - serverInfo.setAddressBooks(addressBooks); + } catch(IOException|HttpException|DavException e) { + Constants.log.info("CardDAV detection failed", e); } // CalDAV Constants.log.info("*** CalDAV resource detection ***"); - principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "caldav"); - - principal = new DavResource(httpClient, principalUrl); - principal.propfind(0, CalendarHomeSet.NAME); - CalendarHomeSet calHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME); - if (calHomeSet != null && !calHomeSet.hrefs.isEmpty()) { - Constants.log.info("Found calendar home set(s): " + calHomeSet); - - // enumerate address books - List - calendars = new LinkedList<>(), - taskLists = new LinkedList<>(); - - for (String href : calHomeSet.hrefs) { - DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href)); - homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME, - CalendarDescription.NAME, CalendarColor.NAME, CalendarTimezone.NAME, SupportedCalendarComponentSet.NAME); - for (DavResource member : homeSet.members) { - ResourceType type = (ResourceType)member.properties.get(ResourceType.NAME); - if (type != null && type.types.contains(ResourceType.CALENDAR)) { - Constants.log.info("Found calendar: " + member.location); - - DisplayName displayName = (DisplayName)member.properties.get(DisplayName.NAME); - CalendarDescription description = (CalendarDescription)member.properties.get(CalendarDescription.NAME); - CalendarColor color = (CalendarColor)member.properties.get(CalendarColor.NAME); - - CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet)member.properties.get(CurrentUserPrivilegeSet.NAME); - boolean readOnly = false; - if (privs != null) { - if (!privs.mayRead) { - Constants.log.info("Calendar not readable, ignoring this one"); - continue; + try { + HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "caldav"); + + DavResource principal = new DavResource(httpClient, principalUrl); + principal.propfind(0, CalendarHomeSet.NAME); + CalendarHomeSet calHomeSet = (CalendarHomeSet) principal.properties.get(CalendarHomeSet.NAME); + if (calHomeSet != null && !calHomeSet.hrefs.isEmpty()) { + Constants.log.info("Found calendar home set(s): " + calHomeSet); + + // enumerate address books + List + calendars = new LinkedList<>(), + taskLists = new LinkedList<>(); + + for (String href : calHomeSet.hrefs) { + DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href)); + homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME, + CalendarDescription.NAME, CalendarColor.NAME, CalendarTimezone.NAME, SupportedCalendarComponentSet.NAME); + for (DavResource member : homeSet.members) { + ResourceType type = (ResourceType) member.properties.get(ResourceType.NAME); + if (type != null && type.types.contains(ResourceType.CALENDAR)) { + Constants.log.info("Found calendar: " + member.location); + + DisplayName displayName = (DisplayName) member.properties.get(DisplayName.NAME); + CalendarDescription description = (CalendarDescription) member.properties.get(CalendarDescription.NAME); + CalendarColor color = (CalendarColor) member.properties.get(CalendarColor.NAME); + + CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet) member.properties.get(CurrentUserPrivilegeSet.NAME); + boolean readOnly = false; + if (privs != null) { + if (!privs.mayRead) { + Constants.log.info("Calendar not readable, ignoring this one"); + continue; + } + readOnly = !privs.mayWriteContent; } - readOnly = !privs.mayWriteContent; - } - ServerInfo.ResourceInfo collection = new ServerInfo.ResourceInfo( - ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, - readOnly, - member.location.toString(), - displayName != null ? displayName.displayName : null, - description != null ? description.description : null, - color != null ? color.color : null - ); - - CalendarTimezone tz = (CalendarTimezone)member.properties.get(CalendarTimezone.NAME); - if (tz != null) - collection.timezone = tz.vTimeZone; - - boolean isCalendar = true, isTaskList = true; - SupportedCalendarComponentSet comp = (SupportedCalendarComponentSet)member.properties.get(SupportedCalendarComponentSet.NAME); - if (comp != null) { - isCalendar = comp.supportsEvents; - isTaskList = comp.supportsTasks; - } + ServerInfo.ResourceInfo collection = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, + readOnly, + member.location.toString(), + displayName != null ? displayName.displayName : null, + description != null ? description.description : null, + color != null ? color.color : null + ); + + CalendarTimezone tz = (CalendarTimezone) member.properties.get(CalendarTimezone.NAME); + if (tz != null) + collection.timezone = tz.vTimeZone; + + boolean isCalendar = true, isTaskList = true; + SupportedCalendarComponentSet comp = (SupportedCalendarComponentSet) member.properties.get(SupportedCalendarComponentSet.NAME); + if (comp != null) { + isCalendar = comp.supportsEvents; + isTaskList = comp.supportsTasks; + } - if (isCalendar) - calendars.add(collection); - if (isTaskList) - taskLists.add(collection); + if (isCalendar) + calendars.add(collection); + if (isTaskList) + taskLists.add(collection); + } } } + serverInfo.setCalendars(calendars); + serverInfo.setTaskLists(taskLists); } - serverInfo.setCalendars(calendars); - serverInfo.setTaskLists(taskLists); + } catch(IOException|HttpException|DavException e) { + Constants.log.info("CalDAV detection failed", e); } + // TODO /*if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()) throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav));*/ } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java index fa987ba1..afdd543d 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalEvent.java @@ -10,6 +10,7 @@ package at.bitfire.davdroid.resource; import android.content.ContentProviderOperation; import android.content.ContentValues; +import android.os.Build; import android.os.RemoteException; import android.provider.CalendarContract; import android.provider.CalendarContract.Events; @@ -32,8 +33,8 @@ public class LocalEvent extends AndroidEvent implements LocalResource { } static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1, - COLUMN_UID = CalendarContract.Events.UID_2445, - COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA2; + COLUMN_UID = Build.VERSION.SDK_INT >= 17 ? Events.UID_2445 : Events.SYNC_DATA2, + COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3; @Getter protected String fileName; @Getter @Setter protected String eTag; diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java index 6e7b9ecc..067cecb3 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java @@ -36,8 +36,8 @@ public class LocalTask extends AndroidTask implements LocalResource { } static final String COLUMN_ETAG = Tasks.SYNC1, - COLUMN_UID = Tasks._UID, - COLUMN_SEQUENCE = Tasks.SYNC2; + COLUMN_UID = Tasks.SYNC2, + COLUMN_SEQUENCE = Tasks.SYNC3; @Getter protected String fileName; @Getter @Setter protected String eTag; diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java index ccee003d..d39ff89a 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -33,6 +33,7 @@ import java.util.UUID; import at.bitfire.dav4android.DavResource; import at.bitfire.dav4android.exception.DavException; import at.bitfire.dav4android.exception.HttpException; +import at.bitfire.dav4android.exception.UnauthorizedException; import at.bitfire.dav4android.exception.PreconditionFailedException; import at.bitfire.dav4android.exception.ServiceUnavailableException; import at.bitfire.dav4android.property.GetCTag; @@ -43,6 +44,7 @@ import at.bitfire.davdroid.R; import at.bitfire.davdroid.resource.LocalCollection; import at.bitfire.davdroid.resource.LocalResource; import at.bitfire.davdroid.ui.DebugInfoActivity; +import at.bitfire.davdroid.ui.settings.AccountActivity; import at.bitfire.ical4android.CalendarStorageException; import at.bitfire.vcard4android.ContactsStorageException; @@ -165,18 +167,36 @@ abstract public class SyncManager { } } catch(Exception e) { - if (e instanceof HttpException || e instanceof DavException) { + final int messageString; + + if (e instanceof UnauthorizedException) { + Constants.log.error("Not authorized anymore", e); + messageString = R.string.sync_error_unauthorized; + syncResult.stats.numAuthExceptions++; + } else if (e instanceof HttpException || e instanceof DavException) { Constants.log.error("HTTP/DAV Exception during sync", e); + messageString = R.string.sync_error_http_dav; syncResult.stats.numParseExceptions++; } else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) { Constants.log.error("Couldn't access local storage", e); + messageString = R.string.sync_error_local_storage; syncResult.databaseError = true; + } else { + Constants.log.error("Unknown sync error", e); + messageString = R.string.sync_error; + syncResult.stats.numParseExceptions++; } - Intent detailsIntent = new Intent(context, DebugInfoActivity.class); - detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e); - detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account); - detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase); + final Intent detailsIntent; + if (e instanceof UnauthorizedException) { + detailsIntent = new Intent(context, AccountActivity.class); + detailsIntent.putExtra(AccountActivity.EXTRA_ACCOUNT, account); + } else { + detailsIntent = new Intent(context, DebugInfoActivity.class); + detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e); + detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account); + detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase); + } Notification.Builder builder = new Notification.Builder(context); Notification notification; @@ -184,9 +204,13 @@ abstract public class SyncManager { .setContentTitle(context.getString(R.string.sync_error_title, account.name)) .setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT)); - String[] phases = context.getResources().getStringArray(R.array.sync_error_phases); - if (phases.length > syncPhase) - builder.setContentText(context.getString(R.string.sync_error_http, phases[syncPhase])); + try { + String[] phases = context.getResources().getStringArray(R.array.sync_error_phases); + String message = context.getString(messageString, phases[syncPhase]); + builder.setContentText(message); + } catch (IndexOutOfBoundsException ex) { + // should never happen + } if (Build.VERSION.SDK_INT >= 16) { if (Build.VERSION.SDK_INT >= 21) diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java index 80f40032..b0ab3c67 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java @@ -182,7 +182,7 @@ public class TasksSyncManager extends SyncManager { try { tasks = Task.fromStream(stream, charset); } catch (InvalidCalendarException e) { - Constants.log.error("Received invalid iCalendar, ignoring"); + Constants.log.error("Received invalid iCalendar, ignoring", e); return; } @@ -203,7 +203,7 @@ public class TasksSyncManager extends SyncManager { syncResult.stats.numInserts++; } } else - Constants.log.error("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName); + Constants.log.error("Received VCALENDAR with not exactly one VTODO; ignoring " + fileName); } } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.java b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.java index 962fc39b..c5244828 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.java @@ -23,6 +23,10 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; +import com.google.common.base.Throwables; + +import org.apache.commons.lang3.exception.ExceptionUtils; + import java.io.PrintWriter; import java.io.StringWriter; @@ -137,10 +141,7 @@ public class DebugInfoActivity extends Activity { if (exception != null) { report.append("STACK TRACE\n"); - StringWriter writer = new StringWriter(); - @Cleanup PrintWriter printWriter = new PrintWriter(writer); - exception.printStackTrace(printWriter); - report.append(writer.toString()); + report.append(Throwables.getStackTraceAsString(exception)); } return report.toString(); diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3935b748..3aa4c9ca 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -178,7 +178,9 @@ schreibgeschützt Synchronisierung von %s fehlgeschlagen - HTTP-Fehler beim %1$s + Fehler beim %1$s + Serverfehler + Datenbank-Fehler Vorbereiten der Synchronisierung Abfragen der Server-Fähigkeiten @@ -188,8 +190,10 @@ Abfragen des Synchronisierungs-Zustands Auflisten lokaler Einträge Auflisten der Server-Einträge + Vergleichen tw. Datenbank/Server Herunterladen von Server-Einträgen Speichern des Synchronisierungs-Zustands + Benutzername/Passwort falsch \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16da753b..8afc501f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,7 +193,9 @@ Debug info Synchronization of %s failed - HTTP error while %1$s + Error while %1$s + Server error + Database error preparing synchronization querying capabilities @@ -203,8 +205,10 @@ checking sync state listing local entries listing remote entries + comparing local/remote entries downloading remote entries saving sync state + User name/password wrong