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