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 }