Home | History | Annotate | Download | only in month
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.calendar.month;
     18 
     19 import com.android.calendar.Event;
     20 import com.android.calendar.R;
     21 import com.android.calendar.Utils;
     22 
     23 import android.animation.Animator;
     24 import android.animation.AnimatorListenerAdapter;
     25 import android.animation.ObjectAnimator;
     26 import android.app.Service;
     27 import android.content.Context;
     28 import android.content.res.Configuration;
     29 import android.content.res.Resources;
     30 import android.graphics.Canvas;
     31 import android.graphics.Color;
     32 import android.graphics.Paint;
     33 import android.graphics.Paint.Align;
     34 import android.graphics.Paint.Style;
     35 import android.graphics.Typeface;
     36 import android.graphics.drawable.Drawable;
     37 import android.provider.CalendarContract.Attendees;
     38 import android.text.TextPaint;
     39 import android.text.TextUtils;
     40 import android.text.format.DateFormat;
     41 import android.text.format.DateUtils;
     42 import android.text.format.Time;
     43 import android.util.Log;
     44 import android.view.MotionEvent;
     45 import android.view.accessibility.AccessibilityEvent;
     46 import android.view.accessibility.AccessibilityManager;
     47 
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.Formatter;
     51 import java.util.HashMap;
     52 import java.util.Iterator;
     53 import java.util.List;
     54 import java.util.Locale;
     55 
     56 public class MonthWeekEventsView extends SimpleWeekView {
     57 
     58     private static final String TAG = "MonthView";
     59 
     60     private static final boolean DEBUG_LAYOUT = false;
     61 
     62     public static final String VIEW_PARAMS_ORIENTATION = "orientation";
     63     public static final String VIEW_PARAMS_ANIMATE_TODAY = "animate_today";
     64 
     65     /* NOTE: these are not constants, and may be multiplied by a scale factor */
     66     private static int TEXT_SIZE_MONTH_NUMBER = 32;
     67     private static int TEXT_SIZE_EVENT = 12;
     68     private static int TEXT_SIZE_EVENT_TITLE = 14;
     69     private static int TEXT_SIZE_MORE_EVENTS = 12;
     70     private static int TEXT_SIZE_MONTH_NAME = 14;
     71     private static int TEXT_SIZE_WEEK_NUM = 12;
     72 
     73     private static int DNA_MARGIN = 4;
     74     private static int DNA_ALL_DAY_HEIGHT = 4;
     75     private static int DNA_MIN_SEGMENT_HEIGHT = 4;
     76     private static int DNA_WIDTH = 8;
     77     private static int DNA_ALL_DAY_WIDTH = 32;
     78     private static int DNA_SIDE_PADDING = 6;
     79     private static int CONFLICT_COLOR = Color.BLACK;
     80     private static int EVENT_TEXT_COLOR = Color.WHITE;
     81 
     82     private static int DEFAULT_EDGE_SPACING = 0;
     83     private static int SIDE_PADDING_MONTH_NUMBER = 4;
     84     private static int TOP_PADDING_MONTH_NUMBER = 4;
     85     private static int TOP_PADDING_WEEK_NUMBER = 4;
     86     private static int SIDE_PADDING_WEEK_NUMBER = 20;
     87     private static int DAY_SEPARATOR_OUTER_WIDTH = 0;
     88     private static int DAY_SEPARATOR_INNER_WIDTH = 1;
     89     private static int DAY_SEPARATOR_VERTICAL_LENGTH = 53;
     90     private static int DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT = 64;
     91     private static int MIN_WEEK_WIDTH = 50;
     92 
     93     private static int EVENT_X_OFFSET_LANDSCAPE = 38;
     94     private static int EVENT_Y_OFFSET_LANDSCAPE = 8;
     95     private static int EVENT_Y_OFFSET_PORTRAIT = 7;
     96     private static int EVENT_SQUARE_WIDTH = 10;
     97     private static int EVENT_SQUARE_BORDER = 2;
     98     private static int EVENT_LINE_PADDING = 2;
     99     private static int EVENT_RIGHT_PADDING = 4;
    100     private static int EVENT_BOTTOM_PADDING = 3;
    101 
    102     private static int TODAY_HIGHLIGHT_WIDTH = 2;
    103 
    104     private static int SPACING_WEEK_NUMBER = 24;
    105     private static boolean mInitialized = false;
    106     private static boolean mShowDetailsInMonth;
    107 
    108     protected Time mToday = new Time();
    109     protected boolean mHasToday = false;
    110     protected int mTodayIndex = -1;
    111     protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE;
    112     protected List<ArrayList<Event>> mEvents = null;
    113     protected ArrayList<Event> mUnsortedEvents = null;
    114     HashMap<Integer, Utils.DNAStrand> mDna = null;
    115     // This is for drawing the outlines around event chips and supports up to 10
    116     // events being drawn on each day. The code will expand this if necessary.
    117     protected FloatRef mEventOutlines = new FloatRef(10 * 4 * 4 * 7);
    118 
    119 
    120 
    121     protected static StringBuilder mStringBuilder = new StringBuilder(50);
    122     // TODO recreate formatter when locale changes
    123     protected static Formatter mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
    124 
    125     protected Paint mMonthNamePaint;
    126     protected TextPaint mEventPaint;
    127     protected TextPaint mSolidBackgroundEventPaint;
    128     protected TextPaint mFramedEventPaint;
    129     protected TextPaint mDeclinedEventPaint;
    130     protected TextPaint mEventExtrasPaint;
    131     protected TextPaint mEventDeclinedExtrasPaint;
    132     protected Paint mWeekNumPaint;
    133     protected Paint mDNAAllDayPaint;
    134     protected Paint mDNATimePaint;
    135     protected Paint mEventSquarePaint;
    136 
    137 
    138     protected Drawable mTodayDrawable;
    139 
    140     protected int mMonthNumHeight;
    141     protected int mMonthNumAscentHeight;
    142     protected int mEventHeight;
    143     protected int mEventAscentHeight;
    144     protected int mExtrasHeight;
    145     protected int mExtrasAscentHeight;
    146     protected int mExtrasDescent;
    147     protected int mWeekNumAscentHeight;
    148 
    149     protected int mMonthBGColor;
    150     protected int mMonthBGOtherColor;
    151     protected int mMonthBGTodayColor;
    152     protected int mMonthNumColor;
    153     protected int mMonthNumOtherColor;
    154     protected int mMonthNumTodayColor;
    155     protected int mMonthNameColor;
    156     protected int mMonthNameOtherColor;
    157     protected int mMonthEventColor;
    158     protected int mMonthDeclinedEventColor;
    159     protected int mMonthDeclinedExtrasColor;
    160     protected int mMonthEventExtraColor;
    161     protected int mMonthEventOtherColor;
    162     protected int mMonthEventExtraOtherColor;
    163     protected int mMonthWeekNumColor;
    164     protected int mMonthBusyBitsBgColor;
    165     protected int mMonthBusyBitsBusyTimeColor;
    166     protected int mMonthBusyBitsConflictTimeColor;
    167     private int mClickedDayIndex = -1;
    168     private int mClickedDayColor;
    169     private static final int mClickedAlpha = 128;
    170 
    171     protected int mEventChipOutlineColor = 0xFFFFFFFF;
    172     protected int mDaySeparatorInnerColor;
    173     protected int mTodayAnimateColor;
    174 
    175     private boolean mAnimateToday;
    176     private int mAnimateTodayAlpha = 0;
    177     private ObjectAnimator mTodayAnimator = null;
    178 
    179     private final TodayAnimatorListener mAnimatorListener = new TodayAnimatorListener();
    180 
    181     class TodayAnimatorListener extends AnimatorListenerAdapter {
    182         private volatile Animator mAnimator = null;
    183         private volatile boolean mFadingIn = false;
    184 
    185         @Override
    186         public void onAnimationEnd(Animator animation) {
    187             synchronized (this) {
    188                 if (mAnimator != animation) {
    189                     animation.removeAllListeners();
    190                     animation.cancel();
    191                     return;
    192                 }
    193                 if (mFadingIn) {
    194                     if (mTodayAnimator != null) {
    195                         mTodayAnimator.removeAllListeners();
    196                         mTodayAnimator.cancel();
    197                     }
    198                     mTodayAnimator = ObjectAnimator.ofInt(MonthWeekEventsView.this,
    199                             "animateTodayAlpha", 255, 0);
    200                     mAnimator = mTodayAnimator;
    201                     mFadingIn = false;
    202                     mTodayAnimator.addListener(this);
    203                     mTodayAnimator.setDuration(600);
    204                     mTodayAnimator.start();
    205                 } else {
    206                     mAnimateToday = false;
    207                     mAnimateTodayAlpha = 0;
    208                     mAnimator.removeAllListeners();
    209                     mAnimator = null;
    210                     mTodayAnimator = null;
    211                     invalidate();
    212                 }
    213             }
    214         }
    215 
    216         public void setAnimator(Animator animation) {
    217             mAnimator = animation;
    218         }
    219 
    220         public void setFadingIn(boolean fadingIn) {
    221             mFadingIn = fadingIn;
    222         }
    223 
    224     }
    225 
    226     private int[] mDayXs;
    227 
    228     /**
    229      * This provides a reference to a float array which allows for easy size
    230      * checking and reallocation. Used for drawing lines.
    231      */
    232     private class FloatRef {
    233         float[] array;
    234 
    235         public FloatRef(int size) {
    236             array = new float[size];
    237         }
    238 
    239         public void ensureSize(int newSize) {
    240             if (newSize >= array.length) {
    241                 // Add enough space for 7 more boxes to be drawn
    242                 array = Arrays.copyOf(array, newSize + 16 * 7);
    243             }
    244         }
    245     }
    246 
    247     /**
    248      * Shows up as an error if we don't include this.
    249      */
    250     public MonthWeekEventsView(Context context) {
    251         super(context);
    252     }
    253 
    254     // Sets the list of events for this week. Takes a sorted list of arrays
    255     // divided up by day for generating the large month version and the full
    256     // arraylist sorted by start time to generate the dna version.
    257     public void setEvents(List<ArrayList<Event>> sortedEvents, ArrayList<Event> unsortedEvents) {
    258         setEvents(sortedEvents);
    259         // The MIN_WEEK_WIDTH is a hack to prevent the view from trying to
    260         // generate dna bits before its width has been fixed.
    261         createDna(unsortedEvents);
    262     }
    263 
    264     /**
    265      * Sets up the dna bits for the view. This will return early if the view
    266      * isn't in a state that will create a valid set of dna yet (such as the
    267      * views width not being set correctly yet).
    268      */
    269     public void createDna(ArrayList<Event> unsortedEvents) {
    270         if (unsortedEvents == null || mWidth <= MIN_WEEK_WIDTH || getContext() == null) {
    271             // Stash the list of events for use when this view is ready, or
    272             // just clear it if a null set has been passed to this view
    273             mUnsortedEvents = unsortedEvents;
    274             mDna = null;
    275             return;
    276         } else {
    277             // clear the cached set of events since we're ready to build it now
    278             mUnsortedEvents = null;
    279         }
    280         // Create the drawing coordinates for dna
    281         if (!mShowDetailsInMonth) {
    282             int numDays = mEvents.size();
    283             int effectiveWidth = mWidth - mPadding * 2;
    284             if (mShowWeekNum) {
    285                 effectiveWidth -= SPACING_WEEK_NUMBER;
    286             }
    287             DNA_ALL_DAY_WIDTH = effectiveWidth / numDays - 2 * DNA_SIDE_PADDING;
    288             mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
    289             mDayXs = new int[numDays];
    290             for (int day = 0; day < numDays; day++) {
    291                 mDayXs[day] = computeDayLeftPosition(day) + DNA_WIDTH / 2 + DNA_SIDE_PADDING;
    292 
    293             }
    294 
    295             int top = DAY_SEPARATOR_INNER_WIDTH + DNA_MARGIN + DNA_ALL_DAY_HEIGHT + 1;
    296             int bottom = mHeight - DNA_MARGIN;
    297             mDna = Utils.createDNAStrands(mFirstJulianDay, unsortedEvents, top, bottom,
    298                     DNA_MIN_SEGMENT_HEIGHT, mDayXs, getContext());
    299         }
    300     }
    301 
    302     public void setEvents(List<ArrayList<Event>> sortedEvents) {
    303         mEvents = sortedEvents;
    304         if (sortedEvents == null) {
    305             return;
    306         }
    307         if (sortedEvents.size() != mNumDays) {
    308             if (Log.isLoggable(TAG, Log.ERROR)) {
    309                 Log.wtf(TAG, "Events size must be same as days displayed: size="
    310                         + sortedEvents.size() + " days=" + mNumDays);
    311             }
    312             mEvents = null;
    313             return;
    314         }
    315     }
    316 
    317     protected void loadColors(Context context) {
    318         Resources res = context.getResources();
    319         mMonthWeekNumColor = res.getColor(R.color.month_week_num_color);
    320         mMonthNumColor = res.getColor(R.color.month_day_number);
    321         mMonthNumOtherColor = res.getColor(R.color.month_day_number_other);
    322         mMonthNumTodayColor = res.getColor(R.color.month_today_number);
    323         mMonthNameColor = mMonthNumColor;
    324         mMonthNameOtherColor = mMonthNumOtherColor;
    325         mMonthEventColor = res.getColor(R.color.month_event_color);
    326         mMonthDeclinedEventColor = res.getColor(R.color.agenda_item_declined_color);
    327         mMonthDeclinedExtrasColor = res.getColor(R.color.agenda_item_where_declined_text_color);
    328         mMonthEventExtraColor = res.getColor(R.color.month_event_extra_color);
    329         mMonthEventOtherColor = res.getColor(R.color.month_event_other_color);
    330         mMonthEventExtraOtherColor = res.getColor(R.color.month_event_extra_other_color);
    331         mMonthBGTodayColor = res.getColor(R.color.month_today_bgcolor);
    332         mMonthBGOtherColor = res.getColor(R.color.month_other_bgcolor);
    333         mMonthBGColor = res.getColor(R.color.month_bgcolor);
    334         mDaySeparatorInnerColor = res.getColor(R.color.month_grid_lines);
    335         mTodayAnimateColor = res.getColor(R.color.today_highlight_color);
    336         mClickedDayColor = res.getColor(R.color.day_clicked_background_color);
    337         mTodayDrawable = res.getDrawable(R.drawable.today_blue_week_holo_light);
    338     }
    339 
    340     /**
    341      * Sets up the text and style properties for painting. Override this if you
    342      * want to use a different paint.
    343      */
    344     @Override
    345     protected void initView() {
    346         super.initView();
    347 
    348         if (!mInitialized) {
    349             Resources resources = getContext().getResources();
    350             mShowDetailsInMonth = Utils.getConfigBool(getContext(), R.bool.show_details_in_month);
    351             TEXT_SIZE_EVENT_TITLE = resources.getInteger(R.integer.text_size_event_title);
    352             TEXT_SIZE_MONTH_NUMBER = resources.getInteger(R.integer.text_size_month_number);
    353             SIDE_PADDING_MONTH_NUMBER = resources.getInteger(R.integer.month_day_number_margin);
    354             CONFLICT_COLOR = resources.getColor(R.color.month_dna_conflict_time_color);
    355             EVENT_TEXT_COLOR = resources.getColor(R.color.calendar_event_text_color);
    356             if (mScale != 1) {
    357                 TOP_PADDING_MONTH_NUMBER *= mScale;
    358                 TOP_PADDING_WEEK_NUMBER *= mScale;
    359                 SIDE_PADDING_MONTH_NUMBER *= mScale;
    360                 SIDE_PADDING_WEEK_NUMBER *= mScale;
    361                 SPACING_WEEK_NUMBER *= mScale;
    362                 TEXT_SIZE_MONTH_NUMBER *= mScale;
    363                 TEXT_SIZE_EVENT *= mScale;
    364                 TEXT_SIZE_EVENT_TITLE *= mScale;
    365                 TEXT_SIZE_MORE_EVENTS *= mScale;
    366                 TEXT_SIZE_MONTH_NAME *= mScale;
    367                 TEXT_SIZE_WEEK_NUM *= mScale;
    368                 DAY_SEPARATOR_OUTER_WIDTH *= mScale;
    369                 DAY_SEPARATOR_INNER_WIDTH *= mScale;
    370                 DAY_SEPARATOR_VERTICAL_LENGTH *= mScale;
    371                 DAY_SEPARATOR_VERTICAL_LENGHT_PORTRAIT *= mScale;
    372                 EVENT_X_OFFSET_LANDSCAPE *= mScale;
    373                 EVENT_Y_OFFSET_LANDSCAPE *= mScale;
    374                 EVENT_Y_OFFSET_PORTRAIT *= mScale;
    375                 EVENT_SQUARE_WIDTH *= mScale;
    376                 EVENT_SQUARE_BORDER *= mScale;
    377                 EVENT_LINE_PADDING *= mScale;
    378                 EVENT_BOTTOM_PADDING *= mScale;
    379                 EVENT_RIGHT_PADDING *= mScale;
    380                 DNA_MARGIN *= mScale;
    381                 DNA_WIDTH *= mScale;
    382                 DNA_ALL_DAY_HEIGHT *= mScale;
    383                 DNA_MIN_SEGMENT_HEIGHT *= mScale;
    384                 DNA_SIDE_PADDING *= mScale;
    385                 DEFAULT_EDGE_SPACING *= mScale;
    386                 DNA_ALL_DAY_WIDTH *= mScale;
    387                 TODAY_HIGHLIGHT_WIDTH *= mScale;
    388             }
    389             if (!mShowDetailsInMonth) {
    390                 TOP_PADDING_MONTH_NUMBER += DNA_ALL_DAY_HEIGHT + DNA_MARGIN;
    391             }
    392             mInitialized = true;
    393         }
    394         mPadding = DEFAULT_EDGE_SPACING;
    395         loadColors(getContext());
    396         // TODO modify paint properties depending on isMini
    397 
    398         mMonthNumPaint = new Paint();
    399         mMonthNumPaint.setFakeBoldText(false);
    400         mMonthNumPaint.setAntiAlias(true);
    401         mMonthNumPaint.setTextSize(TEXT_SIZE_MONTH_NUMBER);
    402         mMonthNumPaint.setColor(mMonthNumColor);
    403         mMonthNumPaint.setStyle(Style.FILL);
    404         mMonthNumPaint.setTextAlign(Align.RIGHT);
    405         mMonthNumPaint.setTypeface(Typeface.DEFAULT);
    406 
    407         mMonthNumAscentHeight = (int) (-mMonthNumPaint.ascent() + 0.5f);
    408         mMonthNumHeight = (int) (mMonthNumPaint.descent() - mMonthNumPaint.ascent() + 0.5f);
    409 
    410         mEventPaint = new TextPaint();
    411         mEventPaint.setFakeBoldText(true);
    412         mEventPaint.setAntiAlias(true);
    413         mEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
    414         mEventPaint.setColor(mMonthEventColor);
    415 
    416         mSolidBackgroundEventPaint = new TextPaint(mEventPaint);
    417         mSolidBackgroundEventPaint.setColor(EVENT_TEXT_COLOR);
    418         mFramedEventPaint = new TextPaint(mSolidBackgroundEventPaint);
    419 
    420         mDeclinedEventPaint = new TextPaint();
    421         mDeclinedEventPaint.setFakeBoldText(true);
    422         mDeclinedEventPaint.setAntiAlias(true);
    423         mDeclinedEventPaint.setTextSize(TEXT_SIZE_EVENT_TITLE);
    424         mDeclinedEventPaint.setColor(mMonthDeclinedEventColor);
    425 
    426         mEventAscentHeight = (int) (-mEventPaint.ascent() + 0.5f);
    427         mEventHeight = (int) (mEventPaint.descent() - mEventPaint.ascent() + 0.5f);
    428 
    429         mEventExtrasPaint = new TextPaint();
    430         mEventExtrasPaint.setFakeBoldText(false);
    431         mEventExtrasPaint.setAntiAlias(true);
    432         mEventExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
    433         mEventExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
    434         mEventExtrasPaint.setColor(mMonthEventExtraColor);
    435         mEventExtrasPaint.setStyle(Style.FILL);
    436         mEventExtrasPaint.setTextAlign(Align.LEFT);
    437         mExtrasHeight = (int)(mEventExtrasPaint.descent() - mEventExtrasPaint.ascent() + 0.5f);
    438         mExtrasAscentHeight = (int)(-mEventExtrasPaint.ascent() + 0.5f);
    439         mExtrasDescent = (int)(mEventExtrasPaint.descent() + 0.5f);
    440 
    441         mEventDeclinedExtrasPaint = new TextPaint();
    442         mEventDeclinedExtrasPaint.setFakeBoldText(false);
    443         mEventDeclinedExtrasPaint.setAntiAlias(true);
    444         mEventDeclinedExtrasPaint.setStrokeWidth(EVENT_SQUARE_BORDER);
    445         mEventDeclinedExtrasPaint.setTextSize(TEXT_SIZE_EVENT);
    446         mEventDeclinedExtrasPaint.setColor(mMonthDeclinedExtrasColor);
    447         mEventDeclinedExtrasPaint.setStyle(Style.FILL);
    448         mEventDeclinedExtrasPaint.setTextAlign(Align.LEFT);
    449 
    450         mWeekNumPaint = new Paint();
    451         mWeekNumPaint.setFakeBoldText(false);
    452         mWeekNumPaint.setAntiAlias(true);
    453         mWeekNumPaint.setTextSize(TEXT_SIZE_WEEK_NUM);
    454         mWeekNumPaint.setColor(mWeekNumColor);
    455         mWeekNumPaint.setStyle(Style.FILL);
    456         mWeekNumPaint.setTextAlign(Align.RIGHT);
    457 
    458         mWeekNumAscentHeight = (int) (-mWeekNumPaint.ascent() + 0.5f);
    459 
    460         mDNAAllDayPaint = new Paint();
    461         mDNATimePaint = new Paint();
    462         mDNATimePaint.setColor(mMonthBusyBitsBusyTimeColor);
    463         mDNATimePaint.setStyle(Style.FILL_AND_STROKE);
    464         mDNATimePaint.setStrokeWidth(DNA_WIDTH);
    465         mDNATimePaint.setAntiAlias(false);
    466         mDNAAllDayPaint.setColor(mMonthBusyBitsConflictTimeColor);
    467         mDNAAllDayPaint.setStyle(Style.FILL_AND_STROKE);
    468         mDNAAllDayPaint.setStrokeWidth(DNA_ALL_DAY_WIDTH);
    469         mDNAAllDayPaint.setAntiAlias(false);
    470 
    471         mEventSquarePaint = new Paint();
    472         mEventSquarePaint.setStrokeWidth(EVENT_SQUARE_BORDER);
    473         mEventSquarePaint.setAntiAlias(false);
    474 
    475         if (DEBUG_LAYOUT) {
    476             Log.d("EXTRA", "mScale=" + mScale);
    477             Log.d("EXTRA", "mMonthNumPaint ascent=" + mMonthNumPaint.ascent()
    478                     + " descent=" + mMonthNumPaint.descent() + " int height=" + mMonthNumHeight);
    479             Log.d("EXTRA", "mEventPaint ascent=" + mEventPaint.ascent()
    480                     + " descent=" + mEventPaint.descent() + " int height=" + mEventHeight
    481                     + " int ascent=" + mEventAscentHeight);
    482             Log.d("EXTRA", "mEventExtrasPaint ascent=" + mEventExtrasPaint.ascent()
    483                     + " descent=" + mEventExtrasPaint.descent() + " int height=" + mExtrasHeight);
    484             Log.d("EXTRA", "mWeekNumPaint ascent=" + mWeekNumPaint.ascent()
    485                     + " descent=" + mWeekNumPaint.descent());
    486         }
    487     }
    488 
    489     @Override
    490     public void setWeekParams(HashMap<String, Integer> params, String tz) {
    491         super.setWeekParams(params, tz);
    492 
    493         if (params.containsKey(VIEW_PARAMS_ORIENTATION)) {
    494             mOrientation = params.get(VIEW_PARAMS_ORIENTATION);
    495         }
    496 
    497         updateToday(tz);
    498         mNumCells = mNumDays + 1;
    499 
    500         if (params.containsKey(VIEW_PARAMS_ANIMATE_TODAY) && mHasToday) {
    501             synchronized (mAnimatorListener) {
    502                 if (mTodayAnimator != null) {
    503                     mTodayAnimator.removeAllListeners();
    504                     mTodayAnimator.cancel();
    505                 }
    506                 mTodayAnimator = ObjectAnimator.ofInt(this, "animateTodayAlpha",
    507                         Math.max(mAnimateTodayAlpha, 80), 255);
    508                 mTodayAnimator.setDuration(150);
    509                 mAnimatorListener.setAnimator(mTodayAnimator);
    510                 mAnimatorListener.setFadingIn(true);
    511                 mTodayAnimator.addListener(mAnimatorListener);
    512                 mAnimateToday = true;
    513                 mTodayAnimator.start();
    514             }
    515         }
    516     }
    517 
    518     /**
    519      * @param tz
    520      */
    521     public boolean updateToday(String tz) {
    522         mToday.timezone = tz;
    523         mToday.setToNow();
    524         mToday.normalize(true);
    525         int julianToday = Time.getJulianDay(mToday.toMillis(false), mToday.gmtoff);
    526         if (julianToday >= mFirstJulianDay && julianToday < mFirstJulianDay + mNumDays) {
    527             mHasToday = true;
    528             mTodayIndex = julianToday - mFirstJulianDay;
    529         } else {
    530             mHasToday = false;
    531             mTodayIndex = -1;
    532         }
    533         return mHasToday;
    534     }
    535 
    536     public void setAnimateTodayAlpha(int alpha) {
    537         mAnimateTodayAlpha = alpha;
    538         invalidate();
    539     }
    540 
    541     @Override
    542     protected void onDraw(Canvas canvas) {
    543         drawBackground(canvas);
    544         drawWeekNums(canvas);
    545         drawDaySeparators(canvas);
    546         if (mHasToday && mAnimateToday) {
    547             drawToday(canvas);
    548         }
    549         if (mShowDetailsInMonth) {
    550             drawEvents(canvas);
    551         } else {
    552             if (mDna == null && mUnsortedEvents != null) {
    553                 createDna(mUnsortedEvents);
    554             }
    555             drawDNA(canvas);
    556         }
    557         drawClick(canvas);
    558     }
    559 
    560     protected void drawToday(Canvas canvas) {
    561         r.top = DAY_SEPARATOR_INNER_WIDTH + (TODAY_HIGHLIGHT_WIDTH / 2);
    562         r.bottom = mHeight - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
    563         p.setStyle(Style.STROKE);
    564         p.setStrokeWidth(TODAY_HIGHLIGHT_WIDTH);
    565         r.left = computeDayLeftPosition(mTodayIndex) + (TODAY_HIGHLIGHT_WIDTH / 2);
    566         r.right = computeDayLeftPosition(mTodayIndex + 1)
    567                 - (int) Math.ceil(TODAY_HIGHLIGHT_WIDTH / 2.0f);
    568         p.setColor(mTodayAnimateColor | (mAnimateTodayAlpha << 24));
    569         canvas.drawRect(r, p);
    570         p.setStyle(Style.FILL);
    571     }
    572 
    573     // TODO move into SimpleWeekView
    574     // Computes the x position for the left side of the given day
    575     private int computeDayLeftPosition(int day) {
    576         int effectiveWidth = mWidth;
    577         int x = 0;
    578         int xOffset = 0;
    579         if (mShowWeekNum) {
    580             xOffset = SPACING_WEEK_NUMBER + mPadding;
    581             effectiveWidth -= xOffset;
    582         }
    583         x = day * effectiveWidth / mNumDays + xOffset;
    584         return x;
    585     }
    586 
    587     @Override
    588     protected void drawDaySeparators(Canvas canvas) {
    589         float lines[] = new float[8 * 4];
    590         int count = 6 * 4;
    591         int wkNumOffset = 0;
    592         int i = 0;
    593         if (mShowWeekNum) {
    594             // This adds the first line separating the week number
    595             int xOffset = SPACING_WEEK_NUMBER + mPadding;
    596             count += 4;
    597             lines[i++] = xOffset;
    598             lines[i++] = 0;
    599             lines[i++] = xOffset;
    600             lines[i++] = mHeight;
    601             wkNumOffset++;
    602         }
    603         count += 4;
    604         lines[i++] = 0;
    605         lines[i++] = 0;
    606         lines[i++] = mWidth;
    607         lines[i++] = 0;
    608         int y0 = 0;
    609         int y1 = mHeight;
    610 
    611         while (i < count) {
    612             int x = computeDayLeftPosition(i / 4 - wkNumOffset);
    613             lines[i++] = x;
    614             lines[i++] = y0;
    615             lines[i++] = x;
    616             lines[i++] = y1;
    617         }
    618         p.setColor(mDaySeparatorInnerColor);
    619         p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH);
    620         canvas.drawLines(lines, 0, count, p);
    621     }
    622 
    623     @Override
    624     protected void drawBackground(Canvas canvas) {
    625         int i = 0;
    626         int offset = 0;
    627         r.top = DAY_SEPARATOR_INNER_WIDTH;
    628         r.bottom = mHeight;
    629         if (mShowWeekNum) {
    630             i++;
    631             offset++;
    632         }
    633         if (!mOddMonth[i]) {
    634             while (++i < mOddMonth.length && !mOddMonth[i])
    635                 ;
    636             r.right = computeDayLeftPosition(i - offset);
    637             r.left = 0;
    638             p.setColor(mMonthBGOtherColor);
    639             canvas.drawRect(r, p);
    640             // compute left edge for i, set up r, draw
    641         } else if (!mOddMonth[(i = mOddMonth.length - 1)]) {
    642             while (--i >= offset && !mOddMonth[i])
    643                 ;
    644             i++;
    645             // compute left edge for i, set up r, draw
    646             r.right = mWidth;
    647             r.left = computeDayLeftPosition(i - offset);
    648             p.setColor(mMonthBGOtherColor);
    649             canvas.drawRect(r, p);
    650         }
    651         if (mHasToday) {
    652             p.setColor(mMonthBGTodayColor);
    653             r.left = computeDayLeftPosition(mTodayIndex);
    654             r.right = computeDayLeftPosition(mTodayIndex + 1);
    655             canvas.drawRect(r, p);
    656         }
    657     }
    658 
    659     // Draw the "clicked" color on the tapped day
    660     private void drawClick(Canvas canvas) {
    661         if (mClickedDayIndex != -1) {
    662             int alpha = p.getAlpha();
    663             p.setColor(mClickedDayColor);
    664             p.setAlpha(mClickedAlpha);
    665             r.left = computeDayLeftPosition(mClickedDayIndex);
    666             r.right = computeDayLeftPosition(mClickedDayIndex + 1);
    667             r.top = DAY_SEPARATOR_INNER_WIDTH;
    668             r.bottom = mHeight;
    669             canvas.drawRect(r, p);
    670             p.setAlpha(alpha);
    671         }
    672     }
    673 
    674     @Override
    675     protected void drawWeekNums(Canvas canvas) {
    676         int y;
    677 
    678         int i = 0;
    679         int offset = -1;
    680         int todayIndex = mTodayIndex;
    681         int x = 0;
    682         int numCount = mNumDays;
    683         if (mShowWeekNum) {
    684             x = SIDE_PADDING_WEEK_NUMBER + mPadding;
    685             y = mWeekNumAscentHeight + TOP_PADDING_WEEK_NUMBER;
    686             canvas.drawText(mDayNumbers[0], x, y, mWeekNumPaint);
    687             numCount++;
    688             i++;
    689             todayIndex++;
    690             offset++;
    691 
    692         }
    693 
    694         y = mMonthNumAscentHeight + TOP_PADDING_MONTH_NUMBER;
    695 
    696         boolean isFocusMonth = mFocusDay[i];
    697         boolean isBold = false;
    698         mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
    699         for (; i < numCount; i++) {
    700             if (mHasToday && todayIndex == i) {
    701                 mMonthNumPaint.setColor(mMonthNumTodayColor);
    702                 mMonthNumPaint.setFakeBoldText(isBold = true);
    703                 if (i + 1 < numCount) {
    704                     // Make sure the color will be set back on the next
    705                     // iteration
    706                     isFocusMonth = !mFocusDay[i + 1];
    707                 }
    708             } else if (mFocusDay[i] != isFocusMonth) {
    709                 isFocusMonth = mFocusDay[i];
    710                 mMonthNumPaint.setColor(isFocusMonth ? mMonthNumColor : mMonthNumOtherColor);
    711             }
    712             x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER);
    713             canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
    714             if (isBold) {
    715                 mMonthNumPaint.setFakeBoldText(isBold = false);
    716             }
    717         }
    718     }
    719 
    720     protected void drawEvents(Canvas canvas) {
    721         if (mEvents == null) {
    722             return;
    723         }
    724 
    725         int day = -1;
    726         for (ArrayList<Event> eventDay : mEvents) {
    727             day++;
    728             if (eventDay == null || eventDay.size() == 0) {
    729                 continue;
    730             }
    731             int ySquare;
    732             int xSquare = computeDayLeftPosition(day) + SIDE_PADDING_MONTH_NUMBER + 1;
    733             int rightEdge = computeDayLeftPosition(day + 1);
    734 
    735             if (mOrientation == Configuration.ORIENTATION_PORTRAIT) {
    736                 ySquare = EVENT_Y_OFFSET_PORTRAIT + mMonthNumHeight + TOP_PADDING_MONTH_NUMBER;
    737                 rightEdge -= SIDE_PADDING_MONTH_NUMBER + 1;
    738             } else {
    739                 ySquare = EVENT_Y_OFFSET_LANDSCAPE;
    740                 rightEdge -= EVENT_X_OFFSET_LANDSCAPE;
    741             }
    742 
    743             // Determine if everything will fit when time ranges are shown.
    744             boolean showTimes = true;
    745             Iterator<Event> iter = eventDay.iterator();
    746             int yTest = ySquare;
    747             while (iter.hasNext()) {
    748                 Event event = iter.next();
    749                 int newY = drawEvent(canvas, event, xSquare, yTest, rightEdge, iter.hasNext(),
    750                         showTimes, /*doDraw*/ false);
    751                 if (newY == yTest) {
    752                     showTimes = false;
    753                     break;
    754                 }
    755                 yTest = newY;
    756             }
    757 
    758             int eventCount = 0;
    759             iter = eventDay.iterator();
    760             while (iter.hasNext()) {
    761                 Event event = iter.next();
    762                 int newY = drawEvent(canvas, event, xSquare, ySquare, rightEdge, iter.hasNext(),
    763                         showTimes, /*doDraw*/ true);
    764                 if (newY == ySquare) {
    765                     break;
    766                 }
    767                 eventCount++;
    768                 ySquare = newY;
    769             }
    770 
    771             int remaining = eventDay.size() - eventCount;
    772             if (remaining > 0) {
    773                 drawMoreEvents(canvas, remaining, xSquare);
    774             }
    775         }
    776     }
    777 
    778     protected int addChipOutline(FloatRef lines, int count, int x, int y) {
    779         lines.ensureSize(count + 16);
    780         // top of box
    781         lines.array[count++] = x;
    782         lines.array[count++] = y;
    783         lines.array[count++] = x + EVENT_SQUARE_WIDTH;
    784         lines.array[count++] = y;
    785         // right side of box
    786         lines.array[count++] = x + EVENT_SQUARE_WIDTH;
    787         lines.array[count++] = y;
    788         lines.array[count++] = x + EVENT_SQUARE_WIDTH;
    789         lines.array[count++] = y + EVENT_SQUARE_WIDTH;
    790         // left side of box
    791         lines.array[count++] = x;
    792         lines.array[count++] = y;
    793         lines.array[count++] = x;
    794         lines.array[count++] = y + EVENT_SQUARE_WIDTH + 1;
    795         // bottom of box
    796         lines.array[count++] = x;
    797         lines.array[count++] = y + EVENT_SQUARE_WIDTH;
    798         lines.array[count++] = x + EVENT_SQUARE_WIDTH + 1;
    799         lines.array[count++] = y + EVENT_SQUARE_WIDTH;
    800 
    801         return count;
    802     }
    803 
    804     /**
    805      * Attempts to draw the given event. Returns the y for the next event or the
    806      * original y if the event will not fit. An event is considered to not fit
    807      * if the event and its extras won't fit or if there are more events and the
    808      * more events line would not fit after drawing this event.
    809      *
    810      * @param canvas the canvas to draw on
    811      * @param event the event to draw
    812      * @param x the top left corner for this event's color chip
    813      * @param y the top left corner for this event's color chip
    814      * @param rightEdge the rightmost point we're allowed to draw on (exclusive)
    815      * @param moreEvents indicates whether additional events will follow this one
    816      * @param showTimes if set, a second line with a time range will be displayed for non-all-day
    817      *   events
    818      * @param doDraw if set, do the actual drawing; otherwise this just computes the height
    819      *   and returns
    820      * @return the y for the next event or the original y if it won't fit
    821      */
    822     protected int drawEvent(Canvas canvas, Event event, int x, int y, int rightEdge,
    823             boolean moreEvents, boolean showTimes, boolean doDraw) {
    824         /*
    825          * Vertical layout:
    826          *   (top of box)
    827          * a. EVENT_Y_OFFSET_LANDSCAPE or portrait equivalent
    828          * b. Event title: mEventHeight for a normal event, + 2xBORDER_SPACE for all-day event
    829          * c. [optional] Time range (mExtrasHeight)
    830          * d. EVENT_LINE_PADDING
    831          *
    832          * Repeat (b,c,d) as needed and space allows.  If we have more events than fit, we need
    833          * to leave room for something like "+2" at the bottom:
    834          *
    835          * e. "+ more" line (mExtrasHeight)
    836          *
    837          * f. EVENT_BOTTOM_PADDING (overlaps EVENT_LINE_PADDING)
    838          *   (bottom of box)
    839          */
    840         final int BORDER_SPACE = EVENT_SQUARE_BORDER + 1;       // want a 1-pixel gap inside border
    841         final int STROKE_WIDTH_ADJ = EVENT_SQUARE_BORDER / 2;   // adjust bounds for stroke width
    842         boolean allDay = event.allDay;
    843         int eventRequiredSpace = mEventHeight;
    844         if (allDay) {
    845             // Add a few pixels for the box we draw around all-day events.
    846             eventRequiredSpace += BORDER_SPACE * 2;
    847         } else if (showTimes) {
    848             // Need room for the "1pm - 2pm" line.
    849             eventRequiredSpace += mExtrasHeight;
    850         }
    851         int reservedSpace = EVENT_BOTTOM_PADDING;   // leave a bit of room at the bottom
    852         if (moreEvents) {
    853             // More events follow.  Leave a bit of space between events.
    854             eventRequiredSpace += EVENT_LINE_PADDING;
    855 
    856             // Make sure we have room for the "+ more" line.  (The "+ more" line is expected
    857             // to be <= the height of an event line, so we won't show "+1" when we could be
    858             // showing the event.)
    859             reservedSpace += mExtrasHeight;
    860         }
    861 
    862         if (y + eventRequiredSpace + reservedSpace > mHeight) {
    863             // Not enough space, return original y
    864             return y;
    865         } else if (!doDraw) {
    866             return y + eventRequiredSpace;
    867         }
    868 
    869         boolean isDeclined = event.selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED;
    870         int color = event.color;
    871         if (isDeclined) {
    872             color = Utils.getDeclinedColorFromColor(color);
    873         }
    874 
    875         int textX, textY, textRightEdge;
    876 
    877         if (allDay) {
    878             // We shift the render offset "inward", because drawRect with a stroke width greater
    879             // than 1 draws outside the specified bounds.  (We don't adjust the left edge, since
    880             // we want to match the existing appearance of the "event square".)
    881             r.left = x;
    882             r.right = rightEdge - STROKE_WIDTH_ADJ;
    883             r.top = y + STROKE_WIDTH_ADJ;
    884             r.bottom = y + mEventHeight + BORDER_SPACE * 2 - STROKE_WIDTH_ADJ;
    885             textX = x + BORDER_SPACE;
    886             textY = y + mEventAscentHeight + BORDER_SPACE;
    887             textRightEdge = rightEdge - BORDER_SPACE;
    888         } else {
    889             r.left = x;
    890             r.right = x + EVENT_SQUARE_WIDTH;
    891             r.bottom = y + mEventAscentHeight;
    892             r.top = r.bottom - EVENT_SQUARE_WIDTH;
    893             textX = x + EVENT_SQUARE_WIDTH + EVENT_RIGHT_PADDING;
    894             textY = y + mEventAscentHeight;
    895             textRightEdge = rightEdge;
    896         }
    897 
    898         Style boxStyle = Style.STROKE;
    899         boolean solidBackground = false;
    900         if (event.selfAttendeeStatus != Attendees.ATTENDEE_STATUS_INVITED) {
    901             boxStyle = Style.FILL_AND_STROKE;
    902             if (allDay) {
    903                 solidBackground = true;
    904             }
    905         }
    906         mEventSquarePaint.setStyle(boxStyle);
    907         mEventSquarePaint.setColor(color);
    908         canvas.drawRect(r, mEventSquarePaint);
    909 
    910         float avail = textRightEdge - textX;
    911         CharSequence text = TextUtils.ellipsize(
    912                 event.title, mEventPaint, avail, TextUtils.TruncateAt.END);
    913         Paint textPaint;
    914         if (solidBackground) {
    915             // Text color needs to contrast with solid background.
    916             textPaint = mSolidBackgroundEventPaint;
    917         } else if (isDeclined) {
    918             // Use "declined event" color.
    919             textPaint = mDeclinedEventPaint;
    920         } else if (allDay) {
    921             // Text inside frame is same color as frame.
    922             mFramedEventPaint.setColor(color);
    923             textPaint = mFramedEventPaint;
    924         } else {
    925             // Use generic event text color.
    926             textPaint = mEventPaint;
    927         }
    928         canvas.drawText(text.toString(), textX, textY, textPaint);
    929         y += mEventHeight;
    930         if (allDay) {
    931             y += BORDER_SPACE * 2;
    932         }
    933 
    934         if (showTimes && !allDay) {
    935             // show start/end time, e.g. "1pm - 2pm"
    936             textY = y + mExtrasAscentHeight;
    937             mStringBuilder.setLength(0);
    938             text = DateUtils.formatDateRange(getContext(), mFormatter, event.startMillis,
    939                     event.endMillis, DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL,
    940                     Utils.getTimeZone(getContext(), null)).toString();
    941             text = TextUtils.ellipsize(text, mEventExtrasPaint, avail, TextUtils.TruncateAt.END);
    942             canvas.drawText(text.toString(), textX, textY, isDeclined ? mEventDeclinedExtrasPaint
    943                     : mEventExtrasPaint);
    944             y += mExtrasHeight;
    945         }
    946 
    947         y += EVENT_LINE_PADDING;
    948 
    949         return y;
    950     }
    951 
    952     protected void drawMoreEvents(Canvas canvas, int remainingEvents, int x) {
    953         int y = mHeight - (mExtrasDescent + EVENT_BOTTOM_PADDING);
    954         String text = getContext().getResources().getQuantityString(
    955                 R.plurals.month_more_events, remainingEvents);
    956         mEventExtrasPaint.setAntiAlias(true);
    957         mEventExtrasPaint.setFakeBoldText(true);
    958         canvas.drawText(String.format(text, remainingEvents), x, y, mEventExtrasPaint);
    959         mEventExtrasPaint.setFakeBoldText(false);
    960     }
    961 
    962     /**
    963      * Draws a line showing busy times in each day of week The method draws
    964      * non-conflicting times in the event color and times with conflicting
    965      * events in the dna conflict color defined in colors.
    966      *
    967      * @param canvas
    968      */
    969     protected void drawDNA(Canvas canvas) {
    970         // Draw event and conflict times
    971         if (mDna != null) {
    972             for (Utils.DNAStrand strand : mDna.values()) {
    973                 if (strand.color == CONFLICT_COLOR || strand.points == null
    974                         || strand.points.length == 0) {
    975                     continue;
    976                 }
    977                 mDNATimePaint.setColor(strand.color);
    978                 canvas.drawLines(strand.points, mDNATimePaint);
    979             }
    980             // Draw black last to make sure it's on top
    981             Utils.DNAStrand strand = mDna.get(CONFLICT_COLOR);
    982             if (strand != null && strand.points != null && strand.points.length != 0) {
    983                 mDNATimePaint.setColor(strand.color);
    984                 canvas.drawLines(strand.points, mDNATimePaint);
    985             }
    986             if (mDayXs == null) {
    987                 return;
    988             }
    989             int numDays = mDayXs.length;
    990             int xOffset = (DNA_ALL_DAY_WIDTH - DNA_WIDTH) / 2;
    991             if (strand != null && strand.allDays != null && strand.allDays.length == numDays) {
    992                 for (int i = 0; i < numDays; i++) {
    993                     // this adds at most 7 draws. We could sort it by color and
    994                     // build an array instead but this is easier.
    995                     if (strand.allDays[i] != 0) {
    996                         mDNAAllDayPaint.setColor(strand.allDays[i]);
    997                         canvas.drawLine(mDayXs[i] + xOffset, DNA_MARGIN, mDayXs[i] + xOffset,
    998                                 DNA_MARGIN + DNA_ALL_DAY_HEIGHT, mDNAAllDayPaint);
    999                     }
   1000                 }
   1001             }
   1002         }
   1003     }
   1004 
   1005     @Override
   1006     protected void updateSelectionPositions() {
   1007         if (mHasSelectedDay) {
   1008             int selectedPosition = mSelectedDay - mWeekStart;
   1009             if (selectedPosition < 0) {
   1010                 selectedPosition += 7;
   1011             }
   1012             int effectiveWidth = mWidth - mPadding * 2;
   1013             effectiveWidth -= SPACING_WEEK_NUMBER;
   1014             mSelectedLeft = selectedPosition * effectiveWidth / mNumDays + mPadding;
   1015             mSelectedRight = (selectedPosition + 1) * effectiveWidth / mNumDays + mPadding;
   1016             mSelectedLeft += SPACING_WEEK_NUMBER;
   1017             mSelectedRight += SPACING_WEEK_NUMBER;
   1018         }
   1019     }
   1020 
   1021     public int getDayIndexFromLocation(float x) {
   1022         int dayStart = mShowWeekNum ? SPACING_WEEK_NUMBER + mPadding : mPadding;
   1023         if (x < dayStart || x > mWidth - mPadding) {
   1024             return -1;
   1025         }
   1026         // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
   1027         return ((int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mPadding)));
   1028     }
   1029 
   1030     @Override
   1031     public Time getDayFromLocation(float x) {
   1032         int dayPosition = getDayIndexFromLocation(x);
   1033         if (dayPosition == -1) {
   1034             return null;
   1035         }
   1036         int day = mFirstJulianDay + dayPosition;
   1037 
   1038         Time time = new Time(mTimeZone);
   1039         if (mWeek == 0) {
   1040             // This week is weird...
   1041             if (day < Time.EPOCH_JULIAN_DAY) {
   1042                 day++;
   1043             } else if (day == Time.EPOCH_JULIAN_DAY) {
   1044                 time.set(1, 0, 1970);
   1045                 time.normalize(true);
   1046                 return time;
   1047             }
   1048         }
   1049 
   1050         time.setJulianDay(day);
   1051         return time;
   1052     }
   1053 
   1054     @Override
   1055     public boolean onHoverEvent(MotionEvent event) {
   1056         Context context = getContext();
   1057         // only send accessibility events if accessibility and exploration are
   1058         // on.
   1059         AccessibilityManager am = (AccessibilityManager) context
   1060                 .getSystemService(Service.ACCESSIBILITY_SERVICE);
   1061         if (!am.isEnabled() || !am.isTouchExplorationEnabled()) {
   1062             return super.onHoverEvent(event);
   1063         }
   1064         if (event.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
   1065             Time hover = getDayFromLocation(event.getX());
   1066             if (hover != null
   1067                     && (mLastHoverTime == null || Time.compare(hover, mLastHoverTime) != 0)) {
   1068                 Long millis = hover.toMillis(true);
   1069                 String date = Utils.formatDateRange(context, millis, millis,
   1070                         DateUtils.FORMAT_SHOW_DATE);
   1071                 AccessibilityEvent accessEvent = AccessibilityEvent
   1072                         .obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
   1073                 accessEvent.getText().add(date);
   1074                 if (mShowDetailsInMonth && mEvents != null) {
   1075                     int dayStart = SPACING_WEEK_NUMBER + mPadding;
   1076                     int dayPosition = (int) ((event.getX() - dayStart) * mNumDays / (mWidth
   1077                             - dayStart - mPadding));
   1078                     ArrayList<Event> events = mEvents.get(dayPosition);
   1079                     List<CharSequence> text = accessEvent.getText();
   1080                     for (Event e : events) {
   1081                         text.add(e.getTitleAndLocation() + ". ");
   1082                         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
   1083                         if (!e.allDay) {
   1084                             flags |= DateUtils.FORMAT_SHOW_TIME;
   1085                             if (DateFormat.is24HourFormat(context)) {
   1086                                 flags |= DateUtils.FORMAT_24HOUR;
   1087                             }
   1088                         } else {
   1089                             flags |= DateUtils.FORMAT_UTC;
   1090                         }
   1091                         text.add(Utils.formatDateRange(context, e.startMillis, e.endMillis,
   1092                                 flags) + ". ");
   1093                     }
   1094                 }
   1095                 sendAccessibilityEventUnchecked(accessEvent);
   1096                 mLastHoverTime = hover;
   1097             }
   1098         }
   1099         return true;
   1100     }
   1101 
   1102     public void setClickedDay(float xLocation) {
   1103         mClickedDayIndex = getDayIndexFromLocation(xLocation);
   1104         invalidate();
   1105     }
   1106     public void clearClickedDay() {
   1107         mClickedDayIndex = -1;
   1108         invalidate();
   1109     }
   1110 }
   1111