|  | /* | 
|  | * Copyright (C) 2007 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package com.android.calendar; | 
|  |  | 
|  | import android.animation.Animator; | 
|  | import android.animation.AnimatorListenerAdapter; | 
|  | import android.animation.ObjectAnimator; | 
|  | import android.animation.ValueAnimator; | 
|  | import android.app.AlertDialog; | 
|  | import android.app.Service; | 
|  | import android.content.ContentResolver; | 
|  | import android.content.ContentUris; | 
|  | import android.content.Context; | 
|  | import android.content.DialogInterface; | 
|  | import android.content.res.Resources; | 
|  | import android.content.res.TypedArray; | 
|  | import android.database.Cursor; | 
|  | import android.graphics.Canvas; | 
|  | import android.graphics.Paint; | 
|  | import android.graphics.Paint.Align; | 
|  | import android.graphics.Paint.Style; | 
|  | import android.graphics.Rect; | 
|  | import android.graphics.Typeface; | 
|  | import android.graphics.drawable.Drawable; | 
|  | import android.net.Uri; | 
|  | import android.os.Handler; | 
|  | import android.provider.CalendarContract.Attendees; | 
|  | import android.provider.CalendarContract.Calendars; | 
|  | import android.provider.CalendarContract.Events; | 
|  | import android.text.Layout.Alignment; | 
|  | import android.text.SpannableStringBuilder; | 
|  | import android.text.StaticLayout; | 
|  | import android.text.TextPaint; | 
|  | import android.text.TextUtils; | 
|  | import android.text.format.DateFormat; | 
|  | import android.text.format.DateUtils; | 
|  | import android.text.format.Time; | 
|  | import android.text.style.StyleSpan; | 
|  | import android.util.Log; | 
|  | import android.view.ContextMenu; | 
|  | import android.view.ContextMenu.ContextMenuInfo; | 
|  | import android.view.GestureDetector; | 
|  | import android.view.Gravity; | 
|  | import android.view.KeyEvent; | 
|  | import android.view.LayoutInflater; | 
|  | import android.view.MenuItem; | 
|  | import android.view.MotionEvent; | 
|  | import android.view.ScaleGestureDetector; | 
|  | import android.view.View; | 
|  | import android.view.ViewConfiguration; | 
|  | import android.view.ViewGroup; | 
|  | import android.view.WindowManager; | 
|  | import android.view.accessibility.AccessibilityEvent; | 
|  | import android.view.accessibility.AccessibilityManager; | 
|  | import android.view.animation.AccelerateDecelerateInterpolator; | 
|  | import android.view.animation.Animation; | 
|  | import android.view.animation.Interpolator; | 
|  | import android.view.animation.TranslateAnimation; | 
|  | import android.widget.EdgeEffect; | 
|  | import android.widget.ImageView; | 
|  | import android.widget.OverScroller; | 
|  | import android.widget.PopupWindow; | 
|  | import android.widget.TextView; | 
|  | import android.widget.ViewSwitcher; | 
|  |  | 
|  | import com.android.calendar.CalendarController.EventType; | 
|  | import com.android.calendar.CalendarController.ViewType; | 
|  |  | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.Calendar; | 
|  | import java.util.Formatter; | 
|  | import java.util.Locale; | 
|  | import java.util.regex.Matcher; | 
|  | import java.util.regex.Pattern; | 
|  |  | 
|  | /** | 
|  | * View for multi-day view. So far only 1 and 7 day have been tested. | 
|  | */ | 
|  | public class DayView extends View implements View.OnCreateContextMenuListener, | 
|  | ScaleGestureDetector.OnScaleGestureListener, View.OnClickListener, View.OnLongClickListener | 
|  | { | 
|  | private static String TAG = "DayView"; | 
|  | private static boolean DEBUG = false; | 
|  | private static boolean DEBUG_SCALING = false; | 
|  | private static final String PERIOD_SPACE = ". "; | 
|  |  | 
|  | private static float mScale = 0; // Used for supporting different screen densities | 
|  | private static final long INVALID_EVENT_ID = -1; //This is used for remembering a null event | 
|  | // Duration of the allday expansion | 
|  | private static final long ANIMATION_DURATION = 400; | 
|  | // duration of the more allday event text fade | 
|  | private static final long ANIMATION_SECONDARY_DURATION = 200; | 
|  | // duration of the scroll to go to a specified time | 
|  | private static final int GOTO_SCROLL_DURATION = 200; | 
|  | // duration for events' cross-fade animation | 
|  | private static final int EVENTS_CROSS_FADE_DURATION = 400; | 
|  | // duration to show the event clicked | 
|  | private static final int CLICK_DISPLAY_DURATION = 50; | 
|  |  | 
|  | private static final int MENU_AGENDA = 2; | 
|  | private static final int MENU_DAY = 3; | 
|  | private static final int MENU_EVENT_VIEW = 5; | 
|  | private static final int MENU_EVENT_CREATE = 6; | 
|  | private static final int MENU_EVENT_EDIT = 7; | 
|  | private static final int MENU_EVENT_DELETE = 8; | 
|  |  | 
|  | private static int DEFAULT_CELL_HEIGHT = 64; | 
|  | private static int MAX_CELL_HEIGHT = 150; | 
|  | private static int MIN_Y_SPAN = 100; | 
|  |  | 
|  | private boolean mOnFlingCalled; | 
|  | private boolean mStartingScroll = false; | 
|  | protected boolean mPaused = true; | 
|  | private Handler mHandler; | 
|  | /** | 
|  | * ID of the last event which was displayed with the toast popup. | 
|  | * | 
|  | * This is used to prevent popping up multiple quick views for the same event, especially | 
|  | * during calendar syncs. This becomes valid when an event is selected, either by default | 
|  | * on starting calendar or by scrolling to an event. It becomes invalid when the user | 
|  | * explicitly scrolls to an empty time slot, changes views, or deletes the event. | 
|  | */ | 
|  | private long mLastPopupEventID; | 
|  |  | 
|  | protected Context mContext; | 
|  |  | 
|  | private static final String[] CALENDARS_PROJECTION = new String[] { | 
|  | Calendars._ID,          // 0 | 
|  | Calendars.CALENDAR_ACCESS_LEVEL, // 1 | 
|  | Calendars.OWNER_ACCOUNT, // 2 | 
|  | }; | 
|  | private static final int CALENDARS_INDEX_ACCESS_LEVEL = 1; | 
|  | private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; | 
|  | private static final String CALENDARS_WHERE = Calendars._ID + "=%d"; | 
|  |  | 
|  | private static final int FROM_NONE = 0; | 
|  | private static final int FROM_ABOVE = 1; | 
|  | private static final int FROM_BELOW = 2; | 
|  | private static final int FROM_LEFT = 4; | 
|  | private static final int FROM_RIGHT = 8; | 
|  |  | 
|  | private static final int ACCESS_LEVEL_NONE = 0; | 
|  | private static final int ACCESS_LEVEL_DELETE = 1; | 
|  | private static final int ACCESS_LEVEL_EDIT = 2; | 
|  |  | 
|  | private static int mHorizontalSnapBackThreshold = 128; | 
|  |  | 
|  | private final ContinueScroll mContinueScroll = new ContinueScroll(); | 
|  |  | 
|  | // Make this visible within the package for more informative debugging | 
|  | Time mBaseDate; | 
|  | private Time mCurrentTime; | 
|  | //Update the current time line every five minutes if the window is left open that long | 
|  | private static final int UPDATE_CURRENT_TIME_DELAY = 300000; | 
|  | private final UpdateCurrentTime mUpdateCurrentTime = new UpdateCurrentTime(); | 
|  | private int mTodayJulianDay; | 
|  |  | 
|  | private final Typeface mBold = Typeface.DEFAULT_BOLD; | 
|  | private int mFirstJulianDay; | 
|  | private int mLoadedFirstJulianDay = -1; | 
|  | private int mLastJulianDay; | 
|  |  | 
|  | private int mMonthLength; | 
|  | private int mFirstVisibleDate; | 
|  | private int mFirstVisibleDayOfWeek; | 
|  | private int[] mEarliestStartHour;    // indexed by the week day offset | 
|  | private boolean[] mHasAllDayEvent;   // indexed by the week day offset | 
|  | private String mEventCountTemplate; | 
|  | private final CharSequence[] mLongPressItems; | 
|  | private String mLongPressTitle; | 
|  | private Event mClickedEvent;           // The event the user clicked on | 
|  | private Event mSavedClickedEvent; | 
|  | private static int mOnDownDelay; | 
|  | private int mClickedYLocation; | 
|  | private long mDownTouchTime; | 
|  |  | 
|  | private int mEventsAlpha = 255; | 
|  | private ObjectAnimator mEventsCrossFadeAnimation; | 
|  |  | 
|  | protected static StringBuilder mStringBuilder = new StringBuilder(50); | 
|  | // TODO recreate formatter when locale changes | 
|  | protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); | 
|  |  | 
|  | private final Runnable mTZUpdater = new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | String tz = Utils.getTimeZone(mContext, this); | 
|  | mBaseDate.timezone = tz; | 
|  | mBaseDate.normalize(true); | 
|  | mCurrentTime.switchTimezone(tz); | 
|  | invalidate(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | // Sets the "clicked" color from the clicked event | 
|  | private final Runnable mSetClick = new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | mClickedEvent = mSavedClickedEvent; | 
|  | mSavedClickedEvent = null; | 
|  | DayView.this.invalidate(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | // Clears the "clicked" color from the clicked event and launch the event | 
|  | private final Runnable mClearClick = new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | if (mClickedEvent != null) { | 
|  | mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, mClickedEvent.id, | 
|  | mClickedEvent.startMillis, mClickedEvent.endMillis, | 
|  | DayView.this.getWidth() / 2, mClickedYLocation, | 
|  | getSelectedTimeInMillis()); | 
|  | } | 
|  | mClickedEvent = null; | 
|  | DayView.this.invalidate(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | private final TodayAnimatorListener mTodayAnimatorListener = new TodayAnimatorListener(); | 
|  |  | 
|  | class TodayAnimatorListener extends AnimatorListenerAdapter { | 
|  | private volatile Animator mAnimator = null; | 
|  | private volatile boolean mFadingIn = false; | 
|  |  | 
|  | @Override | 
|  | public void onAnimationEnd(Animator animation) { | 
|  | synchronized (this) { | 
|  | if (mAnimator != animation) { | 
|  | animation.removeAllListeners(); | 
|  | animation.cancel(); | 
|  | return; | 
|  | } | 
|  | if (mFadingIn) { | 
|  | if (mTodayAnimator != null) { | 
|  | mTodayAnimator.removeAllListeners(); | 
|  | mTodayAnimator.cancel(); | 
|  | } | 
|  | mTodayAnimator = ObjectAnimator | 
|  | .ofInt(DayView.this, "animateTodayAlpha", 255, 0); | 
|  | mAnimator = mTodayAnimator; | 
|  | mFadingIn = false; | 
|  | mTodayAnimator.addListener(this); | 
|  | mTodayAnimator.setDuration(600); | 
|  | mTodayAnimator.start(); | 
|  | } else { | 
|  | mAnimateToday = false; | 
|  | mAnimateTodayAlpha = 0; | 
|  | mAnimator.removeAllListeners(); | 
|  | mAnimator = null; | 
|  | mTodayAnimator = null; | 
|  | invalidate(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public void setAnimator(Animator animation) { | 
|  | mAnimator = animation; | 
|  | } | 
|  |  | 
|  | public void setFadingIn(boolean fadingIn) { | 
|  | mFadingIn = fadingIn; | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { | 
|  | @Override | 
|  | public void onAnimationStart(Animator animation) { | 
|  | mScrolling = true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onAnimationCancel(Animator animation) { | 
|  | mScrolling = false; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onAnimationEnd(Animator animation) { | 
|  | mScrolling = false; | 
|  | resetSelectedHour(); | 
|  | invalidate(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * This variable helps to avoid unnecessarily reloading events by keeping | 
|  | * track of the start millis parameter used for the most recent loading | 
|  | * of events.  If the next reload matches this, then the events are not | 
|  | * reloaded.  To force a reload, set this to zero (this is set to zero | 
|  | * in the method clearCachedEvents()). | 
|  | */ | 
|  | private long mLastReloadMillis; | 
|  |  | 
|  | private ArrayList<Event> mEvents = new ArrayList<Event>(); | 
|  | private ArrayList<Event> mAllDayEvents = new ArrayList<Event>(); | 
|  | private StaticLayout[] mLayouts = null; | 
|  | private StaticLayout[] mAllDayLayouts = null; | 
|  | private int mSelectionDay;        // Julian day | 
|  | private int mSelectionHour; | 
|  |  | 
|  | boolean mSelectionAllday; | 
|  |  | 
|  | // Current selection info for accessibility | 
|  | private int mSelectionDayForAccessibility;        // Julian day | 
|  | private int mSelectionHourForAccessibility; | 
|  | private Event mSelectedEventForAccessibility; | 
|  | // Last selection info for accessibility | 
|  | private int mLastSelectionDayForAccessibility; | 
|  | private int mLastSelectionHourForAccessibility; | 
|  | private Event mLastSelectedEventForAccessibility; | 
|  |  | 
|  |  | 
|  | /** Width of a day or non-conflicting event */ | 
|  | private int mCellWidth; | 
|  |  | 
|  | // Pre-allocate these objects and re-use them | 
|  | private final Rect mRect = new Rect(); | 
|  | private final Rect mDestRect = new Rect(); | 
|  | private final Rect mSelectionRect = new Rect(); | 
|  | // This encloses the more allDay events icon | 
|  | private final Rect mExpandAllDayRect = new Rect(); | 
|  | // TODO Clean up paint usage | 
|  | private final Paint mPaint = new Paint(); | 
|  | private final Paint mEventTextPaint = new Paint(); | 
|  | private final Paint mSelectionPaint = new Paint(); | 
|  | private float[] mLines; | 
|  |  | 
|  | private int mFirstDayOfWeek; // First day of the week | 
|  |  | 
|  | private PopupWindow mPopup; | 
|  | private View mPopupView; | 
|  |  | 
|  | // The number of milliseconds to show the popup window | 
|  | private static final int POPUP_DISMISS_DELAY = 3000; | 
|  | private final DismissPopup mDismissPopup = new DismissPopup(); | 
|  |  | 
|  | private boolean mRemeasure = true; | 
|  |  | 
|  | private final EventLoader mEventLoader; | 
|  | protected final EventGeometry mEventGeometry; | 
|  |  | 
|  | private static float GRID_LINE_LEFT_MARGIN = 0; | 
|  | private static final float GRID_LINE_INNER_WIDTH = 1; | 
|  |  | 
|  | private static final int DAY_GAP = 1; | 
|  | private static final int HOUR_GAP = 1; | 
|  | // This is the standard height of an allday event with no restrictions | 
|  | private static int SINGLE_ALLDAY_HEIGHT = 34; | 
|  | /** | 
|  | * This is the minimum desired height of a allday event. | 
|  | * When unexpanded, allday events will use this height. | 
|  | * When expanded allDay events will attempt to grow to fit all | 
|  | * events at this height. | 
|  | */ | 
|  | private static float MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = 28.0F; // in pixels | 
|  | /** | 
|  | * This is how big the unexpanded allday height is allowed to be. | 
|  | * It will get adjusted based on screen size | 
|  | */ | 
|  | private static int MAX_UNEXPANDED_ALLDAY_HEIGHT = | 
|  | (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); | 
|  | /** | 
|  | * This is the minimum size reserved for displaying regular events. | 
|  | * The expanded allDay region can't expand into this. | 
|  | */ | 
|  | private static int MIN_HOURS_HEIGHT = 180; | 
|  | private static int ALLDAY_TOP_MARGIN = 1; | 
|  | // The largest a single allDay event will become. | 
|  | private static int MAX_HEIGHT_OF_ONE_ALLDAY_EVENT = 34; | 
|  |  | 
|  | private static int HOURS_TOP_MARGIN = 2; | 
|  | private static int HOURS_LEFT_MARGIN = 2; | 
|  | private static int HOURS_RIGHT_MARGIN = 4; | 
|  | private static int HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; | 
|  | private static int NEW_EVENT_MARGIN = 4; | 
|  | private static int NEW_EVENT_WIDTH = 2; | 
|  | private static int NEW_EVENT_MAX_LENGTH = 16; | 
|  |  | 
|  | private static int CURRENT_TIME_LINE_SIDE_BUFFER = 4; | 
|  | private static int CURRENT_TIME_LINE_TOP_OFFSET = 2; | 
|  |  | 
|  | /* package */ static final int MINUTES_PER_HOUR = 60; | 
|  | /* package */ static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * 24; | 
|  | /* package */ static final int MILLIS_PER_MINUTE = 60 * 1000; | 
|  | /* package */ static final int MILLIS_PER_HOUR = (3600 * 1000); | 
|  | /* package */ static final int MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; | 
|  |  | 
|  | // More events text will transition between invisible and this alpha | 
|  | private static final int MORE_EVENTS_MAX_ALPHA = 0x4C; | 
|  | private static int DAY_HEADER_ONE_DAY_LEFT_MARGIN = 0; | 
|  | private static int DAY_HEADER_ONE_DAY_RIGHT_MARGIN = 5; | 
|  | private static int DAY_HEADER_ONE_DAY_BOTTOM_MARGIN = 6; | 
|  | private static int DAY_HEADER_RIGHT_MARGIN = 4; | 
|  | private static int DAY_HEADER_BOTTOM_MARGIN = 3; | 
|  | private static float DAY_HEADER_FONT_SIZE = 14; | 
|  | private static float DATE_HEADER_FONT_SIZE = 32; | 
|  | private static float NORMAL_FONT_SIZE = 12; | 
|  | private static float EVENT_TEXT_FONT_SIZE = 12; | 
|  | private static float HOURS_TEXT_SIZE = 12; | 
|  | private static float AMPM_TEXT_SIZE = 9; | 
|  | private static int MIN_HOURS_WIDTH = 96; | 
|  | private static int MIN_CELL_WIDTH_FOR_TEXT = 20; | 
|  | private static final int MAX_EVENT_TEXT_LEN = 500; | 
|  | // smallest height to draw an event with | 
|  | private static float MIN_EVENT_HEIGHT = 24.0F; // in pixels | 
|  | private static int CALENDAR_COLOR_SQUARE_SIZE = 10; | 
|  | private static int EVENT_RECT_TOP_MARGIN = 1; | 
|  | private static int EVENT_RECT_BOTTOM_MARGIN = 0; | 
|  | private static int EVENT_RECT_LEFT_MARGIN = 1; | 
|  | private static int EVENT_RECT_RIGHT_MARGIN = 0; | 
|  | private static int EVENT_RECT_STROKE_WIDTH = 2; | 
|  | private static int EVENT_TEXT_TOP_MARGIN = 2; | 
|  | private static int EVENT_TEXT_BOTTOM_MARGIN = 2; | 
|  | private static int EVENT_TEXT_LEFT_MARGIN = 6; | 
|  | private static int EVENT_TEXT_RIGHT_MARGIN = 6; | 
|  | private static int ALL_DAY_EVENT_RECT_BOTTOM_MARGIN = 1; | 
|  | private static int EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; | 
|  | private static int EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_BOTTOM_MARGIN; | 
|  | private static int EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; | 
|  | private static int EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_RIGHT_MARGIN; | 
|  | // margins and sizing for the expand allday icon | 
|  | private static int EXPAND_ALL_DAY_BOTTOM_MARGIN = 10; | 
|  | // sizing for "box +n" in allDay events | 
|  | private static int EVENT_SQUARE_WIDTH = 10; | 
|  | private static int EVENT_LINE_PADDING = 4; | 
|  | private static int NEW_EVENT_HINT_FONT_SIZE = 12; | 
|  |  | 
|  | private static int mPressedColor; | 
|  | private static int mClickedColor; | 
|  | private static int mEventTextColor; | 
|  | private static int mMoreEventsTextColor; | 
|  |  | 
|  | private static int mWeek_saturdayColor; | 
|  | private static int mWeek_sundayColor; | 
|  | private static int mCalendarDateBannerTextColor; | 
|  | private static int mCalendarAmPmLabel; | 
|  | private static int mCalendarGridAreaSelected; | 
|  | private static int mCalendarGridLineInnerHorizontalColor; | 
|  | private static int mCalendarGridLineInnerVerticalColor; | 
|  | private static int mFutureBgColor; | 
|  | private static int mFutureBgColorRes; | 
|  | private static int mBgColor; | 
|  | private static int mNewEventHintColor; | 
|  | private static int mCalendarHourLabelColor; | 
|  | private static int mMoreAlldayEventsTextAlpha = MORE_EVENTS_MAX_ALPHA; | 
|  |  | 
|  | private float mAnimationDistance = 0; | 
|  | private int mViewStartX; | 
|  | private int mViewStartY; | 
|  | private int mMaxViewStartY; | 
|  | private int mViewHeight; | 
|  | private int mViewWidth; | 
|  | private int mGridAreaHeight = -1; | 
|  | private static int mCellHeight = 0; // shared among all DayViews | 
|  | private static int mMinCellHeight = 32; | 
|  | private int mScrollStartY; | 
|  | private int mPreviousDirection; | 
|  |  | 
|  | /** | 
|  | * Vertical distance or span between the two touch points at the start of a | 
|  | * scaling gesture | 
|  | */ | 
|  | private float mStartingSpanY = 0; | 
|  | /** Height of 1 hour in pixels at the start of a scaling gesture */ | 
|  | private int mCellHeightBeforeScaleGesture; | 
|  | /** The hour at the center two touch points */ | 
|  | private float mGestureCenterHour = 0; | 
|  |  | 
|  | private boolean mRecalCenterHour = false; | 
|  |  | 
|  | /** | 
|  | * Flag to decide whether to handle the up event. Cases where up events | 
|  | * should be ignored are 1) right after a scale gesture and 2) finger was | 
|  | * down before app launch | 
|  | */ | 
|  | private boolean mHandleActionUp = true; | 
|  |  | 
|  | private int mHoursTextHeight; | 
|  | /** | 
|  | * The height of the area used for allday events | 
|  | */ | 
|  | private int mAlldayHeight; | 
|  | /** | 
|  | * The height of the allday event area used during animation | 
|  | */ | 
|  | private int mAnimateDayHeight = 0; | 
|  | /** | 
|  | * The height of an individual allday event during animation | 
|  | */ | 
|  | private int mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; | 
|  | /** | 
|  | * Whether to use the expand or collapse icon. | 
|  | */ | 
|  | private static boolean mUseExpandIcon = true; | 
|  | /** | 
|  | * The height of the day names/numbers | 
|  | */ | 
|  | private static int DAY_HEADER_HEIGHT = 45; | 
|  | /** | 
|  | * The height of the day names/numbers for multi-day views | 
|  | */ | 
|  | private static int MULTI_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; | 
|  | /** | 
|  | * The height of the day names/numbers when viewing a single day | 
|  | */ | 
|  | private static int ONE_DAY_HEADER_HEIGHT = DAY_HEADER_HEIGHT; | 
|  | /** | 
|  | * Max of all day events in a given day in this view. | 
|  | */ | 
|  | private int mMaxAlldayEvents; | 
|  | /** | 
|  | * A count of the number of allday events that were not drawn for each day | 
|  | */ | 
|  | private int[] mSkippedAlldayEvents; | 
|  | /** | 
|  | * The number of allDay events at which point we start hiding allDay events. | 
|  | */ | 
|  | private int mMaxUnexpandedAlldayEventCount = 4; | 
|  | /** | 
|  | * Whether or not to expand the allDay area to fill the screen | 
|  | */ | 
|  | private static boolean mShowAllAllDayEvents = false; | 
|  |  | 
|  | protected int mNumDays = 7; | 
|  | private int mNumHours = 10; | 
|  |  | 
|  | /** Width of the time line (list of hours) to the left. */ | 
|  | private int mHoursWidth; | 
|  | private int mDateStrWidth; | 
|  | /** Top of the scrollable region i.e. below date labels and all day events */ | 
|  | private int mFirstCell; | 
|  | /** First fully visibile hour */ | 
|  | private int mFirstHour = -1; | 
|  | /** Distance between the mFirstCell and the top of first fully visible hour. */ | 
|  | private int mFirstHourOffset; | 
|  | private String[] mHourStrs; | 
|  | private String[] mDayStrs; | 
|  | private String[] mDayStrs2Letter; | 
|  | private boolean mIs24HourFormat; | 
|  |  | 
|  | private final ArrayList<Event> mSelectedEvents = new ArrayList<Event>(); | 
|  | private boolean mComputeSelectedEvents; | 
|  | private boolean mUpdateToast; | 
|  | private Event mSelectedEvent; | 
|  | private Event mPrevSelectedEvent; | 
|  | private final Rect mPrevBox = new Rect(); | 
|  | protected final Resources mResources; | 
|  | protected final Drawable mCurrentTimeLine; | 
|  | protected final Drawable mCurrentTimeAnimateLine; | 
|  | protected final Drawable mTodayHeaderDrawable; | 
|  | protected final Drawable mExpandAlldayDrawable; | 
|  | protected final Drawable mCollapseAlldayDrawable; | 
|  | protected Drawable mAcceptedOrTentativeEventBoxDrawable; | 
|  | private String mAmString; | 
|  | private String mPmString; | 
|  | private final DeleteEventHelper mDeleteEventHelper; | 
|  | private static int sCounter = 0; | 
|  |  | 
|  | private final ContextMenuHandler mContextMenuHandler = new ContextMenuHandler(); | 
|  |  | 
|  | ScaleGestureDetector mScaleGestureDetector; | 
|  |  | 
|  | /** | 
|  | * The initial state of the touch mode when we enter this view. | 
|  | */ | 
|  | private static final int TOUCH_MODE_INITIAL_STATE = 0; | 
|  |  | 
|  | /** | 
|  | * Indicates we just received the touch event and we are waiting to see if | 
|  | * it is a tap or a scroll gesture. | 
|  | */ | 
|  | private static final int TOUCH_MODE_DOWN = 1; | 
|  |  | 
|  | /** | 
|  | * Indicates the touch gesture is a vertical scroll | 
|  | */ | 
|  | private static final int TOUCH_MODE_VSCROLL = 0x20; | 
|  |  | 
|  | /** | 
|  | * Indicates the touch gesture is a horizontal scroll | 
|  | */ | 
|  | private static final int TOUCH_MODE_HSCROLL = 0x40; | 
|  |  | 
|  | private int mTouchMode = TOUCH_MODE_INITIAL_STATE; | 
|  |  | 
|  | /** | 
|  | * The selection modes are HIDDEN, PRESSED, SELECTED, and LONGPRESS. | 
|  | */ | 
|  | private static final int SELECTION_HIDDEN = 0; | 
|  | private static final int SELECTION_PRESSED = 1; // D-pad down but not up yet | 
|  | private static final int SELECTION_SELECTED = 2; | 
|  | private static final int SELECTION_LONGPRESS = 3; | 
|  |  | 
|  | private int mSelectionMode = SELECTION_HIDDEN; | 
|  |  | 
|  | private boolean mScrolling = false; | 
|  |  | 
|  | // Pixels scrolled | 
|  | private float mInitialScrollX; | 
|  | private float mInitialScrollY; | 
|  |  | 
|  | private boolean mAnimateToday = false; | 
|  | private int mAnimateTodayAlpha = 0; | 
|  |  | 
|  | // Animates the height of the allday region | 
|  | ObjectAnimator mAlldayAnimator; | 
|  | // Animates the height of events in the allday region | 
|  | ObjectAnimator mAlldayEventAnimator; | 
|  | // Animates the transparency of the more events text | 
|  | ObjectAnimator mMoreAlldayEventsAnimator; | 
|  | // Animates the current time marker when Today is pressed | 
|  | ObjectAnimator mTodayAnimator; | 
|  | // whether or not an event is stopping because it was cancelled | 
|  | private boolean mCancellingAnimations = false; | 
|  | // tracks whether a touch originated in the allday area | 
|  | private boolean mTouchStartedInAlldayArea = false; | 
|  |  | 
|  | private final CalendarController mController; | 
|  | private final ViewSwitcher mViewSwitcher; | 
|  | private final GestureDetector mGestureDetector; | 
|  | private final OverScroller mScroller; | 
|  | private final EdgeEffect mEdgeEffectTop; | 
|  | private final EdgeEffect mEdgeEffectBottom; | 
|  | private boolean mCallEdgeEffectOnAbsorb; | 
|  | private final int OVERFLING_DISTANCE; | 
|  | private float mLastVelocity; | 
|  |  | 
|  | private final ScrollInterpolator mHScrollInterpolator; | 
|  | private AccessibilityManager mAccessibilityMgr = null; | 
|  | private boolean mIsAccessibilityEnabled = false; | 
|  | private boolean mTouchExplorationEnabled = false; | 
|  | private final String mCreateNewEventString; | 
|  | private final String mNewEventHintString; | 
|  |  | 
|  | public DayView(Context context, CalendarController controller, | 
|  | ViewSwitcher viewSwitcher, EventLoader eventLoader, int numDays) { | 
|  | super(context); | 
|  | mContext = context; | 
|  | initAccessibilityVariables(); | 
|  |  | 
|  | mResources = context.getResources(); | 
|  | mCreateNewEventString = mResources.getString(R.string.event_create); | 
|  | mNewEventHintString = mResources.getString(R.string.day_view_new_event_hint); | 
|  | mNumDays = numDays; | 
|  |  | 
|  | DATE_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.date_header_text_size); | 
|  | DAY_HEADER_FONT_SIZE = (int) mResources.getDimension(R.dimen.day_label_text_size); | 
|  | ONE_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.one_day_header_height); | 
|  | DAY_HEADER_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.day_header_bottom_margin); | 
|  | EXPAND_ALL_DAY_BOTTOM_MARGIN = (int) mResources.getDimension(R.dimen.all_day_bottom_margin); | 
|  | HOURS_TEXT_SIZE = (int) mResources.getDimension(R.dimen.hours_text_size); | 
|  | AMPM_TEXT_SIZE = (int) mResources.getDimension(R.dimen.ampm_text_size); | 
|  | MIN_HOURS_WIDTH = (int) mResources.getDimension(R.dimen.min_hours_width); | 
|  | HOURS_LEFT_MARGIN = (int) mResources.getDimension(R.dimen.hours_left_margin); | 
|  | HOURS_RIGHT_MARGIN = (int) mResources.getDimension(R.dimen.hours_right_margin); | 
|  | MULTI_DAY_HEADER_HEIGHT = (int) mResources.getDimension(R.dimen.day_header_height); | 
|  | int eventTextSizeId; | 
|  | if (mNumDays == 1) { | 
|  | eventTextSizeId = R.dimen.day_view_event_text_size; | 
|  | } else { | 
|  | eventTextSizeId = R.dimen.week_view_event_text_size; | 
|  | } | 
|  | EVENT_TEXT_FONT_SIZE = (int) mResources.getDimension(eventTextSizeId); | 
|  | NEW_EVENT_HINT_FONT_SIZE = (int) mResources.getDimension(R.dimen.new_event_hint_text_size); | 
|  | MIN_EVENT_HEIGHT = mResources.getDimension(R.dimen.event_min_height); | 
|  | MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT = MIN_EVENT_HEIGHT; | 
|  | EVENT_TEXT_TOP_MARGIN = (int) mResources.getDimension(R.dimen.event_text_vertical_margin); | 
|  | EVENT_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; | 
|  | EVENT_ALL_DAY_TEXT_TOP_MARGIN = EVENT_TEXT_TOP_MARGIN; | 
|  | EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN = EVENT_TEXT_TOP_MARGIN; | 
|  |  | 
|  | EVENT_TEXT_LEFT_MARGIN = (int) mResources | 
|  | .getDimension(R.dimen.event_text_horizontal_margin); | 
|  | EVENT_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; | 
|  | EVENT_ALL_DAY_TEXT_LEFT_MARGIN = EVENT_TEXT_LEFT_MARGIN; | 
|  | EVENT_ALL_DAY_TEXT_RIGHT_MARGIN = EVENT_TEXT_LEFT_MARGIN; | 
|  |  | 
|  | if (mScale == 0) { | 
|  |  | 
|  | mScale = mResources.getDisplayMetrics().density; | 
|  | if (mScale != 1) { | 
|  | SINGLE_ALLDAY_HEIGHT *= mScale; | 
|  | ALLDAY_TOP_MARGIN *= mScale; | 
|  | MAX_HEIGHT_OF_ONE_ALLDAY_EVENT *= mScale; | 
|  |  | 
|  | NORMAL_FONT_SIZE *= mScale; | 
|  | GRID_LINE_LEFT_MARGIN *= mScale; | 
|  | HOURS_TOP_MARGIN *= mScale; | 
|  | MIN_CELL_WIDTH_FOR_TEXT *= mScale; | 
|  | MAX_UNEXPANDED_ALLDAY_HEIGHT *= mScale; | 
|  | mAnimateDayEventHeight = (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; | 
|  |  | 
|  | CURRENT_TIME_LINE_SIDE_BUFFER *= mScale; | 
|  | CURRENT_TIME_LINE_TOP_OFFSET *= mScale; | 
|  |  | 
|  | MIN_Y_SPAN *= mScale; | 
|  | MAX_CELL_HEIGHT *= mScale; | 
|  | DEFAULT_CELL_HEIGHT *= mScale; | 
|  | DAY_HEADER_HEIGHT *= mScale; | 
|  | DAY_HEADER_RIGHT_MARGIN *= mScale; | 
|  | DAY_HEADER_ONE_DAY_LEFT_MARGIN *= mScale; | 
|  | DAY_HEADER_ONE_DAY_RIGHT_MARGIN *= mScale; | 
|  | DAY_HEADER_ONE_DAY_BOTTOM_MARGIN *= mScale; | 
|  | CALENDAR_COLOR_SQUARE_SIZE *= mScale; | 
|  | EVENT_RECT_TOP_MARGIN *= mScale; | 
|  | EVENT_RECT_BOTTOM_MARGIN *= mScale; | 
|  | ALL_DAY_EVENT_RECT_BOTTOM_MARGIN *= mScale; | 
|  | EVENT_RECT_LEFT_MARGIN *= mScale; | 
|  | EVENT_RECT_RIGHT_MARGIN *= mScale; | 
|  | EVENT_RECT_STROKE_WIDTH *= mScale; | 
|  | EVENT_SQUARE_WIDTH *= mScale; | 
|  | EVENT_LINE_PADDING *= mScale; | 
|  | NEW_EVENT_MARGIN *= mScale; | 
|  | NEW_EVENT_WIDTH *= mScale; | 
|  | NEW_EVENT_MAX_LENGTH *= mScale; | 
|  | } | 
|  | } | 
|  | HOURS_MARGIN = HOURS_LEFT_MARGIN + HOURS_RIGHT_MARGIN; | 
|  | DAY_HEADER_HEIGHT = mNumDays == 1 ? ONE_DAY_HEADER_HEIGHT : MULTI_DAY_HEADER_HEIGHT; | 
|  |  | 
|  | mCurrentTimeLine = mResources.getDrawable(R.drawable.timeline_indicator_holo_light); | 
|  | mCurrentTimeAnimateLine = mResources | 
|  | .getDrawable(R.drawable.timeline_indicator_activated_holo_light); | 
|  | mTodayHeaderDrawable = mResources.getDrawable(R.drawable.today_blue_week_holo_light); | 
|  | mExpandAlldayDrawable = mResources.getDrawable(R.drawable.ic_expand_holo_light); | 
|  | mCollapseAlldayDrawable = mResources.getDrawable(R.drawable.ic_collapse_holo_light); | 
|  | mNewEventHintColor =  mResources.getColor(R.color.new_event_hint_text_color); | 
|  | mAcceptedOrTentativeEventBoxDrawable = mResources | 
|  | .getDrawable(R.drawable.panel_month_event_holo_light); | 
|  |  | 
|  | mEventLoader = eventLoader; | 
|  | mEventGeometry = new EventGeometry(); | 
|  | mEventGeometry.setMinEventHeight(MIN_EVENT_HEIGHT); | 
|  | mEventGeometry.setHourGap(HOUR_GAP); | 
|  | mEventGeometry.setCellMargin(DAY_GAP); | 
|  | mLongPressItems = new CharSequence[] { | 
|  | mResources.getString(R.string.new_event_dialog_option) | 
|  | }; | 
|  | mLongPressTitle = mResources.getString(R.string.new_event_dialog_label); | 
|  | mDeleteEventHelper = new DeleteEventHelper(context, null, false /* don't exit when done */); | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | mController = controller; | 
|  | mViewSwitcher = viewSwitcher; | 
|  | mGestureDetector = new GestureDetector(context, new CalendarGestureListener()); | 
|  | mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); | 
|  | if (mCellHeight == 0) { | 
|  | mCellHeight = Utils.getSharedPreference(mContext, | 
|  | GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, DEFAULT_CELL_HEIGHT); | 
|  | } | 
|  | mScroller = new OverScroller(context); | 
|  | mHScrollInterpolator = new ScrollInterpolator(); | 
|  | mEdgeEffectTop = new EdgeEffect(context); | 
|  | mEdgeEffectBottom = new EdgeEffect(context); | 
|  | ViewConfiguration vc = ViewConfiguration.get(context); | 
|  | mOnDownDelay = ViewConfiguration.getTapTimeout(); | 
|  | OVERFLING_DISTANCE = vc.getScaledOverflingDistance(); | 
|  |  | 
|  | init(context); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onAttachedToWindow() { | 
|  | if (mHandler == null) { | 
|  | mHandler = getHandler(); | 
|  | mHandler.post(mUpdateCurrentTime); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void init(Context context) { | 
|  | setFocusable(true); | 
|  |  | 
|  | // Allow focus in touch mode so that we can do keyboard shortcuts | 
|  | // even after we've entered touch mode. | 
|  | setFocusableInTouchMode(true); | 
|  | setClickable(true); | 
|  | setOnCreateContextMenuListener(this); | 
|  |  | 
|  | mFirstDayOfWeek = Utils.getFirstDayOfWeek(context); | 
|  |  | 
|  | mCurrentTime = new Time(Utils.getTimeZone(context, mTZUpdater)); | 
|  | long currentTime = System.currentTimeMillis(); | 
|  | mCurrentTime.set(currentTime); | 
|  | mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); | 
|  |  | 
|  | mWeek_saturdayColor = mResources.getColor(R.color.week_saturday); | 
|  | mWeek_sundayColor = mResources.getColor(R.color.week_sunday); | 
|  | mCalendarDateBannerTextColor = mResources.getColor(R.color.calendar_date_banner_text_color); | 
|  | mFutureBgColorRes = mResources.getColor(R.color.calendar_future_bg_color); | 
|  | mBgColor = mResources.getColor(R.color.calendar_hour_background); | 
|  | mCalendarAmPmLabel = mResources.getColor(R.color.calendar_ampm_label); | 
|  | mCalendarGridAreaSelected = mResources.getColor(R.color.calendar_grid_area_selected); | 
|  | mCalendarGridLineInnerHorizontalColor = mResources | 
|  | .getColor(R.color.calendar_grid_line_inner_horizontal_color); | 
|  | mCalendarGridLineInnerVerticalColor = mResources | 
|  | .getColor(R.color.calendar_grid_line_inner_vertical_color); | 
|  | mCalendarHourLabelColor = mResources.getColor(R.color.calendar_hour_label); | 
|  | mPressedColor = mResources.getColor(R.color.pressed); | 
|  | mClickedColor = mResources.getColor(R.color.day_event_clicked_background_color); | 
|  | mEventTextColor = mResources.getColor(R.color.calendar_event_text_color); | 
|  | mMoreEventsTextColor = mResources.getColor(R.color.month_event_other_color); | 
|  |  | 
|  | mEventTextPaint.setTextSize(EVENT_TEXT_FONT_SIZE); | 
|  | mEventTextPaint.setTextAlign(Paint.Align.LEFT); | 
|  | mEventTextPaint.setAntiAlias(true); | 
|  |  | 
|  | int gridLineColor = mResources.getColor(R.color.calendar_grid_line_highlight_color); | 
|  | Paint p = mSelectionPaint; | 
|  | p.setColor(gridLineColor); | 
|  | p.setStyle(Style.FILL); | 
|  | p.setAntiAlias(false); | 
|  |  | 
|  | p = mPaint; | 
|  | p.setAntiAlias(true); | 
|  |  | 
|  | // Allocate space for 2 weeks worth of weekday names so that we can | 
|  | // easily start the week display at any week day. | 
|  | mDayStrs = new String[14]; | 
|  |  | 
|  | // Also create an array of 2-letter abbreviations. | 
|  | mDayStrs2Letter = new String[14]; | 
|  |  | 
|  | for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) { | 
|  | int index = i - Calendar.SUNDAY; | 
|  | // e.g. Tue for Tuesday | 
|  | mDayStrs[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_MEDIUM) | 
|  | .toUpperCase(); | 
|  | mDayStrs[index + 7] = mDayStrs[index]; | 
|  | // e.g. Tu for Tuesday | 
|  | mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORT) | 
|  | .toUpperCase(); | 
|  |  | 
|  | // If we don't have 2-letter day strings, fall back to 1-letter. | 
|  | if (mDayStrs2Letter[index].equals(mDayStrs[index])) { | 
|  | mDayStrs2Letter[index] = DateUtils.getDayOfWeekString(i, DateUtils.LENGTH_SHORTEST); | 
|  | } | 
|  |  | 
|  | mDayStrs2Letter[index + 7] = mDayStrs2Letter[index]; | 
|  | } | 
|  |  | 
|  | // Figure out how much space we need for the 3-letter abbrev names | 
|  | // in the worst case. | 
|  | p.setTextSize(DATE_HEADER_FONT_SIZE); | 
|  | p.setTypeface(mBold); | 
|  | String[] dateStrs = {" 28", " 30"}; | 
|  | mDateStrWidth = computeMaxStringWidth(0, dateStrs, p); | 
|  | p.setTextSize(DAY_HEADER_FONT_SIZE); | 
|  | mDateStrWidth += computeMaxStringWidth(0, mDayStrs, p); | 
|  |  | 
|  | p.setTextSize(HOURS_TEXT_SIZE); | 
|  | p.setTypeface(null); | 
|  | handleOnResume(); | 
|  |  | 
|  | mAmString = DateUtils.getAMPMString(Calendar.AM).toUpperCase(); | 
|  | mPmString = DateUtils.getAMPMString(Calendar.PM).toUpperCase(); | 
|  | String[] ampm = {mAmString, mPmString}; | 
|  | p.setTextSize(AMPM_TEXT_SIZE); | 
|  | mHoursWidth = Math.max(HOURS_MARGIN, computeMaxStringWidth(mHoursWidth, ampm, p) | 
|  | + HOURS_RIGHT_MARGIN); | 
|  | mHoursWidth = Math.max(MIN_HOURS_WIDTH, mHoursWidth); | 
|  |  | 
|  | LayoutInflater inflater; | 
|  | inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | 
|  | mPopupView = inflater.inflate(R.layout.bubble_event, null); | 
|  | mPopupView.setLayoutParams(new ViewGroup.LayoutParams( | 
|  | ViewGroup.LayoutParams.MATCH_PARENT, | 
|  | ViewGroup.LayoutParams.WRAP_CONTENT)); | 
|  | mPopup = new PopupWindow(context); | 
|  | mPopup.setContentView(mPopupView); | 
|  | Resources.Theme dialogTheme = getResources().newTheme(); | 
|  | dialogTheme.applyStyle(android.R.style.Theme_Dialog, true); | 
|  | TypedArray ta = dialogTheme.obtainStyledAttributes(new int[] { | 
|  | android.R.attr.windowBackground }); | 
|  | mPopup.setBackgroundDrawable(ta.getDrawable(0)); | 
|  | ta.recycle(); | 
|  |  | 
|  | // Enable touching the popup window | 
|  | mPopupView.setOnClickListener(this); | 
|  | // Catch long clicks for creating a new event | 
|  | setOnLongClickListener(this); | 
|  |  | 
|  | mBaseDate = new Time(Utils.getTimeZone(context, mTZUpdater)); | 
|  | long millis = System.currentTimeMillis(); | 
|  | mBaseDate.set(millis); | 
|  |  | 
|  | mEarliestStartHour = new int[mNumDays]; | 
|  | mHasAllDayEvent = new boolean[mNumDays]; | 
|  |  | 
|  | // mLines is the array of points used with Canvas.drawLines() in | 
|  | // drawGridBackground() and drawAllDayEvents().  Its size depends | 
|  | // on the max number of lines that can ever be drawn by any single | 
|  | // drawLines() call in either of those methods. | 
|  | final int maxGridLines = (24 + 1)  // max horizontal lines we might draw | 
|  | + (mNumDays + 1); // max vertical lines we might draw | 
|  | mLines = new float[maxGridLines * 4]; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * This is called when the popup window is pressed. | 
|  | */ | 
|  | public void onClick(View v) { | 
|  | if (v == mPopupView) { | 
|  | // Pretend it was a trackball click because that will always | 
|  | // jump to the "View event" screen. | 
|  | switchViews(true /* trackball */); | 
|  | } | 
|  | } | 
|  |  | 
|  | public void handleOnResume() { | 
|  | initAccessibilityVariables(); | 
|  | if(Utils.getSharedPreference(mContext, OtherPreferences.KEY_OTHER_1, false)) { | 
|  | mFutureBgColor = 0; | 
|  | } else { | 
|  | mFutureBgColor = mFutureBgColorRes; | 
|  | } | 
|  | mIs24HourFormat = DateFormat.is24HourFormat(mContext); | 
|  | mHourStrs = mIs24HourFormat ? CalendarData.s24Hours : CalendarData.s12HoursNoAmPm; | 
|  | mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext); | 
|  | mLastSelectionDayForAccessibility = 0; | 
|  | mLastSelectionHourForAccessibility = 0; | 
|  | mLastSelectedEventForAccessibility = null; | 
|  | mSelectionMode = SELECTION_HIDDEN; | 
|  | } | 
|  |  | 
|  | private void initAccessibilityVariables() { | 
|  | mAccessibilityMgr = (AccessibilityManager) mContext | 
|  | .getSystemService(Service.ACCESSIBILITY_SERVICE); | 
|  | mIsAccessibilityEnabled = mAccessibilityMgr != null && mAccessibilityMgr.isEnabled(); | 
|  | mTouchExplorationEnabled = isTouchExplorationEnabled(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the start of the selected time in milliseconds since the epoch. | 
|  | * | 
|  | * @return selected time in UTC milliseconds since the epoch. | 
|  | */ | 
|  | long getSelectedTimeInMillis() { | 
|  | Time time = new Time(mBaseDate); | 
|  | time.setJulianDay(mSelectionDay); | 
|  | time.hour = mSelectionHour; | 
|  |  | 
|  | // We ignore the "isDst" field because we want normalize() to figure | 
|  | // out the correct DST value and not adjust the selected time based | 
|  | // on the current setting of DST. | 
|  | return time.normalize(true /* ignore isDst */); | 
|  | } | 
|  |  | 
|  | Time getSelectedTime() { | 
|  | Time time = new Time(mBaseDate); | 
|  | time.setJulianDay(mSelectionDay); | 
|  | time.hour = mSelectionHour; | 
|  |  | 
|  | // We ignore the "isDst" field because we want normalize() to figure | 
|  | // out the correct DST value and not adjust the selected time based | 
|  | // on the current setting of DST. | 
|  | time.normalize(true /* ignore isDst */); | 
|  | return time; | 
|  | } | 
|  |  | 
|  | Time getSelectedTimeForAccessibility() { | 
|  | Time time = new Time(mBaseDate); | 
|  | time.setJulianDay(mSelectionDayForAccessibility); | 
|  | time.hour = mSelectionHourForAccessibility; | 
|  |  | 
|  | // We ignore the "isDst" field because we want normalize() to figure | 
|  | // out the correct DST value and not adjust the selected time based | 
|  | // on the current setting of DST. | 
|  | time.normalize(true /* ignore isDst */); | 
|  | return time; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the start of the selected time in minutes since midnight, | 
|  | * local time.  The derived class must ensure that this is consistent | 
|  | * with the return value from getSelectedTimeInMillis(). | 
|  | */ | 
|  | int getSelectedMinutesSinceMidnight() { | 
|  | return mSelectionHour * MINUTES_PER_HOUR; | 
|  | } | 
|  |  | 
|  | int getFirstVisibleHour() { | 
|  | return mFirstHour; | 
|  | } | 
|  |  | 
|  | void setFirstVisibleHour(int firstHour) { | 
|  | mFirstHour = firstHour; | 
|  | mFirstHourOffset = 0; | 
|  | } | 
|  |  | 
|  | public void setSelected(Time time, boolean ignoreTime, boolean animateToday) { | 
|  | mBaseDate.set(time); | 
|  | setSelectedHour(mBaseDate.hour); | 
|  | setSelectedEvent(null); | 
|  | mPrevSelectedEvent = null; | 
|  | long millis = mBaseDate.toMillis(false /* use isDst */); | 
|  | setSelectedDay(Time.getJulianDay(millis, mBaseDate.gmtoff)); | 
|  | mSelectedEvents.clear(); | 
|  | mComputeSelectedEvents = true; | 
|  |  | 
|  | int gotoY = Integer.MIN_VALUE; | 
|  |  | 
|  | if (!ignoreTime && mGridAreaHeight != -1) { | 
|  | int lastHour = 0; | 
|  |  | 
|  | if (mBaseDate.hour < mFirstHour) { | 
|  | // Above visible region | 
|  | gotoY = mBaseDate.hour * (mCellHeight + HOUR_GAP); | 
|  | } else { | 
|  | lastHour = (mGridAreaHeight - mFirstHourOffset) / (mCellHeight + HOUR_GAP) | 
|  | + mFirstHour; | 
|  |  | 
|  | if (mBaseDate.hour >= lastHour) { | 
|  | // Below visible region | 
|  |  | 
|  | // target hour + 1 (to give it room to see the event) - | 
|  | // grid height (to get the y of the top of the visible | 
|  | // region) | 
|  | gotoY = (int) ((mBaseDate.hour + 1 + mBaseDate.minute / 60.0f) | 
|  | * (mCellHeight + HOUR_GAP) - mGridAreaHeight); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (DEBUG) { | 
|  | Log.e(TAG, "Go " + gotoY + " 1st " + mFirstHour + ":" + mFirstHourOffset + "CH " | 
|  | + (mCellHeight + HOUR_GAP) + " lh " + lastHour + " gh " + mGridAreaHeight | 
|  | + " ymax " + mMaxViewStartY); | 
|  | } | 
|  |  | 
|  | if (gotoY > mMaxViewStartY) { | 
|  | gotoY = mMaxViewStartY; | 
|  | } else if (gotoY < 0 && gotoY != Integer.MIN_VALUE) { | 
|  | gotoY = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | recalc(); | 
|  |  | 
|  | mRemeasure = true; | 
|  | invalidate(); | 
|  |  | 
|  | boolean delayAnimateToday = false; | 
|  | if (gotoY != Integer.MIN_VALUE) { | 
|  | ValueAnimator scrollAnim = ObjectAnimator.ofInt(this, "viewStartY", mViewStartY, gotoY); | 
|  | scrollAnim.setDuration(GOTO_SCROLL_DURATION); | 
|  | scrollAnim.setInterpolator(new AccelerateDecelerateInterpolator()); | 
|  | scrollAnim.addListener(mAnimatorListener); | 
|  | scrollAnim.start(); | 
|  | delayAnimateToday = true; | 
|  | } | 
|  | if (animateToday) { | 
|  | synchronized (mTodayAnimatorListener) { | 
|  | if (mTodayAnimator != null) { | 
|  | mTodayAnimator.removeAllListeners(); | 
|  | mTodayAnimator.cancel(); | 
|  | } | 
|  | mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha", | 
|  | mAnimateTodayAlpha, 255); | 
|  | mAnimateToday = true; | 
|  | mTodayAnimatorListener.setFadingIn(true); | 
|  | mTodayAnimatorListener.setAnimator(mTodayAnimator); | 
|  | mTodayAnimator.addListener(mTodayAnimatorListener); | 
|  | mTodayAnimator.setDuration(150); | 
|  | if (delayAnimateToday) { | 
|  | mTodayAnimator.setStartDelay(GOTO_SCROLL_DURATION); | 
|  | } | 
|  | mTodayAnimator.start(); | 
|  | } | 
|  | } | 
|  | sendAccessibilityEventAsNeeded(false); | 
|  | } | 
|  |  | 
|  | // Called from animation framework via reflection. Do not remove | 
|  | public void setViewStartY(int viewStartY) { | 
|  | if (viewStartY > mMaxViewStartY) { | 
|  | viewStartY = mMaxViewStartY; | 
|  | } | 
|  |  | 
|  | mViewStartY = viewStartY; | 
|  |  | 
|  | computeFirstHour(); | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | public void setAnimateTodayAlpha(int todayAlpha) { | 
|  | mAnimateTodayAlpha = todayAlpha; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | public Time getSelectedDay() { | 
|  | Time time = new Time(mBaseDate); | 
|  | time.setJulianDay(mSelectionDay); | 
|  | time.hour = mSelectionHour; | 
|  |  | 
|  | // We ignore the "isDst" field because we want normalize() to figure | 
|  | // out the correct DST value and not adjust the selected time based | 
|  | // on the current setting of DST. | 
|  | time.normalize(true /* ignore isDst */); | 
|  | return time; | 
|  | } | 
|  |  | 
|  | public void updateTitle() { | 
|  | Time start = new Time(mBaseDate); | 
|  | start.normalize(true); | 
|  | Time end = new Time(start); | 
|  | end.monthDay += mNumDays - 1; | 
|  | // Move it forward one minute so the formatter doesn't lose a day | 
|  | end.minute += 1; | 
|  | end.normalize(true); | 
|  |  | 
|  | long formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; | 
|  | if (mNumDays != 1) { | 
|  | // Don't show day of the month if for multi-day view | 
|  | formatFlags |= DateUtils.FORMAT_NO_MONTH_DAY; | 
|  |  | 
|  | // Abbreviate the month if showing multiple months | 
|  | if (start.month != end.month) { | 
|  | formatFlags |= DateUtils.FORMAT_ABBREV_MONTH; | 
|  | } | 
|  | } | 
|  |  | 
|  | mController.sendEvent(this, EventType.UPDATE_TITLE, start, end, null, -1, ViewType.CURRENT, | 
|  | formatFlags, null, null); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * return a negative number if "time" is comes before the visible time | 
|  | * range, a positive number if "time" is after the visible time range, and 0 | 
|  | * if it is in the visible time range. | 
|  | */ | 
|  | public int compareToVisibleTimeRange(Time time) { | 
|  |  | 
|  | int savedHour = mBaseDate.hour; | 
|  | int savedMinute = mBaseDate.minute; | 
|  | int savedSec = mBaseDate.second; | 
|  |  | 
|  | mBaseDate.hour = 0; | 
|  | mBaseDate.minute = 0; | 
|  | mBaseDate.second = 0; | 
|  |  | 
|  | if (DEBUG) { | 
|  | Log.d(TAG, "Begin " + mBaseDate.toString()); | 
|  | Log.d(TAG, "Diff  " + time.toString()); | 
|  | } | 
|  |  | 
|  | // Compare beginning of range | 
|  | int diff = Time.compare(time, mBaseDate); | 
|  | if (diff > 0) { | 
|  | // Compare end of range | 
|  | mBaseDate.monthDay += mNumDays; | 
|  | mBaseDate.normalize(true); | 
|  | diff = Time.compare(time, mBaseDate); | 
|  |  | 
|  | if (DEBUG) Log.d(TAG, "End   " + mBaseDate.toString()); | 
|  |  | 
|  | mBaseDate.monthDay -= mNumDays; | 
|  | mBaseDate.normalize(true); | 
|  | if (diff < 0) { | 
|  | // in visible time | 
|  | diff = 0; | 
|  | } else if (diff == 0) { | 
|  | // Midnight of following day | 
|  | diff = 1; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (DEBUG) Log.d(TAG, "Diff: " + diff); | 
|  |  | 
|  | mBaseDate.hour = savedHour; | 
|  | mBaseDate.minute = savedMinute; | 
|  | mBaseDate.second = savedSec; | 
|  | return diff; | 
|  | } | 
|  |  | 
|  | private void recalc() { | 
|  | // Set the base date to the beginning of the week if we are displaying | 
|  | // 7 days at a time. | 
|  | if (mNumDays == 7) { | 
|  | adjustToBeginningOfWeek(mBaseDate); | 
|  | } | 
|  |  | 
|  | final long start = mBaseDate.toMillis(false /* use isDst */); | 
|  | mFirstJulianDay = Time.getJulianDay(start, mBaseDate.gmtoff); | 
|  | mLastJulianDay = mFirstJulianDay + mNumDays - 1; | 
|  |  | 
|  | mMonthLength = mBaseDate.getActualMaximum(Time.MONTH_DAY); | 
|  | mFirstVisibleDate = mBaseDate.monthDay; | 
|  | mFirstVisibleDayOfWeek = mBaseDate.weekDay; | 
|  | } | 
|  |  | 
|  | private void adjustToBeginningOfWeek(Time time) { | 
|  | int dayOfWeek = time.weekDay; | 
|  | int diff = dayOfWeek - mFirstDayOfWeek; | 
|  | if (diff != 0) { | 
|  | if (diff < 0) { | 
|  | diff += 7; | 
|  | } | 
|  | time.monthDay -= diff; | 
|  | time.normalize(true /* ignore isDst */); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onSizeChanged(int width, int height, int oldw, int oldh) { | 
|  | mViewWidth = width; | 
|  | mViewHeight = height; | 
|  | mEdgeEffectTop.setSize(mViewWidth, mViewHeight); | 
|  | mEdgeEffectBottom.setSize(mViewWidth, mViewHeight); | 
|  | int gridAreaWidth = width - mHoursWidth; | 
|  | mCellWidth = (gridAreaWidth - (mNumDays * DAY_GAP)) / mNumDays; | 
|  |  | 
|  | // This would be about 1 day worth in a 7 day view | 
|  | mHorizontalSnapBackThreshold = width / 7; | 
|  |  | 
|  | Paint p = new Paint(); | 
|  | p.setTextSize(HOURS_TEXT_SIZE); | 
|  | mHoursTextHeight = (int) Math.abs(p.ascent()); | 
|  | remeasure(width, height); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Measures the space needed for various parts of the view after | 
|  | * loading new events.  This can change if there are all-day events. | 
|  | */ | 
|  | private void remeasure(int width, int height) { | 
|  | // Shrink to fit available space but make sure we can display at least two events | 
|  | MAX_UNEXPANDED_ALLDAY_HEIGHT = (int) (MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 4); | 
|  | MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.min(MAX_UNEXPANDED_ALLDAY_HEIGHT, height / 6); | 
|  | MAX_UNEXPANDED_ALLDAY_HEIGHT = Math.max(MAX_UNEXPANDED_ALLDAY_HEIGHT, | 
|  | (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT * 2); | 
|  | mMaxUnexpandedAlldayEventCount = | 
|  | (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT / MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); | 
|  |  | 
|  | // First, clear the array of earliest start times, and the array | 
|  | // indicating presence of an all-day event. | 
|  | for (int day = 0; day < mNumDays; day++) { | 
|  | mEarliestStartHour[day] = 25;  // some big number | 
|  | mHasAllDayEvent[day] = false; | 
|  | } | 
|  |  | 
|  | int maxAllDayEvents = mMaxAlldayEvents; | 
|  |  | 
|  | // The min is where 24 hours cover the entire visible area | 
|  | mMinCellHeight = Math.max((height - DAY_HEADER_HEIGHT) / 24, (int) MIN_EVENT_HEIGHT); | 
|  | if (mCellHeight < mMinCellHeight) { | 
|  | mCellHeight = mMinCellHeight; | 
|  | } | 
|  |  | 
|  | // Calculate mAllDayHeight | 
|  | mFirstCell = DAY_HEADER_HEIGHT; | 
|  | int allDayHeight = 0; | 
|  | if (maxAllDayEvents > 0) { | 
|  | int maxAllAllDayHeight = height - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; | 
|  | // If there is at most one all-day event per day, then use less | 
|  | // space (but more than the space for a single event). | 
|  | if (maxAllDayEvents == 1) { | 
|  | allDayHeight = SINGLE_ALLDAY_HEIGHT; | 
|  | } else if (maxAllDayEvents <= mMaxUnexpandedAlldayEventCount){ | 
|  | // Allow the all-day area to grow in height depending on the | 
|  | // number of all-day events we need to show, up to a limit. | 
|  | allDayHeight = maxAllDayEvents * MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; | 
|  | if (allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { | 
|  | allDayHeight = MAX_UNEXPANDED_ALLDAY_HEIGHT; | 
|  | } | 
|  | } else { | 
|  | // if we have more than the magic number, check if we're animating | 
|  | // and if not adjust the sizes appropriately | 
|  | if (mAnimateDayHeight != 0) { | 
|  | // Don't shrink the space past the final allDay space. The animation | 
|  | // continues to hide the last event so the more events text can | 
|  | // fade in. | 
|  | allDayHeight = Math.max(mAnimateDayHeight, MAX_UNEXPANDED_ALLDAY_HEIGHT); | 
|  | } else { | 
|  | // Try to fit all the events in | 
|  | allDayHeight = (int) (maxAllDayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); | 
|  | // But clip the area depending on which mode we're in | 
|  | if (!mShowAllAllDayEvents && allDayHeight > MAX_UNEXPANDED_ALLDAY_HEIGHT) { | 
|  | allDayHeight = (int) (mMaxUnexpandedAlldayEventCount * | 
|  | MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT); | 
|  | } else if (allDayHeight > maxAllAllDayHeight) { | 
|  | allDayHeight = maxAllAllDayHeight; | 
|  | } | 
|  | } | 
|  | } | 
|  | mFirstCell = DAY_HEADER_HEIGHT + allDayHeight + ALLDAY_TOP_MARGIN; | 
|  | } else { | 
|  | mSelectionAllday = false; | 
|  | } | 
|  | mAlldayHeight = allDayHeight; | 
|  |  | 
|  | mGridAreaHeight = height - mFirstCell; | 
|  |  | 
|  | // Set up the expand icon position | 
|  | int allDayIconWidth = mExpandAlldayDrawable.getIntrinsicWidth(); | 
|  | mExpandAllDayRect.left = Math.max((mHoursWidth - allDayIconWidth) / 2, | 
|  | EVENT_ALL_DAY_TEXT_LEFT_MARGIN); | 
|  | mExpandAllDayRect.right = Math.min(mExpandAllDayRect.left + allDayIconWidth, mHoursWidth | 
|  | - EVENT_ALL_DAY_TEXT_RIGHT_MARGIN); | 
|  | mExpandAllDayRect.bottom = mFirstCell - EXPAND_ALL_DAY_BOTTOM_MARGIN; | 
|  | mExpandAllDayRect.top = mExpandAllDayRect.bottom | 
|  | - mExpandAlldayDrawable.getIntrinsicHeight(); | 
|  |  | 
|  | mNumHours = mGridAreaHeight / (mCellHeight + HOUR_GAP); | 
|  | mEventGeometry.setHourHeight(mCellHeight); | 
|  |  | 
|  | final long minimumDurationMillis = (long) | 
|  | (MIN_EVENT_HEIGHT * DateUtils.MINUTE_IN_MILLIS / (mCellHeight / 60.0f)); | 
|  | Event.computePositions(mEvents, minimumDurationMillis); | 
|  |  | 
|  | // Compute the top of our reachable view | 
|  | mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; | 
|  | if (DEBUG) { | 
|  | Log.e(TAG, "mViewStartY: " + mViewStartY); | 
|  | Log.e(TAG, "mMaxViewStartY: " + mMaxViewStartY); | 
|  | } | 
|  | if (mViewStartY > mMaxViewStartY) { | 
|  | mViewStartY = mMaxViewStartY; | 
|  | computeFirstHour(); | 
|  | } | 
|  |  | 
|  | if (mFirstHour == -1) { | 
|  | initFirstHour(); | 
|  | mFirstHourOffset = 0; | 
|  | } | 
|  |  | 
|  | // When we change the base date, the number of all-day events may | 
|  | // change and that changes the cell height.  When we switch dates, | 
|  | // we use the mFirstHourOffset from the previous view, but that may | 
|  | // be too large for the new view if the cell height is smaller. | 
|  | if (mFirstHourOffset >= mCellHeight + HOUR_GAP) { | 
|  | mFirstHourOffset = mCellHeight + HOUR_GAP - 1; | 
|  | } | 
|  | mViewStartY = mFirstHour * (mCellHeight + HOUR_GAP) - mFirstHourOffset; | 
|  |  | 
|  | final int eventAreaWidth = mNumDays * (mCellWidth + DAY_GAP); | 
|  | //When we get new events we don't want to dismiss the popup unless the event changes | 
|  | if (mSelectedEvent != null && mLastPopupEventID != mSelectedEvent.id) { | 
|  | mPopup.dismiss(); | 
|  | } | 
|  | mPopup.setWidth(eventAreaWidth - 20); | 
|  | mPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Initialize the state for another view.  The given view is one that has | 
|  | * its own bitmap and will use an animation to replace the current view. | 
|  | * The current view and new view are either both Week views or both Day | 
|  | * views.  They differ in their base date. | 
|  | * | 
|  | * @param view the view to initialize. | 
|  | */ | 
|  | private void initView(DayView view) { | 
|  | view.setSelectedHour(mSelectionHour); | 
|  | view.mSelectedEvents.clear(); | 
|  | view.mComputeSelectedEvents = true; | 
|  | view.mFirstHour = mFirstHour; | 
|  | view.mFirstHourOffset = mFirstHourOffset; | 
|  | view.remeasure(getWidth(), getHeight()); | 
|  | view.initAllDayHeights(); | 
|  |  | 
|  | view.setSelectedEvent(null); | 
|  | view.mPrevSelectedEvent = null; | 
|  | view.mFirstDayOfWeek = mFirstDayOfWeek; | 
|  | if (view.mEvents.size() > 0) { | 
|  | view.mSelectionAllday = mSelectionAllday; | 
|  | } else { | 
|  | view.mSelectionAllday = false; | 
|  | } | 
|  |  | 
|  | // Redraw the screen so that the selection box will be redrawn.  We may | 
|  | // have scrolled to a different part of the day in some other view | 
|  | // so the selection box in this view may no longer be visible. | 
|  | view.recalc(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Switch to another view based on what was selected (an event or a free | 
|  | * slot) and how it was selected (by touch or by trackball). | 
|  | * | 
|  | * @param trackBallSelection true if the selection was made using the | 
|  | * trackball. | 
|  | */ | 
|  | private void switchViews(boolean trackBallSelection) { | 
|  | Event selectedEvent = mSelectedEvent; | 
|  |  | 
|  | mPopup.dismiss(); | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | if (mNumDays > 1) { | 
|  | // This is the Week view. | 
|  | // With touch, we always switch to Day/Agenda View | 
|  | // With track ball, if we selected a free slot, then create an event. | 
|  | // If we selected a specific event, switch to EventInfo view. | 
|  | if (trackBallSelection) { | 
|  | if (selectedEvent == null) { | 
|  | // Switch to the EditEvent view | 
|  | long startMillis = getSelectedTimeInMillis(); | 
|  | long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; | 
|  | long extraLong = 0; | 
|  | if (mSelectionAllday) { | 
|  | extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; | 
|  | } | 
|  | mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, | 
|  | startMillis, endMillis, -1, -1, extraLong, -1); | 
|  | } else { | 
|  | if (mIsAccessibilityEnabled) { | 
|  | mAccessibilityMgr.interrupt(); | 
|  | } | 
|  | // Switch to the EventInfo view | 
|  | mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, | 
|  | selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, | 
|  | getSelectedTimeInMillis()); | 
|  | } | 
|  | } else { | 
|  | // This was a touch selection.  If the touch selected a single | 
|  | // unambiguous event, then view that event.  Otherwise go to | 
|  | // Day/Agenda view. | 
|  | if (mSelectedEvents.size() == 1) { | 
|  | if (mIsAccessibilityEnabled) { | 
|  | mAccessibilityMgr.interrupt(); | 
|  | } | 
|  | mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, | 
|  | selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, | 
|  | getSelectedTimeInMillis()); | 
|  | } | 
|  | } | 
|  | } else { | 
|  | // This is the Day view. | 
|  | // If we selected a free slot, then create an event. | 
|  | // If we selected an event, then go to the EventInfo view. | 
|  | if (selectedEvent == null) { | 
|  | // Switch to the EditEvent view | 
|  | long startMillis = getSelectedTimeInMillis(); | 
|  | long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; | 
|  | long extraLong = 0; | 
|  | if (mSelectionAllday) { | 
|  | extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; | 
|  | } | 
|  | mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, | 
|  | startMillis, endMillis, -1, -1, extraLong, -1); | 
|  | } else { | 
|  | if (mIsAccessibilityEnabled) { | 
|  | mAccessibilityMgr.interrupt(); | 
|  | } | 
|  | mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT, selectedEvent.id, | 
|  | selectedEvent.startMillis, selectedEvent.endMillis, 0, 0, | 
|  | getSelectedTimeInMillis()); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onKeyUp(int keyCode, KeyEvent event) { | 
|  | mScrolling = false; | 
|  | long duration = event.getEventTime() - event.getDownTime(); | 
|  |  | 
|  | switch (keyCode) { | 
|  | case KeyEvent.KEYCODE_DPAD_CENTER: | 
|  | if (mSelectionMode == SELECTION_HIDDEN) { | 
|  | // Don't do anything unless the selection is visible. | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (mSelectionMode == SELECTION_PRESSED) { | 
|  | // This was the first press when there was nothing selected. | 
|  | // Change the selection from the "pressed" state to the | 
|  | // the "selected" state.  We treat short-press and | 
|  | // long-press the same here because nothing was selected. | 
|  | mSelectionMode = SELECTION_SELECTED; | 
|  | invalidate(); | 
|  | break; | 
|  | } | 
|  |  | 
|  | // Check the duration to determine if this was a short press | 
|  | if (duration < ViewConfiguration.getLongPressTimeout()) { | 
|  | switchViews(true /* trackball */); | 
|  | } else { | 
|  | mSelectionMode = SELECTION_LONGPRESS; | 
|  | invalidate(); | 
|  | performLongClick(); | 
|  | } | 
|  | break; | 
|  | //            case KeyEvent.KEYCODE_BACK: | 
|  | //                if (event.isTracking() && !event.isCanceled()) { | 
|  | //                    mPopup.dismiss(); | 
|  | //                    mContext.finish(); | 
|  | //                    return true; | 
|  | //                } | 
|  | //                break; | 
|  | } | 
|  | return super.onKeyUp(keyCode, event); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onKeyDown(int keyCode, KeyEvent event) { | 
|  | if (mSelectionMode == SELECTION_HIDDEN) { | 
|  | if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT | 
|  | || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP | 
|  | || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { | 
|  | // Display the selection box but don't move or select it | 
|  | // on this key press. | 
|  | mSelectionMode = SELECTION_SELECTED; | 
|  | invalidate(); | 
|  | return true; | 
|  | } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { | 
|  | // Display the selection box but don't select it | 
|  | // on this key press. | 
|  | mSelectionMode = SELECTION_PRESSED; | 
|  | invalidate(); | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | mSelectionMode = SELECTION_SELECTED; | 
|  | mScrolling = false; | 
|  | boolean redraw; | 
|  | int selectionDay = mSelectionDay; | 
|  |  | 
|  | switch (keyCode) { | 
|  | case KeyEvent.KEYCODE_DEL: | 
|  | // Delete the selected event, if any | 
|  | Event selectedEvent = mSelectedEvent; | 
|  | if (selectedEvent == null) { | 
|  | return false; | 
|  | } | 
|  | mPopup.dismiss(); | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  |  | 
|  | long begin = selectedEvent.startMillis; | 
|  | long end = selectedEvent.endMillis; | 
|  | long id = selectedEvent.id; | 
|  | mDeleteEventHelper.delete(begin, end, id, -1); | 
|  | return true; | 
|  | case KeyEvent.KEYCODE_ENTER: | 
|  | switchViews(true /* trackball or keyboard */); | 
|  | return true; | 
|  | case KeyEvent.KEYCODE_BACK: | 
|  | if (event.getRepeatCount() == 0) { | 
|  | event.startTracking(); | 
|  | return true; | 
|  | } | 
|  | return super.onKeyDown(keyCode, event); | 
|  | case KeyEvent.KEYCODE_DPAD_LEFT: | 
|  | if (mSelectedEvent != null) { | 
|  | setSelectedEvent(mSelectedEvent.nextLeft); | 
|  | } | 
|  | if (mSelectedEvent == null) { | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | selectionDay -= 1; | 
|  | } | 
|  | redraw = true; | 
|  | break; | 
|  |  | 
|  | case KeyEvent.KEYCODE_DPAD_RIGHT: | 
|  | if (mSelectedEvent != null) { | 
|  | setSelectedEvent(mSelectedEvent.nextRight); | 
|  | } | 
|  | if (mSelectedEvent == null) { | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | selectionDay += 1; | 
|  | } | 
|  | redraw = true; | 
|  | break; | 
|  |  | 
|  | case KeyEvent.KEYCODE_DPAD_UP: | 
|  | if (mSelectedEvent != null) { | 
|  | setSelectedEvent(mSelectedEvent.nextUp); | 
|  | } | 
|  | if (mSelectedEvent == null) { | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | if (!mSelectionAllday) { | 
|  | setSelectedHour(mSelectionHour - 1); | 
|  | adjustHourSelection(); | 
|  | mSelectedEvents.clear(); | 
|  | mComputeSelectedEvents = true; | 
|  | } | 
|  | } | 
|  | redraw = true; | 
|  | break; | 
|  |  | 
|  | case KeyEvent.KEYCODE_DPAD_DOWN: | 
|  | if (mSelectedEvent != null) { | 
|  | setSelectedEvent(mSelectedEvent.nextDown); | 
|  | } | 
|  | if (mSelectedEvent == null) { | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | if (mSelectionAllday) { | 
|  | mSelectionAllday = false; | 
|  | } else { | 
|  | setSelectedHour(mSelectionHour + 1); | 
|  | adjustHourSelection(); | 
|  | mSelectedEvents.clear(); | 
|  | mComputeSelectedEvents = true; | 
|  | } | 
|  | } | 
|  | redraw = true; | 
|  | break; | 
|  |  | 
|  | default: | 
|  | return super.onKeyDown(keyCode, event); | 
|  | } | 
|  |  | 
|  | if ((selectionDay < mFirstJulianDay) || (selectionDay > mLastJulianDay)) { | 
|  | DayView view = (DayView) mViewSwitcher.getNextView(); | 
|  | Time date = view.mBaseDate; | 
|  | date.set(mBaseDate); | 
|  | if (selectionDay < mFirstJulianDay) { | 
|  | date.monthDay -= mNumDays; | 
|  | } else { | 
|  | date.monthDay += mNumDays; | 
|  | } | 
|  | date.normalize(true /* ignore isDst */); | 
|  | view.setSelectedDay(selectionDay); | 
|  |  | 
|  | initView(view); | 
|  |  | 
|  | Time end = new Time(date); | 
|  | end.monthDay += mNumDays - 1; | 
|  | mController.sendEvent(this, EventType.GO_TO, date, end, -1, ViewType.CURRENT); | 
|  | return true; | 
|  | } | 
|  | if (mSelectionDay != selectionDay) { | 
|  | Time date = new Time(mBaseDate); | 
|  | date.setJulianDay(selectionDay); | 
|  | date.hour = mSelectionHour; | 
|  | mController.sendEvent(this, EventType.GO_TO, date, date, -1, ViewType.CURRENT); | 
|  | } | 
|  | setSelectedDay(selectionDay); | 
|  | mSelectedEvents.clear(); | 
|  | mComputeSelectedEvents = true; | 
|  | mUpdateToast = true; | 
|  |  | 
|  | if (redraw) { | 
|  | invalidate(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return super.onKeyDown(keyCode, event); | 
|  | } | 
|  |  | 
|  |  | 
|  | @Override | 
|  | public boolean onHoverEvent(MotionEvent event) { | 
|  | if (DEBUG) { | 
|  | int action = event.getAction(); | 
|  | switch (action) { | 
|  | case MotionEvent.ACTION_HOVER_ENTER: | 
|  | Log.e(TAG, "ACTION_HOVER_ENTER"); | 
|  | break; | 
|  | case MotionEvent.ACTION_HOVER_MOVE: | 
|  | Log.e(TAG, "ACTION_HOVER_MOVE"); | 
|  | break; | 
|  | case MotionEvent.ACTION_HOVER_EXIT: | 
|  | Log.e(TAG, "ACTION_HOVER_EXIT"); | 
|  | break; | 
|  | default: | 
|  | Log.e(TAG, "Unknown hover event action. " + event); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Mouse also generates hover events | 
|  | // Send accessibility events if accessibility and exploration are on. | 
|  | if (!mTouchExplorationEnabled) { | 
|  | return super.onHoverEvent(event); | 
|  | } | 
|  | if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) { | 
|  | setSelectionFromPosition((int) event.getX(), (int) event.getY(), true); | 
|  | invalidate(); | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | private boolean isTouchExplorationEnabled() { | 
|  | return mIsAccessibilityEnabled && mAccessibilityMgr.isTouchExplorationEnabled(); | 
|  | } | 
|  |  | 
|  | private void sendAccessibilityEventAsNeeded(boolean speakEvents) { | 
|  | if (!mIsAccessibilityEnabled) { | 
|  | return; | 
|  | } | 
|  | boolean dayChanged = mLastSelectionDayForAccessibility != mSelectionDayForAccessibility; | 
|  | boolean hourChanged = mLastSelectionHourForAccessibility != mSelectionHourForAccessibility; | 
|  | if (dayChanged || hourChanged || | 
|  | mLastSelectedEventForAccessibility != mSelectedEventForAccessibility) { | 
|  | mLastSelectionDayForAccessibility = mSelectionDayForAccessibility; | 
|  | mLastSelectionHourForAccessibility = mSelectionHourForAccessibility; | 
|  | mLastSelectedEventForAccessibility = mSelectedEventForAccessibility; | 
|  |  | 
|  | StringBuilder b = new StringBuilder(); | 
|  |  | 
|  | // Announce only the changes i.e. day or hour or both | 
|  | if (dayChanged) { | 
|  | b.append(getSelectedTimeForAccessibility().format("%A ")); | 
|  | } | 
|  | if (hourChanged) { | 
|  | b.append(getSelectedTimeForAccessibility().format(mIs24HourFormat ? "%k" : "%l%p")); | 
|  | } | 
|  | if (dayChanged || hourChanged) { | 
|  | b.append(PERIOD_SPACE); | 
|  | } | 
|  |  | 
|  | if (speakEvents) { | 
|  | if (mEventCountTemplate == null) { | 
|  | mEventCountTemplate = mContext.getString(R.string.template_announce_item_index); | 
|  | } | 
|  |  | 
|  | // Read out the relevant event(s) | 
|  | int numEvents = mSelectedEvents.size(); | 
|  | if (numEvents > 0) { | 
|  | if (mSelectedEventForAccessibility == null) { | 
|  | // Read out all the events | 
|  | int i = 1; | 
|  | for (Event calEvent : mSelectedEvents) { | 
|  | if (numEvents > 1) { | 
|  | // Read out x of numEvents if there are more than one event | 
|  | mStringBuilder.setLength(0); | 
|  | b.append(mFormatter.format(mEventCountTemplate, i++, numEvents)); | 
|  | b.append(" "); | 
|  | } | 
|  | appendEventAccessibilityString(b, calEvent); | 
|  | } | 
|  | } else { | 
|  | if (numEvents > 1) { | 
|  | // Read out x of numEvents if there are more than one event | 
|  | mStringBuilder.setLength(0); | 
|  | b.append(mFormatter.format(mEventCountTemplate, mSelectedEvents | 
|  | .indexOf(mSelectedEventForAccessibility) + 1, numEvents)); | 
|  | b.append(" "); | 
|  | } | 
|  | appendEventAccessibilityString(b, mSelectedEventForAccessibility); | 
|  | } | 
|  | } else { | 
|  | b.append(mCreateNewEventString); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (dayChanged || hourChanged || speakEvents) { | 
|  | AccessibilityEvent event = AccessibilityEvent | 
|  | .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); | 
|  | CharSequence msg = b.toString(); | 
|  | event.getText().add(msg); | 
|  | event.setAddedCount(msg.length()); | 
|  | sendAccessibilityEventUnchecked(event); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param b | 
|  | * @param calEvent | 
|  | */ | 
|  | private void appendEventAccessibilityString(StringBuilder b, Event calEvent) { | 
|  | b.append(calEvent.getTitleAndLocation()); | 
|  | b.append(PERIOD_SPACE); | 
|  | String when; | 
|  | int flags = DateUtils.FORMAT_SHOW_DATE; | 
|  | if (calEvent.allDay) { | 
|  | flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; | 
|  | } else { | 
|  | flags |= DateUtils.FORMAT_SHOW_TIME; | 
|  | if (DateFormat.is24HourFormat(mContext)) { | 
|  | flags |= DateUtils.FORMAT_24HOUR; | 
|  | } | 
|  | } | 
|  | when = Utils.formatDateRange(mContext, calEvent.startMillis, calEvent.endMillis, flags); | 
|  | b.append(when); | 
|  | b.append(PERIOD_SPACE); | 
|  | } | 
|  |  | 
|  | private class GotoBroadcaster implements Animation.AnimationListener { | 
|  | private final int mCounter; | 
|  | private final Time mStart; | 
|  | private final Time mEnd; | 
|  |  | 
|  | public GotoBroadcaster(Time start, Time end) { | 
|  | mCounter = ++sCounter; | 
|  | mStart = start; | 
|  | mEnd = end; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onAnimationEnd(Animation animation) { | 
|  | DayView view = (DayView) mViewSwitcher.getCurrentView(); | 
|  | view.mViewStartX = 0; | 
|  | view = (DayView) mViewSwitcher.getNextView(); | 
|  | view.mViewStartX = 0; | 
|  |  | 
|  | if (mCounter == sCounter) { | 
|  | mController.sendEvent(this, EventType.GO_TO, mStart, mEnd, null, -1, | 
|  | ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onAnimationRepeat(Animation animation) { | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onAnimationStart(Animation animation) { | 
|  | } | 
|  | } | 
|  |  | 
|  | private View switchViews(boolean forward, float xOffSet, float width, float velocity) { | 
|  | mAnimationDistance = width - xOffSet; | 
|  | if (DEBUG) { | 
|  | Log.d(TAG, "switchViews(" + forward + ") O:" + xOffSet + " Dist:" + mAnimationDistance); | 
|  | } | 
|  |  | 
|  | float progress = Math.abs(xOffSet) / width; | 
|  | if (progress > 1.0f) { | 
|  | progress = 1.0f; | 
|  | } | 
|  |  | 
|  | float inFromXValue, inToXValue; | 
|  | float outFromXValue, outToXValue; | 
|  | if (forward) { | 
|  | inFromXValue = 1.0f - progress; | 
|  | inToXValue = 0.0f; | 
|  | outFromXValue = -progress; | 
|  | outToXValue = -1.0f; | 
|  | } else { | 
|  | inFromXValue = progress - 1.0f; | 
|  | inToXValue = 0.0f; | 
|  | outFromXValue = progress; | 
|  | outToXValue = 1.0f; | 
|  | } | 
|  |  | 
|  | final Time start = new Time(mBaseDate.timezone); | 
|  | start.set(mController.getTime()); | 
|  | if (forward) { | 
|  | start.monthDay += mNumDays; | 
|  | } else { | 
|  | start.monthDay -= mNumDays; | 
|  | } | 
|  | mController.setTime(start.normalize(true)); | 
|  |  | 
|  | Time newSelected = start; | 
|  |  | 
|  | if (mNumDays == 7) { | 
|  | newSelected = new Time(start); | 
|  | adjustToBeginningOfWeek(start); | 
|  | } | 
|  |  | 
|  | final Time end = new Time(start); | 
|  | end.monthDay += mNumDays - 1; | 
|  |  | 
|  | // We have to allocate these animation objects each time we switch views | 
|  | // because that is the only way to set the animation parameters. | 
|  | TranslateAnimation inAnimation = new TranslateAnimation( | 
|  | Animation.RELATIVE_TO_SELF, inFromXValue, | 
|  | Animation.RELATIVE_TO_SELF, inToXValue, | 
|  | Animation.ABSOLUTE, 0.0f, | 
|  | Animation.ABSOLUTE, 0.0f); | 
|  |  | 
|  | TranslateAnimation outAnimation = new TranslateAnimation( | 
|  | Animation.RELATIVE_TO_SELF, outFromXValue, | 
|  | Animation.RELATIVE_TO_SELF, outToXValue, | 
|  | Animation.ABSOLUTE, 0.0f, | 
|  | Animation.ABSOLUTE, 0.0f); | 
|  |  | 
|  | long duration = calculateDuration(width - Math.abs(xOffSet), width, velocity); | 
|  | inAnimation.setDuration(duration); | 
|  | inAnimation.setInterpolator(mHScrollInterpolator); | 
|  | outAnimation.setInterpolator(mHScrollInterpolator); | 
|  | outAnimation.setDuration(duration); | 
|  | outAnimation.setAnimationListener(new GotoBroadcaster(start, end)); | 
|  | mViewSwitcher.setInAnimation(inAnimation); | 
|  | mViewSwitcher.setOutAnimation(outAnimation); | 
|  |  | 
|  | DayView view = (DayView) mViewSwitcher.getCurrentView(); | 
|  | view.cleanup(); | 
|  | mViewSwitcher.showNext(); | 
|  | view = (DayView) mViewSwitcher.getCurrentView(); | 
|  | view.setSelected(newSelected, true, false); | 
|  | view.requestFocus(); | 
|  | view.reloadEvents(); | 
|  | view.updateTitle(); | 
|  | view.restartCurrentTimeUpdates(); | 
|  |  | 
|  | return view; | 
|  | } | 
|  |  | 
|  | // This is called after scrolling stops to move the selected hour | 
|  | // to the visible part of the screen. | 
|  | private void resetSelectedHour() { | 
|  | if (mSelectionHour < mFirstHour + 1) { | 
|  | setSelectedHour(mFirstHour + 1); | 
|  | setSelectedEvent(null); | 
|  | mSelectedEvents.clear(); | 
|  | mComputeSelectedEvents = true; | 
|  | } else if (mSelectionHour > mFirstHour + mNumHours - 3) { | 
|  | setSelectedHour(mFirstHour + mNumHours - 3); | 
|  | setSelectedEvent(null); | 
|  | mSelectedEvents.clear(); | 
|  | mComputeSelectedEvents = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | private void initFirstHour() { | 
|  | mFirstHour = mSelectionHour - mNumHours / 5; | 
|  | if (mFirstHour < 0) { | 
|  | mFirstHour = 0; | 
|  | } else if (mFirstHour + mNumHours > 24) { | 
|  | mFirstHour = 24 - mNumHours; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Recomputes the first full hour that is visible on screen after the | 
|  | * screen is scrolled. | 
|  | */ | 
|  | private void computeFirstHour() { | 
|  | // Compute the first full hour that is visible on screen | 
|  | mFirstHour = (mViewStartY + mCellHeight + HOUR_GAP - 1) / (mCellHeight + HOUR_GAP); | 
|  | mFirstHourOffset = mFirstHour * (mCellHeight + HOUR_GAP) - mViewStartY; | 
|  | } | 
|  |  | 
|  | private void adjustHourSelection() { | 
|  | if (mSelectionHour < 0) { | 
|  | setSelectedHour(0); | 
|  | if (mMaxAlldayEvents > 0) { | 
|  | mPrevSelectedEvent = null; | 
|  | mSelectionAllday = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mSelectionHour > 23) { | 
|  | setSelectedHour(23); | 
|  | } | 
|  |  | 
|  | // If the selected hour is at least 2 time slots from the top and | 
|  | // bottom of the screen, then don't scroll the view. | 
|  | if (mSelectionHour < mFirstHour + 1) { | 
|  | // If there are all-days events for the selected day but there | 
|  | // are no more normal events earlier in the day, then jump to | 
|  | // the all-day event area. | 
|  | // Exception 1: allow the user to scroll to 8am with the trackball | 
|  | // before jumping to the all-day event area. | 
|  | // Exception 2: if 12am is on screen, then allow the user to select | 
|  | // 12am before going up to the all-day event area. | 
|  | int daynum = mSelectionDay - mFirstJulianDay; | 
|  | if (mMaxAlldayEvents > 0 && mEarliestStartHour[daynum] > mSelectionHour | 
|  | && mFirstHour > 0 && mFirstHour < 8) { | 
|  | mPrevSelectedEvent = null; | 
|  | mSelectionAllday = true; | 
|  | setSelectedHour(mFirstHour + 1); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (mFirstHour > 0) { | 
|  | mFirstHour -= 1; | 
|  | mViewStartY -= (mCellHeight + HOUR_GAP); | 
|  | if (mViewStartY < 0) { | 
|  | mViewStartY = 0; | 
|  | } | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mSelectionHour > mFirstHour + mNumHours - 3) { | 
|  | if (mFirstHour < 24 - mNumHours) { | 
|  | mFirstHour += 1; | 
|  | mViewStartY += (mCellHeight + HOUR_GAP); | 
|  | if (mViewStartY > mMaxViewStartY) { | 
|  | mViewStartY = mMaxViewStartY; | 
|  | } | 
|  | return; | 
|  | } else if (mFirstHour == 24 - mNumHours && mFirstHourOffset > 0) { | 
|  | mViewStartY = mMaxViewStartY; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void clearCachedEvents() { | 
|  | mLastReloadMillis = 0; | 
|  | } | 
|  |  | 
|  | private final Runnable mCancelCallback = new Runnable() { | 
|  | public void run() { | 
|  | clearCachedEvents(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | /* package */ void reloadEvents() { | 
|  | // Protect against this being called before this view has been | 
|  | // initialized. | 
|  | //        if (mContext == null) { | 
|  | //            return; | 
|  | //        } | 
|  |  | 
|  | // Make sure our time zones are up to date | 
|  | mTZUpdater.run(); | 
|  |  | 
|  | setSelectedEvent(null); | 
|  | mPrevSelectedEvent = null; | 
|  | mSelectedEvents.clear(); | 
|  |  | 
|  | // The start date is the beginning of the week at 12am | 
|  | Time weekStart = new Time(Utils.getTimeZone(mContext, mTZUpdater)); | 
|  | weekStart.set(mBaseDate); | 
|  | weekStart.hour = 0; | 
|  | weekStart.minute = 0; | 
|  | weekStart.second = 0; | 
|  | long millis = weekStart.normalize(true /* ignore isDst */); | 
|  |  | 
|  | // Avoid reloading events unnecessarily. | 
|  | if (millis == mLastReloadMillis) { | 
|  | return; | 
|  | } | 
|  | mLastReloadMillis = millis; | 
|  |  | 
|  | // load events in the background | 
|  | //        mContext.startProgressSpinner(); | 
|  | final ArrayList<Event> events = new ArrayList<Event>(); | 
|  | mEventLoader.loadEventsInBackground(mNumDays, events, mFirstJulianDay, new Runnable() { | 
|  | public void run() { | 
|  | boolean fadeinEvents = mFirstJulianDay != mLoadedFirstJulianDay; | 
|  | mEvents = events; | 
|  | mLoadedFirstJulianDay = mFirstJulianDay; | 
|  | if (mAllDayEvents == null) { | 
|  | mAllDayEvents = new ArrayList<Event>(); | 
|  | } else { | 
|  | mAllDayEvents.clear(); | 
|  | } | 
|  |  | 
|  | // Create a shorter array for all day events | 
|  | for (Event e : events) { | 
|  | if (e.drawAsAllday()) { | 
|  | mAllDayEvents.add(e); | 
|  | } | 
|  | } | 
|  |  | 
|  | // New events, new layouts | 
|  | if (mLayouts == null || mLayouts.length < events.size()) { | 
|  | mLayouts = new StaticLayout[events.size()]; | 
|  | } else { | 
|  | Arrays.fill(mLayouts, null); | 
|  | } | 
|  |  | 
|  | if (mAllDayLayouts == null || mAllDayLayouts.length < mAllDayEvents.size()) { | 
|  | mAllDayLayouts = new StaticLayout[events.size()]; | 
|  | } else { | 
|  | Arrays.fill(mAllDayLayouts, null); | 
|  | } | 
|  |  | 
|  | computeEventRelations(); | 
|  |  | 
|  | mRemeasure = true; | 
|  | mComputeSelectedEvents = true; | 
|  | recalc(); | 
|  |  | 
|  | // Start animation to cross fade the events | 
|  | if (fadeinEvents) { | 
|  | if (mEventsCrossFadeAnimation == null) { | 
|  | mEventsCrossFadeAnimation = | 
|  | ObjectAnimator.ofInt(DayView.this, "EventsAlpha", 0, 255); | 
|  | mEventsCrossFadeAnimation.setDuration(EVENTS_CROSS_FADE_DURATION); | 
|  | } | 
|  | mEventsCrossFadeAnimation.start(); | 
|  | } else{ | 
|  | invalidate(); | 
|  | } | 
|  | } | 
|  | }, mCancelCallback); | 
|  | } | 
|  |  | 
|  | public void setEventsAlpha(int alpha) { | 
|  | mEventsAlpha = alpha; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | public int getEventsAlpha() { | 
|  | return mEventsAlpha; | 
|  | } | 
|  |  | 
|  | public void stopEventsAnimation() { | 
|  | if (mEventsCrossFadeAnimation != null) { | 
|  | mEventsCrossFadeAnimation.cancel(); | 
|  | } | 
|  | mEventsAlpha = 255; | 
|  | } | 
|  |  | 
|  | private void computeEventRelations() { | 
|  | // Compute the layout relation between each event before measuring cell | 
|  | // width, as the cell width should be adjusted along with the relation. | 
|  | // | 
|  | // Examples: A (1:00pm - 1:01pm), B (1:02pm - 2:00pm) | 
|  | // We should mark them as "overwapped". Though they are not overwapped logically, but | 
|  | // minimum cell height implicitly expands the cell height of A and it should look like | 
|  | // (1:00pm - 1:15pm) after the cell height adjustment. | 
|  |  | 
|  | // Compute the space needed for the all-day events, if any. | 
|  | // Make a pass over all the events, and keep track of the maximum | 
|  | // number of all-day events in any one day.  Also, keep track of | 
|  | // the earliest event in each day. | 
|  | int maxAllDayEvents = 0; | 
|  | final ArrayList<Event> events = mEvents; | 
|  | final int len = events.size(); | 
|  | // Num of all-day-events on each day. | 
|  | final int eventsCount[] = new int[mLastJulianDay - mFirstJulianDay + 1]; | 
|  | Arrays.fill(eventsCount, 0); | 
|  | for (int ii = 0; ii < len; ii++) { | 
|  | Event event = events.get(ii); | 
|  | if (event.startDay > mLastJulianDay || event.endDay < mFirstJulianDay) { | 
|  | continue; | 
|  | } | 
|  | if (event.drawAsAllday()) { | 
|  | // Count all the events being drawn as allDay events | 
|  | final int firstDay = Math.max(event.startDay, mFirstJulianDay); | 
|  | final int lastDay = Math.min(event.endDay, mLastJulianDay); | 
|  | for (int day = firstDay; day <= lastDay; day++) { | 
|  | final int count = ++eventsCount[day - mFirstJulianDay]; | 
|  | if (maxAllDayEvents < count) { | 
|  | maxAllDayEvents = count; | 
|  | } | 
|  | } | 
|  |  | 
|  | int daynum = event.startDay - mFirstJulianDay; | 
|  | int durationDays = event.endDay - event.startDay + 1; | 
|  | if (daynum < 0) { | 
|  | durationDays += daynum; | 
|  | daynum = 0; | 
|  | } | 
|  | if (daynum + durationDays > mNumDays) { | 
|  | durationDays = mNumDays - daynum; | 
|  | } | 
|  | for (int day = daynum; durationDays > 0; day++, durationDays--) { | 
|  | mHasAllDayEvent[day] = true; | 
|  | } | 
|  | } else { | 
|  | int daynum = event.startDay - mFirstJulianDay; | 
|  | int hour = event.startTime / 60; | 
|  | if (daynum >= 0 && hour < mEarliestStartHour[daynum]) { | 
|  | mEarliestStartHour[daynum] = hour; | 
|  | } | 
|  |  | 
|  | // Also check the end hour in case the event spans more than | 
|  | // one day. | 
|  | daynum = event.endDay - mFirstJulianDay; | 
|  | hour = event.endTime / 60; | 
|  | if (daynum < mNumDays && hour < mEarliestStartHour[daynum]) { | 
|  | mEarliestStartHour[daynum] = hour; | 
|  | } | 
|  | } | 
|  | } | 
|  | mMaxAlldayEvents = maxAllDayEvents; | 
|  | initAllDayHeights(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onDraw(Canvas canvas) { | 
|  | if (mRemeasure) { | 
|  | remeasure(getWidth(), getHeight()); | 
|  | mRemeasure = false; | 
|  | } | 
|  | canvas.save(); | 
|  |  | 
|  | float yTranslate = -mViewStartY + DAY_HEADER_HEIGHT + mAlldayHeight; | 
|  | // offset canvas by the current drag and header position | 
|  | canvas.translate(-mViewStartX, yTranslate); | 
|  | // clip to everything below the allDay area | 
|  | Rect dest = mDestRect; | 
|  | dest.top = (int) (mFirstCell - yTranslate); | 
|  | dest.bottom = (int) (mViewHeight - yTranslate); | 
|  | dest.left = 0; | 
|  | dest.right = mViewWidth; | 
|  | canvas.save(); | 
|  | canvas.clipRect(dest); | 
|  | // Draw the movable part of the view | 
|  | doDraw(canvas); | 
|  | // restore to having no clip | 
|  | canvas.restore(); | 
|  |  | 
|  | if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { | 
|  | float xTranslate; | 
|  | if (mViewStartX > 0) { | 
|  | xTranslate = mViewWidth; | 
|  | } else { | 
|  | xTranslate = -mViewWidth; | 
|  | } | 
|  | // Move the canvas around to prep it for the next view | 
|  | // specifically, shift it by a screen and undo the | 
|  | // yTranslation which will be redone in the nextView's onDraw(). | 
|  | canvas.translate(xTranslate, -yTranslate); | 
|  | DayView nextView = (DayView) mViewSwitcher.getNextView(); | 
|  |  | 
|  | // Prevent infinite recursive calls to onDraw(). | 
|  | nextView.mTouchMode = TOUCH_MODE_INITIAL_STATE; | 
|  |  | 
|  | nextView.onDraw(canvas); | 
|  | // Move it back for this view | 
|  | canvas.translate(-xTranslate, 0); | 
|  | } else { | 
|  | // If we drew another view we already translated it back | 
|  | // If we didn't draw another view we should be at the edge of the | 
|  | // screen | 
|  | canvas.translate(mViewStartX, -yTranslate); | 
|  | } | 
|  |  | 
|  | // Draw the fixed areas (that don't scroll) directly to the canvas. | 
|  | drawAfterScroll(canvas); | 
|  | if (mComputeSelectedEvents && mUpdateToast) { | 
|  | updateEventDetails(); | 
|  | mUpdateToast = false; | 
|  | } | 
|  | mComputeSelectedEvents = false; | 
|  |  | 
|  | // Draw overscroll glow | 
|  | if (!mEdgeEffectTop.isFinished()) { | 
|  | if (DAY_HEADER_HEIGHT != 0) { | 
|  | canvas.translate(0, DAY_HEADER_HEIGHT); | 
|  | } | 
|  | if (mEdgeEffectTop.draw(canvas)) { | 
|  | invalidate(); | 
|  | } | 
|  | if (DAY_HEADER_HEIGHT != 0) { | 
|  | canvas.translate(0, -DAY_HEADER_HEIGHT); | 
|  | } | 
|  | } | 
|  | if (!mEdgeEffectBottom.isFinished()) { | 
|  | canvas.rotate(180, mViewWidth/2, mViewHeight/2); | 
|  | if (mEdgeEffectBottom.draw(canvas)) { | 
|  | invalidate(); | 
|  | } | 
|  | } | 
|  | canvas.restore(); | 
|  | } | 
|  |  | 
|  | private void drawAfterScroll(Canvas canvas) { | 
|  | Paint p = mPaint; | 
|  | Rect r = mRect; | 
|  |  | 
|  | drawAllDayHighlights(r, canvas, p); | 
|  | if (mMaxAlldayEvents != 0) { | 
|  | drawAllDayEvents(mFirstJulianDay, mNumDays, canvas, p); | 
|  | drawUpperLeftCorner(r, canvas, p); | 
|  | } | 
|  |  | 
|  | drawScrollLine(r, canvas, p); | 
|  | drawDayHeaderLoop(r, canvas, p); | 
|  |  | 
|  | // Draw the AM and PM indicators if we're in 12 hour mode | 
|  | if (!mIs24HourFormat) { | 
|  | drawAmPm(canvas, p); | 
|  | } | 
|  | } | 
|  |  | 
|  | // This isn't really the upper-left corner. It's the square area just | 
|  | // below the upper-left corner, above the hours and to the left of the | 
|  | // all-day area. | 
|  | private void drawUpperLeftCorner(Rect r, Canvas canvas, Paint p) { | 
|  | setupHourTextPaint(p); | 
|  | if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { | 
|  | // Draw the allDay expand/collapse icon | 
|  | if (mUseExpandIcon) { | 
|  | mExpandAlldayDrawable.setBounds(mExpandAllDayRect); | 
|  | mExpandAlldayDrawable.draw(canvas); | 
|  | } else { | 
|  | mCollapseAlldayDrawable.setBounds(mExpandAllDayRect); | 
|  | mCollapseAlldayDrawable.draw(canvas); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private void drawScrollLine(Rect r, Canvas canvas, Paint p) { | 
|  | final int right = computeDayLeftPosition(mNumDays); | 
|  | final int y = mFirstCell - 1; | 
|  |  | 
|  | p.setAntiAlias(false); | 
|  | p.setStyle(Style.FILL); | 
|  |  | 
|  | p.setColor(mCalendarGridLineInnerHorizontalColor); | 
|  | p.setStrokeWidth(GRID_LINE_INNER_WIDTH); | 
|  | canvas.drawLine(GRID_LINE_LEFT_MARGIN, y, right, y, p); | 
|  | p.setAntiAlias(true); | 
|  | } | 
|  |  | 
|  | // Computes the x position for the left side of the given day (base 0) | 
|  | private int computeDayLeftPosition(int day) { | 
|  | int effectiveWidth = mViewWidth - mHoursWidth; | 
|  | return day * effectiveWidth / mNumDays + mHoursWidth; | 
|  | } | 
|  |  | 
|  | private void drawAllDayHighlights(Rect r, Canvas canvas, Paint p) { | 
|  | if (mFutureBgColor != 0) { | 
|  | // First, color the labels area light gray | 
|  | r.top = 0; | 
|  | r.bottom = DAY_HEADER_HEIGHT; | 
|  | r.left = 0; | 
|  | r.right = mViewWidth; | 
|  | p.setColor(mBgColor); | 
|  | p.setStyle(Style.FILL); | 
|  | canvas.drawRect(r, p); | 
|  | // and the area that says All day | 
|  | r.top = DAY_HEADER_HEIGHT; | 
|  | r.bottom = mFirstCell - 1; | 
|  | r.left = 0; | 
|  | r.right = mHoursWidth; | 
|  | canvas.drawRect(r, p); | 
|  |  | 
|  | int startIndex = -1; | 
|  |  | 
|  | int todayIndex = mTodayJulianDay - mFirstJulianDay; | 
|  | if (todayIndex < 0) { | 
|  | // Future | 
|  | startIndex = 0; | 
|  | } else if (todayIndex >= 1 && todayIndex + 1 < mNumDays) { | 
|  | // Multiday - tomorrow is visible. | 
|  | startIndex = todayIndex + 1; | 
|  | } | 
|  |  | 
|  | if (startIndex >= 0) { | 
|  | // Draw the future highlight | 
|  | r.top = 0; | 
|  | r.bottom = mFirstCell - 1; | 
|  | r.left = computeDayLeftPosition(startIndex) + 1; | 
|  | r.right = computeDayLeftPosition(mNumDays); | 
|  | p.setColor(mFutureBgColor); | 
|  | p.setStyle(Style.FILL); | 
|  | canvas.drawRect(r, p); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mSelectionAllday && mSelectionMode != SELECTION_HIDDEN) { | 
|  | // Draw the selection highlight on the selected all-day area | 
|  | mRect.top = DAY_HEADER_HEIGHT + 1; | 
|  | mRect.bottom = mRect.top + mAlldayHeight + ALLDAY_TOP_MARGIN - 2; | 
|  | int daynum = mSelectionDay - mFirstJulianDay; | 
|  | mRect.left = computeDayLeftPosition(daynum) + 1; | 
|  | mRect.right = computeDayLeftPosition(daynum + 1); | 
|  | p.setColor(mCalendarGridAreaSelected); | 
|  | canvas.drawRect(mRect, p); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void drawDayHeaderLoop(Rect r, Canvas canvas, Paint p) { | 
|  | // Draw the horizontal day background banner | 
|  | // p.setColor(mCalendarDateBannerBackground); | 
|  | // r.top = 0; | 
|  | // r.bottom = DAY_HEADER_HEIGHT; | 
|  | // r.left = 0; | 
|  | // r.right = mHoursWidth + mNumDays * (mCellWidth + DAY_GAP); | 
|  | // canvas.drawRect(r, p); | 
|  | // | 
|  | // Fill the extra space on the right side with the default background | 
|  | // r.left = r.right; | 
|  | // r.right = mViewWidth; | 
|  | // p.setColor(mCalendarGridAreaBackground); | 
|  | // canvas.drawRect(r, p); | 
|  | if (mNumDays == 1 && ONE_DAY_HEADER_HEIGHT == 0) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | p.setTypeface(mBold); | 
|  | p.setTextAlign(Paint.Align.RIGHT); | 
|  | int cell = mFirstJulianDay; | 
|  |  | 
|  | String[] dayNames; | 
|  | if (mDateStrWidth < mCellWidth) { | 
|  | dayNames = mDayStrs; | 
|  | } else { | 
|  | dayNames = mDayStrs2Letter; | 
|  | } | 
|  |  | 
|  | p.setAntiAlias(true); | 
|  | for (int day = 0; day < mNumDays; day++, cell++) { | 
|  | int dayOfWeek = day + mFirstVisibleDayOfWeek; | 
|  | if (dayOfWeek >= 14) { | 
|  | dayOfWeek -= 14; | 
|  | } | 
|  |  | 
|  | int color = mCalendarDateBannerTextColor; | 
|  | if (mNumDays == 1) { | 
|  | if (dayOfWeek == Time.SATURDAY) { | 
|  | color = mWeek_saturdayColor; | 
|  | } else if (dayOfWeek == Time.SUNDAY) { | 
|  | color = mWeek_sundayColor; | 
|  | } | 
|  | } else { | 
|  | final int column = day % 7; | 
|  | if (Utils.isSaturday(column, mFirstDayOfWeek)) { | 
|  | color = mWeek_saturdayColor; | 
|  | } else if (Utils.isSunday(column, mFirstDayOfWeek)) { | 
|  | color = mWeek_sundayColor; | 
|  | } | 
|  | } | 
|  |  | 
|  | p.setColor(color); | 
|  | drawDayHeader(dayNames[dayOfWeek], day, cell, canvas, p); | 
|  | } | 
|  | p.setTypeface(null); | 
|  | } | 
|  |  | 
|  | private void drawAmPm(Canvas canvas, Paint p) { | 
|  | p.setColor(mCalendarAmPmLabel); | 
|  | p.setTextSize(AMPM_TEXT_SIZE); | 
|  | p.setTypeface(mBold); | 
|  | p.setAntiAlias(true); | 
|  | p.setTextAlign(Paint.Align.RIGHT); | 
|  | String text = mAmString; | 
|  | if (mFirstHour >= 12) { | 
|  | text = mPmString; | 
|  | } | 
|  | int y = mFirstCell + mFirstHourOffset + 2 * mHoursTextHeight + HOUR_GAP; | 
|  | canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); | 
|  |  | 
|  | if (mFirstHour < 12 && mFirstHour + mNumHours > 12) { | 
|  | // Also draw the "PM" | 
|  | text = mPmString; | 
|  | y = mFirstCell + mFirstHourOffset + (12 - mFirstHour) * (mCellHeight + HOUR_GAP) | 
|  | + 2 * mHoursTextHeight + HOUR_GAP; | 
|  | canvas.drawText(text, HOURS_LEFT_MARGIN, y, p); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void drawCurrentTimeLine(Rect r, final int day, final int top, Canvas canvas, | 
|  | Paint p) { | 
|  | r.left = computeDayLeftPosition(day) - CURRENT_TIME_LINE_SIDE_BUFFER + 1; | 
|  | r.right = computeDayLeftPosition(day + 1) + CURRENT_TIME_LINE_SIDE_BUFFER + 1; | 
|  |  | 
|  | r.top = top - CURRENT_TIME_LINE_TOP_OFFSET; | 
|  | r.bottom = r.top + mCurrentTimeLine.getIntrinsicHeight(); | 
|  |  | 
|  | mCurrentTimeLine.setBounds(r); | 
|  | mCurrentTimeLine.draw(canvas); | 
|  | if (mAnimateToday) { | 
|  | mCurrentTimeAnimateLine.setBounds(r); | 
|  | mCurrentTimeAnimateLine.setAlpha(mAnimateTodayAlpha); | 
|  | mCurrentTimeAnimateLine.draw(canvas); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void doDraw(Canvas canvas) { | 
|  | Paint p = mPaint; | 
|  | Rect r = mRect; | 
|  |  | 
|  | if (mFutureBgColor != 0) { | 
|  | drawBgColors(r, canvas, p); | 
|  | } | 
|  | drawGridBackground(r, canvas, p); | 
|  | drawHours(r, canvas, p); | 
|  |  | 
|  | // Draw each day | 
|  | int cell = mFirstJulianDay; | 
|  | p.setAntiAlias(false); | 
|  | int alpha = p.getAlpha(); | 
|  | p.setAlpha(mEventsAlpha); | 
|  | for (int day = 0; day < mNumDays; day++, cell++) { | 
|  | // TODO Wow, this needs cleanup. drawEvents loop through all the | 
|  | // events on every call. | 
|  | drawEvents(cell, day, HOUR_GAP, canvas, p); | 
|  | // If this is today | 
|  | if (cell == mTodayJulianDay) { | 
|  | int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) | 
|  | + ((mCurrentTime.minute * mCellHeight) / 60) + 1; | 
|  |  | 
|  | // And the current time shows up somewhere on the screen | 
|  | if (lineY >= mViewStartY && lineY < mViewStartY + mViewHeight - 2) { | 
|  | drawCurrentTimeLine(r, day, lineY, canvas, p); | 
|  | } | 
|  | } | 
|  | } | 
|  | p.setAntiAlias(true); | 
|  | p.setAlpha(alpha); | 
|  |  | 
|  | drawSelectedRect(r, canvas, p); | 
|  | } | 
|  |  | 
|  | private void drawSelectedRect(Rect r, Canvas canvas, Paint p) { | 
|  | // Draw a highlight on the selected hour (if needed) | 
|  | if (mSelectionMode != SELECTION_HIDDEN && !mSelectionAllday) { | 
|  | int daynum = mSelectionDay - mFirstJulianDay; | 
|  | r.top = mSelectionHour * (mCellHeight + HOUR_GAP); | 
|  | r.bottom = r.top + mCellHeight + HOUR_GAP; | 
|  | r.left = computeDayLeftPosition(daynum) + 1; | 
|  | r.right = computeDayLeftPosition(daynum + 1) + 1; | 
|  |  | 
|  | saveSelectionPosition(r.left, r.top, r.right, r.bottom); | 
|  |  | 
|  | // Draw the highlight on the grid | 
|  | p.setColor(mCalendarGridAreaSelected); | 
|  | r.top += HOUR_GAP; | 
|  | r.right -= DAY_GAP; | 
|  | p.setAntiAlias(false); | 
|  | canvas.drawRect(r, p); | 
|  |  | 
|  | // Draw a "new event hint" on top of the highlight | 
|  | // For the week view, show a "+", for day view, show "+ New event" | 
|  | p.setColor(mNewEventHintColor); | 
|  | if (mNumDays > 1) { | 
|  | p.setStrokeWidth(NEW_EVENT_WIDTH); | 
|  | int width = r.right - r.left; | 
|  | int midX = r.left + width / 2; | 
|  | int midY = r.top + mCellHeight / 2; | 
|  | int length = Math.min(mCellHeight, width) - NEW_EVENT_MARGIN * 2; | 
|  | length = Math.min(length, NEW_EVENT_MAX_LENGTH); | 
|  | int verticalPadding = (mCellHeight - length) / 2; | 
|  | int horizontalPadding = (width - length) / 2; | 
|  | canvas.drawLine(r.left + horizontalPadding, midY, r.right - horizontalPadding, | 
|  | midY, p); | 
|  | canvas.drawLine(midX, r.top + verticalPadding, midX, r.bottom - verticalPadding, p); | 
|  | } else { | 
|  | p.setStyle(Paint.Style.FILL); | 
|  | p.setTextSize(NEW_EVENT_HINT_FONT_SIZE); | 
|  | p.setTextAlign(Paint.Align.LEFT); | 
|  | p.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); | 
|  | canvas.drawText(mNewEventHintString, r.left + EVENT_TEXT_LEFT_MARGIN, | 
|  | r.top + Math.abs(p.getFontMetrics().ascent) + EVENT_TEXT_TOP_MARGIN , p); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private void drawHours(Rect r, Canvas canvas, Paint p) { | 
|  | setupHourTextPaint(p); | 
|  |  | 
|  | int y = HOUR_GAP + mHoursTextHeight + HOURS_TOP_MARGIN; | 
|  |  | 
|  | for (int i = 0; i < 24; i++) { | 
|  | String time = mHourStrs[i]; | 
|  | canvas.drawText(time, HOURS_LEFT_MARGIN, y, p); | 
|  | y += mCellHeight + HOUR_GAP; | 
|  | } | 
|  | } | 
|  |  | 
|  | private void setupHourTextPaint(Paint p) { | 
|  | p.setColor(mCalendarHourLabelColor); | 
|  | p.setTextSize(HOURS_TEXT_SIZE); | 
|  | p.setTypeface(Typeface.DEFAULT); | 
|  | p.setTextAlign(Paint.Align.RIGHT); | 
|  | p.setAntiAlias(true); | 
|  | } | 
|  |  | 
|  | private void drawDayHeader(String dayStr, int day, int cell, Canvas canvas, Paint p) { | 
|  | int dateNum = mFirstVisibleDate + day; | 
|  | int x; | 
|  | if (dateNum > mMonthLength) { | 
|  | dateNum -= mMonthLength; | 
|  | } | 
|  | p.setAntiAlias(true); | 
|  |  | 
|  | int todayIndex = mTodayJulianDay - mFirstJulianDay; | 
|  | // Draw day of the month | 
|  | String dateNumStr = String.valueOf(dateNum); | 
|  | if (mNumDays > 1) { | 
|  | float y = DAY_HEADER_HEIGHT - DAY_HEADER_BOTTOM_MARGIN; | 
|  |  | 
|  | // Draw day of the month | 
|  | x = computeDayLeftPosition(day + 1) - DAY_HEADER_RIGHT_MARGIN; | 
|  | p.setTextAlign(Align.RIGHT); | 
|  | p.setTextSize(DATE_HEADER_FONT_SIZE); | 
|  |  | 
|  | p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); | 
|  | canvas.drawText(dateNumStr, x, y, p); | 
|  |  | 
|  | // Draw day of the week | 
|  | x -= p.measureText(" " + dateNumStr); | 
|  | p.setTextSize(DAY_HEADER_FONT_SIZE); | 
|  | p.setTypeface(Typeface.DEFAULT); | 
|  | canvas.drawText(dayStr, x, y, p); | 
|  | } else { | 
|  | float y = ONE_DAY_HEADER_HEIGHT - DAY_HEADER_ONE_DAY_BOTTOM_MARGIN; | 
|  | p.setTextAlign(Align.LEFT); | 
|  |  | 
|  |  | 
|  | // Draw day of the week | 
|  | x = computeDayLeftPosition(day) + DAY_HEADER_ONE_DAY_LEFT_MARGIN; | 
|  | p.setTextSize(DAY_HEADER_FONT_SIZE); | 
|  | p.setTypeface(Typeface.DEFAULT); | 
|  | canvas.drawText(dayStr, x, y, p); | 
|  |  | 
|  | // Draw day of the month | 
|  | x += p.measureText(dayStr) + DAY_HEADER_ONE_DAY_RIGHT_MARGIN; | 
|  | p.setTextSize(DATE_HEADER_FONT_SIZE); | 
|  | p.setTypeface(todayIndex == day ? mBold : Typeface.DEFAULT); | 
|  | canvas.drawText(dateNumStr, x, y, p); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void drawGridBackground(Rect r, Canvas canvas, Paint p) { | 
|  | Paint.Style savedStyle = p.getStyle(); | 
|  |  | 
|  | final float stopX = computeDayLeftPosition(mNumDays); | 
|  | float y = 0; | 
|  | final float deltaY = mCellHeight + HOUR_GAP; | 
|  | int linesIndex = 0; | 
|  | final float startY = 0; | 
|  | final float stopY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP); | 
|  | float x = mHoursWidth; | 
|  |  | 
|  | // Draw the inner horizontal grid lines | 
|  | p.setColor(mCalendarGridLineInnerHorizontalColor); | 
|  | p.setStrokeWidth(GRID_LINE_INNER_WIDTH); | 
|  | p.setAntiAlias(false); | 
|  | y = 0; | 
|  | linesIndex = 0; | 
|  | for (int hour = 0; hour <= 24; hour++) { | 
|  | mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; | 
|  | mLines[linesIndex++] = y; | 
|  | mLines[linesIndex++] = stopX; | 
|  | mLines[linesIndex++] = y; | 
|  | y += deltaY; | 
|  | } | 
|  | if (mCalendarGridLineInnerVerticalColor != mCalendarGridLineInnerHorizontalColor) { | 
|  | canvas.drawLines(mLines, 0, linesIndex, p); | 
|  | linesIndex = 0; | 
|  | p.setColor(mCalendarGridLineInnerVerticalColor); | 
|  | } | 
|  |  | 
|  | // Draw the inner vertical grid lines | 
|  | for (int day = 0; day <= mNumDays; day++) { | 
|  | x = computeDayLeftPosition(day); | 
|  | mLines[linesIndex++] = x; | 
|  | mLines[linesIndex++] = startY; | 
|  | mLines[linesIndex++] = x; | 
|  | mLines[linesIndex++] = stopY; | 
|  | } | 
|  | canvas.drawLines(mLines, 0, linesIndex, p); | 
|  |  | 
|  | // Restore the saved style. | 
|  | p.setStyle(savedStyle); | 
|  | p.setAntiAlias(true); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param r | 
|  | * @param canvas | 
|  | * @param p | 
|  | */ | 
|  | private void drawBgColors(Rect r, Canvas canvas, Paint p) { | 
|  | int todayIndex = mTodayJulianDay - mFirstJulianDay; | 
|  | // Draw the hours background color | 
|  | r.top = mDestRect.top; | 
|  | r.bottom = mDestRect.bottom; | 
|  | r.left = 0; | 
|  | r.right = mHoursWidth; | 
|  | p.setColor(mBgColor); | 
|  | p.setStyle(Style.FILL); | 
|  | p.setAntiAlias(false); | 
|  | canvas.drawRect(r, p); | 
|  |  | 
|  | // Draw background for grid area | 
|  | if (mNumDays == 1 && todayIndex == 0) { | 
|  | // Draw a white background for the time later than current time | 
|  | int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) | 
|  | + ((mCurrentTime.minute * mCellHeight) / 60) + 1; | 
|  | if (lineY < mViewStartY + mViewHeight) { | 
|  | lineY = Math.max(lineY, mViewStartY); | 
|  | r.left = mHoursWidth; | 
|  | r.right = mViewWidth; | 
|  | r.top = lineY; | 
|  | r.bottom = mViewStartY + mViewHeight; | 
|  | p.setColor(mFutureBgColor); | 
|  | canvas.drawRect(r, p); | 
|  | } | 
|  | } else if (todayIndex >= 0 && todayIndex < mNumDays) { | 
|  | // Draw today with a white background for the time later than current time | 
|  | int lineY = mCurrentTime.hour * (mCellHeight + HOUR_GAP) | 
|  | + ((mCurrentTime.minute * mCellHeight) / 60) + 1; | 
|  | if (lineY < mViewStartY + mViewHeight) { | 
|  | lineY = Math.max(lineY, mViewStartY); | 
|  | r.left = computeDayLeftPosition(todayIndex) + 1; | 
|  | r.right = computeDayLeftPosition(todayIndex + 1); | 
|  | r.top = lineY; | 
|  | r.bottom = mViewStartY + mViewHeight; | 
|  | p.setColor(mFutureBgColor); | 
|  | canvas.drawRect(r, p); | 
|  | } | 
|  |  | 
|  | // Paint Tomorrow and later days with future color | 
|  | if (todayIndex + 1 < mNumDays) { | 
|  | r.left = computeDayLeftPosition(todayIndex + 1) + 1; | 
|  | r.right = computeDayLeftPosition(mNumDays); | 
|  | r.top = mDestRect.top; | 
|  | r.bottom = mDestRect.bottom; | 
|  | p.setColor(mFutureBgColor); | 
|  | canvas.drawRect(r, p); | 
|  | } | 
|  | } else if (todayIndex < 0) { | 
|  | // Future | 
|  | r.left = computeDayLeftPosition(0) + 1; | 
|  | r.right = computeDayLeftPosition(mNumDays); | 
|  | r.top = mDestRect.top; | 
|  | r.bottom = mDestRect.bottom; | 
|  | p.setColor(mFutureBgColor); | 
|  | canvas.drawRect(r, p); | 
|  | } | 
|  | p.setAntiAlias(true); | 
|  | } | 
|  |  | 
|  | Event getSelectedEvent() { | 
|  | if (mSelectedEvent == null) { | 
|  | // There is no event at the selected hour, so create a new event. | 
|  | return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), | 
|  | getSelectedMinutesSinceMidnight()); | 
|  | } | 
|  | return mSelectedEvent; | 
|  | } | 
|  |  | 
|  | boolean isEventSelected() { | 
|  | return (mSelectedEvent != null); | 
|  | } | 
|  |  | 
|  | Event getNewEvent() { | 
|  | return getNewEvent(mSelectionDay, getSelectedTimeInMillis(), | 
|  | getSelectedMinutesSinceMidnight()); | 
|  | } | 
|  |  | 
|  | static Event getNewEvent(int julianDay, long utcMillis, | 
|  | int minutesSinceMidnight) { | 
|  | Event event = Event.newInstance(); | 
|  | event.startDay = julianDay; | 
|  | event.endDay = julianDay; | 
|  | event.startMillis = utcMillis; | 
|  | event.endMillis = event.startMillis + MILLIS_PER_HOUR; | 
|  | event.startTime = minutesSinceMidnight; | 
|  | event.endTime = event.startTime + MINUTES_PER_HOUR; | 
|  | return event; | 
|  | } | 
|  |  | 
|  | private int computeMaxStringWidth(int currentMax, String[] strings, Paint p) { | 
|  | float maxWidthF = 0.0f; | 
|  |  | 
|  | int len = strings.length; | 
|  | for (int i = 0; i < len; i++) { | 
|  | float width = p.measureText(strings[i]); | 
|  | maxWidthF = Math.max(width, maxWidthF); | 
|  | } | 
|  | int maxWidth = (int) (maxWidthF + 0.5); | 
|  | if (maxWidth < currentMax) { | 
|  | maxWidth = currentMax; | 
|  | } | 
|  | return maxWidth; | 
|  | } | 
|  |  | 
|  | private void saveSelectionPosition(float left, float top, float right, float bottom) { | 
|  | mPrevBox.left = (int) left; | 
|  | mPrevBox.right = (int) right; | 
|  | mPrevBox.top = (int) top; | 
|  | mPrevBox.bottom = (int) bottom; | 
|  | } | 
|  |  | 
|  | private Rect getCurrentSelectionPosition() { | 
|  | Rect box = new Rect(); | 
|  | box.top = mSelectionHour * (mCellHeight + HOUR_GAP); | 
|  | box.bottom = box.top + mCellHeight + HOUR_GAP; | 
|  | int daynum = mSelectionDay - mFirstJulianDay; | 
|  | box.left = computeDayLeftPosition(daynum) + 1; | 
|  | box.right = computeDayLeftPosition(daynum + 1); | 
|  | return box; | 
|  | } | 
|  |  | 
|  | private void setupTextRect(Rect r) { | 
|  | if (r.bottom <= r.top || r.right <= r.left) { | 
|  | r.bottom = r.top; | 
|  | r.right = r.left; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (r.bottom - r.top > EVENT_TEXT_TOP_MARGIN + EVENT_TEXT_BOTTOM_MARGIN) { | 
|  | r.top += EVENT_TEXT_TOP_MARGIN; | 
|  | r.bottom -= EVENT_TEXT_BOTTOM_MARGIN; | 
|  | } | 
|  | if (r.right - r.left > EVENT_TEXT_LEFT_MARGIN + EVENT_TEXT_RIGHT_MARGIN) { | 
|  | r.left += EVENT_TEXT_LEFT_MARGIN; | 
|  | r.right -= EVENT_TEXT_RIGHT_MARGIN; | 
|  | } | 
|  | } | 
|  |  | 
|  | private void setupAllDayTextRect(Rect r) { | 
|  | if (r.bottom <= r.top || r.right <= r.left) { | 
|  | r.bottom = r.top; | 
|  | r.right = r.left; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (r.bottom - r.top > EVENT_ALL_DAY_TEXT_TOP_MARGIN + EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN) { | 
|  | r.top += EVENT_ALL_DAY_TEXT_TOP_MARGIN; | 
|  | r.bottom -= EVENT_ALL_DAY_TEXT_BOTTOM_MARGIN; | 
|  | } | 
|  | if (r.right - r.left > EVENT_ALL_DAY_TEXT_LEFT_MARGIN + EVENT_ALL_DAY_TEXT_RIGHT_MARGIN) { | 
|  | r.left += EVENT_ALL_DAY_TEXT_LEFT_MARGIN; | 
|  | r.right -= EVENT_ALL_DAY_TEXT_RIGHT_MARGIN; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Return the layout for a numbered event. Create it if not already existing | 
|  | */ | 
|  | private StaticLayout getEventLayout(StaticLayout[] layouts, int i, Event event, Paint paint, | 
|  | Rect r) { | 
|  | if (i < 0 || i >= layouts.length) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | StaticLayout layout = layouts[i]; | 
|  | // Check if we have already initialized the StaticLayout and that | 
|  | // the width hasn't changed (due to vertical resizing which causes | 
|  | // re-layout of events at min height) | 
|  | if (layout == null || r.width() != layout.getWidth()) { | 
|  | SpannableStringBuilder bob = new SpannableStringBuilder(); | 
|  | if (event.title != null) { | 
|  | // MAX - 1 since we add a space | 
|  | bob.append(drawTextSanitizer(event.title.toString(), MAX_EVENT_TEXT_LEN - 1)); | 
|  | bob.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, bob.length(), 0); | 
|  | bob.append(' '); | 
|  | } | 
|  | if (event.location != null) { | 
|  | bob.append(drawTextSanitizer(event.location.toString(), | 
|  | MAX_EVENT_TEXT_LEN - bob.length())); | 
|  | } | 
|  |  | 
|  | switch (event.selfAttendeeStatus) { | 
|  | case Attendees.ATTENDEE_STATUS_INVITED: | 
|  | paint.setColor(event.color); | 
|  | break; | 
|  | case Attendees.ATTENDEE_STATUS_DECLINED: | 
|  | paint.setColor(mEventTextColor); | 
|  | paint.setAlpha(Utils.DECLINED_EVENT_TEXT_ALPHA); | 
|  | break; | 
|  | case Attendees.ATTENDEE_STATUS_NONE: // Your own events | 
|  | case Attendees.ATTENDEE_STATUS_ACCEPTED: | 
|  | case Attendees.ATTENDEE_STATUS_TENTATIVE: | 
|  | default: | 
|  | paint.setColor(mEventTextColor); | 
|  | break; | 
|  | } | 
|  |  | 
|  | // Leave a one pixel boundary on the left and right of the rectangle for the event | 
|  | layout = new StaticLayout(bob, 0, bob.length(), new TextPaint(paint), r.width(), | 
|  | Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true, null, r.width()); | 
|  |  | 
|  | layouts[i] = layout; | 
|  | } | 
|  | layout.getPaint().setAlpha(mEventsAlpha); | 
|  | return layout; | 
|  | } | 
|  |  | 
|  | private void drawAllDayEvents(int firstDay, int numDays, Canvas canvas, Paint p) { | 
|  |  | 
|  | p.setTextSize(NORMAL_FONT_SIZE); | 
|  | p.setTextAlign(Paint.Align.LEFT); | 
|  | Paint eventTextPaint = mEventTextPaint; | 
|  |  | 
|  | final float startY = DAY_HEADER_HEIGHT; | 
|  | final float stopY = startY + mAlldayHeight + ALLDAY_TOP_MARGIN; | 
|  | float x = 0; | 
|  | int linesIndex = 0; | 
|  |  | 
|  | // Draw the inner vertical grid lines | 
|  | p.setColor(mCalendarGridLineInnerVerticalColor); | 
|  | x = mHoursWidth; | 
|  | p.setStrokeWidth(GRID_LINE_INNER_WIDTH); | 
|  | // Line bounding the top of the all day area | 
|  | mLines[linesIndex++] = GRID_LINE_LEFT_MARGIN; | 
|  | mLines[linesIndex++] = startY; | 
|  | mLines[linesIndex++] = computeDayLeftPosition(mNumDays); | 
|  | mLines[linesIndex++] = startY; | 
|  |  | 
|  | for (int day = 0; day <= mNumDays; day++) { | 
|  | x = computeDayLeftPosition(day); | 
|  | mLines[linesIndex++] = x; | 
|  | mLines[linesIndex++] = startY; | 
|  | mLines[linesIndex++] = x; | 
|  | mLines[linesIndex++] = stopY; | 
|  | } | 
|  | p.setAntiAlias(false); | 
|  | canvas.drawLines(mLines, 0, linesIndex, p); | 
|  | p.setStyle(Style.FILL); | 
|  |  | 
|  | int y = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; | 
|  | int lastDay = firstDay + numDays - 1; | 
|  | final ArrayList<Event> events = mAllDayEvents; | 
|  | int numEvents = events.size(); | 
|  | // Whether or not we should draw the more events text | 
|  | boolean hasMoreEvents = false; | 
|  | // size of the allDay area | 
|  | float drawHeight = mAlldayHeight; | 
|  | // max number of events being drawn in one day of the allday area | 
|  | float numRectangles = mMaxAlldayEvents; | 
|  | // Where to cut off drawn allday events | 
|  | int allDayEventClip = DAY_HEADER_HEIGHT + mAlldayHeight + ALLDAY_TOP_MARGIN; | 
|  | // The number of events that weren't drawn in each day | 
|  | mSkippedAlldayEvents = new int[numDays]; | 
|  | if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount && !mShowAllAllDayEvents && | 
|  | mAnimateDayHeight == 0) { | 
|  | // We draw one fewer event than will fit so that more events text | 
|  | // can be drawn | 
|  | numRectangles = mMaxUnexpandedAlldayEventCount - 1; | 
|  | // We also clip the events above the more events text | 
|  | allDayEventClip -= MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; | 
|  | hasMoreEvents = true; | 
|  | } else if (mAnimateDayHeight != 0) { | 
|  | // clip at the end of the animating space | 
|  | allDayEventClip = DAY_HEADER_HEIGHT + mAnimateDayHeight + ALLDAY_TOP_MARGIN; | 
|  | } | 
|  |  | 
|  | int alpha = eventTextPaint.getAlpha(); | 
|  | eventTextPaint.setAlpha(mEventsAlpha); | 
|  | for (int i = 0; i < numEvents; i++) { | 
|  | Event event = events.get(i); | 
|  | int startDay = event.startDay; | 
|  | int endDay = event.endDay; | 
|  | if (startDay > lastDay || endDay < firstDay) { | 
|  | continue; | 
|  | } | 
|  | if (startDay < firstDay) { | 
|  | startDay = firstDay; | 
|  | } | 
|  | if (endDay > lastDay) { | 
|  | endDay = lastDay; | 
|  | } | 
|  | int startIndex = startDay - firstDay; | 
|  | int endIndex = endDay - firstDay; | 
|  | float height = mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount ? mAnimateDayEventHeight : | 
|  | drawHeight / numRectangles; | 
|  |  | 
|  | // Prevent a single event from getting too big | 
|  | if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { | 
|  | height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; | 
|  | } | 
|  |  | 
|  | // Leave a one-pixel space between the vertical day lines and the | 
|  | // event rectangle. | 
|  | event.left = computeDayLeftPosition(startIndex); | 
|  | event.right = computeDayLeftPosition(endIndex + 1) - DAY_GAP; | 
|  | event.top = y + height * event.getColumn(); | 
|  | event.bottom = event.top + height - ALL_DAY_EVENT_RECT_BOTTOM_MARGIN; | 
|  | if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { | 
|  | // check if we should skip this event. We skip if it starts | 
|  | // after the clip bound or ends after the skip bound and we're | 
|  | // not animating. | 
|  | if (event.top >= allDayEventClip) { | 
|  | incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); | 
|  | continue; | 
|  | } else if (event.bottom > allDayEventClip) { | 
|  | if (hasMoreEvents) { | 
|  | incrementSkipCount(mSkippedAlldayEvents, startIndex, endIndex); | 
|  | continue; | 
|  | } | 
|  | event.bottom = allDayEventClip; | 
|  | } | 
|  | } | 
|  | Rect r = drawEventRect(event, canvas, p, eventTextPaint, (int) event.top, | 
|  | (int) event.bottom); | 
|  | setupAllDayTextRect(r); | 
|  | StaticLayout layout = getEventLayout(mAllDayLayouts, i, event, eventTextPaint, r); | 
|  | drawEventText(layout, r, canvas, r.top, r.bottom, true); | 
|  |  | 
|  | // Check if this all-day event intersects the selected day | 
|  | if (mSelectionAllday && mComputeSelectedEvents) { | 
|  | if (startDay <= mSelectionDay && endDay >= mSelectionDay) { | 
|  | mSelectedEvents.add(event); | 
|  | } | 
|  | } | 
|  | } | 
|  | eventTextPaint.setAlpha(alpha); | 
|  |  | 
|  | if (mMoreAlldayEventsTextAlpha != 0 && mSkippedAlldayEvents != null) { | 
|  | // If the more allday text should be visible, draw it. | 
|  | alpha = p.getAlpha(); | 
|  | p.setAlpha(mEventsAlpha); | 
|  | p.setColor(mMoreAlldayEventsTextAlpha << 24 & mMoreEventsTextColor); | 
|  | for (int i = 0; i < mSkippedAlldayEvents.length; i++) { | 
|  | if (mSkippedAlldayEvents[i] > 0) { | 
|  | drawMoreAlldayEvents(canvas, mSkippedAlldayEvents[i], i, p); | 
|  | } | 
|  | } | 
|  | p.setAlpha(alpha); | 
|  | } | 
|  |  | 
|  | if (mSelectionAllday) { | 
|  | // Compute the neighbors for the list of all-day events that | 
|  | // intersect the selected day. | 
|  | computeAllDayNeighbors(); | 
|  |  | 
|  | // Set the selection position to zero so that when we move down | 
|  | // to the normal event area, we will highlight the topmost event. | 
|  | saveSelectionPosition(0f, 0f, 0f, 0f); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Helper method for counting the number of allday events skipped on each day | 
|  | private void incrementSkipCount(int[] counts, int startIndex, int endIndex) { | 
|  | if (counts == null || startIndex < 0 || endIndex > counts.length) { | 
|  | return; | 
|  | } | 
|  | for (int i = startIndex; i <= endIndex; i++) { | 
|  | counts[i]++; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Draws the "box +n" text for hidden allday events | 
|  | protected void drawMoreAlldayEvents(Canvas canvas, int remainingEvents, int day, Paint p) { | 
|  | int x = computeDayLeftPosition(day) + EVENT_ALL_DAY_TEXT_LEFT_MARGIN; | 
|  | int y = (int) (mAlldayHeight - .5f * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - .5f | 
|  | * EVENT_SQUARE_WIDTH + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN); | 
|  | Rect r = mRect; | 
|  | r.top = y; | 
|  | r.left = x; | 
|  | r.bottom = y + EVENT_SQUARE_WIDTH; | 
|  | r.right = x + EVENT_SQUARE_WIDTH; | 
|  | p.setColor(mMoreEventsTextColor); | 
|  | p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); | 
|  | p.setStyle(Style.STROKE); | 
|  | p.setAntiAlias(false); | 
|  | canvas.drawRect(r, p); | 
|  | p.setAntiAlias(true); | 
|  | p.setStyle(Style.FILL); | 
|  | p.setTextSize(EVENT_TEXT_FONT_SIZE); | 
|  | String text = mResources.getQuantityString(R.plurals.month_more_events, remainingEvents); | 
|  | y += EVENT_SQUARE_WIDTH; | 
|  | x += EVENT_SQUARE_WIDTH + EVENT_LINE_PADDING; | 
|  | canvas.drawText(String.format(text, remainingEvents), x, y, p); | 
|  | } | 
|  |  | 
|  | private void computeAllDayNeighbors() { | 
|  | int len = mSelectedEvents.size(); | 
|  | if (len == 0 || mSelectedEvent != null) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // First, clear all the links | 
|  | for (int ii = 0; ii < len; ii++) { | 
|  | Event ev = mSelectedEvents.get(ii); | 
|  | ev.nextUp = null; | 
|  | ev.nextDown = null; | 
|  | ev.nextLeft = null; | 
|  | ev.nextRight = null; | 
|  | } | 
|  |  | 
|  | // For each event in the selected event list "mSelectedEvents", find | 
|  | // its neighbors in the up and down directions. This could be done | 
|  | // more efficiently by sorting on the Event.getColumn() field, but | 
|  | // the list is expected to be very small. | 
|  |  | 
|  | // Find the event in the same row as the previously selected all-day | 
|  | // event, if any. | 
|  | int startPosition = -1; | 
|  | if (mPrevSelectedEvent != null && mPrevSelectedEvent.drawAsAllday()) { | 
|  | startPosition = mPrevSelectedEvent.getColumn(); | 
|  | } | 
|  | int maxPosition = -1; | 
|  | Event startEvent = null; | 
|  | Event maxPositionEvent = null; | 
|  | for (int ii = 0; ii < len; ii++) { | 
|  | Event ev = mSelectedEvents.get(ii); | 
|  | int position = ev.getColumn(); | 
|  | if (position == startPosition) { | 
|  | startEvent = ev; | 
|  | } else if (position > maxPosition) { | 
|  | maxPositionEvent = ev; | 
|  | maxPosition = position; | 
|  | } | 
|  | for (int jj = 0; jj < len; jj++) { | 
|  | if (jj == ii) { | 
|  | continue; | 
|  | } | 
|  | Event neighbor = mSelectedEvents.get(jj); | 
|  | int neighborPosition = neighbor.getColumn(); | 
|  | if (neighborPosition == position - 1) { | 
|  | ev.nextUp = neighbor; | 
|  | } else if (neighborPosition == position + 1) { | 
|  | ev.nextDown = neighbor; | 
|  | } | 
|  | } | 
|  | } | 
|  | if (startEvent != null) { | 
|  | setSelectedEvent(startEvent); | 
|  | } else { | 
|  | setSelectedEvent(maxPositionEvent); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void drawEvents(int date, int dayIndex, int top, Canvas canvas, Paint p) { | 
|  | Paint eventTextPaint = mEventTextPaint; | 
|  | int left = computeDayLeftPosition(dayIndex) + 1; | 
|  | int cellWidth = computeDayLeftPosition(dayIndex + 1) - left + 1; | 
|  | int cellHeight = mCellHeight; | 
|  |  | 
|  | // Use the selected hour as the selection region | 
|  | Rect selectionArea = mSelectionRect; | 
|  | selectionArea.top = top + mSelectionHour * (cellHeight + HOUR_GAP); | 
|  | selectionArea.bottom = selectionArea.top + cellHeight; | 
|  | selectionArea.left = left; | 
|  | selectionArea.right = selectionArea.left + cellWidth; | 
|  |  | 
|  | final ArrayList<Event> events = mEvents; | 
|  | int numEvents = events.size(); | 
|  | EventGeometry geometry = mEventGeometry; | 
|  |  | 
|  | final int viewEndY = mViewStartY + mViewHeight - DAY_HEADER_HEIGHT - mAlldayHeight; | 
|  |  | 
|  | int alpha = eventTextPaint.getAlpha(); | 
|  | eventTextPaint.setAlpha(mEventsAlpha); | 
|  | for (int i = 0; i < numEvents; i++) { | 
|  | Event event = events.get(i); | 
|  | if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Don't draw it if it is not visible | 
|  | if (event.bottom < mViewStartY || event.top > viewEndY) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (date == mSelectionDay && !mSelectionAllday && mComputeSelectedEvents | 
|  | && geometry.eventIntersectsSelection(event, selectionArea)) { | 
|  | mSelectedEvents.add(event); | 
|  | } | 
|  |  | 
|  | Rect r = drawEventRect(event, canvas, p, eventTextPaint, mViewStartY, viewEndY); | 
|  | setupTextRect(r); | 
|  |  | 
|  | // Don't draw text if it is not visible | 
|  | if (r.top > viewEndY || r.bottom < mViewStartY) { | 
|  | continue; | 
|  | } | 
|  | StaticLayout layout = getEventLayout(mLayouts, i, event, eventTextPaint, r); | 
|  | // TODO: not sure why we are 4 pixels off | 
|  | drawEventText(layout, r, canvas, mViewStartY + 4, mViewStartY + mViewHeight | 
|  | - DAY_HEADER_HEIGHT - mAlldayHeight, false); | 
|  | } | 
|  | eventTextPaint.setAlpha(alpha); | 
|  |  | 
|  | if (date == mSelectionDay && !mSelectionAllday && isFocused() | 
|  | && mSelectionMode != SELECTION_HIDDEN) { | 
|  | computeNeighbors(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Computes the "nearest" neighbor event in four directions (left, right, | 
|  | // up, down) for each of the events in the mSelectedEvents array. | 
|  | private void computeNeighbors() { | 
|  | int len = mSelectedEvents.size(); | 
|  | if (len == 0 || mSelectedEvent != null) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // First, clear all the links | 
|  | for (int ii = 0; ii < len; ii++) { | 
|  | Event ev = mSelectedEvents.get(ii); | 
|  | ev.nextUp = null; | 
|  | ev.nextDown = null; | 
|  | ev.nextLeft = null; | 
|  | ev.nextRight = null; | 
|  | } | 
|  |  | 
|  | Event startEvent = mSelectedEvents.get(0); | 
|  | int startEventDistance1 = 100000; // any large number | 
|  | int startEventDistance2 = 100000; // any large number | 
|  | int prevLocation = FROM_NONE; | 
|  | int prevTop; | 
|  | int prevBottom; | 
|  | int prevLeft; | 
|  | int prevRight; | 
|  | int prevCenter = 0; | 
|  | Rect box = getCurrentSelectionPosition(); | 
|  | if (mPrevSelectedEvent != null) { | 
|  | prevTop = (int) mPrevSelectedEvent.top; | 
|  | prevBottom = (int) mPrevSelectedEvent.bottom; | 
|  | prevLeft = (int) mPrevSelectedEvent.left; | 
|  | prevRight = (int) mPrevSelectedEvent.right; | 
|  | // Check if the previously selected event intersects the previous | 
|  | // selection box. (The previously selected event may be from a | 
|  | // much older selection box.) | 
|  | if (prevTop >= mPrevBox.bottom || prevBottom <= mPrevBox.top | 
|  | || prevRight <= mPrevBox.left || prevLeft >= mPrevBox.right) { | 
|  | mPrevSelectedEvent = null; | 
|  | prevTop = mPrevBox.top; | 
|  | prevBottom = mPrevBox.bottom; | 
|  | prevLeft = mPrevBox.left; | 
|  | prevRight = mPrevBox.right; | 
|  | } else { | 
|  | // Clip the top and bottom to the previous selection box. | 
|  | if (prevTop < mPrevBox.top) { | 
|  | prevTop = mPrevBox.top; | 
|  | } | 
|  | if (prevBottom > mPrevBox.bottom) { | 
|  | prevBottom = mPrevBox.bottom; | 
|  | } | 
|  | } | 
|  | } else { | 
|  | // Just use the previously drawn selection box | 
|  | prevTop = mPrevBox.top; | 
|  | prevBottom = mPrevBox.bottom; | 
|  | prevLeft = mPrevBox.left; | 
|  | prevRight = mPrevBox.right; | 
|  | } | 
|  |  | 
|  | // Figure out where we came from and compute the center of that area. | 
|  | if (prevLeft >= box.right) { | 
|  | // The previously selected event was to the right of us. | 
|  | prevLocation = FROM_RIGHT; | 
|  | prevCenter = (prevTop + prevBottom) / 2; | 
|  | } else if (prevRight <= box.left) { | 
|  | // The previously selected event was to the left of us. | 
|  | prevLocation = FROM_LEFT; | 
|  | prevCenter = (prevTop + prevBottom) / 2; | 
|  | } else if (prevBottom <= box.top) { | 
|  | // The previously selected event was above us. | 
|  | prevLocation = FROM_ABOVE; | 
|  | prevCenter = (prevLeft + prevRight) / 2; | 
|  | } else if (prevTop >= box.bottom) { | 
|  | // The previously selected event was below us. | 
|  | prevLocation = FROM_BELOW; | 
|  | prevCenter = (prevLeft + prevRight) / 2; | 
|  | } | 
|  |  | 
|  | // For each event in the selected event list "mSelectedEvents", search | 
|  | // all the other events in that list for the nearest neighbor in 4 | 
|  | // directions. | 
|  | for (int ii = 0; ii < len; ii++) { | 
|  | Event ev = mSelectedEvents.get(ii); | 
|  |  | 
|  | int startTime = ev.startTime; | 
|  | int endTime = ev.endTime; | 
|  | int left = (int) ev.left; | 
|  | int right = (int) ev.right; | 
|  | int top = (int) ev.top; | 
|  | if (top < box.top) { | 
|  | top = box.top; | 
|  | } | 
|  | int bottom = (int) ev.bottom; | 
|  | if (bottom > box.bottom) { | 
|  | bottom = box.bottom; | 
|  | } | 
|  | //            if (false) { | 
|  | //                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL | 
|  | //                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; | 
|  | //                if (DateFormat.is24HourFormat(mContext)) { | 
|  | //                    flags |= DateUtils.FORMAT_24HOUR; | 
|  | //                } | 
|  | //                String timeRange = DateUtils.formatDateRange(mContext, ev.startMillis, | 
|  | //                        ev.endMillis, flags); | 
|  | //                Log.i("Cal", "left: " + left + " right: " + right + " top: " + top + " bottom: " | 
|  | //                        + bottom + " ev: " + timeRange + " " + ev.title); | 
|  | //            } | 
|  | int upDistanceMin = 10000; // any large number | 
|  | int downDistanceMin = 10000; // any large number | 
|  | int leftDistanceMin = 10000; // any large number | 
|  | int rightDistanceMin = 10000; // any large number | 
|  | Event upEvent = null; | 
|  | Event downEvent = null; | 
|  | Event leftEvent = null; | 
|  | Event rightEvent = null; | 
|  |  | 
|  | // Pick the starting event closest to the previously selected event, | 
|  | // if any. distance1 takes precedence over distance2. | 
|  | int distance1 = 0; | 
|  | int distance2 = 0; | 
|  | if (prevLocation == FROM_ABOVE) { | 
|  | if (left >= prevCenter) { | 
|  | distance1 = left - prevCenter; | 
|  | } else if (right <= prevCenter) { | 
|  | distance1 = prevCenter - right; | 
|  | } | 
|  | distance2 = top - prevBottom; | 
|  | } else if (prevLocation == FROM_BELOW) { | 
|  | if (left >= prevCenter) { | 
|  | distance1 = left - prevCenter; | 
|  | } else if (right <= prevCenter) { | 
|  | distance1 = prevCenter - right; | 
|  | } | 
|  | distance2 = prevTop - bottom; | 
|  | } else if (prevLocation == FROM_LEFT) { | 
|  | if (bottom <= prevCenter) { | 
|  | distance1 = prevCenter - bottom; | 
|  | } else if (top >= prevCenter) { | 
|  | distance1 = top - prevCenter; | 
|  | } | 
|  | distance2 = left - prevRight; | 
|  | } else if (prevLocation == FROM_RIGHT) { | 
|  | if (bottom <= prevCenter) { | 
|  | distance1 = prevCenter - bottom; | 
|  | } else if (top >= prevCenter) { | 
|  | distance1 = top - prevCenter; | 
|  | } | 
|  | distance2 = prevLeft - right; | 
|  | } | 
|  | if (distance1 < startEventDistance1 | 
|  | || (distance1 == startEventDistance1 && distance2 < startEventDistance2)) { | 
|  | startEvent = ev; | 
|  | startEventDistance1 = distance1; | 
|  | startEventDistance2 = distance2; | 
|  | } | 
|  |  | 
|  | // For each neighbor, figure out if it is above or below or left | 
|  | // or right of me and compute the distance. | 
|  | for (int jj = 0; jj < len; jj++) { | 
|  | if (jj == ii) { | 
|  | continue; | 
|  | } | 
|  | Event neighbor = mSelectedEvents.get(jj); | 
|  | int neighborLeft = (int) neighbor.left; | 
|  | int neighborRight = (int) neighbor.right; | 
|  | if (neighbor.endTime <= startTime) { | 
|  | // This neighbor is entirely above me. | 
|  | // If we overlap the same column, then compute the distance. | 
|  | if (neighborLeft < right && neighborRight > left) { | 
|  | int distance = startTime - neighbor.endTime; | 
|  | if (distance < upDistanceMin) { | 
|  | upDistanceMin = distance; | 
|  | upEvent = neighbor; | 
|  | } else if (distance == upDistanceMin) { | 
|  | int center = (left + right) / 2; | 
|  | int currentDistance = 0; | 
|  | int currentLeft = (int) upEvent.left; | 
|  | int currentRight = (int) upEvent.right; | 
|  | if (currentRight <= center) { | 
|  | currentDistance = center - currentRight; | 
|  | } else if (currentLeft >= center) { | 
|  | currentDistance = currentLeft - center; | 
|  | } | 
|  |  | 
|  | int neighborDistance = 0; | 
|  | if (neighborRight <= center) { | 
|  | neighborDistance = center - neighborRight; | 
|  | } else if (neighborLeft >= center) { | 
|  | neighborDistance = neighborLeft - center; | 
|  | } | 
|  | if (neighborDistance < currentDistance) { | 
|  | upDistanceMin = distance; | 
|  | upEvent = neighbor; | 
|  | } | 
|  | } | 
|  | } | 
|  | } else if (neighbor.startTime >= endTime) { | 
|  | // This neighbor is entirely below me. | 
|  | // If we overlap the same column, then compute the distance. | 
|  | if (neighborLeft < right && neighborRight > left) { | 
|  | int distance = neighbor.startTime - endTime; | 
|  | if (distance < downDistanceMin) { | 
|  | downDistanceMin = distance; | 
|  | downEvent = neighbor; | 
|  | } else if (distance == downDistanceMin) { | 
|  | int center = (left + right) / 2; | 
|  | int currentDistance = 0; | 
|  | int currentLeft = (int) downEvent.left; | 
|  | int currentRight = (int) downEvent.right; | 
|  | if (currentRight <= center) { | 
|  | currentDistance = center - currentRight; | 
|  | } else if (currentLeft >= center) { | 
|  | currentDistance = currentLeft - center; | 
|  | } | 
|  |  | 
|  | int neighborDistance = 0; | 
|  | if (neighborRight <= center) { | 
|  | neighborDistance = center - neighborRight; | 
|  | } else if (neighborLeft >= center) { | 
|  | neighborDistance = neighborLeft - center; | 
|  | } | 
|  | if (neighborDistance < currentDistance) { | 
|  | downDistanceMin = distance; | 
|  | downEvent = neighbor; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (neighborLeft >= right) { | 
|  | // This neighbor is entirely to the right of me. | 
|  | // Take the closest neighbor in the y direction. | 
|  | int center = (top + bottom) / 2; | 
|  | int distance = 0; | 
|  | int neighborBottom = (int) neighbor.bottom; | 
|  | int neighborTop = (int) neighbor.top; | 
|  | if (neighborBottom <= center) { | 
|  | distance = center - neighborBottom; | 
|  | } else if (neighborTop >= center) { | 
|  | distance = neighborTop - center; | 
|  | } | 
|  | if (distance < rightDistanceMin) { | 
|  | rightDistanceMin = distance; | 
|  | rightEvent = neighbor; | 
|  | } else if (distance == rightDistanceMin) { | 
|  | // Pick the closest in the x direction | 
|  | int neighborDistance = neighborLeft - right; | 
|  | int currentDistance = (int) rightEvent.left - right; | 
|  | if (neighborDistance < currentDistance) { | 
|  | rightDistanceMin = distance; | 
|  | rightEvent = neighbor; | 
|  | } | 
|  | } | 
|  | } else if (neighborRight <= left) { | 
|  | // This neighbor is entirely to the left of me. | 
|  | // Take the closest neighbor in the y direction. | 
|  | int center = (top + bottom) / 2; | 
|  | int distance = 0; | 
|  | int neighborBottom = (int) neighbor.bottom; | 
|  | int neighborTop = (int) neighbor.top; | 
|  | if (neighborBottom <= center) { | 
|  | distance = center - neighborBottom; | 
|  | } else if (neighborTop >= center) { | 
|  | distance = neighborTop - center; | 
|  | } | 
|  | if (distance < leftDistanceMin) { | 
|  | leftDistanceMin = distance; | 
|  | leftEvent = neighbor; | 
|  | } else if (distance == leftDistanceMin) { | 
|  | // Pick the closest in the x direction | 
|  | int neighborDistance = left - neighborRight; | 
|  | int currentDistance = left - (int) leftEvent.right; | 
|  | if (neighborDistance < currentDistance) { | 
|  | leftDistanceMin = distance; | 
|  | leftEvent = neighbor; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | ev.nextUp = upEvent; | 
|  | ev.nextDown = downEvent; | 
|  | ev.nextLeft = leftEvent; | 
|  | ev.nextRight = rightEvent; | 
|  | } | 
|  | setSelectedEvent(startEvent); | 
|  | } | 
|  |  | 
|  | private Rect drawEventRect(Event event, Canvas canvas, Paint p, Paint eventTextPaint, | 
|  | int visibleTop, int visibleBot) { | 
|  | // Draw the Event Rect | 
|  | Rect r = mRect; | 
|  | r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN, visibleTop); | 
|  | r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN, visibleBot); | 
|  | r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; | 
|  | r.right = (int) event.right; | 
|  |  | 
|  | int color; | 
|  | if (event == mClickedEvent) { | 
|  | color = mClickedColor; | 
|  | } else { | 
|  | color = event.color; | 
|  | } | 
|  |  | 
|  | switch (event.selfAttendeeStatus) { | 
|  | case Attendees.ATTENDEE_STATUS_INVITED: | 
|  | if (event != mClickedEvent) { | 
|  | p.setStyle(Style.STROKE); | 
|  | } | 
|  | break; | 
|  | case Attendees.ATTENDEE_STATUS_DECLINED: | 
|  | if (event != mClickedEvent) { | 
|  | color = Utils.getDeclinedColorFromColor(color); | 
|  | } | 
|  | case Attendees.ATTENDEE_STATUS_NONE: // Your own events | 
|  | case Attendees.ATTENDEE_STATUS_ACCEPTED: | 
|  | case Attendees.ATTENDEE_STATUS_TENTATIVE: | 
|  | default: | 
|  | p.setStyle(Style.FILL_AND_STROKE); | 
|  | break; | 
|  | } | 
|  |  | 
|  | p.setAntiAlias(false); | 
|  |  | 
|  | int floorHalfStroke = (int) Math.floor(EVENT_RECT_STROKE_WIDTH / 2.0f); | 
|  | int ceilHalfStroke = (int) Math.ceil(EVENT_RECT_STROKE_WIDTH / 2.0f); | 
|  | r.top = Math.max((int) event.top + EVENT_RECT_TOP_MARGIN + floorHalfStroke, visibleTop); | 
|  | r.bottom = Math.min((int) event.bottom - EVENT_RECT_BOTTOM_MARGIN - ceilHalfStroke, | 
|  | visibleBot); | 
|  | r.left += floorHalfStroke; | 
|  | r.right -= ceilHalfStroke; | 
|  | p.setStrokeWidth(EVENT_RECT_STROKE_WIDTH); | 
|  | p.setColor(color); | 
|  | int alpha = p.getAlpha(); | 
|  | p.setAlpha(mEventsAlpha); | 
|  | canvas.drawRect(r, p); | 
|  | p.setAlpha(alpha); | 
|  | p.setStyle(Style.FILL); | 
|  |  | 
|  | // If this event is selected, then use the selection color | 
|  | if (mSelectedEvent == event && mClickedEvent != null) { | 
|  | boolean paintIt = false; | 
|  | color = 0; | 
|  | if (mSelectionMode == SELECTION_PRESSED) { | 
|  | // Also, remember the last selected event that we drew | 
|  | mPrevSelectedEvent = event; | 
|  | color = mPressedColor; | 
|  | paintIt = true; | 
|  | } else if (mSelectionMode == SELECTION_SELECTED) { | 
|  | // Also, remember the last selected event that we drew | 
|  | mPrevSelectedEvent = event; | 
|  | color = mPressedColor; | 
|  | paintIt = true; | 
|  | } | 
|  |  | 
|  | if (paintIt) { | 
|  | p.setColor(color); | 
|  | canvas.drawRect(r, p); | 
|  | } | 
|  | p.setAntiAlias(true); | 
|  | } | 
|  |  | 
|  | // Draw cal color square border | 
|  | // r.top = (int) event.top + CALENDAR_COLOR_SQUARE_V_OFFSET; | 
|  | // r.left = (int) event.left + CALENDAR_COLOR_SQUARE_H_OFFSET; | 
|  | // r.bottom = r.top + CALENDAR_COLOR_SQUARE_SIZE + 1; | 
|  | // r.right = r.left + CALENDAR_COLOR_SQUARE_SIZE + 1; | 
|  | // p.setColor(0xFFFFFFFF); | 
|  | // canvas.drawRect(r, p); | 
|  |  | 
|  | // Draw cal color | 
|  | // r.top++; | 
|  | // r.left++; | 
|  | // r.bottom--; | 
|  | // r.right--; | 
|  | // p.setColor(event.color); | 
|  | // canvas.drawRect(r, p); | 
|  |  | 
|  | // Setup rect for drawEventText which follows | 
|  | r.top = (int) event.top + EVENT_RECT_TOP_MARGIN; | 
|  | r.bottom = (int) event.bottom - EVENT_RECT_BOTTOM_MARGIN; | 
|  | r.left = (int) event.left + EVENT_RECT_LEFT_MARGIN; | 
|  | r.right = (int) event.right - EVENT_RECT_RIGHT_MARGIN; | 
|  | return r; | 
|  | } | 
|  |  | 
|  | private final Pattern drawTextSanitizerFilter = Pattern.compile("[\t\n],"); | 
|  |  | 
|  | // Sanitize a string before passing it to drawText or else we get little | 
|  | // squares. For newlines and tabs before a comma, delete the character. | 
|  | // Otherwise, just replace them with a space. | 
|  | private String drawTextSanitizer(String string, int maxEventTextLen) { | 
|  | Matcher m = drawTextSanitizerFilter.matcher(string); | 
|  | string = m.replaceAll(","); | 
|  |  | 
|  | int len = string.length(); | 
|  | if (maxEventTextLen <= 0) { | 
|  | string = ""; | 
|  | len = 0; | 
|  | } else if (len > maxEventTextLen) { | 
|  | string = string.substring(0, maxEventTextLen); | 
|  | len = maxEventTextLen; | 
|  | } | 
|  |  | 
|  | return string.replace('\n', ' '); | 
|  | } | 
|  |  | 
|  | private void drawEventText(StaticLayout eventLayout, Rect rect, Canvas canvas, int top, | 
|  | int bottom, boolean center) { | 
|  | // drawEmptyRect(canvas, rect, 0xFFFF00FF); // for debugging | 
|  |  | 
|  | int width = rect.right - rect.left; | 
|  | int height = rect.bottom - rect.top; | 
|  |  | 
|  | // If the rectangle is too small for text, then return | 
|  | if (eventLayout == null || width < MIN_CELL_WIDTH_FOR_TEXT) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | int totalLineHeight = 0; | 
|  | int lineCount = eventLayout.getLineCount(); | 
|  | for (int i = 0; i < lineCount; i++) { | 
|  | int lineBottom = eventLayout.getLineBottom(i); | 
|  | if (lineBottom <= height) { | 
|  | totalLineHeight = lineBottom; | 
|  | } else { | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (totalLineHeight == 0 || rect.top > bottom || rect.top + totalLineHeight < top) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Use a StaticLayout to format the string. | 
|  | canvas.save(); | 
|  | //  canvas.translate(rect.left, rect.top + (rect.bottom - rect.top / 2)); | 
|  | int padding = center? (rect.bottom - rect.top - totalLineHeight) / 2 : 0; | 
|  | canvas.translate(rect.left, rect.top + padding); | 
|  | rect.left = 0; | 
|  | rect.right = width; | 
|  | rect.top = 0; | 
|  | rect.bottom = totalLineHeight; | 
|  |  | 
|  | // There's a bug somewhere. If this rect is outside of a previous | 
|  | // cliprect, this becomes a no-op. What happens is that the text draw | 
|  | // past the event rect. The current fix is to not draw the staticLayout | 
|  | // at all if it is completely out of bound. | 
|  | canvas.clipRect(rect); | 
|  | eventLayout.draw(canvas); | 
|  | canvas.restore(); | 
|  | } | 
|  |  | 
|  | // This is to replace p.setStyle(Style.STROKE); canvas.drawRect() since it | 
|  | // doesn't work well with hardware acceleration | 
|  | //    private void drawEmptyRect(Canvas canvas, Rect r, int color) { | 
|  | //        int linesIndex = 0; | 
|  | //        mLines[linesIndex++] = r.left; | 
|  | //        mLines[linesIndex++] = r.top; | 
|  | //        mLines[linesIndex++] = r.right; | 
|  | //        mLines[linesIndex++] = r.top; | 
|  | // | 
|  | //        mLines[linesIndex++] = r.left; | 
|  | //        mLines[linesIndex++] = r.bottom; | 
|  | //        mLines[linesIndex++] = r.right; | 
|  | //        mLines[linesIndex++] = r.bottom; | 
|  | // | 
|  | //        mLines[linesIndex++] = r.left; | 
|  | //        mLines[linesIndex++] = r.top; | 
|  | //        mLines[linesIndex++] = r.left; | 
|  | //        mLines[linesIndex++] = r.bottom; | 
|  | // | 
|  | //        mLines[linesIndex++] = r.right; | 
|  | //        mLines[linesIndex++] = r.top; | 
|  | //        mLines[linesIndex++] = r.right; | 
|  | //        mLines[linesIndex++] = r.bottom; | 
|  | //        mPaint.setColor(color); | 
|  | //        canvas.drawLines(mLines, 0, linesIndex, mPaint); | 
|  | //    } | 
|  |  | 
|  | private void updateEventDetails() { | 
|  | if (mSelectedEvent == null || mSelectionMode == SELECTION_HIDDEN | 
|  | || mSelectionMode == SELECTION_LONGPRESS) { | 
|  | mPopup.dismiss(); | 
|  | return; | 
|  | } | 
|  | if (mLastPopupEventID == mSelectedEvent.id) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | mLastPopupEventID = mSelectedEvent.id; | 
|  |  | 
|  | // Remove any outstanding callbacks to dismiss the popup. | 
|  | mHandler.removeCallbacks(mDismissPopup); | 
|  |  | 
|  | Event event = mSelectedEvent; | 
|  | TextView titleView = (TextView) mPopupView.findViewById(R.id.event_title); | 
|  | titleView.setText(event.title); | 
|  |  | 
|  | ImageView imageView = (ImageView) mPopupView.findViewById(R.id.reminder_icon); | 
|  | imageView.setVisibility(event.hasAlarm ? View.VISIBLE : View.GONE); | 
|  |  | 
|  | imageView = (ImageView) mPopupView.findViewById(R.id.repeat_icon); | 
|  | imageView.setVisibility(event.isRepeating ? View.VISIBLE : View.GONE); | 
|  |  | 
|  | int flags; | 
|  | if (event.allDay) { | 
|  | flags = DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_DATE | 
|  | | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL; | 
|  | } else { | 
|  | flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE | 
|  | | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_ALL | 
|  | | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; | 
|  | } | 
|  | if (DateFormat.is24HourFormat(mContext)) { | 
|  | flags |= DateUtils.FORMAT_24HOUR; | 
|  | } | 
|  | String timeRange = Utils.formatDateRange(mContext, event.startMillis, event.endMillis, | 
|  | flags); | 
|  | TextView timeView = (TextView) mPopupView.findViewById(R.id.time); | 
|  | timeView.setText(timeRange); | 
|  |  | 
|  | TextView whereView = (TextView) mPopupView.findViewById(R.id.where); | 
|  | final boolean empty = TextUtils.isEmpty(event.location); | 
|  | whereView.setVisibility(empty ? View.GONE : View.VISIBLE); | 
|  | if (!empty) whereView.setText(event.location); | 
|  |  | 
|  | mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.LEFT, mHoursWidth, 5); | 
|  | mHandler.postDelayed(mDismissPopup, POPUP_DISMISS_DELAY); | 
|  | } | 
|  |  | 
|  | // The following routines are called from the parent activity when certain | 
|  | // touch events occur. | 
|  | private void doDown(MotionEvent ev) { | 
|  | mTouchMode = TOUCH_MODE_DOWN; | 
|  | mViewStartX = 0; | 
|  | mOnFlingCalled = false; | 
|  | mHandler.removeCallbacks(mContinueScroll); | 
|  | int x = (int) ev.getX(); | 
|  | int y = (int) ev.getY(); | 
|  |  | 
|  | // Save selection information: we use setSelectionFromPosition to find the selected event | 
|  | // in order to show the "clicked" color. But since it is also setting the selected info | 
|  | // for new events, we need to restore the old info after calling the function. | 
|  | Event oldSelectedEvent = mSelectedEvent; | 
|  | int oldSelectionDay = mSelectionDay; | 
|  | int oldSelectionHour = mSelectionHour; | 
|  | if (setSelectionFromPosition(x, y, false)) { | 
|  | // If a time was selected (a blue selection box is visible) and the click location | 
|  | // is in the selected time, do not show a click on an event to prevent a situation | 
|  | // of both a selection and an event are clicked when they overlap. | 
|  | boolean pressedSelected = (mSelectionMode != SELECTION_HIDDEN) | 
|  | && oldSelectionDay == mSelectionDay && oldSelectionHour == mSelectionHour; | 
|  | if (!pressedSelected && mSelectedEvent != null) { | 
|  | mSavedClickedEvent = mSelectedEvent; | 
|  | mDownTouchTime = System.currentTimeMillis(); | 
|  | postDelayed (mSetClick,mOnDownDelay); | 
|  | } else { | 
|  | eventClickCleanup(); | 
|  | } | 
|  | } | 
|  | mSelectedEvent = oldSelectedEvent; | 
|  | mSelectionDay = oldSelectionDay; | 
|  | mSelectionHour = oldSelectionHour; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | // Kicks off all the animations when the expand allday area is tapped | 
|  | private void doExpandAllDayClick() { | 
|  | mShowAllAllDayEvents = !mShowAllAllDayEvents; | 
|  |  | 
|  | ObjectAnimator.setFrameDelay(0); | 
|  |  | 
|  | // Determine the starting height | 
|  | if (mAnimateDayHeight == 0) { | 
|  | mAnimateDayHeight = mShowAllAllDayEvents ? | 
|  | mAlldayHeight - (int) MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT : mAlldayHeight; | 
|  | } | 
|  | // Cancel current animations | 
|  | mCancellingAnimations = true; | 
|  | if (mAlldayAnimator != null) { | 
|  | mAlldayAnimator.cancel(); | 
|  | } | 
|  | if (mAlldayEventAnimator != null) { | 
|  | mAlldayEventAnimator.cancel(); | 
|  | } | 
|  | if (mMoreAlldayEventsAnimator != null) { | 
|  | mMoreAlldayEventsAnimator.cancel(); | 
|  | } | 
|  | mCancellingAnimations = false; | 
|  | // get new animators | 
|  | mAlldayAnimator = getAllDayAnimator(); | 
|  | mAlldayEventAnimator = getAllDayEventAnimator(); | 
|  | mMoreAlldayEventsAnimator = ObjectAnimator.ofInt(this, | 
|  | "moreAllDayEventsTextAlpha", | 
|  | mShowAllAllDayEvents ? MORE_EVENTS_MAX_ALPHA : 0, | 
|  | mShowAllAllDayEvents ? 0 : MORE_EVENTS_MAX_ALPHA); | 
|  |  | 
|  | // Set up delays and start the animators | 
|  | mAlldayAnimator.setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); | 
|  | mAlldayAnimator.start(); | 
|  | mMoreAlldayEventsAnimator.setStartDelay(mShowAllAllDayEvents ? 0 : ANIMATION_DURATION); | 
|  | mMoreAlldayEventsAnimator.setDuration(ANIMATION_SECONDARY_DURATION); | 
|  | mMoreAlldayEventsAnimator.start(); | 
|  | if (mAlldayEventAnimator != null) { | 
|  | // This is the only animator that can return null, so check it | 
|  | mAlldayEventAnimator | 
|  | .setStartDelay(mShowAllAllDayEvents ? ANIMATION_SECONDARY_DURATION : 0); | 
|  | mAlldayEventAnimator.start(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Figures out the initial heights for allDay events and space when | 
|  | * a view is being set up. | 
|  | */ | 
|  | public void initAllDayHeights() { | 
|  | if (mMaxAlldayEvents <= mMaxUnexpandedAlldayEventCount) { | 
|  | return; | 
|  | } | 
|  | if (mShowAllAllDayEvents) { | 
|  | int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; | 
|  | maxADHeight = Math.min(maxADHeight, | 
|  | (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); | 
|  | mAnimateDayEventHeight = maxADHeight / mMaxAlldayEvents; | 
|  | } else { | 
|  | mAnimateDayEventHeight = (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Sets up an animator for changing the height of allday events | 
|  | private ObjectAnimator getAllDayEventAnimator() { | 
|  | // First calculate the absolute max height | 
|  | int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; | 
|  | // Now expand to fit but not beyond the absolute max | 
|  | maxADHeight = | 
|  | Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); | 
|  | // calculate the height of individual events in order to fit | 
|  | int fitHeight = maxADHeight / mMaxAlldayEvents; | 
|  | int currentHeight = mAnimateDayEventHeight; | 
|  | int desiredHeight = | 
|  | mShowAllAllDayEvents ? fitHeight : (int)MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT; | 
|  | // if there's nothing to animate just return | 
|  | if (currentHeight == desiredHeight) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Set up the animator with the calculated values | 
|  | ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayEventHeight", | 
|  | currentHeight, desiredHeight); | 
|  | animator.setDuration(ANIMATION_DURATION); | 
|  | return animator; | 
|  | } | 
|  |  | 
|  | // Sets up an animator for changing the height of the allday area | 
|  | private ObjectAnimator getAllDayAnimator() { | 
|  | // Calculate the absolute max height | 
|  | int maxADHeight = mViewHeight - DAY_HEADER_HEIGHT - MIN_HOURS_HEIGHT; | 
|  | // Find the desired height but don't exceed abs max | 
|  | maxADHeight = | 
|  | Math.min(maxADHeight, (int)(mMaxAlldayEvents * MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)); | 
|  | // calculate the current and desired heights | 
|  | int currentHeight = mAnimateDayHeight != 0 ? mAnimateDayHeight : mAlldayHeight; | 
|  | int desiredHeight = mShowAllAllDayEvents ? maxADHeight : | 
|  | (int) (MAX_UNEXPANDED_ALLDAY_HEIGHT - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT - 1); | 
|  |  | 
|  | // Set up the animator with the calculated values | 
|  | ObjectAnimator animator = ObjectAnimator.ofInt(this, "animateDayHeight", | 
|  | currentHeight, desiredHeight); | 
|  | animator.setDuration(ANIMATION_DURATION); | 
|  |  | 
|  | animator.addListener(new AnimatorListenerAdapter() { | 
|  | @Override | 
|  | public void onAnimationEnd(Animator animation) { | 
|  | if (!mCancellingAnimations) { | 
|  | // when finished, set this to 0 to signify not animating | 
|  | mAnimateDayHeight = 0; | 
|  | mUseExpandIcon = !mShowAllAllDayEvents; | 
|  | } | 
|  | mRemeasure = true; | 
|  | invalidate(); | 
|  | } | 
|  | }); | 
|  | return animator; | 
|  | } | 
|  |  | 
|  | // setter for the 'box +n' alpha text used by the animator | 
|  | public void setMoreAllDayEventsTextAlpha(int alpha) { | 
|  | mMoreAlldayEventsTextAlpha = alpha; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | // setter for the height of the allday area used by the animator | 
|  | public void setAnimateDayHeight(int height) { | 
|  | mAnimateDayHeight = height; | 
|  | mRemeasure = true; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | // setter for the height of allday events used by the animator | 
|  | public void setAnimateDayEventHeight(int height) { | 
|  | mAnimateDayEventHeight = height; | 
|  | mRemeasure = true; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | private void doSingleTapUp(MotionEvent ev) { | 
|  | if (!mHandleActionUp || mScrolling) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | int x = (int) ev.getX(); | 
|  | int y = (int) ev.getY(); | 
|  | int selectedDay = mSelectionDay; | 
|  | int selectedHour = mSelectionHour; | 
|  |  | 
|  | if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { | 
|  | // check if the tap was in the allday expansion area | 
|  | int bottom = mFirstCell; | 
|  | if((x < mHoursWidth && y > DAY_HEADER_HEIGHT && y < DAY_HEADER_HEIGHT + mAlldayHeight) | 
|  | || (!mShowAllAllDayEvents && mAnimateDayHeight == 0 && y < bottom && | 
|  | y >= bottom - MIN_UNEXPANDED_ALLDAY_EVENT_HEIGHT)) { | 
|  | doExpandAllDayClick(); | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | boolean validPosition = setSelectionFromPosition(x, y, false); | 
|  | if (!validPosition) { | 
|  | if (y < DAY_HEADER_HEIGHT) { | 
|  | Time selectedTime = new Time(mBaseDate); | 
|  | selectedTime.setJulianDay(mSelectionDay); | 
|  | selectedTime.hour = mSelectionHour; | 
|  | selectedTime.normalize(true /* ignore isDst */); | 
|  | mController.sendEvent(this, EventType.GO_TO, null, null, selectedTime, -1, | 
|  | ViewType.DAY, CalendarController.EXTRA_GOTO_DATE, null, null); | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | boolean hasSelection = mSelectionMode != SELECTION_HIDDEN; | 
|  | boolean pressedSelected = (hasSelection || mTouchExplorationEnabled) | 
|  | && selectedDay == mSelectionDay && selectedHour == mSelectionHour; | 
|  |  | 
|  | if (pressedSelected && mSavedClickedEvent == null) { | 
|  | // If the tap is on an already selected hour slot, then create a new | 
|  | // event | 
|  | long extraLong = 0; | 
|  | if (mSelectionAllday) { | 
|  | extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; | 
|  | } | 
|  | mSelectionMode = SELECTION_SELECTED; | 
|  | mController.sendEventRelatedEventWithExtra(this, EventType.CREATE_EVENT, -1, | 
|  | getSelectedTimeInMillis(), 0, (int) ev.getRawX(), (int) ev.getRawY(), | 
|  | extraLong, -1); | 
|  | } else if (mSelectedEvent != null) { | 
|  | // If the tap is on an event, launch the "View event" view | 
|  | if (mIsAccessibilityEnabled) { | 
|  | mAccessibilityMgr.interrupt(); | 
|  | } | 
|  |  | 
|  | mSelectionMode = SELECTION_HIDDEN; | 
|  |  | 
|  | int yLocation = | 
|  | (int)((mSelectedEvent.top + mSelectedEvent.bottom)/2); | 
|  | // Y location is affected by the position of the event in the scrolling | 
|  | // view (mViewStartY) and the presence of all day events (mFirstCell) | 
|  | if (!mSelectedEvent.allDay) { | 
|  | yLocation += (mFirstCell - mViewStartY); | 
|  | } | 
|  | mClickedYLocation = yLocation; | 
|  | long clearDelay = (CLICK_DISPLAY_DURATION + mOnDownDelay) - | 
|  | (System.currentTimeMillis() - mDownTouchTime); | 
|  | if (clearDelay > 0) { | 
|  | this.postDelayed(mClearClick, clearDelay); | 
|  | } else { | 
|  | this.post(mClearClick); | 
|  | } | 
|  | } else { | 
|  | // Select time | 
|  | Time startTime = new Time(mBaseDate); | 
|  | startTime.setJulianDay(mSelectionDay); | 
|  | startTime.hour = mSelectionHour; | 
|  | startTime.normalize(true /* ignore isDst */); | 
|  |  | 
|  | Time endTime = new Time(startTime); | 
|  | endTime.hour++; | 
|  |  | 
|  | mSelectionMode = SELECTION_SELECTED; | 
|  | mController.sendEvent(this, EventType.GO_TO, startTime, endTime, -1, ViewType.CURRENT, | 
|  | CalendarController.EXTRA_GOTO_TIME, null, null); | 
|  | } | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | private void doLongPress(MotionEvent ev) { | 
|  | eventClickCleanup(); | 
|  | if (mScrolling) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Scale gesture in progress | 
|  | if (mStartingSpanY != 0) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | int x = (int) ev.getX(); | 
|  | int y = (int) ev.getY(); | 
|  |  | 
|  | boolean validPosition = setSelectionFromPosition(x, y, false); | 
|  | if (!validPosition) { | 
|  | // return if the touch wasn't on an area of concern | 
|  | return; | 
|  | } | 
|  |  | 
|  | mSelectionMode = SELECTION_LONGPRESS; | 
|  | invalidate(); | 
|  | performLongClick(); | 
|  | } | 
|  |  | 
|  | private void doScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) { | 
|  | cancelAnimation(); | 
|  | if (mStartingScroll) { | 
|  | mInitialScrollX = 0; | 
|  | mInitialScrollY = 0; | 
|  | mStartingScroll = false; | 
|  | } | 
|  |  | 
|  | mInitialScrollX += deltaX; | 
|  | mInitialScrollY += deltaY; | 
|  | int distanceX = (int) mInitialScrollX; | 
|  | int distanceY = (int) mInitialScrollY; | 
|  |  | 
|  | final float focusY = getAverageY(e2); | 
|  | if (mRecalCenterHour) { | 
|  | // Calculate the hour that correspond to the average of the Y touch points | 
|  | mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) | 
|  | / (mCellHeight + DAY_GAP); | 
|  | mRecalCenterHour = false; | 
|  | } | 
|  |  | 
|  | // If we haven't figured out the predominant scroll direction yet, | 
|  | // then do it now. | 
|  | if (mTouchMode == TOUCH_MODE_DOWN) { | 
|  | int absDistanceX = Math.abs(distanceX); | 
|  | int absDistanceY = Math.abs(distanceY); | 
|  | mScrollStartY = mViewStartY; | 
|  | mPreviousDirection = 0; | 
|  |  | 
|  | if (absDistanceX > absDistanceY) { | 
|  | mTouchMode = TOUCH_MODE_HSCROLL; | 
|  | mViewStartX = distanceX; | 
|  | initNextView(-mViewStartX); | 
|  | } else { | 
|  | mTouchMode = TOUCH_MODE_VSCROLL; | 
|  | } | 
|  | } else if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { | 
|  | // We are already scrolling horizontally, so check if we | 
|  | // changed the direction of scrolling so that the other week | 
|  | // is now visible. | 
|  | mViewStartX = distanceX; | 
|  | if (distanceX != 0) { | 
|  | int direction = (distanceX > 0) ? 1 : -1; | 
|  | if (direction != mPreviousDirection) { | 
|  | // The user has switched the direction of scrolling | 
|  | // so re-init the next view | 
|  | initNextView(-mViewStartX); | 
|  | mPreviousDirection = direction; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if ((mTouchMode & TOUCH_MODE_VSCROLL) != 0) { | 
|  | // Calculate the top of the visible region in the calendar grid. | 
|  | // Increasing/decrease this will scroll the calendar grid up/down. | 
|  | mViewStartY = (int) ((mGestureCenterHour * (mCellHeight + DAY_GAP)) | 
|  | - focusY + DAY_HEADER_HEIGHT + mAlldayHeight); | 
|  |  | 
|  | // If dragging while already at the end, do a glow | 
|  | final int pulledToY = (int) (mScrollStartY + deltaY); | 
|  | if (pulledToY < 0) { | 
|  | mEdgeEffectTop.onPull(deltaY / mViewHeight); | 
|  | if (!mEdgeEffectBottom.isFinished()) { | 
|  | mEdgeEffectBottom.onRelease(); | 
|  | } | 
|  | } else if (pulledToY > mMaxViewStartY) { | 
|  | mEdgeEffectBottom.onPull(deltaY / mViewHeight); | 
|  | if (!mEdgeEffectTop.isFinished()) { | 
|  | mEdgeEffectTop.onRelease(); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (mViewStartY < 0) { | 
|  | mViewStartY = 0; | 
|  | mRecalCenterHour = true; | 
|  | } else if (mViewStartY > mMaxViewStartY) { | 
|  | mViewStartY = mMaxViewStartY; | 
|  | mRecalCenterHour = true; | 
|  | } | 
|  | if (mRecalCenterHour) { | 
|  | // Calculate the hour that correspond to the average of the Y touch points | 
|  | mGestureCenterHour = (mViewStartY + focusY - DAY_HEADER_HEIGHT - mAlldayHeight) | 
|  | / (mCellHeight + DAY_GAP); | 
|  | mRecalCenterHour = false; | 
|  | } | 
|  | computeFirstHour(); | 
|  | } | 
|  |  | 
|  | mScrolling = true; | 
|  |  | 
|  | mSelectionMode = SELECTION_HIDDEN; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | private float getAverageY(MotionEvent me) { | 
|  | int count = me.getPointerCount(); | 
|  | float focusY = 0; | 
|  | for (int i = 0; i < count; i++) { | 
|  | focusY += me.getY(i); | 
|  | } | 
|  | focusY /= count; | 
|  | return focusY; | 
|  | } | 
|  |  | 
|  | private void cancelAnimation() { | 
|  | Animation in = mViewSwitcher.getInAnimation(); | 
|  | if (in != null) { | 
|  | // cancel() doesn't terminate cleanly. | 
|  | in.scaleCurrentDuration(0); | 
|  | } | 
|  | Animation out = mViewSwitcher.getOutAnimation(); | 
|  | if (out != null) { | 
|  | // cancel() doesn't terminate cleanly. | 
|  | out.scaleCurrentDuration(0); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void doFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { | 
|  | cancelAnimation(); | 
|  |  | 
|  | mSelectionMode = SELECTION_HIDDEN; | 
|  | eventClickCleanup(); | 
|  |  | 
|  | mOnFlingCalled = true; | 
|  |  | 
|  | if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { | 
|  | // Horizontal fling. | 
|  | // initNextView(deltaX); | 
|  | mTouchMode = TOUCH_MODE_INITIAL_STATE; | 
|  | if (DEBUG) Log.d(TAG, "doFling: velocityX " + velocityX); | 
|  | int deltaX = (int) e2.getX() - (int) e1.getX(); | 
|  | switchViews(deltaX < 0, mViewStartX, mViewWidth, velocityX); | 
|  | mViewStartX = 0; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if ((mTouchMode & TOUCH_MODE_VSCROLL) == 0) { | 
|  | if (DEBUG) Log.d(TAG, "doFling: no fling"); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Vertical fling. | 
|  | mTouchMode = TOUCH_MODE_INITIAL_STATE; | 
|  | mViewStartX = 0; | 
|  |  | 
|  | if (DEBUG) { | 
|  | Log.d(TAG, "doFling: mViewStartY" + mViewStartY + " velocityY " + velocityY); | 
|  | } | 
|  |  | 
|  | // Continue scrolling vertically | 
|  | mScrolling = true; | 
|  | mScroller.fling(0 /* startX */, mViewStartY /* startY */, 0 /* velocityX */, | 
|  | (int) -velocityY, 0 /* minX */, 0 /* maxX */, 0 /* minY */, | 
|  | mMaxViewStartY /* maxY */, OVERFLING_DISTANCE, OVERFLING_DISTANCE); | 
|  |  | 
|  | // When flinging down, show a glow when it hits the end only if it | 
|  | // wasn't started at the top | 
|  | if (velocityY > 0 && mViewStartY != 0) { | 
|  | mCallEdgeEffectOnAbsorb = true; | 
|  | } | 
|  | // When flinging up, show a glow when it hits the end only if it wasn't | 
|  | // started at the bottom | 
|  | else if (velocityY < 0 && mViewStartY != mMaxViewStartY) { | 
|  | mCallEdgeEffectOnAbsorb = true; | 
|  | } | 
|  | mHandler.post(mContinueScroll); | 
|  | } | 
|  |  | 
|  | private boolean initNextView(int deltaX) { | 
|  | // Change the view to the previous day or week | 
|  | DayView view = (DayView) mViewSwitcher.getNextView(); | 
|  | Time date = view.mBaseDate; | 
|  | date.set(mBaseDate); | 
|  | boolean switchForward; | 
|  | if (deltaX > 0) { | 
|  | date.monthDay -= mNumDays; | 
|  | view.setSelectedDay(mSelectionDay - mNumDays); | 
|  | switchForward = false; | 
|  | } else { | 
|  | date.monthDay += mNumDays; | 
|  | view.setSelectedDay(mSelectionDay + mNumDays); | 
|  | switchForward = true; | 
|  | } | 
|  | date.normalize(true /* ignore isDst */); | 
|  | initView(view); | 
|  | view.layout(getLeft(), getTop(), getRight(), getBottom()); | 
|  | view.reloadEvents(); | 
|  | return switchForward; | 
|  | } | 
|  |  | 
|  | // ScaleGestureDetector.OnScaleGestureListener | 
|  | public boolean onScaleBegin(ScaleGestureDetector detector) { | 
|  | mHandleActionUp = false; | 
|  | float gestureCenterInPixels = detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; | 
|  | mGestureCenterHour = (mViewStartY + gestureCenterInPixels) / (mCellHeight + DAY_GAP); | 
|  |  | 
|  | mStartingSpanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); | 
|  | mCellHeightBeforeScaleGesture = mCellHeight; | 
|  |  | 
|  | if (DEBUG_SCALING) { | 
|  | float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); | 
|  | Log.d(TAG, "onScaleBegin: mGestureCenterHour:" + mGestureCenterHour | 
|  | + "\tViewStartHour: " + ViewStartHour + "\tmViewStartY:" + mViewStartY | 
|  | + "\tmCellHeight:" + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // ScaleGestureDetector.OnScaleGestureListener | 
|  | public boolean onScale(ScaleGestureDetector detector) { | 
|  | float spanY = Math.max(MIN_Y_SPAN, Math.abs(detector.getCurrentSpanY())); | 
|  |  | 
|  | mCellHeight = (int) (mCellHeightBeforeScaleGesture * spanY / mStartingSpanY); | 
|  |  | 
|  | if (mCellHeight < mMinCellHeight) { | 
|  | // If mStartingSpanY is too small, even a small increase in the | 
|  | // gesture can bump the mCellHeight beyond MAX_CELL_HEIGHT | 
|  | mStartingSpanY = spanY; | 
|  | mCellHeight = mMinCellHeight; | 
|  | mCellHeightBeforeScaleGesture = mMinCellHeight; | 
|  | } else if (mCellHeight > MAX_CELL_HEIGHT) { | 
|  | mStartingSpanY = spanY; | 
|  | mCellHeight = MAX_CELL_HEIGHT; | 
|  | mCellHeightBeforeScaleGesture = MAX_CELL_HEIGHT; | 
|  | } | 
|  |  | 
|  | int gestureCenterInPixels = (int) detector.getFocusY() - DAY_HEADER_HEIGHT - mAlldayHeight; | 
|  | mViewStartY = (int) (mGestureCenterHour * (mCellHeight + DAY_GAP)) - gestureCenterInPixels; | 
|  | mMaxViewStartY = HOUR_GAP + 24 * (mCellHeight + HOUR_GAP) - mGridAreaHeight; | 
|  |  | 
|  | if (DEBUG_SCALING) { | 
|  | float ViewStartHour = mViewStartY / (float) (mCellHeight + DAY_GAP); | 
|  | Log.d(TAG, "onScale: mGestureCenterHour:" + mGestureCenterHour + "\tViewStartHour: " | 
|  | + ViewStartHour + "\tmViewStartY:" + mViewStartY + "\tmCellHeight:" | 
|  | + mCellHeight + " SpanY:" + detector.getCurrentSpanY()); | 
|  | } | 
|  |  | 
|  | if (mViewStartY < 0) { | 
|  | mViewStartY = 0; | 
|  | mGestureCenterHour = (mViewStartY + gestureCenterInPixels) | 
|  | / (float) (mCellHeight + DAY_GAP); | 
|  | } else if (mViewStartY > mMaxViewStartY) { | 
|  | mViewStartY = mMaxViewStartY; | 
|  | mGestureCenterHour = (mViewStartY + gestureCenterInPixels) | 
|  | / (float) (mCellHeight + DAY_GAP); | 
|  | } | 
|  | computeFirstHour(); | 
|  |  | 
|  | mRemeasure = true; | 
|  | invalidate(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // ScaleGestureDetector.OnScaleGestureListener | 
|  | public void onScaleEnd(ScaleGestureDetector detector) { | 
|  | mScrollStartY = mViewStartY; | 
|  | mInitialScrollY = 0; | 
|  | mInitialScrollX = 0; | 
|  | mStartingSpanY = 0; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onTouchEvent(MotionEvent ev) { | 
|  | int action = ev.getAction(); | 
|  | if (DEBUG) Log.e(TAG, "" + action + " ev.getPointerCount() = " + ev.getPointerCount()); | 
|  |  | 
|  | if ((ev.getActionMasked() == MotionEvent.ACTION_DOWN) || | 
|  | (ev.getActionMasked() == MotionEvent.ACTION_UP) || | 
|  | (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP) || | 
|  | (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN)) { | 
|  | mRecalCenterHour = true; | 
|  | } | 
|  |  | 
|  | if ((mTouchMode & TOUCH_MODE_HSCROLL) == 0) { | 
|  | mScaleGestureDetector.onTouchEvent(ev); | 
|  | } | 
|  |  | 
|  | switch (action) { | 
|  | case MotionEvent.ACTION_DOWN: | 
|  | mStartingScroll = true; | 
|  | if (DEBUG) { | 
|  | Log.e(TAG, "ACTION_DOWN ev.getDownTime = " + ev.getDownTime() + " Cnt=" | 
|  | + ev.getPointerCount()); | 
|  | } | 
|  |  | 
|  | int bottom = mAlldayHeight + DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; | 
|  | if (ev.getY() < bottom) { | 
|  | mTouchStartedInAlldayArea = true; | 
|  | } else { | 
|  | mTouchStartedInAlldayArea = false; | 
|  | } | 
|  | mHandleActionUp = true; | 
|  | mGestureDetector.onTouchEvent(ev); | 
|  | return true; | 
|  |  | 
|  | case MotionEvent.ACTION_MOVE: | 
|  | if (DEBUG) Log.e(TAG, "ACTION_MOVE Cnt=" + ev.getPointerCount() + DayView.this); | 
|  | mGestureDetector.onTouchEvent(ev); | 
|  | return true; | 
|  |  | 
|  | case MotionEvent.ACTION_UP: | 
|  | if (DEBUG) Log.e(TAG, "ACTION_UP Cnt=" + ev.getPointerCount() + mHandleActionUp); | 
|  | mEdgeEffectTop.onRelease(); | 
|  | mEdgeEffectBottom.onRelease(); | 
|  | mStartingScroll = false; | 
|  | mGestureDetector.onTouchEvent(ev); | 
|  | if (!mHandleActionUp) { | 
|  | mHandleActionUp = true; | 
|  | mViewStartX = 0; | 
|  | invalidate(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (mOnFlingCalled) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // If we were scrolling, then reset the selected hour so that it | 
|  | // is visible. | 
|  | if (mScrolling) { | 
|  | mScrolling = false; | 
|  | resetSelectedHour(); | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | if ((mTouchMode & TOUCH_MODE_HSCROLL) != 0) { | 
|  | mTouchMode = TOUCH_MODE_INITIAL_STATE; | 
|  | if (Math.abs(mViewStartX) > mHorizontalSnapBackThreshold) { | 
|  | // The user has gone beyond the threshold so switch views | 
|  | if (DEBUG) Log.d(TAG, "- horizontal scroll: switch views"); | 
|  | switchViews(mViewStartX > 0, mViewStartX, mViewWidth, 0); | 
|  | mViewStartX = 0; | 
|  | return true; | 
|  | } else { | 
|  | // Not beyond the threshold so invalidate which will cause | 
|  | // the view to snap back. Also call recalc() to ensure | 
|  | // that we have the correct starting date and title. | 
|  | if (DEBUG) Log.d(TAG, "- horizontal scroll: snap back"); | 
|  | recalc(); | 
|  | invalidate(); | 
|  | mViewStartX = 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | return true; | 
|  |  | 
|  | // This case isn't expected to happen. | 
|  | case MotionEvent.ACTION_CANCEL: | 
|  | if (DEBUG) Log.e(TAG, "ACTION_CANCEL"); | 
|  | mGestureDetector.onTouchEvent(ev); | 
|  | mScrolling = false; | 
|  | resetSelectedHour(); | 
|  | return true; | 
|  |  | 
|  | default: | 
|  | if (DEBUG) Log.e(TAG, "Not MotionEvent " + ev.toString()); | 
|  | if (mGestureDetector.onTouchEvent(ev)) { | 
|  | return true; | 
|  | } | 
|  | return super.onTouchEvent(ev); | 
|  | } | 
|  | } | 
|  |  | 
|  | public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { | 
|  | MenuItem item; | 
|  |  | 
|  | // If the trackball is held down, then the context menu pops up and | 
|  | // we never get onKeyUp() for the long-press. So check for it here | 
|  | // and change the selection to the long-press state. | 
|  | if (mSelectionMode != SELECTION_LONGPRESS) { | 
|  | mSelectionMode = SELECTION_LONGPRESS; | 
|  | invalidate(); | 
|  | } | 
|  |  | 
|  | final long startMillis = getSelectedTimeInMillis(); | 
|  | int flags = DateUtils.FORMAT_SHOW_TIME | 
|  | | DateUtils.FORMAT_CAP_NOON_MIDNIGHT | 
|  | | DateUtils.FORMAT_SHOW_WEEKDAY; | 
|  | final String title = Utils.formatDateRange(mContext, startMillis, startMillis, flags); | 
|  | menu.setHeaderTitle(title); | 
|  |  | 
|  | int numSelectedEvents = mSelectedEvents.size(); | 
|  | if (mNumDays == 1) { | 
|  | // Day view. | 
|  |  | 
|  | // If there is a selected event, then allow it to be viewed and | 
|  | // edited. | 
|  | if (numSelectedEvents >= 1) { | 
|  | item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_info_details); | 
|  |  | 
|  | int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); | 
|  | if (accessLevel == ACCESS_LEVEL_EDIT) { | 
|  | item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_edit); | 
|  | item.setAlphabeticShortcut('e'); | 
|  | } | 
|  |  | 
|  | if (accessLevel >= ACCESS_LEVEL_DELETE) { | 
|  | item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_delete); | 
|  | } | 
|  |  | 
|  | item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_add); | 
|  | item.setAlphabeticShortcut('n'); | 
|  | } else { | 
|  | // Otherwise, if the user long-pressed on a blank hour, allow | 
|  | // them to create an event. They can also do this by tapping. | 
|  | item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_add); | 
|  | item.setAlphabeticShortcut('n'); | 
|  | } | 
|  | } else { | 
|  | // Week view. | 
|  |  | 
|  | // If there is a selected event, then allow it to be viewed and | 
|  | // edited. | 
|  | if (numSelectedEvents >= 1) { | 
|  | item = menu.add(0, MENU_EVENT_VIEW, 0, R.string.event_view); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_info_details); | 
|  |  | 
|  | int accessLevel = getEventAccessLevel(mContext, mSelectedEvent); | 
|  | if (accessLevel == ACCESS_LEVEL_EDIT) { | 
|  | item = menu.add(0, MENU_EVENT_EDIT, 0, R.string.event_edit); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_edit); | 
|  | item.setAlphabeticShortcut('e'); | 
|  | } | 
|  |  | 
|  | if (accessLevel >= ACCESS_LEVEL_DELETE) { | 
|  | item = menu.add(0, MENU_EVENT_DELETE, 0, R.string.event_delete); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_delete); | 
|  | } | 
|  | } | 
|  |  | 
|  | item = menu.add(0, MENU_EVENT_CREATE, 0, R.string.event_create); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_add); | 
|  | item.setAlphabeticShortcut('n'); | 
|  |  | 
|  | item = menu.add(0, MENU_DAY, 0, R.string.show_day_view); | 
|  | item.setOnMenuItemClickListener(mContextMenuHandler); | 
|  | item.setIcon(android.R.drawable.ic_menu_day); | 
|  | item.setAlphabeticShortcut('d'); | 
|  | } | 
|  |  | 
|  | mPopup.dismiss(); | 
|  | } | 
|  |  | 
|  | private class ContextMenuHandler implements MenuItem.OnMenuItemClickListener { | 
|  | public boolean onMenuItemClick(MenuItem item) { | 
|  | switch (item.getItemId()) { | 
|  | case MENU_EVENT_VIEW: { | 
|  | if (mSelectedEvent != null) { | 
|  | mController.sendEventRelatedEvent(this, EventType.VIEW_EVENT_DETAILS, | 
|  | mSelectedEvent.id, mSelectedEvent.startMillis, | 
|  | mSelectedEvent.endMillis, 0, 0, -1); | 
|  | } | 
|  | break; | 
|  | } | 
|  | case MENU_EVENT_EDIT: { | 
|  | if (mSelectedEvent != null) { | 
|  | mController.sendEventRelatedEvent(this, EventType.EDIT_EVENT, | 
|  | mSelectedEvent.id, mSelectedEvent.startMillis, | 
|  | mSelectedEvent.endMillis, 0, 0, -1); | 
|  | } | 
|  | break; | 
|  | } | 
|  | case MENU_DAY: { | 
|  | mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, | 
|  | ViewType.DAY); | 
|  | break; | 
|  | } | 
|  | case MENU_AGENDA: { | 
|  | mController.sendEvent(this, EventType.GO_TO, getSelectedTime(), null, -1, | 
|  | ViewType.AGENDA); | 
|  | break; | 
|  | } | 
|  | case MENU_EVENT_CREATE: { | 
|  | long startMillis = getSelectedTimeInMillis(); | 
|  | long endMillis = startMillis + DateUtils.HOUR_IN_MILLIS; | 
|  | mController.sendEventRelatedEvent(this, EventType.CREATE_EVENT, -1, | 
|  | startMillis, endMillis, 0, 0, -1); | 
|  | break; | 
|  | } | 
|  | case MENU_EVENT_DELETE: { | 
|  | if (mSelectedEvent != null) { | 
|  | Event selectedEvent = mSelectedEvent; | 
|  | long begin = selectedEvent.startMillis; | 
|  | long end = selectedEvent.endMillis; | 
|  | long id = selectedEvent.id; | 
|  | mController.sendEventRelatedEvent(this, EventType.DELETE_EVENT, id, begin, | 
|  | end, 0, 0, -1); | 
|  | } | 
|  | break; | 
|  | } | 
|  | default: { | 
|  | return false; | 
|  | } | 
|  | } | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | private static int getEventAccessLevel(Context context, Event e) { | 
|  | ContentResolver cr = context.getContentResolver(); | 
|  |  | 
|  | int accessLevel = Calendars.CAL_ACCESS_NONE; | 
|  |  | 
|  | // Get the calendar id for this event | 
|  | Cursor cursor = cr.query(ContentUris.withAppendedId(Events.CONTENT_URI, e.id), | 
|  | new String[] { Events.CALENDAR_ID }, | 
|  | null /* selection */, | 
|  | null /* selectionArgs */, | 
|  | null /* sort */); | 
|  |  | 
|  | if (cursor == null) { | 
|  | return ACCESS_LEVEL_NONE; | 
|  | } | 
|  |  | 
|  | if (cursor.getCount() == 0) { | 
|  | cursor.close(); | 
|  | return ACCESS_LEVEL_NONE; | 
|  | } | 
|  |  | 
|  | cursor.moveToFirst(); | 
|  | long calId = cursor.getLong(0); | 
|  | cursor.close(); | 
|  |  | 
|  | Uri uri = Calendars.CONTENT_URI; | 
|  | String where = String.format(CALENDARS_WHERE, calId); | 
|  | cursor = cr.query(uri, CALENDARS_PROJECTION, where, null, null); | 
|  |  | 
|  | String calendarOwnerAccount = null; | 
|  | if (cursor != null) { | 
|  | cursor.moveToFirst(); | 
|  | accessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL); | 
|  | calendarOwnerAccount = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); | 
|  | cursor.close(); | 
|  | } | 
|  |  | 
|  | if (accessLevel < Calendars.CAL_ACCESS_CONTRIBUTOR) { | 
|  | return ACCESS_LEVEL_NONE; | 
|  | } | 
|  |  | 
|  | if (e.guestsCanModify) { | 
|  | return ACCESS_LEVEL_EDIT; | 
|  | } | 
|  |  | 
|  | if (!TextUtils.isEmpty(calendarOwnerAccount) | 
|  | && calendarOwnerAccount.equalsIgnoreCase(e.organizer)) { | 
|  | return ACCESS_LEVEL_EDIT; | 
|  | } | 
|  |  | 
|  | return ACCESS_LEVEL_DELETE; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Sets mSelectionDay and mSelectionHour based on the (x,y) touch position. | 
|  | * If the touch position is not within the displayed grid, then this | 
|  | * method returns false. | 
|  | * | 
|  | * @param x the x position of the touch | 
|  | * @param y the y position of the touch | 
|  | * @param keepOldSelection - do not change the selection info (used for invoking accessibility | 
|  | *                           messages) | 
|  | * @return true if the touch position is valid | 
|  | */ | 
|  | private boolean setSelectionFromPosition(int x, final int y, boolean keepOldSelection) { | 
|  |  | 
|  | Event savedEvent = null; | 
|  | int savedDay = 0; | 
|  | int savedHour = 0; | 
|  | boolean savedAllDay = false; | 
|  | if (keepOldSelection) { | 
|  | // Store selection info and restore it at the end. This way, we can invoke the | 
|  | // right accessibility message without affecting the selection. | 
|  | savedEvent = mSelectedEvent; | 
|  | savedDay = mSelectionDay; | 
|  | savedHour = mSelectionHour; | 
|  | savedAllDay = mSelectionAllday; | 
|  | } | 
|  | if (x < mHoursWidth) { | 
|  | x = mHoursWidth; | 
|  | } | 
|  |  | 
|  | int day = (x - mHoursWidth) / (mCellWidth + DAY_GAP); | 
|  | if (day >= mNumDays) { | 
|  | day = mNumDays - 1; | 
|  | } | 
|  | day += mFirstJulianDay; | 
|  | setSelectedDay(day); | 
|  |  | 
|  | if (y < DAY_HEADER_HEIGHT) { | 
|  | sendAccessibilityEventAsNeeded(false); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | setSelectedHour(mFirstHour); /* First fully visible hour */ | 
|  |  | 
|  | if (y < mFirstCell) { | 
|  | mSelectionAllday = true; | 
|  | } else { | 
|  | // y is now offset from top of the scrollable region | 
|  | int adjustedY = y - mFirstCell; | 
|  |  | 
|  | if (adjustedY < mFirstHourOffset) { | 
|  | setSelectedHour(mSelectionHour - 1); /* In the partially visible hour */ | 
|  | } else { | 
|  | setSelectedHour(mSelectionHour + | 
|  | (adjustedY - mFirstHourOffset) / (mCellHeight + HOUR_GAP)); | 
|  | } | 
|  |  | 
|  | mSelectionAllday = false; | 
|  | } | 
|  |  | 
|  | findSelectedEvent(x, y); | 
|  |  | 
|  | //        Log.i("Cal", "setSelectionFromPosition( " + x + ", " + y + " ) day: " + day + " hour: " | 
|  | //                + mSelectionHour + " mFirstCell: " + mFirstCell + " mFirstHourOffset: " | 
|  | //                + mFirstHourOffset); | 
|  | //        if (mSelectedEvent != null) { | 
|  | //            Log.i("Cal", "  num events: " + mSelectedEvents.size() + " event: " | 
|  | //                    + mSelectedEvent.title); | 
|  | //            for (Event ev : mSelectedEvents) { | 
|  | //                int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL | 
|  | //                        | DateUtils.FORMAT_CAP_NOON_MIDNIGHT; | 
|  | //                String timeRange = formatDateRange(mContext, ev.startMillis, ev.endMillis, flags); | 
|  | // | 
|  | //                Log.i("Cal", "  " + timeRange + " " + ev.title); | 
|  | //            } | 
|  | //        } | 
|  | sendAccessibilityEventAsNeeded(true); | 
|  |  | 
|  | // Restore old values | 
|  | if (keepOldSelection) { | 
|  | mSelectedEvent = savedEvent; | 
|  | mSelectionDay = savedDay; | 
|  | mSelectionHour = savedHour; | 
|  | mSelectionAllday = savedAllDay; | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | private void findSelectedEvent(int x, int y) { | 
|  | int date = mSelectionDay; | 
|  | int cellWidth = mCellWidth; | 
|  | ArrayList<Event> events = mEvents; | 
|  | int numEvents = events.size(); | 
|  | int left = computeDayLeftPosition(mSelectionDay - mFirstJulianDay); | 
|  | int top = 0; | 
|  | setSelectedEvent(null); | 
|  |  | 
|  | mSelectedEvents.clear(); | 
|  | if (mSelectionAllday) { | 
|  | float yDistance; | 
|  | float minYdistance = 10000.0f; // any large number | 
|  | Event closestEvent = null; | 
|  | float drawHeight = mAlldayHeight; | 
|  | int yOffset = DAY_HEADER_HEIGHT + ALLDAY_TOP_MARGIN; | 
|  | int maxUnexpandedColumn = mMaxUnexpandedAlldayEventCount; | 
|  | if (mMaxAlldayEvents > mMaxUnexpandedAlldayEventCount) { | 
|  | // Leave a gap for the 'box +n' text | 
|  | maxUnexpandedColumn--; | 
|  | } | 
|  | events = mAllDayEvents; | 
|  | numEvents = events.size(); | 
|  | for (int i = 0; i < numEvents; i++) { | 
|  | Event event = events.get(i); | 
|  | if (!event.drawAsAllday() || | 
|  | (!mShowAllAllDayEvents && event.getColumn() >= maxUnexpandedColumn)) { | 
|  | // Don't check non-allday events or events that aren't shown | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (event.startDay <= mSelectionDay && event.endDay >= mSelectionDay) { | 
|  | float numRectangles = mShowAllAllDayEvents ? mMaxAlldayEvents | 
|  | : mMaxUnexpandedAlldayEventCount; | 
|  | float height = drawHeight / numRectangles; | 
|  | if (height > MAX_HEIGHT_OF_ONE_ALLDAY_EVENT) { | 
|  | height = MAX_HEIGHT_OF_ONE_ALLDAY_EVENT; | 
|  | } | 
|  | float eventTop = yOffset + height * event.getColumn(); | 
|  | float eventBottom = eventTop + height; | 
|  | if (eventTop < y && eventBottom > y) { | 
|  | // If the touch is inside the event rectangle, then | 
|  | // add the event. | 
|  | mSelectedEvents.add(event); | 
|  | closestEvent = event; | 
|  | break; | 
|  | } else { | 
|  | // Find the closest event | 
|  | if (eventTop >= y) { | 
|  | yDistance = eventTop - y; | 
|  | } else { | 
|  | yDistance = y - eventBottom; | 
|  | } | 
|  | if (yDistance < minYdistance) { | 
|  | minYdistance = yDistance; | 
|  | closestEvent = event; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | setSelectedEvent(closestEvent); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Adjust y for the scrollable bitmap | 
|  | y += mViewStartY - mFirstCell; | 
|  |  | 
|  | // Use a region around (x,y) for the selection region | 
|  | Rect region = mRect; | 
|  | region.left = x - 10; | 
|  | region.right = x + 10; | 
|  | region.top = y - 10; | 
|  | region.bottom = y + 10; | 
|  |  | 
|  | EventGeometry geometry = mEventGeometry; | 
|  |  | 
|  | for (int i = 0; i < numEvents; i++) { | 
|  | Event event = events.get(i); | 
|  | // Compute the event rectangle. | 
|  | if (!geometry.computeEventRect(date, left, top, cellWidth, event)) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // If the event intersects the selection region, then add it to | 
|  | // mSelectedEvents. | 
|  | if (geometry.eventIntersectsSelection(event, region)) { | 
|  | mSelectedEvents.add(event); | 
|  | } | 
|  | } | 
|  |  | 
|  | // If there are any events in the selected region, then assign the | 
|  | // closest one to mSelectedEvent. | 
|  | if (mSelectedEvents.size() > 0) { | 
|  | int len = mSelectedEvents.size(); | 
|  | Event closestEvent = null; | 
|  | float minDist = mViewWidth + mViewHeight; // some large distance | 
|  | for (int index = 0; index < len; index++) { | 
|  | Event ev = mSelectedEvents.get(index); | 
|  | float dist = geometry.pointToEvent(x, y, ev); | 
|  | if (dist < minDist) { | 
|  | minDist = dist; | 
|  | closestEvent = ev; | 
|  | } | 
|  | } | 
|  | setSelectedEvent(closestEvent); | 
|  |  | 
|  | // Keep the selected hour and day consistent with the selected | 
|  | // event. They could be different if we touched on an empty hour | 
|  | // slot very close to an event in the previous hour slot. In | 
|  | // that case we will select the nearby event. | 
|  | int startDay = mSelectedEvent.startDay; | 
|  | int endDay = mSelectedEvent.endDay; | 
|  | if (mSelectionDay < startDay) { | 
|  | setSelectedDay(startDay); | 
|  | } else if (mSelectionDay > endDay) { | 
|  | setSelectedDay(endDay); | 
|  | } | 
|  |  | 
|  | int startHour = mSelectedEvent.startTime / 60; | 
|  | int endHour; | 
|  | if (mSelectedEvent.startTime < mSelectedEvent.endTime) { | 
|  | endHour = (mSelectedEvent.endTime - 1) / 60; | 
|  | } else { | 
|  | endHour = mSelectedEvent.endTime / 60; | 
|  | } | 
|  |  | 
|  | if (mSelectionHour < startHour && mSelectionDay == startDay) { | 
|  | setSelectedHour(startHour); | 
|  | } else if (mSelectionHour > endHour && mSelectionDay == endDay) { | 
|  | setSelectedHour(endHour); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Encapsulates the code to continue the scrolling after the | 
|  | // finger is lifted. Instead of stopping the scroll immediately, | 
|  | // the scroll continues to "free spin" and gradually slows down. | 
|  | private class ContinueScroll implements Runnable { | 
|  | public void run() { | 
|  | mScrolling = mScrolling && mScroller.computeScrollOffset(); | 
|  | if (!mScrolling || mPaused) { | 
|  | resetSelectedHour(); | 
|  | invalidate(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | mViewStartY = mScroller.getCurrY(); | 
|  |  | 
|  | if (mCallEdgeEffectOnAbsorb) { | 
|  | if (mViewStartY < 0) { | 
|  | mEdgeEffectTop.onAbsorb((int) mLastVelocity); | 
|  | mCallEdgeEffectOnAbsorb = false; | 
|  | } else if (mViewStartY > mMaxViewStartY) { | 
|  | mEdgeEffectBottom.onAbsorb((int) mLastVelocity); | 
|  | mCallEdgeEffectOnAbsorb = false; | 
|  | } | 
|  | mLastVelocity = mScroller.getCurrVelocity(); | 
|  | } | 
|  |  | 
|  | if (mScrollStartY == 0 || mScrollStartY == mMaxViewStartY) { | 
|  | // Allow overscroll/springback only on a fling, | 
|  | // not a pull/fling from the end | 
|  | if (mViewStartY < 0) { | 
|  | mViewStartY = 0; | 
|  | } else if (mViewStartY > mMaxViewStartY) { | 
|  | mViewStartY = mMaxViewStartY; | 
|  | } | 
|  | } | 
|  |  | 
|  | computeFirstHour(); | 
|  | mHandler.post(this); | 
|  | invalidate(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Cleanup the pop-up and timers. | 
|  | */ | 
|  | public void cleanup() { | 
|  | // Protect against null-pointer exceptions | 
|  | if (mPopup != null) { | 
|  | mPopup.dismiss(); | 
|  | } | 
|  | mPaused = true; | 
|  | mLastPopupEventID = INVALID_EVENT_ID; | 
|  | if (mHandler != null) { | 
|  | mHandler.removeCallbacks(mDismissPopup); | 
|  | mHandler.removeCallbacks(mUpdateCurrentTime); | 
|  | } | 
|  |  | 
|  | Utils.setSharedPreference(mContext, GeneralPreferences.KEY_DEFAULT_CELL_HEIGHT, | 
|  | mCellHeight); | 
|  | // Clear all click animations | 
|  | eventClickCleanup(); | 
|  | // Turn off redraw | 
|  | mRemeasure = false; | 
|  | // Turn off scrolling to make sure the view is in the correct state if we fling back to it | 
|  | mScrolling = false; | 
|  | } | 
|  |  | 
|  | private void eventClickCleanup() { | 
|  | this.removeCallbacks(mClearClick); | 
|  | this.removeCallbacks(mSetClick); | 
|  | mClickedEvent = null; | 
|  | mSavedClickedEvent = null; | 
|  | } | 
|  |  | 
|  | private void setSelectedEvent(Event e) { | 
|  | mSelectedEvent = e; | 
|  | mSelectedEventForAccessibility = e; | 
|  | } | 
|  |  | 
|  | private void setSelectedHour(int h) { | 
|  | mSelectionHour = h; | 
|  | mSelectionHourForAccessibility = h; | 
|  | } | 
|  | private void setSelectedDay(int d) { | 
|  | mSelectionDay = d; | 
|  | mSelectionDayForAccessibility = d; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Restart the update timer | 
|  | */ | 
|  | public void restartCurrentTimeUpdates() { | 
|  | mPaused = false; | 
|  | if (mHandler != null) { | 
|  | mHandler.removeCallbacks(mUpdateCurrentTime); | 
|  | mHandler.post(mUpdateCurrentTime); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected void onDetachedFromWindow() { | 
|  | cleanup(); | 
|  | super.onDetachedFromWindow(); | 
|  | } | 
|  |  | 
|  | class DismissPopup implements Runnable { | 
|  | public void run() { | 
|  | // Protect against null-pointer exceptions | 
|  | if (mPopup != null) { | 
|  | mPopup.dismiss(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | class UpdateCurrentTime implements Runnable { | 
|  | public void run() { | 
|  | long currentTime = System.currentTimeMillis(); | 
|  | mCurrentTime.set(currentTime); | 
|  | //% causes update to occur on 5 minute marks (11:10, 11:15, 11:20, etc.) | 
|  | if (!DayView.this.mPaused) { | 
|  | mHandler.postDelayed(mUpdateCurrentTime, UPDATE_CURRENT_TIME_DELAY | 
|  | - (currentTime % UPDATE_CURRENT_TIME_DELAY)); | 
|  | } | 
|  | mTodayJulianDay = Time.getJulianDay(currentTime, mCurrentTime.gmtoff); | 
|  | invalidate(); | 
|  | } | 
|  | } | 
|  |  | 
|  | class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { | 
|  | @Override | 
|  | public boolean onSingleTapUp(MotionEvent ev) { | 
|  | if (DEBUG) Log.e(TAG, "GestureDetector.onSingleTapUp"); | 
|  | DayView.this.doSingleTapUp(ev); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onLongPress(MotionEvent ev) { | 
|  | if (DEBUG) Log.e(TAG, "GestureDetector.onLongPress"); | 
|  | DayView.this.doLongPress(ev); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { | 
|  | if (DEBUG) Log.e(TAG, "GestureDetector.onScroll"); | 
|  | eventClickCleanup(); | 
|  | if (mTouchStartedInAlldayArea) { | 
|  | if (Math.abs(distanceX) < Math.abs(distanceY)) { | 
|  | // Make sure that click feedback is gone when you scroll from the | 
|  | // all day area | 
|  | invalidate(); | 
|  | return false; | 
|  | } | 
|  | // don't scroll vertically if this started in the allday area | 
|  | distanceY = 0; | 
|  | } | 
|  | DayView.this.doScroll(e1, e2, distanceX, distanceY); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { | 
|  | if (DEBUG) Log.e(TAG, "GestureDetector.onFling"); | 
|  |  | 
|  | if (mTouchStartedInAlldayArea) { | 
|  | if (Math.abs(velocityX) < Math.abs(velocityY)) { | 
|  | return false; | 
|  | } | 
|  | // don't fling vertically if this started in the allday area | 
|  | velocityY = 0; | 
|  | } | 
|  | DayView.this.doFling(e1, e2, velocityX, velocityY); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onDown(MotionEvent ev) { | 
|  | if (DEBUG) Log.e(TAG, "GestureDetector.onDown"); | 
|  | DayView.this.doDown(ev); | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean onLongClick(View v) { | 
|  | int flags = DateUtils.FORMAT_SHOW_WEEKDAY; | 
|  | long time = getSelectedTimeInMillis(); | 
|  | if (!mSelectionAllday) { | 
|  | flags |= DateUtils.FORMAT_SHOW_TIME; | 
|  | } | 
|  | if (DateFormat.is24HourFormat(mContext)) { | 
|  | flags |= DateUtils.FORMAT_24HOUR; | 
|  | } | 
|  | mLongPressTitle = Utils.formatDateRange(mContext, time, time, flags); | 
|  | new AlertDialog.Builder(mContext).setTitle(mLongPressTitle) | 
|  | .setItems(mLongPressItems, new DialogInterface.OnClickListener() { | 
|  | @Override | 
|  | public void onClick(DialogInterface dialog, int which) { | 
|  | if (which == 0) { | 
|  | long extraLong = 0; | 
|  | if (mSelectionAllday) { | 
|  | extraLong = CalendarController.EXTRA_CREATE_ALL_DAY; | 
|  | } | 
|  | mController.sendEventRelatedEventWithExtra(this, | 
|  | EventType.CREATE_EVENT, -1, getSelectedTimeInMillis(), 0, -1, | 
|  | -1, extraLong, -1); | 
|  | } | 
|  | } | 
|  | }).show().setCanceledOnTouchOutside(true); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // The rest of this file was borrowed from Launcher2 - PagedView.java | 
|  | private static final int MINIMUM_SNAP_VELOCITY = 2200; | 
|  |  | 
|  | private class ScrollInterpolator implements Interpolator { | 
|  | public ScrollInterpolator() { | 
|  | } | 
|  |  | 
|  | public float getInterpolation(float t) { | 
|  | t -= 1.0f; | 
|  | t = t * t * t * t * t + 1; | 
|  |  | 
|  | if ((1 - t) * mAnimationDistance < 1) { | 
|  | cancelAnimation(); | 
|  | } | 
|  |  | 
|  | return t; | 
|  | } | 
|  | } | 
|  |  | 
|  | private long calculateDuration(float delta, float width, float velocity) { | 
|  | /* | 
|  | * Here we compute a "distance" that will be used in the computation of | 
|  | * the overall snap duration. This is a function of the actual distance | 
|  | * that needs to be traveled; we keep this value close to half screen | 
|  | * size in order to reduce the variance in snap duration as a function | 
|  | * of the distance the page needs to travel. | 
|  | */ | 
|  | final float halfScreenSize = width / 2; | 
|  | float distanceRatio = delta / width; | 
|  | float distanceInfluenceForSnapDuration = distanceInfluenceForSnapDuration(distanceRatio); | 
|  | float distance = halfScreenSize + halfScreenSize * distanceInfluenceForSnapDuration; | 
|  |  | 
|  | velocity = Math.abs(velocity); | 
|  | velocity = Math.max(MINIMUM_SNAP_VELOCITY, velocity); | 
|  |  | 
|  | /* | 
|  | * we want the page's snap velocity to approximately match the velocity | 
|  | * at which the user flings, so we scale the duration by a value near to | 
|  | * the derivative of the scroll interpolator at zero, ie. 5. We use 6 to | 
|  | * make it a little slower. | 
|  | */ | 
|  | long duration = 6 * Math.round(1000 * Math.abs(distance / velocity)); | 
|  | if (DEBUG) { | 
|  | Log.e(TAG, "halfScreenSize:" + halfScreenSize + " delta:" + delta + " distanceRatio:" | 
|  | + distanceRatio + " distance:" + distance + " velocity:" + velocity | 
|  | + " duration:" + duration + " distanceInfluenceForSnapDuration:" | 
|  | + distanceInfluenceForSnapDuration); | 
|  | } | 
|  | return duration; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * We want the duration of the page snap animation to be influenced by the | 
|  | * distance that the screen has to travel, however, we don't want this | 
|  | * duration to be effected in a purely linear fashion. Instead, we use this | 
|  | * method to moderate the effect that the distance of travel has on the | 
|  | * overall snap duration. | 
|  | */ | 
|  | private float distanceInfluenceForSnapDuration(float f) { | 
|  | f -= 0.5f; // center the values about 0. | 
|  | f *= 0.3f * Math.PI / 2.0f; | 
|  | return (float) Math.sin(f); | 
|  | } | 
|  | } |