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 
17 package com.android.calendar.widget;
18 
19 import com.android.calendar.R;
20 import com.android.calendar.Utils;
21 
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.text.TextUtils;
25 import android.text.format.DateFormat;
26 import android.text.format.DateUtils;
27 import android.text.format.Time;
28 import android.util.Log;
29 import android.view.View;
30 
31 import java.util.ArrayList;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.TimeZone;
35 
36 class 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 
RowInfo(int type, int index)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 
EventInfo()90         public EventInfo() {
91             visibWhen = View.GONE;
92             visibWhere = View.GONE;
93             visibTitle = View.GONE;
94         }
95 
96         @Override
toString()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
hashCode()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
equals(Object obj)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 
DayInfo(int julianDay, String label)203         DayInfo(int julianDay, String label) {
204             mJulianDay = julianDay;
205             mDayLabel = label;
206         }
207 
208         @Override
toString()209         public String toString() {
210             return mDayLabel;
211         }
212 
213         @Override
hashCode()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
equals(Object obj)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 
CalendarAppWidgetModel(Context context, String timeZone)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 
buildFromCursor(Cursor cursor, String timeZone)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 
populateEventInfo(long eventId, boolean allDay, long start, long end, int startDay, int endDay, String title, String location, int color, int selfStatus)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 
populateDayInfo(int julianDay, Time recycle)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
toString()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 }