ProcessEventsTask.kt 6.44 KB
Newer Older
Ricki Hirner's avatar
Ricki Hirner committed
1 2 3 4 5 6
package at.bitfire.icsdroid

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.Log
7
import androidx.core.app.NotificationCompat
Ricki Hirner's avatar
Ricki Hirner committed
8
import at.bitfire.ical4android.AndroidCalendar
Ricki Hirner's avatar
Ricki Hirner committed
9
import at.bitfire.ical4android.Event
10
import at.bitfire.icsdroid.db.CalendarCredentials
Ricki Hirner's avatar
Ricki Hirner committed
11 12 13 14
import at.bitfire.icsdroid.db.LocalCalendar
import at.bitfire.icsdroid.db.LocalEvent
import at.bitfire.icsdroid.ui.CalendarListActivity
import at.bitfire.icsdroid.ui.NotificationUtils
15 16
import okhttp3.MediaType
import java.io.InputStream
Ricki Hirner's avatar
Ricki Hirner committed
17 18 19 20 21 22 23 24 25 26 27 28 29 30
import java.io.InputStreamReader
import java.net.MalformedURLException
import java.net.URL
import java.util.*

class ProcessEventsTask(
        val context: Context,
        val calendar: LocalCalendar
): Runnable {

    override fun run() {
        Thread.currentThread().contextClassLoader = context.classLoader

        try {
Ricki Hirner's avatar
Ricki Hirner committed
31 32 33
            // provide iCalendar event color values to Android
            AndroidCalendar.insertColors(calendar.provider, calendar.account)

Ricki Hirner's avatar
Ricki Hirner committed
34
            processEvents()
35 36
        } catch(e: Exception) {
            Log.e(Constants.TAG, "Couldn't sync calendar", e)
Ricki Hirner's avatar
Ricki Hirner committed
37
            calendar.updateStatusError(e.localizedMessage ?: e.toString())
Ricki Hirner's avatar
Ricki Hirner committed
38 39 40 41 42
        }
        Log.i(Constants.TAG, "iCalendar file completely processed")
    }

    private fun processEvents() {
43
        val url: URL
Ricki Hirner's avatar
Ricki Hirner committed
44 45 46 47
        try {
            url = URL(calendar.url)
        } catch(e: MalformedURLException) {
            Log.e(Constants.TAG, "Invalid calendar URL", e)
Ricki Hirner's avatar
Ricki Hirner committed
48
            calendar.updateStatusError(e.localizedMessage ?: e.toString())
Ricki Hirner's avatar
Ricki Hirner committed
49 50
            return
        }
51
        Log.i(Constants.TAG, "Synchronizing $url")
Ricki Hirner's avatar
Ricki Hirner committed
52 53 54 55

        // dismiss old notifications
        val notificationManager = NotificationUtils.createChannels(context)
        notificationManager.cancel(calendar.id.toString(), 0)
56
        var errorMessage: String? = null
Ricki Hirner's avatar
Ricki Hirner committed
57

58 59 60 61
        val downloader = object: CalendarFetcher(context, url) {
            override fun onSuccess(data: InputStream, contentType: MediaType?, eTag: String?, lastModified: Long?) {
                InputStreamReader(data, contentType?.charset() ?: Charsets.UTF_8).use { reader ->
                    try {
62
                        val events = Event.eventsFromReader(reader)
63
                        processEvents(events)
Ricki Hirner's avatar
Ricki Hirner committed
64

65 66 67 68 69
                        Log.i(Constants.TAG, "Calendar sync successful, ETag=$eTag, lastModified=$lastModified")
                        calendar.updateStatusSuccess(eTag, lastModified ?: 0L)
                    } catch (e: Exception) {
                        Log.e(Constants.TAG, "Couldn't process events", e)
                        errorMessage = e.localizedMessage
Ricki Hirner's avatar
Ricki Hirner committed
70 71
                    }
                }
72
            }
Ricki Hirner's avatar
Ricki Hirner committed
73

74 75 76 77
            override fun onNotModified() {
                Log.i(Constants.TAG, "Calendar has not been modified since last sync")
                calendar.updateStatusNotModified()
            }
78

Ricki Hirner's avatar
Ricki Hirner committed
79
            override fun onNewPermanentUrl(target: URL) {
80
                super.onNewPermanentUrl(target)
Ricki Hirner's avatar
Ricki Hirner committed
81 82
                Log.i(Constants.TAG, "Got permanent redirect, saving new URL: $target")
                calendar.updateUrl(target.toString())
83
            }
Ricki Hirner's avatar
Ricki Hirner committed
84

85 86 87
            override fun onError(error: Exception) {
                Log.w(Constants.TAG, "Sync error", error)
                errorMessage = error.localizedMessage
Ricki Hirner's avatar
Ricki Hirner committed
88 89 90
            }
        }

91
        CalendarCredentials(context).get(calendar).let { (username, password) ->
92 93 94 95
            downloader.username = username
            downloader.password = password
        }

96 97 98 99 100 101 102
        if (calendar.eTag != null)
            downloader.ifNoneMatch = calendar.eTag
        if (calendar.lastModified != 0L)
            downloader.ifModifiedSince = calendar.lastModified

        downloader.run()

Ricki Hirner's avatar
Ricki Hirner committed
103 104 105 106
        errorMessage?.let { msg ->
            val notification = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_SYNC)
                    .setSmallIcon(R.drawable.ic_sync_problem_white)
                    .setCategory(NotificationCompat.CATEGORY_ERROR)
107
                    .setGroup(context.getString(R.string.app_name))
Ricki Hirner's avatar
Ricki Hirner committed
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
                    .setContentTitle(context.getString(R.string.sync_error_title))
                    .setContentText(msg)
                    .setSubText(calendar.displayName)
                    .setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, CalendarListActivity::class.java), 0))
                    .setAutoCancel(true)
                    .setWhen(System.currentTimeMillis())
                    .setOnlyAlertOnce(true)
            calendar.color?.let { notification.color = it }
            notificationManager.notify(calendar.id.toString(), 0, notification.build())

            calendar.updateStatusError(msg)
        }
    }

    private fun processEvents(events: List<Event>) {
        Log.i(Constants.TAG, "Processing ${events.size} events")
        val uids = HashSet<String>(events.size)

        for (event in events) {
            val uid = event.uid!!
            Log.d(Constants.TAG, "Found VEVENT: $uid")
            uids += uid

            val localEvents = calendar.queryByUID(uid)
            if (localEvents.isEmpty()) {
                Log.d(Constants.TAG, "$uid not in local calendar, adding")
                LocalEvent(calendar, event).add()

            } else {
                val localEvent = localEvents.first()
                var lastModified = event.lastModified

                if (lastModified != null) {
                    // process LAST-MODIFIED of exceptions
                    for (exception in event.exceptions) {
                        val exLastModified = exception.lastModified
                        if (exLastModified == null) {
                            lastModified = null
                            break
147
                        } else if (lastModified != null && exLastModified.dateTime > lastModified.date)
Ricki Hirner's avatar
Ricki Hirner committed
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
                            lastModified = exLastModified
                    }
                }

                if (lastModified == null || lastModified.dateTime.time > localEvent.lastModified)
                    // either there is no LAST-MODIFIED, or LAST-MODIFIED has been increased
                    localEvent.update(event)
                else
                    Log.d(Constants.TAG, "$uid has not been modified since last sync")
            }
        }

        Log.i(Constants.TAG, "Deleting old events (retaining ${uids.size} events by UID) …")
        val deleted = calendar.retainByUID(uids)
        Log.i(Constants.TAG, "… $deleted events deleted")
    }

165
}