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