diff --git a/build.gradle b/build.gradle
index 6d4eeb0ac4..2acbde8b54 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,6 +16,7 @@ allprojects {
repositories {
google()
jcenter()
+ maven { url 'https://jitpack.io' }
}
}
@@ -158,6 +159,8 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
+ implementation 'com.gitlab.bitfireAT:ical4android:640fc41119'
+
implementation project(':external:calendar')
implementation project(':external:colorpicker')
implementation project(':external:timezonepicker')
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f55fade3e7..f4e5b1da9d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -764,6 +764,7 @@
Unnamed calendar
%1$s account
Delete calendar
+ Import .ics file
Change name
Account type
Color
diff --git a/src/com/android/calendar/ical4android/LocalCalendar.kt b/src/com/android/calendar/ical4android/LocalCalendar.kt
new file mode 100644
index 0000000000..6a55802598
--- /dev/null
+++ b/src/com/android/calendar/ical4android/LocalCalendar.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 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 com.android.calendar.ical4android
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.ContentUris
+import android.content.ContentValues
+import android.os.RemoteException
+import android.provider.CalendarContract.Calendars
+import android.provider.CalendarContract.Events
+import at.bitfire.ical4android.AndroidCalendar
+import at.bitfire.ical4android.AndroidCalendarFactory
+import at.bitfire.ical4android.CalendarStorageException
+
+/**
+ * Based on ICSx5
+ */
+class LocalCalendar private constructor(
+ account: Account,
+ provider: ContentProviderClient,
+ id: Long
+): AndroidCalendar(account, provider, LocalEvent.Factory, id) {
+
+ companion object {
+
+ const val DEFAULT_COLOR = 0xFF2F80C7.toInt()
+
+ const val COLUMN_ETAG = Calendars.CAL_SYNC1
+ const val COLUMN_LAST_MODIFIED = Calendars.CAL_SYNC4
+ const val COLUMN_LAST_SYNC = Calendars.CAL_SYNC5
+ const val COLUMN_ERROR_MESSAGE = Calendars.CAL_SYNC6
+
+ fun findById(account: Account, provider: ContentProviderClient, id: Long) =
+ AndroidCalendar.findByID(account, provider, Factory, id)
+
+ fun findAll(account: Account, provider: ContentProviderClient) =
+ AndroidCalendar.find(account, provider, Factory, null, null)
+
+ }
+
+ var url: String? = null // URL of iCalendar file
+ var eTag: String? = null // iCalendar ETag at last successful sync
+
+ var lastModified = 0L // iCalendar Last-Modified at last successful sync (or 0 for none)
+ var lastSync = 0L // time of last sync (0 if none)
+ var errorMessage: String? = null // error message (HTTP status or exception name) of last sync (or null)
+
+
+ override fun populate(info: ContentValues) {
+ super.populate(info)
+ url = info.getAsString(Calendars.NAME)
+
+ eTag = info.getAsString(COLUMN_ETAG)
+ info.getAsLong(COLUMN_LAST_MODIFIED)?.let { lastModified = it }
+
+ info.getAsLong(COLUMN_LAST_SYNC)?.let { lastSync = it }
+ errorMessage = info.getAsString(COLUMN_ERROR_MESSAGE)
+ }
+
+ fun updateStatusSuccess(eTag: String?, lastModified: Long) {
+ this.eTag = eTag
+ this.lastModified = lastModified
+ lastSync = System.currentTimeMillis()
+
+ val values = ContentValues(4)
+ values.put(COLUMN_ETAG, eTag)
+ values.put(COLUMN_LAST_MODIFIED, lastModified)
+ values.put(COLUMN_LAST_SYNC, lastSync)
+ values.putNull(COLUMN_ERROR_MESSAGE)
+ update(values)
+ }
+
+ fun updateStatusNotModified() {
+ lastSync = System.currentTimeMillis()
+
+ val values = ContentValues(1)
+ values.put(COLUMN_LAST_SYNC, lastSync)
+ update(values)
+ }
+
+ fun updateStatusError(message: String) {
+ eTag = null
+ lastModified = 0
+ lastSync = System.currentTimeMillis()
+ errorMessage = message
+
+ val values = ContentValues(4)
+ values.putNull(COLUMN_ETAG)
+ values.putNull(COLUMN_LAST_MODIFIED)
+ values.put(COLUMN_LAST_SYNC, lastSync)
+ values.put(COLUMN_ERROR_MESSAGE, message)
+ update(values)
+ }
+
+ fun updateUrl(url: String) {
+ this.url = url
+
+ val values = ContentValues(1)
+ values.put(Calendars.NAME, url)
+ update(values)
+ }
+
+ fun queryByUID(uid: String) =
+ queryEvents("${Events._SYNC_ID}=?", arrayOf(uid))
+
+ fun retainByUID(uids: MutableSet): Int {
+ var deleted = 0
+ try {
+ provider.query(syncAdapterURI(Events.CONTENT_URI, account),
+ arrayOf(Events._ID, Events._SYNC_ID, Events.ORIGINAL_SYNC_ID),
+ "${Events.CALENDAR_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS NULL", arrayOf(id.toString()), null)?.use { row ->
+ while (row.moveToNext()) {
+ val eventId = row.getLong(0)
+ val syncId = row.getString(1)
+ if (!uids.contains(syncId)) {
+ provider.delete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account), null, null)
+ deleted++
+
+ uids -= syncId
+ }
+ }
+ }
+ return deleted
+ } catch(e: RemoteException) {
+ throw CalendarStorageException("Couldn't delete local events")
+ }
+ }
+
+
+ object Factory: AndroidCalendarFactory {
+
+ override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
+ LocalCalendar(account, provider, id)
+
+ }
+
+}
diff --git a/src/com/android/calendar/ical4android/LocalEvent.kt b/src/com/android/calendar/ical4android/LocalEvent.kt
new file mode 100644
index 0000000000..01e3c664c5
--- /dev/null
+++ b/src/com/android/calendar/ical4android/LocalEvent.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 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 com.android.calendar.ical4android
+
+import android.content.ContentProviderOperation.Builder
+import android.content.ContentValues
+import android.provider.CalendarContract
+import at.bitfire.ical4android.AndroidCalendar
+import at.bitfire.ical4android.AndroidEvent
+import at.bitfire.ical4android.AndroidEventFactory
+import at.bitfire.ical4android.Event
+import net.fortuna.ical4j.model.DateTime
+import net.fortuna.ical4j.model.property.LastModified
+
+/**
+ * Based on ICSx5
+ */
+class LocalEvent: AndroidEvent {
+
+ companion object {
+ const val COLUMN_LAST_MODIFIED = CalendarContract.Events.SYNC_DATA2
+ }
+
+ var uid: String? = null
+ var lastModified = 0L
+
+ private constructor(calendar: AndroidCalendar, values: ContentValues): super(calendar, values) {
+ uid = values.getAsString(CalendarContract.Events._SYNC_ID)
+ lastModified = values.getAsLong(COLUMN_LAST_MODIFIED) ?: 0
+ }
+
+ constructor(calendar: AndroidCalendar, event: Event): super(calendar, event) {
+ uid = event.uid
+ lastModified = event.lastModified?.dateTime?.time ?: 0
+ }
+
+ override fun populateEvent(row: ContentValues) {
+ super.populateEvent(row)
+
+ val event = requireNotNull(event)
+ event.uid = row.getAsString(CalendarContract.Events._SYNC_ID)
+
+ row.getAsLong(COLUMN_LAST_MODIFIED).let {
+ lastModified = it
+ event.lastModified = LastModified(DateTime(it))
+ }
+ }
+
+ override fun buildEvent(recurrence: Event?, builder: Builder) {
+ super.buildEvent(recurrence, builder)
+
+ if (recurrence == null) {
+ // master event
+ builder .withValue(CalendarContract.Events._SYNC_ID, uid)
+ .withValue(COLUMN_LAST_MODIFIED, lastModified)
+ } else
+ // exception
+ builder.withValue(CalendarContract.Events.ORIGINAL_SYNC_ID, uid)
+ }
+
+
+ object Factory: AndroidEventFactory {
+
+ override fun fromProvider(calendar: AndroidCalendar, values: ContentValues) =
+ LocalEvent(calendar, values)
+
+ }
+
+}
diff --git a/src/com/android/calendar/settings/CalendarPreferences.kt b/src/com/android/calendar/settings/CalendarPreferences.kt
index 7261fb8b7e..50294ef653 100644
--- a/src/com/android/calendar/settings/CalendarPreferences.kt
+++ b/src/com/android/calendar/settings/CalendarPreferences.kt
@@ -20,16 +20,26 @@ package com.android.calendar.settings
import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AuthenticatorDescription
+import android.app.Activity
+import android.content.ContentProviderClient
+import android.content.Intent
import android.graphics.drawable.Drawable
+import android.net.Uri
import android.os.Bundle
import android.provider.CalendarContract
import android.util.TypedValue
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.*
+import at.bitfire.ical4android.Event
import com.android.calendar.Utils
+import com.android.calendar.ical4android.LocalCalendar
+import com.android.calendar.ical4android.LocalEvent
import com.android.calendar.persistence.CalendarRepository
import ws.xsoh.etar.R
+import java.io.IOException
+import java.io.InputStreamReader
+import java.util.HashSet
class CalendarPreferences : PreferenceFragmentCompat() {
@@ -95,12 +105,21 @@ class CalendarPreferences : PreferenceFragmentCompat() {
deleteCalendar()
true
}
+ val importPreference = Preference(context).apply {
+ title = getString(R.string.preferences_calendar_import)
+ isVisible = isLocalAccount
+ }
+ importPreference.setOnPreferenceClickListener {
+ openIcsFile()
+ true
+ }
screen.addPreference(synchronizePreference)
screen.addPreference(visiblePreference)
screen.addPreference(colorPreference)
screen.addPreference(displayNamePreference)
screen.addPreference(deletePreference)
+ screen.addPreference(importPreference)
val accountCategory = PreferenceCategory(context).apply {
title = getString(R.string.preferences_calendar_account_category)
@@ -121,6 +140,94 @@ class CalendarPreferences : PreferenceFragmentCompat() {
preferenceScreen = screen
}
+ private fun openIcsFile() {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "text/calendar"
+
+ // undocumented extra to show internal storage
+ putExtra("android.content.extra.SHOW_ADVANCED", true)
+ }
+
+ startActivityForResult(intent, PICK_CALENDAR_FILE)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == PICK_CALENDAR_FILE && resultCode == Activity.RESULT_OK) {
+ data?.data?.also { uri ->
+ importIcs(uri)
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun importIcs(uri: Uri) {
+ context!!.contentResolver.openInputStream(uri)?.use { inputStream ->
+ InputStreamReader(inputStream, Charsets.UTF_8).use { reader ->
+ try {
+ val events = Event.eventsFromReader(reader)
+ processEvents(events)
+
+// 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
+ }
+ }
+ }
+ }
+
+
+ private fun processEvents(events: List) {
+// Log.i(Constants.TAG, "Processing ${events.size} events")
+ val uids = HashSet(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")
+
+ val client: ContentProviderClient? = requireActivity().contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
+ val calendar = LocalCalendar.findById(account, client!!, calendarId)
+ 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
+// } else if (lastModified != null && exLastModified.dateTime.after(lastModified.date))
+// 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")
+ }
+
+
private fun getThemeDrawable(attr: Int): Drawable {
val typedValue = TypedValue()
context!!.theme.resolveAttribute(attr, typedValue, true)
@@ -201,6 +308,8 @@ class CalendarPreferences : PreferenceFragmentCompat() {
companion object {
const val COLOR_PICKER_DIALOG_TAG = "CalendarColorPickerDialog"
+ const val PICK_CALENDAR_FILE = 2
+
const val ARG_CALENDAR_ID = "calendarId"
const val SYNCHRONIZE_KEY = "synchronize"