From cea7f8fdc668bfbf39c35acba29fc4a0ffefc1fb Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 27 Aug 2020 17:20:51 +0300 Subject: [PATCH] Implement showing changelog item. --- .../ui/etebase/CollectionItemFragment.kt | 436 ++++++++++++++++++ .../ui/etebase/ListEntriesFragment.kt | 7 +- .../res/menu/collection_item_fragment.xml | 26 ++ 3 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt create mode 100644 app/src/main/res/menu/collection_item_fragment.xml diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt new file mode 100644 index 00000000..a42f4f14 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/CollectionItemFragment.kt @@ -0,0 +1,436 @@ +package com.etesync.syncadapter.ui.etebase + +import android.content.Context +import android.os.Bundle +import android.text.format.DateFormat +import android.text.format.DateUtils +import android.view.* +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.activityViewModels +import androidx.viewpager.widget.ViewPager +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.ical4android.Task +import at.bitfire.vcard4android.Contact +import com.etesync.syncadapter.CachedCollection +import com.etesync.syncadapter.CachedItem +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.ui.BaseActivity +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 CollectionItemFragment(private val cachedItem: CachedItem) : Fragment() { + private val collectionModel: CollectionViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val ret = inflater.inflate(R.layout.journal_item_activity, container, false) + setHasOptionsMenu(true) + + if (savedInstanceState == null) { + collectionModel.observe(this) { + (activity as? BaseActivity?)?.supportActionBar?.title = it.meta.name + if (container != null) { + initUi(inflater, ret, it) + } + } + } + + return ret + } + + private fun initUi(inflater: LayoutInflater, v: View, cachedCollection: CachedCollection) { + val viewPager = v.findViewById(R.id.viewpager) + viewPager.adapter = TabsAdapter(childFragmentManager, requireContext(), cachedCollection, cachedItem) + + val tabLayout = v.findViewById(R.id.tabs) + tabLayout.setupWithViewPager(viewPager) + + ListEntriesFragment.setItemView(v.findViewById(R.id.journal_list_item), cachedCollection.meta.collectionType, cachedItem) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.collection_item_fragment, menu) + // menu.setGroupVisible(R.id.journal_item_menu_event_invite, emailInvitationEvent != null) + } +} + +private class TabsAdapter(fm: FragmentManager, private val context: Context, private val cachedCollection: CachedCollection, private val cachedItem: CachedItem) : 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(cachedCollection, cachedItem.content) + } else { + TextFragment(cachedItem.content) + } + } +} + + +class TextFragment(private val content: String) : 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(R.id.content) as TextView + + tv.text = content + + return v + } +} + +class PrettyFragment(private val cachedCollection: CachedCollection, private val content: String) : Fragment() { + private var asyncTask: Future? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + var v: View? = null + + when (cachedCollection.meta.collectionType) { + Constants.ETEBASE_TYPE_ADDRESS_BOOK -> { + v = inflater.inflate(R.layout.contact_info, container, false) + asyncTask = loadContactTask(v) + } + Constants.ETEBASE_TYPE_CALENDAR -> { + v = inflater.inflate(R.layout.event_info, container, false) + asyncTask = loadEventTask(v) + } + Constants.ETEBASE_TYPE_TASKS -> { + v = inflater.inflate(R.layout.task_info, container, false) + asyncTask = loadTaskTask(v) + } + } + + return v + } + + override fun onDestroyView() { + super.onDestroyView() + if (asyncTask != null) + asyncTask!!.cancel(true) + } + + private fun loadEventTask(view: View): Future { + return doAsync { + var event: Event? = null + val inputReader = StringReader(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(R.id.event_info_loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.event_info_scroll_view) + contentContainer.visibility = View.VISIBLE + + setTextViewText(view, R.id.title, event.summary) + + setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart?.date?.time!!, event.dtEnd?.date!!.time, event.isAllDay(), context)) + + setTextViewText(view, R.id.where, event.location) + + val organizer = event.organizer + if (organizer != null) { + val tv = view.findViewById(R.id.organizer) as TextView + tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "") + } else { + val organizerView = view.findViewById(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()) + + /* FIXME: + if (event.attendees.isNotEmpty() && activity != null) { + (activity as JournalItemActivity).allowSendEmail(event, syncEntry.content) + } + */ + } + } + } + } + + private fun loadTaskTask(view: View): Future { + return doAsync { + var task: Task? = null + val inputReader = StringReader(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(R.id.task_info_loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(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(R.id.organizer) as TextView + tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "") + } else { + val organizerView = view.findViewById(R.id.organizer_container) + organizerView.visibility = View.GONE + } + + setTextViewText(view, R.id.description, task.description) + } + } + } + } + + private fun loadContactTask(view: View): Future { + return doAsync { + var contact: Contact? = null + val reader = StringReader(content) + + try { + contact = Contact.fromReader(reader, null)[0] + } catch (e: IOException) { + e.printStackTrace() + } + + if (contact != null) { + uiThread { + val loader = view.findViewById(R.id.loading_msg) + loader.visibility = View.GONE + val contentContainer = view.findViewById(R.id.content_container) + contentContainer.visibility = View.VISIBLE + + val tv = view.findViewById(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 = requireView() + + val mainCard = view.findViewById(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 = requireView() + val mainCard = view.findViewById(R.id.main_card) as ViewGroup + val aboutCard = view.findViewById(R.id.about_card) as ViewGroup + aboutCard.findViewById(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 { + private fun addInfoItem(context: Context, parent: ViewGroup, type: String, label: String?, value: String?): View { + val layout = parent.findViewById(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(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 + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt index 123ccabc..e6dde3ff 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/etebase/ListEntriesFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.* import androidx.fragment.app.ListFragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.commit import com.etesync.syncadapter.CachedCollection import com.etesync.syncadapter.CachedItem import com.etesync.syncadapter.Constants @@ -60,8 +61,10 @@ class ListEntriesFragment : ListFragment(), AdapterView.OnItemClickListener { override fun onItemClick(parent: AdapterView<*>, view: View, position: Int, id: Long) { val item = listAdapter?.getItem(position) as CachedItem - Toast.makeText(context, "Clicked ${item.item.uid}", Toast.LENGTH_LONG).show() - // startActivity(JournalItemActivity.newIntent(requireContext(), account, info, entry.content)) + activity?.supportFragmentManager?.commit { + replace(R.id.fragment_container, CollectionItemFragment(item)) + addToBackStack(EditCollectionFragment::class.java.name) + } } internal inner class EntriesListAdapter(context: Context, val cachedCollection: CachedCollection) : ArrayAdapter(context, R.layout.journal_viewer_list_item) { diff --git a/app/src/main/res/menu/collection_item_fragment.xml b/app/src/main/res/menu/collection_item_fragment.xml new file mode 100644 index 00000000..2abd4b51 --- /dev/null +++ b/app/src/main/res/menu/collection_item_fragment.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + \ No newline at end of file