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