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