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