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