You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/com/etesync/syncadapter/ui/JournalItemActivity.kt

583 lines
24 KiB

package com.etesync.syncadapter.ui
import android.accounts.Account
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.view.*
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.Contact
import com.etesync.syncadapter.App
import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.journalmanager.model.SyncEntry
import com.etesync.syncadapter.resource.*
import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment.Companion.setJournalEntryView
import com.etesync.syncadapter.utils.EventEmailInvitation
import com.etesync.syncadapter.utils.TaskProviderHandling
import com.google.android.material.tabs.TabLayout
import ezvcard.util.PartialDate
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.io.IOException
import java.io.StringReader
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Future
class JournalItemActivity : BaseActivity(), Refreshable {
private var journalEntity: JournalEntity? = null
private lateinit var account: Account
protected lateinit var info: CollectionInfo
private lateinit var syncEntry: SyncEntry
private var emailInvitationEvent: Event? = null
private var emailInvitationEventString: String? = null
override fun refresh() {
val data = (applicationContext as App).data
journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid)
if (journalEntity == null || journalEntity!!.isDeleted) {
finish()
return
}
account = intent.extras!!.getParcelable(ViewCollectionActivity.EXTRA_ACCOUNT)!!
info = journalEntity!!.info
title = info.displayName
setJournalEntryView(findViewById(R.id.journal_list_item), info, syncEntry)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.journal_item_activity)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
info = intent.extras!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo
syncEntry = intent.extras!!.getSerializable(KEY_SYNC_ENTRY) as SyncEntry
refresh()
val viewPager = findViewById<View>(R.id.viewpager) as ViewPager
viewPager.adapter = TabsAdapter(supportFragmentManager, this, info, syncEntry)
val tabLayout = findViewById<View>(R.id.tabs) as TabLayout
tabLayout.setupWithViewPager(viewPager)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.activity_journal_item, menu)
menu.setGroupVisible(R.id.journal_item_menu_event_invite, emailInvitationEvent != null)
return true
}
fun allowSendEmail(event: Event?, icsContent: String) {
emailInvitationEvent = event
emailInvitationEventString = icsContent
invalidateOptionsMenu()
}
fun sendEventInvite(item: MenuItem) {
val intent = EventEmailInvitation(this, account).createIntent(emailInvitationEvent!!, emailInvitationEventString!!)
startActivity(intent)
}
fun restoreItem(item: MenuItem) {
// FIXME: This code makes the assumption that providers are all available. May not be true for tasks, and potentially others too.
when (info.enumType) {
CollectionInfo.Type.CALENDAR -> {
val provider = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!
val localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!)!!
val event = Event.eventsFromReader(StringReader(syncEntry.content))[0]
var localEvent = localCalendar.findByUid(event.uid!!)
if (localEvent != null) {
localEvent.updateAsDirty(event)
} else {
localEvent = LocalEvent(localCalendar, event, event.uid, null)
localEvent.addAsDirty()
}
}
CollectionInfo.Type.TASKS -> {
TaskProviderHandling.getWantedTaskSyncProvider(applicationContext)?.let {
val provider = TaskProvider.acquire(this, it)!!
val localTaskList = LocalTaskList.findByName(account, provider, LocalTaskList.Factory, info.uid!!)!!
val task = Task.tasksFromReader(StringReader(syncEntry.content))[0]
var localTask = localTaskList.findByUid(task.uid!!)
if (localTask != null) {
localTask.updateAsDirty(task)
} else {
localTask = LocalTask(localTaskList, task, task.uid, null)
localTask.addAsDirty()
}
}
}
CollectionInfo.Type.ADDRESS_BOOK -> {
val provider = contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!
val localAddressBook = LocalAddressBook.findByUid(this, provider, account, info.uid!!)!!
val contact = Contact.fromReader(StringReader(syncEntry.content), null)[0]
if (contact.group) {
// FIXME: not currently supported
} else {
var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact?
if (localContact != null) {
localContact.updateAsDirty(contact)
} else {
localContact = LocalContact(localAddressBook, contact, contact.uid, null)
localContact.createAsDirty()
}
}
}
null -> {
}
}
val dialog = AlertDialog.Builder(this)
.setTitle(R.string.journal_item_restore_action)
.setIcon(R.drawable.ic_restore_black)
.setMessage(R.string.journal_item_restore_dialog_body)
.setPositiveButton(android.R.string.ok) { dialog, which ->
// dismiss
}
.create()
dialog.show()
}
private class TabsAdapter(fm: FragmentManager, private val context: Context, private val info: CollectionInfo, private val syncEntry: SyncEntry) : FragmentPagerAdapter(fm) {
override fun getCount(): Int {
// FIXME: Make it depend on info enumType (only have non-raw for known types)
return 2
}
override fun getPageTitle(position: Int): CharSequence? {
return if (position == 0) {
context.getString(R.string.journal_item_tab_main)
} else {
context.getString(R.string.journal_item_tab_raw)
}
}
override fun getItem(position: Int): Fragment {
return if (position == 0) {
PrettyFragment.newInstance(info, syncEntry)
} else {
TextFragment.newInstance(syncEntry)
}
}
}
class TextFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.text_fragment, container, false)
val tv = v.findViewById<View>(R.id.content) as TextView
val syncEntry = arguments!!.getSerializable(KEY_SYNC_ENTRY) as SyncEntry
tv.text = syncEntry.content
return v
}
companion object {
fun newInstance(syncEntry: SyncEntry): TextFragment {
val frag = TextFragment()
val args = Bundle(1)
args.putSerializable(KEY_SYNC_ENTRY, syncEntry)
frag.arguments = args
return frag
}
}
}
class PrettyFragment : Fragment() {
internal lateinit var info: CollectionInfo
internal lateinit var syncEntry: SyncEntry
private var asyncTask: Future<Unit>? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var v: View? = null
info = arguments!!.getSerializable(Constants.KEY_COLLECTION_INFO) as CollectionInfo
syncEntry = arguments!!.getSerializable(KEY_SYNC_ENTRY) as SyncEntry
when (info.enumType) {
CollectionInfo.Type.ADDRESS_BOOK -> {
v = inflater.inflate(R.layout.contact_info, container, false)
asyncTask = loadContactTask(v)
}
CollectionInfo.Type.CALENDAR -> {
v = inflater.inflate(R.layout.event_info, container, false)
asyncTask = loadEventTask(v)
}
CollectionInfo.Type.TASKS -> {
v = inflater.inflate(R.layout.task_info, container, false)
asyncTask = loadTaskTask(v)
}
null -> {
}
}
return v
}
override fun onDestroyView() {
super.onDestroyView()
if (asyncTask != null)
asyncTask!!.cancel(true)
}
private fun loadEventTask(view: View): Future<Unit> {
return doAsync {
var event: Event? = null
val inputReader = StringReader(syncEntry.content)
try {
event = Event.eventsFromReader(inputReader, null)[0]
} catch (e: InvalidCalendarException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
if (event != null) {
uiThread {
val loader = view.findViewById<View>(R.id.event_info_loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.event_info_scroll_view)
contentContainer.visibility = View.VISIBLE
setTextViewText(view, R.id.title, event.summary)
val dtStart = event.dtStart?.date?.time
val dtEnd = event.dtEnd?.date?.time
if ((dtStart == null) || (dtEnd == null)) {
setTextViewText(view, R.id.when_datetime, getString(R.string.loading_error_title))
} else {
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(dtStart, dtEnd, event.isAllDay(), context))
}
setTextViewText(view, R.id.where, event.location)
val organizer = event.organizer
if (organizer != null) {
val tv = view.findViewById<View>(R.id.organizer) as TextView
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
} else {
val organizerView = view.findViewById<View>(R.id.organizer_container)
organizerView.visibility = View.GONE
}
setTextViewText(view, R.id.description, event.description)
var first = true
var sb = StringBuilder()
for (attendee in event.attendees) {
if (first) {
first = false
sb.append(getString(R.string.journal_item_attendees)).append(": ")
} else {
sb.append(", ")
}
sb.append(attendee.calAddress.toString().replaceFirst("mailto:".toRegex(), ""))
}
setTextViewText(view, R.id.attendees, sb.toString())
first = true
sb = StringBuilder()
for (alarm in event.alarms) {
if (first) {
first = false
sb.append(getString(R.string.journal_item_reminders)).append(": ")
} else {
sb.append(", ")
}
sb.append(alarm.trigger.value)
}
setTextViewText(view, R.id.reminders, sb.toString())
if (event.attendees.isNotEmpty() && activity != null) {
(activity as JournalItemActivity).allowSendEmail(event, syncEntry.content)
}
}
}
}
}
private fun loadTaskTask(view: View): Future<Unit> {
return doAsync {
var task: Task? = null
val inputReader = StringReader(syncEntry.content)
try {
task = Task.tasksFromReader(inputReader)[0]
} catch (e: InvalidCalendarException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
if (task != null) {
uiThread {
val loader = view.findViewById<View>(R.id.task_info_loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.task_info_scroll_view)
contentContainer.visibility = View.VISIBLE
setTextViewText(view, R.id.title, task.summary)
setTextViewText(view, R.id.where, task.location)
val organizer = task.organizer
if (organizer != null) {
val tv = view.findViewById<View>(R.id.organizer) as TextView
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
} else {
val organizerView = view.findViewById<View>(R.id.organizer_container)
organizerView.visibility = View.GONE
}
setTextViewText(view, R.id.description, task.description)
}
}
}
}
private fun loadContactTask(view: View): Future<Unit> {
return doAsync {
var contact: Contact? = null
val reader = StringReader(syncEntry.content)
try {
contact = Contact.fromReader(reader, null)[0]
} catch (e: IOException) {
e.printStackTrace()
}
if (contact != null) {
uiThread {
val loader = view.findViewById<View>(R.id.loading_msg)
loader.visibility = View.GONE
val contentContainer = view.findViewById<View>(R.id.content_container)
contentContainer.visibility = View.VISIBLE
val tv = view.findViewById<View>(R.id.display_name) as TextView
tv.text = contact.displayName
if (contact.group) {
showGroup(contact)
} else {
showContact(contact)
}
}
}
}
}
private fun showGroup(contact: Contact) {
val view = this.view!!
val mainCard = view.findViewById<View>(R.id.main_card) as ViewGroup
addInfoItem(view.context, mainCard, getString(R.string.journal_item_member_count), null, contact.members.size.toString())
for (member in contact.members) {
addInfoItem(view.context, mainCard, getString(R.string.journal_item_member), null, member)
}
}
private fun showContact(contact: Contact) {
val view = this.view!!
val mainCard = view.findViewById<View>(R.id.main_card) as ViewGroup
val aboutCard = view.findViewById<View>(R.id.about_card) as ViewGroup
aboutCard.findViewById<View>(R.id.title_container).visibility = View.VISIBLE
// TEL
for (labeledPhone in contact.phoneNumbers) {
val types = labeledPhone.property.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, mainCard, getString(R.string.journal_item_phone), type, labeledPhone.property.text)
}
// EMAIL
for (labeledEmail in contact.emails) {
val types = labeledEmail.property.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, mainCard, getString(R.string.journal_item_email), type, labeledEmail.property.value)
}
// ORG, TITLE, ROLE
if (contact.organization != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization?.values!![0])
}
if (contact.jobDescription != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle)
}
// IMPP
for (labeledImpp in contact.impps) {
addInfoItem(view.context, mainCard, getString(R.string.journal_item_impp), labeledImpp.property.protocol, labeledImpp.property.handle)
}
// NICKNAME
if (contact.nickName != null && !contact.nickName?.values?.isEmpty()!!) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName?.values!![0])
}
// ADR
for (labeledAddress in contact.addresses) {
val types = labeledAddress.property.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, mainCard, getString(R.string.journal_item_address), type, labeledAddress.property.label)
}
// NOTE
if (contact.note != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_note), null, contact.note)
}
// URL
for (labeledUrl in contact.urls) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_website), null, labeledUrl.property.value)
}
// ANNIVERSARY
if (contact.anniversary != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary?.date, contact.anniversary?.partialDate))
}
// BDAY
if (contact.birthDay != null) {
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay?.date, contact.birthDay?.partialDate))
}
// RELATED
for (related in contact.relations) {
val types = related.types
val type = if (types.size > 0) types[0].value else null
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_relation), type, related.text)
}
// PHOTO
// if (contact.photo != null)
}
private fun getDisplayedDate(date: Date?, partialDate: PartialDate?): String? {
if (date != null) {
val epochDate = date.time
return getDisplayedDatetime(epochDate, epochDate, true, context)
} else if (partialDate != null){
val formatter = SimpleDateFormat("d MMMM", Locale.getDefault())
val calendar = GregorianCalendar()
calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!)
calendar.set(Calendar.MONTH, partialDate.month!! - 1)
return formatter.format(calendar.time)
}
return null
}
companion object {
fun newInstance(info: CollectionInfo, syncEntry: SyncEntry): PrettyFragment {
val frag = PrettyFragment()
val args = Bundle(1)
args.putSerializable(Constants.KEY_COLLECTION_INFO, info)
args.putSerializable(KEY_SYNC_ENTRY, syncEntry)
frag.arguments = args
return frag
}
private fun addInfoItem(context: Context, parent: ViewGroup, type: String, label: String?, value: String?): View {
val layout = parent.findViewById<View>(R.id.container) as ViewGroup
val infoItem = LayoutInflater.from(context).inflate(R.layout.contact_info_item, layout, false)
layout.addView(infoItem)
setTextViewText(infoItem, R.id.type, type)
setTextViewText(infoItem, R.id.title, label)
setTextViewText(infoItem, R.id.content, value)
parent.visibility = View.VISIBLE
return infoItem
}
private fun setTextViewText(parent: View, id: Int, text: String?) {
val tv = parent.findViewById<View>(id) as TextView
if (text == null) {
tv.visibility = View.GONE
} else {
tv.text = text
}
}
fun getDisplayedDatetime(startMillis: Long, endMillis: Long, allDay: Boolean, context: Context?): String? {
// Configure date/time formatting.
val flagsDate = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
var flagsTime = DateUtils.FORMAT_SHOW_TIME
if (DateFormat.is24HourFormat(context)) {
flagsTime = flagsTime or DateUtils.FORMAT_24HOUR
}
val datetimeString: String
if (allDay) {
// For multi-day allday events or single-day all-day events that are not
// today or tomorrow, use framework formatter.
// We need to remove 24hrs because full day events are from the start of a day until the start of the next
var adjustedEnd = endMillis - 24 * 60 * 60 * 1000;
if (adjustedEnd < startMillis) {
adjustedEnd = startMillis;
}
val f = Formatter(StringBuilder(50), Locale.getDefault())
datetimeString = DateUtils.formatDateRange(context, f, startMillis,
adjustedEnd, flagsDate).toString()
} else {
// For multiday events, shorten day/month names.
// Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm"
val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or
DateUtils.FORMAT_ABBREV_WEEKDAY
datetimeString = DateUtils.formatDateRange(context, startMillis, endMillis,
flagsDatetime)
}
return datetimeString
}
}
}
override fun onResume() {
super.onResume()
refresh()
}
companion object {
private val KEY_SYNC_ENTRY = "syncEntry"
fun newIntent(context: Context, account: Account, info: CollectionInfo, syncEntry: SyncEntry): Intent {
val intent = Intent(context, JournalItemActivity::class.java)
intent.putExtra(ViewCollectionActivity.EXTRA_ACCOUNT, account)
intent.putExtra(Constants.KEY_COLLECTION_INFO, info)
intent.putExtra(KEY_SYNC_ENTRY, syncEntry)
return intent
}
}
}