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