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