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 com.android.calendar.CalendarController.ViewType;
     22 
     23 import android.app.Activity;
     24 import android.app.SearchManager;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.SharedPreferences;
     28 import android.content.res.Configuration;
     29 import android.content.res.Resources;
     30 import android.database.Cursor;
     31 import android.database.MatrixCursor;
     32 import android.graphics.Color;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 import android.text.TextUtils;
     36 import android.text.format.DateUtils;
     37 import android.text.format.Time;
     38 import android.util.Log;
     39 import android.widget.SearchView;
     40 
     41 import com.android.calendar.CalendarUtils.TimeZoneUtils;
     42 
     43 import java.util.ArrayList;
     44 import java.util.Arrays;
     45 import java.util.Calendar;
     46 import java.util.Formatter;
     47 import java.util.HashMap;
     48 import java.util.Iterator;
     49 import java.util.LinkedList;
     50 import java.util.List;
     51 import java.util.Map;
     52 
     53 public class Utils {
     54     private static final boolean DEBUG = false;
     55     private static final String TAG = "CalUtils";
     56     // Set to 0 until we have UI to perform undo
     57     public static final long UNDO_DELAY = 0;
     58 
     59     // For recurring events which instances of the series are being modified
     60     public static final int MODIFY_UNINITIALIZED = 0;
     61     public static final int MODIFY_SELECTED = 1;
     62     public static final int MODIFY_ALL_FOLLOWING = 2;
     63     public static final int MODIFY_ALL = 3;
     64 
     65     // When the edit event view finishes it passes back the appropriate exit
     66     // code.
     67     public static final int DONE_REVERT = 1 << 0;
     68     public static final int DONE_SAVE = 1 << 1;
     69     public static final int DONE_DELETE = 1 << 2;
     70     // And should re run with DONE_EXIT if it should also leave the view, just
     71     // exiting is identical to reverting
     72     public static final int DONE_EXIT = 1 << 0;
     73 
     74     public static final String OPEN_EMAIL_MARKER = " <";
     75     public static final String CLOSE_EMAIL_MARKER = ">";
     76 
     77     public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW";
     78     public static final String INTENT_KEY_VIEW_TYPE = "VIEW";
     79     public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY";
     80     public static final String INTENT_KEY_HOME = "KEY_HOME";
     81 
     82     public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3;
     83 
     84     private static final float SATURATION_ADJUST = 0.3f;
     85 
     86     // Defines used by the DNA generation code
     87     static final int DAY_IN_MINUTES = 60 * 24;
     88     static final int WEEK_IN_MINUTES = DAY_IN_MINUTES * 7;
     89     // The work day is being counted as 6am to 8pm
     90     static int WORK_DAY_MINUTES = 14 * 60;
     91     static int WORK_DAY_START_MINUTES = 6 * 60;
     92     static int WORK_DAY_END_MINUTES = 20 * 60;
     93     static int WORK_DAY_END_LENGTH = (24 * 60) - WORK_DAY_END_MINUTES;
     94     static int CONFLICT_COLOR = 0xFF000000;
     95     static boolean mMinutesLoaded = false;
     96 
     97     // The name of the shared preferences file. This name must be maintained for
     98     // historical
     99     // reasons, as it's what PreferenceManager assigned the first time the file
    100     // was created.
    101     private static final String SHARED_PREFS_NAME = "com.android.calendar_preferences";
    102 
    103     public static final String APPWIDGET_DATA_TYPE = "vnd.android.data/update";
    104 
    105     private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME);
    106     private static boolean mAllowWeekForDetailView = false;
    107     private static long mTardis = 0;
    108 
    109     public static int getViewTypeFromIntentAndSharedPref(Activity activity) {
    110         Intent intent = activity.getIntent();
    111         Bundle extras = intent.getExtras();
    112         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity);
    113 
    114         if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) {
    115             return ViewType.EDIT;
    116         }
    117         if (extras != null) {
    118             if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) {
    119                 // This is the "detail" view which is either agenda or day view
    120                 return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW,
    121                         GeneralPreferences.DEFAULT_DETAILED_VIEW);
    122             } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) {
    123                 // Not sure who uses this. This logic came from LaunchActivity
    124                 return ViewType.DAY;
    125             }
    126         }
    127 
    128         // Default to the last view
    129         return prefs.getInt(
    130                 GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW);
    131     }
    132 
    133     /**
    134      * Gets the intent action for telling the widget to update.
    135      */
    136     public static String getWidgetUpdateAction(Context context) {
    137         return context.getPackageName() + ".APPWIDGET_UPDATE";
    138     }
    139 
    140     /**
    141      * Gets the intent action for telling the widget to update.
    142      */
    143     public static String getWidgetScheduledUpdateAction(Context context) {
    144         return context.getPackageName() + ".APPWIDGET_SCHEDULED_UPDATE";
    145     }
    146 
    147     /**
    148      * Gets the intent action for telling the widget to update.
    149      */
    150     public static String getSearchAuthority(Context context) {
    151         return context.getPackageName() + ".CalendarRecentSuggestionsProvider";
    152     }
    153 
    154     /**
    155      * Writes a new home time zone to the db. Updates the home time zone in the
    156      * db asynchronously and updates the local cache. Sending a time zone of
    157      * **tbd** will cause it to be set to the device's time zone. null or empty
    158      * tz will be ignored.
    159      *
    160      * @param context The calling activity
    161      * @param timeZone The time zone to set Calendar to, or **tbd**
    162      */
    163     public static void setTimeZone(Context context, String timeZone) {
    164         mTZUtils.setTimeZone(context, timeZone);
    165     }
    166 
    167     /**
    168      * Gets the time zone that Calendar should be displayed in This is a helper
    169      * method to get the appropriate time zone for Calendar. If this is the
    170      * first time this method has been called it will initiate an asynchronous
    171      * query to verify that the data in preferences is correct. The callback
    172      * supplied will only be called if this query returns a value other than
    173      * what is stored in preferences and should cause the calling activity to
    174      * refresh anything that depends on calling this method.
    175      *
    176      * @param context The calling activity
    177      * @param callback The runnable that should execute if a query returns new
    178      *            values
    179      * @return The string value representing the time zone Calendar should
    180      *         display
    181      */
    182     public static String getTimeZone(Context context, Runnable callback) {
    183         return mTZUtils.getTimeZone(context, callback);
    184     }
    185 
    186     /**
    187      * Formats a date or a time range according to the local conventions.
    188      *
    189      * @param context the context is required only if the time is shown
    190      * @param startMillis the start time in UTC milliseconds
    191      * @param endMillis the end time in UTC milliseconds
    192      * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter,
    193      * long, long, int, String) formatDateRange}
    194      * @return a string containing the formatted date/time range.
    195      */
    196     public static String formatDateRange(
    197             Context context, long startMillis, long endMillis, int flags) {
    198         return mTZUtils.formatDateRange(context, startMillis, endMillis, flags);
    199     }
    200 
    201     public static String getSharedPreference(Context context, String key, String defaultValue) {
    202         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    203         return prefs.getString(key, defaultValue);
    204     }
    205 
    206     public static int getSharedPreference(Context context, String key, int defaultValue) {
    207         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    208         return prefs.getInt(key, defaultValue);
    209     }
    210 
    211     public static boolean getSharedPreference(Context context, String key, boolean defaultValue) {
    212         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    213         return prefs.getBoolean(key, defaultValue);
    214     }
    215 
    216     /**
    217      * Asynchronously sets the preference with the given key to the given value
    218      *
    219      * @param context the context to use to get preferences from
    220      * @param key the key of the preference to set
    221      * @param value the value to set
    222      */
    223     public static void setSharedPreference(Context context, String key, String value) {
    224         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    225         prefs.edit().putString(key, value).apply();
    226     }
    227 
    228     protected static void tardis() {
    229         mTardis = System.currentTimeMillis();
    230     }
    231 
    232     protected static long getTardis() {
    233         return mTardis;
    234     }
    235 
    236     static void setSharedPreference(Context context, String key, boolean value) {
    237         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    238         SharedPreferences.Editor editor = prefs.edit();
    239         editor.putBoolean(key, value);
    240         editor.apply();
    241     }
    242 
    243     static void setSharedPreference(Context context, String key, int value) {
    244         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    245         SharedPreferences.Editor editor = prefs.edit();
    246         editor.putInt(key, value);
    247         editor.apply();
    248     }
    249 
    250     /**
    251      * Save default agenda/day/week/month view for next time
    252      *
    253      * @param context
    254      * @param viewId {@link CalendarController.ViewType}
    255      */
    256     static void setDefaultView(Context context, int viewId) {
    257         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    258         SharedPreferences.Editor editor = prefs.edit();
    259 
    260         boolean validDetailView = false;
    261         if (mAllowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) {
    262             validDetailView = true;
    263         } else {
    264             validDetailView = viewId == CalendarController.ViewType.AGENDA
    265                     || viewId == CalendarController.ViewType.DAY;
    266         }
    267 
    268         if (validDetailView) {
    269             // Record the detail start view
    270             editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId);
    271         }
    272 
    273         // Record the (new) start view
    274         editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId);
    275         editor.apply();
    276     }
    277 
    278     public static MatrixCursor matrixCursorFromCursor(Cursor cursor) {
    279         MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
    280         int numColumns = cursor.getColumnCount();
    281         String data[] = new String[numColumns];
    282         cursor.moveToPosition(-1);
    283         while (cursor.moveToNext()) {
    284             for (int i = 0; i < numColumns; i++) {
    285                 data[i] = cursor.getString(i);
    286             }
    287             newCursor.addRow(data);
    288         }
    289         return newCursor;
    290     }
    291 
    292     /**
    293      * Compares two cursors to see if they contain the same data.
    294      *
    295      * @return Returns true of the cursors contain the same data and are not
    296      *         null, false otherwise
    297      */
    298     public static boolean compareCursors(Cursor c1, Cursor c2) {
    299         if (c1 == null || c2 == null) {
    300             return false;
    301         }
    302 
    303         int numColumns = c1.getColumnCount();
    304         if (numColumns != c2.getColumnCount()) {
    305             return false;
    306         }
    307 
    308         if (c1.getCount() != c2.getCount()) {
    309             return false;
    310         }
    311 
    312         c1.moveToPosition(-1);
    313         c2.moveToPosition(-1);
    314         while (c1.moveToNext() && c2.moveToNext()) {
    315             for (int i = 0; i < numColumns; i++) {
    316                 if (!TextUtils.equals(c1.getString(i), c2.getString(i))) {
    317                     return false;
    318                 }
    319             }
    320         }
    321 
    322         return true;
    323     }
    324 
    325     /**
    326      * If the given intent specifies a time (in milliseconds since the epoch),
    327      * then that time is returned. Otherwise, the current time is returned.
    328      */
    329     public static final long timeFromIntentInMillis(Intent intent) {
    330         // If the time was specified, then use that. Otherwise, use the current
    331         // time.
    332         Uri data = intent.getData();
    333         long millis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1);
    334         if (millis == -1 && data != null && data.isHierarchical()) {
    335             List<String> path = data.getPathSegments();
    336             if (path.size() == 2 && path.get(0).equals("time")) {
    337                 try {
    338                     millis = Long.valueOf(data.getLastPathSegment());
    339                 } catch (NumberFormatException e) {
    340                     Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time "
    341                             + "found. Using current time.");
    342                 }
    343             }
    344         }
    345         if (millis <= 0) {
    346             millis = System.currentTimeMillis();
    347         }
    348         return millis;
    349     }
    350 
    351     /**
    352      * Formats the given Time object so that it gives the month and year (for
    353      * example, "September 2007").
    354      *
    355      * @param time the time to format
    356      * @return the string containing the weekday and the date
    357      */
    358     public static String formatMonthYear(Context context, Time time) {
    359         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
    360                 | DateUtils.FORMAT_SHOW_YEAR;
    361         long millis = time.toMillis(true);
    362         return formatDateRange(context, millis, millis, flags);
    363     }
    364 
    365     /**
    366      * Returns a list joined together by the provided delimiter, for example,
    367      * ["a", "b", "c"] could be joined into "a,b,c"
    368      *
    369      * @param things the things to join together
    370      * @param delim the delimiter to use
    371      * @return a string contained the things joined together
    372      */
    373     public static String join(List<?> things, String delim) {
    374         StringBuilder builder = new StringBuilder();
    375         boolean first = true;
    376         for (Object thing : things) {
    377             if (first) {
    378                 first = false;
    379             } else {
    380                 builder.append(delim);
    381             }
    382             builder.append(thing.toString());
    383         }
    384         return builder.toString();
    385     }
    386 
    387     /**
    388      * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970)
    389      * adjusted for first day of week.
    390      *
    391      * This takes a julian day and the week start day and calculates which
    392      * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting
    393      * at 0. *Do not* use this to compute the ISO week number for the year.
    394      *
    395      * @param julianDay The julian day to calculate the week number for
    396      * @param firstDayOfWeek Which week day is the first day of the week,
    397      *          see {@link Time#SUNDAY}
    398      * @return Weeks since the epoch
    399      */
    400     public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
    401         int diff = Time.THURSDAY - firstDayOfWeek;
    402         if (diff < 0) {
    403             diff += 7;
    404         }
    405         int refDay = Time.EPOCH_JULIAN_DAY - diff;
    406         return (julianDay - refDay) / 7;
    407     }
    408 
    409     /**
    410      * Takes a number of weeks since the epoch and calculates the Julian day of
    411      * the Monday for that week.
    412      *
    413      * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY}
    414      * is considered week 0. It returns the Julian day for the Monday
    415      * {@code week} weeks after the Monday of the week containing the epoch.
    416      *
    417      * @param week Number of weeks since the epoch
    418      * @return The julian day for the Monday of the given week since the epoch
    419      */
    420     public static int getJulianMondayFromWeeksSinceEpoch(int week) {
    421         return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
    422     }
    423 
    424     /**
    425      * Get first day of week as android.text.format.Time constant.
    426      *
    427      * @return the first day of week in android.text.format.Time
    428      */
    429     public static int getFirstDayOfWeek(Context context) {
    430         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    431         String pref = prefs.getString(
    432                 GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT);
    433 
    434         int startDay;
    435         if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) {
    436             startDay = Calendar.getInstance().getFirstDayOfWeek();
    437         } else {
    438             startDay = Integer.parseInt(pref);
    439         }
    440 
    441         if (startDay == Calendar.SATURDAY) {
    442             return Time.SATURDAY;
    443         } else if (startDay == Calendar.MONDAY) {
    444             return Time.MONDAY;
    445         } else {
    446             return Time.SUNDAY;
    447         }
    448     }
    449 
    450     /**
    451      * @return true when week number should be shown.
    452      */
    453     public static boolean getShowWeekNumber(Context context) {
    454         final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    455         return prefs.getBoolean(
    456                 GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM);
    457     }
    458 
    459     /**
    460      * @return true when declined events should be hidden.
    461      */
    462     public static boolean getHideDeclinedEvents(Context context) {
    463         final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    464         return prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false);
    465     }
    466 
    467     public static int getDaysPerWeek(Context context) {
    468         final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
    469         return prefs.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7);
    470     }
    471 
    472     /**
    473      * Determine whether the column position is Saturday or not.
    474      *
    475      * @param column the column position
    476      * @param firstDayOfWeek the first day of week in android.text.format.Time
    477      * @return true if the column is Saturday position
    478      */
    479     public static boolean isSaturday(int column, int firstDayOfWeek) {
    480         return (firstDayOfWeek == Time.SUNDAY && column == 6)
    481                 || (firstDayOfWeek == Time.MONDAY && column == 5)
    482                 || (firstDayOfWeek == Time.SATURDAY && column == 0);
    483     }
    484 
    485     /**
    486      * Determine whether the column position is Sunday or not.
    487      *
    488      * @param column the column position
    489      * @param firstDayOfWeek the first day of week in android.text.format.Time
    490      * @return true if the column is Sunday position
    491      */
    492     public static boolean isSunday(int column, int firstDayOfWeek) {
    493         return (firstDayOfWeek == Time.SUNDAY && column == 0)
    494                 || (firstDayOfWeek == Time.MONDAY && column == 6)
    495                 || (firstDayOfWeek == Time.SATURDAY && column == 1);
    496     }
    497 
    498     /**
    499      * Convert given UTC time into current local time. This assumes it is for an
    500      * allday event and will adjust the time to be on a midnight boundary.
    501      *
    502      * @param recycle Time object to recycle, otherwise null.
    503      * @param utcTime Time to convert, in UTC.
    504      * @param tz The time zone to convert this time to.
    505      */
    506     public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) {
    507         if (recycle == null) {
    508             recycle = new Time();
    509         }
    510         recycle.timezone = Time.TIMEZONE_UTC;
    511         recycle.set(utcTime);
    512         recycle.timezone = tz;
    513         return recycle.normalize(true);
    514     }
    515 
    516     public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) {
    517         if (recycle == null) {
    518             recycle = new Time();
    519         }
    520         recycle.timezone = tz;
    521         recycle.set(localTime);
    522         recycle.timezone = Time.TIMEZONE_UTC;
    523         return recycle.normalize(true);
    524     }
    525 
    526     /**
    527      * Scan through a cursor of calendars and check if names are duplicated.
    528      * This travels a cursor containing calendar display names and fills in the
    529      * provided map with whether or not each name is repeated.
    530      *
    531      * @param isDuplicateName The map to put the duplicate check results in.
    532      * @param cursor The query of calendars to check
    533      * @param nameIndex The column of the query that contains the display name
    534      */
    535     public static void checkForDuplicateNames(
    536             Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) {
    537         isDuplicateName.clear();
    538         cursor.moveToPosition(-1);
    539         while (cursor.moveToNext()) {
    540             String displayName = cursor.getString(nameIndex);
    541             // Set it to true if we've seen this name before, false otherwise
    542             if (displayName != null) {
    543                 isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName));
    544             }
    545         }
    546     }
    547 
    548     /**
    549      * Null-safe object comparison
    550      *
    551      * @param s1
    552      * @param s2
    553      * @return
    554      */
    555     public static boolean equals(Object o1, Object o2) {
    556         return o1 == null ? o2 == null : o1.equals(o2);
    557     }
    558 
    559     public static void setAllowWeekForDetailView(boolean allowWeekView) {
    560         mAllowWeekForDetailView  = allowWeekView;
    561     }
    562 
    563     public static boolean getAllowWeekForDetailView() {
    564         return mAllowWeekForDetailView;
    565     }
    566 
    567     public static boolean isMultiPaneConfiguration (Context c) {
    568         return (c.getResources().getConfiguration().screenLayout &
    569                 Configuration.SCREENLAYOUT_SIZE_XLARGE) != 0;
    570     }
    571 
    572     public static boolean getConfigBool(Context c, int key) {
    573         return c.getResources().getBoolean(key);
    574     }
    575 
    576     public static int getDisplayColorFromColor(int color) {
    577         float[] hsv = new float[3];
    578         Color.colorToHSV(color, hsv);
    579         hsv[1] = Math.max(hsv[1] - SATURATION_ADJUST, 0.0f);
    580         return Color.HSVToColor(hsv);
    581     }
    582 
    583     // This takes a color and computes what it would look like blended with
    584     // white. The result is the color that should be used for declined events.
    585     public static int getDeclinedColorFromColor(int color) {
    586         int bg = 0xffffffff;
    587         int a = 0x66;
    588         int r = (((color & 0x00ff0000) * a) + ((bg & 0x00ff0000) * (0xff - a))) & 0xff000000;
    589         int g = (((color & 0x0000ff00) * a) + ((bg & 0x0000ff00) * (0xff - a))) & 0x00ff0000;
    590         int b = (((color & 0x000000ff) * a) + ((bg & 0x000000ff) * (0xff - a))) & 0x0000ff00;
    591         return (0xff000000) | ((r | g | b) >> 8);
    592     }
    593 
    594     // A single strand represents one color of events. Events are divided up by
    595     // color to make them convenient to draw. The black strand is special in
    596     // that it holds conflicting events as well as color settings for allday on
    597     // each day.
    598     public static class DNAStrand {
    599         public float[] points;
    600         public int[] allDays; // color for the allday, 0 means no event
    601         int position;
    602         public int color;
    603         int count;
    604     }
    605 
    606     // A segment is a single continuous length of time occupied by a single
    607     // color. Segments should never span multiple days.
    608     private static class DNASegment {
    609         int startMinute; // in minutes since the start of the week
    610         int endMinute;
    611         int color; // Calendar color or black for conflicts
    612         int day; // quick reference to the day this segment is on
    613     }
    614 
    615     /**
    616      * Converts a list of events to a list of segments to draw. Assumes list is
    617      * ordered by start time of the events. The function processes events for a
    618      * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1.
    619      * The algorithm goes over all the events and creates a set of segments
    620      * ordered by start time. This list of segments is then converted into a
    621      * HashMap of strands which contain the draw points and are organized by
    622      * color. The strands can then be drawn by setting the paint color to each
    623      * strand's color and calling drawLines on its set of points. The points are
    624      * set up using the following parameters.
    625      * <ul>
    626      * <li>Events between midnight and WORK_DAY_START_MINUTES are compressed
    627      * into the first 1/8th of the space between top and bottom.</li>
    628      * <li>Events between WORK_DAY_END_MINUTES and the following midnight are
    629      * compressed into the last 1/8th of the space between top and bottom</li>
    630      * <li>Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use
    631      * the remaining 3/4ths of the space</li>
    632      * <li>All segments drawn will maintain at least minPixels height, except
    633      * for conflicts in the first or last 1/8th, which may be smaller</li>
    634      * </ul>
    635      *
    636      * @param firstJulianDay The julian day of the first day of events
    637      * @param events A list of events sorted by start time
    638      * @param top The lowest y value the dna should be drawn at
    639      * @param bottom The highest y value the dna should be drawn at
    640      * @param dayXs An array of x values to draw the dna at, one for each day
    641      * @param conflictColor the color to use for conflicts
    642      * @return
    643      */
    644     public static HashMap<Integer, DNAStrand> createDNAStrands(int firstJulianDay,
    645             ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs,
    646             Context context) {
    647 
    648         if (!mMinutesLoaded) {
    649             if (context == null) {
    650                 Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA.");
    651             }
    652             Resources res = context.getResources();
    653             CONFLICT_COLOR = res.getColor(R.color.month_dna_conflict_time_color);
    654             WORK_DAY_START_MINUTES = res.getInteger(R.integer.work_start_minutes);
    655             WORK_DAY_END_MINUTES = res.getInteger(R.integer.work_end_minutes);
    656             WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES;
    657             WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES;
    658             mMinutesLoaded = true;
    659         }
    660 
    661         if (events == null || events.isEmpty() || dayXs == null || dayXs.length < 1
    662                 || bottom - top < 8 || minPixels < 0) {
    663             Log.e(TAG,
    664                     "Bad values for createDNAStrands! events:" + events + " dayXs:"
    665                             + Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:"
    666                             + minPixels);
    667             return null;
    668         }
    669 
    670         LinkedList<DNASegment> segments = new LinkedList<DNASegment>();
    671         HashMap<Integer, DNAStrand> strands = new HashMap<Integer, DNAStrand>();
    672         // add a black strand by default, other colors will get added in
    673         // the loop
    674         DNAStrand blackStrand = new DNAStrand();
    675         blackStrand.color = CONFLICT_COLOR;
    676         strands.put(CONFLICT_COLOR, blackStrand);
    677         // the min length is the number of minutes that will occupy
    678         // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the
    679         // minutes/pixel * minpx where the number of pixels are 3/4 the total
    680         // dna height: 4*(mins/(px * 3/4))
    681         int minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top));
    682 
    683         // There are slightly fewer than half as many pixels in 1/6 the space,
    684         // so round to 2.5x for the min minutes in the non-work area
    685         int minOtherMinutes = minMinutes * 5 / 2;
    686         int lastJulianDay = firstJulianDay + dayXs.length - 1;
    687 
    688         Event event = new Event();
    689         // Go through all the events for the week
    690         for (Event currEvent : events) {
    691             // if this event is outside the weeks range skip it
    692             if (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay) {
    693                 continue;
    694             }
    695             if (currEvent.drawAsAllday()) {
    696                 addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.length);
    697                 continue;
    698             }
    699             // Copy the event over so we can clip its start and end to our range
    700             currEvent.copyTo(event);
    701             if (event.startDay < firstJulianDay) {
    702                 event.startDay = firstJulianDay;
    703                 event.startTime = 0;
    704             }
    705             // If it starts after the work day make sure the start is at least
    706             // minPixels from midnight
    707             if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) {
    708                 event.startTime = DAY_IN_MINUTES - minOtherMinutes;
    709             }
    710             if (event.endDay > lastJulianDay) {
    711                 event.endDay = lastJulianDay;
    712                 event.endTime = DAY_IN_MINUTES - 1;
    713             }
    714             // If the end time is before the work day make sure it ends at least
    715             // minPixels after midnight
    716             if (event.endTime < minOtherMinutes) {
    717                 event.endTime = minOtherMinutes;
    718             }
    719             // If the start and end are on the same day make sure they are at
    720             // least minPixels apart. This only needs to be done for times
    721             // outside the work day as the min distance for within the work day
    722             // is enforced in the segment code.
    723             if (event.startDay == event.endDay &&
    724                     event.endTime - event.startTime < minOtherMinutes) {
    725                 // If it's less than minPixels in an area before the work
    726                 // day
    727                 if (event.startTime < WORK_DAY_START_MINUTES) {
    728                     // extend the end to the first easy guarantee that it's
    729                     // minPixels
    730                     event.endTime = Math.min(event.startTime + minOtherMinutes,
    731                             WORK_DAY_START_MINUTES + minMinutes);
    732                     // if it's in the area after the work day
    733                 } else if (event.endTime > WORK_DAY_END_MINUTES) {
    734                     // First try shifting the end but not past midnight
    735                     event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1);
    736                     // if it's still too small move the start back
    737                     if (event.endTime - event.startTime < minOtherMinutes) {
    738                         event.startTime = event.endTime - minOtherMinutes;
    739                     }
    740                 }
    741             }
    742 
    743             // This handles adding the first segment
    744             if (segments.size() == 0) {
    745                 addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes);
    746                 continue;
    747             }
    748             // Now compare our current start time to the end time of the last
    749             // segment in the list
    750             DNASegment lastSegment = segments.getLast();
    751             int startMinute = (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime;
    752             int endMinute = Math.max((event.endDay - firstJulianDay) * DAY_IN_MINUTES
    753                     + event.endTime, startMinute + minMinutes);
    754 
    755             if (startMinute < 0) {
    756                 startMinute = 0;
    757             }
    758             if (endMinute >= WEEK_IN_MINUTES) {
    759                 endMinute = WEEK_IN_MINUTES - 1;
    760             }
    761             // If we start before the last segment in the list ends we need to
    762             // start going through the list as this may conflict with other
    763             // events
    764             if (startMinute < lastSegment.endMinute) {
    765                 int i = segments.size();
    766                 // find the last segment this event intersects with
    767                 while (--i >= 0 && endMinute < segments.get(i).startMinute);
    768 
    769                 DNASegment currSegment;
    770                 // for each segment this event intersects with
    771                 for (; i >= 0 && startMinute <= (currSegment = segments.get(i)).endMinute; i--) {
    772                     // if the segment is already a conflict ignore it
    773                     if (currSegment.color == CONFLICT_COLOR) {
    774                         continue;
    775                     }
    776                     // if the event ends before the segment and wouldn't create
    777                     // a segment that is too small split off the right side
    778                     if (endMinute < currSegment.endMinute - minMinutes) {
    779                         DNASegment rhs = new DNASegment();
    780                         rhs.endMinute = currSegment.endMinute;
    781                         rhs.color = currSegment.color;
    782                         rhs.startMinute = endMinute + 1;
    783                         rhs.day = currSegment.day;
    784                         currSegment.endMinute = endMinute;
    785                         segments.add(i + 1, rhs);
    786                         strands.get(rhs.color).count++;
    787                         if (DEBUG) {
    788                             Log.d(TAG, "Added rhs, curr:" + currSegment.toString() + " i:"
    789                                     + segments.get(i).toString());
    790                         }
    791                     }
    792                     // if the event starts after the segment and wouldn't create
    793                     // a segment that is too small split off the left side
    794                     if (startMinute > currSegment.startMinute + minMinutes) {
    795                         DNASegment lhs = new DNASegment();
    796                         lhs.startMinute = currSegment.startMinute;
    797                         lhs.color = currSegment.color;
    798                         lhs.endMinute = startMinute - 1;
    799                         lhs.day = currSegment.day;
    800                         currSegment.startMinute = startMinute;
    801                         // increment i so that we are at the right position when
    802                         // referencing the segments to the right and left of the
    803                         // current segment.
    804                         segments.add(i++, lhs);
    805                         strands.get(lhs.color).count++;
    806                         if (DEBUG) {
    807                             Log.d(TAG, "Added lhs, curr:" + currSegment.toString() + " i:"
    808                                     + segments.get(i).toString());
    809                         }
    810                     }
    811                     // if the right side is black merge this with the segment to
    812                     // the right if they're on the same day and overlap
    813                     if (i + 1 < segments.size()) {
    814                         DNASegment rhs = segments.get(i + 1);
    815                         if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day
    816                                 && rhs.startMinute <= currSegment.endMinute + 1) {
    817                             rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute);
    818                             segments.remove(currSegment);
    819                             strands.get(currSegment.color).count--;
    820                             // point at the new current segment
    821                             currSegment = rhs;
    822                         }
    823                     }
    824                     // if the left side is black merge this with the segment to
    825                     // the left if they're on the same day and overlap
    826                     if (i - 1 >= 0) {
    827                         DNASegment lhs = segments.get(i - 1);
    828                         if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day
    829                                 && lhs.endMinute >= currSegment.startMinute - 1) {
    830                             lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute);
    831                             segments.remove(currSegment);
    832                             strands.get(currSegment.color).count--;
    833                             // point at the new current segment
    834                             currSegment = lhs;
    835                             // point i at the new current segment in case new
    836                             // code is added
    837                             i--;
    838                         }
    839                     }
    840                     // if we're still not black, decrement the count for the
    841                     // color being removed, change this to black, and increment
    842                     // the black count
    843                     if (currSegment.color != CONFLICT_COLOR) {
    844                         strands.get(currSegment.color).count--;
    845                         currSegment.color = CONFLICT_COLOR;
    846                         strands.get(CONFLICT_COLOR).count++;
    847                     }
    848                 }
    849 
    850             }
    851             // If this event extends beyond the last segment add a new segment
    852             if (endMinute > lastSegment.endMinute) {
    853                 addNewSegment(segments, event, strands, firstJulianDay, lastSegment.endMinute,
    854                         minMinutes);
    855             }
    856         }
    857         weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs);
    858         return strands;
    859     }
    860 
    861     // This figures out allDay colors as allDay events are found
    862     private static void addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands,
    863             int firstJulianDay, int numDays) {
    864         DNAStrand strand = getOrCreateStrand(strands, CONFLICT_COLOR);
    865         // if we haven't initialized the allDay portion create it now
    866         if (strand.allDays == null) {
    867             strand.allDays = new int[numDays];
    868         }
    869 
    870         // For each day this event is on update the color
    871         int end = Math.min(event.endDay - firstJulianDay, numDays - 1);
    872         for (int i = Math.max(event.startDay - firstJulianDay, 0); i <= end; i++) {
    873             if (strand.allDays[i] != 0) {
    874                 // if this day already had a color, it is now a conflict
    875                 strand.allDays[i] = CONFLICT_COLOR;
    876             } else {
    877                 // else it's just the color of the event
    878                 strand.allDays[i] = event.color;
    879             }
    880         }
    881     }
    882 
    883     // This processes all the segments, sorts them by color, and generates a
    884     // list of points to draw
    885     private static void weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay,
    886             HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs) {
    887         // First, get rid of any colors that ended up with no segments
    888         Iterator<DNAStrand> strandIterator = strands.values().iterator();
    889         while (strandIterator.hasNext()) {
    890             DNAStrand strand = strandIterator.next();
    891             if (strand.count < 1 && strand.allDays == null) {
    892                 strandIterator.remove();
    893                 continue;
    894             }
    895             strand.points = new float[strand.count * 4];
    896             strand.position = 0;
    897         }
    898         // Go through each segment and compute its points
    899         for (DNASegment segment : segments) {
    900             // Add the points to the strand of that color
    901             DNAStrand strand = strands.get(segment.color);
    902             int dayIndex = segment.day - firstJulianDay;
    903             int dayStartMinute = segment.startMinute % DAY_IN_MINUTES;
    904             int dayEndMinute = segment.endMinute % DAY_IN_MINUTES;
    905             int height = bottom - top;
    906             int workDayHeight = height * 3 / 4;
    907             int remainderHeight = (height - workDayHeight) / 2;
    908 
    909             int x = dayXs[dayIndex];
    910             int y0 = 0;
    911             int y1 = 0;
    912 
    913             y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight);
    914             y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight);
    915             if (DEBUG) {
    916                 Log.d(TAG, "Adding " + Integer.toHexString(segment.color) + " at x,y0,y1: " + x
    917                         + " " + y0 + " " + y1 + " for " + dayStartMinute + " " + dayEndMinute);
    918             }
    919             strand.points[strand.position++] = x;
    920             strand.points[strand.position++] = y0;
    921             strand.points[strand.position++] = x;
    922             strand.points[strand.position++] = y1;
    923         }
    924     }
    925 
    926     /**
    927      * Compute a pixel offset from the top for a given minute from the work day
    928      * height and the height of the top area.
    929      */
    930     private static int getPixelOffsetFromMinutes(int minute, int workDayHeight,
    931             int remainderHeight) {
    932         int y;
    933         if (minute < WORK_DAY_START_MINUTES) {
    934             y = minute * remainderHeight / WORK_DAY_START_MINUTES;
    935         } else if (minute < WORK_DAY_END_MINUTES) {
    936             y = remainderHeight + (minute - WORK_DAY_START_MINUTES) * workDayHeight
    937                     / WORK_DAY_MINUTES;
    938         } else {
    939             y = remainderHeight + workDayHeight + (minute - WORK_DAY_END_MINUTES) * remainderHeight
    940                     / WORK_DAY_END_LENGTH;
    941         }
    942         return y;
    943     }
    944 
    945     /**
    946      * Add a new segment based on the event provided. This will handle splitting
    947      * segments across day boundaries and ensures a minimum size for segments.
    948      */
    949     private static void addNewSegment(LinkedList<DNASegment> segments, Event event,
    950             HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes) {
    951         if (event.startDay > event.endDay) {
    952             Log.wtf(TAG, "Event starts after it ends: " + event.toString());
    953         }
    954         // If this is a multiday event split it up by day
    955         if (event.startDay != event.endDay) {
    956             Event lhs = new Event();
    957             lhs.color = event.color;
    958             lhs.startDay = event.startDay;
    959             // the first day we want the start time to be the actual start time
    960             lhs.startTime = event.startTime;
    961             lhs.endDay = lhs.startDay;
    962             lhs.endTime = DAY_IN_MINUTES - 1;
    963             // Nearly recursive iteration!
    964             while (lhs.startDay != event.endDay) {
    965                 addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes);
    966                 // The days in between are all day, even though that shouldn't
    967                 // actually happen due to the allday filtering
    968                 lhs.startDay++;
    969                 lhs.endDay = lhs.startDay;
    970                 lhs.startTime = 0;
    971                 minStart = 0;
    972             }
    973             // The last day we want the end time to be the actual end time
    974             lhs.endTime = event.endTime;
    975             event = lhs;
    976         }
    977         // Create the new segment and compute its fields
    978         DNASegment segment = new DNASegment();
    979         int dayOffset = (event.startDay - firstJulianDay) * DAY_IN_MINUTES;
    980         int endOfDay = dayOffset + DAY_IN_MINUTES - 1;
    981         // clip the start if needed
    982         segment.startMinute = Math.max(dayOffset + event.startTime, minStart);
    983         // and extend the end if it's too small, but not beyond the end of the
    984         // day
    985         int minEnd = Math.min(segment.startMinute + minMinutes, endOfDay);
    986         segment.endMinute = Math.max(dayOffset + event.endTime, minEnd);
    987         if (segment.endMinute > endOfDay) {
    988             segment.endMinute = endOfDay;
    989         }
    990 
    991         segment.color = event.color;
    992         segment.day = event.startDay;
    993         segments.add(segment);
    994         // increment the count for the correct color or add a new strand if we
    995         // don't have that color yet
    996         DNAStrand strand = getOrCreateStrand(strands, segment.color);
    997         strand.count++;
    998     }
    999 
   1000     /**
   1001      * Try to get a strand of the given color. Create it if it doesn't exist.
   1002      */
   1003     private static DNAStrand getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color) {
   1004         DNAStrand strand = strands.get(color);
   1005         if (strand == null) {
   1006             strand = new DNAStrand();
   1007             strand.color = color;
   1008             strand.count = 0;
   1009             strands.put(strand.color, strand);
   1010         }
   1011         return strand;
   1012     }
   1013 
   1014     /**
   1015      * Sends an intent to launch the top level Calendar view.
   1016      *
   1017      * @param context
   1018      */
   1019     public static void returnToCalendarHome(Context context) {
   1020         Intent launchIntent = new Intent(context, AllInOneActivity.class);
   1021         launchIntent.setAction(Intent.ACTION_DEFAULT);
   1022         launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
   1023         launchIntent.putExtra(INTENT_KEY_HOME, true);
   1024         context.startActivity(launchIntent);
   1025     }
   1026 
   1027     /**
   1028      * This sets up a search view to use Calendar's search suggestions provider
   1029      * and to allow refining the search.
   1030      *
   1031      * @param view The {@link SearchView} to set up
   1032      * @param act The activity using the view
   1033      */
   1034     public static void setUpSearchView(SearchView view, Activity act) {
   1035         SearchManager searchManager = (SearchManager) act.getSystemService(Context.SEARCH_SERVICE);
   1036         view.setSearchableInfo(searchManager.getSearchableInfo(act.getComponentName()));
   1037         view.setQueryRefinementEnabled(true);
   1038     }
   1039 
   1040     /**
   1041      * Given a context and a time in millis since unix epoch figures out the
   1042      * correct week of the year for that time.
   1043      *
   1044      * @param millisSinceEpoch
   1045      * @return
   1046      */
   1047     public static int getWeekNumberFromTime(long millisSinceEpoch, Context context) {
   1048         Time weekTime = new Time(getTimeZone(context, null));
   1049         weekTime.set(millisSinceEpoch);
   1050         weekTime.normalize(true);
   1051         int firstDayOfWeek = getFirstDayOfWeek(context);
   1052         // if the date is on Saturday or Sunday and the start of the week
   1053         // isn't Monday we may need to shift the date to be in the correct
   1054         // week
   1055         if (weekTime.weekDay == Time.SUNDAY
   1056                 && (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY)) {
   1057             weekTime.monthDay++;
   1058             weekTime.normalize(true);
   1059         } else if (weekTime.weekDay == Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) {
   1060             weekTime.monthDay += 2;
   1061             weekTime.normalize(true);
   1062         }
   1063         return weekTime.getWeekNumber();
   1064     }
   1065 
   1066     /**
   1067      * Formats a day of the week string. This is either just the name of the day
   1068      * or a combination of yesterday/today/tomorrow and the day of the week.
   1069      *
   1070      * @param julianDay The julian day to get the string for
   1071      * @param todayJulianDay The julian day for today's date
   1072      * @param millis A utc millis since epoch time that falls on julian day
   1073      * @param context The calling context, used to get the timezone and do the
   1074      *            formatting
   1075      * @return
   1076      */
   1077     public static String getDayOfWeekString(int julianDay, int todayJulianDay, long millis,
   1078             Context context) {
   1079         String tz = getTimeZone(context, null);
   1080         int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
   1081         String dayViewText;
   1082         if (julianDay == todayJulianDay) {
   1083             dayViewText = context.getString(R.string.agenda_today,
   1084                     mTZUtils.formatDateRange(context, millis, millis, flags).toString());
   1085         } else if (julianDay == todayJulianDay - 1) {
   1086             dayViewText = context.getString(R.string.agenda_yesterday,
   1087                     mTZUtils.formatDateRange(context, millis, millis, flags).toString());
   1088         } else if (julianDay == todayJulianDay + 1) {
   1089             dayViewText = context.getString(R.string.agenda_tomorrow,
   1090                     mTZUtils.formatDateRange(context, millis, millis, flags).toString());
   1091         } else {
   1092             dayViewText = mTZUtils.formatDateRange(context, millis, millis, flags).toString();
   1093         }
   1094         dayViewText = dayViewText.toUpperCase();
   1095         return dayViewText;
   1096     }
   1097 }
   1098