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
17 
18 import android.app.Activity
19 import android.content.ComponentName
20 import android.content.ContentResolver
21 import android.content.Context
22 import android.content.Intent
23 import android.content.SharedPreferences
24 import android.content.pm.PackageManager
25 import android.content.res.Resources
26 import android.database.Cursor
27 import android.database.MatrixCursor
28 import android.graphics.Color
29 import android.graphics.drawable.Drawable
30 import android.graphics.drawable.LayerDrawable
31 import android.net.Uri
32 import android.os.Build
33 import android.os.Bundle
34 import android.os.Handler
35 import android.provider.CalendarContract.Calendars
36 import android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME
37 import android.text.TextUtils
38 import android.text.format.DateFormat
39 import android.text.format.DateUtils
40 import android.text.format.Time
41 import android.util.Log
42 import com.android.calendar.CalendarController.ViewType
43 import com.android.calendar.CalendarUtils.TimeZoneUtils
44 import java.util.ArrayList
45 import java.util.Arrays
46 import java.util.Calendar
47 import java.util.Formatter
48 import java.util.HashMap
49 import java.util.LinkedHashSet
50 import java.util.LinkedList
51 import java.util.List
52 import java.util.Locale
53 import java.util.TimeZone
54 import java.util.regex.Pattern
55 
56 object Utils {
57     private const val DEBUG = false
58     private const val TAG = "CalUtils"
59 
60     // Set to 0 until we have UI to perform undo
61     const val UNDO_DELAY: Long = 0
62 
63     // For recurring events which instances of the series are being modified
64     const val MODIFY_UNINITIALIZED = 0
65     const val MODIFY_SELECTED = 1
66     const val MODIFY_ALL_FOLLOWING = 2
67     const val MODIFY_ALL = 3
68 
69     // When the edit event view finishes it passes back the appropriate exit
70     // code.
71     const val DONE_REVERT = 1 shl 0
72     const val DONE_SAVE = 1 shl 1
73     const val DONE_DELETE = 1 shl 2
74 
75     // And should re run with DONE_EXIT if it should also leave the view, just
76     // exiting is identical to reverting
77     const val DONE_EXIT = 1 shl 0
78     const val OPEN_EMAIL_MARKER = " <"
79     const val CLOSE_EMAIL_MARKER = ">"
80     const val INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW"
81     const val INTENT_KEY_VIEW_TYPE = "VIEW"
82     const val INTENT_VALUE_VIEW_TYPE_DAY = "DAY"
83     const val INTENT_KEY_HOME = "KEY_HOME"
84     val MONDAY_BEFORE_JULIAN_EPOCH: Int = Time.EPOCH_JULIAN_DAY - 3
85     const val DECLINED_EVENT_ALPHA = 0x66
86     const val DECLINED_EVENT_TEXT_ALPHA = 0xC0
87     private const val SATURATION_ADJUST = 1.3f
88     private const val INTENSITY_ADJUST = 0.8f
89 
90     // Defines used by the DNA generation code
91     const val DAY_IN_MINUTES = 60 * 24
92     const val WEEK_IN_MINUTES = DAY_IN_MINUTES * 7
93 
94     // The work day is being counted as 6am to 8pm
95     var WORK_DAY_MINUTES = 14 * 60
96     var WORK_DAY_START_MINUTES = 6 * 60
97     var WORK_DAY_END_MINUTES = 20 * 60
98     var WORK_DAY_END_LENGTH = 24 * 60 - WORK_DAY_END_MINUTES
99     var CONFLICT_COLOR = -0x1000000
100     var mMinutesLoaded = false
101     const val YEAR_MIN = 1970
102     const val YEAR_MAX = 2036
103 
104     // The name of the shared preferences file. This name must be maintained for
105     // historical
106     // reasons, as it's what PreferenceManager assigned the first time the file
107     // was created.
108     const val SHARED_PREFS_NAME = "com.android.calendar_preferences"
109     const val KEY_QUICK_RESPONSES = "preferences_quick_responses"
110     const val KEY_ALERTS_VIBRATE_WHEN = "preferences_alerts_vibrateWhen"
111     const val APPWIDGET_DATA_TYPE = "vnd.android.data/update"
112     const val MACHINE_GENERATED_ADDRESS = "calendar.google.com"
113     private val mTZUtils: TimeZoneUtils? = TimeZoneUtils(SHARED_PREFS_NAME)
114     @JvmField var allowWeekForDetailView = false
115     internal var tardis: Long = 0
116         private set
117     private var sVersion: String? = null
118     private val mWildcardPattern: Pattern = Pattern.compile("^.*$")
119 
120     /**
121      * A coordinate must be of the following form for Google Maps to correctly use it:
122      * Latitude, Longitude
123      *
124      * This may be in decimal form:
125      * Latitude: {-90 to 90}
126      * Longitude: {-180 to 180}
127      *
128      * Or, in degrees, minutes, and seconds:
129      * Latitude: {-90 to 90}° {0 to 59}' {0 to 59}"
130      * Latitude: {-180 to 180}° {0 to 59}' {0 to 59}"
131      * + or - degrees may also be represented with N or n, S or s for latitude, and with
132      * E or e, W or w for longitude, where the direction may either precede or follow the value.
133      *
134      * Some examples of coordinates that will be accepted by the regex:
135      * 37.422081°, -122.084576°
136      * 37.422081,-122.084576
137      * +37°25'19.49", -122°5'4.47"
138      * 37°25'19.49"N, 122°5'4.47"W
139      * N 37° 25' 19.49",  W 122° 5' 4.47"
140      */
141     private const val COORD_DEGREES_LATITUDE = ("([-+NnSs]" + "(\\s)*)?" +
142         "[1-9]?[0-9](\u00B0)" + "(\\s)*" +
143         "([1-5]?[0-9]\')?" + "(\\s)*" +
144         "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" +
145         "((\\s)*" + "[NnSs])?")
146     private const val COORD_DEGREES_LONGITUDE = ("([-+EeWw]" + "(\\s)*)?" +
147         "(1)?[0-9]?[0-9](\u00B0)" + "(\\s)*" +
148         "([1-5]?[0-9]\')?" + "(\\s)*" +
149         "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?" +
150         "((\\s)*" + "[EeWw])?")
151     private const val COORD_DEGREES_PATTERN = (COORD_DEGREES_LATITUDE + "(\\s)*" + "," + "(\\s)*" +
152         COORD_DEGREES_LONGITUDE)
153     private const val COORD_DECIMAL_LATITUDE = ("[+-]?" +
154         "[1-9]?[0-9]" + "(\\.[0-9]+)" +
155         "(\u00B0)?")
156     private const val COORD_DECIMAL_LONGITUDE = ("[+-]?" +
157         "(1)?[0-9]?[0-9]" + "(\\.[0-9]+)" +
158         "(\u00B0)?")
159     private const val COORD_DECIMAL_PATTERN = (COORD_DECIMAL_LATITUDE + "(\\s)*" + "," + "(\\s)*" +
160         COORD_DECIMAL_LONGITUDE)
161     private val COORD_PATTERN: Pattern =
162         Pattern.compile(COORD_DEGREES_PATTERN + "|" + COORD_DECIMAL_PATTERN)
163     private const val NANP_ALLOWED_SYMBOLS = "()+-*#."
164     private const val NANP_MIN_DIGITS = 7
165     private const val NANP_MAX_DIGITS = 11
166 
167     /**
168      * Returns whether the SDK is the KeyLimePie release or later.
169      */
isKeyLimePieOrLaternull170     @JvmStatic fun isKeyLimePieOrLater(): Boolean {
171         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
172     }
173 
174     /**
175      * Returns whether the SDK is the Jellybean release or later.
176      */
isJellybeanOrLaternull177     @JvmStatic fun isJellybeanOrLater(): Boolean {
178         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
179     }
180 
getViewTypeFromIntentAndSharedPrefnull181     @JvmStatic fun getViewTypeFromIntentAndSharedPref(activity: Activity): Int {
182         val intent: Intent? = activity.getIntent()
183         val extras: Bundle? = intent?.getExtras()
184         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(activity)
185         if (TextUtils.equals(intent?.getAction(), Intent.ACTION_EDIT)) {
186             return ViewType.EDIT
187         }
188         if (extras != null) {
189             if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) {
190                 // This is the "detail" view which is either agenda or day view
191                 return prefs?.getInt(
192                     GeneralPreferences.KEY_DETAILED_VIEW,
193                     GeneralPreferences.DEFAULT_DETAILED_VIEW
194                 ) as Int
195             } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) {
196                 // Not sure who uses this. This logic came from LaunchActivity
197                 return ViewType.DAY
198             }
199         }
200 
201         // Default to the last view
202         return prefs?.getInt(
203             GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW
204         ) as Int
205     }
206 
207     /**
208      * Gets the intent action for telling the widget to update.
209      */
getWidgetUpdateActionnull210     @JvmStatic fun getWidgetUpdateAction(context: Context): String {
211         return context.getPackageName().toString() + ".APPWIDGET_UPDATE"
212     }
213 
214     /**
215      * Gets the intent action for telling the widget to update.
216      */
getWidgetScheduledUpdateActionnull217     @JvmStatic fun getWidgetScheduledUpdateAction(context: Context): String {
218         return context.getPackageName().toString() + ".APPWIDGET_SCHEDULED_UPDATE"
219     }
220 
221     /**
222      * Writes a new home time zone to the db. Updates the home time zone in the
223      * db asynchronously and updates the local cache. Sending a time zone of
224      * **tbd** will cause it to be set to the device's time zone. null or empty
225      * tz will be ignored.
226      *
227      * @param context The calling activity
228      * @param timeZone The time zone to set Calendar to, or **tbd**
229      */
setTimeZonenull230     @JvmStatic fun setTimeZone(context: Context?, timeZone: String?) {
231         mTZUtils?.setTimeZone(context as Context, timeZone as String)
232     }
233 
234     /**
235      * Gets the time zone that Calendar should be displayed in This is a helper
236      * method to get the appropriate time zone for Calendar. If this is the
237      * first time this method has been called it will initiate an asynchronous
238      * query to verify that the data in preferences is correct. The callback
239      * supplied will only be called if this query returns a value other than
240      * what is stored in preferences and should cause the calling activity to
241      * refresh anything that depends on calling this method.
242      *
243      * @param context The calling activity
244      * @param callback The runnable that should execute if a query returns new
245      * values
246      * @return The string value representing the time zone Calendar should
247      * display
248      */
getTimeZonenull249     @JvmStatic fun getTimeZone(context: Context?, callback: Runnable?): String? {
250         return mTZUtils?.getTimeZone(context as Context, callback)
251     }
252 
253     /**
254      * Formats a date or a time range according to the local conventions.
255      *
256      * @param context the context is required only if the time is shown
257      * @param startMillis the start time in UTC milliseconds
258      * @param endMillis the end time in UTC milliseconds
259      * @param flags a bit mask of options See [formatDateRange][DateUtils.formatDateRange]
260      * @return a string containing the formatted date/time range.
261      */
formatDateRangenull262     @JvmStatic fun formatDateRange(
263         context: Context?,
264         startMillis: Long,
265         endMillis: Long,
266         flags: Int
267     ): String? {
268         return mTZUtils?.formatDateRange(context as Context, startMillis, endMillis, flags)
269     }
270 
getDefaultVibratenull271     @JvmStatic fun getDefaultVibrate(context: Context, prefs: SharedPreferences?): Boolean {
272         val vibrate: Boolean
273         if (prefs?.contains(KEY_ALERTS_VIBRATE_WHEN) == true) {
274             // Migrate setting to new 4.2 behavior
275             //
276             // silent and never -> off
277             // always -> on
278             val vibrateWhen: String? = prefs.getString(KEY_ALERTS_VIBRATE_WHEN, null)
279             vibrate = vibrateWhen != null && vibrateWhen.equals(
280                 context
281                     .getString(R.string.prefDefault_alerts_vibrate_true)
282             )
283             prefs.edit().remove(KEY_ALERTS_VIBRATE_WHEN).commit()
284             Log.d(
285                 TAG, "Migrating KEY_ALERTS_VIBRATE_WHEN(" +
286                     vibrateWhen + ") to KEY_ALERTS_VIBRATE = " + vibrate
287             )
288         } else {
289             vibrate = prefs?.getBoolean(
290                 GeneralPreferences.KEY_ALERTS_VIBRATE,
291                 false
292             ) as Boolean
293         }
294         return vibrate
295     }
296 
getSharedPreferencenull297     @JvmStatic fun getSharedPreference(
298         context: Context?,
299         key: String?,
300         defaultValue: Array<String>?
301     ): Array<String>? {
302         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
303         val ss = prefs?.getStringSet(key, null)
304         if (ss != null) {
305             val strings = arrayOfNulls<String>(ss.size)
306             return ss.toTypedArray()
307         }
308         return defaultValue
309     }
310 
getSharedPreferencenull311     @JvmStatic fun getSharedPreference(
312         context: Context?,
313         key: String?,
314         defaultValue: String?
315     ): String? {
316         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
317         return prefs?.getString(key, defaultValue)
318     }
319 
getSharedPreferencenull320     @JvmStatic fun getSharedPreference(context: Context?, key: String?, defaultValue: Int): Int {
321         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
322         return prefs?.getInt(key, defaultValue) as Int
323     }
324 
getSharedPreferencenull325     @JvmStatic fun getSharedPreference(
326         context: Context?,
327         key: String?,
328         defaultValue: Boolean
329     ): Boolean {
330         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
331         return prefs?.getBoolean(key, defaultValue) as Boolean
332     }
333 
334     /**
335      * Asynchronously sets the preference with the given key to the given value
336      *
337      * @param context the context to use to get preferences from
338      * @param key the key of the preference to set
339      * @param value the value to set
340      */
setSharedPreferencenull341     @JvmStatic fun setSharedPreference(context: Context?, key: String?, value: String?) {
342         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
343         prefs?.edit()?.putString(key, value)?.apply()
344     }
345 
setSharedPreferencenull346     @JvmStatic fun setSharedPreference(context: Context?, key: String?, values: Array<String?>) {
347         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
348         val set: LinkedHashSet<String?> = LinkedHashSet<String?>()
349         for (value in values) {
350             set.add(value)
351         }
352         prefs?.edit()?.putStringSet(key, set)?.apply()
353     }
354 
tardisnull355     internal fun tardis() {
356         tardis = System.currentTimeMillis()
357     }
358 
setSharedPreferencenull359     @JvmStatic fun setSharedPreference(context: Context?, key: String?, value: Boolean) {
360         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
361         val editor: SharedPreferences.Editor? = prefs?.edit()
362         editor?.putBoolean(key, value)
363         editor?.apply()
364     }
365 
setSharedPreferencenull366     @JvmStatic fun setSharedPreference(context: Context?, key: String?, value: Int) {
367         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
368         val editor: SharedPreferences.Editor? = prefs?.edit()
369         editor?.putInt(key, value)
370         editor?.apply()
371     }
372 
removeSharedPreferencenull373     @JvmStatic fun removeSharedPreference(context: Context?, key: String?) {
374         val prefs: SharedPreferences? = context?.getSharedPreferences(
375             GeneralPreferences.SHARED_PREFS_NAME, Context.MODE_PRIVATE
376         )
377         prefs?.edit()?.remove(key)?.apply()
378     }
379 
380     /**
381      * Save default agenda/day/week/month view for next time
382      *
383      * @param context
384      * @param viewId [CalendarController.ViewType]
385      */
setDefaultViewnull386     @JvmStatic fun setDefaultView(context: Context?, viewId: Int) {
387         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
388         val editor: SharedPreferences.Editor? = prefs?.edit()
389         var validDetailView = false
390         validDetailView =
391             if (allowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) {
392                 true
393             } else {
394                 (viewId == CalendarController.ViewType.AGENDA ||
395                     viewId == CalendarController.ViewType.DAY)
396             }
397         if (validDetailView) {
398             // Record the detail start view
399             editor?.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId)
400         }
401 
402         // Record the (new) start view
403         editor?.putInt(GeneralPreferences.KEY_START_VIEW, viewId)
404         editor?.apply()
405     }
406 
matrixCursorFromCursornull407     @JvmStatic fun matrixCursorFromCursor(cursor: Cursor?): MatrixCursor? {
408         if (cursor == null) {
409             return null
410         }
411         var columnNames: Array<String?> = cursor.getColumnNames()
412         if (columnNames == null) {
413             columnNames = arrayOf()
414         }
415         val newCursor = MatrixCursor(columnNames)
416         val numColumns: Int = cursor.getColumnCount()
417         val data = arrayOfNulls<String>(numColumns)
418         cursor.moveToPosition(-1)
419         while (cursor.moveToNext()) {
420             for (i in 0 until numColumns) {
421                 data[i] = cursor.getString(i)
422             }
423             newCursor.addRow(data)
424         }
425         return newCursor
426     }
427 
428     /**
429      * Compares two cursors to see if they contain the same data.
430      *
431      * @return Returns true of the cursors contain the same data and are not
432      * null, false otherwise
433      */
compareCursorsnull434     @JvmStatic fun compareCursors(c1: Cursor?, c2: Cursor?): Boolean {
435         if (c1 == null || c2 == null) {
436             return false
437         }
438         val numColumns: Int = c1.getColumnCount()
439         if (numColumns != c2.getColumnCount()) {
440             return false
441         }
442         if (c1.getCount() !== c2.getCount()) {
443             return false
444         }
445         c1.moveToPosition(-1)
446         c2.moveToPosition(-1)
447         while (c1.moveToNext() && c2.moveToNext()) {
448             for (i in 0 until numColumns) {
449                 if (!TextUtils.equals(c1.getString(i), c2.getString(i))) {
450                     return false
451                 }
452             }
453         }
454         return true
455     }
456 
457     /**
458      * If the given intent specifies a time (in milliseconds since the epoch),
459      * then that time is returned. Otherwise, the current time is returned.
460      */
timeFromIntentInMillisnull461     @JvmStatic fun timeFromIntentInMillis(intent: Intent?): Long? {
462         // If the time was specified, then use that. Otherwise, use the current
463         // time.
464         val data: Uri? = intent?.getData()
465         var millis: Long? = intent?.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1)?.toLong()
466         if (millis == -1L && data != null && data.isHierarchical()) {
467             val path: List<String> = data.getPathSegments() as List<String>
468             if (path.size == 2 && path[0].equals("time")) {
469                 try {
470                     millis = (data.getLastPathSegment()?.toLong())
471                 } catch (e: NumberFormatException) {
472                     Log.i(
473                         "Calendar", "timeFromIntentInMillis: Data existed but no valid time " +
474                             "found. Using current time."
475                     )
476                 }
477             }
478         }
479         if ((millis ?: 0L) <= 0) {
480             millis = System.currentTimeMillis()
481         }
482         return millis
483     }
484 
485     /**
486      * Formats the given Time object so that it gives the month and year (for
487      * example, "September 2007").
488      *
489      * @param time the time to format
490      * @return the string containing the weekday and the date
491      */
formatMonthYearnull492     @JvmStatic fun formatMonthYear(context: Context?, time: Time): String? {
493         val flags: Int = (DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_NO_MONTH_DAY
494             or DateUtils.FORMAT_SHOW_YEAR)
495         val millis: Long = time.toMillis(true)
496         return formatDateRange(context, millis, millis, flags)
497     }
498 
499     /**
500      * Returns a list joined together by the provided delimiter, for example,
501      * ["a", "b", "c"] could be joined into "a,b,c"
502      *
503      * @param things the things to join together
504      * @param delim the delimiter to use
505      * @return a string contained the things joined together
506      */
joinnull507     @JvmStatic fun join(things: List<*>, delim: String?): String {
508         val builder = StringBuilder()
509         var first = true
510         for (thing in things) {
511             if (first) {
512                 first = false
513             } else {
514                 builder.append(delim)
515             }
516             builder.append(thing.toString())
517         }
518         return builder.toString()
519     }
520 
521     /**
522      * Returns the week since [Time.EPOCH_JULIAN_DAY] (Jan 1, 1970)
523      * adjusted for first day of week.
524      *
525      * This takes a julian day and the week start day and calculates which
526      * week since [Time.EPOCH_JULIAN_DAY] that day occurs in, starting
527      * at 0. *Do not* use this to compute the ISO week number for the year.
528      *
529      * @param julianDay The julian day to calculate the week number for
530      * @param firstDayOfWeek Which week day is the first day of the week,
531      * see [Time.SUNDAY]
532      * @return Weeks since the epoch
533      */
getWeeksSinceEpochFromJulianDaynull534     @JvmStatic fun getWeeksSinceEpochFromJulianDay(julianDay: Int, firstDayOfWeek: Int): Int {
535         var diff: Int = Time.THURSDAY - firstDayOfWeek
536         if (diff < 0) {
537             diff += 7
538         }
539         val refDay: Int = Time.EPOCH_JULIAN_DAY - diff
540         return (julianDay - refDay) / 7
541     }
542 
543     /**
544      * Takes a number of weeks since the epoch and calculates the Julian day of
545      * the Monday for that week.
546      *
547      * This assumes that the week containing the [Time.EPOCH_JULIAN_DAY]
548      * is considered week 0. It returns the Julian day for the Monday
549      * `week` weeks after the Monday of the week containing the epoch.
550      *
551      * @param week Number of weeks since the epoch
552      * @return The julian day for the Monday of the given week since the epoch
553      */
getJulianMondayFromWeeksSinceEpochnull554     @JvmStatic fun getJulianMondayFromWeeksSinceEpoch(week: Int): Int {
555         return MONDAY_BEFORE_JULIAN_EPOCH + week * 7
556     }
557 
558     /**
559      * Get first day of week as android.text.format.Time constant.
560      *
561      * @return the first day of week in android.text.format.Time
562      */
getFirstDayOfWeeknull563     @JvmStatic fun getFirstDayOfWeek(context: Context?): Int {
564         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
565         val pref: String? = prefs?.getString(
566             GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT
567         )
568         val startDay: Int
569         startDay = if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) {
570             Calendar.getInstance().getFirstDayOfWeek()
571         } else {
572             Integer.parseInt(pref)
573         }
574         return if (startDay == Calendar.SATURDAY) {
575             Time.SATURDAY
576         } else if (startDay == Calendar.MONDAY) {
577             Time.MONDAY
578         } else {
579             Time.SUNDAY
580         }
581     }
582 
583     /**
584      * Get first day of week as java.util.Calendar constant.
585      *
586      * @return the first day of week as a java.util.Calendar constant
587      */
getFirstDayOfWeekAsCalendarnull588     @JvmStatic fun getFirstDayOfWeekAsCalendar(context: Context?): Int {
589         return convertDayOfWeekFromTimeToCalendar(getFirstDayOfWeek(context))
590     }
591 
592     /**
593      * Converts the day of the week from android.text.format.Time to java.util.Calendar
594      */
convertDayOfWeekFromTimeToCalendarnull595     @JvmStatic fun convertDayOfWeekFromTimeToCalendar(timeDayOfWeek: Int): Int {
596         return when (timeDayOfWeek) {
597             Time.MONDAY -> Calendar.MONDAY
598             Time.TUESDAY -> Calendar.TUESDAY
599             Time.WEDNESDAY -> Calendar.WEDNESDAY
600             Time.THURSDAY -> Calendar.THURSDAY
601             Time.FRIDAY -> Calendar.FRIDAY
602             Time.SATURDAY -> Calendar.SATURDAY
603             Time.SUNDAY -> Calendar.SUNDAY
604             else -> throw IllegalArgumentException(
605                 "Argument must be between Time.SUNDAY and " +
606                     "Time.SATURDAY"
607             )
608         }
609     }
610 
611     /**
612      * @return true when week number should be shown.
613      */
getShowWeekNumbernull614     @JvmStatic fun getShowWeekNumber(context: Context?): Boolean {
615         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
616         return prefs?.getBoolean(
617             GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM
618         ) as Boolean
619     }
620 
621     /**
622      * @return true when declined events should be hidden.
623      */
getHideDeclinedEventsnull624     @JvmStatic fun getHideDeclinedEvents(context: Context?): Boolean {
625         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
626         return prefs?.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false) as Boolean
627     }
628 
getDaysPerWeeknull629     @JvmStatic fun getDaysPerWeek(context: Context?): Int {
630         val prefs: SharedPreferences? = GeneralPreferences.getSharedPreferences(context)
631         return prefs?.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7) as Int
632     }
633 
634     /**
635      * Determine whether the column position is Saturday or not.
636      *
637      * @param column the column position
638      * @param firstDayOfWeek the first day of week in android.text.format.Time
639      * @return true if the column is Saturday position
640      */
isSaturdaynull641     @JvmStatic fun isSaturday(column: Int, firstDayOfWeek: Int): Boolean {
642         return (firstDayOfWeek == Time.SUNDAY && column == 6 ||
643             firstDayOfWeek == Time.MONDAY && column == 5 ||
644             firstDayOfWeek == Time.SATURDAY && column == 0)
645     }
646 
647     /**
648      * Determine whether the column position is Sunday or not.
649      *
650      * @param column the column position
651      * @param firstDayOfWeek the first day of week in android.text.format.Time
652      * @return true if the column is Sunday position
653      */
isSundaynull654     @JvmStatic fun isSunday(column: Int, firstDayOfWeek: Int): Boolean {
655         return (firstDayOfWeek == Time.SUNDAY && column == 0 ||
656             firstDayOfWeek == Time.MONDAY && column == 6 ||
657             firstDayOfWeek == Time.SATURDAY && column == 1)
658     }
659 
660     /**
661      * Convert given UTC time into current local time. This assumes it is for an
662      * allday event and will adjust the time to be on a midnight boundary.
663      *
664      * @param recycle Time object to recycle, otherwise null.
665      * @param utcTime Time to convert, in UTC.
666      * @param tz The time zone to convert this time to.
667      */
convertAlldayUtcToLocalnull668     @JvmStatic fun convertAlldayUtcToLocal(recycle: Time?, utcTime: Long, tz: String): Long {
669         var recycle: Time? = recycle
670         if (recycle == null) {
671             recycle = Time()
672         }
673         recycle.timezone = Time.TIMEZONE_UTC
674         recycle.set(utcTime)
675         recycle.timezone = tz
676         return recycle.normalize(true)
677     }
678 
convertAlldayLocalToUTCnull679     @JvmStatic fun convertAlldayLocalToUTC(recycle: Time?, localTime: Long, tz: String): Long {
680         var recycle: Time? = recycle
681         if (recycle == null) {
682             recycle = Time()
683         }
684         recycle.timezone = tz
685         recycle.set(localTime)
686         recycle.timezone = Time.TIMEZONE_UTC
687         return recycle.normalize(true)
688     }
689 
690     /**
691      * Finds and returns the next midnight after "theTime" in milliseconds UTC
692      *
693      * @param recycle - Time object to recycle, otherwise null.
694      * @param theTime - Time used for calculations (in UTC)
695      * @param tz The time zone to convert this time to.
696      */
getNextMidnightnull697     @JvmStatic fun getNextMidnight(recycle: Time?, theTime: Long, tz: String): Long {
698         var recycle: Time? = recycle
699         if (recycle == null) {
700             recycle = Time()
701         }
702         recycle.timezone = tz
703         recycle.set(theTime)
704         recycle.monthDay++
705         recycle.hour = 0
706         recycle.minute = 0
707         recycle.second = 0
708         return recycle.normalize(true)
709     }
710 
setAllowWeekForDetailViewnull711     @JvmStatic fun setAllowWeekForDetailView(allowWeekView: Boolean) {
712         this.allowWeekForDetailView = allowWeekView
713     }
714 
getAllowWeekForDetailViewnull715     @JvmStatic fun getAllowWeekForDetailView(): Boolean {
716         return this.allowWeekForDetailView
717     }
718 
getConfigBoolnull719     @JvmStatic fun getConfigBool(c: Context, key: Int): Boolean {
720         return c.getResources().getBoolean(key)
721     }
722 
723     /**
724      * For devices with Jellybean or later, darkens the given color to ensure that white text is
725      * clearly visible on top of it.  For devices prior to Jellybean, does nothing, as the
726      * sync adapter handles the color change.
727      *
728      * @param color
729      */
getDisplayColorFromColornull730     @JvmStatic fun getDisplayColorFromColor(color: Int): Int {
731         if (!isJellybeanOrLater()) {
732             return color
733         }
734         val hsv = FloatArray(3)
735         Color.colorToHSV(color, hsv)
736         hsv[1] = Math.min(hsv[1] * SATURATION_ADJUST, 1.0f)
737         hsv[2] = hsv[2] * INTENSITY_ADJUST
738         return Color.HSVToColor(hsv)
739     }
740 
741     // This takes a color and computes what it would look like blended with
742     // white. The result is the color that should be used for declined events.
getDeclinedColorFromColornull743     @JvmStatic fun getDeclinedColorFromColor(color: Int): Int {
744         val bg = -0x1
745         val a = DECLINED_EVENT_ALPHA
746         val r = (color and 0x00ff0000) * a + (bg and 0x00ff0000) * (0xff - a) and -0x1000000
747         val g = (color and 0x0000ff00) * a + (bg and 0x0000ff00) * (0xff - a) and 0x00ff0000
748         val b = (color and 0x000000ff) * a + (bg and 0x000000ff) * (0xff - a) and 0x0000ff00
749         return -0x1000000 or (r or g or b shr 8)
750     }
751 
trySyncAndDisableUpgradeReceivernull752     @JvmStatic fun trySyncAndDisableUpgradeReceiver(context: Context?) {
753         val pm: PackageManager? = context?.getPackageManager()
754         val upgradeComponent = ComponentName(context as Context, UpgradeReceiver::class.java)
755         if (pm?.getComponentEnabledSetting(upgradeComponent) ===
756             PackageManager.COMPONENT_ENABLED_STATE_DISABLED
757         ) {
758             // The upgrade receiver has been disabled, which means this code has been run before,
759             // so no need to sync.
760             return
761         }
762         val extras = Bundle()
763         extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
764         ContentResolver.requestSync(
765             null /* no account */,
766             Calendars.CONTENT_URI.getAuthority(),
767             extras
768         )
769 
770         // Now unregister the receiver so that we won't continue to sync every time.
771         pm?.setComponentEnabledSetting(
772             upgradeComponent,
773             PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
774         )
775     }
776 
777     /**
778      * Converts a list of events to a list of segments to draw. Assumes list is
779      * ordered by start time of the events. The function processes events for a
780      * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1.
781      * The algorithm goes over all the events and creates a set of segments
782      * ordered by start time. This list of segments is then converted into a
783      * HashMap of strands which contain the draw points and are organized by
784      * color. The strands can then be drawn by setting the paint color to each
785      * strand's color and calling drawLines on its set of points. The points are
786      * set up using the following parameters.
787      *
788      *  * Events between midnight and WORK_DAY_START_MINUTES are compressed
789      * into the first 1/8th of the space between top and bottom.
790      *  * Events between WORK_DAY_END_MINUTES and the following midnight are
791      * compressed into the last 1/8th of the space between top and bottom
792      *  * Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use
793      * the remaining 3/4ths of the space
794      *  * All segments drawn will maintain at least minPixels height, except
795      * for conflicts in the first or last 1/8th, which may be smaller
796      *
797      *
798      * @param firstJulianDay The julian day of the first day of events
799      * @param events A list of events sorted by start time
800      * @param top The lowest y value the dna should be drawn at
801      * @param bottom The highest y value the dna should be drawn at
802      * @param dayXs An array of x values to draw the dna at, one for each day
803      * @param conflictColor the color to use for conflicts
804      * @return
805      */
createDNAStrandsnull806     @JvmStatic fun createDNAStrands(
807         firstJulianDay: Int,
808         events: ArrayList<Event?>?,
809         top: Int,
810         bottom: Int,
811         minPixels: Int,
812         dayXs: IntArray?,
813         context: Context?
814     ): HashMap<Int, DNAStrand>? {
815         if (!mMinutesLoaded) {
816             if (context == null) {
817                 Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA.")
818             }
819             val res: Resources? = context?.getResources()
820             CONFLICT_COLOR = res?.getColor(R.color.month_dna_conflict_time_color) as Int
821             WORK_DAY_START_MINUTES = res.getInteger(R.integer.work_start_minutes) as Int
822             WORK_DAY_END_MINUTES = res.getInteger(R.integer.work_end_minutes) as Int
823             WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES
824             WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES
825             mMinutesLoaded = true
826         }
827         if (events == null || events.isEmpty() || dayXs == null || dayXs.size < 1 ||
828             bottom - top < 8 || minPixels < 0) {
829             Log.e(
830                 TAG,
831                 "Bad values for createDNAStrands! events:" + events + " dayXs:" +
832                     Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:" +
833                     minPixels
834             )
835             return null
836         }
837         val segments: LinkedList<DNASegment> = LinkedList<DNASegment>()
838         val strands: HashMap<Int, DNAStrand> = HashMap<Int, DNAStrand>()
839         // add a black strand by default, other colors will get added in
840         // the loop
841         val blackStrand = DNAStrand()
842         blackStrand.color = CONFLICT_COLOR
843         strands.put(CONFLICT_COLOR, blackStrand)
844         // the min length is the number of minutes that will occupy
845         // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the
846         // minutes/pixel * minpx where the number of pixels are 3/4 the total
847         // dna height: 4*(mins/(px * 3/4))
848         val minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top))
849 
850         // There are slightly fewer than half as many pixels in 1/6 the space,
851         // so round to 2.5x for the min minutes in the non-work area
852         val minOtherMinutes = minMinutes * 5 / 2
853         val lastJulianDay = firstJulianDay + dayXs.size - 1
854         val event = Event()
855         // Go through all the events for the week
856         for (currEvent in events) {
857             // if this event is outside the weeks range skip it
858             if (currEvent != null &&
859                 (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay)) {
860                 continue
861             }
862             if (currEvent?.drawAsAllday() == true) {
863                 addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.size)
864                 continue
865             }
866             // Copy the event over so we can clip its start and end to our range
867             currEvent?.copyTo(event)
868             if (event.startDay < firstJulianDay) {
869                 event.startDay = firstJulianDay
870                 event.startTime = 0
871             }
872             // If it starts after the work day make sure the start is at least
873             // minPixels from midnight
874             if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) {
875                 event.startTime = DAY_IN_MINUTES - minOtherMinutes
876             }
877             if (event.endDay > lastJulianDay) {
878                 event.endDay = lastJulianDay
879                 event.endTime = DAY_IN_MINUTES - 1
880             }
881             // If the end time is before the work day make sure it ends at least
882             // minPixels after midnight
883             if (event.endTime < minOtherMinutes) {
884                 event.endTime = minOtherMinutes
885             }
886             // If the start and end are on the same day make sure they are at
887             // least minPixels apart. This only needs to be done for times
888             // outside the work day as the min distance for within the work day
889             // is enforced in the segment code.
890             if (event.startDay === event.endDay &&
891                 event.endTime - event.startTime < minOtherMinutes
892             ) {
893                 // If it's less than minPixels in an area before the work
894                 // day
895                 if (event.startTime < WORK_DAY_START_MINUTES) {
896                     // extend the end to the first easy guarantee that it's
897                     // minPixels
898                     event.endTime = Math.min(
899                         event.startTime + minOtherMinutes,
900                         WORK_DAY_START_MINUTES + minMinutes
901                     )
902                     // if it's in the area after the work day
903                 } else if (event.endTime > WORK_DAY_END_MINUTES) {
904                     // First try shifting the end but not past midnight
905                     event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1)
906                     // if it's still too small move the start back
907                     if (event.endTime - event.startTime < minOtherMinutes) {
908                         event.startTime = event.endTime - minOtherMinutes
909                     }
910                 }
911             }
912 
913             // This handles adding the first segment
914             if (segments.size == 0) {
915                 addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes)
916                 continue
917             }
918             // Now compare our current start time to the end time of the last
919             // segment in the list
920             val lastSegment: DNASegment = segments.getLast()
921             var startMinute: Int =
922                 (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime
923             var endMinute: Int = Math.max(
924                 (event.endDay - firstJulianDay) * DAY_IN_MINUTES +
925                     event.endTime, startMinute + minMinutes
926             )
927             if (startMinute < 0) {
928                 startMinute = 0
929             }
930             if (endMinute >= WEEK_IN_MINUTES) {
931                 endMinute = WEEK_IN_MINUTES - 1
932             }
933             // If we start before the last segment in the list ends we need to
934             // start going through the list as this may conflict with other
935             // events
936             if (startMinute < lastSegment.endMinute) {
937                 var i: Int = segments.size
938                 // find the last segment this event intersects with
939                 while (--i >= 0 && endMinute < segments.get(i).startMinute) {}
940 
941                 var currSegment: DNASegment = DNASegment()
942                 // for each segment this event intersects with
943                 while (i >= 0 && startMinute <= segments.get(i)
944                         .also { currSegment = it }.endMinute) {
945 
946                     // if the segment is already a conflict ignore it
947                     if (currSegment.color == CONFLICT_COLOR) {
948                         i--
949                         continue
950                     }
951                     // if the event ends before the segment and wouldn't create
952                     // a segment that is too small split off the right side
953                     if (endMinute < currSegment.endMinute - minMinutes) {
954                         val rhs = DNASegment()
955                         rhs.endMinute = currSegment.endMinute
956                         rhs.color = currSegment.color
957                         rhs.startMinute = endMinute + 1
958                         rhs.day = currSegment.day
959                         currSegment.endMinute = endMinute
960                         segments.add(i + 1, rhs)
961                         // Equivalent to strands.get(rhs.color)?.count++
962                         // but there is no null safe invocation for ++
963                         strands.get(rhs.color)?.count = strands.get(rhs.color)?.count?.inc() as Int
964                         if (DEBUG) {
965                             Log.d(
966                                 TAG, "Added rhs, curr:" + currSegment.toString() + " i:" +
967                                     segments.get(i).toString()
968                             )
969                         }
970                     }
971                     // if the event starts after the segment and wouldn't create
972                     // a segment that is too small split off the left side
973                     if (startMinute > currSegment.startMinute + minMinutes) {
974                         val lhs = DNASegment()
975                         lhs.startMinute = currSegment.startMinute
976                         lhs.color = currSegment.color
977                         lhs.endMinute = startMinute - 1
978                         lhs.day = currSegment.day
979                         currSegment.startMinute = startMinute
980                         // increment i so that we are at the right position when
981                         // referencing the segments to the right and left of the
982                         // current segment.
983                         segments.add(i++, lhs)
984                         strands.get(lhs.color)?.count = strands.get(lhs.color)?.count?.inc() as Int
985                         if (DEBUG) {
986                             Log.d(
987                                 TAG, "Added lhs, curr:" + currSegment.toString() + " i:" +
988                                     segments.get(i).toString()
989                             )
990                         }
991                     }
992                     // if the right side is black merge this with the segment to
993                     // the right if they're on the same day and overlap
994                     if (i + 1 < segments.size) {
995                         val rhs: DNASegment = segments.get(i + 1)
996                         if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day &&
997                             rhs.startMinute <= currSegment.endMinute + 1) {
998                             rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute)
999                             segments.remove(currSegment)
1000                             strands.get(currSegment.color)?.count =
1001                                 strands.get(currSegment.color)?.count?.dec() as Int
1002                             // point at the new current segment
1003                             currSegment = rhs
1004                         }
1005                     }
1006                     // if the left side is black merge this with the segment to
1007                     // the left if they're on the same day and overlap
1008                     if (i - 1 >= 0) {
1009                         val lhs: DNASegment = segments.get(i - 1)
1010                         if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day &&
1011                             lhs.endMinute >= currSegment.startMinute - 1) {
1012                             lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute)
1013                             segments.remove(currSegment)
1014                             strands.get(currSegment.color)?.count =
1015                                 strands.get(currSegment.color)?.count?.dec() as Int
1016                             // point at the new current segment
1017                             currSegment = lhs
1018                             // point i at the new current segment in case new
1019                             // code is added
1020                             i--
1021                         }
1022                     }
1023                     // if we're still not black, decrement the count for the
1024                     // color being removed, change this to black, and increment
1025                     // the black count
1026                     if (currSegment.color != CONFLICT_COLOR) {
1027                         strands.get(currSegment.color)?.count =
1028                             strands.get(currSegment.color)?.count?.dec() as Int
1029                         currSegment.color = CONFLICT_COLOR
1030                         strands.get(CONFLICT_COLOR)?.count =
1031                             strands.get(CONFLICT_COLOR)?.count?.inc() as Int
1032                     }
1033                     i--
1034                 }
1035             }
1036             // If this event extends beyond the last segment add a new segment
1037             if (endMinute > lastSegment.endMinute) {
1038                 addNewSegment(
1039                     segments, event, strands, firstJulianDay, lastSegment.endMinute,
1040                     minMinutes
1041                 )
1042             }
1043         }
1044         weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs)
1045         return strands
1046     }
1047 
1048     // This figures out allDay colors as allDay events are found
addAllDayToStrandsnull1049     private fun addAllDayToStrands(
1050         event: Event?,
1051         strands: HashMap<Int, DNAStrand>,
1052         firstJulianDay: Int,
1053         numDays: Int
1054     ) {
1055         val strand = getOrCreateStrand(strands, CONFLICT_COLOR)
1056         // if we haven't initialized the allDay portion create it now
1057         if (strand?.allDays == null) {
1058             strand?.allDays = IntArray(numDays)
1059         }
1060 
1061         // For each day this event is on update the color
1062         val end: Int = Math.min((event?.endDay ?: 0) - firstJulianDay, numDays - 1)
1063         for (i in Math.max((event?.startDay ?: 0) - firstJulianDay, 0)..end) {
1064             if (strand?.allDays!![i] != 0) {
1065                 // if this day already had a color, it is now a conflict
1066                 strand?.allDays!![i] = CONFLICT_COLOR
1067             } else {
1068                 // else it's just the color of the event
1069                 strand?.allDays!![i] = event?.color as Int
1070             }
1071         }
1072     }
1073 
1074     // This processes all the segments, sorts them by color, and generates a
1075     // list of points to draw
weaveDNAStrandsnull1076     private fun weaveDNAStrands(
1077         segments: LinkedList<DNASegment>,
1078         firstJulianDay: Int,
1079         strands: HashMap<Int, DNAStrand>,
1080         top: Int,
1081         bottom: Int,
1082         dayXs: IntArray
1083     ) {
1084         // First, get rid of any colors that ended up with no segments
1085         val strandIterator = strands.values.iterator()
1086         while (strandIterator.hasNext()) {
1087             val strand = strandIterator.next()
1088             if (strand.count < 1 && strand.allDays == null) {
1089                 strandIterator.remove()
1090                 continue
1091             }
1092             strand.points = FloatArray(strand.count * 4)
1093             strand.position = 0
1094         }
1095         // Go through each segment and compute its points
1096         for (segment in segments) {
1097             // Add the points to the strand of that color
1098             val strand: DNAStrand? = strands.get(segment.color)
1099             val dayIndex = segment.day - firstJulianDay
1100             val dayStartMinute = segment.startMinute % DAY_IN_MINUTES
1101             val dayEndMinute = segment.endMinute % DAY_IN_MINUTES
1102             val height = bottom - top
1103             val workDayHeight = height * 3 / 4
1104             val remainderHeight = (height - workDayHeight) / 2
1105             val x = dayXs[dayIndex]
1106             var y0 = 0
1107             var y1 = 0
1108             y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight)
1109             y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight)
1110             if (DEBUG) {
1111                 Log.d(
1112                     TAG,
1113                     "Adding " + Integer.toHexString(segment.color).toString() + " at x,y0,y1: " + x
1114                         .toString() + " " + y0.toString() + " " + y1.toString() +
1115                         " for " + dayStartMinute.toString() + " " + dayEndMinute
1116                 )
1117             }
1118             strand?.points!![strand.position] = x.toFloat()
1119             strand.position = strand.position.inc() as Int
1120 
1121             strand.points!![strand.position] = y0.toFloat()
1122             strand.position = strand.position.inc() as Int
1123 
1124             strand.points!![strand.position] = x.toFloat()
1125             strand.position = strand.position.inc() as Int
1126 
1127             strand.points!![strand.position] = y1.toFloat()
1128             strand.position = strand.position.inc() as Int
1129         }
1130     }
1131 
1132     /**
1133      * Compute a pixel offset from the top for a given minute from the work day
1134      * height and the height of the top area.
1135      */
getPixelOffsetFromMinutesnull1136     private fun getPixelOffsetFromMinutes(
1137         minute: Int,
1138         workDayHeight: Int,
1139         remainderHeight: Int
1140     ): Int {
1141         val y: Int
1142         if (minute < WORK_DAY_START_MINUTES) {
1143             y = minute * remainderHeight / WORK_DAY_START_MINUTES
1144         } else if (minute < WORK_DAY_END_MINUTES) {
1145             y = remainderHeight + (minute - WORK_DAY_START_MINUTES) *
1146                 workDayHeight / WORK_DAY_MINUTES
1147         } else {
1148             y = remainderHeight + workDayHeight +
1149                 (minute - WORK_DAY_END_MINUTES) * remainderHeight / WORK_DAY_END_LENGTH
1150         }
1151         return y
1152     }
1153 
1154     /**
1155      * Add a new segment based on the event provided. This will handle splitting
1156      * segments across day boundaries and ensures a minimum size for segments.
1157      */
addNewSegmentnull1158     private fun addNewSegment(
1159         segments: LinkedList<DNASegment>,
1160         event: Event,
1161         strands: HashMap<Int, DNAStrand>,
1162         firstJulianDay: Int,
1163         minStart: Int,
1164         minMinutes: Int
1165     ) {
1166         var event: Event = event
1167         var minStart = minStart
1168         if (event.startDay > event.endDay) {
1169             Log.wtf(TAG, "Event starts after it ends: " + event.toString())
1170         }
1171         // If this is a multiday event split it up by day
1172         if (event.startDay !== event.endDay) {
1173             val lhs = Event()
1174             lhs.color = event.color
1175             lhs.startDay = event.startDay
1176             // the first day we want the start time to be the actual start time
1177             lhs.startTime = event.startTime
1178             lhs.endDay = lhs.startDay
1179             lhs.endTime = DAY_IN_MINUTES - 1
1180             // Nearly recursive iteration!
1181             while (lhs.startDay !== event.endDay) {
1182                 addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes)
1183                 // The days in between are all day, even though that shouldn't
1184                 // actually happen due to the allday filtering
1185                 lhs.startDay++
1186                 lhs.endDay = lhs.startDay
1187                 lhs.startTime = 0
1188                 minStart = 0
1189             }
1190             // The last day we want the end time to be the actual end time
1191             lhs.endTime = event.endTime
1192             event = lhs
1193         }
1194         // Create the new segment and compute its fields
1195         val segment = DNASegment()
1196         val dayOffset: Int = (event.startDay - firstJulianDay) * DAY_IN_MINUTES
1197         val endOfDay = dayOffset + DAY_IN_MINUTES - 1
1198         // clip the start if needed
1199         segment.startMinute = Math.max(dayOffset + event.startTime, minStart)
1200         // and extend the end if it's too small, but not beyond the end of the
1201         // day
1202         val minEnd: Int = Math.min(segment.startMinute + minMinutes, endOfDay)
1203         segment.endMinute = Math.max(dayOffset + event.endTime, minEnd)
1204         if (segment.endMinute > endOfDay) {
1205             segment.endMinute = endOfDay
1206         }
1207         segment.color = event.color
1208         segment.day = event.startDay
1209         segments.add(segment)
1210         // increment the count for the correct color or add a new strand if we
1211         // don't have that color yet
1212         val strand = getOrCreateStrand(strands, segment.color)
1213         strand?.count
1214         strand?.count = strand?.count?.inc() as Int
1215     }
1216 
1217     /**
1218      * Try to get a strand of the given color. Create it if it doesn't exist.
1219      */
getOrCreateStrandnull1220     private fun getOrCreateStrand(strands: HashMap<Int, DNAStrand>, color: Int): DNAStrand? {
1221         var strand: DNAStrand? = strands.get(color)
1222         if (strand == null) {
1223             strand = DNAStrand()
1224             strand.color = color
1225             strand.count = 0
1226             strands.put(strand.color, strand)
1227         }
1228         return strand
1229     }
1230 
1231     /**
1232      * Sends an intent to launch the top level Calendar view.
1233      *
1234      * @param context
1235      */
returnToCalendarHomenull1236     @JvmStatic fun returnToCalendarHome(context: Context) {
1237         val launchIntent = Intent(context, AllInOneActivity::class.java)
1238         launchIntent.setAction(Intent.ACTION_DEFAULT)
1239         launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
1240         launchIntent.putExtra(INTENT_KEY_HOME, true)
1241         context.startActivity(launchIntent)
1242     }
1243 
1244     /**
1245      * Given a context and a time in millis since unix epoch figures out the
1246      * correct week of the year for that time.
1247      *
1248      * @param millisSinceEpoch
1249      * @return
1250      */
getWeekNumberFromTimenull1251     @JvmStatic fun getWeekNumberFromTime(millisSinceEpoch: Long, context: Context?): Int {
1252         val weekTime = Time(getTimeZone(context, null))
1253         weekTime.set(millisSinceEpoch)
1254         weekTime.normalize(true)
1255         val firstDayOfWeek = getFirstDayOfWeek(context)
1256         // if the date is on Saturday or Sunday and the start of the week
1257         // isn't Monday we may need to shift the date to be in the correct
1258         // week
1259         if (weekTime.weekDay === Time.SUNDAY &&
1260             (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY)
1261         ) {
1262             weekTime.monthDay++
1263             weekTime.normalize(true)
1264         } else if (weekTime.weekDay === Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) {
1265             weekTime.monthDay += 2
1266             weekTime.normalize(true)
1267         }
1268         return weekTime.getWeekNumber()
1269     }
1270 
1271     /**
1272      * Formats a day of the week string. This is either just the name of the day
1273      * or a combination of yesterday/today/tomorrow and the day of the week.
1274      *
1275      * @param julianDay The julian day to get the string for
1276      * @param todayJulianDay The julian day for today's date
1277      * @param millis A utc millis since epoch time that falls on julian day
1278      * @param context The calling context, used to get the timezone and do the
1279      * formatting
1280      * @return
1281      */
getDayOfWeekStringnull1282     @JvmStatic fun getDayOfWeekString(
1283         julianDay: Int,
1284         todayJulianDay: Int,
1285         millis: Long,
1286         context: Context
1287     ): String {
1288         getTimeZone(context, null)
1289         val flags: Int = DateUtils.FORMAT_SHOW_WEEKDAY
1290         var dayViewText: String
1291         dayViewText = if (julianDay == todayJulianDay) {
1292             context.getString(
1293                 R.string.agenda_today,
1294                 mTZUtils?.formatDateRange(context, millis, millis, flags)
1295                     .toString()
1296             )
1297         } else if (julianDay == todayJulianDay - 1) {
1298             context.getString(
1299                 R.string.agenda_yesterday,
1300                 mTZUtils?.formatDateRange(context, millis, millis, flags)
1301                     .toString()
1302             )
1303         } else if (julianDay == todayJulianDay + 1) {
1304             context.getString(
1305                 R.string.agenda_tomorrow,
1306                 mTZUtils?.formatDateRange(context, millis, millis, flags)
1307                     .toString()
1308             )
1309         } else {
1310             mTZUtils?.formatDateRange(context, millis, millis, flags)
1311                 .toString()
1312         }
1313         dayViewText = dayViewText.toUpperCase()
1314         return dayViewText
1315     }
1316 
1317     // Calculate the time until midnight + 1 second and set the handler to
1318     // do run the runnable
setMidnightUpdaternull1319     @JvmStatic fun setMidnightUpdater(h: Handler?, r: Runnable?, timezone: String?) {
1320         if (h == null || r == null || timezone == null) {
1321             return
1322         }
1323         val now: Long = System.currentTimeMillis()
1324         val time = Time(timezone)
1325         time.set(now)
1326         val runInMillis: Long = ((24 * 3600 - time.hour * 3600 - time.minute * 60 -
1327             time.second + 1) * 1000).toLong()
1328         h.removeCallbacks(r)
1329         h.postDelayed(r, runInMillis)
1330     }
1331 
1332     // Stop the midnight update thread
resetMidnightUpdaternull1333     @JvmStatic fun resetMidnightUpdater(h: Handler?, r: Runnable?) {
1334         if (h == null || r == null) {
1335             return
1336         }
1337         h.removeCallbacks(r)
1338     }
1339 
1340     /**
1341      * Returns a string description of the specified time interval.
1342      */
getDisplayedDatetimenull1343     @JvmStatic fun getDisplayedDatetime(
1344         startMillis: Long,
1345         endMillis: Long,
1346         currentMillis: Long,
1347         localTimezone: String,
1348         allDay: Boolean,
1349         context: Context
1350     ): String? {
1351         // Configure date/time formatting.
1352         val flagsDate: Int = DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
1353         var flagsTime: Int = DateUtils.FORMAT_SHOW_TIME
1354         if (DateFormat.is24HourFormat(context)) {
1355             flagsTime = flagsTime or DateUtils.FORMAT_24HOUR
1356         }
1357         val currentTime = Time(localTimezone)
1358         currentTime.set(currentMillis)
1359         val resources: Resources = context.getResources()
1360         var datetimeString: String? = null
1361         if (allDay) {
1362             // All day events require special timezone adjustment.
1363             val localStartMillis = convertAlldayUtcToLocal(null, startMillis, localTimezone)
1364             val localEndMillis = convertAlldayUtcToLocal(null, endMillis, localTimezone)
1365             if (singleDayEvent(localStartMillis, localEndMillis, currentTime.gmtoff)) {
1366                 // If possible, use "Today" or "Tomorrow" instead of a full date string.
1367                 val todayOrTomorrow = isTodayOrTomorrow(
1368                     context.getResources(),
1369                     localStartMillis, currentMillis, currentTime.gmtoff
1370                 )
1371                 if (TODAY == todayOrTomorrow) {
1372                     datetimeString = resources.getString(R.string.today)
1373                 } else if (TOMORROW == todayOrTomorrow) {
1374                     datetimeString = resources.getString(R.string.tomorrow)
1375                 }
1376             }
1377             if (datetimeString == null) {
1378                 // For multi-day allday events or single-day all-day events that are not
1379                 // today or tomorrow, use framework formatter.
1380                 val f = Formatter(StringBuilder(50), Locale.getDefault())
1381                 datetimeString = DateUtils.formatDateRange(
1382                     context, f, startMillis,
1383                     endMillis, flagsDate, Time.TIMEZONE_UTC
1384                 ).toString()
1385             }
1386         } else {
1387             datetimeString = if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) {
1388                 // Format the time.
1389                 val timeString = formatDateRange(
1390                     context, startMillis, endMillis,
1391                     flagsTime
1392                 )
1393 
1394                 // If possible, use "Today" or "Tomorrow" instead of a full date string.
1395                 val todayOrTomorrow = isTodayOrTomorrow(
1396                     context.getResources(), startMillis,
1397                     currentMillis, currentTime.gmtoff
1398                 )
1399                 if (TODAY == todayOrTomorrow) {
1400                     // Example: "Today at 1:00pm - 2:00 pm"
1401                     resources.getString(
1402                         R.string.today_at_time_fmt,
1403                         timeString
1404                     )
1405                 } else if (TOMORROW == todayOrTomorrow) {
1406                     // Example: "Tomorrow at 1:00pm - 2:00 pm"
1407                     resources.getString(
1408                         R.string.tomorrow_at_time_fmt,
1409                         timeString
1410                     )
1411                 } else {
1412                     // Format the full date. Example: "Thursday, April 12, 1:00pm - 2:00pm"
1413                     val dateString = formatDateRange(
1414                         context, startMillis, endMillis,
1415                         flagsDate
1416                     )
1417                     resources.getString(
1418                         R.string.date_time_fmt, dateString,
1419                         timeString
1420                     )
1421                 }
1422             } else {
1423                 // For multiday events, shorten day/month names.
1424                 // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm"
1425                 val flagsDatetime = flagsDate or flagsTime or DateUtils.FORMAT_ABBREV_MONTH or
1426                     DateUtils.FORMAT_ABBREV_WEEKDAY
1427                 formatDateRange(
1428                     context, startMillis, endMillis,
1429                     flagsDatetime
1430                 )
1431             }
1432         }
1433         return datetimeString
1434     }
1435 
1436     /**
1437      * Returns the timezone to display in the event info, if the local timezone is different
1438      * from the event timezone.  Otherwise returns null.
1439      */
getDisplayedTimezonenull1440     @JvmStatic fun getDisplayedTimezone(
1441         startMillis: Long,
1442         localTimezone: String?,
1443         eventTimezone: String?
1444     ): String? {
1445         var tzDisplay: String? = null
1446         if (!TextUtils.equals(localTimezone, eventTimezone)) {
1447             // Figure out if this is in DST
1448             val tz: TimeZone = TimeZone.getTimeZone(localTimezone)
1449             tzDisplay = if (tz == null || tz.getID().equals("GMT")) {
1450                 localTimezone
1451             } else {
1452                 val startTime = Time(localTimezone)
1453                 startTime.set(startMillis)
1454                 tz.getDisplayName(startTime.isDst !== 0, TimeZone.SHORT)
1455             }
1456         }
1457         return tzDisplay
1458     }
1459 
1460     /**
1461      * Returns whether the specified time interval is in a single day.
1462      */
singleDayEventnull1463     private fun singleDayEvent(startMillis: Long, endMillis: Long, localGmtOffset: Long): Boolean {
1464         if (startMillis == endMillis) {
1465             return true
1466         }
1467 
1468         // An event ending at midnight should still be a single-day event, so check
1469         // time end-1.
1470         val startDay: Int = Time.getJulianDay(startMillis, localGmtOffset)
1471         val endDay: Int = Time.getJulianDay(endMillis - 1, localGmtOffset)
1472         return startDay == endDay
1473     }
1474 
1475     // Using int constants as a return value instead of an enum to minimize resources.
1476     private const val TODAY = 1
1477     private const val TOMORROW = 2
1478     private const val NONE = 0
1479 
1480     /**
1481      * Returns TODAY or TOMORROW if applicable.  Otherwise returns NONE.
1482      */
isTodayOrTomorrownull1483     private fun isTodayOrTomorrow(
1484         r: Resources,
1485         dayMillis: Long,
1486         currentMillis: Long,
1487         localGmtOffset: Long
1488     ): Int {
1489         val startDay: Int = Time.getJulianDay(dayMillis, localGmtOffset)
1490         val currentDay: Int = Time.getJulianDay(currentMillis, localGmtOffset)
1491         val days = startDay - currentDay
1492         return if (days == 1) {
1493             TOMORROW
1494         } else if (days == 0) {
1495             TODAY
1496         } else {
1497             NONE
1498         }
1499     }
1500 
1501     /**
1502      * Inserts a drawable with today's day into the today's icon in the option menu
1503      * @param icon - today's icon from the options menu
1504      */
setTodayIconnull1505     @JvmStatic fun setTodayIcon(icon: LayerDrawable, c: Context?, timezone: String?) {
1506         val today: DayOfMonthDrawable
1507 
1508         // Reuse current drawable if possible
1509         val currentDrawable: Drawable? = icon.findDrawableByLayerId(R.id.today_icon_day)
1510         if (currentDrawable != null && currentDrawable is DayOfMonthDrawable) {
1511             today = currentDrawable as DayOfMonthDrawable
1512         } else {
1513             today = DayOfMonthDrawable(c as Context)
1514         }
1515         // Set the day and update the icon
1516         val now = Time(timezone)
1517         now.setToNow()
1518         now.normalize(false)
1519         today.setDayOfMonth(now.monthDay)
1520         icon.mutate()
1521         icon.setDrawableByLayerId(R.id.today_icon_day, today)
1522     }
1523 
1524     /**
1525      * Get a list of quick responses used for emailing guests from the
1526      * SharedPreferences. If not are found, get the hard coded ones that shipped
1527      * with the app
1528      *
1529      * @param context
1530      * @return a list of quick responses.
1531      */
getQuickResponsesnull1532     fun getQuickResponses(context: Context): Array<String> {
1533         var s = getSharedPreference(context, KEY_QUICK_RESPONSES, null as Array<String>?)
1534         if (s == null) {
1535             s = context.getResources().getStringArray(R.array.quick_response_defaults)
1536         }
1537         return s
1538     }
1539 
1540     /**
1541      * Return the app version code.
1542      */
getVersionCodenull1543     fun getVersionCode(context: Context): String? {
1544         if (sVersion == null) {
1545             try {
1546                 sVersion = context.getPackageManager().getPackageInfo(
1547                     context.getPackageName(), 0
1548                 ).versionName
1549             } catch (e: PackageManager.NameNotFoundException) {
1550                 // Can't find version; just leave it blank.
1551                 Log.e(TAG, "Error finding package " + context.getApplicationInfo().packageName)
1552             }
1553         }
1554         return sVersion
1555     }
1556 
1557     // A single strand represents one color of events. Events are divided up by
1558     // color to make them convenient to draw. The black strand is special in
1559     // that it holds conflicting events as well as color settings for allday on
1560     // each day.
1561     class DNAStrand {
1562         @JvmField var points: FloatArray? = null
1563         @JvmField var allDays: IntArray? = null // color for the allday, 0 means no event
1564         @JvmField var position = 0
1565         @JvmField var color = 0
1566         @JvmField var count = 0
1567     }
1568 
1569     // A segment is a single continuous length of time occupied by a single
1570     // color. Segments should never span multiple days.
1571     private class DNASegment {
1572         var startMinute = 0 // in minutes since the start of the week =
1573         var endMinute = 0
1574         var color = 0 // Calendar color or black for conflicts =
1575         var day = 0 // quick reference to the day this segment is on =
1576     }
1577 }