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