1/*
2 * Copyright (C) 2010 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 android.content.AsyncQueryHandler;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.SharedPreferences;
24import android.database.Cursor;
25import android.provider.CalendarContract.CalendarCache;
26import android.text.TextUtils;
27import android.text.format.DateUtils;
28import android.text.format.Time;
29import android.util.Log;
30
31import java.util.Formatter;
32import java.util.HashSet;
33import java.util.Locale;
34
35/**
36 * A class containing utility methods related to Calendar apps.
37 *
38 * This class is expected to move into the app framework eventually.
39 */
40public class CalendarUtils {
41    private static final boolean DEBUG = false;
42    private static final String TAG = "CalendarUtils";
43
44    /**
45     * This class contains methods specific to reading and writing time zone
46     * values.
47     */
48    public static class TimeZoneUtils {
49        private static final String[] TIMEZONE_TYPE_ARGS = { CalendarCache.KEY_TIMEZONE_TYPE };
50        private static final String[] TIMEZONE_INSTANCES_ARGS =
51                { CalendarCache.KEY_TIMEZONE_INSTANCES };
52        public static final String[] CALENDAR_CACHE_POJECTION = {
53                CalendarCache.KEY, CalendarCache.VALUE
54        };
55
56        private static StringBuilder mSB = new StringBuilder(50);
57        private static Formatter mF = new Formatter(mSB, Locale.getDefault());
58        private volatile static boolean mFirstTZRequest = true;
59        private volatile static boolean mTZQueryInProgress = false;
60
61        private volatile static boolean mUseHomeTZ = false;
62        private volatile static String mHomeTZ = Time.getCurrentTimezone();
63
64        private static HashSet<Runnable> mTZCallbacks = new HashSet<Runnable>();
65        private static int mToken = 1;
66        private static AsyncTZHandler mHandler;
67
68        // The name of the shared preferences file. This name must be maintained for historical
69        // reasons, as it's what PreferenceManager assigned the first time the file was created.
70        private final String mPrefsName;
71
72        /**
73         * This is the key used for writing whether or not a home time zone should
74         * be used in the Calendar app to the Calendar Preferences.
75         */
76        public static final String KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled";
77        /**
78         * This is the key used for writing the time zone that should be used if
79         * home time zones are enabled for the Calendar app.
80         */
81        public static final String KEY_HOME_TZ = "preferences_home_tz";
82
83        /**
84         * This is a helper class for handling the async queries and updates for the
85         * time zone settings in Calendar.
86         */
87        private class AsyncTZHandler extends AsyncQueryHandler {
88            public AsyncTZHandler(ContentResolver cr) {
89                super(cr);
90            }
91
92            @Override
93            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
94                synchronized (mTZCallbacks) {
95                    if (cursor == null) {
96                        mTZQueryInProgress = false;
97                        mFirstTZRequest = true;
98                        return;
99                    }
100
101                    boolean writePrefs = false;
102                    // Check the values in the db
103                    int keyColumn = cursor.getColumnIndexOrThrow(CalendarCache.KEY);
104                    int valueColumn = cursor.getColumnIndexOrThrow(CalendarCache.VALUE);
105                    while(cursor.moveToNext()) {
106                        String key = cursor.getString(keyColumn);
107                        String value = cursor.getString(valueColumn);
108                        if (TextUtils.equals(key, CalendarCache.KEY_TIMEZONE_TYPE)) {
109                            boolean useHomeTZ = !TextUtils.equals(
110                                    value, CalendarCache.TIMEZONE_TYPE_AUTO);
111                            if (useHomeTZ != mUseHomeTZ) {
112                                writePrefs = true;
113                                mUseHomeTZ = useHomeTZ;
114                            }
115                        } else if (TextUtils.equals(
116                                key, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) {
117                            if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) {
118                                writePrefs = true;
119                                mHomeTZ = value;
120                            }
121                        }
122                    }
123                    cursor.close();
124                    if (writePrefs) {
125                        SharedPreferences prefs = getSharedPreferences((Context)cookie, mPrefsName);
126                        // Write the prefs
127                        setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ);
128                        setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ);
129                    }
130
131                    mTZQueryInProgress = false;
132                    for (Runnable callback : mTZCallbacks) {
133                        if (callback != null) {
134                            callback.run();
135                        }
136                    }
137                    mTZCallbacks.clear();
138                }
139            }
140        }
141
142        /**
143         * The name of the file where the shared prefs for Calendar are stored
144         * must be provided. All activities within an app should provide the
145         * same preferences name or behavior may become erratic.
146         *
147         * @param prefsName
148         */
149        public TimeZoneUtils(String prefsName) {
150            mPrefsName = prefsName;
151        }
152
153        /**
154         * Formats a date or a time range according to the local conventions.
155         *
156         * This formats a date/time range using Calendar's time zone and the
157         * local conventions for the region of the device.
158         *
159         * If the {@link DateUtils#FORMAT_UTC} flag is used it will pass in
160         * the UTC time zone instead.
161         *
162         * @param context the context is required only if the time is shown
163         * @param startMillis the start time in UTC milliseconds
164         * @param endMillis the end time in UTC milliseconds
165         * @param flags a bit mask of options See
166         * {@link DateUtils#formatDateRange(Context, Formatter, long, long, int, String) formatDateRange}
167         * @return a string containing the formatted date/time range.
168         */
169        public String formatDateRange(Context context, long startMillis,
170                long endMillis, int flags) {
171            String date;
172            String tz;
173            if ((flags & DateUtils.FORMAT_UTC) != 0) {
174                tz = Time.TIMEZONE_UTC;
175            } else {
176                tz = getTimeZone(context, null);
177            }
178            synchronized (mSB) {
179                mSB.setLength(0);
180                date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags,
181                        tz).toString();
182            }
183            return date;
184        }
185
186        /**
187         * Writes a new home time zone to the db.
188         *
189         * Updates the home time zone in the db asynchronously and updates
190         * the local cache. Sending a time zone of
191         * {@link CalendarCache#TIMEZONE_TYPE_AUTO} will cause it to be set
192         * to the device's time zone. null or empty tz will be ignored.
193         *
194         * @param context The calling activity
195         * @param timeZone The time zone to set Calendar to, or
196         * {@link CalendarCache#TIMEZONE_TYPE_AUTO}
197         */
198        public void setTimeZone(Context context, String timeZone) {
199            if (TextUtils.isEmpty(timeZone)) {
200                if (DEBUG) {
201                    Log.d(TAG, "Empty time zone, nothing to be done.");
202                }
203                return;
204            }
205            boolean updatePrefs = false;
206            synchronized (mTZCallbacks) {
207                if (CalendarCache.TIMEZONE_TYPE_AUTO.equals(timeZone)) {
208                    if (mUseHomeTZ) {
209                        updatePrefs = true;
210                    }
211                    mUseHomeTZ = false;
212                } else {
213                    if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) {
214                        updatePrefs = true;
215                    }
216                    mUseHomeTZ = true;
217                    mHomeTZ = timeZone;
218                }
219            }
220            if (updatePrefs) {
221                // Write the prefs
222                SharedPreferences prefs = getSharedPreferences(context, mPrefsName);
223                setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ);
224                setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ);
225
226                // Update the db
227                ContentValues values = new ContentValues();
228                if (mHandler != null) {
229                    mHandler.cancelOperation(mToken);
230                }
231
232                mHandler = new AsyncTZHandler(context.getContentResolver());
233
234                // skip 0 so query can use it
235                if (++mToken == 0) {
236                    mToken = 1;
237                }
238
239                // Write the use home tz setting
240                values.put(CalendarCache.VALUE, mUseHomeTZ ? CalendarCache.TIMEZONE_TYPE_HOME
241                        : CalendarCache.TIMEZONE_TYPE_AUTO);
242                mHandler.startUpdate(mToken, null, CalendarCache.URI, values, "key=?",
243                        TIMEZONE_TYPE_ARGS);
244
245                // If using a home tz write it to the db
246                if (mUseHomeTZ) {
247                    ContentValues values2 = new ContentValues();
248                    values2.put(CalendarCache.VALUE, mHomeTZ);
249                    mHandler.startUpdate(mToken, null, CalendarCache.URI, values2,
250                            "key=?", TIMEZONE_INSTANCES_ARGS);
251                }
252            }
253        }
254
255        /**
256         * Gets the time zone that Calendar should be displayed in
257         *
258         * This is a helper method to get the appropriate time zone for Calendar. If this
259         * is the first time this method has been called it will initiate an asynchronous
260         * query to verify that the data in preferences is correct. The callback supplied
261         * will only be called if this query returns a value other than what is stored in
262         * preferences and should cause the calling activity to refresh anything that
263         * depends on calling this method.
264         *
265         * @param context The calling activity
266         * @param callback The runnable that should execute if a query returns new values
267         * @return The string value representing the time zone Calendar should display
268         */
269        public String getTimeZone(Context context, Runnable callback) {
270            synchronized (mTZCallbacks){
271                if (mFirstTZRequest) {
272                    mTZQueryInProgress = true;
273                    mFirstTZRequest = false;
274
275                    SharedPreferences prefs = getSharedPreferences(context, mPrefsName);
276                    mUseHomeTZ = prefs.getBoolean(KEY_HOME_TZ_ENABLED, false);
277                    mHomeTZ = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone());
278
279                    // When the async query returns it should synchronize on
280                    // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the
281                    // preferences, set mTZQueryInProgress to false, and call all
282                    // the runnables in mTZCallbacks.
283                    if (mHandler == null) {
284                        mHandler = new AsyncTZHandler(context.getContentResolver());
285                    }
286                    mHandler.startQuery(0, context, CalendarCache.URI, CALENDAR_CACHE_POJECTION,
287                            null, null, null);
288                }
289                if (mTZQueryInProgress) {
290                    mTZCallbacks.add(callback);
291                }
292            }
293            return mUseHomeTZ ? mHomeTZ : Time.getCurrentTimezone();
294        }
295
296        /**
297         * Forces a query of the database to check for changes to the time zone.
298         * This should be called if another app may have modified the db. If a
299         * query is already in progress the callback will be added to the list
300         * of callbacks to be called when it returns.
301         *
302         * @param context The calling activity
303         * @param callback The runnable that should execute if a query returns
304         *            new values
305         */
306        public void forceDBRequery(Context context, Runnable callback) {
307            synchronized (mTZCallbacks){
308                if (mTZQueryInProgress) {
309                    mTZCallbacks.add(callback);
310                    return;
311                }
312                mFirstTZRequest = true;
313                getTimeZone(context, callback);
314            }
315        }
316    }
317
318        /**
319         * A helper method for writing a String value to the preferences
320         * asynchronously.
321         *
322         * @param context A context with access to the correct preferences
323         * @param key The preference to write to
324         * @param value The value to write
325         */
326        public static void setSharedPreference(SharedPreferences prefs, String key, String value) {
327//            SharedPreferences prefs = getSharedPreferences(context);
328            SharedPreferences.Editor editor = prefs.edit();
329            editor.putString(key, value);
330            editor.apply();
331        }
332
333        /**
334         * A helper method for writing a boolean value to the preferences
335         * asynchronously.
336         *
337         * @param context A context with access to the correct preferences
338         * @param key The preference to write to
339         * @param value The value to write
340         */
341        public static void setSharedPreference(SharedPreferences prefs, String key, boolean value) {
342//            SharedPreferences prefs = getSharedPreferences(context, prefsName);
343            SharedPreferences.Editor editor = prefs.edit();
344            editor.putBoolean(key, value);
345            editor.apply();
346        }
347
348        /** Return a properly configured SharedPreferences instance */
349        public static SharedPreferences getSharedPreferences(Context context, String prefsName) {
350            return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
351        }
352}
353