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