Merge changes from topic "CalendarAppWidgetService" am: aa98535cdb am: d9ace364ed am: 081bde028b

Original change: https://android-review.googlesource.com/c/platform/packages/apps/Calendar/+/1754291

Change-Id: I2ec59fde89cf82577191f932e8cdb883a046077a
diff --git a/Android.bp b/Android.bp
index 142971b..af827ed 100644
--- a/Android.bp
+++ b/Android.bp
@@ -30,6 +30,8 @@
     "src/**/calendar/month/MonthByWeekFragment.java",
     "src/**/calendar/widget/CalendarAppWidgetModel.java",
     "src/**/calendar/widget/CalendarAppWidgetProvider.java",
+    "src/**/calendar/widget/CalendarAppWidgetService.java",
+    "src/**/calendar/AllInOneActivity.java",
     "src/**/calendar/CalendarController.java",
     "src/**/calendar/DayOfMonthDrawable.java",
     "src/**/calendar/Event.java",
diff --git a/src/com/android/calendar/widget/CalendarAppWidgetService.kt b/src/com/android/calendar/widget/CalendarAppWidgetService.kt
new file mode 100644
index 0000000..114fdf1
--- /dev/null
+++ b/src/com/android/calendar/widget/CalendarAppWidgetService.kt
@@ -0,0 +1,665 @@
+/*
+ * 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 android.app.AlarmManager
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.CursorLoader
+import android.content.Intent
+import android.content.Loader
+import android.content.res.Resources
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.Handler
+import android.provider.CalendarContract.Attendees
+import android.provider.CalendarContract.Calendars
+import android.provider.CalendarContract.Instances
+import android.text.format.DateUtils
+import android.text.format.Time
+import android.util.Log
+import android.view.View
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService
+import com.android.calendar.R
+import com.android.calendar.Utils
+import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo
+import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo
+import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicInteger
+
+class CalendarAppWidgetService : RemoteViewsService() {
+    companion object {
+        private const val TAG = "CalendarWidget"
+        const val EVENT_MIN_COUNT = 20
+        const val EVENT_MAX_COUNT = 100
+
+        // Minimum delay between queries on the database for widget updates in ms
+        const val WIDGET_UPDATE_THROTTLE = 500
+        private val EVENT_SORT_ORDER: String = (Instances.START_DAY.toString() + " ASC, " +
+            Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, " +
+            Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT)
+        private val EVENT_SELECTION: String = Calendars.VISIBLE.toString() + "=1"
+        private val EVENT_SELECTION_HIDE_DECLINED: String =
+            (Calendars.VISIBLE.toString() + "=1 AND " +
+                Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED)
+        @JvmField
+        val EVENT_PROJECTION = arrayOf<String>(
+            Instances.ALL_DAY,
+            Instances.BEGIN,
+            Instances.END,
+            Instances.TITLE,
+            Instances.EVENT_LOCATION,
+            Instances.EVENT_ID,
+            Instances.START_DAY,
+            Instances.END_DAY,
+            Instances.DISPLAY_COLOR, // If SDK < 16, set to Instances.CALENDAR_COLOR.
+            Instances.SELF_ATTENDEE_STATUS
+        )
+        const val INDEX_ALL_DAY = 0
+        const val INDEX_BEGIN = 1
+        const val INDEX_END = 2
+        const val INDEX_TITLE = 3
+        const val INDEX_EVENT_LOCATION = 4
+        const val INDEX_EVENT_ID = 5
+        const val INDEX_START_DAY = 6
+        const val INDEX_END_DAY = 7
+        const val INDEX_COLOR = 8
+        const val INDEX_SELF_ATTENDEE_STATUS = 9
+        const val MAX_DAYS = 7
+        private val SEARCH_DURATION: Long = MAX_DAYS * DateUtils.DAY_IN_MILLIS
+
+        /**
+         * Update interval used when no next-update calculated, or bad trigger time in past.
+         * Unit: milliseconds.
+         */
+        private val UPDATE_TIME_NO_EVENTS: Long = DateUtils.HOUR_IN_MILLIS * 6
+
+        /**
+         * Format given time for debugging output.
+         *
+         * @param unixTime Target time to report.
+         * @param now Current system time from [System.currentTimeMillis]
+         * for calculating time difference.
+         */
+        fun formatDebugTime(unixTime: Long, now: Long): String {
+            val time = Time()
+            time.set(unixTime)
+            var delta = unixTime - now
+            return if (delta > DateUtils.MINUTE_IN_MILLIS) {
+                delta /= DateUtils.MINUTE_IN_MILLIS
+                String.format(
+                    "[%d] %s (%+d mins)", unixTime,
+                    time.format("%H:%M:%S"), delta
+                )
+            } else {
+                delta /= DateUtils.SECOND_IN_MILLIS
+                String.format(
+                    "[%d] %s (%+d secs)", unixTime,
+                    time.format("%H:%M:%S"), delta
+                )
+            }
+        }
+
+        init {
+            if (!Utils.isJellybeanOrLater()) {
+                EVENT_PROJECTION[INDEX_COLOR] = Instances.CALENDAR_COLOR
+            }
+        }
+    }
+
+    @Override
+    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
+        return CalendarFactory(getApplicationContext(), intent)
+    }
+
+    class CalendarFactory : BroadcastReceiver, RemoteViewsService.RemoteViewsFactory,
+                            Loader.OnLoadCompleteListener<Cursor?> {
+        private var mContext: Context? = null
+        private var mResources: Resources? = null
+        private var mLastSerialNum = -1
+        private var mLoader: CursorLoader? = null
+        private val mHandler: Handler = Handler()
+        private val executor: ExecutorService = Executors.newSingleThreadExecutor()
+        private var mAppWidgetId = 0
+        private var mDeclinedColor = 0
+        private var mStandardColor = 0
+        private var mAllDayColor = 0
+        private val mTimezoneChanged: Runnable = object : Runnable {
+            @Override
+            override fun run() {
+                if (mLoader != null) {
+                    mLoader?.forceLoad()
+                }
+            }
+        }
+
+        private fun createUpdateLoaderRunnable(
+            selection: String,
+            result: PendingResult,
+            version: Int
+        ): Runnable {
+            return object : Runnable {
+                @Override
+                override fun run() {
+                    // If there is a newer load request in the queue, skip loading.
+                    if (mLoader != null && version >= currentVersion.get()) {
+                        val uri: Uri = createLoaderUri()
+                        mLoader?.setUri(uri)
+                        mLoader?.setSelection(selection)
+                        synchronized(mLock) { mLastSerialNum = ++mSerialNum }
+                        mLoader?.forceLoad()
+                    }
+                    result.finish()
+                }
+            }
+        }
+
+        constructor(context: Context, intent: Intent) {
+            mContext = context
+            mResources = context.getResources()
+            mAppWidgetId = intent.getIntExtra(
+                AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID
+            )
+            mDeclinedColor = mResources?.getColor(R.color.appwidget_item_declined_color) as Int
+            mStandardColor = mResources?.getColor(R.color.appwidget_item_standard_color) as Int
+            mAllDayColor = mResources?.getColor(R.color.appwidget_item_allday_color) as Int
+        }
+
+        constructor() {
+            // This is being created as part of onReceive
+        }
+
+        @Override
+        override fun onCreate() {
+            val selection = queryForSelection()
+            initLoader(selection)
+        }
+
+        @Override
+        override fun onDataSetChanged() {
+        }
+
+        @Override
+        override fun onDestroy() {
+            if (mLoader != null) {
+                mLoader?.reset()
+            }
+        }
+
+        @Override
+        override fun getLoadingView(): RemoteViews {
+            val views = RemoteViews(mContext?.getPackageName(), R.layout.appwidget_loading)
+            return views
+        }
+
+        @Override
+        override fun getViewAt(position: Int): RemoteViews? {
+            // we use getCount here so that it doesn't return null when empty
+            if (position < 0 || position >= getCount()) {
+                return null
+            }
+            if (mModel == null) {
+                val views = RemoteViews(
+                    mContext?.getPackageName(),
+                    R.layout.appwidget_loading
+                )
+                val intent: Intent = CalendarAppWidgetProvider.getLaunchFillInIntent(
+                    mContext,
+                    0,
+                    0,
+                    0,
+                    false
+                )
+                views.setOnClickFillInIntent(R.id.appwidget_loading, intent)
+                return views
+            }
+            if (mModel!!.mEventInfos!!.isEmpty() || mModel!!.mRowInfos!!.isEmpty()) {
+                val views = RemoteViews(
+                    mContext?.getPackageName(),
+                    R.layout.appwidget_no_events
+                )
+                val intent: Intent = CalendarAppWidgetProvider.getLaunchFillInIntent(
+                    mContext,
+                    0,
+                    0,
+                    0,
+                    false
+                )
+                views.setOnClickFillInIntent(R.id.appwidget_no_events, intent)
+                return views
+            }
+            val rowInfo: RowInfo? = mModel?.mRowInfos?.get(position)
+            return if (rowInfo!!.mType == RowInfo!!.TYPE_DAY) {
+                val views = RemoteViews(
+                    mContext?.getPackageName(),
+                    R.layout.appwidget_day
+                )
+                val dayInfo: DayInfo? = mModel?.mDayInfos?.get(rowInfo!!.mIndex)
+                updateTextView(views, R.id.date, View.VISIBLE, dayInfo!!.mDayLabel)
+                views
+            } else {
+                val views: RemoteViews?
+                val eventInfo: EventInfo? = mModel?.mEventInfos?.get(rowInfo.mIndex)
+                if (eventInfo!!.allDay) {
+                    views = RemoteViews(
+                        mContext?.getPackageName(),
+                        R.layout.widget_all_day_item
+                    )
+                } else {
+                    views = RemoteViews(mContext?.getPackageName(), R.layout.widget_item)
+                }
+                val displayColor: Int = Utils.getDisplayColorFromColor(eventInfo!!.color)
+                val now: Long = System.currentTimeMillis()
+                if (!eventInfo!!.allDay && eventInfo!!.start <= now && now <= eventInfo!!.end) {
+                    views?.setInt(
+                        R.id.widget_row, "setBackgroundResource",
+                        R.drawable.agenda_item_bg_secondary
+                    )
+                } else {
+                    views?.setInt(
+                        R.id.widget_row, "setBackgroundResource",
+                        R.drawable.agenda_item_bg_primary
+                    )
+                }
+                if (!eventInfo?.allDay) {
+                    updateTextView(views, R.id.`when`, eventInfo?.visibWhen
+                        as Int, eventInfo?.`when`)
+                    updateTextView(views, R.id.where, eventInfo?.visibWhere
+                        as Int, eventInfo?.where)
+                }
+                updateTextView(views, R.id.title, eventInfo?.visibTitle as Int, eventInfo?.title)
+                views.setViewVisibility(R.id.agenda_item_color, View.VISIBLE)
+                val selfAttendeeStatus: Int = eventInfo?.selfAttendeeStatus as Int
+                if (eventInfo!!.allDay) {
+                    if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_INVITED) {
+                        views?.setInt(
+                            R.id.agenda_item_color, "setImageResource",
+                            R.drawable.widget_chip_not_responded_bg
+                        )
+                        views?.setInt(R.id.title, "setTextColor", displayColor)
+                    } else {
+                        views?.setInt(
+                            R.id.agenda_item_color, "setImageResource",
+                            R.drawable.widget_chip_responded_bg
+                        )
+                        views?.setInt(R.id.title, "setTextColor", mAllDayColor)
+                    }
+                    if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
+                        // 40% opacity
+                        views?.setInt(
+                            R.id.agenda_item_color, "setColorFilter",
+                            Utils.getDeclinedColorFromColor(displayColor)
+                        )
+                    } else {
+                        views?.setInt(R.id.agenda_item_color, "setColorFilter", displayColor)
+                    }
+                } else if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
+                    views?.setInt(R.id.title, "setTextColor", mDeclinedColor)
+                    views?.setInt(R.id.`when`, "setTextColor", mDeclinedColor)
+                    views?.setInt(R.id.where, "setTextColor", mDeclinedColor)
+                    views?.setInt(
+                        R.id.agenda_item_color, "setImageResource",
+                        R.drawable.widget_chip_responded_bg
+                    )
+                    // 40% opacity
+                    views?.setInt(
+                        R.id.agenda_item_color, "setColorFilter",
+                        Utils.getDeclinedColorFromColor(displayColor)
+                    )
+                } else {
+                    views?.setInt(R.id.title, "setTextColor", mStandardColor)
+                    views?.setInt(R.id.`when`, "setTextColor", mStandardColor)
+                    views?.setInt(R.id.where, "setTextColor", mStandardColor)
+                    if (selfAttendeeStatus == Attendees.ATTENDEE_STATUS_INVITED) {
+                        views?.setInt(
+                            R.id.agenda_item_color, "setImageResource",
+                            R.drawable.widget_chip_not_responded_bg
+                        )
+                    } else {
+                        views?.setInt(
+                            R.id.agenda_item_color, "setImageResource",
+                            R.drawable.widget_chip_responded_bg
+                        )
+                    }
+                    views?.setInt(R.id.agenda_item_color, "setColorFilter", displayColor)
+                }
+                var start: Long = eventInfo?.start as Long
+                var end: Long = eventInfo?.end as Long
+                // An element in ListView.
+                if (eventInfo!!.allDay) {
+                    val tz: String? = Utils.getTimeZone(mContext, null)
+                    val recycle = Time()
+                    start = Utils.convertAlldayLocalToUTC(recycle, start, tz as String)
+                    end = Utils.convertAlldayLocalToUTC(recycle, end, tz as String)
+                }
+                val fillInIntent: Intent = CalendarAppWidgetProvider.getLaunchFillInIntent(
+                    mContext, eventInfo?.id, start, end, eventInfo?.allDay
+                )
+                views.setOnClickFillInIntent(R.id.widget_row, fillInIntent)
+                views
+            }
+        }
+
+        @Override
+        override fun getViewTypeCount(): Int {
+            return 5
+        }
+
+        @Override
+        override fun getCount(): Int {
+            // if there are no events, we still return 1 to represent the "no
+            // events" view
+            if (mModel == null) {
+                return 1
+            }
+            return Math.max(1, mModel?.mRowInfos?.size as Int)
+        }
+
+        @Override
+        override fun getItemId(position: Int): Long {
+            if (mModel == null || mModel?.mRowInfos?.isEmpty() as Boolean ||
+                position >= getCount()) {
+                return 0
+            }
+            val rowInfo: RowInfo = mModel?.mRowInfos?.get(position) as RowInfo
+            if (rowInfo.mType == RowInfo.TYPE_DAY) {
+                return rowInfo.mIndex.toLong()
+            }
+            val eventInfo: EventInfo = mModel?.mEventInfos?.get(rowInfo.mIndex) as EventInfo
+            val prime: Long = 31
+            var result: Long = 1
+            result = prime * result + (eventInfo.id xor (eventInfo.id ushr 32)) as Int
+            result = prime * result + (eventInfo.start xor (eventInfo.start ushr 32)) as Int
+            return result
+        }
+
+        @Override
+        override fun hasStableIds(): Boolean {
+            return true
+        }
+
+        /**
+         * Query across all calendars for upcoming event instances from now
+         * until some time in the future. Widen the time range that we query by
+         * one day on each end so that we can catch all-day events. All-day
+         * events are stored starting at midnight in UTC but should be included
+         * in the list of events starting at midnight local time. This may fetch
+         * more events than we actually want, so we filter them out later.
+         *
+         * @param selection The selection string for the loader to filter the query with.
+         */
+        fun initLoader(selection: String?) {
+            if (LOGD) Log.d(TAG, "Querying for widget events...")
+
+            // Search for events from now until some time in the future
+            val uri: Uri = createLoaderUri()
+            mLoader = CursorLoader(
+                mContext, uri, EVENT_PROJECTION, selection, null,
+                EVENT_SORT_ORDER
+            )
+            mLoader?.setUpdateThrottle(WIDGET_UPDATE_THROTTLE.toLong())
+            synchronized(mLock) { mLastSerialNum = ++mSerialNum }
+            mLoader?.registerListener(mAppWidgetId, this)
+            mLoader?.startLoading()
+        }
+
+        /**
+         * This gets the selection string for the loader.  This ends up doing a query in the
+         * shared preferences.
+         */
+        private fun queryForSelection(): String {
+            return if (Utils.getHideDeclinedEvents(mContext)) EVENT_SELECTION_HIDE_DECLINED
+            else EVENT_SELECTION
+        }
+
+        /**
+         * @return The uri for the loader
+         */
+        private fun createLoaderUri(): Uri {
+            val now: Long = System.currentTimeMillis()
+            // Add a day on either side to catch all-day events
+            val begin: Long = now - DateUtils.DAY_IN_MILLIS
+            val end: Long =
+                now + SEARCH_DURATION + DateUtils.DAY_IN_MILLIS
+            return Uri.withAppendedPath(
+                Instances.CONTENT_URI,
+                begin.toString() + "/" + end
+            )
+        }
+
+        /**
+         * Calculates and returns the next time we should push widget updates.
+         */
+        private fun calculateUpdateTime(
+            model: CalendarAppWidgetModel,
+            now: Long,
+            timeZone: String
+        ): Long {
+            // Make sure an update happens at midnight or earlier
+            var minUpdateTime = getNextMidnightTimeMillis(timeZone)
+            for (event in model.mEventInfos) {
+                val start: Long
+                val end: Long
+                start = event.start
+                end = event.end
+
+                // We want to update widget when we enter/exit time range of an event.
+                if (now < start) {
+                    minUpdateTime = Math.min(minUpdateTime, start)
+                } else if (now < end) {
+                    minUpdateTime = Math.min(minUpdateTime, end)
+                }
+            }
+            return minUpdateTime
+        }
+
+        /*
+         * (non-Javadoc)
+         * @see
+         * android.content.Loader.OnLoadCompleteListener#onLoadComplete(android
+         * .content.Loader, java.lang.Object)
+         */
+        @Override
+        override fun onLoadComplete(loader: Loader<Cursor?>?, cursor: Cursor?) {
+            if (cursor == null) {
+                return
+            }
+            // If a newer update has happened since we started clean up and
+            // return
+            synchronized(mLock) {
+                if (cursor.isClosed()) {
+                    Log.wtf(TAG, "Got a closed cursor from onLoadComplete")
+                    return
+                }
+                if (mLastSerialNum != mSerialNum) {
+                    return
+                }
+                val now: Long = System.currentTimeMillis()
+                val tz: String? = Utils.getTimeZone(mContext, mTimezoneChanged)
+
+                // Copy it to a local static cursor.
+                val matrixCursor: MatrixCursor? = Utils.matrixCursorFromCursor(cursor)
+                try {
+                    mModel = buildAppWidgetModel(mContext, matrixCursor, tz)
+                } finally {
+                    if (matrixCursor != null) {
+                        matrixCursor?.close()
+                    }
+                    if (cursor != null) {
+                        cursor?.close()
+                    }
+                }
+
+                // Schedule an alarm to wake ourselves up for the next update.
+                // We also cancel
+                // all existing wake-ups because PendingIntents don't match
+                // against extras.
+                var triggerTime = calculateUpdateTime(mModel as CalendarAppWidgetModel,
+                    now, tz as String)
+
+                // If no next-update calculated, or bad trigger time in past,
+                // schedule
+                // update about six hours from now.
+                if (triggerTime < now) {
+                    Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now))
+                    triggerTime = now + UPDATE_TIME_NO_EVENTS
+                }
+                val alertManager: AlarmManager = mContext
+                    ?.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+                val pendingUpdate: PendingIntent = CalendarAppWidgetProvider
+                    .getUpdateIntent(mContext)
+                alertManager.cancel(pendingUpdate)
+                alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate)
+                val time = Time(Utils.getTimeZone(mContext, null))
+                time.setToNow()
+                if (time.normalize(true) !== sLastUpdateTime) {
+                    val time2 = Time(Utils.getTimeZone(mContext, null))
+                    time2.set(sLastUpdateTime)
+                    time2.normalize(true)
+                    if (time.year !== time2.year || time.yearDay !== time2.yearDay) {
+                        val updateIntent = Intent(
+                            Utils.getWidgetUpdateAction(mContext as Context)
+                        )
+                        mContext?.sendBroadcast(updateIntent)
+                    }
+                    sLastUpdateTime = time.toMillis(true)
+                }
+                val widgetManager: AppWidgetManager = AppWidgetManager.getInstance(mContext)
+                if (widgetManager == null) {
+                    return
+                }
+                if (mAppWidgetId == -1) {
+                    val ids: IntArray = widgetManager.getAppWidgetIds(
+                        CalendarAppWidgetProvider
+                            .getComponentName(mContext)
+                    )
+                    widgetManager.notifyAppWidgetViewDataChanged(ids, R.id.events_list)
+                } else {
+                    widgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.events_list)
+                }
+            }
+        }
+
+        @Override
+        override fun onReceive(context: Context?, intent: Intent) {
+            if (LOGD) Log.d(TAG, "AppWidgetService received an intent. It was " + intent.toString())
+            mContext = context
+
+            // We cannot do any queries from the UI thread, so push the 'selection' query
+            // to a background thread.  However the implementation of the latter query
+            // (cursor loading) uses CursorLoader which must be initiated from the UI thread,
+            // so there is some convoluted handshaking here.
+            //
+            // Note that as currently implemented, this must run in a single threaded executor
+            // or else the loads may be run out of order.
+            //
+            // TODO: Remove use of mHandler and CursorLoader, and do all the work synchronously
+            // in the background thread.  All the handshaking going on here between the UI and
+            // background thread with using goAsync, mHandler, and CursorLoader is confusing.
+            val result: PendingResult = goAsync()
+            executor.submit(object : Runnable {
+                @Override
+                override fun run() {
+                    // We always complete queryForSelection() even if the load task ends up being
+                    // canceled because of a more recent one.  Optimizing this to allow
+                    // canceling would require keeping track of all the PendingResults
+                    // (from goAsync) to abort them.  Defer this until it becomes a problem.
+                    val selection = queryForSelection()
+                    if (mLoader == null) {
+                        mAppWidgetId = -1
+                        mHandler.post(object : Runnable {
+                            @Override
+                            override fun run() {
+                                initLoader(selection)
+                                result.finish()
+                            }
+                        })
+                    } else {
+                        mHandler.post(
+                            createUpdateLoaderRunnable(
+                                selection, result,
+                                currentVersion.incrementAndGet()
+                            )
+                        )
+                    }
+                }
+            })
+        }
+
+        internal companion object {
+            private const val LOGD = false
+
+            // Suppress unnecessary logging about update time. Need to be static as this object is
+            // re-instantiated frequently.
+            // TODO: It seems loadData() is called via onCreate() four times, which should mean
+            // unnecessary CalendarFactory object is created and dropped. It is not efficient.
+            private var sLastUpdateTime = UPDATE_TIME_NO_EVENTS
+            private var mModel: CalendarAppWidgetModel? = null
+            private val mLock: Object = Object()
+
+            @Volatile
+            private var mSerialNum = 0
+            private val currentVersion: AtomicInteger = AtomicInteger(0)
+
+            /* @VisibleForTesting */
+            @JvmStatic protected fun buildAppWidgetModel(
+                context: Context?,
+                cursor: Cursor?,
+                timeZone: String?
+            ): CalendarAppWidgetModel {
+                val model = CalendarAppWidgetModel(context as Context, timeZone)
+                model.buildFromCursor(cursor as Cursor, timeZone)
+                return model
+            }
+
+            @JvmStatic private fun getNextMidnightTimeMillis(timezone: String): Long {
+                val time = Time()
+                time.setToNow()
+                time.monthDay++
+                time.hour = 0
+                time.minute = 0
+                time.second = 0
+                val midnightDeviceTz: Long = time.normalize(true)
+                time.timezone = timezone
+                time.setToNow()
+                time.monthDay++
+                time.hour = 0
+                time.minute = 0
+                time.second = 0
+                val midnightHomeTz: Long = time.normalize(true)
+                return Math.min(midnightDeviceTz, midnightHomeTz)
+            }
+
+            @JvmStatic fun updateTextView(
+                views: RemoteViews,
+                id: Int,
+                visibility: Int,
+                string: String?
+            ) {
+                views.setViewVisibility(id, visibility)
+                if (visibility == View.VISIBLE) {
+                    views.setTextViewText(id, string)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file