Utils.java revision 35d1362a75eac7cebbe9de23d08fea08c4aac817
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.database.Cursor;
28import android.database.MatrixCursor;
29import android.graphics.drawable.Drawable;
30import android.graphics.drawable.GradientDrawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.text.TextUtils;
34import android.text.format.DateUtils;
35import android.text.format.Time;
36import android.util.Log;
37
38import java.util.Calendar;
39import java.util.Formatter;
40import java.util.HashSet;
41import java.util.List;
42import java.util.Locale;
43import java.util.Map;
44import java.util.TimeZone;
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 code.
59    public static final int DONE_REVERT = 0;
60    public static final int DONE_SAVE = 1;
61    public static final int DONE_DELETE = 2;
62
63    private static final int CLEAR_ALPHA_MASK = 0x00FFFFFF;
64    private static final int HIGH_ALPHA = 255 << 24;
65    private static final int MED_ALPHA = 180 << 24;
66    private static final int LOW_ALPHA = 150 << 24;
67
68    protected static final String OPEN_EMAIL_MARKER = " <";
69    protected static final String CLOSE_EMAIL_MARKER = ">";
70    /* The corner should be rounded on the top right and bottom right */
71    private static final float[] CORNERS = new float[] {0, 0, 5, 5, 5, 5, 0, 0};
72
73    public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW";
74    public static final String INTENT_KEY_VIEW_TYPE = "VIEW";
75    public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY";
76
77    private static StringBuilder mSB = new StringBuilder(50);
78    private static Formatter mF = new Formatter(mSB, Locale.getDefault());
79
80    private volatile static boolean mFirstTZRequest = true;
81    private volatile static boolean mTZQueryInProgress = false;
82
83    private volatile static boolean mUseHomeTZ = false;
84    private volatile static String mHomeTZ = Time.getCurrentTimezone();
85
86    private static HashSet<Runnable> mTZCallbacks = new HashSet<Runnable>();
87
88    public static int getViewTypeFromIntentAndSharedPref(Activity activity) {
89        Intent intent = activity.getIntent();
90        Bundle extras = intent.getExtras();
91        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(activity);
92
93        if (TextUtils.equals(intent.getAction(),Intent.ACTION_EDIT)) {
94            return ViewType.EDIT;
95        }
96        if (extras != null) {
97            if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) {
98                // This is the "detail" view which is either agenda or day view
99                return prefs.getInt(CalendarPreferenceActivity.KEY_DETAILED_VIEW,
100                        CalendarPreferenceActivity.DEFAULT_DETAILED_VIEW);
101            } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) {
102                // Not sure who uses this. This logic came from LaunchActivity
103                return ViewType.DAY;
104            }
105        }
106
107        // Default to the last view
108        return prefs.getInt(CalendarPreferenceActivity.KEY_START_VIEW,
109                CalendarPreferenceActivity.DEFAULT_START_VIEW);
110    }
111
112    /**
113     * Writes a new home time zone to the db.
114     *
115     * Updates the home time zone in the db asynchronously and updates
116     * the local cache. Sending a time zone of **tbd** will cause it to
117     * be set to the device's time zone. null or empty tz will be ignored.
118     *
119     * @param context The calling activity
120     * @param timeZone The time zone to set Calendar to, or **tbd**
121     */
122    public static void setTimeZone(Context context, String timeZone) {
123        if (TextUtils.isEmpty(timeZone)) {
124            if (DEBUG) {
125                Log.d(TAG, "Empty time zone, nothing to be done.");
126            }
127            return;
128        }
129        boolean updatePrefs = false;
130        synchronized (mTZCallbacks) {
131            if (CalendarPreferenceActivity.LOCAL_TZ.equals(timeZone)) {
132                if (mUseHomeTZ) {
133                    updatePrefs = true;
134                }
135                mUseHomeTZ = false;
136            } else {
137                if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) {
138                    updatePrefs = true;
139                }
140                mUseHomeTZ = true;
141                mHomeTZ = timeZone;
142            }
143        }
144        if (updatePrefs) {
145            setSharedPreference(context, CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED,
146                    mUseHomeTZ);
147            setSharedPreference(context, CalendarPreferenceActivity.KEY_HOME_TZ, mHomeTZ);
148        }
149        // TODO async update db
150    }
151
152    /**
153     * Gets the time zone that Calendar should be displayed in
154     *
155     * This is a helper method to get the appropriate time zone for Calendar. If this
156     * is the first time this method has been called it will initiate an asynchronous
157     * query to verify that the data in preferences is correct. The callback supplied
158     * will only be called if this query returns a value other than what is stored in
159     * preferences and should cause the calling activity to refresh anything that
160     * depends on calling this method.
161     *
162     * @param context The calling activity
163     * @param callback The runnable that should execute if a query returns new values
164     * @return The string value representing the time zone Calendar should display
165     */
166    public static String getTimeZone(Context context, Runnable callback) {
167        synchronized (mTZCallbacks){
168            if (mFirstTZRequest) {
169                mTZQueryInProgress = true;
170                mFirstTZRequest = false;
171
172                SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
173                mUseHomeTZ = prefs.getBoolean(
174                        CalendarPreferenceActivity.KEY_HOME_TZ_ENABLED, false);
175                mHomeTZ = prefs.getString(
176                        CalendarPreferenceActivity.KEY_HOME_TZ, Time.getCurrentTimezone());
177                // TODO kick off async query
178                // When the async query returns it should synchronize on
179                // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the
180                // preferences, set mTZQueryInProgress to false, and call all
181                // the runnables in mTZCallbacks.
182                // TODO remove this line when we have a query
183                mTZQueryInProgress = false;
184            }
185            if (mTZQueryInProgress) {
186                mTZCallbacks.add(callback);
187            }
188        }
189        return mUseHomeTZ ? mHomeTZ : Time.getCurrentTimezone();
190    }
191
192    public static String getSharedPreference(Context context, String key, String defaultValue) {
193        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
194        return prefs.getString(key, defaultValue);
195    }
196
197    /**
198     * Formats a date or a time range according to the local conventions.
199     *
200     * @param context the context is required only if the time is shown
201     * @param startMillis the start time in UTC milliseconds
202     * @param endMillis the end time in UTC milliseconds
203     * @param flags a bit mask of options See
204     * {@link #formatDateRange(Context, Formatter, long, long, int, String) formatDateRange}
205     * @return a string containing the formatted date/time range.
206     */
207    public static String formatDateRange(Context context, long startMillis,
208            long endMillis, int flags) {
209        String date;
210        synchronized (mSB) {
211            mSB.setLength(0);
212            date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags,
213                    getTimeZone(context, null)).toString();
214        }
215        return date;
216    }
217
218    public static int getSharedPreference(Context context, String key, int defaultValue) {
219        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
220        return prefs.getInt(key, defaultValue);
221    }
222
223    /**
224     * Asynchronously sets the preference with the given key to the given value
225     *
226     * @param context the context to use to get preferences from
227     * @param key the key of the preference to set
228     * @param value the value to set
229     */
230    public static void setSharedPreference(Context context, String key, String value) {
231        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
232        prefs.edit().putString(key, value).apply();
233    }
234
235    static void setSharedPreference(Context context, String key, boolean value) {
236        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
237        SharedPreferences.Editor editor = prefs.edit();
238        editor.putBoolean(key, value);
239        editor.apply();
240    }
241
242    /**
243     * Save default agenda/day/week/month view for next time
244     *
245     * @param context
246     * @param viewId {@link CalendarController.ViewType}
247     */
248    static void setDefaultView(Context context, int viewId) {
249        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
250        SharedPreferences.Editor editor = prefs.edit();
251
252        if (viewId == CalendarController.ViewType.AGENDA
253                || viewId == CalendarController.ViewType.DAY) {
254            // Record the (new) detail start view only for Agenda and Day
255            editor.putInt(CalendarPreferenceActivity.KEY_DETAILED_VIEW, viewId);
256        }
257
258        // Record the (new) start view
259        editor.putInt(CalendarPreferenceActivity.KEY_START_VIEW, viewId);
260        editor.apply();
261    }
262
263    public static MatrixCursor matrixCursorFromCursor(Cursor cursor) {
264        MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
265        int numColumns = cursor.getColumnCount();
266        String data[] = new String[numColumns];
267        cursor.moveToPosition(-1);
268        while (cursor.moveToNext()) {
269            for (int i = 0; i < numColumns; i++) {
270                data[i] = cursor.getString(i);
271            }
272            newCursor.addRow(data);
273        }
274        return newCursor;
275    }
276
277    /**
278     * Compares two cursors to see if they contain the same data.
279     *
280     * @return Returns true of the cursors contain the same data and are not null, false
281     * otherwise
282     */
283    public static boolean compareCursors(Cursor c1, Cursor c2) {
284        if(c1 == null || c2 == null) {
285            return false;
286        }
287
288        int numColumns = c1.getColumnCount();
289        if (numColumns != c2.getColumnCount()) {
290            return false;
291        }
292
293        if (c1.getCount() != c2.getCount()) {
294            return false;
295        }
296
297        c1.moveToPosition(-1);
298        c2.moveToPosition(-1);
299        while(c1.moveToNext() && c2.moveToNext()) {
300            for(int i = 0; i < numColumns; i++) {
301                if(!TextUtils.equals(c1.getString(i), c2.getString(i))) {
302                    return false;
303                }
304            }
305        }
306
307        return true;
308    }
309
310    /**
311     * If the given intent specifies a time (in milliseconds since the epoch),
312     * then that time is returned. Otherwise, the current time is returned.
313     */
314    public static final long timeFromIntentInMillis(Intent intent) {
315        // If the time was specified, then use that.  Otherwise, use the current time.
316        Uri data = intent.getData();
317        long millis = intent.getLongExtra(EVENT_BEGIN_TIME, -1);
318        if (millis == -1 && data != null && data.isHierarchical()) {
319            List<String> path = data.getPathSegments();
320            if(path.size() == 2 && path.get(0).equals("time")) {
321                try {
322                    millis = Long.valueOf(data.getLastPathSegment());
323                } catch (NumberFormatException e) {
324                    Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " +
325                            "found. Using current time.");
326                }
327            }
328        }
329        if (millis <= 0) {
330            millis = System.currentTimeMillis();
331        }
332        return millis;
333    }
334
335    public static Drawable getColorChip(int color) {
336        /*
337         * We want the color chip to have a nice gradient using
338         * the color of the calendar. To do this we use a GradientDrawable.
339         * The color supplied has an alpha of FF so we first do:
340         * color & 0x00FFFFFF
341         * to clear the alpha. Then we add our alpha to it.
342         * We use 3 colors to get a step effect where it starts off very
343         * light and quickly becomes dark and then a slow transition to
344         * be even darker.
345         */
346        color &= CLEAR_ALPHA_MASK;
347        int startColor = color | HIGH_ALPHA;
348        int middleColor = color | MED_ALPHA;
349        int endColor = color | LOW_ALPHA;
350        int[] colors = new int[] {startColor, middleColor, endColor};
351        GradientDrawable d = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors);
352        d.setCornerRadii(CORNERS);
353        return d;
354    }
355
356    /**
357     * Formats the given Time object so that it gives the month and year
358     * (for example, "September 2007").
359     *
360     * @param time the time to format
361     * @return the string containing the weekday and the date
362     */
363    public static String formatMonthYear(Context context, Time time) {
364        return time.format(context.getResources().getString(R.string.month_year));
365    }
366
367    /**
368     * Returns a list joined together by the provided delimiter, for example,
369     * ["a", "b", "c"] could be joined into "a,b,c"
370     *
371     * @param things the things to join together
372     * @param delim the delimiter to use
373     * @return a string contained the things joined together
374     */
375    public static String join(List<?> things, String delim) {
376        StringBuilder builder = new StringBuilder();
377        boolean first = true;
378        for (Object thing : things) {
379            if (first) {
380                first = false;
381            } else {
382                builder.append(delim);
383            }
384            builder.append(thing.toString());
385        }
386        return builder.toString();
387    }
388
389    /**
390     * Get first day of week as android.text.format.Time constant.
391     * @return the first day of week in android.text.format.Time
392     */
393    public static int getFirstDayOfWeek(Context context) {
394        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(context);
395        String pref = prefs.getString(CalendarPreferenceActivity.KEY_WEEK_START_DAY,
396                CalendarPreferenceActivity.WEEK_START_DEFAULT);
397
398        int startDay;
399        if (CalendarPreferenceActivity.WEEK_START_DEFAULT.equals(pref)) {
400            startDay = Calendar.getInstance().getFirstDayOfWeek();
401        } else {
402            startDay = Integer.parseInt(pref);
403        }
404
405        if (startDay == Calendar.SATURDAY) {
406            return Time.SATURDAY;
407        } else if (startDay == Calendar.MONDAY) {
408            return Time.MONDAY;
409        } else {
410            return Time.SUNDAY;
411        }
412    }
413
414    /**
415     * Determine whether the column position is Saturday or not.
416     * @param column the column position
417     * @param firstDayOfWeek the first day of week in android.text.format.Time
418     * @return true if the column is Saturday position
419     */
420    public static boolean isSaturday(int column, int firstDayOfWeek) {
421        return (firstDayOfWeek == Time.SUNDAY && column == 6)
422            || (firstDayOfWeek == Time.MONDAY && column == 5)
423            || (firstDayOfWeek == Time.SATURDAY && column == 0);
424    }
425
426    /**
427     * Determine whether the column position is Sunday or not.
428     * @param column the column position
429     * @param firstDayOfWeek the first day of week in android.text.format.Time
430     * @return true if the column is Sunday position
431     */
432    public static boolean isSunday(int column, int firstDayOfWeek) {
433        return (firstDayOfWeek == Time.SUNDAY && column == 0)
434            || (firstDayOfWeek == Time.MONDAY && column == 6)
435            || (firstDayOfWeek == Time.SATURDAY && column == 1);
436    }
437
438    /**
439     * Convert given UTC time into current local time.
440     *
441     * @param recycle Time object to recycle, otherwise null.
442     * @param utcTime Time to convert, in UTC.
443     */
444    public static long convertUtcToLocal(Time recycle, long utcTime) {
445        if (recycle == null) {
446            recycle = new Time();
447        }
448        recycle.timezone = Time.TIMEZONE_UTC;
449        recycle.set(utcTime);
450        recycle.timezone = TimeZone.getDefault().getID();
451        return recycle.normalize(true);
452    }
453
454    /**
455     * Scan through a cursor of calendars and check if names are duplicated.
456     *
457     * This travels a cursor containing calendar display names and fills in the provided map with
458     * whether or not each name is repeated.
459     * @param isDuplicateName The map to put the duplicate check results in.
460     * @param cursor The query of calendars to check
461     * @param nameIndex The column of the query that contains the display name
462     */
463    public static void checkForDuplicateNames(Map<String, Boolean> isDuplicateName, Cursor cursor,
464            int nameIndex) {
465        isDuplicateName.clear();
466        cursor.moveToPosition(-1);
467        while (cursor.moveToNext()) {
468            String displayName = cursor.getString(nameIndex);
469            // Set it to true if we've seen this name before, false otherwise
470            if (displayName != null) {
471                isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName));
472            }
473        }
474    }
475
476    /**
477     * Null-safe object comparison
478     * @param s1
479     * @param s2
480     * @return
481     */
482    public static boolean equals(Object o1, Object o2) {
483        return o1 == null ? o2 == null : o1.equals(o2);
484    }
485}
486