MonthByWeekAdapter.java revision cc272e7bb0c63c09993cb33e4c9e1c5ded75874c
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calendar.month;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.text.format.Time;
22import android.util.Log;
23import android.view.GestureDetector;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.ViewConfiguration;
27import android.view.ViewGroup;
28import android.widget.AbsListView.LayoutParams;
29
30import com.android.calendar.CalendarController;
31import com.android.calendar.CalendarController.EventType;
32import com.android.calendar.CalendarController.ViewType;
33import com.android.calendar.Event;
34import com.android.calendar.R;
35import com.android.calendar.Utils;
36
37import java.util.ArrayList;
38import java.util.HashMap;
39
40public class MonthByWeekAdapter extends SimpleWeeksAdapter {
41    private static final String TAG = "MonthByWeek";
42
43    public static final String WEEK_PARAMS_IS_MINI = "mini_month";
44    protected static int DEFAULT_QUERY_DAYS = 7 * 8; // 8 weeks
45    private static final long ANIMATE_TODAY_TIMEOUT = 1000;
46
47    protected CalendarController mController;
48    protected String mHomeTimeZone;
49    protected Time mTempTime;
50    protected Time mToday;
51    protected int mFirstJulianDay;
52    protected int mQueryDays;
53    protected boolean mIsMiniMonth = true;
54    protected int mOrientation = Configuration.ORIENTATION_LANDSCAPE;
55    private final boolean mShowAgendaWithMonth;
56
57    protected ArrayList<ArrayList<Event>> mEventDayList = new ArrayList<ArrayList<Event>>();
58    protected ArrayList<Event> mEvents = null;
59
60    private boolean mAnimateToday = false;
61    private long mAnimateTime = 0;
62
63    MonthWeekEventsView mClickedView;
64    MonthWeekEventsView mSingleTapUpView;
65
66    float mClickedXLocation;                // Used to find which day was clicked
67    long mClickTime;                        // Used to calculate minimum click animation time
68    // Used to insure minimal time for seeing the click animation before switching views
69    private static final int mOnTapDelay = 100;
70    // Minimal time for a down touch action before stating the click animation, this insures that
71    // there is no click animation on flings
72    private static int mOnDownDelay;
73    private static int mTotalClickDelay;
74    // Minimal distance to move the finger in order to cancel the click animation
75    private static float mMovedPixelToCancel;
76
77    public MonthByWeekAdapter(Context context, HashMap<String, Integer> params) {
78        super(context, params);
79        if (params.containsKey(WEEK_PARAMS_IS_MINI)) {
80            mIsMiniMonth = params.get(WEEK_PARAMS_IS_MINI) != 0;
81        }
82        mShowAgendaWithMonth = Utils.getConfigBool(context, R.bool.show_agenda_with_month);
83        ViewConfiguration vc = ViewConfiguration.get(context);
84        mOnDownDelay = vc.getTapTimeout();
85        mMovedPixelToCancel = vc.getScaledTouchSlop();
86        mTotalClickDelay = mOnDownDelay + mOnTapDelay;
87
88    }
89
90    public void animateToday() {
91        mAnimateToday = true;
92        mAnimateTime = System.currentTimeMillis();
93    }
94
95    @Override
96    protected void init() {
97        super.init();
98        mController = CalendarController.getInstance(mContext);
99        mHomeTimeZone = Utils.getTimeZone(mContext, null);
100        mSelectedDay.switchTimezone(mHomeTimeZone);
101        mToday = new Time(mHomeTimeZone);
102        mToday.setToNow();
103        mTempTime = new Time(mHomeTimeZone);
104    }
105
106    private void updateTimeZones() {
107        mSelectedDay.timezone = mHomeTimeZone;
108        mSelectedDay.normalize(true);
109        mToday.timezone = mHomeTimeZone;
110        mToday.setToNow();
111        mTempTime.switchTimezone(mHomeTimeZone);
112    }
113
114    @Override
115    public void setSelectedDay(Time selectedTime) {
116        mSelectedDay.set(selectedTime);
117        long millis = mSelectedDay.normalize(true);
118        mSelectedWeek = Utils.getWeeksSinceEpochFromJulianDay(
119                Time.getJulianDay(millis, mSelectedDay.gmtoff), mFirstDayOfWeek);
120        notifyDataSetChanged();
121    }
122
123    public void setEvents(int firstJulianDay, int numDays, ArrayList<Event> events) {
124        if (mIsMiniMonth) {
125            if (Log.isLoggable(TAG, Log.ERROR)) {
126                Log.e(TAG, "Attempted to set events for mini view. Events only supported in full"
127                        + " view.");
128            }
129            return;
130        }
131        mEvents = events;
132        mFirstJulianDay = firstJulianDay;
133        mQueryDays = numDays;
134        // Create a new list, this is necessary since the weeks are referencing
135        // pieces of the old list
136        ArrayList<ArrayList<Event>> eventDayList = new ArrayList<ArrayList<Event>>();
137        for (int i = 0; i < numDays; i++) {
138            eventDayList.add(new ArrayList<Event>());
139        }
140
141        if (events == null || events.size() == 0) {
142            if(Log.isLoggable(TAG, Log.DEBUG)) {
143                Log.d(TAG, "No events. Returning early--go schedule something fun.");
144            }
145            mEventDayList = eventDayList;
146            refresh();
147            return;
148        }
149
150        // Compute the new set of days with events
151        for (Event event : events) {
152            int startDay = event.startDay - mFirstJulianDay;
153            int endDay = event.endDay - mFirstJulianDay + 1;
154            if (startDay < numDays || endDay >= 0) {
155                if (startDay < 0) {
156                    startDay = 0;
157                }
158                if (startDay > numDays) {
159                    continue;
160                }
161                if (endDay < 0) {
162                    continue;
163                }
164                if (endDay > numDays) {
165                    endDay = numDays;
166                }
167                for (int j = startDay; j < endDay; j++) {
168                    eventDayList.get(j).add(event);
169                }
170            }
171        }
172        if(Log.isLoggable(TAG, Log.DEBUG)) {
173            Log.d(TAG, "Processed " + events.size() + " events.");
174        }
175        mEventDayList = eventDayList;
176        refresh();
177    }
178
179    @SuppressWarnings("unchecked")
180    @Override
181    public View getView(int position, View convertView, ViewGroup parent) {
182        if (mIsMiniMonth) {
183            return super.getView(position, convertView, parent);
184        }
185        MonthWeekEventsView v;
186        LayoutParams params = new LayoutParams(
187                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
188        HashMap<String, Integer> drawingParams = null;
189        boolean isAnimatingToday = false;
190        if (convertView != null) {
191            v = (MonthWeekEventsView) convertView;
192            // Checking updateToday uses the current params instead of the new
193            // params, so this is assuming the view is relatively stable
194            if (mAnimateToday && v.updateToday(mSelectedDay.timezone)) {
195                long currentTime = System.currentTimeMillis();
196                // If it's been too long since we tried to start the animation
197                // don't show it. This can happen if the user stops a scroll
198                // before reaching today.
199                if (currentTime - mAnimateTime > ANIMATE_TODAY_TIMEOUT) {
200                    mAnimateToday = false;
201                    mAnimateTime = 0;
202                } else {
203                    isAnimatingToday = true;
204                    // There is a bug that causes invalidates to not work some
205                    // of the time unless we recreate the view.
206                    v = new MonthWeekEventsView(mContext);
207                }
208            } else {
209                drawingParams = (HashMap<String, Integer>) v.getTag();
210            }
211        } else {
212            v = new MonthWeekEventsView(mContext);
213        }
214        if (drawingParams == null) {
215            drawingParams = new HashMap<String, Integer>();
216        }
217        drawingParams.clear();
218
219        v.setLayoutParams(params);
220        v.setClickable(true);
221        v.setOnTouchListener(this);
222
223        int selectedDay = -1;
224        if (mSelectedWeek == position) {
225            selectedDay = mSelectedDay.weekDay;
226        }
227
228        drawingParams.put(SimpleWeekView.VIEW_PARAMS_HEIGHT,
229                (parent.getHeight() + parent.getTop()) / mNumWeeks);
230        drawingParams.put(SimpleWeekView.VIEW_PARAMS_SELECTED_DAY, selectedDay);
231        drawingParams.put(SimpleWeekView.VIEW_PARAMS_SHOW_WK_NUM, mShowWeekNumber ? 1 : 0);
232        drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK_START, mFirstDayOfWeek);
233        drawingParams.put(SimpleWeekView.VIEW_PARAMS_NUM_DAYS, mDaysPerWeek);
234        drawingParams.put(SimpleWeekView.VIEW_PARAMS_WEEK, position);
235        drawingParams.put(SimpleWeekView.VIEW_PARAMS_FOCUS_MONTH, mFocusMonth);
236        drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ORIENTATION, mOrientation);
237
238        if (isAnimatingToday) {
239            drawingParams.put(MonthWeekEventsView.VIEW_PARAMS_ANIMATE_TODAY, 1);
240            mAnimateToday = false;
241        }
242
243        v.setWeekParams(drawingParams, mSelectedDay.timezone);
244        sendEventsToView(v);
245        return v;
246    }
247
248    private void sendEventsToView(MonthWeekEventsView v) {
249        if (mEventDayList.size() == 0) {
250            if (Log.isLoggable(TAG, Log.DEBUG)) {
251                Log.d(TAG, "No events loaded, did not pass any events to view.");
252            }
253            v.setEvents(null, null);
254            return;
255        }
256        int viewJulianDay = v.getFirstJulianDay();
257        int start = viewJulianDay - mFirstJulianDay;
258        int end = start + v.mNumDays;
259        if (start < 0 || end > mEventDayList.size()) {
260            if (Log.isLoggable(TAG, Log.DEBUG)) {
261                Log.d(TAG, "Week is outside range of loaded events. viewStart: " + viewJulianDay
262                        + " eventsStart: " + mFirstJulianDay);
263            }
264            v.setEvents(null, null);
265            return;
266        }
267        v.setEvents(mEventDayList.subList(start, end), mEvents);
268    }
269
270    @Override
271    protected void refresh() {
272        mFirstDayOfWeek = Utils.getFirstDayOfWeek(mContext);
273        mShowWeekNumber = Utils.getShowWeekNumber(mContext);
274        mHomeTimeZone = Utils.getTimeZone(mContext, null);
275        mOrientation = mContext.getResources().getConfiguration().orientation;
276        updateTimeZones();
277        notifyDataSetChanged();
278    }
279
280    @Override
281    protected void onDayTapped(Time day) {
282        day.timezone = mHomeTimeZone;
283        Time currTime = new Time(mHomeTimeZone);
284        currTime.set(mController.getTime());
285        day.hour = currTime.hour;
286        day.minute = currTime.minute;
287        day.allDay = false;
288        day.normalize(true);
289         if (mShowAgendaWithMonth && !mIsMiniMonth) {
290            // If agenda view is visible with month view , refresh the views
291            // with the selected day's info
292            mController.sendEvent(mContext, EventType.GO_TO, day, day, -1,
293                    ViewType.CURRENT, CalendarController.EXTRA_GOTO_DATE, null, null);
294        } else {
295            // Else , switch to the detailed view
296            mController.sendEvent(mContext, EventType.GO_TO, day, day, -1,
297                    mIsMiniMonth ? ViewType.CURRENT : ViewType.DETAIL,
298                    CalendarController.EXTRA_GOTO_DATE
299                            | CalendarController.EXTRA_GOTO_BACK_TO_PREVIOUS, null, null);
300        }
301    }
302
303
304    @Override
305    public boolean onTouch(View v, MotionEvent event) {
306        if (!(v instanceof MonthWeekEventsView)) {
307            return super.onTouch(v, event);
308        }
309
310        int action = event.getAction();
311
312        // Event was tapped - switch to the detailed view making sure the click animation
313        // is done first.
314        if (mGestureDetector.onTouchEvent(event)) {
315            mSingleTapUpView = (MonthWeekEventsView) v;
316            long delay = System.currentTimeMillis() - mClickTime;
317            // Make sure the animation is visible for at least mOnTapDelay - mOnDownDelay ms
318            mListView.postDelayed(mDoSingleTapUp,
319                    delay > mTotalClickDelay ? 0 : mTotalClickDelay - delay);
320            return true;
321        } else {
322            // Animate a click - on down: show the selected day in the "clicked" color.
323            // On Up/scroll/move/cancel: hide the "clicked" color.
324            switch (action) {
325                case MotionEvent.ACTION_DOWN:
326                    mClickedView = (MonthWeekEventsView)v;
327                    mClickedXLocation = event.getX();
328                    mClickTime = System.currentTimeMillis();
329                    mListView.postDelayed(mDoClick, mOnDownDelay);
330                    break;
331                case MotionEvent.ACTION_UP:
332                case MotionEvent.ACTION_SCROLL:
333                case MotionEvent.ACTION_CANCEL:
334                    clearClickedView((MonthWeekEventsView)v);
335                    break;
336                case MotionEvent.ACTION_MOVE:
337                    // No need to cancel on vertical movement, ACTION_SCROLL will do that.
338                    if (Math.abs(event.getX() - mClickedXLocation) > mMovedPixelToCancel) {
339                        clearClickedView((MonthWeekEventsView)v);
340                    }
341                    break;
342                default:
343                    break;
344            }
345        }
346        // Do not tell the frameworks we consumed the touch action so that fling actions can be
347        // processed by the fragment.
348        return false;
349    }
350
351    /**
352     * This is here so we can identify events and process them
353     */
354    protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
355        @Override
356        public boolean onSingleTapUp(MotionEvent e) {
357            return true;
358        }
359    }
360
361    // Clear the visual cues of the click animation and related running code.
362    private void clearClickedView(MonthWeekEventsView v) {
363        mListView.removeCallbacks(mDoClick);
364        synchronized(v) {
365            v.clearClickedDay();
366        }
367        mClickedView = null;
368    }
369
370    // Perform the tap animation in a runnable to allow a delay before showing the tap color.
371    // This is done to prevent a click animation when a fling is done.
372    private final Runnable mDoClick = new Runnable() {
373        @Override
374        public void run() {
375            if (mClickedView != null) {
376                synchronized(mClickedView) {
377                    mClickedView.setClickedDay(mClickedXLocation);
378                }
379                mClickedView = null;
380                // This is a workaround , sometimes the top item on the listview doesn't refresh on
381                // invalidate, so this forces a re-draw.
382                mListView.invalidate();
383            }
384        }
385    };
386
387    // Performs the single tap operation: go to the tapped day.
388    // This is done in a runnable to allow the click animation to finish before switching views
389    private final Runnable mDoSingleTapUp = new Runnable() {
390        @Override
391        public void run() {
392            if (mSingleTapUpView != null) {
393                Time day = mSingleTapUpView.getDayFromLocation(mClickedXLocation);
394                if (Log.isLoggable(TAG, Log.DEBUG)) {
395                    Log.d(TAG, "Touched day at Row=" + mSingleTapUpView.mWeek + " day=" + day.toString());
396                }
397                if (day != null) {
398                    onDayTapped(day);
399                }
400                clearClickedView(mSingleTapUpView);
401                mSingleTapUpView = null;
402            }
403        }
404    };
405}
406