5
5
package at.bitfire.davdroid.sync
6
6
7
7
import android.accounts.Account
8
- import android.app.PendingIntent
9
- import android.content.ContentUris
10
8
import android.content.Context
11
- import android.content.Intent
12
- import android.net.Uri
13
9
import android.os.DeadObjectException
14
10
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
20
11
import at.bitfire.dav4jvm.DavCollection
21
12
import at.bitfire.dav4jvm.DavResource
22
13
import at.bitfire.dav4jvm.Error
@@ -45,25 +36,16 @@ import at.bitfire.davdroid.repository.DavCollectionRepository
45
36
import at.bitfire.davdroid.repository.DavServiceRepository
46
37
import at.bitfire.davdroid.repository.DavSyncStatsRepository
47
38
import at.bitfire.davdroid.resource.LocalCollection
48
- import at.bitfire.davdroid.resource.LocalContact
49
- import at.bitfire.davdroid.resource.LocalEvent
50
39
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
55
40
import at.bitfire.ical4android.CalendarStorageException
56
41
import at.bitfire.ical4android.Ical4Android
57
- import at.bitfire.ical4android.TaskProvider
58
42
import at.bitfire.vcard4android.ContactsStorageException
59
- import com.google.common.base.Ascii
60
43
import dagger.hilt.android.qualifiers.ApplicationContext
61
44
import kotlinx.coroutines.coroutineScope
62
45
import kotlinx.coroutines.launch
63
46
import kotlinx.coroutines.runBlocking
64
47
import okhttp3.HttpUrl
65
48
import okhttp3.RequestBody
66
- import org.dmfs.tasks.contract.TaskContract
67
49
import java.io.IOException
68
50
import java.io.InterruptedIOException
69
51
import java.net.HttpURLConnection
@@ -151,9 +133,6 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
151
133
@Inject
152
134
lateinit var logger: Logger
153
135
154
- @Inject
155
- lateinit var notificationRegistry: NotificationRegistry
156
-
157
136
@Inject
158
137
lateinit var accountRepository: AccountRepository
159
138
@@ -166,23 +145,27 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
166
145
@Inject
167
146
lateinit var collectionRepository: DavCollectionRepository
168
147
148
+ @Inject
149
+ lateinit var syncNotificationManagerFactory: SyncNotificationManager .Factory
150
+
169
151
170
152
init {
171
153
// required for ServiceLoader -> ical4j -> ical4android
172
154
Ical4Android .checkThreadContextClassLoader()
173
155
}
174
156
175
- protected val notificationTag = localCollection.tag
176
-
177
157
protected lateinit var davCollection: RemoteType
178
158
179
159
protected var hasCollectionSync = false
180
160
161
+ private val syncNotificationManager by lazy {
162
+ syncNotificationManagerFactory.create(account)
163
+ }
181
164
182
165
fun performSync () {
183
166
// 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
+
186
169
187
170
try {
188
171
logger.info(" Preparing synchronization" )
@@ -322,7 +305,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
322
305
323
306
// when a certificate is rejected by cert4android, the cause will be a CertificateException
324
307
if (e.cause !is CertificateException )
325
- notifyException (e, local, remote)
308
+ handleException (e, local, remote)
326
309
}
327
310
328
311
// specific HTTP errors
@@ -335,7 +318,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
335
318
336
319
// all others
337
320
else ->
338
- notifyException (e, local, remote)
321
+ handleException (e, local, remote)
339
322
}
340
323
}
341
324
}
@@ -742,158 +725,66 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
742
725
}
743
726
744
727
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
750
729
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)
781
740
}
782
741
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)
795
746
}
796
747
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)
808
752
}
809
753
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)
842
758
}
843
759
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
862
764
}
863
765
}
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
- }
875
766
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
+ )
895
776
}
896
777
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
+
897
788
protected abstract fun notifyInvalidResourceTitle (): String
898
789
899
790
}
0 commit comments