1/*
2 * Copyright (C) 2008 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.R;
20import com.android.calendar.Utils;
21import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo;
22
23import android.content.Context;
24import android.database.Cursor;
25import android.graphics.Typeface;
26import android.text.TextUtils;
27import android.text.format.DateUtils;
28import android.text.format.Time;
29import android.util.Log;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.BaseAdapter;
34import android.widget.TextView;
35
36import java.util.ArrayList;
37import java.util.Formatter;
38import java.util.Iterator;
39import java.util.LinkedList;
40import java.util.Locale;
41
42public class AgendaByDayAdapter extends BaseAdapter {
43    private static final int TYPE_DAY = 0;
44    private static final int TYPE_MEETING = 1;
45    static final int TYPE_LAST = 2;
46
47    private final Context mContext;
48    private final AgendaAdapter mAgendaAdapter;
49    private final LayoutInflater mInflater;
50    private ArrayList<RowInfo> mRowInfo;
51    private int mTodayJulianDay;
52    private Time mTmpTime;
53    private String mTimeZone;
54    // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
55    private Formatter mFormatter;
56    private StringBuilder mStringBuilder;
57
58    static class ViewHolder {
59        TextView dayView;
60        TextView dateView;
61        int julianDay;
62        boolean grayed;
63    }
64
65    private Runnable mTZUpdater = new Runnable() {
66        @Override
67        public void run() {
68            mTimeZone = Utils.getTimeZone(mContext, this);
69            mTmpTime = new Time(mTimeZone);
70            notifyDataSetChanged();
71        }
72    };
73
74    public AgendaByDayAdapter(Context context) {
75        mContext = context;
76        mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item);
77        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
78        mStringBuilder = new StringBuilder(50);
79        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
80        mTimeZone = Utils.getTimeZone(context, mTZUpdater);
81        mTmpTime = new Time(mTimeZone);
82    }
83
84    public long getInstanceId(int position) {
85        if (mRowInfo == null || position >= mRowInfo.size()) {
86            return -1;
87        }
88        return mRowInfo.get(position).mInstanceId;
89    }
90
91    // Returns the position of a header of a specific item
92    public int getHeaderPosition(int position) {
93        if (mRowInfo == null || position >= mRowInfo.size()) {
94            return -1;
95        }
96
97        for (int i = position; i >=0; i --) {
98            RowInfo row = mRowInfo.get(i);
99            if (row != null && row.mType == TYPE_DAY)
100                return i;
101        }
102        return -1;
103    }
104
105    // Returns the number of items in a section defined by a specific header location
106    public int getHeaderItemsCount(int position) {
107        if (mRowInfo == null) {
108            return -1;
109        }
110        int count = 0;
111        for (int i = position +1; i < mRowInfo.size(); i++) {
112            if (mRowInfo.get(i).mType != TYPE_MEETING) {
113                return count;
114            }
115            count ++;
116        }
117        return count;
118    }
119
120    public int getCount() {
121        if (mRowInfo != null) {
122            return mRowInfo.size();
123        }
124        return mAgendaAdapter.getCount();
125    }
126
127    public Object getItem(int position) {
128        if (mRowInfo != null) {
129            RowInfo row = mRowInfo.get(position);
130            if (row.mType == TYPE_DAY) {
131                return row;
132            } else {
133                return mAgendaAdapter.getItem(row.mPosition);
134            }
135        }
136        return mAgendaAdapter.getItem(position);
137    }
138
139    public long getItemId(int position) {
140        if (mRowInfo != null) {
141            RowInfo row = mRowInfo.get(position);
142            if (row.mType == TYPE_DAY) {
143                return -position;
144            } else {
145                return mAgendaAdapter.getItemId(row.mPosition);
146            }
147        }
148        return mAgendaAdapter.getItemId(position);
149    }
150
151    @Override
152    public int getViewTypeCount() {
153        return TYPE_LAST;
154    }
155
156    @Override
157    public int getItemViewType(int position) {
158        return mRowInfo != null && mRowInfo.size() > position ?
159                mRowInfo.get(position).mType : TYPE_DAY;
160    }
161
162    public boolean isDayHeaderView(int position) {
163        return (getItemViewType(position) == TYPE_DAY);
164    }
165
166    public View getView(int position, View convertView, ViewGroup parent) {
167        if ((mRowInfo == null) || (position > mRowInfo.size())) {
168            // If we have no row info, mAgendaAdapter returns the view.
169            return mAgendaAdapter.getView(position, convertView, parent);
170        }
171
172        RowInfo row = mRowInfo.get(position);
173        if (row.mType == TYPE_DAY) {
174            ViewHolder holder = null;
175            View agendaDayView = null;
176            if ((convertView != null) && (convertView.getTag() != null)) {
177                // Listview may get confused and pass in a different type of
178                // view since we keep shifting data around. Not a big problem.
179                Object tag = convertView.getTag();
180                if (tag instanceof ViewHolder) {
181                    agendaDayView = convertView;
182                    holder = (ViewHolder) tag;
183                    holder.julianDay = row.mDay;
184                }
185            }
186
187            if (holder == null) {
188                // Create a new AgendaView with a ViewHolder for fast access to
189                // views w/o calling findViewById()
190                holder = new ViewHolder();
191                agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false);
192                holder.dayView = (TextView) agendaDayView.findViewById(R.id.day);
193                holder.dateView = (TextView) agendaDayView.findViewById(R.id.date);
194                holder.julianDay = row.mDay;
195                holder.grayed = false;
196                agendaDayView.setTag(holder);
197            }
198
199            // Re-use the member variable "mTime" which is set to the local
200            // time zone.
201            // It's difficult to find and update all these adapters when the
202            // home tz changes so check it here and update if needed.
203            String tz = Utils.getTimeZone(mContext, mTZUpdater);
204            if (!TextUtils.equals(tz, mTmpTime.timezone)) {
205                mTimeZone = tz;
206                mTmpTime = new Time(tz);
207            }
208
209            // Build the text for the day of the week.
210            // Should be yesterday/today/tomorrow (if applicable) + day of the week
211
212            Time date = mTmpTime;
213            long millis = date.setJulianDay(row.mDay);
214            int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
215            mStringBuilder.setLength(0);
216
217            String dayViewText = Utils.getDayOfWeekString(row.mDay, mTodayJulianDay, millis,
218                    mContext);
219
220            // Build text for the date
221            // Format should be month day
222
223            mStringBuilder.setLength(0);
224            flags = DateUtils.FORMAT_SHOW_DATE;
225            String dateViewText = DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
226                    flags, mTimeZone).toString();
227
228            if (AgendaWindowAdapter.BASICLOG) {
229                dayViewText += " P:" + position;
230                dateViewText += " P:" + position;
231            }
232            holder.dayView.setText(dayViewText);
233            holder.dateView.setText(dateViewText);
234
235            // Set the background of the view, it is grayed for day that are in the past and today
236            if (row.mDay > mTodayJulianDay) {
237                agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_primary);
238                holder.grayed = false;
239            } else {
240                agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_secondary);
241                holder.grayed = true;
242            }
243            return agendaDayView;
244        } else if (row.mType == TYPE_MEETING) {
245            View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);
246            AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag());
247            TextView title = holder.title;
248            long eventStartTime = holder.startTimeMilli;
249            boolean allDay = holder.allDay;
250            if (AgendaWindowAdapter.BASICLOG) {
251                title.setText(title.getText() + " P:" + position);
252            } else {
253                title.setText(title.getText());
254            }
255
256            // if event in the past or started already, un-bold the title and set the background
257            if ((!allDay && eventStartTime <= System.currentTimeMillis()) ||
258                    (allDay && row.mDay <= mTodayJulianDay)) {
259                itemView.setBackgroundResource(R.drawable.agenda_item_bg_secondary);
260                title.setTypeface(Typeface.DEFAULT);
261                holder.grayed = true;
262            } else {
263                itemView.setBackgroundResource(R.drawable.agenda_item_bg_primary);
264                title.setTypeface(Typeface.DEFAULT_BOLD);
265                holder.grayed = false;
266            }
267            holder.julianDay = row.mDay;
268            return itemView;
269        } else {
270            // Error
271            throw new IllegalStateException("Unknown event type:" + row.mType);
272        }
273    }
274
275    public void clearDayHeaderInfo() {
276        mRowInfo = null;
277    }
278
279    public void changeCursor(DayAdapterInfo info) {
280        calculateDays(info);
281        mAgendaAdapter.changeCursor(info.cursor);
282    }
283
284    public void calculateDays(DayAdapterInfo dayAdapterInfo) {
285        Cursor cursor = dayAdapterInfo.cursor;
286        ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>();
287        int prevStartDay = -1;
288
289        Time tempTime = new Time(mTimeZone);
290        long now = System.currentTimeMillis();
291        tempTime.set(now);
292        mTodayJulianDay = Time.getJulianDay(now, tempTime.gmtoff);
293
294        LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>();
295        for (int position = 0; cursor.moveToNext(); position++) {
296            int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
297            long id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
298            long startTime =  cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
299            long endTime =  cursor.getLong(AgendaWindowAdapter.INDEX_END);
300            long instanceId = cursor.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID);
301            boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
302            if (allDay) {
303                startTime = Utils.convertAlldayUtcToLocal(tempTime, startTime, mTimeZone);
304                endTime = Utils.convertAlldayUtcToLocal(tempTime, endTime, mTimeZone);
305            }
306            // Skip over the days outside of the adapter's range
307            startDay = Math.max(startDay, dayAdapterInfo.start);
308            // Make sure event's start time is not before the start of the day
309            // (setJulianDay sets the time to 12:00am)
310            long adapterStartTime = tempTime.setJulianDay(startDay);
311            startTime = Math.max(startTime, adapterStartTime);
312
313            if (startDay != prevStartDay) {
314                // Check if we skipped over any empty days
315                if (prevStartDay == -1) {
316                    rowInfo.add(new RowInfo(TYPE_DAY, startDay));
317                } else {
318                    // If there are any multiple-day events that span the empty
319                    // range of days, then create day headers and events for
320                    // those multiple-day events.
321                    boolean dayHeaderAdded = false;
322                    for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) {
323                        dayHeaderAdded = false;
324                        Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
325                        while (iter.hasNext()) {
326                            MultipleDayInfo info = iter.next();
327                            // If this event has ended then remove it from the
328                            // list.
329                            if (info.mEndDay < currentDay) {
330                                iter.remove();
331                                continue;
332                            }
333
334                            // If this is the first event for the day, then
335                            // insert a day header.
336                            if (!dayHeaderAdded) {
337                                rowInfo.add(new RowInfo(TYPE_DAY, currentDay));
338                                dayHeaderAdded = true;
339                            }
340                            long nextMidnight = Utils.getNextMidnight(tempTime,
341                                    info.mEventStartTimeMilli, mTimeZone);
342
343                            long infoEndTime = (info.mEndDay == currentDay) ?
344                                    info.mEventEndTimeMilli : nextMidnight;
345                            rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition,
346                                    info.mEventId, info.mEventStartTimeMilli,
347                                    infoEndTime, info.mInstanceId, info.mAllDay));
348
349                            info.mEventStartTimeMilli = nextMidnight;
350                        }
351                    }
352
353                    // If the day header was not added for the start day, then
354                    // add it now.
355                    if (!dayHeaderAdded) {
356                        rowInfo.add(new RowInfo(TYPE_DAY, startDay));
357                    }
358                }
359                prevStartDay = startDay;
360            }
361
362            // Add in the event for this cursor position
363            rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, endTime,
364                    instanceId, allDay));
365
366            // If this event spans multiple days, then add it to the multipleDay
367            // list.
368            int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY);
369
370            // Skip over the days outside of the adapter's range
371            endDay = Math.min(endDay, dayAdapterInfo.end);
372            if (endDay > startDay) {
373                multipleDayList.add(new MultipleDayInfo(position, endDay, id,
374                        Utils.getNextMidnight(tempTime, startTime, mTimeZone),
375                        endTime, instanceId, allDay));
376            }
377        }
378
379        // There are no more cursor events but we might still have multiple-day
380        // events left.  So create day headers and events for those.
381        if (prevStartDay > 0) {
382            for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end;
383                    currentDay++) {
384                boolean dayHeaderAdded = false;
385                Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
386                while (iter.hasNext()) {
387                    MultipleDayInfo info = iter.next();
388                    // If this event has ended then remove it from the
389                    // list.
390                    if (info.mEndDay < currentDay) {
391                        iter.remove();
392                        continue;
393                    }
394
395                    // If this is the first event for the day, then
396                    // insert a day header.
397                    if (!dayHeaderAdded) {
398                        rowInfo.add(new RowInfo(TYPE_DAY, currentDay));
399                        dayHeaderAdded = true;
400                    }
401                    long nextMidnight = Utils.getNextMidnight(tempTime, info.mEventStartTimeMilli,
402                            mTimeZone);
403                    long infoEndTime =
404                            (info.mEndDay == currentDay) ? info.mEventEndTimeMilli : nextMidnight;
405                    rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition,
406                            info.mEventId, info.mEventStartTimeMilli, infoEndTime,
407                            info.mInstanceId, info.mAllDay));
408
409                    info.mEventStartTimeMilli = nextMidnight;
410                }
411            }
412        }
413        mRowInfo = rowInfo;
414    }
415
416    private static class RowInfo {
417        // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING)
418        final int mType;
419
420        final int mDay;          // Julian day
421        final int mPosition;     // cursor position (not used for TYPE_DAY)
422        // This is used to mark a day header as the first day with events that is "today"
423        // or later. This flag is used by the adapter to create a view with a visual separator
424        // between the past and the present/future
425        boolean mFirstDayAfterYesterday;
426        final long mEventId;
427        final long mEventStartTimeMilli;
428        final long mEventEndTimeMilli;
429        final long mInstanceId;
430        final boolean mAllDay;
431
432        RowInfo(int type, int julianDay, int position, long id, long startTime, long endTime,
433                long instanceId, boolean allDay) {
434            mType = type;
435            mDay = julianDay;
436            mPosition = position;
437            mEventId = id;
438            mEventStartTimeMilli = startTime;
439            mEventEndTimeMilli = endTime;
440            mFirstDayAfterYesterday = false;
441            mInstanceId = instanceId;
442            mAllDay = allDay;
443        }
444
445        RowInfo(int type, int julianDay) {
446            mType = type;
447            mDay = julianDay;
448            mPosition = 0;
449            mEventId = 0;
450            mEventStartTimeMilli = 0;
451            mEventEndTimeMilli = 0;
452            mFirstDayAfterYesterday = false;
453            mInstanceId = -1;
454            mAllDay = false;
455        }
456    }
457
458    private static class MultipleDayInfo {
459        final int mPosition;
460        final int mEndDay;
461        final long mEventId;
462        long mEventStartTimeMilli;
463        long mEventEndTimeMilli;
464        final long mInstanceId;
465        final boolean mAllDay;
466
467        MultipleDayInfo(int position, int endDay, long id, long startTime, long endTime,
468                long instanceId, boolean allDay) {
469            mPosition = position;
470            mEndDay = endDay;
471            mEventId = id;
472            mEventStartTimeMilli = startTime;
473            mEventEndTimeMilli = endTime;
474            mInstanceId = instanceId;
475            mAllDay = allDay;
476        }
477    }
478
479    /**
480     * Finds the position in the cursor of the event that best matches the time and Id.
481     * It will try to find the event that has the specified id and start time, if such event
482     * doesn't exist, it will return the event with a matching id that is closest to the start time.
483     * If the id doesn't exist, it will return the event with start time closest to the specified
484     * time.
485     * @param time - start of event in milliseconds (or any arbitrary time if event id is unknown)
486     * @param id - Event id (-1 if unknown).
487     * @return Position of event (if found) or position of nearest event according to the time.
488     *         Zero if no event found
489     */
490    public int findEventPositionNearestTime(Time time, long id) {
491        if (mRowInfo == null) {
492            return 0;
493        }
494        long millis = time.toMillis(false /* use isDst */);
495        long minDistance =  Integer.MAX_VALUE;  // some big number
496        long IdFoundMinDistance =  Integer.MAX_VALUE;  // some big number
497        int minIndex = 0;
498        int idFoundMinIndex = 0;
499        int eventInTimeIndex = -1;
500        int allDayEventInTimeIndex = -1;
501        int allDayEventDay = 0;
502        int minDay = 0;
503        boolean idFound = false;
504        int len = mRowInfo.size();
505
506        // Loop through the events and find the best match
507        // 1. Event id and start time matches requested id and time
508        // 2. Event id matches and closest time
509        // 3. No event id match , time matches a all day event (midnight)
510        // 4. No event id match , time is between event start and end
511        // 5. No event id match , all day event
512        // 6. The closest event to the requested time
513
514        for (int index = 0; index < len; index++) {
515            RowInfo row = mRowInfo.get(index);
516            if (row.mType == TYPE_DAY) {
517                continue;
518            }
519
520            // Found exact match - done
521            if (row.mEventId == id) {
522                if (row.mEventStartTimeMilli == millis) {
523                    return index;
524                }
525
526                // Not an exact match, Save event index if it is the closest to time so far
527                long distance = Math.abs(millis - row.mEventStartTimeMilli);
528                if (distance < minDistance) {
529                    IdFoundMinDistance = distance;
530                    idFoundMinIndex = index;
531                }
532                idFound = true;
533            }
534            if (!idFound) {
535                // Found an event that contains the requested time
536                if (millis >= row.mEventStartTimeMilli && millis <= row.mEventEndTimeMilli) {
537                    if (row.mAllDay) {
538                        if (millis == row.mEventStartTimeMilli) {
539                            return index;
540                        }
541                        allDayEventInTimeIndex = index;
542                        allDayEventDay = row.mDay;
543                    } else {
544                        eventInTimeIndex = index;
545                    }
546                } else {
547                    // Save event index if it is the closest to time so far
548                    long distance = Math.abs(millis - row.mEventStartTimeMilli);
549                    if (distance < minDistance) {
550                        minDistance = distance;
551                        minIndex = index;
552                        minDay = row.mDay;
553                    }
554                }
555            }
556        }
557        // We didn't find an exact match so take the best matching event
558        if (idFound) {
559            return idFoundMinIndex;
560        }
561        if (eventInTimeIndex != -1) {
562            return eventInTimeIndex;
563        } else if (allDayEventInTimeIndex != -1 && minDay != allDayEventDay) {
564            return allDayEventInTimeIndex;
565        }
566        return minIndex;
567    }
568
569
570    /**
571     * Returns a flag indicating if this position is the first day after "yesterday" that has
572     * events in it.
573     *
574     * @return a flag indicating if this is the "first day after yesterday"
575     */
576    public boolean isFirstDayAfterYesterday(int position) {
577        int headerPos = getHeaderPosition(position);
578        RowInfo row = mRowInfo.get(headerPos);
579        if (row != null) {
580            return row.mFirstDayAfterYesterday;
581        }
582        return false;
583    }
584
585    /**
586     * Finds the Julian day containing the event at the given position.
587     *
588     * @param position the list position of an event
589     * @return the Julian day containing that event
590     */
591    public int findJulianDayFromPosition(int position) {
592        if (mRowInfo == null || position < 0) {
593            return 0;
594        }
595
596        int len = mRowInfo.size();
597        if (position >= len) return 0;  // no row info at this position
598
599        for (int index = position; index >= 0; index--) {
600            RowInfo row = mRowInfo.get(index);
601            if (row.mType == TYPE_DAY) {
602                return row.mDay;
603            }
604        }
605        return 0;
606    }
607
608    /**
609     * Marks the current row as the first day that has events after "yesterday".
610     * Used to mark the separation between the past and the present/future
611     *
612     * @param position in the adapter
613     */
614    public void setAsFirstDayAfterYesterday(int position) {
615        if (mRowInfo == null || position < 0 || position > mRowInfo.size()) {
616            return;
617        }
618        RowInfo row = mRowInfo.get(position);
619        row.mFirstDayAfterYesterday = true;
620    }
621
622    /**
623     * Converts a list position to a cursor position.  The list contains
624     * day headers as well as events.  The cursor contains only events.
625     *
626     * @param listPos the list position of an event
627     * @return the corresponding cursor position of that event
628     *         if the position point to day header , it will give the position of the next event
629     *         negated.
630     */
631    public int getCursorPosition(int listPos) {
632        if (mRowInfo != null && listPos >= 0) {
633            RowInfo row = mRowInfo.get(listPos);
634            if (row.mType == TYPE_MEETING) {
635                return row.mPosition;
636            } else {
637                int nextPos = listPos + 1;
638                if (nextPos < mRowInfo.size()) {
639                    nextPos = getCursorPosition(nextPos);
640                    if (nextPos >= 0) {
641                        return -nextPos;
642                    }
643                }
644            }
645        }
646        return Integer.MIN_VALUE;
647    }
648
649    @Override
650    public boolean areAllItemsEnabled() {
651        return false;
652    }
653
654    @Override
655    public boolean isEnabled(int position) {
656        if (mRowInfo != null && position < mRowInfo.size()) {
657            RowInfo row = mRowInfo.get(position);
658            return row.mType == TYPE_MEETING;
659        }
660        return true;
661    }
662}
663