1/*
2 * Copyright (C) 2010 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.widget;
18
19import com.android.calendar.R;
20import com.android.calendar.Utils;
21
22import android.content.Context;
23import android.database.Cursor;
24import android.text.TextUtils;
25import android.text.format.DateFormat;
26import android.text.format.DateUtils;
27import android.text.format.Time;
28import android.util.Log;
29import android.view.View;
30
31import java.util.ArrayList;
32import java.util.LinkedList;
33import java.util.List;
34import java.util.TimeZone;
35
36class CalendarAppWidgetModel {
37    private static final String TAG = CalendarAppWidgetModel.class.getSimpleName();
38    private static final boolean LOGD = false;
39
40    private String mHomeTZName;
41    private boolean mShowTZ;
42    /**
43     * {@link RowInfo} is a class that represents a single row in the widget. It
44     * is actually only a pointer to either a {@link DayInfo} or an
45     * {@link EventInfo} instance, since a row in the widget might be either a
46     * day header or an event.
47     */
48    static class RowInfo {
49        static final int TYPE_DAY = 0;
50        static final int TYPE_MEETING = 1;
51
52        /**
53         *  mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING)
54         */
55        final int mType;
56
57        /**
58         * If mType is TYPE_DAY, then mData is the index into day infos.
59         * Otherwise mType is TYPE_MEETING and mData is the index into event
60         * infos.
61         */
62        final int mIndex;
63
64        RowInfo(int type, int index) {
65            mType = type;
66            mIndex = index;
67        }
68    }
69
70    /**
71     * {@link EventInfo} is a class that represents an event in the widget. It
72     * contains all of the data necessary to display that event, including the
73     * properly localized strings and visibility settings.
74     */
75    static class EventInfo {
76        int visibWhen; // Visibility value for When textview (View.GONE or View.VISIBLE)
77        String when;
78        int visibWhere; // Visibility value for Where textview (View.GONE or View.VISIBLE)
79        String where;
80        int visibTitle; // Visibility value for Title textview (View.GONE or View.VISIBLE)
81        String title;
82        int selfAttendeeStatus;
83
84        long id;
85        long start;
86        long end;
87        boolean allDay;
88        int color;
89
90        public EventInfo() {
91            visibWhen = View.GONE;
92            visibWhere = View.GONE;
93            visibTitle = View.GONE;
94        }
95
96        @Override
97        public String toString() {
98            StringBuilder builder = new StringBuilder();
99            builder.append("EventInfo [visibTitle=");
100            builder.append(visibTitle);
101            builder.append(", title=");
102            builder.append(title);
103            builder.append(", visibWhen=");
104            builder.append(visibWhen);
105            builder.append(", id=");
106            builder.append(id);
107            builder.append(", when=");
108            builder.append(when);
109            builder.append(", visibWhere=");
110            builder.append(visibWhere);
111            builder.append(", where=");
112            builder.append(where);
113            builder.append(", color=");
114            builder.append(String.format("0x%x", color));
115            builder.append(", selfAttendeeStatus=");
116            builder.append(selfAttendeeStatus);
117            builder.append("]");
118            return builder.toString();
119        }
120
121        @Override
122        public int hashCode() {
123            final int prime = 31;
124            int result = 1;
125            result = prime * result + (allDay ? 1231 : 1237);
126            result = prime * result + (int) (id ^ (id >>> 32));
127            result = prime * result + (int) (end ^ (end >>> 32));
128            result = prime * result + (int) (start ^ (start >>> 32));
129            result = prime * result + ((title == null) ? 0 : title.hashCode());
130            result = prime * result + visibTitle;
131            result = prime * result + visibWhen;
132            result = prime * result + visibWhere;
133            result = prime * result + ((when == null) ? 0 : when.hashCode());
134            result = prime * result + ((where == null) ? 0 : where.hashCode());
135            result = prime * result + color;
136            result = prime * result + selfAttendeeStatus;
137            return result;
138        }
139
140        @Override
141        public boolean equals(Object obj) {
142            if (this == obj)
143                return true;
144            if (obj == null)
145                return false;
146            if (getClass() != obj.getClass())
147                return false;
148            EventInfo other = (EventInfo) obj;
149            if (id != other.id)
150                return false;
151            if (allDay != other.allDay)
152                return false;
153            if (end != other.end)
154                return false;
155            if (start != other.start)
156                return false;
157            if (title == null) {
158                if (other.title != null)
159                    return false;
160            } else if (!title.equals(other.title))
161                return false;
162            if (visibTitle != other.visibTitle)
163                return false;
164            if (visibWhen != other.visibWhen)
165                return false;
166            if (visibWhere != other.visibWhere)
167                return false;
168            if (when == null) {
169                if (other.when != null)
170                    return false;
171            } else if (!when.equals(other.when)) {
172                return false;
173            }
174            if (where == null) {
175                if (other.where != null)
176                    return false;
177            } else if (!where.equals(other.where)) {
178                return false;
179            }
180            if (color != other.color) {
181                return false;
182            }
183            if (selfAttendeeStatus != other.selfAttendeeStatus) {
184                return false;
185            }
186            return true;
187        }
188    }
189
190    /**
191     * {@link DayInfo} is a class that represents a day header in the widget. It
192     * contains all of the data necessary to display that day header, including
193     * the properly localized string.
194     */
195    static class DayInfo {
196
197        /** The Julian day */
198        final int mJulianDay;
199
200        /** The string representation of this day header, to be displayed */
201        final String mDayLabel;
202
203        DayInfo(int julianDay, String label) {
204            mJulianDay = julianDay;
205            mDayLabel = label;
206        }
207
208        @Override
209        public String toString() {
210            return mDayLabel;
211        }
212
213        @Override
214        public int hashCode() {
215            final int prime = 31;
216            int result = 1;
217            result = prime * result + ((mDayLabel == null) ? 0 : mDayLabel.hashCode());
218            result = prime * result + mJulianDay;
219            return result;
220        }
221
222        @Override
223        public boolean equals(Object obj) {
224            if (this == obj)
225                return true;
226            if (obj == null)
227                return false;
228            if (getClass() != obj.getClass())
229                return false;
230            DayInfo other = (DayInfo) obj;
231            if (mDayLabel == null) {
232                if (other.mDayLabel != null)
233                    return false;
234            } else if (!mDayLabel.equals(other.mDayLabel))
235                return false;
236            if (mJulianDay != other.mJulianDay)
237                return false;
238            return true;
239        }
240
241    }
242
243    final List<RowInfo> mRowInfos;
244    final List<EventInfo> mEventInfos;
245    final List<DayInfo> mDayInfos;
246    final Context mContext;
247    final long mNow;
248    final int mTodayJulianDay;
249    final int mMaxJulianDay;
250
251    public CalendarAppWidgetModel(Context context, String timeZone) {
252        mNow = System.currentTimeMillis();
253        Time time = new Time(timeZone);
254        time.setToNow(); // This is needed for gmtoff to be set
255        mTodayJulianDay = Time.getJulianDay(mNow, time.gmtoff);
256        mMaxJulianDay = mTodayJulianDay + CalendarAppWidgetService.MAX_DAYS - 1;
257        mEventInfos = new ArrayList<EventInfo>(50);
258        mRowInfos = new ArrayList<RowInfo>(50);
259        mDayInfos = new ArrayList<DayInfo>(8);
260        mContext = context;
261    }
262
263    public void buildFromCursor(Cursor cursor, String timeZone) {
264        final Time recycle = new Time(timeZone);
265        final ArrayList<LinkedList<RowInfo>> mBuckets =
266                new ArrayList<LinkedList<RowInfo>>(CalendarAppWidgetService.MAX_DAYS);
267        for (int i = 0; i < CalendarAppWidgetService.MAX_DAYS; i++) {
268            mBuckets.add(new LinkedList<RowInfo>());
269        }
270        recycle.setToNow();
271        mShowTZ = !TextUtils.equals(timeZone, Time.getCurrentTimezone());
272        if (mShowTZ) {
273            mHomeTZName = TimeZone.getTimeZone(timeZone).getDisplayName(recycle.isDst != 0,
274                    TimeZone.SHORT);
275        }
276
277        cursor.moveToPosition(-1);
278        String tz = Utils.getTimeZone(mContext, null);
279        while (cursor.moveToNext()) {
280            final int rowId = cursor.getPosition();
281            final long eventId = cursor.getLong(CalendarAppWidgetService.INDEX_EVENT_ID);
282            final boolean allDay = cursor.getInt(CalendarAppWidgetService.INDEX_ALL_DAY) != 0;
283            long start = cursor.getLong(CalendarAppWidgetService.INDEX_BEGIN);
284            long end = cursor.getLong(CalendarAppWidgetService.INDEX_END);
285            final String title = cursor.getString(CalendarAppWidgetService.INDEX_TITLE);
286            final String location =
287                    cursor.getString(CalendarAppWidgetService.INDEX_EVENT_LOCATION);
288            // we don't compute these ourselves because it seems to produce the
289            // wrong endDay for all day events
290            final int startDay = cursor.getInt(CalendarAppWidgetService.INDEX_START_DAY);
291            final int endDay = cursor.getInt(CalendarAppWidgetService.INDEX_END_DAY);
292            final int color = cursor.getInt(CalendarAppWidgetService.INDEX_COLOR);
293            final int selfStatus = cursor
294                    .getInt(CalendarAppWidgetService.INDEX_SELF_ATTENDEE_STATUS);
295
296            // Adjust all-day times into local timezone
297            if (allDay) {
298                start = Utils.convertAlldayUtcToLocal(recycle, start, tz);
299                end = Utils.convertAlldayUtcToLocal(recycle, end, tz);
300            }
301
302            if (LOGD) {
303                Log.d(TAG, "Row #" + rowId + " allDay:" + allDay + " start:" + start
304                        + " end:" + end + " eventId:" + eventId);
305            }
306
307            // we might get some extra events when querying, in order to
308            // deal with all-day events
309            if (end < mNow) {
310                continue;
311            }
312
313            int i = mEventInfos.size();
314            mEventInfos.add(populateEventInfo(eventId, allDay, start, end, startDay, endDay, title,
315                    location, color, selfStatus));
316            // populate the day buckets that this event falls into
317            int from = Math.max(startDay, mTodayJulianDay);
318            int to = Math.min(endDay, mMaxJulianDay);
319            for (int day = from; day <= to; day++) {
320                LinkedList<RowInfo> bucket = mBuckets.get(day - mTodayJulianDay);
321                RowInfo rowInfo = new RowInfo(RowInfo.TYPE_MEETING, i);
322                if (allDay) {
323                    bucket.addFirst(rowInfo);
324                } else {
325                    bucket.add(rowInfo);
326                }
327            }
328        }
329
330        int day = mTodayJulianDay;
331        int count = 0;
332        for (LinkedList<RowInfo> bucket : mBuckets) {
333            if (!bucket.isEmpty()) {
334                // We don't show day header in today
335                if (day != mTodayJulianDay) {
336                    final DayInfo dayInfo = populateDayInfo(day, recycle);
337                    // Add the day header
338                    final int dayIndex = mDayInfos.size();
339                    mDayInfos.add(dayInfo);
340                    mRowInfos.add(new RowInfo(RowInfo.TYPE_DAY, dayIndex));
341                }
342
343                // Add the event row infos
344                mRowInfos.addAll(bucket);
345                count += bucket.size();
346            }
347            day++;
348            if (count >= CalendarAppWidgetService.EVENT_MIN_COUNT) {
349                break;
350            }
351        }
352    }
353
354    private EventInfo populateEventInfo(long eventId, boolean allDay, long start, long end,
355            int startDay, int endDay, String title, String location, int color, int selfStatus) {
356        EventInfo eventInfo = new EventInfo();
357
358        // Compute a human-readable string for the start time of the event
359        StringBuilder whenString = new StringBuilder();
360        int visibWhen;
361        int flags = DateUtils.FORMAT_ABBREV_ALL;
362        visibWhen = View.VISIBLE;
363        if (allDay) {
364            flags |= DateUtils.FORMAT_SHOW_DATE;
365            whenString.append(Utils.formatDateRange(mContext, start, end, flags));
366        } else {
367            flags |= DateUtils.FORMAT_SHOW_TIME;
368            if (DateFormat.is24HourFormat(mContext)) {
369                flags |= DateUtils.FORMAT_24HOUR;
370            }
371            if (endDay > startDay) {
372                flags |= DateUtils.FORMAT_SHOW_DATE;
373            }
374            whenString.append(Utils.formatDateRange(mContext, start, end, flags));
375
376            if (mShowTZ) {
377                whenString.append(" ").append(mHomeTZName);
378            }
379        }
380        eventInfo.id = eventId;
381        eventInfo.start = start;
382        eventInfo.end = end;
383        eventInfo.allDay = allDay;
384        eventInfo.when = whenString.toString();
385        eventInfo.visibWhen = visibWhen;
386        eventInfo.color = color;
387        eventInfo.selfAttendeeStatus = selfStatus;
388
389        // What
390        if (TextUtils.isEmpty(title)) {
391            eventInfo.title = mContext.getString(R.string.no_title_label);
392        } else {
393            eventInfo.title = title;
394        }
395        eventInfo.visibTitle = View.VISIBLE;
396
397        // Where
398        if (!TextUtils.isEmpty(location)) {
399            eventInfo.visibWhere = View.VISIBLE;
400            eventInfo.where = location;
401        } else {
402            eventInfo.visibWhere = View.GONE;
403        }
404        return eventInfo;
405    }
406
407    private DayInfo populateDayInfo(int julianDay, Time recycle) {
408        long millis = recycle.setJulianDay(julianDay);
409        int flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
410
411        String label;
412        if (julianDay == mTodayJulianDay + 1) {
413            label = mContext.getString(R.string.agenda_tomorrow,
414                    Utils.formatDateRange(mContext, millis, millis, flags).toString());
415        } else {
416            flags |= DateUtils.FORMAT_SHOW_WEEKDAY;
417            label = Utils.formatDateRange(mContext, millis, millis, flags);
418        }
419        return new DayInfo(julianDay, label);
420    }
421
422    @Override
423    public String toString() {
424        StringBuilder builder = new StringBuilder();
425        builder.append("\nCalendarAppWidgetModel [eventInfos=");
426        builder.append(mEventInfos);
427        builder.append("]");
428        return builder.toString();
429    }
430}