Utils.java revision 636269c7220a2b12e090cab43a91eb34922eb61f
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
17package com.android.calendar;
18
19import static android.provider.Calendar.EVENT_BEGIN_TIME;
20
21import com.android.calendar.CalendarController.ViewType;
22
23import android.app.Activity;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.content.res.Configuration;
28import android.database.Cursor;
29import android.database.MatrixCursor;
30import android.net.Uri;
31import android.os.Bundle;
32import android.text.TextUtils;
33import android.text.format.DateUtils;
34import android.text.format.Time;
35import android.util.Log;
36import com.android.calendar.CalendarUtils.TimeZoneUtils;
37
38import java.util.ArrayList;
39import java.util.Calendar;
40import java.util.Formatter;
41import java.util.Iterator;
42import java.util.List;
43import java.util.Map;
44
45public class Utils {
46    private static final boolean DEBUG = true;
47    private static final String TAG = "CalUtils";
48    // Set to 0 until we have UI to perform undo
49    public static final long UNDO_DELAY = 0;
50
51    // For recurring events which instances of the series are being modified
52    public static final int MODIFY_UNINITIALIZED = 0;
53    public static final int MODIFY_SELECTED = 1;
54    public static final int MODIFY_ALL_FOLLOWING = 2;
55    public static final int MODIFY_ALL = 3;
56
57    // When the edit event view finishes it passes back the appropriate exit
58    // code.
59    public static final int DONE_REVERT = 1 << 0;
60    public static final int DONE_SAVE = 1 << 1;
61    public static final int DONE_DELETE = 1 << 2;
62    // And should re run with DONE_EXIT if it should also leave the view, just
63    // exiting is identical to reverting
64    public static final int DONE_EXIT = 1 << 0;
65
66    protected static final String OPEN_EMAIL_MARKER = " <";
67    protected static final String CLOSE_EMAIL_MARKER = ">";
68
69    public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW";
70    public static final String INTENT_KEY_VIEW_TYPE = "VIEW";
71    public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY";
72
73    public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3;
74
75    // The name of the shared preferences file. This name must be maintained for
76    // historical
77    // reasons, as it's what PreferenceManager assigned the first time the file
78    // was created.
79    private static final String SHARED_PREFS_NAME = "com.android.calendar_preferences";
80
81    private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME);
82    private static boolean mAllowWeekForDetailView = false;
83    private static long mTardis = 0;
84
85    public static int getViewTypeFromIntentAndSharedPref(Activity activity) {
86        Intent intent = activity.getIntent();
87        Bundle extras = intent.getExtras();
88        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity);
89
90        if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) {
91            return ViewType.EDIT;
92        }
93        if (extras != null) {
94            if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) {
95                // This is the "detail" view which is either agenda or day view
96                return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW,
97                        GeneralPreferences.DEFAULT_DETAILED_VIEW);
98            } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) {
99                // Not sure who uses this. This logic came from LaunchActivity
100                return ViewType.DAY;
101            }
102        }
103
104        // Default to the last view
105        return prefs.getInt(
106                GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW);
107    }
108
109    /**
110     * Writes a new home time zone to the db. Updates the home time zone in the
111     * db asynchronously and updates the local cache. Sending a time zone of
112     * **tbd** will cause it to be set to the device's time zone. null or empty
113     * tz will be ignored.
114     *
115     * @param context The calling activity
116     * @param timeZone The time zone to set Calendar to, or **tbd**
117     */
118    public static void setTimeZone(Context context, String timeZone) {
119        mTZUtils.setTimeZone(context, timeZone);
120    }
121
122    /**
123     * Gets the time zone that Calendar should be displayed in This is a helper
124     * method to get the appropriate time zone for Calendar. If this is the
125     * first time this method has been called it will initiate an asynchronous
126     * query to verify that the data in preferences is correct. The callback
127     * supplied will only be called if this query returns a value other than
128     * what is stored in preferences and should cause the calling activity to
129     * refresh anything that depends on calling this method.
130     *
131     * @param context The calling activity
132     * @param callback The runnable that should execute if a query returns new
133     *            values
134     * @return The string value representing the time zone Calendar should
135     *         display
136     */
137    public static String getTimeZone(Context context, Runnable callback) {
138        return mTZUtils.getTimeZone(context, callback);
139    }
140
141    /**
142     * Formats a date or a time range according to the local conventions.
143     *
144     * @param context the context is required only if the time is shown
145     * @param startMillis the start time in UTC milliseconds
146     * @param endMillis the end time in UTC milliseconds
147     * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter,
148     * long, long, int, String) formatDateRange}
149     * @return a string containing the formatted date/time range.
150     */
151    public static String formatDateRange(
152            Context context, long startMillis, long endMillis, int flags) {
153        return mTZUtils.formatDateRange(context, startMillis, endMillis, flags);
154    }
155
156    public static String getSharedPreference(Context context, String key, String defaultValue) {
157        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
158        return prefs.getString(key, defaultValue);
159    }
160
161    public static int getSharedPreference(Context context, String key, int defaultValue) {
162        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
163        return prefs.getInt(key, defaultValue);
164    }
165
166    public static boolean getSharedPreference(Context context, String key, boolean defaultValue) {
167        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
168        return prefs.getBoolean(key, defaultValue);
169    }
170
171    /**
172     * Asynchronously sets the preference with the given key to the given value
173     *
174     * @param context the context to use to get preferences from
175     * @param key the key of the preference to set
176     * @param value the value to set
177     */
178    public static void setSharedPreference(Context context, String key, String value) {
179        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
180        prefs.edit().putString(key, value).apply();
181    }
182
183    protected static void tardis() {
184        mTardis = System.currentTimeMillis();
185    }
186
187    protected static long getTardis() {
188        return mTardis;
189    }
190
191    static void setSharedPreference(Context context, String key, boolean value) {
192        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
193        SharedPreferences.Editor editor = prefs.edit();
194        editor.putBoolean(key, value);
195        editor.apply();
196    }
197
198    static void setSharedPreference(Context context, String key, int value) {
199        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
200        SharedPreferences.Editor editor = prefs.edit();
201        editor.putInt(key, value);
202        editor.apply();
203    }
204
205    /**
206     * Save default agenda/day/week/month view for next time
207     *
208     * @param context
209     * @param viewId {@link CalendarController.ViewType}
210     */
211    static void setDefaultView(Context context, int viewId) {
212        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
213        SharedPreferences.Editor editor = prefs.edit();
214
215        boolean validDetailView = false;
216        if (mAllowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) {
217            validDetailView = true;
218        } else {
219            validDetailView = viewId == CalendarController.ViewType.AGENDA
220                    || viewId == CalendarController.ViewType.DAY;
221        }
222
223        if (validDetailView) {
224            // Record the detail start view
225            editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId);
226        }
227
228        // Record the (new) start view
229        editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId);
230        editor.apply();
231    }
232
233    public static MatrixCursor matrixCursorFromCursor(Cursor cursor) {
234        MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
235        int numColumns = cursor.getColumnCount();
236        String data[] = new String[numColumns];
237        cursor.moveToPosition(-1);
238        while (cursor.moveToNext()) {
239            for (int i = 0; i < numColumns; i++) {
240                data[i] = cursor.getString(i);
241            }
242            newCursor.addRow(data);
243        }
244        return newCursor;
245    }
246
247    /**
248     * Compares two cursors to see if they contain the same data.
249     *
250     * @return Returns true of the cursors contain the same data and are not
251     *         null, false otherwise
252     */
253    public static boolean compareCursors(Cursor c1, Cursor c2) {
254        if (c1 == null || c2 == null) {
255            return false;
256        }
257
258        int numColumns = c1.getColumnCount();
259        if (numColumns != c2.getColumnCount()) {
260            return false;
261        }
262
263        if (c1.getCount() != c2.getCount()) {
264            return false;
265        }
266
267        c1.moveToPosition(-1);
268        c2.moveToPosition(-1);
269        while (c1.moveToNext() && c2.moveToNext()) {
270            for (int i = 0; i < numColumns; i++) {
271                if (!TextUtils.equals(c1.getString(i), c2.getString(i))) {
272                    return false;
273                }
274            }
275        }
276
277        return true;
278    }
279
280    /**
281     * If the given intent specifies a time (in milliseconds since the epoch),
282     * then that time is returned. Otherwise, the current time is returned.
283     */
284    public static final long timeFromIntentInMillis(Intent intent) {
285        // If the time was specified, then use that. Otherwise, use the current
286        // time.
287        Uri data = intent.getData();
288        long millis = intent.getLongExtra(EVENT_BEGIN_TIME, -1);
289        if (millis == -1 && data != null && data.isHierarchical()) {
290            List<String> path = data.getPathSegments();
291            if (path.size() == 2 && path.get(0).equals("time")) {
292                try {
293                    millis = Long.valueOf(data.getLastPathSegment());
294                } catch (NumberFormatException e) {
295                    Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time "
296                            + "found. Using current time.");
297                }
298            }
299        }
300        if (millis <= 0) {
301            millis = System.currentTimeMillis();
302        }
303        return millis;
304    }
305
306    /**
307     * Formats the given Time object so that it gives the month and year (for
308     * example, "September 2007").
309     *
310     * @param time the time to format
311     * @return the string containing the weekday and the date
312     */
313    public static String formatMonthYear(Context context, Time time) {
314        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
315                | DateUtils.FORMAT_SHOW_YEAR;
316        long millis = time.toMillis(true);
317        return formatDateRange(context, millis, millis, flags);
318    }
319
320    /**
321     * Returns a list joined together by the provided delimiter, for example,
322     * ["a", "b", "c"] could be joined into "a,b,c"
323     *
324     * @param things the things to join together
325     * @param delim the delimiter to use
326     * @return a string contained the things joined together
327     */
328    public static String join(List<?> things, String delim) {
329        StringBuilder builder = new StringBuilder();
330        boolean first = true;
331        for (Object thing : things) {
332            if (first) {
333                first = false;
334            } else {
335                builder.append(delim);
336            }
337            builder.append(thing.toString());
338        }
339        return builder.toString();
340    }
341
342    /**
343     * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970)
344     * adjusted for first day of week.
345     *
346     * This takes a julian day and the week start day and calculates which
347     * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting
348     * at 0. *Do not* use this to compute the ISO week number for the year.
349     *
350     * @param julianDay The julian day to calculate the week number for
351     * @param firstDayOfWeek Which week day is the first day of the week,
352     *          see {@link Time#SUNDAY}
353     * @return Weeks since the epoch
354     */
355    public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
356        int diff = Time.THURSDAY - firstDayOfWeek;
357        if (diff < 0) {
358            diff += 7;
359        }
360        int refDay = Time.EPOCH_JULIAN_DAY - diff;
361        return (julianDay - refDay) / 7;
362    }
363
364    /**
365     * Takes a number of weeks since the epoch and calculates the Julian day of
366     * the Monday for that week.
367     *
368     * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY}
369     * is considered week 0. It returns the Julian day for the Monday
370     * {@code week} weeks after the Monday of the week containing the epoch.
371     *
372     * @param week Number of weeks since the epoch
373     * @return The julian day for the Monday of the given week since the epoch
374     */
375    public static int getJulianMondayFromWeeksSinceEpoch(int week) {
376        return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
377    }
378
379    /**
380     * Get first day of week as android.text.format.Time constant.
381     *
382     * @return the first day of week in android.text.format.Time
383     */
384    public static int getFirstDayOfWeek(Context context) {
385        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
386        String pref = prefs.getString(
387                GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT);
388
389        int startDay;
390        if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) {
391            startDay = Calendar.getInstance().getFirstDayOfWeek();
392        } else {
393            startDay = Integer.parseInt(pref);
394        }
395
396        if (startDay == Calendar.SATURDAY) {
397            return Time.SATURDAY;
398        } else if (startDay == Calendar.MONDAY) {
399            return Time.MONDAY;
400        } else {
401            return Time.SUNDAY;
402        }
403    }
404
405    /**
406     * @return true when week number should be shown.
407     */
408    public static boolean getShowWeekNumber(Context context) {
409        final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
410        return prefs.getBoolean(
411                GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM);
412    }
413
414    /**
415     * @return true when declined events should be hidden.
416     */
417    public static boolean getHideDeclinedEvents(Context context) {
418        final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
419        return prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false);
420    }
421
422    public static int getDaysPerWeek(Context context) {
423        final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
424        return prefs.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7);
425    }
426
427    /**
428     * Determine whether the column position is Saturday or not.
429     *
430     * @param column the column position
431     * @param firstDayOfWeek the first day of week in android.text.format.Time
432     * @return true if the column is Saturday position
433     */
434    public static boolean isSaturday(int column, int firstDayOfWeek) {
435        return (firstDayOfWeek == Time.SUNDAY && column == 6)
436                || (firstDayOfWeek == Time.MONDAY && column == 5)
437                || (firstDayOfWeek == Time.SATURDAY && column == 0);
438    }
439
440    /**
441     * Determine whether the column position is Sunday or not.
442     *
443     * @param column the column position
444     * @param firstDayOfWeek the first day of week in android.text.format.Time
445     * @return true if the column is Sunday position
446     */
447    public static boolean isSunday(int column, int firstDayOfWeek) {
448        return (firstDayOfWeek == Time.SUNDAY && column == 0)
449                || (firstDayOfWeek == Time.MONDAY && column == 6)
450                || (firstDayOfWeek == Time.SATURDAY && column == 1);
451    }
452
453    /**
454     * Convert given UTC time into current local time. This assumes it is for an
455     * allday event and will adjust the time to be on a midnight boundary.
456     *
457     * @param recycle Time object to recycle, otherwise null.
458     * @param utcTime Time to convert, in UTC.
459     * @param tz The time zone to convert this time to.
460     */
461    public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) {
462        if (recycle == null) {
463            recycle = new Time();
464        }
465        recycle.timezone = Time.TIMEZONE_UTC;
466        recycle.set(utcTime);
467        recycle.timezone = tz;
468        return recycle.normalize(true);
469    }
470
471    public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) {
472        if (recycle == null) {
473            recycle = new Time();
474        }
475        recycle.timezone = tz;
476        recycle.set(localTime);
477        recycle.timezone = Time.TIMEZONE_UTC;
478        return recycle.normalize(true);
479    }
480
481    /**
482     * Scan through a cursor of calendars and check if names are duplicated.
483     * This travels a cursor containing calendar display names and fills in the
484     * provided map with whether or not each name is repeated.
485     *
486     * @param isDuplicateName The map to put the duplicate check results in.
487     * @param cursor The query of calendars to check
488     * @param nameIndex The column of the query that contains the display name
489     */
490    public static void checkForDuplicateNames(
491            Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) {
492        isDuplicateName.clear();
493        cursor.moveToPosition(-1);
494        while (cursor.moveToNext()) {
495            String displayName = cursor.getString(nameIndex);
496            // Set it to true if we've seen this name before, false otherwise
497            if (displayName != null) {
498                isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName));
499            }
500        }
501    }
502
503    /**
504     * Null-safe object comparison
505     *
506     * @param s1
507     * @param s2
508     * @return
509     */
510    public static boolean equals(Object o1, Object o2) {
511        return o1 == null ? o2 == null : o1.equals(o2);
512    }
513
514    public static void setAllowWeekForDetailView(boolean allowWeekView) {
515        mAllowWeekForDetailView  = allowWeekView;
516    }
517
518    public static boolean getAllowWeekForDetailView() {
519        return mAllowWeekForDetailView;
520    }
521
522    public static boolean isMultiPaneConfiguration (Context c) {
523        return (c.getResources().getConfiguration().screenLayout &
524                Configuration.SCREENLAYOUT_SIZE_XLARGE) != 0;
525    }
526
527    public static boolean getConfigBool(Context c, int key) {
528        return c.getResources().getBoolean(key);
529    }
530
531
532     /**
533     * This is a helper class for the createBusyBitSegments method
534     * The class contains information about a specific time that corresponds to either a start
535     * of an event or an end of an event (or both):
536     * 1. The time itself
537     * 2 .The number of event starts and ends (number of starts - number of ends)
538     */
539
540    private static class BusyBitsEventTime {
541
542        public static final int EVENT_START = 1;
543        public static final int EVENT_END = -1;
544
545        public int mTime; // in minutes
546        // Number of events that start and end in this time (+1 for each start,
547        // -1 for each end)
548        public int mStartEndChanges;
549
550        public BusyBitsEventTime(int t, int c) {
551            mTime = t;
552            mStartEndChanges = c;
553        }
554
555        public void addStart() {
556            mStartEndChanges++;
557        }
558
559        public void addEnd() {
560            mStartEndChanges--;
561        }
562    }
563
564    /**
565     * Corrects segments that are overlapping.
566     * The function makes sure the last segment inserted do not overlap with segments in the
567     * segments arrays. It will compare the last inserted segment to last segment in both the
568     * busy array and conflicting array and make corrections to segments if necessary.
569     * The function assumes an overlap could be only 1 pixel.
570     * The function removes segments if necessary
571     * Segment size is from start to end (inclusive)
572     *
573     * @param segments segments an array of 2 float arrays. The first array will contain the
574     *        coordinates for drawing busy segments, the second will contain the coordinates for
575     *        drawing conflicting segments. The first cell in each array contains the number of
576     *        used cell so this method can be called again without overriding data,
577     * @param arrayIndex - index of the segments array that got the last segment
578     * @param prevSegmentInserted - an indicator of the type of the previous segment inserted. This
579     *
580     * @return boolean telling the calling functions whether to add the last segment or not.
581     *         The calling function should first insert a new segment to the array, call this
582     *         function and when getting a "true" value in the return value, update the counter of
583     *         the array to indicate a new segment (Add 4 to the counter in cell 0).
584     */
585
586    static final int START_PIXEL_Y = 1;        // index of pixel locations in a coordinates set
587    static final int END_PIXEL_Y = 3;
588    static final int BUSY_ARRAY_INDEX = 0;
589    static final int CONFLICT_ARRAY_INDEX = 1;
590    static final int COUNTER_INDEX = 0;
591
592    static final int NO_PREV_INSERTED = 0;    // possible status of previous segment insertion
593    static final int BUSY_PREV_INSERTED = 1;
594    static final int CONFLICT_PREV_INSERTED = 2;
595
596
597    public static boolean correctOverlappingSegment(float[][] segments,
598            int arrayIndex, int prevSegmentInserted) {
599
600        if (prevSegmentInserted == NO_PREV_INSERTED) {
601            // First segment - add it
602            return true;
603        }
604
605        // Previous insert and this one are to the busy array
606        if (prevSegmentInserted == BUSY_PREV_INSERTED && arrayIndex == BUSY_ARRAY_INDEX) {
607
608            // Index of last and previously inserted segment
609            int iLast = 1 + (int) segments[BUSY_ARRAY_INDEX][COUNTER_INDEX];
610            int iPrev = 1 + (int) segments[BUSY_ARRAY_INDEX][COUNTER_INDEX] - 4;
611
612            // Segments do not overlap - add the new one
613            if (segments[BUSY_ARRAY_INDEX][iPrev + END_PIXEL_Y] <
614                    segments[BUSY_ARRAY_INDEX][iLast + START_PIXEL_Y]) {
615                return true;
616            }
617
618            // Segments overlap - merge them
619            segments[BUSY_ARRAY_INDEX][iPrev + END_PIXEL_Y] =
620                    segments[BUSY_ARRAY_INDEX][iLast + END_PIXEL_Y];
621            return false;
622        }
623
624        // Previous insert was to the busy array and this one is to the conflict array
625        if (prevSegmentInserted == BUSY_PREV_INSERTED && arrayIndex == CONFLICT_ARRAY_INDEX) {
626
627            // Index of last and previously inserted segment
628            int iLast = 1 + (int) segments[CONFLICT_ARRAY_INDEX][COUNTER_INDEX];
629            int iPrev = 1 + (int) segments[BUSY_ARRAY_INDEX][COUNTER_INDEX] - 4;
630
631            // Segments do not overlap - add the new one
632            if (segments[BUSY_ARRAY_INDEX][iPrev + END_PIXEL_Y] <
633                    segments[CONFLICT_ARRAY_INDEX][iLast + START_PIXEL_Y]) {
634                return true;
635            }
636
637            // Segments overlap - truncate the end of the last busy segment
638            // if it disappears , remove it
639            segments[BUSY_ARRAY_INDEX][iPrev + END_PIXEL_Y]--;
640            if (segments[BUSY_ARRAY_INDEX][iPrev + END_PIXEL_Y] <
641                    segments[BUSY_ARRAY_INDEX][iPrev + START_PIXEL_Y]) {
642                segments[BUSY_ARRAY_INDEX] [COUNTER_INDEX] -= 4;
643            }
644            return true;
645        }
646        // Previous insert was to the conflict array and this one is to the busy array
647        if (prevSegmentInserted == CONFLICT_PREV_INSERTED && arrayIndex == BUSY_ARRAY_INDEX) {
648
649            // Index of last and previously inserted segment
650            int iLast = 1 + (int) segments[BUSY_ARRAY_INDEX][COUNTER_INDEX];
651            int iPrev = 1 + (int) segments[CONFLICT_ARRAY_INDEX][COUNTER_INDEX] - 4;
652
653            // Segments do not overlap - add the new one
654            if (segments[CONFLICT_ARRAY_INDEX][iPrev + END_PIXEL_Y] <
655                    segments[BUSY_ARRAY_INDEX][iLast + START_PIXEL_Y]) {
656                return true;
657            }
658
659            // Segments overlap - truncate the new busy segment , if it disappears , do not
660            // insert it
661            segments[BUSY_ARRAY_INDEX][iLast + START_PIXEL_Y]++;
662            if (segments[BUSY_ARRAY_INDEX][iLast + START_PIXEL_Y] >
663                segments[BUSY_ARRAY_INDEX][iLast + END_PIXEL_Y]) {
664                return false;
665            }
666            return true;
667
668        }
669        // Previous insert and this one are to the conflict array
670        if (prevSegmentInserted == CONFLICT_PREV_INSERTED && arrayIndex == CONFLICT_ARRAY_INDEX) {
671
672            // Index of last and previously inserted segment
673            int iLast = 1 + (int) segments[CONFLICT_ARRAY_INDEX][COUNTER_INDEX];
674            int iPrev = 1 + (int) segments[CONFLICT_ARRAY_INDEX][COUNTER_INDEX] - 4;
675
676            // Segments do not overlap - add the new one
677            if (segments[CONFLICT_ARRAY_INDEX][iPrev + END_PIXEL_Y] <
678                    segments[CONFLICT_ARRAY_INDEX][iLast + START_PIXEL_Y]) {
679                return true;
680            }
681
682            // Segments overlap - merge them
683            segments[CONFLICT_ARRAY_INDEX][iPrev + END_PIXEL_Y] =
684                    segments[CONFLICT_ARRAY_INDEX][iLast + END_PIXEL_Y];
685            return false;
686        }
687        // Unknown state , complain
688        Log.wtf(TAG, "Unkown state in correctOverlappingSegment: prevSegmentInserted = " +
689                prevSegmentInserted + " arrayIndex = " + arrayIndex);
690        return false;
691    }
692
693    /**
694     * Converts a list of events to a list of busy segments to draw.
695     * Assumes list is ordered according to start time of events
696     * The function processes events of a specific day only or part of that day
697     *
698     * The algorithm goes over all the events and creates an ordered list of times.
699     * Each item on the list corresponds to a time where an event started,ended or both.
700     * The item has a count of how many events started and how many events ended at that time.
701     * In the second stage, the algorithm go over the list of times and finds what change happened
702     * at each time. A change can be a switch between either of the free time/busy time/conflicting
703     * time. Every time a change happens, the algorithm creates a segment (in pixels) to be
704     * displayed with the relevant status (free/busy/conflicting).
705     * The algorithm also checks if segments overlap and truncates one of them if needed.
706     *
707     * @param startPixel defines the start of the draw area
708     * @param endPixel defines the end of the draw area
709     * @param xPixel the middle X position of the draw area
710     * @param startTimeMinute start time (in minutes) of the time frame to be displayed as busy bits
711     * @param endTimeMinute end time (in minutes) of the time frame to be displayed as busy bits
712     * @param julianDay the day of the time frame
713     * @param daysEvents - a list of events that took place in the specified day (including
714     *                     recurring events, events that start before the day and/or end after
715     *                     the day
716     * @param segments an array of 2 float arrays. The first array will contain the coordinates
717     *        for drawing busy segments, the second will contain the coordinates for drawing
718     *        conflicting segments. The first cell in each array contains the number of used cell
719     *        so this method can be called again without overriding data,
720     *
721     */
722
723    public static void createBusyBitSegments(int startPixel, int endPixel,
724            int xPixel, int startTimeMinute, int endTimeMinute, int julianDay,
725            ArrayList<Event> daysEvents, float [] [] segments) {
726
727        // No events or illegal parameters , do nothing
728
729        if (daysEvents == null || daysEvents.size() == 0 || startPixel >= endPixel ||
730                startTimeMinute < 0 || startTimeMinute > 24 * 60 || endTimeMinute < 0 ||
731                endTimeMinute > 24 * 60 || startTimeMinute >= endTimeMinute ||
732                segments == null || segments [0] == null || segments [1] == null) {
733            Log.wtf(TAG, "Illegal parameter in createBusyBitSegments,  " +
734                    "daysEvents = " + daysEvents + " , " +
735                    "startPixel = " + startPixel + " , " +
736                    "endPixel = " + endPixel + " , " +
737                    "startTimeMinute = " + startTimeMinute + " , " +
738                    "endTimeMinute = " + endTimeMinute + " , " +
739                    "segments" + segments);
740            return;
741        }
742
743        // Go over all events and create a sorted list of times that include all
744        // the start and end times of all events.
745
746        ArrayList<BusyBitsEventTime> times = new ArrayList<BusyBitsEventTime>();
747
748        Iterator<Event> iter = daysEvents.iterator();
749        // Pointer to the search start in the "times" list. It prevents searching from the beginning
750        // of the list for each event. It is updated every time a new start time is inserted into
751        // the times list, since the events are time ordered, there is no point on searching before
752        // the last start time that was inserted
753        int initialSearchIndex = 0;
754        while (iter.hasNext()) {
755            Event event = iter.next();
756
757            // Take into account the start and end day. This is important for events that span
758            // multiple days.
759            int eStart = event.startTime - (julianDay - event.startDay) * 24 * 60;
760            int eEnd = event.endTime + (event.endDay - julianDay) * 24 * 60;
761
762            // Skip all day events, and events that are not in the time frame
763            if (event.drawAsAllday() || eStart >= endTimeMinute || eEnd <= startTimeMinute) {
764                continue;
765            }
766
767            // If event spans before or after start or end time , truncate it
768            // because we care only about the time span that is passed to the function
769            if (eStart < startTimeMinute) {
770                eStart = startTimeMinute;
771            }
772            if (eEnd > endTimeMinute) {
773                eEnd = endTimeMinute;
774            }
775            // Skip events that are zero length
776            if (eStart == eEnd) {
777                continue;
778            }
779
780            // First event , just put it in the "times" list
781            if (times.size() == 0) {
782                BusyBitsEventTime es = new BusyBitsEventTime(eStart, BusyBitsEventTime.EVENT_START);
783                BusyBitsEventTime ee = new BusyBitsEventTime(eEnd, BusyBitsEventTime.EVENT_END);
784                times.add(es);
785                times.add(ee);
786                continue;
787            }
788
789            // Insert start and end times of event in "times" list.
790            // Loop through the "times" list and put the event start and ends times in the correct
791            // place.
792            boolean startInserted = false;
793            boolean endInserted = false;
794            int i = initialSearchIndex; // Skip times that are before the event time
795            // Two pointers for looping through the "times" list. Current item and next item.
796            int t1, t2;
797            do {
798                t1 = times.get(i).mTime;
799                t2 = times.get(i + 1).mTime;
800                if (!startInserted) {
801                    // Start time equals an existing item in the "times" list, just update the
802                    // starts count of the specific item
803                    if (eStart == t1) {
804                        times.get(i).addStart();
805                        initialSearchIndex = i;
806                        startInserted = true;
807                    } else if (eStart == t2) {
808                        times.get(i + 1).addStart();
809                        initialSearchIndex = i + 1;
810                        startInserted = true;
811                    } else if (eStart > t1 && eStart < t2) {
812                        // The start time is between the times of the current item and next item:
813                        // insert a new start time in between the items.
814                        BusyBitsEventTime e = new BusyBitsEventTime(eStart,
815                                BusyBitsEventTime.EVENT_START);
816                        times.add(i + 1, e);
817                        initialSearchIndex = i + 1;
818                        t2 = eStart;
819                        startInserted = true;
820                    }
821                }
822                if (!endInserted) {
823                    // End time equals an existing item in the "times" list, just update the
824                    // ends count of the specific item
825                    if (eEnd == t1) {
826                        times.get(i).addEnd();
827                        endInserted = true;
828                    } else if (eEnd == t2) {
829                        times.get(i + 1).addEnd();
830                        endInserted = true;
831                    } else if (eEnd > t1 && eEnd < t2) {
832                        // The end time is between the times of the current item and next item:
833                        // insert a new end time in between the items.
834                        BusyBitsEventTime e = new BusyBitsEventTime(eEnd,
835                                BusyBitsEventTime.EVENT_END);
836                        times.add(i + 1, e);
837                        t2 = eEnd;
838                        endInserted = true;
839                    }
840                }
841                i++;
842            } while (!endInserted && i + 1 < times.size());
843
844            // Deal with the last event if not inserted in the list
845            if (!startInserted) {
846                BusyBitsEventTime e = new BusyBitsEventTime(eStart, BusyBitsEventTime.EVENT_START);
847                times.add(e);
848                initialSearchIndex = times.size() - 1;
849            }
850            if (!endInserted) {
851                BusyBitsEventTime e = new BusyBitsEventTime(eEnd, BusyBitsEventTime.EVENT_END);
852                times.add(e);
853            }
854        }
855
856        // No events , return
857        if (times.size() == 0) {
858            return;
859        }
860
861        // Loop through the created "times" list and find busy time segments and conflicting
862        // segments. In the loop, keep the status of time (free/busy/conflicting) and the time
863        // of when last status started. When there is a change in the status, create a segment with
864        // the previous status from the time of the last status started until the time of the
865        // current change.
866        // The loop keeps a count of how many events are conflicting. Zero means free time, one
867        // means a busy time and more than one means conflicting time. The count is updated by
868        // the number of starts and ends from the items in the "times" list. A change is a switch
869        // from free/busy/conflicting status to a different one.
870
871
872        int segmentStartTime = 0;  // default start time
873        int conflictingCount = 0;   // assume starting with free time
874        int pixelSize = endPixel - startPixel;
875        int timeFrame = endTimeMinute - startTimeMinute;
876        int prevSegmentInserted = NO_PREV_INSERTED;
877
878
879        // Arrays are preallocated by the calling code, the first cell in the
880        // array is the number
881        // of already occupied cells.
882        float[] busySegments = segments[BUSY_ARRAY_INDEX];
883        float[] conflictSegments = segments[CONFLICT_ARRAY_INDEX];
884
885        Iterator<BusyBitsEventTime> tIter = times.iterator();
886        while (tIter.hasNext()) {
887            BusyBitsEventTime t = tIter.next();
888            // Get the new count of conflicting events
889            int newCount = conflictingCount + t.mStartEndChanges;
890
891            // No need for a new segment because the free/busy/conflicting
892            // status didn't change
893            if (conflictingCount == newCount || (conflictingCount >= 2 && newCount >= 2)) {
894                conflictingCount = newCount;
895                continue;
896            }
897            if (conflictingCount == 0 && newCount == 1) {
898                // A busy time started - start a new segment
899                if (segmentStartTime != 0) {
900                    // Unknown status, blow up
901                    Log.wtf(TAG, "Unknown state in createBusyBitSegments, segmentStartTime = " +
902                            segmentStartTime + ", nolc = " + newCount);
903                }
904                segmentStartTime = t.mTime;
905            } else if (conflictingCount == 0 && newCount >= 2) {
906                // An conflicting time started - start a new segment
907                if (segmentStartTime != 0) {
908                    // Unknown status, blow up
909                    Log.wtf(TAG, "Unknown state in createBusyBitSegments, segmentStartTime = " +
910                            segmentStartTime + ", nolc = " + newCount);
911                }
912                segmentStartTime = t.mTime;
913            } else if (conflictingCount == 1 && newCount >= 2) {
914                // A busy time ended and conflicting segment started,
915                // Save busy segment and start conflicting segment
916                int iBusy = 1 + (int) busySegments[COUNTER_INDEX];
917                busySegments[iBusy++] = xPixel;
918                busySegments[iBusy++] = (segmentStartTime - startTimeMinute) *
919                        pixelSize / timeFrame + startPixel;
920                busySegments[iBusy++] = xPixel;
921                busySegments[iBusy++] = (t.mTime - startTimeMinute) *
922                        pixelSize / timeFrame + startPixel;
923                // Update the segments counter only after overlap correction
924                if (correctOverlappingSegment(segments, BUSY_ARRAY_INDEX, prevSegmentInserted)) {
925                    busySegments[COUNTER_INDEX] += 4;
926                }
927                segmentStartTime = t.mTime;
928                prevSegmentInserted = BUSY_PREV_INSERTED;
929            } else if (conflictingCount >= 2 && newCount == 1) {
930                // A conflicting time ended and busy segment started.
931                // Save conflicting segment and start busy segment
932                int iConflicting = 1 + (int) conflictSegments[COUNTER_INDEX];
933                conflictSegments[iConflicting++] = xPixel;
934                conflictSegments[iConflicting++] = (segmentStartTime - startTimeMinute) *
935                        pixelSize / timeFrame + startPixel;
936                conflictSegments[iConflicting++] = xPixel;
937                conflictSegments[iConflicting++] = (t.mTime - startTimeMinute) *
938                        pixelSize / timeFrame + startPixel;
939                // Update the segments counter only after overlap correction
940                if (correctOverlappingSegment(segments, CONFLICT_ARRAY_INDEX,
941                        prevSegmentInserted)) {
942                    conflictSegments[COUNTER_INDEX] += 4;
943                }
944                segmentStartTime = t.mTime;
945                prevSegmentInserted = CONFLICT_PREV_INSERTED;
946            } else if (conflictingCount >= 2 && newCount == 0) {
947                // An conflicting segment ended, and a free time segment started
948                // Save conflicting segment
949                int iConflicting = 1 + (int) conflictSegments[COUNTER_INDEX];
950                conflictSegments[iConflicting++] = xPixel;
951                conflictSegments[iConflicting++] = (segmentStartTime - startTimeMinute) *
952                        pixelSize / timeFrame + startPixel;
953                conflictSegments[iConflicting++] = xPixel;
954                conflictSegments[iConflicting++] = (t.mTime - startTimeMinute) *
955                        pixelSize / timeFrame + startPixel;
956                // Update the segments counter only after overlap correction
957                if (correctOverlappingSegment(segments, CONFLICT_ARRAY_INDEX,
958                        prevSegmentInserted)) {
959                    conflictSegments[COUNTER_INDEX] += 4;
960                }
961                segmentStartTime = 0;
962                prevSegmentInserted = CONFLICT_PREV_INSERTED;
963            } else if (conflictingCount == 1 && newCount == 0) {
964                // A busy segment ended, and a free time segment started, save
965                // busy segment
966                int iBusy = 1 + (int) busySegments[COUNTER_INDEX];
967                busySegments[iBusy++] = xPixel;
968                busySegments[iBusy++] = (segmentStartTime - startTimeMinute) *
969                        pixelSize / timeFrame + startPixel;
970                busySegments[iBusy++] = xPixel;
971                busySegments[iBusy++] = (t.mTime - startTimeMinute) *
972                        pixelSize / timeFrame + startPixel;
973                // Update the segments counter only after overlap correction
974                if (correctOverlappingSegment(segments, BUSY_ARRAY_INDEX, prevSegmentInserted)) {
975                    busySegments[COUNTER_INDEX] += 4;
976                }
977                segmentStartTime = 0;
978                prevSegmentInserted = BUSY_PREV_INSERTED;
979            } else {
980                // Unknown status, blow up
981                Log.wtf(TAG, "Unknown state in createBusyBitSegments: time = " + t.mTime +
982                        " , olc = " + conflictingCount + " nolc = " + newCount);
983            }
984            conflictingCount = newCount; // Update count
985        }
986        return;
987    }
988}
989