Skip to content

Commit f41b4fd

Browse files
sunkuprfc2822
andauthored
Show a notification at sync when ContentProvider inaccessible (#1278)
* Extract notification handling to separate class * Notify user at sync if content provider is missing * Dismiss only content provider specific notification * Remove title from notification text body * Move sync warning strings into their own block * Add KDoc, duplicate method for clarity * Show message in notification for disabled tasks apps * Pass authority through method calls * Shorten method names * Don't show content provider error notification when missing permission * Rename methods and remove obsolete var * Add spacing in content provider missing warning * Improve kdoc * Remove obsolete tasks provider error messages * Syntactic sugar --------- Co-authored-by: Ricki Hirner <[email protected]>
1 parent f6bd4b0 commit f41b4fd

File tree

5 files changed

+394
-196
lines changed

5 files changed

+394
-196
lines changed

app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt

+56-165
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,9 @@
55
package at.bitfire.davdroid.sync
66

77
import android.accounts.Account
8-
import android.app.PendingIntent
9-
import android.content.ContentUris
108
import android.content.Context
11-
import android.content.Intent
12-
import android.net.Uri
139
import android.os.DeadObjectException
1410
import android.os.RemoteException
15-
import android.provider.CalendarContract
16-
import android.provider.ContactsContract
17-
import androidx.core.app.NotificationCompat
18-
import androidx.core.app.NotificationManagerCompat
19-
import androidx.core.app.TaskStackBuilder
2011
import at.bitfire.dav4jvm.DavCollection
2112
import at.bitfire.dav4jvm.DavResource
2213
import at.bitfire.dav4jvm.Error
@@ -45,25 +36,16 @@ import at.bitfire.davdroid.repository.DavCollectionRepository
4536
import at.bitfire.davdroid.repository.DavServiceRepository
4637
import at.bitfire.davdroid.repository.DavSyncStatsRepository
4738
import at.bitfire.davdroid.resource.LocalCollection
48-
import at.bitfire.davdroid.resource.LocalContact
49-
import at.bitfire.davdroid.resource.LocalEvent
5039
import at.bitfire.davdroid.resource.LocalResource
51-
import at.bitfire.davdroid.resource.LocalTask
52-
import at.bitfire.davdroid.ui.DebugInfoActivity
53-
import at.bitfire.davdroid.ui.NotificationRegistry
54-
import at.bitfire.davdroid.ui.account.AccountSettingsActivity
5540
import at.bitfire.ical4android.CalendarStorageException
5641
import at.bitfire.ical4android.Ical4Android
57-
import at.bitfire.ical4android.TaskProvider
5842
import at.bitfire.vcard4android.ContactsStorageException
59-
import com.google.common.base.Ascii
6043
import dagger.hilt.android.qualifiers.ApplicationContext
6144
import kotlinx.coroutines.coroutineScope
6245
import kotlinx.coroutines.launch
6346
import kotlinx.coroutines.runBlocking
6447
import okhttp3.HttpUrl
6548
import okhttp3.RequestBody
66-
import org.dmfs.tasks.contract.TaskContract
6749
import java.io.IOException
6850
import java.io.InterruptedIOException
6951
import java.net.HttpURLConnection
@@ -151,9 +133,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
151133
@Inject
152134
lateinit var logger: Logger
153135

154-
@Inject
155-
lateinit var notificationRegistry: NotificationRegistry
156-
157136
@Inject
158137
lateinit var accountRepository: AccountRepository
159138

@@ -166,23 +145,27 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
166145
@Inject
167146
lateinit var collectionRepository: DavCollectionRepository
168147

148+
@Inject
149+
lateinit var syncNotificationManagerFactory: SyncNotificationManager.Factory
150+
169151

170152
init {
171153
// required for ServiceLoader -> ical4j -> ical4android
172154
Ical4Android.checkThreadContextClassLoader()
173155
}
174156

175-
protected val notificationTag = localCollection.tag
176-
177157
protected lateinit var davCollection: RemoteType
178158

179159
protected var hasCollectionSync = false
180160

161+
private val syncNotificationManager by lazy {
162+
syncNotificationManagerFactory.create(account)
163+
}
181164

182165
fun performSync() {
183166
// dismiss previous error notifications
184-
val nm = NotificationManagerCompat.from(context)
185-
nm.cancel(notificationTag, NotificationRegistry.NOTIFY_SYNC_ERROR)
167+
syncNotificationManager.dismissInvalidResource(localCollectionTag = localCollection.tag)
168+
186169

187170
try {
188171
logger.info("Preparing synchronization")
@@ -322,7 +305,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
322305

323306
// when a certificate is rejected by cert4android, the cause will be a CertificateException
324307
if (e.cause !is CertificateException)
325-
notifyException(e, local, remote)
308+
handleException(e, local, remote)
326309
}
327310

328311
// specific HTTP errors
@@ -335,7 +318,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
335318

336319
// all others
337320
else ->
338-
notifyException(e, local, remote)
321+
handleException(e, local, remote)
339322
}
340323
}
341324
}
@@ -742,158 +725,66 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
742725
}
743726

744727

745-
// exception helpers
746-
747-
private fun notifyException(e: Throwable, local: LocalResource<*>?, remote: HttpUrl?) {
748-
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_SYNC_ERROR, tag = notificationTag) {
749-
val message: String
728+
// notification helpers
750729

751-
when (e) {
752-
is IOException -> {
753-
logger.log(Level.WARNING, "I/O error", e)
754-
message = context.getString(R.string.sync_error_io, e.localizedMessage)
755-
syncResult.numIoExceptions++
756-
}
757-
758-
is UnauthorizedException -> {
759-
logger.log(Level.SEVERE, "Not authorized anymore", e)
760-
message = context.getString(R.string.sync_error_authentication_failed)
761-
syncResult.numAuthExceptions++
762-
}
763-
764-
is HttpException, is DavException -> {
765-
logger.log(Level.SEVERE, "HTTP/DAV exception", e)
766-
message = context.getString(R.string.sync_error_http_dav, e.localizedMessage)
767-
syncResult.numHttpExceptions++
768-
}
769-
770-
is CalendarStorageException, is ContactsStorageException, is RemoteException -> {
771-
logger.log(Level.SEVERE, "Couldn't access local storage", e)
772-
message = context.getString(R.string.sync_error_local_storage, e.localizedMessage)
773-
syncResult.localStorageError = true
774-
}
775-
776-
else -> {
777-
logger.log(Level.SEVERE, "Unclassified sync error", e)
778-
message = e.localizedMessage ?: e::class.java.simpleName
779-
syncResult.numUnclassifiedErrors++
780-
}
730+
/**
731+
* Logs the exception, updates sync result and shows a notification to the user.
732+
*/
733+
private fun handleException(e: Throwable, local: LocalResource<*>?, remote: HttpUrl?) {
734+
var message: String
735+
when (e) {
736+
is IOException -> {
737+
logger.log(Level.WARNING, "I/O error", e)
738+
syncResult.numIoExceptions++
739+
message = context.getString(R.string.sync_error_io, e.localizedMessage)
781740
}
782741

783-
val contentIntent: Intent
784-
var viewItemAction: NotificationCompat.Action? = null
785-
if (e is UnauthorizedException) {
786-
contentIntent = Intent(context, AccountSettingsActivity::class.java)
787-
contentIntent.putExtra(
788-
AccountSettingsActivity.EXTRA_ACCOUNT,
789-
account
790-
)
791-
} else {
792-
contentIntent = buildDebugInfoIntent(e, local, remote)
793-
if (local != null)
794-
viewItemAction = buildViewItemAction(local)
742+
is UnauthorizedException -> {
743+
logger.log(Level.SEVERE, "Not authorized anymore", e)
744+
syncResult.numAuthExceptions++
745+
message = context.getString(R.string.sync_error_authentication_failed)
795746
}
796747

797-
// to make the PendingIntent unique
798-
contentIntent.data = Uri.parse("davdroid:exception/${e.hashCode()}")
799-
800-
val channel: String
801-
val priority: Int
802-
if (e is IOException) {
803-
channel = notificationRegistry.CHANNEL_SYNC_IO_ERRORS
804-
priority = NotificationCompat.PRIORITY_MIN
805-
} else {
806-
channel = notificationRegistry.CHANNEL_SYNC_ERRORS
807-
priority = NotificationCompat.PRIORITY_DEFAULT
748+
is HttpException, is DavException -> {
749+
logger.log(Level.SEVERE, "HTTP/DAV exception", e)
750+
syncResult.numHttpExceptions++
751+
message = context.getString(R.string.sync_error_http_dav, e.localizedMessage)
808752
}
809753

810-
val builder = NotificationCompat.Builder(context, channel)
811-
builder.setSmallIcon(R.drawable.ic_sync_problem_notify)
812-
.setContentTitle(localCollection.title)
813-
.setContentText(message)
814-
.setStyle(NotificationCompat.BigTextStyle(builder).bigText(message))
815-
.setSubText(account.name)
816-
.setOnlyAlertOnce(true)
817-
.setContentIntent(
818-
TaskStackBuilder.create(context)
819-
.addNextIntentWithParentStack(contentIntent)
820-
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
821-
)
822-
.setPriority(priority)
823-
.setCategory(NotificationCompat.CATEGORY_ERROR)
824-
viewItemAction?.let { builder.addAction(it) }
825-
826-
builder.build()
827-
}
828-
}
829-
830-
private fun buildDebugInfoIntent(e: Throwable, local: LocalResource<*>?, remote: HttpUrl?): Intent {
831-
val builder = DebugInfoActivity.IntentBuilder(context)
832-
.withAccount(account)
833-
.withAuthority(authority)
834-
.withCause(e)
835-
836-
if (local != null)
837-
try {
838-
// Truncate the string to avoid the Intent to be > 1 MB, which doesn't work (IPC limit)
839-
builder.withLocalResource(Ascii.truncate(local.toString(), 10000, "[…]"))
840-
} catch (_: OutOfMemoryError) {
841-
// For instance because of a huge contact photo; maybe we're lucky and can catch it
754+
is CalendarStorageException, is ContactsStorageException, is RemoteException -> {
755+
logger.log(Level.SEVERE, "Couldn't access local storage", e)
756+
syncResult.localStorageError = true
757+
message = context.getString(R.string.sync_error_local_storage, e.localizedMessage)
842758
}
843759

844-
if (remote != null)
845-
builder.withRemoteResource(remote)
846-
847-
return builder.build()
848-
}
849-
850-
private fun buildViewItemAction(local: LocalResource<*>): NotificationCompat.Action? {
851-
logger.log(Level.FINE, "Adding view action for local resource", local)
852-
val intent = local.id?.let { id ->
853-
when (local) {
854-
is LocalContact ->
855-
Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, id))
856-
is LocalEvent ->
857-
Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id))
858-
is LocalTask ->
859-
Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), id))
860-
else ->
861-
null
760+
else -> {
761+
logger.log(Level.SEVERE, "Unclassified sync error", e)
762+
syncResult.numUnclassifiedErrors++
763+
message = e.localizedMessage ?: e::class.java.simpleName
862764
}
863765
}
864-
return if (intent != null && context.packageManager.resolveActivity(intent, 0) != null)
865-
NotificationCompat.Action(
866-
android.R.drawable.ic_menu_view,
867-
context.getString(R.string.sync_error_view_item),
868-
TaskStackBuilder.create(context)
869-
.addNextIntent(intent)
870-
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
871-
)
872-
else
873-
null
874-
}
875766

876-
protected fun notifyInvalidResource(e: Throwable, fileName: String) {
877-
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_INVALID_RESOURCE, tag = notificationTag) {
878-
val intent = buildDebugInfoIntent(e, null, collection.url.resolve(fileName))
879-
880-
val builder = NotificationCompat.Builder(context, notificationRegistry.CHANNEL_SYNC_WARNINGS)
881-
builder.setSmallIcon(R.drawable.ic_warning_notify)
882-
.setContentTitle(notifyInvalidResourceTitle())
883-
.setContentText(context.getString(R.string.sync_invalid_resources_ignoring))
884-
.setSubText(account.name)
885-
.setContentIntent(
886-
TaskStackBuilder.create(context)
887-
.addNextIntent(intent)
888-
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
889-
)
890-
.setAutoCancel(true)
891-
.setOnlyAlertOnce(true)
892-
.priority = NotificationCompat.PRIORITY_LOW
893-
builder.build()
894-
}
767+
syncNotificationManager.notifyException(
768+
authority,
769+
localCollection.tag,
770+
message,
771+
localCollection,
772+
e,
773+
local,
774+
remote
775+
)
895776
}
896777

778+
protected fun notifyInvalidResource(e: Throwable, fileName: String) =
779+
syncNotificationManager.notifyInvalidResource(
780+
authority,
781+
localCollection.tag,
782+
collection,
783+
e,
784+
fileName,
785+
notifyInvalidResourceTitle()
786+
)
787+
897788
protected abstract fun notifyInvalidResourceTitle(): String
898789

899790
}

0 commit comments

Comments
 (0)