Loading app/build.gradle +2 −2 Original line number Diff line number Diff line Loading @@ -18,8 +18,8 @@ android { minSdkVersion 14 targetSdkVersion 23 versionCode 73 versionName "0.9-alpha1" versionCode 74 versionName "0.9-alpha2" buildConfigField "java.util.Date", "buildTime", "new java.util.Date()" } Loading app/src/main/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -86,6 +86,10 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ui.DebugInfoActivity" android:label="@string/debug_info_title"> </activity> <activity android:name=".ui.setup.AddAccountActivity" android:excludeFromRecents="true" > Loading app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +84 −14 Original line number Diff line number Diff line Loading @@ -8,6 +8,9 @@ package at.bitfire.davdroid.syncadapter; import android.accounts.Account; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; Loading @@ -15,14 +18,13 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SyncResult; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; Loading @@ -33,7 +35,6 @@ import org.apache.commons.io.Charsets; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.util.HashMap; import java.util.HashSet; Loading @@ -56,12 +57,13 @@ import at.bitfire.dav4android.property.SupportedAddressData; import at.bitfire.davdroid.ArrayUtils; import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.HttpClient; import at.bitfire.davdroid.R; import at.bitfire.davdroid.resource.LocalAddressBook; import at.bitfire.davdroid.resource.LocalContact; import at.bitfire.davdroid.ui.DebugInfoActivity; import at.bitfire.vcard4android.Contact; import at.bitfire.vcard4android.ContactsStorageException; import ezvcard.VCardVersion; import ezvcard.property.Uid; import ezvcard.util.IOUtils; import lombok.Cleanup; import lombok.RequiredArgsConstructor; Loading Loading @@ -90,6 +92,20 @@ public class ContactsSyncAdapterService extends Service { private static class ContactsSyncAdapter extends AbstractThreadedSyncAdapter { private static final int NOTIFICATION_ERROR = 1, SYNC_PHASE_QUERY_CAPABILITIES = 0, SYNC_PHASE_PROCESS_LOCALLY_DELETED = 1, SYNC_PHASE_PREPARE_LOCALLY_CREATED = 2, SYNC_PHASE_UPLOAD_DIRTY = 3, SYNC_PHASE_CHECK_STATE = 4, SYNC_PHASE_LIST_LOCAL = 5, SYNC_PHASE_LIST_REMOTE = 6, SYNC_PHASE_COMPARE_ENTRIES = 7, SYNC_PHASE_DOWNLOAD_REMOTE = 8, SYNC_PHASE_SAVE_STATE = 9; public ContactsSyncAdapter(Context context) { super(context, false); } Loading @@ -103,10 +119,16 @@ public class ContactsSyncAdapterService extends Service { HttpUrl addressBookURL = HttpUrl.parse(settings.getAddressBookURL()); DavAddressBook dav = new DavAddressBook(httpClient, addressBookURL); try { // dismiss previous error notifications NotificationManager notificationManager = (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(account.name, NOTIFICATION_ERROR); // prepare local address book LocalAddressBook addressBook = new LocalAddressBook(account, provider); int syncPhase = SYNC_PHASE_QUERY_CAPABILITIES; try { // prepare remote address book boolean hasVCard4 = false; dav.propfind(0, SupportedAddressData.NAME, GetCTag.NAME); Loading @@ -117,6 +139,7 @@ public class ContactsSyncAdapterService extends Service { hasVCard4 = true; Constants.log.info("Server advertises VCard/4 support: " + hasVCard4); syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED; // Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before), // but only if they don't have changed on the server. Then finally remove them from the local address book. LocalContact[] localList = addressBook.getDeleted(); Loading @@ -133,8 +156,10 @@ public class ContactsSyncAdapterService extends Service { } else Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded"); local.delete(); syncResult.stats.numDeletes++; } syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED; // assign file names and UIDs to new contacts so that we can use the file name as an index localList = addressBook.getWithoutFileName(); for (LocalContact local : localList) { Loading @@ -143,6 +168,7 @@ public class ContactsSyncAdapterService extends Service { local.updateFileNameAndUID(uuid); } syncPhase = SYNC_PHASE_UPLOAD_DIRTY; // upload dirty contacts localList = addressBook.getDirty(); for (LocalContact local : localList) { Loading Loading @@ -181,6 +207,7 @@ public class ContactsSyncAdapterService extends Service { local.clearDirty(eTag); } syncPhase = SYNC_PHASE_CHECK_STATE; // check CTag (ignore on manual sync) String currentCTag = null; GetCTag getCTag = (GetCTag) dav.properties.get(GetCTag.NAME); Loading @@ -197,6 +224,7 @@ public class ContactsSyncAdapterService extends Service { Constants.log.info("Remote address book didn't change (CTag=" + currentCTag + "), no need to list VCards"); } else /* remote CTag has changed */ { syncPhase = SYNC_PHASE_LIST_LOCAL; // fetch list of local contacts and build hash table to index file name localList = addressBook.getAll(); Map<String, LocalContact> localContacts = new HashMap<>(localList.length); Loading @@ -205,6 +233,7 @@ public class ContactsSyncAdapterService extends Service { localContacts.put(contact.getFileName(), contact); } syncPhase = SYNC_PHASE_LIST_REMOTE; // fetch list of remote VCards and build hash table to index file name Constants.log.info("Listing remote VCards"); dav.queryMemberETags(); Loading @@ -215,6 +244,7 @@ public class ContactsSyncAdapterService extends Service { remoteContacts.put(fileName, vCard); } syncPhase = SYNC_PHASE_COMPARE_ENTRIES; /* check which contacts 1. are not present anymore remotely -> delete immediately on local side 2. updated remotely -> add to downloadNames Loading @@ -226,6 +256,7 @@ public class ContactsSyncAdapterService extends Service { if (remote == null) { Constants.log.info(localName + " is not on server anymore, deleting"); localContacts.get(localName).delete(); syncResult.stats.numDeletes++; } else { // contact is still on server, check whether it has been updated remotely GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME); Loading @@ -233,7 +264,9 @@ public class ContactsSyncAdapterService extends Service { throw new DavException("Server didn't provide ETag"); String localETag = localContacts.get(localName).eTag, remoteETag = getETag.eTag; if (!remoteETag.equals(localETag)) { if (remoteETag.equals(localETag)) syncResult.stats.numSkippedEntries++; else { Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")"); toDownload.add(remote); } Loading @@ -249,6 +282,7 @@ public class ContactsSyncAdapterService extends Service { toDownload.addAll(remoteContacts.values()); } syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE; Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)"); // prepare downloader which may be used to download external resource like contact photos Loading @@ -267,7 +301,7 @@ public class ContactsSyncAdapterService extends Service { String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag; @Cleanup InputStream stream = body.byteStream(); processVCard(addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader); processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader); } else { // multiple contacts, use multi-get Loading Loading @@ -298,11 +332,12 @@ public class ContactsSyncAdapterService extends Service { throw new DavException("Received multi-get response without address data"); @Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes()); processVCard(addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader); processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader); } } } syncPhase = SYNC_PHASE_SAVE_STATE; /* Save sync state (CTag). It doesn't matter if it has changed during the sync process (for instance, because another client has uploaded changes), because this will simply cause all remote entries to be listed at the next sync. */ Loading @@ -310,15 +345,49 @@ public class ContactsSyncAdapterService extends Service { addressBook.setCTag(currentCTag); } } catch (Exception e) { Log.e("davdroid", "XXX", e); } catch (IOException e) { Constants.log.error("I/O exception during sync, trying again later", e); syncResult.stats.numIoExceptions++; } catch(HttpException e) { Constants.log.error("HTTP Exception during sync", e); syncResult.stats.numParseExceptions++; Intent detailsIntent = new Intent(getContext(), 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(getContext()); Notification notification = null; builder .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(getContext().getString(R.string.sync_error_title, account.name)) .setContentIntent(PendingIntent.getActivity(getContext(), 0, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT)); String[] phases = getContext().getResources().getStringArray(R.array.sync_error_phases); if (phases.length > syncPhase) builder.setContentText(getContext().getString(R.string.sync_error_http, phases[syncPhase])); if (Build.VERSION.SDK_INT >= 16) { if (Build.VERSION.SDK_INT >= 21) builder.setCategory(Notification.CATEGORY_ERROR); notification = builder.build(); } else { notification = builder.getNotification(); } notificationManager.notify(account.name, NOTIFICATION_ERROR, notification); } catch(DavException e) { ; } catch(ContactsStorageException e) { syncResult.databaseError = true; } Constants.log.info("Sync complete for authority " + authority); } private void processVCard(LocalAddressBook addressBook, Map<String, LocalContact>localContacts, String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException { private void processVCard(SyncResult syncResult, LocalAddressBook addressBook, Map<String, LocalContact>localContacts, 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) { Contact newData = contacts[0]; Loading @@ -329,16 +398,17 @@ public class ContactsSyncAdapterService extends Service { Constants.log.info("Updating " + fileName + " in local address book"); localContact.eTag = eTag; localContact.update(newData); syncResult.stats.numUpdates++; } else { Constants.log.info("Adding " + fileName + " to local address book"); localContact = new LocalContact(addressBook, newData, fileName, eTag); localContact.add(); syncResult.stats.numInserts++; } } else Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName); } } Loading app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.java 0 → 100644 +157 −0 Original line number Diff line number Diff line /* * 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.ui; import android.accounts.Account; import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.provider.CalendarContract; import android.provider.ContactsContract; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; import java.io.PrintWriter; import java.io.StringWriter; import at.bitfire.dav4android.exception.HttpException; import at.bitfire.davdroid.BuildConfig; import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.R; import lombok.Cleanup; public class DebugInfoActivity extends Activity { public static final String KEY_EXCEPTION = "exception", KEY_ACCOUNT = "account", KEY_PHASE = "phase"; private static final String APP_ID = "at.bitfire.davdroid"; String report; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.debug_info_activity); TextView tvReport = (TextView)findViewById(R.id.text_report); tvReport.setText(generateReport(getIntent().getExtras())); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.exception_details_activity, menu); return true; } public void onShare(MenuItem item) { if (!TextUtils.isEmpty(report)) { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_SUBJECT, "DAVdroid Exception Details"); sendIntent.putExtra(Intent.EXTRA_TEXT, report); sendIntent.setType("text/plain"); startActivity(sendIntent); } } protected String generateReport(Bundle extras) { Exception exception = null; Account account = null; Integer phase = null; if (extras != null) { exception = (Exception) extras.getSerializable(KEY_EXCEPTION); account = (Account) extras.getParcelable(KEY_ACCOUNT); phase = extras.getInt(KEY_PHASE); } StringBuilder report = new StringBuilder(); try { report.append( "SYSTEM INFORMATION\n" + "Android version: " + Build.VERSION.RELEASE + " (" + Build.DISPLAY + ")\n" + "Device: " + Build.MANUFACTURER + " / " + Build.MODEL + " (" + Build.DEVICE + ")\n\n" ); } catch (Exception ex) { Constants.log.error("Couldn't get system details", ex); } try { PackageManager pm = getPackageManager(); String installedFrom = pm.getInstallerPackageName("at.bitfire.davdroid"); if (TextUtils.isEmpty(installedFrom)) installedFrom = "APK (directly)"; else { PackageInfo installer = pm.getPackageInfo(installedFrom, PackageManager.GET_META_DATA); if (installer != null) installedFrom = pm.getApplicationLabel(installer.applicationInfo).toString(); } report.append( "SOFTWARE INFORMATION\n" + "DAVdroid version: " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ") " + BuildConfig.buildTime.toString() + "\n" + "Installed from: " + installedFrom + "\n\n" ); } catch(Exception ex) { Constants.log.error("Couldn't get software information", ex); } report.append( "CONFIGURATION\n" + "System-wide synchronization: " + (ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually") + " (overrides account settings)\n" ); if (account != null) report.append( "Account name: " + account.name + "\n" + "Address book synchronization: " + syncStatus(account, ContactsContract.AUTHORITY) + "\n" + "Calendar synchronization: " + syncStatus(account, CalendarContract.AUTHORITY) + "\n" + "OpenTasks synchronization: " + syncStatus(account, "org.dmfs.tasks") + "\n\n" ); if (phase != null) { report.append("SYNCHRONIZATION INFO\nSychronization phase: " + phase + "\n\n"); } if (exception instanceof HttpException) { HttpException http = (HttpException)exception; if (http.request != null) report.append("HTTP REQUEST:\n" + http.request + "\n\n"); if (http.response != null) report.append("HTTP RESPONSE:\n" + http.response + "\n\n"); } 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()); } return report.toString(); } protected static String syncStatus(Account account, String authority) { return ContentResolver.getIsSyncable(account, authority) > 0 ? (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY) ? "automatically" : "manually") : "—"; } } app/src/main/java/at/bitfire/davdroid/ui/MainActivity.java +5 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Debug; import android.provider.Settings; import android.text.Html; import android.text.method.LinkMovementMethod; Loading Loading @@ -58,6 +59,10 @@ public class MainActivity extends Activity { startActivity(new Intent(this, AddAccountActivity.class)); } public void showDebugInfo(MenuItem item) { startActivity(new Intent(this, DebugInfoActivity.class)); } public void showSettings(MenuItem item) { startActivity(new Intent(this, SettingsActivity.class)); } Loading Loading
app/build.gradle +2 −2 Original line number Diff line number Diff line Loading @@ -18,8 +18,8 @@ android { minSdkVersion 14 targetSdkVersion 23 versionCode 73 versionName "0.9-alpha1" versionCode 74 versionName "0.9-alpha2" buildConfigField "java.util.Date", "buildTime", "new java.util.Date()" } Loading
app/src/main/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -86,6 +86,10 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ui.DebugInfoActivity" android:label="@string/debug_info_title"> </activity> <activity android:name=".ui.setup.AddAccountActivity" android:excludeFromRecents="true" > Loading
app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +84 −14 Original line number Diff line number Diff line Loading @@ -8,6 +8,9 @@ package at.bitfire.davdroid.syncadapter; import android.accounts.Account; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; Loading @@ -15,14 +18,13 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SyncResult; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; Loading @@ -33,7 +35,6 @@ import org.apache.commons.io.Charsets; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.util.HashMap; import java.util.HashSet; Loading @@ -56,12 +57,13 @@ import at.bitfire.dav4android.property.SupportedAddressData; import at.bitfire.davdroid.ArrayUtils; import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.HttpClient; import at.bitfire.davdroid.R; import at.bitfire.davdroid.resource.LocalAddressBook; import at.bitfire.davdroid.resource.LocalContact; import at.bitfire.davdroid.ui.DebugInfoActivity; import at.bitfire.vcard4android.Contact; import at.bitfire.vcard4android.ContactsStorageException; import ezvcard.VCardVersion; import ezvcard.property.Uid; import ezvcard.util.IOUtils; import lombok.Cleanup; import lombok.RequiredArgsConstructor; Loading Loading @@ -90,6 +92,20 @@ public class ContactsSyncAdapterService extends Service { private static class ContactsSyncAdapter extends AbstractThreadedSyncAdapter { private static final int NOTIFICATION_ERROR = 1, SYNC_PHASE_QUERY_CAPABILITIES = 0, SYNC_PHASE_PROCESS_LOCALLY_DELETED = 1, SYNC_PHASE_PREPARE_LOCALLY_CREATED = 2, SYNC_PHASE_UPLOAD_DIRTY = 3, SYNC_PHASE_CHECK_STATE = 4, SYNC_PHASE_LIST_LOCAL = 5, SYNC_PHASE_LIST_REMOTE = 6, SYNC_PHASE_COMPARE_ENTRIES = 7, SYNC_PHASE_DOWNLOAD_REMOTE = 8, SYNC_PHASE_SAVE_STATE = 9; public ContactsSyncAdapter(Context context) { super(context, false); } Loading @@ -103,10 +119,16 @@ public class ContactsSyncAdapterService extends Service { HttpUrl addressBookURL = HttpUrl.parse(settings.getAddressBookURL()); DavAddressBook dav = new DavAddressBook(httpClient, addressBookURL); try { // dismiss previous error notifications NotificationManager notificationManager = (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(account.name, NOTIFICATION_ERROR); // prepare local address book LocalAddressBook addressBook = new LocalAddressBook(account, provider); int syncPhase = SYNC_PHASE_QUERY_CAPABILITIES; try { // prepare remote address book boolean hasVCard4 = false; dav.propfind(0, SupportedAddressData.NAME, GetCTag.NAME); Loading @@ -117,6 +139,7 @@ public class ContactsSyncAdapterService extends Service { hasVCard4 = true; Constants.log.info("Server advertises VCard/4 support: " + hasVCard4); syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED; // Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before), // but only if they don't have changed on the server. Then finally remove them from the local address book. LocalContact[] localList = addressBook.getDeleted(); Loading @@ -133,8 +156,10 @@ public class ContactsSyncAdapterService extends Service { } else Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded"); local.delete(); syncResult.stats.numDeletes++; } syncPhase = SYNC_PHASE_PREPARE_LOCALLY_CREATED; // assign file names and UIDs to new contacts so that we can use the file name as an index localList = addressBook.getWithoutFileName(); for (LocalContact local : localList) { Loading @@ -143,6 +168,7 @@ public class ContactsSyncAdapterService extends Service { local.updateFileNameAndUID(uuid); } syncPhase = SYNC_PHASE_UPLOAD_DIRTY; // upload dirty contacts localList = addressBook.getDirty(); for (LocalContact local : localList) { Loading Loading @@ -181,6 +207,7 @@ public class ContactsSyncAdapterService extends Service { local.clearDirty(eTag); } syncPhase = SYNC_PHASE_CHECK_STATE; // check CTag (ignore on manual sync) String currentCTag = null; GetCTag getCTag = (GetCTag) dav.properties.get(GetCTag.NAME); Loading @@ -197,6 +224,7 @@ public class ContactsSyncAdapterService extends Service { Constants.log.info("Remote address book didn't change (CTag=" + currentCTag + "), no need to list VCards"); } else /* remote CTag has changed */ { syncPhase = SYNC_PHASE_LIST_LOCAL; // fetch list of local contacts and build hash table to index file name localList = addressBook.getAll(); Map<String, LocalContact> localContacts = new HashMap<>(localList.length); Loading @@ -205,6 +233,7 @@ public class ContactsSyncAdapterService extends Service { localContacts.put(contact.getFileName(), contact); } syncPhase = SYNC_PHASE_LIST_REMOTE; // fetch list of remote VCards and build hash table to index file name Constants.log.info("Listing remote VCards"); dav.queryMemberETags(); Loading @@ -215,6 +244,7 @@ public class ContactsSyncAdapterService extends Service { remoteContacts.put(fileName, vCard); } syncPhase = SYNC_PHASE_COMPARE_ENTRIES; /* check which contacts 1. are not present anymore remotely -> delete immediately on local side 2. updated remotely -> add to downloadNames Loading @@ -226,6 +256,7 @@ public class ContactsSyncAdapterService extends Service { if (remote == null) { Constants.log.info(localName + " is not on server anymore, deleting"); localContacts.get(localName).delete(); syncResult.stats.numDeletes++; } else { // contact is still on server, check whether it has been updated remotely GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME); Loading @@ -233,7 +264,9 @@ public class ContactsSyncAdapterService extends Service { throw new DavException("Server didn't provide ETag"); String localETag = localContacts.get(localName).eTag, remoteETag = getETag.eTag; if (!remoteETag.equals(localETag)) { if (remoteETag.equals(localETag)) syncResult.stats.numSkippedEntries++; else { Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")"); toDownload.add(remote); } Loading @@ -249,6 +282,7 @@ public class ContactsSyncAdapterService extends Service { toDownload.addAll(remoteContacts.values()); } syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE; Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)"); // prepare downloader which may be used to download external resource like contact photos Loading @@ -267,7 +301,7 @@ public class ContactsSyncAdapterService extends Service { String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag; @Cleanup InputStream stream = body.byteStream(); processVCard(addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader); processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader); } else { // multiple contacts, use multi-get Loading Loading @@ -298,11 +332,12 @@ public class ContactsSyncAdapterService extends Service { throw new DavException("Received multi-get response without address data"); @Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes()); processVCard(addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader); processVCard(syncResult, addressBook, localContacts, remote.fileName(), eTag, stream, charset, downloader); } } } syncPhase = SYNC_PHASE_SAVE_STATE; /* Save sync state (CTag). It doesn't matter if it has changed during the sync process (for instance, because another client has uploaded changes), because this will simply cause all remote entries to be listed at the next sync. */ Loading @@ -310,15 +345,49 @@ public class ContactsSyncAdapterService extends Service { addressBook.setCTag(currentCTag); } } catch (Exception e) { Log.e("davdroid", "XXX", e); } catch (IOException e) { Constants.log.error("I/O exception during sync, trying again later", e); syncResult.stats.numIoExceptions++; } catch(HttpException e) { Constants.log.error("HTTP Exception during sync", e); syncResult.stats.numParseExceptions++; Intent detailsIntent = new Intent(getContext(), 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(getContext()); Notification notification = null; builder .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(getContext().getString(R.string.sync_error_title, account.name)) .setContentIntent(PendingIntent.getActivity(getContext(), 0, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT)); String[] phases = getContext().getResources().getStringArray(R.array.sync_error_phases); if (phases.length > syncPhase) builder.setContentText(getContext().getString(R.string.sync_error_http, phases[syncPhase])); if (Build.VERSION.SDK_INT >= 16) { if (Build.VERSION.SDK_INT >= 21) builder.setCategory(Notification.CATEGORY_ERROR); notification = builder.build(); } else { notification = builder.getNotification(); } notificationManager.notify(account.name, NOTIFICATION_ERROR, notification); } catch(DavException e) { ; } catch(ContactsStorageException e) { syncResult.databaseError = true; } Constants.log.info("Sync complete for authority " + authority); } private void processVCard(LocalAddressBook addressBook, Map<String, LocalContact>localContacts, String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException { private void processVCard(SyncResult syncResult, LocalAddressBook addressBook, Map<String, LocalContact>localContacts, 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) { Contact newData = contacts[0]; Loading @@ -329,16 +398,17 @@ public class ContactsSyncAdapterService extends Service { Constants.log.info("Updating " + fileName + " in local address book"); localContact.eTag = eTag; localContact.update(newData); syncResult.stats.numUpdates++; } else { Constants.log.info("Adding " + fileName + " to local address book"); localContact = new LocalContact(addressBook, newData, fileName, eTag); localContact.add(); syncResult.stats.numInserts++; } } else Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName); } } Loading
app/src/main/java/at/bitfire/davdroid/ui/DebugInfoActivity.java 0 → 100644 +157 −0 Original line number Diff line number Diff line /* * 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.ui; import android.accounts.Account; import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.provider.CalendarContract; import android.provider.ContactsContract; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; import java.io.PrintWriter; import java.io.StringWriter; import at.bitfire.dav4android.exception.HttpException; import at.bitfire.davdroid.BuildConfig; import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.R; import lombok.Cleanup; public class DebugInfoActivity extends Activity { public static final String KEY_EXCEPTION = "exception", KEY_ACCOUNT = "account", KEY_PHASE = "phase"; private static final String APP_ID = "at.bitfire.davdroid"; String report; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.debug_info_activity); TextView tvReport = (TextView)findViewById(R.id.text_report); tvReport.setText(generateReport(getIntent().getExtras())); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.exception_details_activity, menu); return true; } public void onShare(MenuItem item) { if (!TextUtils.isEmpty(report)) { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_SUBJECT, "DAVdroid Exception Details"); sendIntent.putExtra(Intent.EXTRA_TEXT, report); sendIntent.setType("text/plain"); startActivity(sendIntent); } } protected String generateReport(Bundle extras) { Exception exception = null; Account account = null; Integer phase = null; if (extras != null) { exception = (Exception) extras.getSerializable(KEY_EXCEPTION); account = (Account) extras.getParcelable(KEY_ACCOUNT); phase = extras.getInt(KEY_PHASE); } StringBuilder report = new StringBuilder(); try { report.append( "SYSTEM INFORMATION\n" + "Android version: " + Build.VERSION.RELEASE + " (" + Build.DISPLAY + ")\n" + "Device: " + Build.MANUFACTURER + " / " + Build.MODEL + " (" + Build.DEVICE + ")\n\n" ); } catch (Exception ex) { Constants.log.error("Couldn't get system details", ex); } try { PackageManager pm = getPackageManager(); String installedFrom = pm.getInstallerPackageName("at.bitfire.davdroid"); if (TextUtils.isEmpty(installedFrom)) installedFrom = "APK (directly)"; else { PackageInfo installer = pm.getPackageInfo(installedFrom, PackageManager.GET_META_DATA); if (installer != null) installedFrom = pm.getApplicationLabel(installer.applicationInfo).toString(); } report.append( "SOFTWARE INFORMATION\n" + "DAVdroid version: " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ") " + BuildConfig.buildTime.toString() + "\n" + "Installed from: " + installedFrom + "\n\n" ); } catch(Exception ex) { Constants.log.error("Couldn't get software information", ex); } report.append( "CONFIGURATION\n" + "System-wide synchronization: " + (ContentResolver.getMasterSyncAutomatically() ? "automatically" : "manually") + " (overrides account settings)\n" ); if (account != null) report.append( "Account name: " + account.name + "\n" + "Address book synchronization: " + syncStatus(account, ContactsContract.AUTHORITY) + "\n" + "Calendar synchronization: " + syncStatus(account, CalendarContract.AUTHORITY) + "\n" + "OpenTasks synchronization: " + syncStatus(account, "org.dmfs.tasks") + "\n\n" ); if (phase != null) { report.append("SYNCHRONIZATION INFO\nSychronization phase: " + phase + "\n\n"); } if (exception instanceof HttpException) { HttpException http = (HttpException)exception; if (http.request != null) report.append("HTTP REQUEST:\n" + http.request + "\n\n"); if (http.response != null) report.append("HTTP RESPONSE:\n" + http.response + "\n\n"); } 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()); } return report.toString(); } protected static String syncStatus(Account account, String authority) { return ContentResolver.getIsSyncable(account, authority) > 0 ? (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY) ? "automatically" : "manually") : "—"; } }
app/src/main/java/at/bitfire/davdroid/ui/MainActivity.java +5 −0 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Debug; import android.provider.Settings; import android.text.Html; import android.text.method.LinkMovementMethod; Loading Loading @@ -58,6 +59,10 @@ public class MainActivity extends Activity { startActivity(new Intent(this, AddAccountActivity.class)); } public void showDebugInfo(MenuItem item) { startActivity(new Intent(this, DebugInfoActivity.class)); } public void showSettings(MenuItem item) { startActivity(new Intent(this, SettingsActivity.class)); } Loading