Utils.java revision 72a9459e1f4cec02ad9e8dbdf824d66920b762ee
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     * Helper class for createBusyBitSegments method.
535     * Contains information about a segment of time (in pixels):
536     * 1. start and end of area to draw.
537     * 2. an indication if the segment represent a period of time with overlapping events (so that
538     *    the drawing function can draw it in a different way)
539     */
540
541    public static class BusyBitsSegment {
542        private int mStartPixel, mEndPixel;
543        private boolean mIsOverlapping;
544
545        public int getStart() {
546            return mStartPixel;
547        }
548
549        public void setStart(int start) {
550            this.mStartPixel = start;
551        }
552
553        public int getEnd() {
554            return mEndPixel;
555        }
556
557        public void setEnd(int end) {
558            this.mEndPixel = end;
559        }
560
561        public boolean isOverlapping() {
562            return mIsOverlapping;
563        }
564
565        public void setIsOverlapping(boolean isOverlapping) {
566            this.mIsOverlapping = isOverlapping;
567        }
568
569        public BusyBitsSegment(int s, int e, boolean isOverlapping) {
570            mStartPixel = s;
571            mEndPixel = e;
572            mIsOverlapping = isOverlapping;
573        }
574
575        @Override
576        public int hashCode() {
577            final int prime = 31;
578            int result = 1;
579            result = prime * result + mEndPixel;
580            result = prime * result + (mIsOverlapping ? 1231 : 1237);
581            result = prime * result + mStartPixel;
582            return result;
583        }
584
585        @Override
586        public boolean equals(Object obj) {
587            if (this == obj) {
588                return true;
589            }
590            if (obj == null) {
591                return false;
592            }
593            if (getClass() != obj.getClass()) {
594                return false;
595            }
596            BusyBitsSegment other = (BusyBitsSegment) obj;
597            if (mEndPixel != other.mEndPixel) {
598                return false;
599            }
600            if (mIsOverlapping != other.mIsOverlapping) {
601                return false;
602            }
603            if (mStartPixel != other.mStartPixel) {
604                return false;
605            }
606            return true;
607        }
608    }
609
610
611    /**
612     * This is a helper class for the createBusyBitSegments method
613     * The class contains information about a specific time that corresponds to either a start
614     * of an event or an end of an event (or both):
615     * 1. The time itself
616     * 2 .The number of event starts and ends (number of starts - number of ends)
617     */
618
619    private static class BusyBitsEventTime {
620
621        public static final int EVENT_START = 1;
622        public static final int EVENT_END = -1;
623
624        public int mTime; // in minutes
625        // Number of events that start and end in this time (+1 for each start,
626        // -1 for each end)
627        public int mStartEndChanges;
628
629        public BusyBitsEventTime(int t, int c) {
630            mTime = t;
631            mStartEndChanges = c;
632        }
633
634        public void addStart() {
635            mStartEndChanges++;
636        }
637
638        public void addEnd() {
639            mStartEndChanges--;
640        }
641    }
642
643    /**
644     * Corrects segments that are overlapping.
645     * The function makes sure the last two segments do not overlap (meaning:
646     * the start pixel of the last segment is bigger than the end pixel of the
647     * "one before last" segment.
648     * The function assumes an overlap could be only 1 pixel.
649     * The function removes segments if necessary
650     * Segment size is from start to end (inclusive)
651     *
652     * @param segments a list of BusyBitsSegment
653     */
654
655    public static void correctOverlappingSegment(ArrayList<BusyBitsSegment> segments) {
656
657        if (segments.size() <= 1)
658            return;
659
660        BusyBitsSegment seg1 = segments.get(segments.size() - 2);
661        BusyBitsSegment seg2 = segments.get(segments.size() - 1);
662
663        // If segments do not touch, no need to change
664        if (seg1.getEnd() < seg2.getStart()) {
665            return;
666        }
667        // If segments are identical , remove the last one
668        // This can only happen if both segments are the size of 1 pixel
669        if (seg1.equals(seg2)) {
670            segments.remove(segments.size() - 1);
671            return;
672        }
673
674        // Always prefer an overlapping segment to non-overlapping one
675        // If by cropping a segment it disappears (start > end), remove it (OK if start == end,
676        // because it is a 1 pixel segment)
677        if (seg1.isOverlapping()) {
678            seg2.setStart(seg2.getStart() + 1);
679            if (seg2.getStart() > seg2.getEnd()) {
680                segments.remove(segments.size() - 1);
681            }
682            return;
683        } else if (seg2.isOverlapping()) {
684            seg1.setEnd(seg1.getEnd() - 1);
685            if (seg1.getStart() > seg1.getEnd()) {
686                segments.remove(segments.size() - 2);
687            }
688            return;
689        } else {
690            // same kind of segments , just shorten the last one
691            seg2.setStart(seg2.getStart() + 1);
692            if (seg2.getStart() > seg2.getEnd()) {
693                segments.remove(segments.size() - 1);
694            }
695        }
696    }
697
698
699    /**
700     * Converts a list of events to a list of busy segments to draw.
701     * Assumes list is ordered according to start time of events
702     * The function processes events of a specific day only or part of that day
703     *
704     * The algorithm goes over all the events and creates an ordered list of times.
705     * Each item on the list corresponds to a time where an event started,ended or both.
706     * The item has a count of how many events started and how many events ended at that time.
707     * In the second stage, the algorithm go over the list of times and finds what change happened
708     * at each time. A change can be a switch between either of the free time/busy time/overlapping
709     * time. Every time a change happens, the algorithm creates a segment (in pixels) to be
710     * displayed with the relevant status (free/busy/overlapped).
711     * The algorithm also checks if segments overlap and truncates one of them if needed.
712     *
713     * @param startPixel defines the start of the draw area
714     * @param endPixel defines the end of the draw area
715     * @param startTimeMinute start time (in minutes) of the time frame to be displayed as busy bits
716     * @param endTimeMinute end time (in minutes) of the time frame to be displayed as busy bits
717     * @param julianDay the day of the time frame
718     * @param daysEvents - a list of events that took place in the specified day (including
719     *                     recurring events, events that start before the day and/or end after
720     *                     the day
721
722     * @return A list of segments to draw. Each segment includes the start and end
723     *         pixels (inclusive).
724     */
725
726    public static ArrayList<BusyBitsSegment> createBusyBitSegments(int startPixel, int endPixel,
727            int startTimeMinute, int endTimeMinute, int julianDay,
728            ArrayList<Event> daysEvents) {
729
730        // No events or illegal parameters , do nothing
731
732        if (daysEvents == null || daysEvents.size() == 0 || startPixel >= endPixel ||
733                startTimeMinute < 0 || startTimeMinute > 24 * 60 || endTimeMinute < 0 ||
734                endTimeMinute > 24 * 60 || startTimeMinute >= endTimeMinute) {
735            Log.wtf(TAG, "Illegal parameter in createBusyBitSegments,  " +
736                    "daysEvents = " + daysEvents + " , " +
737                    "startPixel = " + startPixel + " , " +
738                    "endPixel = " + endPixel + " , " +
739                    "startTimeMinute = " + startTimeMinute + " , " +
740                    "endTimeMinute = " + endTimeMinute + " , ");
741            return null;
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 null;
860        }
861
862        // Loop through the created "times" list and find busy time segments and overlapping
863        // segments. In the loop, keep the status of time (free/busy/overlapping) 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 overlapping. Zero means free time, one
868        // means a busy time and more than one means overlapping 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/overlap status to a different one.
871
872        ArrayList<BusyBitsSegment> segments = new ArrayList<BusyBitsSegment>();
873
874        int segmentStartTime = 0;  // default start time
875        int overlappedCount = 0;   // assume starting with free time
876        int pixelSize = endPixel - startPixel;
877        int timeFrame = endTimeMinute - startTimeMinute;
878
879        Iterator<BusyBitsEventTime> tIter = times.iterator();
880        while (tIter.hasNext()) {
881            BusyBitsEventTime t = tIter.next();
882            // Get the new count of overlapping events
883            int newCount = overlappedCount + t.mStartEndChanges;
884
885            // No need for a new segment because the free/busy/overlapping status didn't change
886            if (overlappedCount == newCount || (overlappedCount >= 2 && newCount >= 2)) {
887                overlappedCount = newCount;
888                continue;
889            }
890            if (overlappedCount == 0 && newCount == 1) {
891                // A busy time started - start a new segment
892                if (segmentStartTime != 0) {
893                    // Unknown status, blow up
894                    Log.wtf(TAG, "Unknown state in createBusyBitSegments, segmentStartTime = " +
895                            segmentStartTime + ", nolc = " + newCount);
896                }
897                segmentStartTime = t.mTime;
898            } else if (overlappedCount == 0 && newCount >= 2) {
899                // An overlapping 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 (overlappedCount == 1 && newCount >= 2) {
907                // A busy time ended and overlapping segment started,
908                // Save busy segment and start overlapping segment
909                BusyBitsSegment s = new BusyBitsSegment(
910                        (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel,
911                        (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, false);
912                segments.add(s);
913                correctOverlappingSegment(segments);
914                segmentStartTime = t.mTime;
915            } else if (overlappedCount >= 2 && newCount == 1) {
916                // An overlapping time ended and busy segment started.
917                // Save overlapping segment and start busy segment
918                BusyBitsSegment s = new BusyBitsSegment(
919                        (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel,
920                        (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, true);
921                segments.add(s);
922                correctOverlappingSegment(segments);
923                segmentStartTime = t.mTime;
924            } else if (overlappedCount >= 2 && newCount == 0) {
925                // An overlapping segment ended, and a free time segment started
926                // Save overlapping segment
927                BusyBitsSegment s = new BusyBitsSegment(
928                        (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel,
929                        (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, true);
930                segments.add(s);
931                correctOverlappingSegment(segments);
932                segmentStartTime = 0;
933            } else if (overlappedCount == 1 && newCount == 0) {
934                // A busy segment ended, and a free time segment started, save busy segment
935                BusyBitsSegment s = new BusyBitsSegment(
936                        (segmentStartTime - startTimeMinute) * pixelSize / timeFrame + startPixel,
937                        (t.mTime - startTimeMinute) * pixelSize / timeFrame + startPixel, false);
938                segments.add(s);
939                correctOverlappingSegment(segments);
940                segmentStartTime = 0;
941            } else {
942                // Unknown status, blow up
943                Log.wtf(TAG, "Unknown state in createBusyBitSegments: time = " + t.mTime +
944                        " , olc = " + overlappedCount + " nolc = " + newCount);
945            }
946            overlappedCount = newCount; // Update count
947        }
948        return segments;
949    }
950}
951