AgendaListView.java revision cf31ab351c9becad6785d4d77eefc934f30b92e3
1/*
2 * Copyright (C) 2009 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.agenda;
18
19import com.android.calendar.CalendarController;
20import com.android.calendar.CalendarController.EventType;
21import com.android.calendar.DeleteEventHelper;
22import com.android.calendar.R;
23import com.android.calendar.Utils;
24import com.android.calendar.agenda.AgendaAdapter.ViewHolder;
25import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo;
26import com.android.calendar.agenda.AgendaWindowAdapter.AgendaItem;
27
28import android.content.Context;
29import android.graphics.Rect;
30import android.os.Handler;
31import android.provider.CalendarContract.Attendees;
32import android.text.format.Time;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.View;
36import android.widget.AdapterView;
37import android.widget.AdapterView.OnItemClickListener;
38import android.widget.ListView;
39import android.widget.TextView;
40
41public class AgendaListView extends ListView implements OnItemClickListener {
42
43    private static final String TAG = "AgendaListView";
44    private static final boolean DEBUG = false;
45    private static final int EVENT_UPDATE_TIME = 300000;  // 5 minutes
46
47    private AgendaWindowAdapter mWindowAdapter;
48    private DeleteEventHelper mDeleteEventHelper;
49    private Context mContext;
50    private String mTimeZone;
51    private Time mTime;
52    private boolean mShowEventDetailsWithAgenda;
53    private Handler mHandler = null;
54
55    private final Runnable mTZUpdater = new Runnable() {
56        @Override
57        public void run() {
58            mTimeZone = Utils.getTimeZone(mContext, this);
59            mTime.switchTimezone(mTimeZone);
60        }
61    };
62
63    // runs every midnight and refreshes the view in order to update the past/present
64    // separator
65    private final Runnable mMidnightUpdater = new Runnable() {
66        @Override
67        public void run() {
68            refresh(true);
69            Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone);
70        }
71    };
72
73    // Runs every EVENT_UPDATE_TIME to gray out past events
74    private final Runnable mPastEventUpdater = new Runnable() {
75        @Override
76        public void run() {
77            if (updatePastEvents() == true) {
78                refresh(true);
79            }
80            setPastEventsUpdater();
81        }
82    };
83
84    public AgendaListView(Context context, AttributeSet attrs) {
85        super(context, attrs);
86        initView(context);
87    }
88
89    private void initView(Context context) {
90        mContext = context;
91        mTimeZone = Utils.getTimeZone(context, mTZUpdater);
92        mTime = new Time(mTimeZone);
93        setOnItemClickListener(this);
94        setVerticalScrollBarEnabled(false);
95        mWindowAdapter = new AgendaWindowAdapter(context, this,
96                Utils.getConfigBool(context, R.bool.show_event_details_with_agenda));
97        mWindowAdapter.setSelectedInstanceId(-1/* TODO:instanceId */);
98        setAdapter(mWindowAdapter);
99        setCacheColorHint(context.getResources().getColor(R.color.agenda_item_not_selected));
100        mDeleteEventHelper =
101                new DeleteEventHelper(context, null, false /* don't exit when done */);
102        mShowEventDetailsWithAgenda = Utils.getConfigBool(mContext,
103                R.bool.show_event_details_with_agenda);
104        // Hide ListView dividers, they are done in the item views themselves
105        setDivider(null);
106        setDividerHeight(0);
107
108        mHandler = new Handler();
109    }
110
111    // Sets a thread to run every EVENT_UPDATE_TIME in order to update the list
112    // with grayed out past events
113    private void setPastEventsUpdater() {
114
115        // Run the thread in the nearest rounded EVENT_UPDATE_TIME
116        long now = System.currentTimeMillis();
117        long roundedTime = (now / EVENT_UPDATE_TIME) * EVENT_UPDATE_TIME;
118        mHandler.removeCallbacks(mPastEventUpdater);
119        mHandler.postDelayed(mPastEventUpdater, EVENT_UPDATE_TIME - (now - roundedTime));
120    }
121
122    // Stop the past events thread
123    private void resetPastEventsUpdater() {
124        mHandler.removeCallbacks(mPastEventUpdater);
125    }
126
127    // Go over all visible views and checks if all past events are grayed out.
128    // Returns true is there is at least one event that ended and it is not
129    // grayed out.
130    private boolean updatePastEvents() {
131
132        int childCount = getChildCount();
133        boolean needUpdate = false;
134        long now = System.currentTimeMillis();
135        Time time = new Time(mTimeZone);
136        time.set(now);
137        int todayJulianDay = Time.getJulianDay(now, time.gmtoff);
138
139        // Go over views in list
140        for (int i = 0; i < childCount; ++i) {
141            View listItem = getChildAt(i);
142            Object o = listItem.getTag();
143            if (o instanceof AgendaByDayAdapter.ViewHolder) {
144                // day view - check if day in the past and not grayed yet
145                AgendaByDayAdapter.ViewHolder holder = (AgendaByDayAdapter.ViewHolder) o;
146                if (holder.julianDay <= todayJulianDay && !holder.grayed) {
147                    needUpdate = true;
148                    break;
149                }
150            } else if (o instanceof AgendaAdapter.ViewHolder) {
151                // meeting view - check if event in the past or started already and not grayed yet
152                // All day meetings for a day are grayed out
153                AgendaAdapter.ViewHolder holder = (AgendaAdapter.ViewHolder) o;
154                if (!holder.grayed && ((!holder.allDay && holder.startTimeMilli <= now) ||
155                        (holder.allDay && holder.julianDay <= todayJulianDay))) {
156                    needUpdate = true;
157                    break;
158                }
159            }
160        }
161        return needUpdate;
162    }
163
164    @Override
165    protected void onDetachedFromWindow() {
166        super.onDetachedFromWindow();
167        mWindowAdapter.close();
168    }
169
170    // Implementation of the interface OnItemClickListener
171    @Override
172    public void onItemClick(AdapterView<?> a, View v, int position, long id) {
173        if (id != -1) {
174            // Switch to the EventInfo view
175            AgendaItem item = mWindowAdapter.getAgendaItemByPosition(position);
176            long oldInstanceId = mWindowAdapter.getSelectedInstanceId();
177            mWindowAdapter.setSelectedView(v);
178
179            // If events are shown to the side of the agenda list , do nothing
180            // when the same event is selected , otherwise show the selected event.
181
182            if (item != null && (oldInstanceId != mWindowAdapter.getSelectedInstanceId() ||
183                    !mShowEventDetailsWithAgenda)) {
184                long startTime = item.begin;
185                long endTime = item.end;
186                // Holder in view holds the start of the specific part of a multi-day event ,
187                // use it for the goto
188                long holderStartTime;
189                Object holder = v.getTag();
190                if (holder instanceof AgendaAdapter.ViewHolder) {
191                    holderStartTime = ((AgendaAdapter.ViewHolder) holder).startTimeMilli;
192                } else {
193                    holderStartTime = startTime;
194                }
195                mTime.set(startTime);
196                CalendarController controller = CalendarController.getInstance(mContext);
197                controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id,
198                        startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong(
199                                Attendees.ATTENDEE_STATUS_NONE, item.allDay), holderStartTime);
200            }
201        }
202    }
203
204    public void goTo(Time time, long id, String searchQuery, boolean forced,
205            boolean refreshEventInfo) {
206        if (time == null) {
207            time = mTime;
208            long goToTime = getFirstVisibleTime(null);
209            if (goToTime <= 0) {
210                goToTime = System.currentTimeMillis();
211            }
212            time.set(goToTime);
213        }
214        mTime.set(time);
215        mTime.switchTimezone(mTimeZone);
216        mTime.normalize(true);
217        if (DEBUG) {
218            Log.d(TAG, "Goto with time " + mTime.toString());
219        }
220        mWindowAdapter.refresh(mTime, id, searchQuery, forced, refreshEventInfo);
221    }
222
223    public void refresh(boolean forced) {
224        mWindowAdapter.refresh(mTime, -1, null, forced, false);
225    }
226
227    public void deleteSelectedAgendaItem() {
228        int position = getSelectedItemPosition();
229        AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(position);
230        if (agendaItem != null) {
231            mDeleteEventHelper.delete(agendaItem.begin, agendaItem.end, agendaItem.id, -1);
232        }
233    }
234
235    public View getFirstVisibleView() {
236        Rect r = new Rect();
237        int childCount = getChildCount();
238        for (int i = 0; i < childCount; ++i) {
239            View listItem = getChildAt(i);
240            listItem.getLocalVisibleRect(r);
241            if (r.top >= 0) { // if visible
242                return listItem;
243            }
244        }
245        return null;
246    }
247
248    public long getSelectedTime() {
249        int position = getSelectedItemPosition();
250        if (position >= 0) {
251            AgendaItem item = mWindowAdapter.getAgendaItemByPosition(position);
252            if (item != null) {
253                return item.begin;
254            }
255        }
256        return getFirstVisibleTime(null);
257    }
258
259    public AgendaAdapter.ViewHolder getSelectedViewHolder() {
260        return mWindowAdapter.getSelectedViewHolder();
261    }
262
263    public long getFirstVisibleTime(AgendaItem item) {
264        AgendaItem agendaItem = item;
265        if (item == null) {
266            agendaItem = getFirstVisibleAgendaItem();
267        }
268        if (agendaItem != null) {
269            Time t = new Time(mTimeZone);
270            t.set(agendaItem.begin);
271            // Save and restore the time since setJulianDay sets the time to 00:00:00
272            int hour = t.hour;
273            int minute = t.minute;
274            int second = t.second;
275            t.setJulianDay(agendaItem.startDay);
276            t.hour = hour;
277            t.minute = minute;
278            t.second = second;
279            if (DEBUG) {
280                t.normalize(true);
281                Log.d(TAG, "first position had time " + t.toString());
282            }
283            return t.normalize(false);
284        }
285        return 0;
286    }
287
288    public AgendaItem getFirstVisibleAgendaItem() {
289        int position = getFirstVisiblePosition();
290        if (DEBUG) {
291            Log.v(TAG, "getFirstVisiblePosition = " + position);
292        }
293
294        // mShowEventDetailsWithAgenda == true implies we have a sticky header. In that case
295        // we may need to take the second visible position, since the first one maybe the one
296        // under the sticky header.
297        if (mShowEventDetailsWithAgenda) {
298            View v = getFirstVisibleView ();
299            if (v != null) {
300                Rect r = new Rect ();
301                v.getLocalVisibleRect(r);
302                if (r.bottom - r.top <=  mWindowAdapter.getStickyHeaderHeight()) {
303                    position ++;
304                }
305            }
306        }
307
308        return mWindowAdapter.getAgendaItemByPosition(position,
309                false /* startDay = date separator date instead of actual event startday */);
310
311    }
312
313    public int getJulianDayFromPosition(int position) {
314        DayAdapterInfo info = mWindowAdapter.getAdapterInfoByPosition(position);
315        if (info != null) {
316            return info.dayAdapter.findJulianDayFromPosition(position - info.offset);
317        }
318        return 0;
319    }
320
321    // Finds is a specific event (defined by start time and id) is visible
322    public boolean isAgendaItemVisible(Time startTime, long id) {
323
324        if (id == -1 || startTime == null) {
325            return false;
326        }
327
328        View child = getChildAt(0);
329        // View not set yet, so not child - return
330        if (child == null) {
331            return false;
332        }
333
334        int start = getPositionForView(child);
335        long milliTime = startTime.toMillis(true);
336        int childCount = getChildCount();
337        int eventsInAdapter = mWindowAdapter.getCount();
338
339        if (DEBUG) {
340            Log.d(TAG,"id: " + id + ", milliTime (after): " + milliTime);
341        }
342        for (int i = 0; i < childCount; i++) {
343            if (i + start >= eventsInAdapter) {
344                break;
345            }
346            AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(i + start);
347            if (agendaItem == null) {
348                continue;
349            }
350
351            // If all-day event, it will be in UTC, so convert it to local time for comparison.
352            long begin = agendaItem.begin;
353            if (agendaItem.allDay) {
354                begin = Utils.convertAlldayUtcToLocal(null, begin, mTimeZone);
355            }
356
357            if (DEBUG) {
358                Log.d(TAG,"   id: " + agendaItem.id + ", startTime: " + agendaItem.begin);
359            }
360            if (agendaItem.id == id && begin == milliTime) {
361                View listItem = getChildAt(i);
362                if (listItem.getTop() <= getHeight() &&
363                        listItem.getTop() >= mWindowAdapter.getStickyHeaderHeight()) {
364                    if (DEBUG) {
365                        Log.d(TAG, "MATCH");
366                    }
367                    return true;
368                }
369            }
370        }
371        if (DEBUG) {
372            Log.d(TAG, "NO MATCH");
373        }
374        return false;
375    }
376
377    public long getSelectedInstanceId() {
378        return mWindowAdapter.getSelectedInstanceId();
379    }
380
381    public void setSelectedInstanceId(long id) {
382        mWindowAdapter.setSelectedInstanceId(id);
383    }
384
385    // Move the currently selected or visible focus down by offset amount.
386    // offset could be negative.
387    public void shiftSelection(int offset) {
388        shiftPosition(offset);
389        int position = getSelectedItemPosition();
390        if (position != INVALID_POSITION) {
391            setSelectionFromTop(position + offset, 0);
392        }
393    }
394
395    private void shiftPosition(int offset) {
396        if (DEBUG) {
397            Log.v(TAG, "Shifting position " + offset);
398        }
399
400        View firstVisibleItem = getFirstVisibleView();
401
402        if (firstVisibleItem != null) {
403            Rect r = new Rect();
404            firstVisibleItem.getLocalVisibleRect(r);
405            // if r.top is < 0, getChildAt(0) and getFirstVisiblePosition() is
406            // returning an item above the first visible item.
407            int position = getPositionForView(firstVisibleItem);
408            setSelectionFromTop(position + offset, r.top > 0 ? -r.top : r.top);
409            if (DEBUG) {
410                if (firstVisibleItem.getTag() instanceof AgendaAdapter.ViewHolder) {
411                    ViewHolder viewHolder = (AgendaAdapter.ViewHolder) firstVisibleItem.getTag();
412                    Log.v(TAG, "Shifting from " + position + " by " + offset + ". Title "
413                            + viewHolder.title.getText());
414                } else if (firstVisibleItem.getTag() instanceof AgendaByDayAdapter.ViewHolder) {
415                    AgendaByDayAdapter.ViewHolder viewHolder =
416                            (AgendaByDayAdapter.ViewHolder) firstVisibleItem.getTag();
417                    Log.v(TAG, "Shifting from " + position + " by " + offset + ". Date  "
418                            + viewHolder.dateView.getText());
419                } else if (firstVisibleItem instanceof TextView) {
420                    Log.v(TAG, "Shifting: Looking at header here. " + getSelectedItemPosition());
421                }
422            }
423        } else if (getSelectedItemPosition() >= 0) {
424            if (DEBUG) {
425                Log.v(TAG, "Shifting selection from " + getSelectedItemPosition() +
426                        " by " + offset);
427            }
428            setSelection(getSelectedItemPosition() + offset);
429        }
430    }
431
432    public void setHideDeclinedEvents(boolean hideDeclined) {
433        mWindowAdapter.setHideDeclinedEvents(hideDeclined);
434    }
435
436    public void onResume() {
437        mTZUpdater.run();
438        Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone);
439        setPastEventsUpdater();
440        mWindowAdapter.onResume();
441    }
442
443    public void onPause() {
444        Utils.resetMidnightUpdater(mHandler, mMidnightUpdater);
445        resetPastEventsUpdater();
446    }
447}
448