summaryrefslogtreecommitdiff
path: root/src/com/android/calendar/widget/CalendarAppWidgetService.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/calendar/widget/CalendarAppWidgetService.kt')
-rw-r--r--src/com/android/calendar/widget/CalendarAppWidgetService.kt665
1 files changed, 665 insertions, 0 deletions
diff --git a/src/com/android/calendar/widget/CalendarAppWidgetService.kt b/src/com/android/calendar/widget/CalendarAppWidgetService.kt
new file mode 100644
index 00000000..114fdf12
--- /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