diff --git a/app/src/main/java/at/bitfire/davdroid/NotificationHelper.java b/app/src/main/java/at/bitfire/davdroid/NotificationHelper.java new file mode 100644 index 00000000..ee07d2ca --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/NotificationHelper.java @@ -0,0 +1,84 @@ +package at.bitfire.davdroid; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v7.app.NotificationCompat; + +import java.util.logging.Level; + +import at.bitfire.davdroid.journalmanager.Exceptions; +import at.bitfire.davdroid.ui.AccountSettingsActivity; +import at.bitfire.davdroid.ui.DebugInfoActivity; +import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.vcard4android.ContactsStorageException; +import lombok.Getter; + +public class NotificationHelper { + final NotificationManagerCompat notificationManager; + final Context context; + final String notificationTag; + final int notificationId; + @Getter + Intent detailsIntent; + int messageString; + + public NotificationHelper(Context context, String notificationTag, int notificationId) { + this.notificationManager = NotificationManagerCompat.from(context); + this.context = context; + this.notificationTag = notificationTag; + this.notificationId = notificationId; + } + + public void setThrowable(Throwable e) { + if (e instanceof Exceptions.UnauthorizedException) { + App.log.log(Level.SEVERE, "Not authorized anymore", e); + messageString = R.string.sync_error_unauthorized; + } else if (e instanceof Exceptions.ServiceUnavailableException) { + App.log.log(Level.SEVERE, "Service unavailable"); + messageString = R.string.sync_error_unavailable; + } else if (e instanceof Exceptions.HttpException) { + App.log.log(Level.SEVERE, "HTTP Exception during sync", e); + messageString = R.string.sync_error_http_dav; + } else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException || e instanceof SQLiteException) { + App.log.log(Level.SEVERE, "Couldn't access local storage", e); + messageString = R.string.sync_error_local_storage; + } else if (e instanceof Exceptions.IntegrityException) { + App.log.log(Level.SEVERE, "Integrity error", e); + messageString = R.string.sync_error_integrity; + } else { + App.log.log(Level.SEVERE, "Unknown sync error", e); + messageString = R.string.sync_error; + } + + if (e instanceof Exceptions.UnauthorizedException) { + detailsIntent = new Intent(context, AccountSettingsActivity.class); + } else { + detailsIntent = new Intent(context, DebugInfoActivity.class); + detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e); + } + + detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + notificationTag)); + } + + public void notify(String title, String state) { + String message = context.getString(messageString, state); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + builder.setSmallIcon(R.drawable.ic_error_light) + .setLargeIcon(App.getLauncherBitmap(context)) + .setContentTitle(title) + .setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT)) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setContentText(message); + + notificationManager.notify(notificationTag, notificationId, builder.build()); + } + + public void cancel() { + notificationManager.cancel(notificationTag, notificationId); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java index 187b7ce4..5fa7a7bd 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java @@ -13,6 +13,7 @@ import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; import android.content.SyncResult; import android.database.Cursor; import android.database.DatabaseUtils; @@ -21,6 +22,7 @@ import android.database.sqlite.SQLiteException; import android.os.Bundle; import android.provider.CalendarContract; import android.support.annotation.NonNull; +import android.support.v4.app.NotificationManagerCompat; import java.util.LinkedHashMap; import java.util.Map; @@ -28,13 +30,17 @@ import java.util.logging.Level; import at.bitfire.davdroid.AccountSettings; import at.bitfire.davdroid.App; -import at.bitfire.davdroid.InvalidAccountException; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.NotificationHelper; +import at.bitfire.davdroid.R; import at.bitfire.davdroid.journalmanager.Exceptions; import at.bitfire.davdroid.model.CollectionInfo; import at.bitfire.davdroid.model.ServiceDB; import at.bitfire.davdroid.model.ServiceDB.Collections; import at.bitfire.davdroid.model.ServiceDB.Services; import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.ui.AccountSettingsActivity; +import at.bitfire.davdroid.ui.DebugInfoActivity; import at.bitfire.ical4android.CalendarStorageException; import lombok.Cleanup; import okhttp3.HttpUrl; @@ -57,6 +63,9 @@ public class CalendarsSyncAdapterService extends SyncAdapterService { public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { super.onPerformSync(account, extras, authority, provider, syncResult); + NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals", Constants.NOTIFICATION_CALENDAR_SYNC); + notificationManager.cancel(); + try { AccountSettings settings = new AccountSettings(getContext(), account); if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings)) @@ -71,15 +80,27 @@ public class CalendarsSyncAdapterService extends SyncAdapterService { CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, settings, extras, authority, syncResult, calendar, principal); syncManager.performSync(); } - } catch (CalendarStorageException | SQLiteException e) { - App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e); - syncResult.databaseError = true; - } catch (InvalidAccountException e) { - App.log.log(Level.SEVERE, "Couldn't get account settings", e); - } catch (Exceptions.HttpException e) { - e.printStackTrace(); - } catch (Exceptions.IntegrityException e) { - e.printStackTrace(); + } catch (Exception | OutOfMemoryError e) { + if (e instanceof CalendarStorageException || e instanceof SQLiteException) { + App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e); + syncResult.databaseError = true; + } + + String syncPhase = SyncManager.SYNC_PHASE_JOURNALS; + String title = getContext().getString(R.string.sync_error_calendar, account.name); + + notificationManager.setThrowable(e); + + final Intent detailsIntent = notificationManager.getDetailsIntent(); + if (e instanceof Exceptions.UnauthorizedException) { + detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account); + } else { + detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account); + detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority); + detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase); + } + + notificationManager.notify(title, syncPhase); } App.log.info("Calendar sync complete"); diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java index cfb4e2de..5ca04737 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java @@ -13,6 +13,7 @@ import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; import android.content.SyncResult; import android.database.Cursor; import android.database.DatabaseUtils; @@ -20,16 +21,22 @@ import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.NotificationManagerCompat; import java.util.logging.Level; import at.bitfire.davdroid.AccountSettings; import at.bitfire.davdroid.App; +import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.InvalidAccountException; +import at.bitfire.davdroid.NotificationHelper; +import at.bitfire.davdroid.R; import at.bitfire.davdroid.journalmanager.Exceptions; import at.bitfire.davdroid.model.CollectionInfo; import at.bitfire.davdroid.model.ServiceDB; import at.bitfire.davdroid.model.ServiceDB.Collections; +import at.bitfire.davdroid.ui.AccountSettingsActivity; +import at.bitfire.davdroid.ui.DebugInfoActivity; import lombok.Cleanup; import okhttp3.HttpUrl; @@ -50,6 +57,8 @@ public class ContactsSyncAdapterService extends SyncAdapterService { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { super.onPerformSync(account, extras, authority, provider, syncResult); + NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals", Constants.NOTIFICATION_CONTACTS_SYNC); + notificationManager.cancel(); ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext()); try { @@ -72,12 +81,21 @@ public class ContactsSyncAdapterService extends SyncAdapterService { } } else App.log.info("No CardDAV service found in DB"); - } catch (InvalidAccountException e) { - App.log.log(Level.SEVERE, "Couldn't get account settings", e); - } catch (Exceptions.HttpException e) { - e.printStackTrace(); - } catch (Exceptions.IntegrityException e) { - e.printStackTrace(); + } catch (Exception | OutOfMemoryError e) { + String syncPhase = SyncManager.SYNC_PHASE_JOURNALS; + String title = getContext().getString(R.string.sync_error_contacts, account.name); + + notificationManager.setThrowable(e); + + final Intent detailsIntent = notificationManager.getDetailsIntent(); + if (e instanceof Exceptions.UnauthorizedException) { + detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account); + } else { + detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account); + detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority); + detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase); + } + notificationManager.notify(title, syncPhase); } finally { dbHelper.close(); } 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 ebf39f87..e82d1c46 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -9,14 +9,11 @@ package at.bitfire.davdroid.syncadapter; import android.accounts.Account; import android.annotation.TargetApi; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SyncResult; -import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NotificationManagerCompat; -import android.support.v7.app.NotificationCompat; import java.io.FileNotFoundException; import java.io.IOException; @@ -34,7 +31,7 @@ import at.bitfire.davdroid.App; import at.bitfire.davdroid.GsonHelper; import at.bitfire.davdroid.HttpClient; import at.bitfire.davdroid.InvalidAccountException; -import at.bitfire.davdroid.R; +import at.bitfire.davdroid.NotificationHelper; import at.bitfire.davdroid.journalmanager.Exceptions; import at.bitfire.davdroid.journalmanager.JournalEntryManager; import at.bitfire.davdroid.model.CollectionInfo; @@ -50,7 +47,8 @@ import okhttp3.OkHttpClient; abstract public class SyncManager { - protected final String SYNC_PHASE_PREPARE = "sync_phase_prepare", + final static String SYNC_PHASE_PREPARE = "sync_phase_prepare", + SYNC_PHASE_JOURNALS = "sync_phase_journals", SYNC_PHASE_QUERY_CAPABILITIES = "sync_phase_query_capabilities", SYNC_PHASE_PREPARE_LOCAL = "sync_phase_prepare_local", SYNC_PHASE_CREATE_LOCAL_ENTRIES = "sync_phase_create_local_entries", @@ -62,7 +60,7 @@ abstract public class SyncManager { SYNC_PHASE_SAVE_SYNC_TAG = "sync_phase_save_sync_tag"; - protected final NotificationManagerCompat notificationManager; + protected final NotificationHelper notificationManager; protected final String uniqueCollectionId; protected final Context context; @@ -113,8 +111,8 @@ abstract public class SyncManager { // dismiss previous error notifications this.uniqueCollectionId = uniqueCollectionId; - notificationManager = NotificationManagerCompat.from(context); - notificationManager.cancel(uniqueCollectionId, notificationId()); + notificationManager = new NotificationHelper(context, uniqueCollectionId, notificationId()); + notificationManager.cancel(); } protected abstract int notificationId(); @@ -193,57 +191,30 @@ abstract public class SyncManager { // syncResult.delayUntil = (retryAfter.getTime() - new Date().getTime()) / 1000; } } catch (Exception | OutOfMemoryError e) { - final int messageString; - if (e instanceof Exceptions.UnauthorizedException) { - App.log.log(Level.SEVERE, "Not authorized anymore", e); - messageString = R.string.sync_error_unauthorized; syncResult.stats.numAuthExceptions++; } else if (e instanceof Exceptions.HttpException) { - App.log.log(Level.SEVERE, "HTTP Exception during sync", e); - messageString = R.string.sync_error_http_dav; syncResult.stats.numParseExceptions++; } else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) { - App.log.log(Level.SEVERE, "Couldn't access local storage", e); - messageString = R.string.sync_error_local_storage; syncResult.databaseError = true; } else if (e instanceof Exceptions.IntegrityException) { - App.log.log(Level.SEVERE, "Integrity error", e); - messageString = R.string.sync_error_integrity; syncResult.stats.numParseExceptions++; } else { - App.log.log(Level.SEVERE, "Unknown sync error", e); - messageString = R.string.sync_error; syncResult.stats.numParseExceptions++; } - final Intent detailsIntent; + notificationManager.setThrowable(e); + + final Intent detailsIntent = notificationManager.getDetailsIntent(); if (e instanceof Exceptions.UnauthorizedException) { - detailsIntent = new Intent(context, AccountSettingsActivity.class); detailsIntent.putExtra(AccountSettingsActivity.EXTRA_ACCOUNT, account); } else { - detailsIntent = new Intent(context, DebugInfoActivity.class); - detailsIntent.putExtra(DebugInfoActivity.KEY_THROWABLE, e); detailsIntent.putExtra(DebugInfoActivity.KEY_ACCOUNT, account); detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority); detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase); } - // to make the PendingIntent unique - detailsIntent.setData(Uri.parse("uri://" + getClass().getName() + "/" + uniqueCollectionId)); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(context); - builder.setSmallIcon(R.drawable.ic_error_light) - .setLargeIcon(App.getLauncherBitmap(context)) - .setContentTitle(getSyncErrorTitle()) - .setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT)) - .setCategory(NotificationCompat.CATEGORY_ERROR); - - String message = context.getString(messageString, syncPhase); - builder.setContentText(message); - - - notificationManager.notify(uniqueCollectionId, notificationId(), builder.build()); + notificationManager.notify(getSyncErrorTitle(), syncPhase); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20805d76..8112a688 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -264,6 +264,7 @@ Error while %s Integrity error while %s Server error while %s + Could not connect to server while %s Database error while %s preparing synchronization