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