/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar.widget import com.android.calendar.R import com.android.calendar.Utils import android.content.Context import android.database.Cursor import android.text.TextUtils import android.text.format.DateFormat import android.text.format.DateUtils import android.text.format.Time import android.util.Log import android.view.View import java.util.ArrayList import java.util.LinkedList import java.util.TimeZone internal class CalendarAppWidgetModel(context: Context, timeZone: String?) { private var mHomeTZName: String? = null private var mShowTZ = false /** * [RowInfo] is a class that represents a single row in the widget. It * is actually only a pointer to either a [DayInfo] or an * [EventInfo] instance, since a row in the widget might be either a * day header or an event. */ internal class RowInfo( /** * mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING) */ @JvmField val mType: Int, /** * If mType is TYPE_DAY, then mData is the index into day infos. * Otherwise mType is TYPE_MEETING and mData is the index into event * infos. */ @JvmField val mIndex: Int ) { companion object { const val TYPE_DAY = 0 const val TYPE_MEETING = 1 } } /** * [EventInfo] is a class that represents an event in the widget. It * contains all of the data necessary to display that event, including the * properly localized strings and visibility settings. */ internal class EventInfo { // Visibility value for When textview (View.GONE or View.VISIBLE) @JvmField var visibWhen: Int @JvmField var `when`: String? = null // Visibility value for Where textview (View.GONE or View.VISIBLE) @JvmField var visibWhere: Int @JvmField var where: String? = null // Visibility value for Title textview (View.GONE or View.VISIBLE) @JvmField var visibTitle: Int @JvmField var title: String? = null @JvmField var selfAttendeeStatus = 0 @JvmField var id: Long = 0 @JvmField var start: Long = 0 @JvmField var end: Long = 0 @JvmField var allDay = false @JvmField var color = 0 @Override override fun toString(): String { val builder = StringBuilder() builder.append("EventInfo [visibTitle=") builder.append(visibTitle) builder.append(", title=") builder.append(title) builder.append(", visibWhen=") builder.append(visibWhen) builder.append(", id=") builder.append(id) builder.append(", when=") builder.append(`when`) builder.append(", visibWhere=") builder.append(visibWhere) builder.append(", where=") builder.append(where) builder.append(", color=") builder.append(String.format("0x%x", color)) builder.append(", selfAttendeeStatus=") builder.append(selfAttendeeStatus) builder.append("]") return builder.toString() } @Override override fun hashCode(): Int { val prime = 31 var result = 1 result = prime * result + if (allDay) 1231 else 1237 result = prime * result + (id xor (id ushr 32)).toInt() result = prime * result + (end xor (end ushr 32)).toInt() result = prime * result + (start xor (start ushr 32)).toInt() result = prime * result + if (title == null) 0 else title!!.hashCode() result = prime * result + visibTitle result = prime * result + visibWhen result = prime * result + visibWhere result = prime * result + if (`when` == null) 0 else `when`!!.hashCode() result = prime * result + if (where == null) 0 else where!!.hashCode() result = prime * result + color result = prime * result + selfAttendeeStatus return result } @Override override fun equals(obj: Any?): Boolean { if (this == obj) return true if (obj == null) return false if (this::class != obj::class) return false val other = obj as EventInfo if (id != other.id) return false if (allDay != other.allDay) return false if (end != other.end) return false if (start != other.start) return false if (title == null) { if (other.title != null) return false } else if (!title!!.equals(other.title)) return false if (visibTitle != other.visibTitle) return false if (visibWhen != other.visibWhen) return false if (visibWhere != other.visibWhere) return false if (`when` == null) { if (other.`when` != null) return false } else if (!`when`!!.equals(other.`when`)) { return false } if (where == null) { if (other.where != null) return false } else if (!where!!.equals(other.where)) { return false } if (color != other.color) { return false } return if (selfAttendeeStatus != other.selfAttendeeStatus) { false } else true } init { visibWhen = View.GONE visibWhere = View.GONE visibTitle = View.GONE } } /** * [DayInfo] is a class that represents a day header in the widget. It * contains all of the data necessary to display that day header, including * the properly localized string. */ internal class DayInfo( /** The Julian day */ @JvmField var mJulianDay: Int, /** The string representation of this day header, to be displayed */ @JvmField var mDayLabel: String? = null ) { @Override override fun toString(): String { return mDayLabel as String } @Override override fun hashCode(): Int { val prime = 31 var result = 1 result = prime * result + (mDayLabel?.hashCode() ?: 0) result = prime * result + mJulianDay return result } @Override override fun equals(obj: Any?): Boolean { if (this == obj) return true if (obj == null) return false if (this::class !== obj::class) return false val other = obj as DayInfo if (mDayLabel == null) { if (other.mDayLabel != null) return false } else if (!mDayLabel.equals(other.mDayLabel)) return false return if (mJulianDay != other.mJulianDay) false else true } } @JvmField val mRowInfos: ArrayList @JvmField val mEventInfos: ArrayList @JvmField val mDayInfos: ArrayList @JvmField val mContext: Context? @JvmField val mNow: Long @JvmField val mTodayJulianDay: Int @JvmField val mMaxJulianDay: Int fun buildFromCursor(cursor: Cursor, timeZone: String?) { val recycle = Time(timeZone) val mBuckets: ArrayList> = ArrayList>(CalendarAppWidgetService.MAX_DAYS) for (i in 0 until CalendarAppWidgetService.MAX_DAYS) { mBuckets.add(LinkedList()) } recycle.setToNow() mShowTZ = !TextUtils.equals(timeZone, Time.getCurrentTimezone()) if (mShowTZ) { mHomeTZName = TimeZone.getTimeZone(timeZone).getDisplayName( recycle.isDst !== 0, TimeZone.SHORT ) } cursor.moveToPosition(-1) val tz = Utils.getTimeZone(mContext, null) while (cursor.moveToNext()) { val rowId: Int = cursor.getPosition() val eventId: Long = cursor.getLong(CalendarAppWidgetService.INDEX_EVENT_ID) val allDay = cursor.getInt(CalendarAppWidgetService.INDEX_ALL_DAY) !== 0 var start: Long = cursor.getLong(CalendarAppWidgetService.INDEX_BEGIN) var end: Long = cursor.getLong(CalendarAppWidgetService.INDEX_END) val title: String = cursor.getString(CalendarAppWidgetService.INDEX_TITLE) val location: String = cursor.getString(CalendarAppWidgetService.INDEX_EVENT_LOCATION) // we don't compute these ourselves because it seems to produce the // wrong endDay for all day events val startDay: Int = cursor.getInt(CalendarAppWidgetService.INDEX_START_DAY) val endDay: Int = cursor.getInt(CalendarAppWidgetService.INDEX_END_DAY) val color: Int = cursor.getInt(CalendarAppWidgetService.INDEX_COLOR) val selfStatus: Int = cursor .getInt(CalendarAppWidgetService.INDEX_SELF_ATTENDEE_STATUS) // Adjust all-day times into local timezone if (allDay) { start = Utils.convertAlldayUtcToLocal(recycle, start, tz as String) end = Utils.convertAlldayUtcToLocal(recycle, end, tz as String) } if (LOGD) { Log.d( TAG, "Row #" + rowId + " allDay:" + allDay + " start:" + start + " end:" + end + " eventId:" + eventId ) } // we might get some extra events when querying, in order to // deal with all-day events if (end < mNow) { continue } val i: Int = mEventInfos.size mEventInfos.add( populateEventInfo( eventId, allDay, start, end, startDay, endDay, title, location, color, selfStatus ) ) // populate the day buckets that this event falls into val from: Int = Math.max(startDay, mTodayJulianDay) val to: Int = Math.min(endDay, mMaxJulianDay) for (day in from..to) { val bucket: LinkedList = mBuckets.get(day - mTodayJulianDay) val rowInfo = RowInfo(RowInfo.TYPE_MEETING, i) if (allDay) { bucket.addFirst(rowInfo) } else { bucket.add(rowInfo) } } } var day = mTodayJulianDay var count = 0 for (bucket in mBuckets) { if (!bucket.isEmpty()) { // We don't show day header in today if (day != mTodayJulianDay) { val dayInfo = populateDayInfo(day, recycle) // Add the day header val dayIndex: Int = mDayInfos.size mDayInfos.add(dayInfo as CalendarAppWidgetModel.DayInfo) mRowInfos.add(RowInfo(RowInfo.TYPE_DAY, dayIndex)) } // Add the event row infos mRowInfos.addAll(bucket) count += bucket.size } day++ if (count >= CalendarAppWidgetService.EVENT_MIN_COUNT) { break } } } private fun populateEventInfo( eventId: Long, allDay: Boolean, start: Long, end: Long, startDay: Int, endDay: Int, title: String, location: String, color: Int, selfStatus: Int ): EventInfo { val eventInfo = EventInfo() // Compute a human-readable string for the start time of the event val whenString = StringBuilder() val visibWhen: Int var flags: Int = DateUtils.FORMAT_ABBREV_ALL visibWhen = View.VISIBLE if (allDay) { flags = flags or DateUtils.FORMAT_SHOW_DATE whenString.append(Utils.formatDateRange(mContext, start, end, flags)) } else { flags = flags or DateUtils.FORMAT_SHOW_TIME if (DateFormat.is24HourFormat(mContext)) { flags = flags or DateUtils.FORMAT_24HOUR } if (endDay > startDay) { flags = flags or DateUtils.FORMAT_SHOW_DATE } whenString.append(Utils.formatDateRange(mContext, start, end, flags)) if (mShowTZ) { whenString.append(" ").append(mHomeTZName) } } eventInfo.id = eventId eventInfo.start = start eventInfo.end = end eventInfo.allDay = allDay eventInfo.`when` = whenString.toString() eventInfo.visibWhen = visibWhen eventInfo.color = color eventInfo.selfAttendeeStatus = selfStatus // What if (TextUtils.isEmpty(title)) { eventInfo.title = mContext?.getString(R.string.no_title_label) } else { eventInfo.title = title } eventInfo.visibTitle = View.VISIBLE // Where if (!TextUtils.isEmpty(location)) { eventInfo.visibWhere = View.VISIBLE eventInfo.where = location } else { eventInfo.visibWhere = View.GONE } return eventInfo } private fun populateDayInfo(julianDay: Int, recycle: Time?): DayInfo? { val millis: Long = recycle?.setJulianDay(julianDay) as Long var flags: Int = DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_DATE val label: String? if (julianDay == mTodayJulianDay + 1) { label = mContext?.getString( R.string.agenda_tomorrow, Utils.formatDateRange(mContext, millis, millis, flags).toString() ) } else { flags = flags or DateUtils.FORMAT_SHOW_WEEKDAY label = Utils.formatDateRange(mContext, millis, millis, flags) } return DayInfo(julianDay, label as String) } @Override override fun toString(): String { val builder = StringBuilder() builder.append("\nCalendarAppWidgetModel [eventInfos=") builder.append(mEventInfos) builder.append("]") return builder.toString() } companion object { private val TAG: String = CalendarAppWidgetModel::class.java.getSimpleName() private const val LOGD = false } init { mNow = System.currentTimeMillis() val time = Time(timeZone) time.setToNow() // This is needed for gmtoff to be set mTodayJulianDay = Time.getJulianDay(mNow, time.gmtoff) mMaxJulianDay = mTodayJulianDay + CalendarAppWidgetService.MAX_DAYS - 1 mEventInfos = ArrayList(50) mRowInfos = ArrayList(50) mDayInfos = ArrayList(8) mContext = context } }