1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.calendar
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.ObjectAnimator
21 import android.app.Activity
22 import android.app.Dialog
23 import android.app.DialogFragment
24 import android.app.Service
25 import android.content.ContentProviderOperation
26 import android.content.ContentUris
27 import android.content.ContentValues
28 import android.content.Context
29 import android.content.res.Resources
30 import android.database.Cursor
31 import android.net.Uri
32 import android.os.Bundle
33 import android.provider.CalendarContract.Attendees
34 import android.provider.CalendarContract.Calendars
35 import android.provider.CalendarContract.Events
36 import android.text.Spannable
37 import android.text.SpannableStringBuilder
38 import android.text.TextUtils
39 import android.text.style.ForegroundColorSpan
40 import android.util.Log
41 import android.util.SparseIntArray
42 import android.view.Gravity
43 import android.view.LayoutInflater
44 import android.view.Menu
45 import android.view.MenuInflater
46 import android.view.MenuItem
47 import android.view.View
48 import android.view.View.OnClickListener
49 import android.view.ViewGroup
50 import android.view.Window
51 import android.view.WindowManager
52 import android.view.accessibility.AccessibilityEvent
53 import android.view.accessibility.AccessibilityManager
54 import android.widget.AdapterView
55 import android.widget.RadioButton
56 import android.widget.RadioGroup
57 import android.widget.RadioGroup.OnCheckedChangeListener
58 import android.widget.ScrollView
59 import android.widget.TextView
60 import com.android.calendar.CalendarController.EventInfo
61 import com.android.calendar.CalendarController.EventType
62 import com.android.calendarcommon2.DateException
63 import com.android.calendarcommon2.Duration
64 import java.util.ArrayList
65 
66 class EventInfoFragment : DialogFragment, OnCheckedChangeListener, CalendarController.EventHandler,
67     OnClickListener {
68     private var mWindowStyle = DIALOG_WINDOW_STYLE
69     private var mCurrentQuery = 0
70 
71     companion object {
72         const val DEBUG = false
73         const val TAG = "EventInfoFragment"
74         internal const val BUNDLE_KEY_EVENT_ID = "key_event_id"
75         internal const val BUNDLE_KEY_START_MILLIS = "key_start_millis"
76         internal const val BUNDLE_KEY_END_MILLIS = "key_end_millis"
77         internal const val BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"
78         internal const val BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible"
79         internal const val BUNDLE_KEY_WINDOW_STYLE = "key_window_style"
80         internal const val BUNDLE_KEY_CALENDAR_COLOR = "key_calendar_color"
81         internal const val BUNDLE_KEY_CALENDAR_COLOR_INIT = "key_calendar_color_init"
82         internal const val BUNDLE_KEY_CURRENT_COLOR = "key_current_color"
83         internal const val BUNDLE_KEY_CURRENT_COLOR_KEY = "key_current_color_key"
84         internal const val BUNDLE_KEY_CURRENT_COLOR_INIT = "key_current_color_init"
85         internal const val BUNDLE_KEY_ORIGINAL_COLOR = "key_original_color"
86         internal const val BUNDLE_KEY_ORIGINAL_COLOR_INIT = "key_original_color_init"
87         internal const val BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response"
88         internal const val BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE = "key_user_set_attendee_response"
89         internal const val BUNDLE_KEY_TENTATIVE_USER_RESPONSE = "key_tentative_user_response"
90         internal const val BUNDLE_KEY_RESPONSE_WHICH_EVENTS = "key_response_which_events"
91         internal const val BUNDLE_KEY_REMINDER_MINUTES = "key_reminder_minutes"
92         internal const val BUNDLE_KEY_REMINDER_METHODS = "key_reminder_methods"
93         private const val PERIOD_SPACE = ". "
94         private const val NO_EVENT_COLOR = ""
95 
96         /**
97          * These are the corresponding indices into the array of strings
98          * "R.array.change_response_labels" in the resource file.
99          */
100         const val UPDATE_SINGLE = 0
101         const val UPDATE_ALL = 1
102 
103         // Style of view
104         const val FULL_WINDOW_STYLE = 0
105         const val DIALOG_WINDOW_STYLE = 1
106 
107         // Query tokens for QueryHandler
108         private const val TOKEN_QUERY_EVENT = 1 shl 0
109         private const val TOKEN_QUERY_CALENDARS = 1 shl 1
110         private const val TOKEN_QUERY_ATTENDEES = 1 shl 2
111         private const val TOKEN_QUERY_DUPLICATE_CALENDARS = 1 shl 3
112         private const val TOKEN_QUERY_REMINDERS = 1 shl 4
113         private const val TOKEN_QUERY_VISIBLE_CALENDARS = 1 shl 5
114         private const val TOKEN_QUERY_COLORS = 1 shl 6
115         private const val TOKEN_QUERY_ALL = (TOKEN_QUERY_DUPLICATE_CALENDARS
116             or TOKEN_QUERY_ATTENDEES or TOKEN_QUERY_CALENDARS or TOKEN_QUERY_EVENT
117             or TOKEN_QUERY_REMINDERS or TOKEN_QUERY_VISIBLE_CALENDARS or TOKEN_QUERY_COLORS)
118         private val EVENT_PROJECTION = arrayOf<String>(
119             Events._ID, // 0  do not remove; used in DeleteEventHelper
120             Events.TITLE,  // 1  do not remove; used in DeleteEventHelper
121             Events.RRULE,  // 2  do not remove; used in DeleteEventHelper
122             Events.ALL_DAY, // 3  do not remove; used in DeleteEventHelper
123             Events.CALENDAR_ID, // 4  do not remove; used in DeleteEventHelper
124             Events.DTSTART, // 5  do not remove; used in DeleteEventHelper
125             Events._SYNC_ID, // 6  do not remove; used in DeleteEventHelper
126             Events.EVENT_TIMEZONE, // 7  do not remove; used in DeleteEventHelper
127             Events.DESCRIPTION, // 8
128             Events.EVENT_LOCATION, // 9
129             Calendars.CALENDAR_ACCESS_LEVEL, // 10
130             Events.CALENDAR_COLOR, // 11
131             Events.EVENT_COLOR, // 12
132             Events.HAS_ATTENDEE_DATA, // 13
133             Events.ORGANIZER,  // 14
134             Events.HAS_ALARM,  // 15
135             Calendars.MAX_REMINDERS, // 16
136             Calendars.ALLOWED_REMINDERS, // 17
137             Events.CUSTOM_APP_PACKAGE, // 18
138             Events.CUSTOM_APP_URI, // 19
139             Events.DTEND, // 20
140             Events.DURATION, // 21
141             Events.ORIGINAL_SYNC_ID // 22 do not remove; used in DeleteEventHelper
142         )
143         private const val EVENT_INDEX_ID = 0
144         private const val EVENT_INDEX_TITLE = 1
145         private const val EVENT_INDEX_RRULE = 2
146         private const val EVENT_INDEX_ALL_DAY = 3
147         private const val EVENT_INDEX_CALENDAR_ID = 4
148         private const val EVENT_INDEX_DTSTART = 5
149         private const val EVENT_INDEX_SYNC_ID = 6
150         private const val EVENT_INDEX_EVENT_TIMEZONE = 7
151         private const val EVENT_INDEX_DESCRIPTION = 8
152         private const val EVENT_INDEX_EVENT_LOCATION = 9
153         private const val EVENT_INDEX_ACCESS_LEVEL = 10
154         private const val EVENT_INDEX_CALENDAR_COLOR = 11
155         private const val EVENT_INDEX_EVENT_COLOR = 12
156         private const val EVENT_INDEX_HAS_ATTENDEE_DATA = 13
157         private const val EVENT_INDEX_ORGANIZER = 14
158         private const val EVENT_INDEX_HAS_ALARM = 15
159         private const val EVENT_INDEX_MAX_REMINDERS = 16
160         private const val EVENT_INDEX_ALLOWED_REMINDERS = 17
161         private const val EVENT_INDEX_CUSTOM_APP_PACKAGE = 18
162         private const val EVENT_INDEX_CUSTOM_APP_URI = 19
163         private const val EVENT_INDEX_DTEND = 20
164         private const val EVENT_INDEX_DURATION = 21
165         val CALENDARS_PROJECTION = arrayOf<String>(
166             Calendars._ID, // 0
167             Calendars.CALENDAR_DISPLAY_NAME, // 1
168             Calendars.OWNER_ACCOUNT, // 2
169             Calendars.CAN_ORGANIZER_RESPOND, // 3
170             Calendars.ACCOUNT_NAME, // 4
171             Calendars.ACCOUNT_TYPE // 5
172         )
173         const val CALENDARS_INDEX_DISPLAY_NAME = 1
174         const val CALENDARS_INDEX_OWNER_ACCOUNT = 2
175         const val CALENDARS_INDEX_OWNER_CAN_RESPOND = 3
176         const val CALENDARS_INDEX_ACCOUNT_NAME = 4
177         const val CALENDARS_INDEX_ACCOUNT_TYPE = 5
178         val CALENDARS_WHERE: String = Calendars._ID.toString() + "=?"
179         val CALENDARS_DUPLICATE_NAME_WHERE: String =
180             Calendars.CALENDAR_DISPLAY_NAME.toString() + "=?"
181         val CALENDARS_VISIBLE_WHERE: String = Calendars.VISIBLE.toString() + "=?"
182         const val COLORS_INDEX_COLOR = 1
183         const val COLORS_INDEX_COLOR_KEY = 2
184         private var mScale = 0f // Used for supporting different screen densities
185         private var mCustomAppIconSize = 32
186         private const val FADE_IN_TIME = 300 // in milliseconds
187         private const val LOADING_MSG_DELAY = 600 // in milliseconds
188         private const val LOADING_MSG_MIN_DISPLAY_TIME = 600
189         private var mDialogWidth = 500
190         private var mDialogHeight = 600
191         private var DIALOG_TOP_MARGIN = 8
getResponseFromButtonIdnull192         fun getResponseFromButtonId(buttonId: Int): Int {
193             return Attendees.ATTENDEE_STATUS_NONE
194         }
195 
findButtonIdForResponsenull196         fun findButtonIdForResponse(response: Int): Int {
197             return -1
198         }
199 
200         init {
201             if (!Utils.isJellybeanOrLater()) {
202                 EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID // nonessential value
203                 EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID // nonessential value
204             }
205         }
206     }
207 
208     private var mView: View? = null
209     private var mUri: Uri? = null
210     var eventId: Long = 0
211         private set
212     private val mEventCursor: Cursor? = null
213     private val mCalendarsCursor: Cursor? = null
214     var startMillis: Long = 0
215         private set
216     var endMillis: Long = 0
217         private set
218     private var mAllDay = false
219     private var mOwnerCanRespond = false
220     private var mSyncAccountName: String? = null
221     private var mCalendarOwnerAccount: String? = null
222     private var mIsBusyFreeCalendar = false
223     private val mOriginalAttendeeResponse = 0
224     private var mAttendeeResponseFromIntent: Int = Attendees.ATTENDEE_STATUS_NONE
225     private val mUserSetResponse: Int = Attendees.ATTENDEE_STATUS_NONE
226     private val mWhichEvents = -1
227 
228     // Used as the temporary response until the dialog is confirmed. It is also
229     // able to be used as a state marker for configuration changes.
230     private val mTentativeUserSetResponse: Int = Attendees.ATTENDEE_STATUS_NONE
231     private var mHasAlarm = false
232 
233     // Used to prevent saving changes in event if it is being deleted.
234     private val mEventDeletionStarted = false
235     private var mTitle: TextView? = null
236     private var mWhenDateTime: TextView? = null
237     private var mWhere: TextView? = null
238     private var mMenu: Menu? = null
239     private var mHeadlines: View? = null
240     private var mScrollView: ScrollView? = null
241     private var mLoadingMsgView: View? = null
242     private var mErrorMsgView: View? = null
243     private var mAnimateAlpha: ObjectAnimator? = null
244     private var mLoadingMsgStartTime: Long = 0
245     private val mDisplayColorKeyMap: SparseIntArray = SparseIntArray()
246     private val mOriginalColor = -1
247     private val mOriginalColorInitialized = false
248     private val mCalendarColor = -1
249     private val mCalendarColorInitialized = false
250     private val mCurrentColor = -1
251     private val mCurrentColorInitialized = false
252     private val mCurrentColorKey = -1
253     private var mNoCrossFade = false // Used to prevent repeated cross-fade
254     private var mResponseRadioGroup: RadioGroup? = null
255     var mToEmails: ArrayList<String> = ArrayList<String>()
256     var mCcEmails: ArrayList<String> = ArrayList<String>()
257     private val mTZUpdater: Runnable = object : Runnable {
258         @Override
runnull259         override fun run() {
260             updateEvent(mView)
261         }
262     }
263     private val mLoadingMsgAlphaUpdater: Runnable = object : Runnable {
264         @Override
runnull265         override fun run() {
266             // Since this is run after a delay, make sure to only show the message
267             // if the event's data is not shown yet.
268             if (!mAnimateAlpha!!.isRunning() && mScrollView!!.getAlpha() == 0f) {
269                 mLoadingMsgStartTime = System.currentTimeMillis()
270                 mLoadingMsgView?.setAlpha(1f)
271             }
272         }
273     }
274     private var mIsDialog = false
275     private var mIsPaused = true
276     private val mDismissOnResume = false
277     private var mX = -1
278     private var mY = -1
279     private var mMinTop = 0 // Dialog cannot be above this location
280     private var mIsTabletConfig = false
281     private var mActivity: Activity? = null
282     private var mContext: Context? = null
283     private var mController: CalendarController? = null
sendAccessibilityEventIfQueryDonenull284     private fun sendAccessibilityEventIfQueryDone(token: Int) {
285         mCurrentQuery = mCurrentQuery or token
286         if (mCurrentQuery == TOKEN_QUERY_ALL) {
287             sendAccessibilityEvent()
288         }
289     }
290 
291     constructor(
292         context: Context,
293         uri: Uri?,
294         startMillis: Long,
295         endMillis: Long,
296         attendeeResponse: Int,
297         isDialog: Boolean,
298         windowStyle: Int
299     ) {
300         val r: Resources = context.getResources()
301         if (mScale == 0f) {
302             mScale = context.getResources().getDisplayMetrics().density
303             if (mScale != 1f) {
304                 mCustomAppIconSize *= mScale.toInt()
305                 if (isDialog) {
306                     DIALOG_TOP_MARGIN *= mScale.toInt()
307                 }
308             }
309         }
310         if (isDialog) {
311             setDialogSize(r)
312         }
313         mIsDialog = isDialog
314         setStyle(DialogFragment.STYLE_NO_TITLE, 0)
315         mUri = uri
316         this.startMillis = startMillis
317         this.endMillis = endMillis
318         mAttendeeResponseFromIntent = attendeeResponse
319         mWindowStyle = windowStyle
320     }
321 
322     // This is currently required by the fragment manager.
323     constructor() {}
324     constructor(
325         context: Context?,
326         eventId: Long,
327         startMillis: Long,
328         endMillis: Long,
329         attendeeResponse: Int,
330         isDialog: Boolean,
331         windowStyle: Int
332     ) : this(
333         context as Context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
334         endMillis, attendeeResponse, isDialog, windowStyle
335     ) {
336         this.eventId = eventId
337     }
338 
339     @Override
onActivityCreatednull340     override fun onActivityCreated(savedInstanceState: Bundle?) {
341         super.onActivityCreated(savedInstanceState)
342         if (mIsDialog) {
343             applyDialogParams()
344         }
345         val activity: Activity = getActivity()
346         mContext = activity
347     }
348 
applyDialogParamsnull349     private fun applyDialogParams() {
350         val dialog: Dialog = getDialog()
351         dialog.setCanceledOnTouchOutside(true)
352         val window: Window? = dialog.getWindow()
353         window?.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
354         val a: WindowManager.LayoutParams? = window?.getAttributes()
355         a!!.dimAmount = .4f
356         a.width = mDialogWidth
357         a.height = mDialogHeight
358 
359         // On tablets , do smart positioning of dialog
360         // On phones , use the whole screen
361         if (mX != -1 || mY != -1) {
362             a.x = mX - mDialogWidth / 2
363             a.y = mY - mDialogHeight / 2
364             if (a.y < mMinTop) {
365                 a.y = mMinTop + DIALOG_TOP_MARGIN
366             }
367             a.gravity = Gravity.LEFT or Gravity.TOP
368         }
369         window.setAttributes(a)
370     }
371 
setDialogParamsnull372     fun setDialogParams(x: Int, y: Int, minTop: Int) {
373         mX = x
374         mY = y
375         mMinTop = minTop
376     }
377 
378     // Implements OnCheckedChangeListener
379     @Override
onCheckedChangednull380     override fun onCheckedChanged(group: RadioGroup?, checkedId: Int) {
381     }
382 
onNothingSelectednull383     fun onNothingSelected(parent: AdapterView<*>?) {}
384     @Override
onDetachnull385     override fun onDetach() {
386         super.onDetach()
387         mController?.deregisterEventHandler(R.layout.event_info)
388     }
389 
390     @Override
onAttachnull391     override fun onAttach(activity: Activity?) {
392         super.onAttach(activity)
393         mActivity = activity
394         // Ensure that mIsTabletConfig is set before creating the menu.
395         mIsTabletConfig = Utils.getConfigBool(mActivity as Context, R.bool.tablet_config)
396         mController = CalendarController.getInstance(mActivity)
397         mController?.registerEventHandler(R.layout.event_info, this)
398         if (!mIsDialog) {
399             setHasOptionsMenu(true)
400         }
401     }
402 
403     @Override
onCreateViewnull404     override fun onCreateView(
405         inflater: LayoutInflater,
406         container: ViewGroup?,
407         savedInstanceState: Bundle?
408     ): View? {
409         mView = if (mWindowStyle == DIALOG_WINDOW_STYLE) {
410             inflater.inflate(R.layout.event_info_dialog, container, false)
411         } else {
412             inflater.inflate(R.layout.event_info, container, false)
413         }
414         mScrollView = mView?.findViewById(R.id.event_info_scroll_view) as ScrollView
415         mLoadingMsgView = mView?.findViewById(R.id.event_info_loading_msg)
416         mErrorMsgView = mView?.findViewById(R.id.event_info_error_msg)
417         mTitle = mView?.findViewById(R.id.title) as TextView
418         mWhenDateTime = mView?.findViewById(R.id.when_datetime) as TextView
419         mWhere = mView?.findViewById(R.id.where) as TextView
420         mHeadlines = mView?.findViewById(R.id.event_info_headline)
421         mResponseRadioGroup = mView?.findViewById(R.id.response_value) as RadioGroup
422         mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0f, 1f)
423         mAnimateAlpha?.setDuration(FADE_IN_TIME.toLong())
424         mAnimateAlpha?.addListener(object : AnimatorListenerAdapter() {
425             var defLayerType = 0
426             @Override
427             override fun onAnimationStart(animation: Animator) {
428                 // Use hardware layer for better performance during animation
429                 defLayerType = mScrollView?.getLayerType() as Int
430                 mScrollView?.setLayerType(View.LAYER_TYPE_HARDWARE, null)
431                 // Ensure that the loading message is gone before showing the
432                 // event info
433                 mLoadingMsgView?.removeCallbacks(mLoadingMsgAlphaUpdater)
434                 mLoadingMsgView?.setVisibility(View.GONE)
435             }
436 
437             @Override
438             override fun onAnimationCancel(animation: Animator) {
439                 mScrollView?.setLayerType(defLayerType, null)
440             }
441 
442             @Override
443             override fun onAnimationEnd(animation: Animator) {
444                 mScrollView?.setLayerType(defLayerType, null)
445                 // Do not cross fade after the first time
446                 mNoCrossFade = true
447             }
448         })
449         mLoadingMsgView?.setAlpha(0f)
450         mScrollView?.setAlpha(0f)
451         mErrorMsgView?.setVisibility(View.INVISIBLE)
452         mLoadingMsgView?.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY.toLong())
453 
454         // Hide Edit/Delete buttons if in full screen mode on a phone
455         if (!mIsDialog && !mIsTabletConfig || mWindowStyle == FULL_WINDOW_STYLE) {
456             mView?.findViewById<View>(R.id.event_info_buttons_container)?.setVisibility(View.GONE)
457         }
458         return mView
459     }
460 
updateTitlenull461     private fun updateTitle() {
462         val res: Resources = getActivity().getResources()
463         getActivity().setTitle(res.getString(R.string.event_info_title))
464     }
465 
466     /**
467      * Initializes the event cursor, which is expected to point to the first
468      * (and only) result from a query.
469      * @return false if the cursor is empty, true otherwise
470      */
initEventCursornull471     private fun initEventCursor(): Boolean {
472         if (mEventCursor == null || mEventCursor.getCount() === 0) {
473             return false
474         }
475         mEventCursor.moveToFirst()
476         eventId = mEventCursor.getInt(EVENT_INDEX_ID).toLong()
477         val rRule: String = mEventCursor.getString(EVENT_INDEX_RRULE)
478         // mHasAlarm will be true if it was saved in the event already.
479         mHasAlarm = if (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) === 1) true else false
480         return true
481     }
482 
483     @Override
onSaveInstanceStatenull484     override fun onSaveInstanceState(outState: Bundle?) {
485         super.onSaveInstanceState(outState)
486     }
487 
488     @Override
onCreateOptionsMenunull489     override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) {
490         super.onCreateOptionsMenu(menu, inflater)
491         // Show color/edit/delete buttons only in non-dialog configuration
492         if (!mIsDialog && !mIsTabletConfig || mWindowStyle == FULL_WINDOW_STYLE) {
493             inflater.inflate(R.menu.event_info_title_bar, menu)
494             mMenu = menu
495         }
496     }
497 
498     @Override
onOptionsItemSelectednull499     override fun onOptionsItemSelected(item: MenuItem): Boolean {
500 
501         // If we're a dialog we don't want to handle menu buttons
502         if (mIsDialog) {
503             return false
504         }
505         // Handles option menu selections:
506         // Home button - close event info activity and start the main calendar
507         // one
508         // Edit button - start the event edit activity and close the info
509         // activity
510         // Delete button - start a delete query that calls a runnable that close
511         // the info activity
512         val itemId: Int = item.getItemId()
513         if (itemId == android.R.id.home) {
514             Utils.returnToCalendarHome(mContext as Context)
515             mActivity?.finish()
516             return true
517         } else if (itemId == R.id.info_action_edit) {
518             mActivity?.finish()
519         }
520         return super.onOptionsItemSelected(item)
521     }
522 
523     @Override
onStopnull524     override fun onStop() {
525         super.onStop()
526     }
527 
528     @Override
onDestroynull529     override fun onDestroy() {
530         if (mEventCursor != null) {
531             mEventCursor.close()
532         }
533         if (mCalendarsCursor != null) {
534             mCalendarsCursor.close()
535         }
536         super.onDestroy()
537     }
538 
539     /**
540      * Creates an exception to a recurring event.  The only change we're making is to the
541      * "self attendee status" value.  The provider will take care of updating the corresponding
542      * Attendees.attendeeStatus entry.
543      *
544      * @param eventId The recurring event.
545      * @param status The new value for selfAttendeeStatus.
546      */
createExceptionResponsenull547     private fun createExceptionResponse(eventId: Long, status: Int) {
548         val values = ContentValues()
549         values.put(Events.ORIGINAL_INSTANCE_TIME, startMillis)
550         values.put(Events.SELF_ATTENDEE_STATUS, status)
551         values.put(Events.STATUS, Events.STATUS_CONFIRMED)
552         val ops: ArrayList<ContentProviderOperation> = ArrayList<ContentProviderOperation>()
553         val exceptionUri: Uri = Uri.withAppendedPath(
554             Events.CONTENT_EXCEPTION_URI,
555             eventId.toString()
556         )
557         ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build())
558     }
559 
displayEventNotFoundnull560     private fun displayEventNotFound() {
561         mErrorMsgView?.setVisibility(View.VISIBLE)
562         mScrollView?.setVisibility(View.GONE)
563         mLoadingMsgView?.setVisibility(View.GONE)
564     }
565 
updateEventnull566     private fun updateEvent(view: View?) {
567         if (mEventCursor == null || view == null) {
568             return
569         }
570         val context: Context = view.getContext() ?: return
571         var eventName: String = mEventCursor.getString(EVENT_INDEX_TITLE)
572         if (eventName == null || eventName.length == 0) {
573             eventName = getActivity().getString(R.string.no_title_label)
574         }
575 
576         // 3rd parties might not have specified the start/end time when firing the
577         // Events.CONTENT_URI intent.  Update these with values read from the db.
578         if (startMillis == 0L && endMillis == 0L) {
579             startMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART)
580             endMillis = mEventCursor.getLong(EVENT_INDEX_DTEND)
581             if (endMillis == 0L) {
582                 val duration: String = mEventCursor.getString(EVENT_INDEX_DURATION)
583                 if (!TextUtils.isEmpty(duration)) {
584                     try {
585                         val d = Duration()
586                         d.parse(duration)
587                         val endMillis: Long = startMillis + d.getMillis()
588                         if (endMillis >= startMillis) {
589                             this.endMillis = endMillis
590                         } else {
591                             Log.d(TAG, "Invalid duration string: $duration")
592                         }
593                     } catch (e: DateException) {
594                         Log.d(TAG, "Error parsing duration string $duration", e)
595                     }
596                 }
597                 if (endMillis == 0L) {
598                     endMillis = startMillis
599                 }
600             }
601         }
602         mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) !== 0
603         val location: String = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION)
604         val description: String = mEventCursor.getString(EVENT_INDEX_DESCRIPTION)
605         val rRule: String = mEventCursor.getString(EVENT_INDEX_RRULE)
606         val eventTimezone: String = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE)
607         mHeadlines?.setBackgroundColor(mCurrentColor)
608 
609         // What
610         if (eventName != null) {
611             setTextCommon(view, R.id.title, eventName)
612         }
613 
614         // When
615         // Set the date and repeats (if any)
616         val localTimezone: String? = Utils.getTimeZone(mActivity, mTZUpdater)
617         val resources: Resources = context.getResources()
618         var displayedDatetime: String? = Utils.getDisplayedDatetime(
619             startMillis, endMillis,
620             System.currentTimeMillis(), localTimezone as String, mAllDay, context
621         )
622         var displayedTimezone: String? = null
623         if (!mAllDay) {
624             displayedTimezone = Utils.getDisplayedTimezone(
625                 startMillis, localTimezone,
626                 eventTimezone
627             )
628         }
629         // Display the datetime.  Make the timezone (if any) transparent.
630         if (displayedTimezone == null) {
631             setTextCommon(view, R.id.when_datetime, displayedDatetime as CharSequence)
632         } else {
633             val timezoneIndex: Int = displayedDatetime!!.length
634             displayedDatetime += "  $displayedTimezone"
635             val sb = SpannableStringBuilder(displayedDatetime)
636             val transparentColorSpan = ForegroundColorSpan(
637                 resources.getColor(R.color.event_info_headline_transparent_color)
638             )
639             sb.setSpan(
640                 transparentColorSpan, timezoneIndex, displayedDatetime.length,
641                 Spannable.SPAN_INCLUSIVE_INCLUSIVE
642             )
643             setTextCommon(view, R.id.when_datetime, sb)
644         }
645         view.findViewById<View>(R.id.when_repeat).setVisibility(View.GONE)
646 
647         // Organizer view is setup in the updateCalendar method
648 
649         // Where
650         if (location == null || location.trim().length == 0) {
651             setVisibilityCommon(view, R.id.where, View.GONE)
652         } else {
653             val textView: TextView? = mWhere
654             if (textView != null) {
655                 textView.setText(location.trim())
656             }
657         }
658 
659         // Launch Custom App
660         if (Utils.isJellybeanOrLater()) {
661             updateCustomAppButton()
662         }
663     }
664 
updateCustomAppButtonnull665     private fun updateCustomAppButton() {
666         setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE)
667         return
668     }
669 
sendAccessibilityEventnull670     private fun sendAccessibilityEvent() {
671         val am: AccessibilityManager =
672             getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE) as AccessibilityManager
673         if (!am.isEnabled()) {
674             return
675         }
676         val event: AccessibilityEvent =
677             AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED)
678         event.setClassName(EventInfoFragment::class.java.getName())
679         event.setPackageName(getActivity().getPackageName())
680         var text = event.getText()
681         if (mResponseRadioGroup?.getVisibility() == View.VISIBLE) {
682             val id: Int = mResponseRadioGroup!!.getCheckedRadioButtonId()
683             if (id != View.NO_ID) {
684                 text.add((getView()?.findViewById(R.id.response_label) as TextView).getText())
685                 text.add(
686                     (mResponseRadioGroup?.findViewById(id) as RadioButton)
687                         .getText().toString() + PERIOD_SPACE
688                 )
689             }
690         }
691         am.sendAccessibilityEvent(event)
692     }
693 
updateCalendarnull694     private fun updateCalendar(view: View?) {
695         mCalendarOwnerAccount = ""
696         if (mCalendarsCursor != null && mEventCursor != null) {
697             mCalendarsCursor.moveToFirst()
698             val tempAccount: String = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT)
699             mCalendarOwnerAccount = tempAccount ?: ""
700             mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) !== 0
701             mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME)
702             setVisibilityCommon(view, R.id.organizer_container, View.GONE)
703             mIsBusyFreeCalendar =
704                 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) === Calendars.CAL_ACCESS_FREEBUSY
705             if (!mIsBusyFreeCalendar) {
706                 val b: View? = mView?.findViewById(R.id.edit)
707                 b?.setEnabled(true)
708                 b?.setOnClickListener(object : OnClickListener {
709                     @Override
710                     override fun onClick(v: View?) {
711                         // For dialogs, just close the fragment
712                         // For full screen, close activity on phone, leave it for tablet
713                         if (mIsDialog) {
714                             this@EventInfoFragment.dismiss()
715                         } else if (!mIsTabletConfig) {
716                             getActivity().finish()
717                         }
718                     }
719                 })
720             }
721             var button: View
722             if ((!mIsDialog && !mIsTabletConfig ||
723                 mWindowStyle == FULL_WINDOW_STYLE) && mMenu != null
724             ) {
725                 mActivity?.invalidateOptionsMenu()
726             }
727         } else {
728             setVisibilityCommon(view, R.id.calendar, View.GONE)
729             sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS)
730         }
731     }
732 
setTextCommonnull733     private fun setTextCommon(view: View, id: Int, text: CharSequence) {
734         val textView: TextView = view.findViewById(id) as TextView ?: return
735         textView.setText(text)
736     }
737 
setVisibilityCommonnull738     private fun setVisibilityCommon(view: View?, id: Int, visibility: Int) {
739         val v: View? = view?.findViewById(id)
740         if (v != null) {
741             v.setVisibility(visibility)
742         }
743         return
744     }
745 
746     @Override
onPausenull747     override fun onPause() {
748         mIsPaused = true
749         super.onPause()
750     }
751 
752     @Override
onResumenull753     override fun onResume() {
754         super.onResume()
755         if (mIsDialog) {
756             setDialogSize(getActivity().getResources())
757             applyDialogParams()
758         }
759         mIsPaused = false
760         if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
761             val buttonId = findButtonIdForResponse(mTentativeUserSetResponse)
762             mResponseRadioGroup?.check(buttonId)
763         }
764     }
765 
766     @Override
eventsChangednull767     override fun eventsChanged() {
768     }
769 
770     @get:Override override val supportedEventTypes: Long
771         get() = EventType.EVENTS_CHANGED
772 
773     @Override
handleEventnull774     override fun handleEvent(event: EventInfo?) {
775         reloadEvents()
776     }
777 
reloadEventsnull778     fun reloadEvents() {}
779     @Override
onClicknull780     override fun onClick(view: View?) {
781     }
782 
setDialogSizenull783     private fun setDialogSize(r: Resources) {
784         mDialogWidth = r.getDimension(R.dimen.event_info_dialog_width).toInt()
785         mDialogHeight = r.getDimension(R.dimen.event_info_dialog_height).toInt()
786     }
787 }