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 
17 package com.android.calendar.agenda;
18 
19 import com.android.calendar.CalendarController;
20 import com.android.calendar.CalendarController.EventType;
21 import com.android.calendar.DeleteEventHelper;
22 import com.android.calendar.R;
23 import com.android.calendar.Utils;
24 import com.android.calendar.agenda.AgendaAdapter.ViewHolder;
25 import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo;
26 import com.android.calendar.agenda.AgendaWindowAdapter.AgendaItem;
27 
28 import android.content.Context;
29 import android.graphics.Rect;
30 import android.os.Handler;
31 import android.provider.CalendarContract.Attendees;
32 import android.text.format.Time;
33 import android.util.AttributeSet;
34 import android.util.Log;
35 import android.view.View;
36 import android.widget.AdapterView;
37 import android.widget.AdapterView.OnItemClickListener;
38 import android.widget.ListView;
39 import android.widget.TextView;
40 
41 public 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 
AgendaListView(Context context, AttributeSet attrs)84     public AgendaListView(Context context, AttributeSet attrs) {
85         super(context, attrs);
86         initView(context);
87     }
88 
initView(Context context)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
setPastEventsUpdater()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
resetPastEventsUpdater()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.
updatePastEvents()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
onDetachedFromWindow()165     protected void onDetachedFromWindow() {
166         super.onDetachedFromWindow();
167         mWindowAdapter.close();
168     }
169 
170     // Implementation of the interface OnItemClickListener
171     @Override
onItemClick(AdapterView<?> a, View v, int position, long id)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                 if (item.allDay) {
196                     startTime = Utils.convertAlldayLocalToUTC(mTime, startTime, mTimeZone);
197                     endTime = Utils.convertAlldayLocalToUTC(mTime, endTime, mTimeZone);
198                 }
199                 mTime.set(startTime);
200                 CalendarController controller = CalendarController.getInstance(mContext);
201                 controller.sendEventRelatedEventWithExtra(this, EventType.VIEW_EVENT, item.id,
202                         startTime, endTime, 0, 0, CalendarController.EventInfo.buildViewExtraLong(
203                                 Attendees.ATTENDEE_STATUS_NONE, item.allDay), holderStartTime);
204             }
205         }
206     }
207 
goTo(Time time, long id, String searchQuery, boolean forced, boolean refreshEventInfo)208     public void goTo(Time time, long id, String searchQuery, boolean forced,
209             boolean refreshEventInfo) {
210         if (time == null) {
211             time = mTime;
212             long goToTime = getFirstVisibleTime(null);
213             if (goToTime <= 0) {
214                 goToTime = System.currentTimeMillis();
215             }
216             time.set(goToTime);
217         }
218         mTime.set(time);
219         mTime.switchTimezone(mTimeZone);
220         mTime.normalize(true);
221         if (DEBUG) {
222             Log.d(TAG, "Goto with time " + mTime.toString());
223         }
224         mWindowAdapter.refresh(mTime, id, searchQuery, forced, refreshEventInfo);
225     }
226 
refresh(boolean forced)227     public void refresh(boolean forced) {
228         mWindowAdapter.refresh(mTime, -1, null, forced, false);
229     }
230 
deleteSelectedEvent()231     public void deleteSelectedEvent() {
232         int position = getSelectedItemPosition();
233         AgendaItem agendaItem = mWindowAdapter.getAgendaItemByPosition(position);
234         if (agendaItem != null) {
235             mDeleteEventHelper.delete(agendaItem.begin, agendaItem.end, agendaItem.id, -1);
236         }
237     }
238 
getFirstVisibleView()239     public View getFirstVisibleView() {
240         Rect r = new Rect();
241         int childCount = getChildCount();
242         for (int i = 0; i < childCount; ++i) {
243             View listItem = getChildAt(i);
244             listItem.getLocalVisibleRect(r);
245             if (r.top >= 0) { // if visible
246                 return listItem;
247             }
248         }
249         return null;
250     }
251 
getSelectedTime()252     public long getSelectedTime() {
253         int position = getSelectedItemPosition();
254         if (position >= 0) {
255             AgendaItem item = mWindowAdapter.getAgendaItemByPosition(position);
256             if (item != null) {
257                 return item.begin;
258             }
259         }
260         return getFirstVisibleTime(null);
261     }
262 
getSelectedViewHolder()263     public AgendaAdapter.ViewHolder getSelectedViewHolder() {
264         return mWindowAdapter.getSelectedViewHolder();
265     }
266 
getFirstVisibleTime(AgendaItem item)267     public long getFirstVisibleTime(AgendaItem item) {
268         AgendaItem agendaItem = item;
269         if (item == null) {
270             agendaItem = getFirstVisibleAgendaItem();
271         }
272         if (agendaItem != null) {
273             Time t = new Time(mTimeZone);
274             t.set(agendaItem.begin);
275             // Save and restore the time since setJulianDay sets the time to 00:00:00
276             int hour = t.hour;
277             int minute = t.minute;
278             int second = t.second;
279             t.setJulianDay(agendaItem.startDay);
280             t.hour = hour;
281             t.minute = minute;
282             t.second = second;
283             if (DEBUG) {
284                 t.normalize(true);
285                 Log.d(TAG, "first position had time " + t.toString());
286             }
287             return t.normalize(false);
288         }
289         return 0;
290     }
291 
getFirstVisibleAgendaItem()292     public AgendaItem getFirstVisibleAgendaItem() {
293         int position = getFirstVisiblePosition();
294         if (DEBUG) {
295             Log.v(TAG, "getFirstVisiblePosition = " + position);
296         }
297 
298         // mShowEventDetailsWithAgenda == true implies we have a sticky header. In that case
299         // we may need to take the second visible position, since the first one maybe the one
300         // under the sticky header.
301         if (mShowEventDetailsWithAgenda) {
302             View v = getFirstVisibleView ();
303             if (v != null) {
304                 Rect r = new Rect ();
305                 v.getLocalVisibleRect(r);
306                 if (r.bottom - r.top <=  mWindowAdapter.getStickyHeaderHeight()) {
307                     position ++;
308                 }
309             }
310         }
311 
312         return mWindowAdapter.getAgendaItemByPosition(position,
313                 false /* startDay = date separator date instead of actual event startday */);
314 
315     }
316 
getJulianDayFromPosition(int position)317     public int getJulianDayFromPosition(int position) {
318         DayAdapterInfo info = mWindowAdapter.getAdapterInfoByPosition(position);
319         if (info != null) {
320             return info.dayAdapter.findJulianDayFromPosition(position - info.offset);
321         }
322         return 0;
323     }
324 
325     // Finds is a specific event (defined by start time and id) is visible
isAgendaItemVisible(Time startTime, long id)326     public boolean isAgendaItemVisible(Time startTime, long id) {
327 
328         if (id == -1 || startTime == null) {
329             return false;
330         }
331 
332         View child = getChildAt(0);
333         // View not set yet, so not child - return
334         if (child == null) {
335             return false;
336         }
337         int start = getPositionForView(child);
338         long milliTime = startTime.toMillis(true);
339         int childCount = getChildCount();
340         int eventsInAdapter = mWindowAdapter.getCount();
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             if (agendaItem.id == id && agendaItem.begin == milliTime) {
351                 View listItem = getChildAt(i);
352                 if (listItem.getTop() <= getHeight() &&
353                         listItem.getTop() >= mWindowAdapter.getStickyHeaderHeight()) {
354                     return true;
355                 }
356             }
357         }
358         return false;
359     }
360 
getSelectedInstanceId()361     public long getSelectedInstanceId() {
362         return mWindowAdapter.getSelectedInstanceId();
363     }
364 
setSelectedInstanceId(long id)365     public void setSelectedInstanceId(long id) {
366         mWindowAdapter.setSelectedInstanceId(id);
367     }
368 
369     // Move the currently selected or visible focus down by offset amount.
370     // offset could be negative.
shiftSelection(int offset)371     public void shiftSelection(int offset) {
372         shiftPosition(offset);
373         int position = getSelectedItemPosition();
374         if (position != INVALID_POSITION) {
375             setSelectionFromTop(position + offset, 0);
376         }
377     }
378 
shiftPosition(int offset)379     private void shiftPosition(int offset) {
380         if (DEBUG) {
381             Log.v(TAG, "Shifting position " + offset);
382         }
383 
384         View firstVisibleItem = getFirstVisibleView();
385 
386         if (firstVisibleItem != null) {
387             Rect r = new Rect();
388             firstVisibleItem.getLocalVisibleRect(r);
389             // if r.top is < 0, getChildAt(0) and getFirstVisiblePosition() is
390             // returning an item above the first visible item.
391             int position = getPositionForView(firstVisibleItem);
392             setSelectionFromTop(position + offset, r.top > 0 ? -r.top : r.top);
393             if (DEBUG) {
394                 if (firstVisibleItem.getTag() instanceof AgendaAdapter.ViewHolder) {
395                     ViewHolder viewHolder = (AgendaAdapter.ViewHolder) firstVisibleItem.getTag();
396                     Log.v(TAG, "Shifting from " + position + " by " + offset + ". Title "
397                             + viewHolder.title.getText());
398                 } else if (firstVisibleItem.getTag() instanceof AgendaByDayAdapter.ViewHolder) {
399                     AgendaByDayAdapter.ViewHolder viewHolder =
400                             (AgendaByDayAdapter.ViewHolder) firstVisibleItem.getTag();
401                     Log.v(TAG, "Shifting from " + position + " by " + offset + ". Date  "
402                             + viewHolder.dateView.getText());
403                 } else if (firstVisibleItem instanceof TextView) {
404                     Log.v(TAG, "Shifting: Looking at header here. " + getSelectedItemPosition());
405                 }
406             }
407         } else if (getSelectedItemPosition() >= 0) {
408             if (DEBUG) {
409                 Log.v(TAG, "Shifting selection from " + getSelectedItemPosition() +
410                         " by " + offset);
411             }
412             setSelection(getSelectedItemPosition() + offset);
413         }
414     }
415 
setHideDeclinedEvents(boolean hideDeclined)416     public void setHideDeclinedEvents(boolean hideDeclined) {
417         mWindowAdapter.setHideDeclinedEvents(hideDeclined);
418     }
419 
onResume()420     public void onResume() {
421         mTZUpdater.run();
422         Utils.setMidnightUpdater(mHandler, mMidnightUpdater, mTimeZone);
423         setPastEventsUpdater();
424         mWindowAdapter.onResume();
425     }
426 
onPause()427     public void onPause() {
428         Utils.resetMidnightUpdater(mHandler, mMidnightUpdater);
429         resetPastEventsUpdater();
430     }
431 }
432