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 
17 package com.android.calendar;
18 
19 import android.content.AsyncQueryHandler;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.SharedPreferences;
24 import android.database.Cursor;
25 import android.os.Looper;
26 import android.provider.CalendarContract.CalendarCache;
27 import android.text.TextUtils;
28 import android.text.format.DateUtils;
29 import android.text.format.Time;
30 import android.util.Log;
31 
32 import java.util.Formatter;
33 import java.util.HashSet;
34 import 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  */
41 public 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 {
AsyncTZHandler(ContentResolver cr)89             public AsyncTZHandler(ContentResolver cr) {
90                 super(cr);
91             }
92 
93             @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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          */
TimeZoneUtils(String prefsName)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          */
formatDateRange(Context context, long startMillis, long endMillis, int flags)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          */
setTimeZone(Context context, String timeZone)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          */
getTimeZone(Context context, Runnable callback)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          */
forceDBRequery(Context context, Runnable callback)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          */
setSharedPreference(SharedPreferences prefs, String key, String value)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          */
setSharedPreference(SharedPreferences prefs, String key, boolean value)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 */
getSharedPreferences(Context context, String prefsName)353         public static SharedPreferences getSharedPreferences(Context context, String prefsName) {
354             return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
355         }
356 }
357