Utils.java revision 63974da72657fd9b9631a75dcc46eb9298c81013
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 android.content.AsyncQueryHandler;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.database.Cursor;
28import android.database.MatrixCursor;
29import android.graphics.drawable.Drawable;
30import android.graphics.drawable.GradientDrawable;
31import android.net.Uri;
32import android.text.TextUtils;
33import android.text.format.DateUtils;
34import android.text.format.Time;
35import android.util.Log;
36import android.view.animation.AlphaAnimation;
37import android.widget.ViewFlipper;
38
39import java.util.Calendar;
40import java.util.Formatter;
41import java.util.HashSet;
42import java.util.List;
43import java.util.Locale;
44import java.util.Map;
45
46public class Utils {
47    private static final boolean DEBUG = true;
48    private static final String TAG = "CalUtils";
49    private static final int CLEAR_ALPHA_MASK = 0x00FFFFFF;
50    private static final int HIGH_ALPHA = 255 << 24;
51    private static final int MED_ALPHA = 180 << 24;
52    private static final int LOW_ALPHA = 150 << 24;
53
54    protected static final String OPEN_EMAIL_MARKER = " <";
55    protected static final String CLOSE_EMAIL_MARKER = ">";
56    /* The corner should be rounded on the top right and bottom right */
57    private static final float[] CORNERS = new float[] {0, 0, 5, 5, 5, 5, 0, 0};
58
59    // TODO switch these to use Calendar.java when it gets added
60    private static final String TIMEZONE_COLUMN_KEY = "key";
61    private static final String TIMEZONE_COLUMN_VALUE = "value";
62    private static final String TIMEZONE_KEY_TYPE = "timezoneType";
63    private static final String TIMEZONE_KEY_INSTANCES = "timezoneInstances";
64    private static final String TIMEZONE_KEY_INSTANCES_PREVIOUS = "timezoneInstancesPrevious";
65    private static final String TIMEZONE_TYPE_AUTO = "auto";
66    private static final String TIMEZONE_TYPE_HOME = "home";
67    private static final String[] TIMEZONE_POJECTION = new String[] {
68        TIMEZONE_COLUMN_KEY,
69        TIMEZONE_COLUMN_VALUE,
70    };
71    // Uri.parse("content://" + AUTHORITY + "/reminders");
72    private static final Uri TIMEZONE_URI =
73            Uri.parse("content://" + android.provider.Calendar.AUTHORITY + "/properties");
74    private static final String TIMEZONE_UPDATE_WHERE = "key=?";
75
76
77    private static StringBuilder mSB = new StringBuilder(50);
78    private static Formatter mF = new Formatter(mSB, Locale.getDefault());
79    private volatile static boolean mFirstTZRequest = true;
80    private volatile static boolean mTZQueryInProgress = false;
81
82    private volatile static boolean mUseHomeTZ = false;
83    private volatile static String mHomeTZ = Time.getCurrentTimezone();
84
85    private static HashSet<Runnable> mTZCallbacks = new HashSet<Runnable>();
86    private static int mToken = 1;
87    private static AsyncTZHandler mHandler;
88
89    private static class AsyncTZHandler extends AsyncQueryHandler {
90        public AsyncTZHandler(ContentResolver cr) {
91            super(cr);
92        }
93
94        @Override
95        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
96            synchronized (mTZCallbacks) {
97                boolean writePrefs = false;
98                // Check the values in the db
99                while(cursor.moveToNext()) {
100                    int keyColumn = cursor.getColumnIndexOrThrow(TIMEZONE_COLUMN_KEY);
101                    int valueColumn = cursor.getColumnIndexOrThrow(TIMEZONE_COLUMN_VALUE);
102                    String key = cursor.getString(keyColumn);
103                    String value = cursor.getString(valueColumn);
104                    if (TextUtils.equals(key, TIMEZONE_KEY_TYPE)) {
105                        boolean useHomeTZ = !TextUtils.equals(value, TIMEZONE_TYPE_AUTO);
106                        if (useHomeTZ != mUseHomeTZ) {
107                            writePrefs = true;
108                            mUseHomeTZ = useHomeTZ;
109                        }
110                    } else if (TextUtils.equals(key, TIMEZONE_KEY_INSTANCES_PREVIOUS)) {
111                        if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) {
112                            writePrefs = true;
113                            mHomeTZ = value;
114                        }
115                    }
116                }
117                if (writePrefs) {
118                    // Write the prefs
119                    setSharedPreference((Context)cookie,
120                            CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED, mUseHomeTZ);
121                    setSharedPreference((Context)cookie,
122                            CalendarPreferenceActivity.KEY_HOME_TZ, mHomeTZ);
123                }
124
125                mTZQueryInProgress = false;
126                for (Runnable callback : mTZCallbacks) {
127                    if (callback != null) {
128                        callback.run();
129                    }
130                }
131                mTZCallbacks.clear();
132            }
133        }
134    }
135
136    public static void startActivity(Context context, String className, long time) {
137        Intent intent = new Intent(Intent.ACTION_VIEW);
138
139        intent.setClassName(context, className);
140        intent.putExtra(EVENT_BEGIN_TIME, time);
141        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
142
143        context.startActivity(intent);
144    }
145
146    static String getSharedPreference(Context context, String key, String defaultValue) {
147        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
148        return prefs.getString(key, defaultValue);
149    }
150
151    /**
152     * Writes a new home time zone to the db.
153     *
154     * Updates the home time zone in the db asynchronously and updates
155     * the local cache. Sending a time zone of **tbd** will cause it to
156     * be set to the device's time zone. null or empty tz will be ignored.
157     *
158     * @param context The calling activity
159     * @param timeZone The time zone to set Calendar to, or **tbd**
160     */
161    public static void setTimeZone(Context context, String timeZone) {
162        if (TextUtils.isEmpty(timeZone)) {
163            if (DEBUG) {
164                Log.d(TAG, "Empty time zone, nothing to be done.");
165            }
166            return;
167        }
168        boolean updatePrefs = false;
169        synchronized (mTZCallbacks) {
170            if (CalendarPreferenceActivity.LOCAL_TZ.equals(timeZone)) {
171                if (mUseHomeTZ) {
172                    updatePrefs = true;
173                }
174                mUseHomeTZ = false;
175            } else {
176                if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) {
177                    updatePrefs = true;
178                }
179                mUseHomeTZ = true;
180                mHomeTZ = timeZone;
181            }
182        }
183        if (updatePrefs) {
184            // Write the prefs
185            setSharedPreference(context, CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED,
186                    mUseHomeTZ);
187            setSharedPreference(context, CalendarPreferenceActivity.KEY_HOME_TZ, mHomeTZ);
188
189            // Update the db
190            ContentValues values = new ContentValues();
191            if (mHandler == null) {
192                mHandler = new AsyncTZHandler(context.getContentResolver());
193            }
194
195            mHandler.cancelOperation(mToken);
196
197            String[] selArgs = new String[] {TIMEZONE_KEY_TYPE};
198            values.put(TIMEZONE_COLUMN_VALUE, mUseHomeTZ ? TIMEZONE_TYPE_HOME : TIMEZONE_TYPE_AUTO);
199            mHandler.startUpdate(mToken, null, TIMEZONE_URI, values, TIMEZONE_UPDATE_WHERE,
200                    selArgs);
201
202            if (mUseHomeTZ) {
203                selArgs[0] = TIMEZONE_KEY_INSTANCES;
204                values.clear();
205                values.put(TIMEZONE_COLUMN_VALUE, mHomeTZ);
206                mHandler.startUpdate(mToken, null, TIMEZONE_URI, values, TIMEZONE_UPDATE_WHERE,
207                        selArgs);
208            }
209
210            // skip 0 so query can use it
211            if (++mToken == 0) {
212                mToken = 1;
213            }
214        }
215    }
216
217    /**
218     * Gets the time zone that Calendar should be displayed in
219     *
220     * This is a helper method to get the appropriate time zone for Calendar. If this
221     * is the first time this method has been called it will initiate an asynchronous
222     * query to verify that the data in preferences is correct. The callback supplied
223     * will only be called if this query returns a value other than what is stored in
224     * preferences and should cause the calling activity to refresh anything that
225     * depends on calling this method.
226     *
227     * @param context The calling activity
228     * @param callback The runnable that should execute if a query returns new values
229     * @return The string value representing the time zone Calendar should display
230     */
231    public static String getTimeZone(Context context, Runnable callback) {
232        synchronized (mTZCallbacks){
233            if (mFirstTZRequest) {
234                mTZQueryInProgress = true;
235                mFirstTZRequest = false;
236
237                SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
238                mUseHomeTZ = prefs.getBoolean(
239                        CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED, false);
240                mHomeTZ = prefs.getString(
241                        CalendarPreferenceActivity.KEY_HOME_TZ, Time.getCurrentTimezone());
242
243                // When the async query returns it should synchronize on
244                // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the
245                // preferences, set mTZQueryInProgress to false, and call all
246                // the runnables in mTZCallbacks.
247                if (mHandler == null) {
248                    mHandler = new AsyncTZHandler(context.getContentResolver());
249                }
250                mHandler.startQuery(0, context, TIMEZONE_URI, TIMEZONE_POJECTION, null, null, null);
251            }
252            if (mTZQueryInProgress) {
253                mTZCallbacks.add(callback);
254            }
255        }
256        return mUseHomeTZ ? mHomeTZ : Time.getCurrentTimezone();
257    }
258
259    /**
260     * Formats a date or a time range according to the local conventions.
261     *
262     * @param context the context is required only if the time is shown
263     * @param startMillis the start time in UTC milliseconds
264     * @param endMillis the end time in UTC milliseconds
265     * @param flags a bit mask of options See
266     * {@link #formatDateRange(Context, Formatter, long, long, int, String) formatDateRange}
267     * @return a string containing the formatted date/time range.
268     */
269    public static String formatDateRange(Context context, long startMillis,
270            long endMillis, int flags) {
271        String date;
272        synchronized (mSB) {
273            mSB.setLength(0);
274            date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags,
275                    getTimeZone(context, null)).toString();
276        }
277        return date;
278    }
279
280    static void setSharedPreference(Context context, String key, String value) {
281        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
282        SharedPreferences.Editor editor = prefs.edit();
283        editor.putString(key, value);
284        editor.commit();
285    }
286
287    static void setSharedPreference(Context context, String key, boolean value) {
288        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
289        SharedPreferences.Editor editor = prefs.edit();
290        editor.putBoolean(key, value);
291        editor.commit();
292    }
293
294    static void setDefaultView(Context context, int viewId) {
295        String activityString = CalendarApplication.ACTIVITY_NAMES[viewId];
296
297        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
298        SharedPreferences.Editor editor = prefs.edit();
299        if (viewId == CalendarApplication.AGENDA_VIEW_ID ||
300                viewId == CalendarApplication.DAY_VIEW_ID) {
301            // Record the (new) detail start view only for Agenda and Day
302            editor.putString(CalendarPreferenceActivity.KEY_DETAILED_VIEW, activityString);
303        }
304
305        // Record the (new) start view
306        editor.putString(CalendarPreferenceActivity.KEY_START_VIEW, activityString);
307        editor.commit();
308    }
309
310    public static final Time timeFromIntent(Intent intent) {
311        Time time = new Time();
312        time.set(timeFromIntentInMillis(intent));
313        return time;
314    }
315
316    public static MatrixCursor matrixCursorFromCursor(Cursor cursor) {
317        MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
318        int numColumns = cursor.getColumnCount();
319        String data[] = new String[numColumns];
320        cursor.moveToPosition(-1);
321        while (cursor.moveToNext()) {
322            for (int i = 0; i < numColumns; i++) {
323                data[i] = cursor.getString(i);
324            }
325            newCursor.addRow(data);
326        }
327        return newCursor;
328    }
329
330    /**
331     * Compares two cursors to see if they contain the same data.
332     *
333     * @return Returns true of the cursors contain the same data and are not null, false
334     * otherwise
335     */
336    public static boolean compareCursors(Cursor c1, Cursor c2) {
337        if(c1 == null || c2 == null) {
338            return false;
339        }
340
341        int numColumns = c1.getColumnCount();
342        if (numColumns != c2.getColumnCount()) {
343            return false;
344        }
345
346        if (c1.getCount() != c2.getCount()) {
347            return false;
348        }
349
350        c1.moveToPosition(-1);
351        c2.moveToPosition(-1);
352        while(c1.moveToNext() && c2.moveToNext()) {
353            for(int i = 0; i < numColumns; i++) {
354                if(!TextUtils.equals(c1.getString(i), c2.getString(i))) {
355                    return false;
356                }
357            }
358        }
359
360        return true;
361    }
362
363    /**
364     * If the given intent specifies a time (in milliseconds since the epoch),
365     * then that time is returned. Otherwise, the current time is returned.
366     */
367    public static final long timeFromIntentInMillis(Intent intent) {
368        // If the time was specified, then use that.  Otherwise, use the current time.
369        Uri data = intent.getData();
370        long millis = intent.getLongExtra(EVENT_BEGIN_TIME, -1);
371        if (millis == -1 && data != null && data.isHierarchical()) {
372            List<String> path = data.getPathSegments();
373            if(path.size() == 2 && path.get(0).equals("time")) {
374                try {
375                    millis = Long.valueOf(data.getLastPathSegment());
376                } catch (NumberFormatException e) {
377                    Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " +
378                            "found. Using current time.");
379                }
380            }
381        }
382        if (millis <= 0) {
383            millis = System.currentTimeMillis();
384        }
385        return millis;
386    }
387
388    public static final void applyAlphaAnimation(ViewFlipper v) {
389        AlphaAnimation in = new AlphaAnimation(0.0f, 1.0f);
390
391        in.setStartOffset(0);
392        in.setDuration(500);
393
394        AlphaAnimation out = new AlphaAnimation(1.0f, 0.0f);
395
396        out.setStartOffset(0);
397        out.setDuration(500);
398
399        v.setInAnimation(in);
400        v.setOutAnimation(out);
401    }
402
403    public static Drawable getColorChip(int color) {
404        /*
405         * We want the color chip to have a nice gradient using
406         * the color of the calendar. To do this we use a GradientDrawable.
407         * The color supplied has an alpha of FF so we first do:
408         * color & 0x00FFFFFF
409         * to clear the alpha. Then we add our alpha to it.
410         * We use 3 colors to get a step effect where it starts off very
411         * light and quickly becomes dark and then a slow transition to
412         * be even darker.
413         */
414        color &= CLEAR_ALPHA_MASK;
415        int startColor = color | HIGH_ALPHA;
416        int middleColor = color | MED_ALPHA;
417        int endColor = color | LOW_ALPHA;
418        int[] colors = new int[] {startColor, middleColor, endColor};
419        GradientDrawable d = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors);
420        d.setCornerRadii(CORNERS);
421        return d;
422    }
423
424    /**
425     * Formats the given Time object so that it gives the month and year
426     * (for example, "September 2007").
427     *
428     * @param time the time to format
429     * @return the string containing the weekday and the date
430     */
431    public static String formatMonthYear(Context context, Time time) {
432        return time.format(context.getResources().getString(R.string.month_year));
433    }
434
435    // TODO: replace this with the correct i18n way to do this
436    public static final String englishNthDay[] = {
437        "", "1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th",
438        "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th",
439        "20th", "21st", "22nd", "23rd", "24th", "25th", "26th", "27th", "28th", "29th",
440        "30th", "31st"
441    };
442
443    public static String formatNth(int nth) {
444        return "the " + englishNthDay[nth];
445    }
446
447    /**
448     * Returns a list joined together by the provided delimiter, for example,
449     * ["a", "b", "c"] could be joined into "a,b,c"
450     *
451     * @param things the things to join together
452     * @param delim the delimiter to use
453     * @return a string contained the things joined together
454     */
455    public static String join(List<?> things, String delim) {
456        StringBuilder builder = new StringBuilder();
457        boolean first = true;
458        for (Object thing : things) {
459            if (first) {
460                first = false;
461            } else {
462                builder.append(delim);
463            }
464            builder.append(thing.toString());
465        }
466        return builder.toString();
467    }
468
469    /**
470     * Sets the time to the beginning of the day (midnight) by clearing the
471     * hour, minute, and second fields.
472     */
473    static void setTimeToStartOfDay(Time time) {
474        time.second = 0;
475        time.minute = 0;
476        time.hour = 0;
477    }
478
479    /**
480     * Get first day of week as android.text.format.Time constant.
481     * @return the first day of week in android.text.format.Time
482     */
483    public static int getFirstDayOfWeek() {
484        int startDay = Calendar.getInstance().getFirstDayOfWeek();
485        if (startDay == Calendar.SATURDAY) {
486            return Time.SATURDAY;
487        } else if (startDay == Calendar.MONDAY) {
488            return Time.MONDAY;
489        } else {
490            return Time.SUNDAY;
491        }
492    }
493
494    /**
495     * Determine whether the column position is Saturday or not.
496     * @param column the column position
497     * @param firstDayOfWeek the first day of week in android.text.format.Time
498     * @return true if the column is Saturday position
499     */
500    public static boolean isSaturday(int column, int firstDayOfWeek) {
501        return (firstDayOfWeek == Time.SUNDAY && column == 6)
502            || (firstDayOfWeek == Time.MONDAY && column == 5)
503            || (firstDayOfWeek == Time.SATURDAY && column == 0);
504    }
505
506    /**
507     * Determine whether the column position is Sunday or not.
508     * @param column the column position
509     * @param firstDayOfWeek the first day of week in android.text.format.Time
510     * @return true if the column is Sunday position
511     */
512    public static boolean isSunday(int column, int firstDayOfWeek) {
513        return (firstDayOfWeek == Time.SUNDAY && column == 0)
514            || (firstDayOfWeek == Time.MONDAY && column == 6)
515            || (firstDayOfWeek == Time.SATURDAY && column == 1);
516    }
517
518    /**
519     * Scan through a cursor of calendars and check if names are duplicated.
520     *
521     * This travels a cursor containing calendar display names and fills in the provided map with
522     * whether or not each name is repeated.
523     * @param isDuplicateName The map to put the duplicate check results in.
524     * @param cursor The query of calendars to check
525     * @param nameIndex The column of the query that contains the display name
526     */
527    public static void checkForDuplicateNames(Map<String, Boolean> isDuplicateName, Cursor cursor,
528            int nameIndex) {
529        isDuplicateName.clear();
530        cursor.moveToPosition(-1);
531        while (cursor.moveToNext()) {
532            String displayName = cursor.getString(nameIndex);
533            // Set it to true if we've seen this name before, false otherwise
534            if (displayName != null) {
535                isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName));
536            }
537        }
538    }
539}
540