AgendaByDayAdapter.java revision 31412a0fea756e0da0bcbdf3cdffe4efae21cdbe
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.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.BaseAdapter;
33import android.widget.TextView;
34
35import java.util.ArrayList;
36import java.util.Formatter;
37import java.util.Iterator;
38import java.util.LinkedList;
39import java.util.Locale;
40
41public class AgendaByDayAdapter extends BaseAdapter {
42    private static final int TYPE_DAY = 0;
43    private static final int TYPE_MEETING = 1;
44    static final int TYPE_LAST = 2;
45
46    private final Context mContext;
47    private final AgendaAdapter mAgendaAdapter;
48    private final LayoutInflater mInflater;
49    private ArrayList<RowInfo> mRowInfo;
50    private int mTodayJulianDay;
51    private Time mTmpTime;
52    private String mTimeZone;
53    // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
54    private Formatter mFormatter;
55    private StringBuilder mStringBuilder;
56
57    static class ViewHolder {
58        TextView dayView;
59        TextView dateView;
60        int julianDay;
61        boolean grayed;
62    }
63
64    private Runnable mTZUpdater = new Runnable() {
65        @Override
66        public void run() {
67            mTimeZone = Utils.getTimeZone(mContext, this);
68            mTmpTime = new Time(mTimeZone);
69            notifyDataSetChanged();
70        }
71    };
72
73    public AgendaByDayAdapter(Context context) {
74        mContext = context;
75        mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item);
76        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
77        mStringBuilder = new StringBuilder(50);
78        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
79        mTimeZone = Utils.getTimeZone(context, mTZUpdater);
80        mTmpTime = new Time(mTimeZone);
81    }
82
83
84    // Returns the position of a header of a specific item
85    public int getHeaderPosition(int position) {
86        if (mRowInfo == null || position >= mRowInfo.size()) {
87            return -1;
88        }
89
90        for (int i = position; i >=0; i --) {
91            RowInfo row = mRowInfo.get(i);
92            if (row != null && row.mType == TYPE_DAY)
93                return i;
94        }
95        return -1;
96    }
97
98    // Returns the number of items in a section defined by a specific header location
99    public int getHeaderItemsCount(int position) {
100        if (mRowInfo == null) {
101            return -1;
102        }
103        int count = 0;
104        for (int i = position +1; i < mRowInfo.size(); i++) {
105            if (mRowInfo.get(i).mType != TYPE_MEETING) {
106                return count;
107            }
108            count ++;
109        }
110        return count;
111    }
112
113    public int getCount() {
114        if (mRowInfo != null) {
115            return mRowInfo.size();
116        }
117        return mAgendaAdapter.getCount();
118    }
119
120    public Object getItem(int position) {
121        if (mRowInfo != null) {
122            RowInfo row = mRowInfo.get(position);
123            if (row.mType == TYPE_DAY) {
124                return row;
125            } else {
126                return mAgendaAdapter.getItem(row.mPosition);
127            }
128        }
129        return mAgendaAdapter.getItem(position);
130    }
131
132    public long getItemId(int position) {
133        if (mRowInfo != null) {
134            RowInfo row = mRowInfo.get(position);
135            if (row.mType == TYPE_DAY) {
136                return -position;
137            } else {
138                return mAgendaAdapter.getItemId(row.mPosition);
139            }
140        }
141        return mAgendaAdapter.getItemId(position);
142    }
143
144    @Override
145    public int getViewTypeCount() {
146        return TYPE_LAST;
147    }
148
149    @Override
150    public int getItemViewType(int position) {
151        return mRowInfo != null && mRowInfo.size() > position ?
152                mRowInfo.get(position).mType : TYPE_DAY;
153    }
154
155    public boolean isDayHeaderView(int position) {
156        return (getItemViewType(position) == TYPE_DAY);
157    }
158
159    public View getView(int position, View convertView, ViewGroup parent) {
160        if ((mRowInfo == null) || (position > mRowInfo.size())) {
161            // If we have no row info, mAgendaAdapter returns the view.
162            return mAgendaAdapter.getView(position, convertView, parent);
163        }
164
165        RowInfo row = mRowInfo.get(position);
166        if (row.mType == TYPE_DAY) {
167            ViewHolder holder = null;
168            View agendaDayView = null;
169            if ((convertView != null) && (convertView.getTag() != null)) {
170                // Listview may get confused and pass in a different type of
171                // view since we keep shifting data around. Not a big problem.
172                Object tag = convertView.getTag();
173                if (tag instanceof ViewHolder) {
174                    agendaDayView = convertView;
175                    holder = (ViewHolder) tag;
176                    holder.julianDay = row.mDay;
177                }
178            }
179
180            if (holder == null) {
181                // Create a new AgendaView with a ViewHolder for fast access to
182                // views w/o calling findViewById()
183                holder = new ViewHolder();
184                agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false);
185                holder.dayView = (TextView) agendaDayView.findViewById(R.id.day);
186                holder.dateView = (TextView) agendaDayView.findViewById(R.id.date);
187                holder.julianDay = row.mDay;
188                holder.grayed = false;
189                agendaDayView.setTag(holder);
190            }
191
192            // Re-use the member variable "mTime" which is set to the local
193            // time zone.
194            // It's difficult to find and update all these adapters when the
195            // home tz changes so check it here and update if needed.
196            String tz = Utils.getTimeZone(mContext, mTZUpdater);
197            if (!TextUtils.equals(tz, mTmpTime.timezone)) {
198                mTimeZone = tz;
199                mTmpTime = new Time(tz);
200            }
201
202            // Build the text for the day of the week.
203            // Should be yesterday/today/tomorrow (if applicable) + day of the week
204
205            Time date = mTmpTime;
206            long millis = date.setJulianDay(row.mDay);
207            int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
208            mStringBuilder.setLength(0);
209
210            String dayViewText = Utils.getDayOfWeekString(row.mDay, mTodayJulianDay, millis,
211                    mContext);
212
213            // Build text for the date
214            // Format should be month day
215
216            mStringBuilder.setLength(0);
217            flags = DateUtils.FORMAT_SHOW_DATE;
218            String dateViewText = DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
219                    flags, mTimeZone).toString();
220
221            if (AgendaWindowAdapter.BASICLOG) {
222                dayViewText += " P:" + position;
223                dateViewText += " P:" + position;
224            }
225            holder.dayView.setText(dayViewText);
226            holder.dateView.setText(dateViewText);
227
228            // Set the background of the view, it is grayed for day that are in the past and today
229            if (row.mDay > mTodayJulianDay) {
230                agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_primary);
231                holder.grayed = false;
232            } else {
233                agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_secondary);
234                holder.grayed = true;
235            }
236            return agendaDayView;
237        } else if (row.mType == TYPE_MEETING) {
238            View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);
239            AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag());
240            TextView title = holder.title;
241            long eventStartTime = holder.startTimeMilli;
242            boolean allDay = holder.allDay;
243            if (AgendaWindowAdapter.BASICLOG) {
244                title.setText(title.getText() + " P:" + position);
245            } else {
246                title.setText(title.getText());
247            }
248
249            // if event in the past or started already, un-bold the title and set the background
250            if ((!allDay && eventStartTime <= System.currentTimeMillis()) ||
251                    (allDay && row.mDay <= mTodayJulianDay)) {
252                itemView.setBackgroundResource(R.drawable.agenda_item_bg_secondary);
253                title.setTypeface(Typeface.DEFAULT);
254                holder.grayed = true;
255            } else {
256                itemView.setBackgroundResource(R.drawable.agenda_item_bg_primary);
257                title.setTypeface(Typeface.DEFAULT_BOLD);
258                holder.grayed = false;
259            }
260            holder.julianDay = row.mDay;
261            return itemView;
262        } else {
263            // Error
264            throw new IllegalStateException("Unknown event type:" + row.mType);
265        }
266    }
267
268    public void clearDayHeaderInfo() {
269        mRowInfo = null;
270    }
271
272    public void changeCursor(DayAdapterInfo info) {
273        calculateDays(info);
274        mAgendaAdapter.changeCursor(info.cursor);
275    }
276
277    public void calculateDays(DayAdapterInfo dayAdapterInfo) {
278        Cursor cursor = dayAdapterInfo.cursor;
279        ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>();
280        int prevStartDay = -1;
281        Time time = new Time(mTimeZone);
282        long now = System.currentTimeMillis();
283        time.set(now);
284        mTodayJulianDay = Time.getJulianDay(now, time.gmtoff);
285        LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>();
286        for (int position = 0; cursor.moveToNext(); position++) {
287            int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
288
289            // Skip over the days outside of the adapter's range
290            startDay = Math.max(startDay, dayAdapterInfo.start);
291
292            if (startDay != prevStartDay) {
293                // Check if we skipped over any empty days
294                if (prevStartDay == -1) {
295                    rowInfo.add(new RowInfo(TYPE_DAY, startDay, 0));
296                } else {
297                    // If there are any multiple-day events that span the empty
298                    // range of days, then create day headers and events for
299                    // those multiple-day events.
300                    boolean dayHeaderAdded = false;
301                    for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) {
302                        dayHeaderAdded = false;
303                        Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
304                        while (iter.hasNext()) {
305                            MultipleDayInfo info = iter.next();
306                            // If this event has ended then remove it from the
307                            // list.
308                            if (info.mEndDay < currentDay) {
309                                iter.remove();
310                                continue;
311                            }
312
313                            // If this is the first event for the day, then
314                            // insert a day header.
315                            if (!dayHeaderAdded) {
316                                rowInfo.add(new RowInfo(TYPE_DAY, currentDay, 0));
317                                dayHeaderAdded = true;
318                            }
319                            rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition));
320                        }
321                    }
322
323                    // If the day header was not added for the start day, then
324                    // add it now.
325                    if (!dayHeaderAdded) {
326                        rowInfo.add(new RowInfo(TYPE_DAY, startDay, 0));
327                    }
328                }
329                prevStartDay = startDay;
330            }
331
332            // Add in the event for this cursor position
333            rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position));
334
335            // If this event spans multiple days, then add it to the multipleDay
336            // list.
337            int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY);
338
339            // Skip over the days outside of the adapter's range
340            endDay = Math.min(endDay, dayAdapterInfo.end);
341            if (endDay > startDay) {
342                multipleDayList.add(new MultipleDayInfo(position, endDay));
343            }
344        }
345
346        // There are no more cursor events but we might still have multiple-day
347        // events left.  So create day headers and events for those.
348        if (prevStartDay > 0) {
349            for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end;
350                    currentDay++) {
351                boolean dayHeaderAdded = false;
352                Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
353                while (iter.hasNext()) {
354                    MultipleDayInfo info = iter.next();
355                    // If this event has ended then remove it from the
356                    // list.
357                    if (info.mEndDay < currentDay) {
358                        iter.remove();
359                        continue;
360                    }
361
362                    // If this is the first event for the day, then
363                    // insert a day header.
364                    if (!dayHeaderAdded) {
365                        rowInfo.add(new RowInfo(TYPE_DAY, currentDay, 0));
366                        dayHeaderAdded = true;
367                    }
368                    rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition));
369                }
370            }
371        }
372        mRowInfo = rowInfo;
373    }
374
375    private static class RowInfo {
376        // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING)
377        final int mType;
378
379        final int mDay;          // Julian day
380        final int mPosition;     // cursor position (not used for TYPE_DAY)
381        // This is used to mark a day header as the first day with events that is "today"
382        // or later. This flag is used by the adapter to create a view with a visual separator
383        // between the past and the present/future
384        boolean mFirstDayAfterYesterday;
385
386        RowInfo(int type, int julianDay, int position) {
387            mType = type;
388            mDay = julianDay;
389            mPosition = position;
390            mFirstDayAfterYesterday = false;
391        }
392    }
393
394    private static class MultipleDayInfo {
395        final int mPosition;
396        final int mEndDay;
397
398        MultipleDayInfo(int position, int endDay) {
399            mPosition = position;
400            mEndDay = endDay;
401        }
402    }
403
404    /**
405     * Searches for the day that matches the given Time object and returns the
406     * list position of that day.  If there are no events for that day, then it
407     * finds the nearest day (before or after) that has events and returns the
408     * list position for that day.
409     *
410     * @param time the date to search for
411     * @return the cursor position of the first event for that date, or zero
412     * if no match was found
413     */
414    public int findDayPositionNearestTime(Time time) {
415        if (mRowInfo == null) {
416            return 0;
417        }
418        long millis = time.toMillis(false /* use isDst */);
419        int julianDay = Time.getJulianDay(millis, time.gmtoff);
420        int minDistance = 1000;  // some big number
421        int minIndex = 0;
422        int len = mRowInfo.size();
423        for (int index = 0; index < len; index++) {
424            RowInfo row = mRowInfo.get(index);
425            if (row.mType == TYPE_DAY) {
426                int distance = Math.abs(julianDay - row.mDay);
427                if (distance == 0) {
428                    return index;
429                }
430                if (distance < minDistance) {
431                    minDistance = distance;
432                    minIndex = index;
433                }
434            }
435        }
436
437        // We didn't find an exact match so take the nearest day that had
438        // events.
439        return minIndex;
440    }
441
442    /**
443     * Returns a flag indicating if this position is the first day after "yesterday" that has
444     * events in it.
445     *
446     * @return a flag indicating if this is the "first day after yesterday"
447     */
448    public boolean isFirstDayAfterYesterday(int position) {
449        int headerPos = getHeaderPosition(position);
450        RowInfo row = mRowInfo.get(headerPos);
451        if (row != null) {
452            return row.mFirstDayAfterYesterday;
453        }
454        return false;
455    }
456
457    /**
458     * Finds the Julian day containing the event at the given position.
459     *
460     * @param position the list position of an event
461     * @return the Julian day containing that event
462     */
463    public int findJulianDayFromPosition(int position) {
464        if (mRowInfo == null || position < 0) {
465            return 0;
466        }
467
468        int len = mRowInfo.size();
469        if (position >= len) return 0;  // no row info at this position
470
471        for (int index = position; index >= 0; index--) {
472            RowInfo row = mRowInfo.get(index);
473            if (row.mType == TYPE_DAY) {
474                return row.mDay;
475            }
476        }
477        return 0;
478    }
479
480    /**
481     * Marks the current row as the first day that has events after "yesterday".
482     * Used to mark the separation between the past and the present/future
483     *
484     * @param position in the adapter
485     */
486    public void setAsFirstDayAfterYesterday(int position) {
487        if (mRowInfo == null || position < 0 || position > mRowInfo.size()) {
488            return;
489        }
490        RowInfo row = mRowInfo.get(position);
491        row.mFirstDayAfterYesterday = true;
492    }
493
494    /**
495     * Converts a list position to a cursor position.  The list contains
496     * day headers as well as events.  The cursor contains only events.
497     *
498     * @param listPos the list position of an event
499     * @return the corresponding cursor position of that event
500     */
501    public int getCursorPosition(int listPos) {
502        if (mRowInfo != null && listPos >= 0) {
503            RowInfo row = mRowInfo.get(listPos);
504            if (row.mType == TYPE_MEETING) {
505                return row.mPosition;
506            } else {
507                int nextPos = listPos + 1;
508                if (nextPos < mRowInfo.size()) {
509                    nextPos = getCursorPosition(nextPos);
510                    if (nextPos >= 0) {
511                        return -nextPos;
512                    }
513                }
514            }
515        }
516        return Integer.MIN_VALUE;
517    }
518
519    @Override
520    public boolean areAllItemsEnabled() {
521        return false;
522    }
523
524    @Override
525    public boolean isEnabled(int position) {
526        if (mRowInfo != null && position < mRowInfo.size()) {
527            RowInfo row = mRowInfo.get(position);
528            return row.mType == TYPE_MEETING;
529        }
530        return true;
531    }
532}
533