1
0
mirror of https://github.com/etesync/android synced 2024-11-26 01:48:34 +00:00

New DebugInfoActivity

* DebugInfoActivity shows and allows to share sync exceptions
* log sync phase
This commit is contained in:
Ricki Hirner 2015-10-14 12:23:02 +02:00
parent 808958a69b
commit c2e9b27831
11 changed files with 332 additions and 21 deletions

View File

@ -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()"
}

View File

@ -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" >

View File

@ -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;
@ -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;
@ -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;
@ -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;
@ -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);
}
@ -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);
@ -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();
@ -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) {
@ -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) {
@ -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);
@ -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);
@ -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();
@ -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
@ -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);
@ -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);
}
@ -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
@ -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
@ -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. */
@ -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];
@ -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);
}
}

View File

@ -0,0 +1,157 @@
/*
* 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") :
"";
}
}

View File

@ -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;
@ -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));
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:typeface="monospace"
android:id="@+id/text_report"/>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:icon="@android:drawable/ic_menu_share"
android:title="@string/share"
android:showAsAction="always"
android:onClick="onShare" />
</menu>

View File

@ -11,5 +11,6 @@
<item android:onClick="addAccount" android:title="@string/setup_add_account" android:showAsAction="always" android:icon="@drawable/ic_action_new_account"/>
<item android:onClick="showSyncSettings" android:title="@string/main_manage_accounts" android:showAsAction="always" android:icon="@drawable/show_sync_settings"/>
<item android:onClick="showSettings" android:title="@string/settings_title" android:showAsAction="never" android:icon="@drawable/ic_action_settings"/>
<item android:onClick="showDebugInfo" android:title="@string/main_show_debug_info" android:showAsAction="never" />
<item android:onClick="showWebsite" android:title="@string/help" android:showAsAction="ifRoom" android:icon="@drawable/view_website"/>
</menu>

View File

@ -177,6 +177,18 @@
<string name="setup_account_name_info">&quot;Verwenden Sie Ihre Email-Adresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen.</string>
<string name="setup_read_only">schreibgeschützt</string>
<string name="sync_error_title">Synchronisierung fehlgeschlagen</string>
<string name="sync_error_title">Synchronisierung von %s fehlgeschlagen</string>
<string name="sync_error_http">HTTP-Fehler beim %1$s</string>
<string-array name="sync_error_phases">
<item>Abfragen der Server-Fähigkeiten</item>
<item>Verarbeiten lokal gelöschter Einträge</item>
<item>Vorbereiten neuer lokaler Einträge</item>
<item>Hochladen neuer/geänderter lokaler Einträge</item>
<item>Abfragen des Sync.-Zustands</item>
<item>Auflisten lokaler Einträge</item>
<item>Auflisten der Server-Einträge</item>
<item>Herunterladen von Server-Einträgen</item>
<item>Speichern des Sync.-Zustands</item>
</string-array>
</resources>

View File

@ -11,9 +11,10 @@
<!-- common strings -->
<string name="app_name">DAVdroid</string>
<string name="next">Next</string>
<string name="skip">Skip</string>
<string name="help">Help</string>
<string name="next">Next</string>
<string name="share">Share</string>
<string name="skip">Skip</string>
<string name="exception_cert_path_validation">Untrusted certificate in certificate path. See FAQ for more info.</string>
<string name="exception_http">HTTP error: %s</string>
@ -23,6 +24,7 @@
<!-- MainActivity -->
<string name="main_manage_accounts">Manage sync accounts</string>
<string name="main_show_debug_info">Show debug info</string>
<string name="html_main_workaround"><![CDATA[
<p>Thank you for buying DAVdroid via Google Play and thus supporting the project. Unfortunately, there are two issues with Google Play:</p>
@ -188,6 +190,20 @@
<string name="setup_account_name_info">"Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can't have two accounts with the same name.</string>
<string name="setup_read_only">read-only</string>
<string name="sync_error_title">Synchronization failed</string>
<!-- sync errors and DebugInfoActivity -->
<string name="debug_info_title">Debug info</string>
<string name="sync_error_title">Synchronization of %s failed</string>
<string name="sync_error_http">HTTP error while %1$s</string>
<string-array name="sync_error_phases">
<item>querying capabilities</item>
<item>processing locally deleted entries</item>
<item>preparing locally created entries</item>
<item>uploading created/modified entries</item>
<item>checking sync state</item>
<item>listing local entries</item>
<item>listing remote entries</item>
<item>downloading remote entries</item>
<item>saving sync state</item>
</string-array>
</resources>

@ -1 +1 @@
Subproject commit 8258787df4c29697e76c683d1b9e4caea42205ec
Subproject commit e6c3ee6da90a94d3c77675b8fdd9be7e2d5f83e3