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