Skip to content

Commit

Permalink
First working hack to integrate ical4android
Browse files Browse the repository at this point in the history
  • Loading branch information
dschuermann committed Feb 9, 2020
1 parent 0200257 commit ee0bce4
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 0 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}

Expand Down Expand Up @@ -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')
Expand Down
1 change: 1 addition & 0 deletions res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,7 @@
<string name="preferences_calendar_no_display_name">Unnamed calendar</string>
<string name="preferences_calendar_account">%1$s account</string>
<string name="preferences_calendar_delete">Delete calendar</string>
<string name="preferences_calendar_import">Import .ics file</string>
<string name="preferences_calendar_display_name">Change name</string>
<string name="preferences_calendar_account_category">Account type</string>
<string name="preferences_calendar_color">Color</string>
Expand Down
144 changes: 144 additions & 0 deletions src/com/android/calendar/ical4android/LocalCalendar.kt
Original file line number Diff line number Diff line change
@@ -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<LocalEvent>(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<String>): 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<LocalCalendar> {

override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
LocalCalendar(account, provider, id)

}

}
75 changes: 75 additions & 0 deletions src/com/android/calendar/ical4android/LocalEvent.kt
Original file line number Diff line number Diff line change
@@ -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<AndroidEvent>, values: ContentValues): super(calendar, values) {
uid = values.getAsString(CalendarContract.Events._SYNC_ID)
lastModified = values.getAsLong(COLUMN_LAST_MODIFIED) ?: 0
}

constructor(calendar: AndroidCalendar<AndroidEvent>, 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<LocalEvent> {

override fun fromProvider(calendar: AndroidCalendar<AndroidEvent>, values: ContentValues) =
LocalEvent(calendar, values)

}

}
109 changes: 109 additions & 0 deletions src/com/android/calendar/settings/CalendarPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand All @@ -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<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")

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)
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit ee0bce4

Please sign in to comment.