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
17package com.android.calendar;
18
19import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY;
20import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME;
21import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME;
22import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH;
23
24import android.animation.Animator;
25import android.animation.AnimatorListenerAdapter;
26import android.animation.ObjectAnimator;
27import android.app.Activity;
28import android.app.Dialog;
29import android.app.DialogFragment;
30import android.app.Service;
31import android.content.ActivityNotFoundException;
32import android.content.ContentProviderOperation;
33import android.content.ContentResolver;
34import android.content.ContentUris;
35import android.content.ContentValues;
36import android.content.Context;
37import android.content.DialogInterface;
38import android.content.Intent;
39import android.content.SharedPreferences;
40import android.content.pm.ApplicationInfo;
41import android.content.pm.PackageManager;
42import android.content.pm.PackageManager.NameNotFoundException;
43import android.content.res.Resources;
44import android.database.Cursor;
45import android.graphics.Rect;
46import android.graphics.drawable.Drawable;
47import android.net.Uri;
48import android.os.Bundle;
49import android.provider.CalendarContract;
50import android.provider.CalendarContract.Attendees;
51import android.provider.CalendarContract.Calendars;
52import android.provider.CalendarContract.Events;
53import android.provider.CalendarContract.Reminders;
54import android.provider.ContactsContract;
55import android.provider.ContactsContract.CommonDataKinds;
56import android.provider.ContactsContract.Intents;
57import android.provider.ContactsContract.QuickContact;
58import android.text.Spannable;
59import android.text.SpannableString;
60import android.text.SpannableStringBuilder;
61import android.text.Spanned;
62import android.text.TextUtils;
63import android.text.format.Time;
64import android.text.method.LinkMovementMethod;
65import android.text.method.MovementMethod;
66import android.text.style.ForegroundColorSpan;
67import android.text.style.URLSpan;
68import android.text.util.Linkify;
69import android.text.util.Rfc822Token;
70import android.util.Log;
71import android.view.Gravity;
72import android.view.LayoutInflater;
73import android.view.Menu;
74import android.view.MenuInflater;
75import android.view.MenuItem;
76import android.view.MotionEvent;
77import android.view.View;
78import android.view.View.OnClickListener;
79import android.view.View.OnTouchListener;
80import android.view.ViewGroup;
81import android.view.Window;
82import android.view.WindowManager;
83import android.view.accessibility.AccessibilityEvent;
84import android.view.accessibility.AccessibilityManager;
85import android.widget.AdapterView;
86import android.widget.AdapterView.OnItemSelectedListener;
87import android.widget.Button;
88import android.widget.LinearLayout;
89import android.widget.RadioButton;
90import android.widget.RadioGroup;
91import android.widget.RadioGroup.OnCheckedChangeListener;
92import android.widget.ScrollView;
93import android.widget.TextView;
94import android.widget.Toast;
95
96import com.android.calendar.CalendarController.EventInfo;
97import com.android.calendar.CalendarController.EventType;
98import com.android.calendar.CalendarEventModel.Attendee;
99import com.android.calendar.CalendarEventModel.ReminderEntry;
100import com.android.calendar.alerts.QuickResponseActivity;
101import com.android.calendar.event.AttendeesView;
102import com.android.calendar.event.EditEventActivity;
103import com.android.calendar.event.EditEventHelper;
104import com.android.calendar.event.EventViewUtils;
105import com.android.calendarcommon2.EventRecurrence;
106
107import java.util.ArrayList;
108import java.util.Arrays;
109import java.util.Collections;
110import java.util.List;
111import java.util.regex.Pattern;
112
113public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener,
114        CalendarController.EventHandler, OnClickListener, DeleteEventHelper.DeleteNotifyListener {
115
116    public static final boolean DEBUG = false;
117
118    public static final String TAG = "EventInfoFragment";
119
120    protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id";
121    protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis";
122    protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis";
123    protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog";
124    protected static final String BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible";
125    protected static final String BUNDLE_KEY_WINDOW_STYLE = "key_window_style";
126    protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response";
127
128    private static final String PERIOD_SPACE = ". ";
129
130    /**
131     * These are the corresponding indices into the array of strings
132     * "R.array.change_response_labels" in the resource file.
133     */
134    static final int UPDATE_SINGLE = 0;
135    static final int UPDATE_ALL = 1;
136
137    // Style of view
138    public static final int FULL_WINDOW_STYLE = 0;
139    public static final int DIALOG_WINDOW_STYLE = 1;
140
141    private int mWindowStyle = DIALOG_WINDOW_STYLE;
142
143    // Query tokens for QueryHandler
144    private static final int TOKEN_QUERY_EVENT = 1 << 0;
145    private static final int TOKEN_QUERY_CALENDARS = 1 << 1;
146    private static final int TOKEN_QUERY_ATTENDEES = 1 << 2;
147    private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3;
148    private static final int TOKEN_QUERY_REMINDERS = 1 << 4;
149    private static final int TOKEN_QUERY_VISIBLE_CALENDARS = 1 << 5;
150    private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS
151            | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT
152            | TOKEN_QUERY_REMINDERS | TOKEN_QUERY_VISIBLE_CALENDARS;
153
154    private int mCurrentQuery = 0;
155
156    private static final String[] EVENT_PROJECTION = new String[] {
157        Events._ID,                  // 0  do not remove; used in DeleteEventHelper
158        Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
159        Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
160        Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
161        Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
162        Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
163        Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
164        Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
165        Events.DESCRIPTION,          // 8
166        Events.EVENT_LOCATION,       // 9
167        Calendars.CALENDAR_ACCESS_LEVEL, // 10
168        Events.DISPLAY_COLOR,        // 11 If SDK < 16, set to Calendars.CALENDAR_COLOR.
169        Events.HAS_ATTENDEE_DATA,    // 12
170        Events.ORGANIZER,            // 13
171        Events.HAS_ALARM,            // 14
172        Calendars.MAX_REMINDERS,     //15
173        Calendars.ALLOWED_REMINDERS, // 16
174        Events.CUSTOM_APP_PACKAGE,   // 17
175        Events.CUSTOM_APP_URI,       // 18
176        Events.ORIGINAL_SYNC_ID,     // 19 do not remove; used in DeleteEventHelper
177    };
178    private static final int EVENT_INDEX_ID = 0;
179    private static final int EVENT_INDEX_TITLE = 1;
180    private static final int EVENT_INDEX_RRULE = 2;
181    private static final int EVENT_INDEX_ALL_DAY = 3;
182    private static final int EVENT_INDEX_CALENDAR_ID = 4;
183    private static final int EVENT_INDEX_SYNC_ID = 6;
184    private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
185    private static final int EVENT_INDEX_DESCRIPTION = 8;
186    private static final int EVENT_INDEX_EVENT_LOCATION = 9;
187    private static final int EVENT_INDEX_ACCESS_LEVEL = 10;
188    private static final int EVENT_INDEX_COLOR = 11;
189    private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12;
190    private static final int EVENT_INDEX_ORGANIZER = 13;
191    private static final int EVENT_INDEX_HAS_ALARM = 14;
192    private static final int EVENT_INDEX_MAX_REMINDERS = 15;
193    private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16;
194    private static final int EVENT_INDEX_CUSTOM_APP_PACKAGE = 17;
195    private static final int EVENT_INDEX_CUSTOM_APP_URI = 18;
196
197    private static final String[] ATTENDEES_PROJECTION = new String[] {
198        Attendees._ID,                      // 0
199        Attendees.ATTENDEE_NAME,            // 1
200        Attendees.ATTENDEE_EMAIL,           // 2
201        Attendees.ATTENDEE_RELATIONSHIP,    // 3
202        Attendees.ATTENDEE_STATUS,          // 4
203        Attendees.ATTENDEE_IDENTITY,        // 5
204        Attendees.ATTENDEE_ID_NAMESPACE     // 6
205    };
206    private static final int ATTENDEES_INDEX_ID = 0;
207    private static final int ATTENDEES_INDEX_NAME = 1;
208    private static final int ATTENDEES_INDEX_EMAIL = 2;
209    private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
210    private static final int ATTENDEES_INDEX_STATUS = 4;
211    private static final int ATTENDEES_INDEX_IDENTITY = 5;
212    private static final int ATTENDEES_INDEX_ID_NAMESPACE = 6;
213
214    static {
215        if (!Utils.isJellybeanOrLater()) {
216            EVENT_PROJECTION[EVENT_INDEX_COLOR] = Calendars.CALENDAR_COLOR;
217            EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID; // dummy value
218            EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID; // dummy value
219
220            ATTENDEES_PROJECTION[ATTENDEES_INDEX_IDENTITY] = Attendees._ID; // dummy value
221            ATTENDEES_PROJECTION[ATTENDEES_INDEX_ID_NAMESPACE] = Attendees._ID; // dummy value
222        }
223    }
224
225    private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?";
226
227    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
228            + Attendees.ATTENDEE_EMAIL + " ASC";
229
230    private static final String[] REMINDERS_PROJECTION = new String[] {
231        Reminders._ID,                      // 0
232        Reminders.MINUTES,            // 1
233        Reminders.METHOD           // 2
234    };
235    private static final int REMINDERS_INDEX_ID = 0;
236    private static final int REMINDERS_MINUTES_ID = 1;
237    private static final int REMINDERS_METHOD_ID = 2;
238
239    private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
240
241    static final String[] CALENDARS_PROJECTION = new String[] {
242        Calendars._ID,           // 0
243        Calendars.CALENDAR_DISPLAY_NAME,  // 1
244        Calendars.OWNER_ACCOUNT, // 2
245        Calendars.CAN_ORGANIZER_RESPOND, // 3
246        Calendars.ACCOUNT_NAME // 4
247    };
248    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
249    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
250    static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
251    static final int CALENDARS_INDEX_ACCOUNT_NAME = 4;
252
253    static final String CALENDARS_WHERE = Calendars._ID + "=?";
254    static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?";
255    static final String CALENDARS_VISIBLE_WHERE = Calendars.VISIBLE + "=?";
256
257    private static final String NANP_ALLOWED_SYMBOLS = "()+-*#.";
258    private static final int NANP_MIN_DIGITS = 7;
259    private static final int NANP_MAX_DIGITS = 11;
260
261
262    private View mView;
263
264    private Uri mUri;
265    private long mEventId;
266    private Cursor mEventCursor;
267    private Cursor mAttendeesCursor;
268    private Cursor mCalendarsCursor;
269    private Cursor mRemindersCursor;
270
271    private static float mScale = 0; // Used for supporting different screen densities
272
273    private static int mCustomAppIconSize = 32;
274
275    private long mStartMillis;
276    private long mEndMillis;
277    private boolean mAllDay;
278
279    private boolean mHasAttendeeData;
280    private String mEventOrganizerEmail;
281    private String mEventOrganizerDisplayName = "";
282    private boolean mIsOrganizer;
283    private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
284    private boolean mOwnerCanRespond;
285    private String mSyncAccountName;
286    private String mCalendarOwnerAccount;
287    private boolean mCanModifyCalendar;
288    private boolean mCanModifyEvent;
289    private boolean mIsBusyFreeCalendar;
290    private int mNumOfAttendees;
291    private EditResponseHelper mEditResponseHelper;
292    private boolean mDeleteDialogVisible = false;
293    private DeleteEventHelper mDeleteHelper;
294
295    private int mOriginalAttendeeResponse;
296    private int mAttendeeResponseFromIntent = Attendees.ATTENDEE_STATUS_NONE;
297    private int mUserSetResponse = Attendees.ATTENDEE_STATUS_NONE;
298    private boolean mIsRepeating;
299    private boolean mHasAlarm;
300    private int mMaxReminders;
301    private String mCalendarAllowedReminders;
302    // Used to prevent saving changes in event if it is being deleted.
303    private boolean mEventDeletionStarted = false;
304
305    private TextView mTitle;
306    private TextView mWhenDateTime;
307    private TextView mWhere;
308    private ExpandableTextView mDesc;
309    private AttendeesView mLongAttendees;
310    private Button emailAttendeesButton;
311    private Menu mMenu = null;
312    private View mHeadlines;
313    private ScrollView mScrollView;
314    private View mLoadingMsgView;
315    private ObjectAnimator mAnimateAlpha;
316    private long mLoadingMsgStartTime;
317    private static final int FADE_IN_TIME = 300;   // in milliseconds
318    private static final int LOADING_MSG_DELAY = 600;   // in milliseconds
319    private static final int LOADING_MSG_MIN_DISPLAY_TIME = 600;
320    private boolean mNoCrossFade = false;  // Used to prevent repeated cross-fade
321
322
323    private static final Pattern mWildcardPattern = Pattern.compile("^.*$");
324
325    ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
326    ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
327    ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
328    ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
329    ArrayList<String> mToEmails = new ArrayList<String>();
330    ArrayList<String> mCcEmails = new ArrayList<String>();
331    private int mColor;
332
333
334    private int mDefaultReminderMinutes;
335    private final ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0);
336    public ArrayList<ReminderEntry> mReminders;
337    public ArrayList<ReminderEntry> mOriginalReminders = new ArrayList<ReminderEntry>();
338    public ArrayList<ReminderEntry> mUnsupportedReminders = new ArrayList<ReminderEntry>();
339    private boolean mUserModifiedReminders = false;
340
341    /**
342     * Contents of the "minutes" spinner.  This has default values from the XML file, augmented
343     * with any additional values that were already associated with the event.
344     */
345    private ArrayList<Integer> mReminderMinuteValues;
346    private ArrayList<String> mReminderMinuteLabels;
347
348    /**
349     * Contents of the "methods" spinner.  The "values" list specifies the method constant
350     * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels.  Any methods that
351     * aren't allowed by the Calendar will be removed.
352     */
353    private ArrayList<Integer> mReminderMethodValues;
354    private ArrayList<String> mReminderMethodLabels;
355
356    private QueryHandler mHandler;
357
358
359    private final Runnable mTZUpdater = new Runnable() {
360        @Override
361        public void run() {
362            updateEvent(mView);
363        }
364    };
365
366    private final Runnable mLoadingMsgAlphaUpdater = new Runnable() {
367        @Override
368        public void run() {
369            // Since this is run after a delay, make sure to only show the message
370            // if the event's data is not shown yet.
371            if (!mAnimateAlpha.isRunning() && mScrollView.getAlpha() == 0) {
372                mLoadingMsgStartTime = System.currentTimeMillis();
373                mLoadingMsgView.setAlpha(1);
374            }
375        }
376    };
377
378    private OnItemSelectedListener mReminderChangeListener;
379
380    private static int mDialogWidth = 500;
381    private static int mDialogHeight = 600;
382    private static int DIALOG_TOP_MARGIN = 8;
383    private boolean mIsDialog = false;
384    private boolean mIsPaused = true;
385    private boolean mDismissOnResume = false;
386    private int mX = -1;
387    private int mY = -1;
388    private int mMinTop;         // Dialog cannot be above this location
389    private boolean mIsTabletConfig;
390    private Activity mActivity;
391    private Context mContext;
392
393    private class QueryHandler extends AsyncQueryService {
394        public QueryHandler(Context context) {
395            super(context);
396        }
397
398        @Override
399        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
400            // if the activity is finishing, then close the cursor and return
401            final Activity activity = getActivity();
402            if (activity == null || activity.isFinishing()) {
403                if (cursor != null) {
404                    cursor.close();
405                }
406                return;
407            }
408
409            switch (token) {
410            case TOKEN_QUERY_EVENT:
411                mEventCursor = Utils.matrixCursorFromCursor(cursor);
412                if (initEventCursor()) {
413                    // The cursor is empty. This can happen if the event was
414                    // deleted.
415                    // FRAG_TODO we should no longer rely on Activity.finish()
416                    activity.finish();
417                    return;
418                }
419                updateEvent(mView);
420                prepareReminders();
421
422                // start calendar query
423                Uri uri = Calendars.CONTENT_URI;
424                String[] args = new String[] {
425                        Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))};
426                startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION,
427                        CALENDARS_WHERE, args, null);
428                break;
429            case TOKEN_QUERY_CALENDARS:
430                mCalendarsCursor = Utils.matrixCursorFromCursor(cursor);
431                updateCalendar(mView);
432                // FRAG_TODO fragments shouldn't set the title anymore
433                updateTitle();
434
435                if (!mIsBusyFreeCalendar) {
436                    args = new String[] { Long.toString(mEventId) };
437
438                    // start attendees query
439                    uri = Attendees.CONTENT_URI;
440                    startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION,
441                            ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER);
442                } else {
443                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES);
444                }
445                if (mHasAlarm) {
446                    // start reminders query
447                    args = new String[] { Long.toString(mEventId) };
448                    uri = Reminders.CONTENT_URI;
449                    startQuery(TOKEN_QUERY_REMINDERS, null, uri,
450                            REMINDERS_PROJECTION, REMINDERS_WHERE, args, null);
451                } else {
452                    sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS);
453                }
454                break;
455            case TOKEN_QUERY_ATTENDEES:
456                mAttendeesCursor = Utils.matrixCursorFromCursor(cursor);
457                initAttendeesCursor(mView);
458                updateResponse(mView);
459                break;
460            case TOKEN_QUERY_REMINDERS:
461                mRemindersCursor = Utils.matrixCursorFromCursor(cursor);
462                initReminders(mView, mRemindersCursor);
463                break;
464            case TOKEN_QUERY_VISIBLE_CALENDARS:
465                if (cursor.getCount() > 1) {
466                    // Start duplicate calendars query to detect whether to add the calendar
467                    // email to the calendar owner display.
468                    String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
469                    mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null,
470                            Calendars.CONTENT_URI, CALENDARS_PROJECTION,
471                            CALENDARS_DUPLICATE_NAME_WHERE, new String[] {displayName}, null);
472                } else {
473                    // Don't need to display the calendar owner when there is only a single
474                    // calendar.  Skip the duplicate calendars query.
475                    setVisibilityCommon(mView, R.id.calendar_container, View.GONE);
476                    mCurrentQuery |= TOKEN_QUERY_DUPLICATE_CALENDARS;
477                }
478                break;
479            case TOKEN_QUERY_DUPLICATE_CALENDARS:
480                Resources res = activity.getResources();
481                SpannableStringBuilder sb = new SpannableStringBuilder();
482
483                // Calendar display name
484                String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
485                sb.append(calendarName);
486
487                // Show email account if display name is not unique and
488                // display name != email
489                String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
490                if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email) &&
491                        Utils.isValidEmail(email)) {
492                    sb.append(" (").append(email).append(")");
493                }
494
495                setVisibilityCommon(mView, R.id.calendar_container, View.VISIBLE);
496                setTextCommon(mView, R.id.calendar_name, sb);
497                break;
498            }
499            cursor.close();
500            sendAccessibilityEventIfQueryDone(token);
501
502            // All queries are done, show the view.
503            if (mCurrentQuery == TOKEN_QUERY_ALL) {
504                if (mLoadingMsgView.getAlpha() == 1) {
505                    // Loading message is showing, let it stay a bit more (to prevent
506                    // flashing) by adding a start delay to the event animation
507                    long timeDiff = LOADING_MSG_MIN_DISPLAY_TIME - (System.currentTimeMillis() -
508                            mLoadingMsgStartTime);
509                    if (timeDiff > 0) {
510                        mAnimateAlpha.setStartDelay(timeDiff);
511                    }
512                }
513                if (!mAnimateAlpha.isRunning() &&!mAnimateAlpha.isStarted() && !mNoCrossFade) {
514                    mAnimateAlpha.start();
515                } else {
516                    mScrollView.setAlpha(1);
517                    mLoadingMsgView.setVisibility(View.GONE);
518                }
519            }
520        }
521    }
522
523    private void sendAccessibilityEventIfQueryDone(int token) {
524        mCurrentQuery |= token;
525        if (mCurrentQuery == TOKEN_QUERY_ALL) {
526            sendAccessibilityEvent();
527        }
528    }
529
530    public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis,
531            int attendeeResponse, boolean isDialog, int windowStyle) {
532
533        Resources r = context.getResources();
534        if (mScale == 0) {
535            mScale = context.getResources().getDisplayMetrics().density;
536            if (mScale != 1) {
537                mCustomAppIconSize *= mScale;
538                if (isDialog) {
539                    DIALOG_TOP_MARGIN *= mScale;
540                }
541            }
542        }
543        if (isDialog) {
544            setDialogSize(r);
545        }
546        mIsDialog = isDialog;
547
548        setStyle(DialogFragment.STYLE_NO_TITLE, 0);
549        mUri = uri;
550        mStartMillis = startMillis;
551        mEndMillis = endMillis;
552        mAttendeeResponseFromIntent = attendeeResponse;
553        mWindowStyle = windowStyle;
554    }
555
556    // This is currently required by the fragment manager.
557    public EventInfoFragment() {
558    }
559
560
561
562    public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis,
563            int attendeeResponse, boolean isDialog, int windowStyle) {
564        this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis,
565                endMillis, attendeeResponse, isDialog, windowStyle);
566        mEventId = eventId;
567    }
568
569    @Override
570    public void onActivityCreated(Bundle savedInstanceState) {
571        super.onActivityCreated(savedInstanceState);
572
573        mReminderChangeListener = new OnItemSelectedListener() {
574            @Override
575            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
576                Integer prevValue = (Integer) parent.getTag();
577                if (prevValue == null || prevValue != position) {
578                    parent.setTag(position);
579                    mUserModifiedReminders = true;
580                }
581            }
582
583            @Override
584            public void onNothingSelected(AdapterView<?> parent) {
585                // do nothing
586            }
587
588        };
589
590        if (savedInstanceState != null) {
591            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
592            mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE,
593                    DIALOG_WINDOW_STYLE);
594        }
595
596        if (mIsDialog) {
597            applyDialogParams();
598        }
599        mContext = getActivity();
600    }
601
602    private void applyDialogParams() {
603        Dialog dialog = getDialog();
604        dialog.setCanceledOnTouchOutside(true);
605
606        Window window = dialog.getWindow();
607        window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
608
609        WindowManager.LayoutParams a = window.getAttributes();
610        a.dimAmount = .4f;
611
612        a.width = mDialogWidth;
613        a.height = mDialogHeight;
614
615
616        // On tablets , do smart positioning of dialog
617        // On phones , use the whole screen
618
619        if (mX != -1 || mY != -1) {
620            a.x = mX - mDialogWidth / 2;
621            a.y = mY - mDialogHeight / 2;
622            if (a.y < mMinTop) {
623                a.y = mMinTop + DIALOG_TOP_MARGIN;
624            }
625            a.gravity = Gravity.LEFT | Gravity.TOP;
626        }
627        window.setAttributes(a);
628    }
629
630    public void setDialogParams(int x, int y, int minTop) {
631        mX = x;
632        mY = y;
633        mMinTop = minTop;
634    }
635
636    // Implements OnCheckedChangeListener
637    @Override
638    public void onCheckedChanged(RadioGroup group, int checkedId) {
639        // If this is not a repeating event, then don't display the dialog
640        // asking which events to change.
641        mUserSetResponse = getResponseFromButtonId(checkedId);
642        if (!mIsRepeating) {
643            return;
644        }
645
646        // If the selection is the same as the original, then don't display the
647        // dialog asking which events to change.
648        if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) {
649            return;
650        }
651
652        // This is a repeating event. We need to ask the user if they mean to
653        // change just this one instance or all instances.
654        mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
655    }
656
657    public void onNothingSelected(AdapterView<?> parent) {
658    }
659
660    @Override
661    public void onAttach(Activity activity) {
662        super.onAttach(activity);
663        mActivity = activity;
664        mEditResponseHelper = new EditResponseHelper(activity);
665
666        if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
667            mEditResponseHelper.setWhichEvents(UPDATE_ALL);
668        }
669        mHandler = new QueryHandler(activity);
670        if (!mIsDialog) {
671            setHasOptionsMenu(true);
672        }
673    }
674
675    @Override
676    public View onCreateView(LayoutInflater inflater, ViewGroup container,
677            Bundle savedInstanceState) {
678
679        if (savedInstanceState != null) {
680            mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false);
681            mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE,
682                    DIALOG_WINDOW_STYLE);
683            mDeleteDialogVisible =
684                savedInstanceState.getBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE,false);
685
686        }
687
688        if (mWindowStyle == DIALOG_WINDOW_STYLE) {
689            mView = inflater.inflate(R.layout.event_info_dialog, container, false);
690        } else {
691            mView = inflater.inflate(R.layout.event_info, container, false);
692        }
693        mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view);
694        mLoadingMsgView = mView.findViewById(R.id.event_info_loading_msg);
695        mTitle = (TextView) mView.findViewById(R.id.title);
696        mWhenDateTime = (TextView) mView.findViewById(R.id.when_datetime);
697        mWhere = (TextView) mView.findViewById(R.id.where);
698        mDesc = (ExpandableTextView) mView.findViewById(R.id.description);
699        mHeadlines = mView.findViewById(R.id.event_info_headline);
700        mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list);
701        mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config);
702
703        if (mUri == null) {
704            // restore event ID from bundle
705            mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID);
706            mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
707            mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS);
708            mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS);
709        }
710
711        mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0, 1);
712        mAnimateAlpha.setDuration(FADE_IN_TIME);
713        mAnimateAlpha.addListener(new AnimatorListenerAdapter() {
714            int defLayerType;
715
716            @Override
717            public void onAnimationStart(Animator animation) {
718                // Use hardware layer for better performance during animation
719                defLayerType = mScrollView.getLayerType();
720                mScrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
721                // Ensure that the loading message is gone before showing the
722                // event info
723                mLoadingMsgView.removeCallbacks(mLoadingMsgAlphaUpdater);
724                mLoadingMsgView.setVisibility(View.GONE);
725            }
726
727            @Override
728            public void onAnimationCancel(Animator animation) {
729                mScrollView.setLayerType(defLayerType, null);
730            }
731
732            @Override
733            public void onAnimationEnd(Animator animation) {
734                mScrollView.setLayerType(defLayerType, null);
735                // Do not cross fade after the first time
736                mNoCrossFade = true;
737            }
738        });
739
740        mLoadingMsgView.setAlpha(0);
741        mScrollView.setAlpha(0);
742        mLoadingMsgView.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY);
743
744        // start loading the data
745
746        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
747                null, null, null);
748
749        View b = mView.findViewById(R.id.delete);
750        b.setOnClickListener(new OnClickListener() {
751            @Override
752            public void onClick(View v) {
753                if (!mCanModifyCalendar) {
754                    return;
755                }
756                mDeleteHelper =
757                        new DeleteEventHelper(mContext, mActivity, !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
758                mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this);
759                mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
760                mDeleteDialogVisible = true;
761                mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
762            }
763        });
764
765        // Hide Edit/Delete buttons if in full screen mode on a phone
766        if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) {
767            mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE);
768        }
769
770        // Create a listener for the email guests button
771        emailAttendeesButton = (Button) mView.findViewById(R.id.email_attendees_button);
772        if (emailAttendeesButton != null) {
773            emailAttendeesButton.setOnClickListener(new View.OnClickListener() {
774                @Override
775                public void onClick(View v) {
776                    emailAttendees();
777                }
778            });
779        }
780
781        // Create a listener for the add reminder button
782        View reminderAddButton = mView.findViewById(R.id.reminder_add);
783        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
784            @Override
785            public void onClick(View v) {
786                addReminder();
787                mUserModifiedReminders = true;
788            }
789        };
790        reminderAddButton.setOnClickListener(addReminderOnClickListener);
791
792        // Set reminders variables
793
794        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
795        String defaultReminderString = prefs.getString(
796                GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
797        mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
798        prepareReminders();
799
800        return mView;
801    }
802
803    private final Runnable onDeleteRunnable = new Runnable() {
804        @Override
805        public void run() {
806            if (EventInfoFragment.this.mIsPaused) {
807                mDismissOnResume = true;
808                return;
809            }
810            if (EventInfoFragment.this.isVisible()) {
811                EventInfoFragment.this.dismiss();
812            }
813        }
814    };
815
816    private void updateTitle() {
817        Resources res = getActivity().getResources();
818        if (mCanModifyCalendar && !mIsOrganizer) {
819            getActivity().setTitle(res.getString(R.string.event_info_title_invite));
820        } else {
821            getActivity().setTitle(res.getString(R.string.event_info_title));
822        }
823    }
824
825    /**
826     * Initializes the event cursor, which is expected to point to the first
827     * (and only) result from a query.
828     * @return true if the cursor is empty.
829     */
830    private boolean initEventCursor() {
831        if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) {
832            return true;
833        }
834        mEventCursor.moveToFirst();
835        mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
836        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
837        mIsRepeating = !TextUtils.isEmpty(rRule);
838        mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false;
839        mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS);
840        mCalendarAllowedReminders =  mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS);
841        return false;
842    }
843
844    @SuppressWarnings("fallthrough")
845    private void initAttendeesCursor(View view) {
846        mOriginalAttendeeResponse = Attendees.ATTENDEE_STATUS_NONE;
847        mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE;
848        mNumOfAttendees = 0;
849        if (mAttendeesCursor != null) {
850            mNumOfAttendees = mAttendeesCursor.getCount();
851            if (mAttendeesCursor.moveToFirst()) {
852                mAcceptedAttendees.clear();
853                mDeclinedAttendees.clear();
854                mTentativeAttendees.clear();
855                mNoResponseAttendees.clear();
856
857                do {
858                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
859                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
860                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
861
862                    if (mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP) ==
863                            Attendees.RELATIONSHIP_ORGANIZER) {
864
865                        // Overwrites the one from Event table if available
866                        if (!TextUtils.isEmpty(name)) {
867                            mEventOrganizerDisplayName = name;
868                            if (!mIsOrganizer) {
869                                setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
870                                setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName);
871                            }
872                        }
873                    }
874
875                    if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE &&
876                            mCalendarOwnerAccount.equalsIgnoreCase(email)) {
877                        mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
878                        mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
879                    } else {
880                        String identity = null;
881                        String idNamespace = null;
882
883                        if (Utils.isJellybeanOrLater()) {
884                            identity = mAttendeesCursor.getString(ATTENDEES_INDEX_IDENTITY);
885                            idNamespace = mAttendeesCursor.getString(ATTENDEES_INDEX_ID_NAMESPACE);
886                        }
887
888                        // Don't show your own status in the list because:
889                        //  1) it doesn't make sense for event without other guests.
890                        //  2) there's a spinner for that for events with guests.
891                        switch(status) {
892                            case Attendees.ATTENDEE_STATUS_ACCEPTED:
893                                mAcceptedAttendees.add(new Attendee(name, email,
894                                        Attendees.ATTENDEE_STATUS_ACCEPTED, identity,
895                                        idNamespace));
896                                break;
897                            case Attendees.ATTENDEE_STATUS_DECLINED:
898                                mDeclinedAttendees.add(new Attendee(name, email,
899                                        Attendees.ATTENDEE_STATUS_DECLINED, identity,
900                                        idNamespace));
901                                break;
902                            case Attendees.ATTENDEE_STATUS_TENTATIVE:
903                                mTentativeAttendees.add(new Attendee(name, email,
904                                        Attendees.ATTENDEE_STATUS_TENTATIVE, identity,
905                                        idNamespace));
906                                break;
907                            default:
908                                mNoResponseAttendees.add(new Attendee(name, email,
909                                        Attendees.ATTENDEE_STATUS_NONE, identity,
910                                        idNamespace));
911                        }
912                    }
913                } while (mAttendeesCursor.moveToNext());
914                mAttendeesCursor.moveToFirst();
915
916                updateAttendees(view);
917            }
918        }
919    }
920
921    @Override
922    public void onSaveInstanceState(Bundle outState) {
923        super.onSaveInstanceState(outState);
924        outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId);
925        outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis);
926        outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis);
927        outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog);
928        outState.putInt(BUNDLE_KEY_WINDOW_STYLE, mWindowStyle);
929        outState.putBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE, mDeleteDialogVisible);
930        outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent);
931    }
932
933
934    @Override
935    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
936        super.onCreateOptionsMenu(menu, inflater);
937        // Show edit/delete buttons only in non-dialog configuration
938        if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) {
939            inflater.inflate(R.menu.event_info_title_bar, menu);
940            mMenu = menu;
941            updateMenu();
942        }
943    }
944
945    @Override
946    public boolean onOptionsItemSelected(MenuItem item) {
947
948        // If we're a dialog we don't want to handle menu buttons
949        if (mIsDialog) {
950            return false;
951        }
952        // Handles option menu selections:
953        // Home button - close event info activity and start the main calendar
954        // one
955        // Edit button - start the event edit activity and close the info
956        // activity
957        // Delete button - start a delete query that calls a runnable that close
958        // the info activity
959
960        final int itemId = item.getItemId();
961        if (itemId == android.R.id.home) {
962            Utils.returnToCalendarHome(mContext);
963            mActivity.finish();
964            return true;
965        } else if (itemId == R.id.info_action_edit) {
966            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
967            Intent intent = new Intent(Intent.ACTION_EDIT, uri);
968            intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis);
969            intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis);
970            intent.putExtra(EXTRA_EVENT_ALL_DAY, mAllDay);
971            intent.setClass(mActivity, EditEventActivity.class);
972            intent.putExtra(EVENT_EDIT_ON_LAUNCH, true);
973            startActivity(intent);
974            mActivity.finish();
975        } else if (itemId == R.id.info_action_delete) {
976            mDeleteHelper =
977                    new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */);
978            mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this);
979            mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
980            mDeleteDialogVisible = true;
981            mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
982        }
983        return super.onOptionsItemSelected(item);
984    }
985
986    @Override
987    public void onDestroyView() {
988
989        if (!mEventDeletionStarted) {
990            boolean responseSaved = saveResponse();
991            if (saveReminders() || responseSaved) {
992                Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show();
993            }
994        }
995        super.onDestroyView();
996    }
997
998    @Override
999    public void onDestroy() {
1000        if (mEventCursor != null) {
1001            mEventCursor.close();
1002        }
1003        if (mCalendarsCursor != null) {
1004            mCalendarsCursor.close();
1005        }
1006        if (mAttendeesCursor != null) {
1007            mAttendeesCursor.close();
1008        }
1009        super.onDestroy();
1010    }
1011
1012    /**
1013     * Asynchronously saves the response to an invitation if the user changed
1014     * the response. Returns true if the database will be updated.
1015     *
1016     * @return true if the database will be changed
1017     */
1018    private boolean saveResponse() {
1019        if (mAttendeesCursor == null || mEventCursor == null) {
1020            return false;
1021        }
1022
1023        RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value);
1024        int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId());
1025        if (status == Attendees.ATTENDEE_STATUS_NONE) {
1026            return false;
1027        }
1028
1029        // If the status has not changed, then don't update the database
1030        if (status == mOriginalAttendeeResponse) {
1031            return false;
1032        }
1033
1034        // If we never got an owner attendee id we can't set the status
1035        if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) {
1036            return false;
1037        }
1038
1039        if (!mIsRepeating) {
1040            // This is a non-repeating event
1041            updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
1042            return true;
1043        }
1044
1045        // This is a repeating event
1046        int whichEvents = mEditResponseHelper.getWhichEvents();
1047        switch (whichEvents) {
1048            case -1:
1049                return false;
1050            case UPDATE_SINGLE:
1051                createExceptionResponse(mEventId, status);
1052                return true;
1053            case UPDATE_ALL:
1054                updateResponse(mEventId, mCalendarOwnerAttendeeId, status);
1055                return true;
1056            default:
1057                Log.e(TAG, "Unexpected choice for updating invitation response");
1058                break;
1059        }
1060        return false;
1061    }
1062
1063    private void updateResponse(long eventId, long attendeeId, int status) {
1064        // Update the attendee status in the attendees table.  the provider
1065        // takes care of updating the self attendance status.
1066        ContentValues values = new ContentValues();
1067
1068        if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
1069            values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
1070        }
1071        values.put(Attendees.ATTENDEE_STATUS, status);
1072        values.put(Attendees.EVENT_ID, eventId);
1073
1074        Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
1075
1076        mHandler.startUpdate(mHandler.getNextToken(), null, uri, values,
1077                null, null, Utils.UNDO_DELAY);
1078    }
1079
1080    /**
1081     * Creates an exception to a recurring event.  The only change we're making is to the
1082     * "self attendee status" value.  The provider will take care of updating the corresponding
1083     * Attendees.attendeeStatus entry.
1084     *
1085     * @param eventId The recurring event.
1086     * @param status The new value for selfAttendeeStatus.
1087     */
1088    private void createExceptionResponse(long eventId, int status) {
1089        ContentValues values = new ContentValues();
1090        values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
1091        values.put(Events.SELF_ATTENDEE_STATUS, status);
1092        values.put(Events.STATUS, Events.STATUS_CONFIRMED);
1093
1094        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1095        Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
1096                String.valueOf(eventId));
1097        ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build());
1098
1099        mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops,
1100                Utils.UNDO_DELAY);
1101   }
1102
1103    public static int getResponseFromButtonId(int buttonId) {
1104        int response;
1105        if (buttonId == R.id.response_yes) {
1106            response = Attendees.ATTENDEE_STATUS_ACCEPTED;
1107        } else if (buttonId == R.id.response_maybe) {
1108            response = Attendees.ATTENDEE_STATUS_TENTATIVE;
1109        } else if (buttonId == R.id.response_no) {
1110            response = Attendees.ATTENDEE_STATUS_DECLINED;
1111        } else {
1112            response = Attendees.ATTENDEE_STATUS_NONE;
1113        }
1114        return response;
1115    }
1116
1117    public static int findButtonIdForResponse(int response) {
1118        int buttonId;
1119        switch (response) {
1120            case Attendees.ATTENDEE_STATUS_ACCEPTED:
1121                buttonId = R.id.response_yes;
1122                break;
1123            case Attendees.ATTENDEE_STATUS_TENTATIVE:
1124                buttonId = R.id.response_maybe;
1125                break;
1126            case Attendees.ATTENDEE_STATUS_DECLINED:
1127                buttonId = R.id.response_no;
1128                break;
1129                default:
1130                    buttonId = -1;
1131        }
1132        return buttonId;
1133    }
1134
1135    private void doEdit() {
1136        Context c = getActivity();
1137        // This ensures that we aren't in the process of closing and have been
1138        // unattached already
1139        if (c != null) {
1140            CalendarController.getInstance(c).sendEventRelatedEvent(
1141                    this, EventType.EDIT_EVENT, mEventId, mStartMillis, mEndMillis, 0
1142                    , 0, -1);
1143        }
1144    }
1145
1146    private void updateEvent(View view) {
1147        if (mEventCursor == null || view == null) {
1148            return;
1149        }
1150
1151        Context context = view.getContext();
1152        if (context == null) {
1153            return;
1154        }
1155
1156        String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
1157        if (eventName == null || eventName.length() == 0) {
1158            eventName = getActivity().getString(R.string.no_title_label);
1159        }
1160
1161        mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1162        String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION);
1163        String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION);
1164        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
1165        String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
1166
1167        mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR));
1168        mHeadlines.setBackgroundColor(mColor);
1169
1170        // What
1171        if (eventName != null) {
1172            setTextCommon(view, R.id.title, eventName);
1173        }
1174
1175        // When
1176        // Set the date and repeats (if any)
1177        String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater);
1178
1179        Resources resources = context.getResources();
1180        String displayedDatetime = Utils.getDisplayedDatetime(mStartMillis, mEndMillis,
1181                System.currentTimeMillis(), localTimezone, mAllDay, context);
1182
1183        String displayedTimezone = null;
1184        if (!mAllDay) {
1185            displayedTimezone = Utils.getDisplayedTimezone(mStartMillis, localTimezone,
1186                    eventTimezone);
1187        }
1188        // Display the datetime.  Make the timezone (if any) transparent.
1189        if (displayedTimezone == null) {
1190            setTextCommon(view, R.id.when_datetime, displayedDatetime);
1191        } else {
1192            int timezoneIndex = displayedDatetime.length();
1193            displayedDatetime += "  " + displayedTimezone;
1194            SpannableStringBuilder sb = new SpannableStringBuilder(displayedDatetime);
1195            ForegroundColorSpan transparentColorSpan = new ForegroundColorSpan(
1196                    resources.getColor(R.color.event_info_headline_transparent_color));
1197            sb.setSpan(transparentColorSpan, timezoneIndex, displayedDatetime.length(),
1198                    Spannable.SPAN_INCLUSIVE_INCLUSIVE);
1199            setTextCommon(view, R.id.when_datetime, sb);
1200        }
1201
1202        // Display the repeat string (if any)
1203        String repeatString = null;
1204        if (!TextUtils.isEmpty(rRule)) {
1205            EventRecurrence eventRecurrence = new EventRecurrence();
1206            eventRecurrence.parse(rRule);
1207            Time date = new Time(localTimezone);
1208            date.set(mStartMillis);
1209            if (mAllDay) {
1210                date.timezone = Time.TIMEZONE_UTC;
1211            }
1212            eventRecurrence.setStartDate(date);
1213            repeatString = EventRecurrenceFormatter.getRepeatString(resources, eventRecurrence);
1214        }
1215        if (repeatString == null) {
1216            view.findViewById(R.id.when_repeat).setVisibility(View.GONE);
1217        } else {
1218            setTextCommon(view, R.id.when_repeat, repeatString);
1219        }
1220
1221        // Organizer view is setup in the updateCalendar method
1222
1223
1224        // Where
1225        if (location == null || location.trim().length() == 0) {
1226            setVisibilityCommon(view, R.id.where, View.GONE);
1227        } else {
1228            final TextView textView = mWhere;
1229            if (textView != null) {
1230                textView.setAutoLinkMask(0);
1231                textView.setText(location.trim());
1232                try {
1233                    linkifyTextView(textView);
1234                } catch (Exception ex) {
1235                    // unexpected
1236                    Log.e(TAG, "Linkification failed", ex);
1237                }
1238
1239                textView.setOnTouchListener(new OnTouchListener() {
1240                    @Override
1241                    public boolean onTouch(View v, MotionEvent event) {
1242                        try {
1243                            return v.onTouchEvent(event);
1244                        } catch (ActivityNotFoundException e) {
1245                            // ignore
1246                            return true;
1247                        }
1248                    }
1249                });
1250            }
1251        }
1252
1253        // Description
1254        if (description != null && description.length() != 0) {
1255            mDesc.setText(description);
1256        }
1257
1258        // Launch Custom App
1259        if (Utils.isJellybeanOrLater()) {
1260            updateCustomAppButton();
1261        }
1262    }
1263
1264    private void updateCustomAppButton() {
1265        buttonSetup: {
1266            final Button launchButton = (Button) mView.findViewById(R.id.launch_custom_app_button);
1267            if (launchButton == null)
1268                break buttonSetup;
1269
1270            final String customAppPackage = mEventCursor.getString(EVENT_INDEX_CUSTOM_APP_PACKAGE);
1271            final String customAppUri = mEventCursor.getString(EVENT_INDEX_CUSTOM_APP_URI);
1272
1273            if (TextUtils.isEmpty(customAppPackage) || TextUtils.isEmpty(customAppUri))
1274                break buttonSetup;
1275
1276            PackageManager pm = mContext.getPackageManager();
1277            if (pm == null)
1278                break buttonSetup;
1279
1280            ApplicationInfo info;
1281            try {
1282                info = pm.getApplicationInfo(customAppPackage, 0);
1283                if (info == null)
1284                    break buttonSetup;
1285            } catch (NameNotFoundException e) {
1286                break buttonSetup;
1287            }
1288
1289            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
1290            final Intent intent = new Intent(CalendarContract.ACTION_HANDLE_CUSTOM_EVENT, uri);
1291            intent.setPackage(customAppPackage);
1292            intent.putExtra(CalendarContract.EXTRA_CUSTOM_APP_URI, customAppUri);
1293            intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis);
1294
1295            // See if we have a taker for our intent
1296            if (pm.resolveActivity(intent, 0) == null)
1297                break buttonSetup;
1298
1299            Drawable icon = pm.getApplicationIcon(info);
1300            if (icon != null) {
1301
1302                Drawable[] d = launchButton.getCompoundDrawables();
1303                icon.setBounds(0, 0, mCustomAppIconSize, mCustomAppIconSize);
1304                launchButton.setCompoundDrawables(icon, d[1], d[2], d[3]);
1305            }
1306
1307            CharSequence label = pm.getApplicationLabel(info);
1308            if (label != null && label.length() != 0) {
1309                launchButton.setText(label);
1310            } else if (icon == null) {
1311                // No icon && no label. Hide button?
1312                break buttonSetup;
1313            }
1314
1315            // Launch custom app
1316            launchButton.setOnClickListener(new View.OnClickListener() {
1317                @Override
1318                public void onClick(View v) {
1319                    try {
1320                        startActivityForResult(intent, 0);
1321                    } catch (ActivityNotFoundException e) {
1322                        // Shouldn't happen as we checked it already
1323                        setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE);
1324                    }
1325                }
1326            });
1327
1328            setVisibilityCommon(mView, R.id.launch_custom_app_container, View.VISIBLE);
1329            return;
1330
1331        }
1332
1333        setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE);
1334        return;
1335    }
1336
1337    /**
1338     * Finds North American Numbering Plan (NANP) phone numbers in the input text.
1339     *
1340     * @param text The text to scan.
1341     * @return A list of [start, end) pairs indicating the positions of phone numbers in the input.
1342     */
1343    // @VisibleForTesting
1344    static int[] findNanpPhoneNumbers(CharSequence text) {
1345        ArrayList<Integer> list = new ArrayList<Integer>();
1346
1347        int startPos = 0;
1348        int endPos = text.length() - NANP_MIN_DIGITS + 1;
1349        if (endPos < 0) {
1350            return new int[] {};
1351        }
1352
1353        /*
1354         * We can't just strip the whitespace out and crunch it down, because the whitespace
1355         * is significant.  March through, trying to figure out where numbers start and end.
1356         */
1357        while (startPos < endPos) {
1358            // skip whitespace
1359            while (Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
1360                startPos++;
1361            }
1362            if (startPos == endPos) {
1363                break;
1364            }
1365
1366            // check for a match at this position
1367            int matchEnd = findNanpMatchEnd(text, startPos);
1368            if (matchEnd > startPos) {
1369                list.add(startPos);
1370                list.add(matchEnd);
1371                startPos = matchEnd;    // skip past match
1372            } else {
1373                // skip to next whitespace char
1374                while (!Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
1375                    startPos++;
1376                }
1377            }
1378        }
1379
1380        int[] result = new int[list.size()];
1381        for (int i = list.size() - 1; i >= 0; i--) {
1382            result[i] = list.get(i);
1383        }
1384        return result;
1385    }
1386
1387    /**
1388     * Checks to see if there is a valid phone number in the input, starting at the specified
1389     * offset.  If so, the index of the last character + 1 is returned.  The input is assumed
1390     * to begin with a non-whitespace character.
1391     *
1392     * @return Exclusive end position, or -1 if not a match.
1393     */
1394    private static int findNanpMatchEnd(CharSequence text, int startPos) {
1395        /*
1396         * A few interesting cases:
1397         *   94043                              # too short, ignore
1398         *   123456789012                       # too long, ignore
1399         *   +1 (650) 555-1212                  # 11 digits, spaces
1400         *   (650) 555 5555                     # Second space, only when first is present.
1401         *   (650) 555-1212, (650) 555-1213     # two numbers, return first
1402         *   1-650-555-1212                     # 11 digits with leading '1'
1403         *   *#650.555.1212#*!                  # 10 digits, include #*, ignore trailing '!'
1404         *   555.1212                           # 7 digits
1405         *
1406         * For the most part we want to break on whitespace, but it's common to leave a space
1407         * between the initial '1' and/or after the area code.
1408         */
1409
1410        // Check for "tel:" URI prefix.
1411        if (text.length() > startPos+4
1412                && text.subSequence(startPos, startPos+4).toString().equalsIgnoreCase("tel:")) {
1413            startPos += 4;
1414        }
1415
1416        int endPos = text.length();
1417        int curPos = startPos;
1418        int foundDigits = 0;
1419        char firstDigit = 'x';
1420        boolean foundWhiteSpaceAfterAreaCode = false;
1421
1422        while (curPos <= endPos) {
1423            char ch;
1424            if (curPos < endPos) {
1425                ch = text.charAt(curPos);
1426            } else {
1427                ch = 27;    // fake invalid symbol at end to trigger loop break
1428            }
1429
1430            if (Character.isDigit(ch)) {
1431                if (foundDigits == 0) {
1432                    firstDigit = ch;
1433                }
1434                foundDigits++;
1435                if (foundDigits > NANP_MAX_DIGITS) {
1436                    // too many digits, stop early
1437                    return -1;
1438                }
1439            } else if (Character.isWhitespace(ch)) {
1440                if ( (firstDigit == '1' && foundDigits == 4) ||
1441                        (foundDigits == 3)) {
1442                    foundWhiteSpaceAfterAreaCode = true;
1443                } else if (firstDigit == '1' && foundDigits == 1) {
1444                } else if (foundWhiteSpaceAfterAreaCode
1445                        && ( (firstDigit == '1' && (foundDigits == 7)) || (foundDigits == 6))) {
1446                } else {
1447                    break;
1448                }
1449            } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) {
1450                break;
1451            }
1452            // else it's an allowed symbol
1453
1454            curPos++;
1455        }
1456
1457        if ((firstDigit != '1' && (foundDigits == 7 || foundDigits == 10)) ||
1458                (firstDigit == '1' && foundDigits == 11)) {
1459            // match
1460            return curPos;
1461        }
1462
1463        return -1;
1464    }
1465
1466    private static int indexFirstNonWhitespaceChar(CharSequence str) {
1467        for (int i = 0; i < str.length(); i++) {
1468            if (!Character.isWhitespace(str.charAt(i))) {
1469                return i;
1470            }
1471        }
1472        return -1;
1473    }
1474
1475    private static int indexLastNonWhitespaceChar(CharSequence str) {
1476        for (int i = str.length() - 1; i >= 0; i--) {
1477            if (!Character.isWhitespace(str.charAt(i))) {
1478                return i;
1479            }
1480        }
1481        return -1;
1482    }
1483
1484    /**
1485     * Replaces stretches of text that look like addresses and phone numbers with clickable
1486     * links.
1487     * <p>
1488     * This is really just an enhanced version of Linkify.addLinks().
1489     */
1490    private static void linkifyTextView(TextView textView) {
1491        /*
1492         * If the text includes a street address like "1600 Amphitheater Parkway, 94043",
1493         * the current Linkify code will identify "94043" as a phone number and invite
1494         * you to dial it (and not provide a map link for the address).  For outside US,
1495         * use Linkify result iff it spans the entire text.  Otherwise send the user to maps.
1496         */
1497        String defaultPhoneRegion = System.getProperty("user.region", "US");
1498        if (!defaultPhoneRegion.equals("US")) {
1499            CharSequence origText = textView.getText();
1500            Linkify.addLinks(textView, Linkify.ALL);
1501
1502            // If Linkify links the entire text, use that result.
1503            if (textView.getText() instanceof Spannable) {
1504                Spannable spanText = (Spannable) textView.getText();
1505                URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1506                if (spans.length == 1) {
1507                    int linkStart = spanText.getSpanStart(spans[0]);
1508                    int linkEnd = spanText.getSpanEnd(spans[0]);
1509                    if (linkStart <= indexFirstNonWhitespaceChar(origText) &&
1510                            linkEnd >= indexLastNonWhitespaceChar(origText) + 1) {
1511                        return;
1512                    }
1513                }
1514            }
1515
1516            // Otherwise default to geo.
1517            textView.setText(origText);
1518            Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
1519            return;
1520        }
1521
1522        /*
1523         * For within US, we want to have better recognition of phone numbers without losing
1524         * any of the existing annotations.  Ideally this would be addressed by improving Linkify.
1525         * For now we manage it as a second pass over the text.
1526         *
1527         * URIs and e-mail addresses are pretty easy to pick out of text.  Phone numbers
1528         * are a bit tricky because they have radically different formats in different
1529         * countries, in terms of both the digits and the way in which they are commonly
1530         * written or presented (e.g. the punctuation and spaces in "(650) 555-1212").
1531         * The expected format of a street address is defined in WebView.findAddress().  It's
1532         * pretty narrowly defined, so it won't often match.
1533         *
1534         * The RFC 3966 specification defines the format of a "tel:" URI.
1535         *
1536         * Start by letting Linkify find anything that isn't a phone number.  We have to let it
1537         * run first because every invocation removes all previous URLSpan annotations.
1538         *
1539         * Ideally we'd use the external/libphonenumber routines, but those aren't available
1540         * to unbundled applications.
1541         */
1542        boolean linkifyFoundLinks = Linkify.addLinks(textView,
1543                Linkify.ALL & ~(Linkify.PHONE_NUMBERS));
1544
1545        /*
1546         * Search for phone numbers.
1547         *
1548         * Some URIs contain strings of digits that look like phone numbers.  If both the URI
1549         * scanner and the phone number scanner find them, we want the URI link to win.  Since
1550         * the URI scanner runs first, we just need to avoid creating overlapping spans.
1551         */
1552        CharSequence text = textView.getText();
1553        int[] phoneSequences = findNanpPhoneNumbers(text);
1554
1555        /*
1556         * If the contents of the TextView are already Spannable (which will be the case if
1557         * Linkify found stuff, but might not be otherwise), we can just add annotations
1558         * to what's there.  If it's not, and we find phone numbers, we need to convert it to
1559         * a Spannable form.  (This mimics the behavior of Linkable.addLinks().)
1560         */
1561        Spannable spanText;
1562        if (text instanceof SpannableString) {
1563            spanText = (SpannableString) text;
1564        } else {
1565            spanText = SpannableString.valueOf(text);
1566        }
1567
1568        /*
1569         * Get a list of any spans created by Linkify, for the overlapping span check.
1570         */
1571        URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1572
1573        /*
1574         * Insert spans for the numbers we found.  We generate "tel:" URIs.
1575         */
1576        int phoneCount = 0;
1577        for (int match = 0; match < phoneSequences.length / 2; match++) {
1578            int start = phoneSequences[match*2];
1579            int end = phoneSequences[match*2 + 1];
1580
1581            if (spanWillOverlap(spanText, existingSpans, start, end)) {
1582                if (Log.isLoggable(TAG, Log.VERBOSE)) {
1583                    CharSequence seq = text.subSequence(start, end);
1584                    Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap");
1585                }
1586                continue;
1587            }
1588
1589            /*
1590             * The Linkify code takes the matching span and strips out everything that isn't a
1591             * digit or '+' sign.  We do the same here.  Extension numbers will get appended
1592             * without a separator, but the dialer wasn't doing anything useful with ";ext="
1593             * anyway.
1594             */
1595
1596            //String dialStr = phoneUtil.format(match.number(),
1597            //        PhoneNumberUtil.PhoneNumberFormat.RFC3966);
1598            StringBuilder dialBuilder = new StringBuilder();
1599            for (int i = start; i < end; i++) {
1600                char ch = spanText.charAt(i);
1601                if (ch == '+' || Character.isDigit(ch)) {
1602                    dialBuilder.append(ch);
1603                }
1604            }
1605            URLSpan span = new URLSpan("tel:" + dialBuilder.toString());
1606
1607            spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1608            phoneCount++;
1609        }
1610
1611        if (phoneCount != 0) {
1612            // If we had to "upgrade" to Spannable, store the object into the TextView.
1613            if (spanText != text) {
1614                textView.setText(spanText);
1615            }
1616
1617            // Linkify.addLinks() sets the TextView movement method if it finds any links.  We
1618            // want to do the same here.  (This is cloned from Linkify.addLinkMovementMethod().)
1619            MovementMethod mm = textView.getMovementMethod();
1620
1621            if ((mm == null) || !(mm instanceof LinkMovementMethod)) {
1622                if (textView.getLinksClickable()) {
1623                    textView.setMovementMethod(LinkMovementMethod.getInstance());
1624                }
1625            }
1626        }
1627
1628        if (!linkifyFoundLinks && phoneCount == 0) {
1629            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1630                Log.v(TAG, "No linkification matches, using geo default");
1631            }
1632            Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
1633        }
1634    }
1635
1636    /**
1637     * Determines whether a new span at [start,end) will overlap with any existing span.
1638     */
1639    private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start,
1640            int end) {
1641        if (start == end) {
1642            // empty span, ignore
1643            return false;
1644        }
1645        for (URLSpan span : spanList) {
1646            int existingStart = spanText.getSpanStart(span);
1647            int existingEnd = spanText.getSpanEnd(span);
1648            if ((start >= existingStart && start < existingEnd) ||
1649                    end > existingStart && end <= existingEnd) {
1650                return true;
1651            }
1652        }
1653
1654        return false;
1655    }
1656
1657    private void sendAccessibilityEvent() {
1658        AccessibilityManager am =
1659            (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE);
1660        if (!am.isEnabled()) {
1661            return;
1662        }
1663
1664        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1665        event.setClassName(getClass().getName());
1666        event.setPackageName(getActivity().getPackageName());
1667        List<CharSequence> text = event.getText();
1668
1669        addFieldToAccessibilityEvent(text, mTitle, null);
1670        addFieldToAccessibilityEvent(text, mWhenDateTime, null);
1671        addFieldToAccessibilityEvent(text, mWhere, null);
1672        addFieldToAccessibilityEvent(text, null, mDesc);
1673
1674        RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value);
1675        if (response.getVisibility() == View.VISIBLE) {
1676            int id = response.getCheckedRadioButtonId();
1677            if (id != View.NO_ID) {
1678                text.add(((TextView) getView().findViewById(R.id.response_label)).getText());
1679                text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE));
1680            }
1681        }
1682
1683        am.sendAccessibilityEvent(event);
1684    }
1685
1686    private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView tv,
1687            ExpandableTextView etv) {
1688        CharSequence cs;
1689        if (tv != null) {
1690            cs = tv.getText();
1691        } else if (etv != null) {
1692            cs = etv.getText();
1693        } else {
1694            return;
1695        }
1696
1697        if (!TextUtils.isEmpty(cs)) {
1698            cs = cs.toString().trim();
1699            if (cs.length() > 0) {
1700                text.add(cs);
1701                text.add(PERIOD_SPACE);
1702            }
1703        }
1704    }
1705
1706    private void updateCalendar(View view) {
1707        mCalendarOwnerAccount = "";
1708        if (mCalendarsCursor != null && mEventCursor != null) {
1709            mCalendarsCursor.moveToFirst();
1710            String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1711            mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount;
1712            mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
1713            mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME);
1714
1715            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
1716
1717            // start visible calendars query
1718            mHandler.startQuery(TOKEN_QUERY_VISIBLE_CALENDARS, null, Calendars.CONTENT_URI,
1719                    CALENDARS_PROJECTION, CALENDARS_VISIBLE_WHERE, new String[] {"1"}, null);
1720
1721            mEventOrganizerEmail = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
1722            mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(mEventOrganizerEmail);
1723
1724            if (!TextUtils.isEmpty(mEventOrganizerEmail) &&
1725                    !mEventOrganizerEmail.endsWith(Utils.MACHINE_GENERATED_ADDRESS)) {
1726                mEventOrganizerDisplayName = mEventOrganizerEmail;
1727            }
1728
1729            if (!mIsOrganizer && !TextUtils.isEmpty(mEventOrganizerDisplayName)) {
1730                setTextCommon(view, R.id.organizer, mEventOrganizerDisplayName);
1731                setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE);
1732            } else {
1733                setVisibilityCommon(view, R.id.organizer_container, View.GONE);
1734            }
1735            mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1736            mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL)
1737                    >= Calendars.CAL_ACCESS_CONTRIBUTOR;
1738            // TODO add "|| guestCanModify" after b/1299071 is fixed
1739            mCanModifyEvent = mCanModifyCalendar && mIsOrganizer;
1740            mIsBusyFreeCalendar =
1741                    mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY;
1742
1743            if (!mIsBusyFreeCalendar) {
1744
1745                View b = mView.findViewById(R.id.edit);
1746                b.setEnabled(true);
1747                b.setOnClickListener(new OnClickListener() {
1748                    @Override
1749                    public void onClick(View v) {
1750                        doEdit();
1751                        // For dialogs, just close the fragment
1752                        // For full screen, close activity on phone, leave it for tablet
1753                        if (mIsDialog) {
1754                            EventInfoFragment.this.dismiss();
1755                        }
1756                        else if (!mIsTabletConfig){
1757                            getActivity().finish();
1758                        }
1759                    }
1760                });
1761            }
1762            View button;
1763            if (mCanModifyCalendar) {
1764                button = mView.findViewById(R.id.delete);
1765                if (button != null) {
1766                    button.setEnabled(true);
1767                    button.setVisibility(View.VISIBLE);
1768                }
1769            }
1770            if (mCanModifyEvent) {
1771                button = mView.findViewById(R.id.edit);
1772                if (button != null) {
1773                    button.setEnabled(true);
1774                    button.setVisibility(View.VISIBLE);
1775                }
1776            }
1777
1778            if ((!mIsDialog && !mIsTabletConfig ||
1779                    mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) && mMenu != null) {
1780                mActivity.invalidateOptionsMenu();
1781            }
1782        } else {
1783            setVisibilityCommon(view, R.id.calendar, View.GONE);
1784            sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS);
1785        }
1786    }
1787
1788    /**
1789     *
1790     */
1791    private void updateMenu() {
1792        if (mMenu == null) {
1793            return;
1794        }
1795        MenuItem delete = mMenu.findItem(R.id.info_action_delete);
1796        MenuItem edit = mMenu.findItem(R.id.info_action_edit);
1797        if (delete != null) {
1798            delete.setVisible(mCanModifyCalendar);
1799            delete.setEnabled(mCanModifyCalendar);
1800        }
1801        if (edit != null) {
1802            edit.setVisible(mCanModifyEvent);
1803            edit.setEnabled(mCanModifyEvent);
1804        }
1805    }
1806
1807    private void updateAttendees(View view) {
1808        if (mAcceptedAttendees.size() + mDeclinedAttendees.size() +
1809                mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) {
1810            mLongAttendees.clearAttendees();
1811            (mLongAttendees).addAttendees(mAcceptedAttendees);
1812            (mLongAttendees).addAttendees(mDeclinedAttendees);
1813            (mLongAttendees).addAttendees(mTentativeAttendees);
1814            (mLongAttendees).addAttendees(mNoResponseAttendees);
1815            mLongAttendees.setEnabled(false);
1816            mLongAttendees.setVisibility(View.VISIBLE);
1817        } else {
1818            mLongAttendees.setVisibility(View.GONE);
1819        }
1820
1821        if (hasEmailableAttendees()) {
1822            setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE);
1823            if (emailAttendeesButton != null) {
1824                emailAttendeesButton.setText(R.string.email_guests_label);
1825            }
1826        } else if (hasEmailableOrganizer()) {
1827            setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE);
1828            if (emailAttendeesButton != null) {
1829                emailAttendeesButton.setText(R.string.email_organizer_label);
1830            }
1831        } else {
1832            setVisibilityCommon(mView, R.id.email_attendees_container, View.GONE);
1833        }
1834    }
1835
1836    /**
1837     * Returns true if there is at least 1 attendee that is not the viewer.
1838     */
1839    private boolean hasEmailableAttendees() {
1840        for (Attendee attendee : mAcceptedAttendees) {
1841            if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1842                return true;
1843            }
1844        }
1845        for (Attendee attendee : mTentativeAttendees) {
1846            if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1847                return true;
1848            }
1849        }
1850        for (Attendee attendee : mNoResponseAttendees) {
1851            if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1852                return true;
1853            }
1854        }
1855        for (Attendee attendee : mDeclinedAttendees) {
1856            if (Utils.isEmailableFrom(attendee.mEmail, mSyncAccountName)) {
1857                return true;
1858            }
1859        }
1860        return false;
1861    }
1862
1863    private boolean hasEmailableOrganizer() {
1864        return mEventOrganizerEmail != null &&
1865                Utils.isEmailableFrom(mEventOrganizerEmail, mSyncAccountName);
1866    }
1867
1868    public void initReminders(View view, Cursor cursor) {
1869
1870        // Add reminders
1871        mOriginalReminders.clear();
1872        mUnsupportedReminders.clear();
1873        while (cursor.moveToNext()) {
1874            int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES);
1875            int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD);
1876
1877            if (method != Reminders.METHOD_DEFAULT && !mReminderMethodValues.contains(method)) {
1878                // Stash unsupported reminder types separately so we don't alter
1879                // them in the UI
1880                mUnsupportedReminders.add(ReminderEntry.valueOf(minutes, method));
1881            } else {
1882                mOriginalReminders.add(ReminderEntry.valueOf(minutes, method));
1883            }
1884        }
1885        // Sort appropriately for display (by time, then type)
1886        Collections.sort(mOriginalReminders);
1887
1888        if (mUserModifiedReminders) {
1889            // If the user has changed the list of reminders don't change what's
1890            // shown.
1891            return;
1892        }
1893
1894        LinearLayout parent = (LinearLayout) mScrollView
1895                .findViewById(R.id.reminder_items_container);
1896        if (parent != null) {
1897            parent.removeAllViews();
1898        }
1899        if (mReminderViews != null) {
1900            mReminderViews.clear();
1901        }
1902
1903        if (mHasAlarm) {
1904            ArrayList<ReminderEntry> reminders = mOriginalReminders;
1905            // Insert any minute values that aren't represented in the minutes list.
1906            for (ReminderEntry re : reminders) {
1907                EventViewUtils.addMinutesToList(
1908                        mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes());
1909            }
1910            // Create a UI element for each reminder.  We display all of the reminders we get
1911            // from the provider, even if the count exceeds the calendar maximum.  (Also, for
1912            // a new event, we won't have a maxReminders value available.)
1913            for (ReminderEntry re : reminders) {
1914                EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
1915                        mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
1916                        mReminderMethodLabels, re, Integer.MAX_VALUE, mReminderChangeListener);
1917            }
1918            EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
1919            // TODO show unsupported reminder types in some fashion.
1920        }
1921    }
1922
1923    void updateResponse(View view) {
1924        // we only let the user accept/reject/etc. a meeting if:
1925        // a) you can edit the event's containing calendar AND
1926        // b) you're not the organizer and only attendee AND
1927        // c) organizerCanRespond is enabled for the calendar
1928        // (if the attendee data has been hidden, the visible number of attendees
1929        // will be 1 -- the calendar owner's).
1930        // (there are more cases involved to be 100% accurate, such as
1931        // paying attention to whether or not an attendee status was
1932        // included in the feed, but we're currently omitting those corner cases
1933        // for simplicity).
1934
1935        // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel.
1936        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
1937                (mIsOrganizer && !mOwnerCanRespond)) {
1938            setVisibilityCommon(view, R.id.response_container, View.GONE);
1939            return;
1940        }
1941
1942        setVisibilityCommon(view, R.id.response_container, View.VISIBLE);
1943
1944
1945        int response;
1946        if (mUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) {
1947            response = mUserSetResponse;
1948        } else if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) {
1949            response = mAttendeeResponseFromIntent;
1950        } else {
1951            response = mOriginalAttendeeResponse;
1952        }
1953
1954        int buttonToCheck = findButtonIdForResponse(response);
1955        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value);
1956        radioGroup.check(buttonToCheck); // -1 clear all radio buttons
1957        radioGroup.setOnCheckedChangeListener(this);
1958    }
1959
1960    private void setTextCommon(View view, int id, CharSequence text) {
1961        TextView textView = (TextView) view.findViewById(id);
1962        if (textView == null)
1963            return;
1964        textView.setText(text);
1965    }
1966
1967    private void setVisibilityCommon(View view, int id, int visibility) {
1968        View v = view.findViewById(id);
1969        if (v != null) {
1970            v.setVisibility(visibility);
1971        }
1972        return;
1973    }
1974
1975    /**
1976     * Taken from com.google.android.gm.HtmlConversationActivity
1977     *
1978     * Send the intent that shows the Contact info corresponding to the email address.
1979     */
1980    public void showContactInfo(Attendee attendee, Rect rect) {
1981        // First perform lookup query to find existing contact
1982        final ContentResolver resolver = getActivity().getContentResolver();
1983        final String address = attendee.mEmail;
1984        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
1985                Uri.encode(address));
1986        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
1987
1988        if (lookupUri != null) {
1989            // Found matching contact, trigger QuickContact
1990            QuickContact.showQuickContact(getActivity(), rect, lookupUri,
1991                    QuickContact.MODE_MEDIUM, null);
1992        } else {
1993            // No matching contact, ask user to create one
1994            final Uri mailUri = Uri.fromParts("mailto", address, null);
1995            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
1996
1997            // Pass along full E-mail string for possible create dialog
1998            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
1999            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
2000
2001            // Only provide personal name hint if we have one
2002            final String senderPersonal = attendee.mName;
2003            if (!TextUtils.isEmpty(senderPersonal)) {
2004                intent.putExtra(Intents.Insert.NAME, senderPersonal);
2005            }
2006
2007            startActivity(intent);
2008        }
2009    }
2010
2011    @Override
2012    public void onPause() {
2013        mIsPaused = true;
2014        mHandler.removeCallbacks(onDeleteRunnable);
2015        super.onPause();
2016        // Remove event deletion alert box since it is being rebuild in the OnResume
2017        // This is done to get the same behavior on OnResume since the AlertDialog is gone on
2018        // rotation but not if you press the HOME key
2019        if (mDeleteDialogVisible && mDeleteHelper != null) {
2020            mDeleteHelper.dismissAlertDialog();
2021            mDeleteHelper = null;
2022        }
2023    }
2024
2025    @Override
2026    public void onResume() {
2027        super.onResume();
2028        if (mIsDialog) {
2029            setDialogSize(getActivity().getResources());
2030            applyDialogParams();
2031        }
2032        mIsPaused = false;
2033        if (mDismissOnResume) {
2034            mHandler.post(onDeleteRunnable);
2035        }
2036        // Display the "delete confirmation" dialog if needed
2037        if (mDeleteDialogVisible) {
2038            mDeleteHelper = new DeleteEventHelper(
2039                    mContext, mActivity,
2040                    !mIsDialog && !mIsTabletConfig /* exitWhenDone */);
2041            mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener());
2042            mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable);
2043        }
2044    }
2045
2046    @Override
2047    public void eventsChanged() {
2048    }
2049
2050    @Override
2051    public long getSupportedEventTypes() {
2052        return EventType.EVENTS_CHANGED;
2053    }
2054
2055    @Override
2056    public void handleEvent(EventInfo event) {
2057        if (event.eventType == EventType.EVENTS_CHANGED && mHandler != null) {
2058            // reload the data
2059            reloadEvents();
2060        }
2061    }
2062
2063    public void reloadEvents() {
2064        mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION,
2065                null, null, null);
2066    }
2067
2068    @Override
2069    public void onClick(View view) {
2070
2071        // This must be a click on one of the "remove reminder" buttons
2072        LinearLayout reminderItem = (LinearLayout) view.getParent();
2073        LinearLayout parent = (LinearLayout) reminderItem.getParent();
2074        parent.removeView(reminderItem);
2075        mReminderViews.remove(reminderItem);
2076        mUserModifiedReminders = true;
2077        EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
2078    }
2079
2080
2081    /**
2082     * Add a new reminder when the user hits the "add reminder" button.  We use the default
2083     * reminder time and method.
2084     */
2085    private void addReminder() {
2086        // TODO: when adding a new reminder, make it different from the
2087        // last one in the list (if any).
2088        if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
2089            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
2090                    mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
2091                    mReminderMethodLabels,
2092                    ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), mMaxReminders,
2093                    mReminderChangeListener);
2094        } else {
2095            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews,
2096                    mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues,
2097                    mReminderMethodLabels, ReminderEntry.valueOf(mDefaultReminderMinutes),
2098                    mMaxReminders, mReminderChangeListener);
2099        }
2100
2101        EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders);
2102    }
2103
2104    synchronized private void prepareReminders() {
2105        // Nothing to do if we've already built these lists _and_ we aren't
2106        // removing not allowed methods
2107        if (mReminderMinuteValues != null && mReminderMinuteLabels != null
2108                && mReminderMethodValues != null && mReminderMethodLabels != null
2109                && mCalendarAllowedReminders == null) {
2110            return;
2111        }
2112        // Load the labels and corresponding numeric values for the minutes and methods lists
2113        // from the assets.  If we're switching calendars, we need to clear and re-populate the
2114        // lists (which may have elements added and removed based on calendar properties).  This
2115        // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
2116        // new event that aren't in the default set.
2117        Resources r = mActivity.getResources();
2118        mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
2119        mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
2120        mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
2121        mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
2122
2123        // Remove any reminder methods that aren't allowed for this calendar.  If this is
2124        // a new event, mCalendarAllowedReminders may not be set the first time we're called.
2125        if (mCalendarAllowedReminders != null) {
2126            EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
2127                    mCalendarAllowedReminders);
2128        }
2129        if (mView != null) {
2130            mView.invalidate();
2131        }
2132    }
2133
2134
2135    private boolean saveReminders() {
2136        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
2137
2138        // Read reminders from UI
2139        mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews,
2140                mReminderMinuteValues, mReminderMethodValues);
2141        mOriginalReminders.addAll(mUnsupportedReminders);
2142        Collections.sort(mOriginalReminders);
2143        mReminders.addAll(mUnsupportedReminders);
2144        Collections.sort(mReminders);
2145
2146        // Check if there are any changes in the reminder
2147        boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders,
2148                mOriginalReminders, false /* no force save */);
2149
2150        if (!changed) {
2151            return false;
2152        }
2153
2154        // save new reminders
2155        AsyncQueryService service = new AsyncQueryService(getActivity());
2156        service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0);
2157        // Update the "hasAlarm" field for the event
2158        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
2159        int len = mReminders.size();
2160        boolean hasAlarm = len > 0;
2161        if (hasAlarm != mHasAlarm) {
2162            ContentValues values = new ContentValues();
2163            values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
2164            service.startUpdate(0, null, uri, values, null, null, 0);
2165        }
2166        return true;
2167    }
2168
2169    /**
2170     * Email all the attendees of the event, except for the viewer (so as to not email
2171     * himself) and resources like conference rooms.
2172     */
2173    private void emailAttendees() {
2174        Intent i = new Intent(getActivity(), QuickResponseActivity.class);
2175        i.putExtra(QuickResponseActivity.EXTRA_EVENT_ID, mEventId);
2176        i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2177        startActivity(i);
2178    }
2179
2180    /**
2181     * Loads an integer array asset into a list.
2182     */
2183    private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
2184        int[] vals = r.getIntArray(resNum);
2185        int size = vals.length;
2186        ArrayList<Integer> list = new ArrayList<Integer>(size);
2187
2188        for (int i = 0; i < size; i++) {
2189            list.add(vals[i]);
2190        }
2191
2192        return list;
2193    }
2194    /**
2195     * Loads a String array asset into a list.
2196     */
2197    private static ArrayList<String> loadStringArray(Resources r, int resNum) {
2198        String[] labels = r.getStringArray(resNum);
2199        ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
2200        return list;
2201    }
2202
2203    public void onDeleteStarted() {
2204        mEventDeletionStarted = true;
2205    }
2206
2207    private Dialog.OnDismissListener createDeleteOnDismissListener() {
2208        return new Dialog.OnDismissListener() {
2209                    @Override
2210                    public void onDismiss(DialogInterface dialog) {
2211                        // Since OnPause will force the dialog to dismiss , do
2212                        // not change the dialog status
2213                        if (!mIsPaused) {
2214                            mDeleteDialogVisible = false;
2215                        }
2216                    }
2217                };
2218    }
2219
2220    public long getEventId() {
2221        return mEventId;
2222    }
2223
2224    public long getStartMillis() {
2225        return mStartMillis;
2226    }
2227    public long getEndMillis() {
2228        return mEndMillis;
2229    }
2230    private void setDialogSize(Resources r) {
2231        mDialogWidth = (int)r.getDimension(R.dimen.event_info_dialog_width);
2232        mDialogHeight = (int)r.getDimension(R.dimen.event_info_dialog_height);
2233    }
2234
2235
2236}
2237