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 }