• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
getViewTypeFromIntentAndSharedPref(Activity activity)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      */
getWidgetUpdateAction(Context context)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      */
getWidgetScheduledUpdateAction(Context context)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      */
getSearchAuthority(Context context)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      */
setTimeZone(Context context, String timeZone)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      */
getTimeZone(Context context, Runnable callback)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      */
formatDateRange( Context context, long startMillis, long endMillis, int flags)196     public static String formatDateRange(
197             Context context, long startMillis, long endMillis, int flags) {
198         return mTZUtils.formatDateRange(context, startMillis, endMillis, flags);
199     }
200 
getSharedPreference(Context context, String key, String defaultValue)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 
getSharedPreference(Context context, String key, int defaultValue)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 
getSharedPreference(Context context, String key, boolean defaultValue)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      */
setSharedPreference(Context context, String key, String value)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 
tardis()228     protected static void tardis() {
229         mTardis = System.currentTimeMillis();
230     }
231 
getTardis()232     protected static long getTardis() {
233         return mTardis;
234     }
235 
setSharedPreference(Context context, String key, boolean value)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 
setSharedPreference(Context context, String key, int value)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      */
setDefaultView(Context context, int viewId)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 
matrixCursorFromCursor(Cursor cursor)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      */
compareCursors(Cursor c1, Cursor c2)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      */
timeFromIntentInMillis(Intent intent)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      */
formatMonthYear(Context context, Time time)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      */
join(List<?> things, String delim)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      */
getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek)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      */
getJulianMondayFromWeeksSinceEpoch(int week)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      */
getFirstDayOfWeek(Context context)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      */
getShowWeekNumber(Context context)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      */
getHideDeclinedEvents(Context context)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 
getDaysPerWeek(Context context)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      */
isSaturday(int column, int firstDayOfWeek)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      */
isSunday(int column, int firstDayOfWeek)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      */
convertAlldayUtcToLocal(Time recycle, long utcTime, String tz)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 
convertAlldayLocalToUTC(Time recycle, long localTime, String tz)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      */
checkForDuplicateNames( Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex)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      */
equals(Object o1, Object o2)555     public static boolean equals(Object o1, Object o2) {
556         return o1 == null ? o2 == null : o1.equals(o2);
557     }
558 
setAllowWeekForDetailView(boolean allowWeekView)559     public static void setAllowWeekForDetailView(boolean allowWeekView) {
560         mAllowWeekForDetailView  = allowWeekView;
561     }
562 
getAllowWeekForDetailView()563     public static boolean getAllowWeekForDetailView() {
564         return mAllowWeekForDetailView;
565     }
566 
isMultiPaneConfiguration(Context c)567     public static boolean isMultiPaneConfiguration (Context c) {
568         return (c.getResources().getConfiguration().screenLayout &
569                 Configuration.SCREENLAYOUT_SIZE_XLARGE) != 0;
570     }
571 
getConfigBool(Context c, int key)572     public static boolean getConfigBool(Context c, int key) {
573         return c.getResources().getBoolean(key);
574     }
575 
getDisplayColorFromColor(int color)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.
getDeclinedColorFromColor(int color)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      */
createDNAStrands(int firstJulianDay, ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs, Context context)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
addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands, int firstJulianDay, int numDays)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
weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay, HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs)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      */
getPixelOffsetFromMinutes(int minute, int workDayHeight, int remainderHeight)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      */
addNewSegment(LinkedList<DNASegment> segments, Event event, HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes)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      */
getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color)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      */
returnToCalendarHome(Context context)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      */
setUpSearchView(SearchView view, Activity act)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      */
getWeekNumberFromTime(long millisSinceEpoch, Context context)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      */
getDayOfWeekString(int julianDay, int todayJulianDay, long millis, Context context)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