blob: 419618528477a73732374e504afb4a958b609c73 [file] [log] [blame]
The Android Open Source Project146de362009-03-03 19:32:18 -08001/*
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
RoboErika27a8862011-06-23 15:26:23 -070019import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME;
Michael Chane8aa59d2009-09-15 14:44:43 -070020
Sam Blitzsteinceae8db2012-11-01 17:29:35 -070021import android.accounts.Account;
Michael Chand6734db2010-07-22 00:48:08 -070022import android.app.Activity;
RoboErik50f10942011-07-26 14:30:25 -070023import android.app.SearchManager;
Isaac Katzenelsonc9993162012-05-08 19:15:12 -070024import android.content.BroadcastReceiver;
Sam Blitzsteinf52c6412013-12-04 12:59:36 -080025import android.content.ComponentName;
Sam Blitzsteinceae8db2012-11-01 17:29:35 -070026import android.content.ContentResolver;
The Android Open Source Project146de362009-03-03 19:32:18 -080027import android.content.Context;
28import android.content.Intent;
Isaac Katzenelsonc9993162012-05-08 19:15:12 -070029import android.content.IntentFilter;
Michael Chane8aa59d2009-09-15 14:44:43 -070030import android.content.SharedPreferences;
Sara Tingdacfb662012-08-21 10:33:22 -070031import android.content.pm.PackageManager;
RoboErik092caec2011-06-23 10:26:12 -070032import android.content.res.Resources;
Michael Chanff6be832010-03-11 17:52:48 -080033import android.database.Cursor;
Erika144f862010-03-29 18:20:32 -070034import android.database.MatrixCursor;
RoboErikbbb5b552011-07-19 10:18:59 -070035import android.graphics.Color;
Isaac Katzenelsonc9993162012-05-08 19:15:12 -070036import android.graphics.drawable.Drawable;
37import android.graphics.drawable.LayerDrawable;
Erik1ef7f3a2010-02-24 14:46:03 -080038import android.net.Uri;
Sara Tingfac2d152012-05-31 14:59:57 -070039import android.os.Build;
Michael Chand6734db2010-07-22 00:48:08 -070040import android.os.Bundle;
Isaac Katzenelson4bd4a5c2012-03-20 11:02:03 -070041import android.os.Handler;
Sam Blitzsteinceae8db2012-11-01 17:29:35 -070042import android.provider.CalendarContract.Calendars;
Sam Blitzstein7e19bf92012-11-13 10:02:41 -080043import android.text.Spannable;
44import android.text.SpannableString;
45import android.text.Spanned;
Erikeb10fa82010-04-09 16:32:17 -070046import android.text.TextUtils;
Sara Ting75f53662012-04-09 15:37:10 -070047import android.text.format.DateFormat;
Daisuke Miyakawa29190972010-10-27 13:14:38 -070048import android.text.format.DateUtils;
The Android Open Source Project146de362009-03-03 19:32:18 -080049import android.text.format.Time;
Sam Blitzstein7e19bf92012-11-13 10:02:41 -080050import android.text.style.URLSpan;
51import android.text.util.Linkify;
Erik1ef7f3a2010-02-24 14:46:03 -080052import android.util.Log;
RoboErik50f10942011-07-26 14:30:25 -070053import android.widget.SearchView;
54
Isaac Katzenelsonc9993162012-05-08 19:15:12 -070055import com.android.calendar.CalendarController.ViewType;
Sam Blitzstein94a1f1a2013-02-12 15:16:35 -080056import com.android.calendar.CalendarEventModel.ReminderEntry;
Andy McFadden636269c2011-06-09 13:15:55 -070057import com.android.calendar.CalendarUtils.TimeZoneUtils;
The Android Open Source Project146de362009-03-03 19:32:18 -080058
Isaac Katzenelson82400dd2011-04-15 11:13:49 -070059import java.util.ArrayList;
RoboErik092caec2011-06-23 10:26:12 -070060import java.util.Arrays;
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +090061import java.util.Calendar;
Erik14276572010-09-03 15:05:28 -070062import java.util.Formatter;
RoboErik092caec2011-06-23 10:26:12 -070063import java.util.HashMap;
Isaac Katzenelson82400dd2011-04-15 11:13:49 -070064import java.util.Iterator;
Michael Chane98dca72012-06-16 08:22:47 -070065import java.util.LinkedHashSet;
RoboErik092caec2011-06-23 10:26:12 -070066import java.util.LinkedList;
Erik1ef7f3a2010-02-24 14:46:03 -080067import java.util.List;
Sara Ting75f53662012-04-09 15:37:10 -070068import java.util.Locale;
Michael Chanff6be832010-03-11 17:52:48 -080069import java.util.Map;
Michael Chane98dca72012-06-16 08:22:47 -070070import java.util.Set;
Sara Ting75f53662012-04-09 15:37:10 -070071import java.util.TimeZone;
Sam Blitzstein29dc76a2012-11-19 10:46:54 -080072import java.util.regex.Matcher;
Sam Blitzstein7e19bf92012-11-13 10:02:41 -080073import java.util.regex.Pattern;
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +090074
The Android Open Source Project146de362009-03-03 19:32:18 -080075public class Utils {
RoboErik092caec2011-06-23 10:26:12 -070076 private static final boolean DEBUG = false;
Erik3dc5e902010-09-07 11:33:07 -070077 private static final String TAG = "CalUtils";
Sara Tingddbc0022012-04-26 17:08:46 -070078
Michael Chanbed02752010-04-27 10:56:53 -070079 // Set to 0 until we have UI to perform undo
80 public static final long UNDO_DELAY = 0;
81
Erik79f22812010-06-23 16:55:38 -070082 // For recurring events which instances of the series are being modified
83 public static final int MODIFY_UNINITIALIZED = 0;
84 public static final int MODIFY_SELECTED = 1;
85 public static final int MODIFY_ALL_FOLLOWING = 2;
86 public static final int MODIFY_ALL = 3;
87
Erik7b92da22010-09-23 14:55:22 -070088 // When the edit event view finishes it passes back the appropriate exit
89 // code.
90 public static final int DONE_REVERT = 1 << 0;
91 public static final int DONE_SAVE = 1 << 1;
92 public static final int DONE_DELETE = 1 << 2;
93 // And should re run with DONE_EXIT if it should also leave the view, just
94 // exiting is identical to reverting
95 public static final int DONE_EXIT = 1 << 0;
Erik79f22812010-06-23 16:55:38 -070096
Michael Chan2aeb8d92011-07-10 13:32:09 -070097 public static final String OPEN_EMAIL_MARKER = " <";
98 public static final String CLOSE_EMAIL_MARKER = ">";
Michael Chanff6be832010-03-11 17:52:48 -080099
Michael Chand6734db2010-07-22 00:48:08 -0700100 public static final String INTENT_KEY_DETAIL_VIEW = "DETAIL_VIEW";
101 public static final String INTENT_KEY_VIEW_TYPE = "VIEW";
102 public static final String INTENT_VALUE_VIEW_TYPE_DAY = "DAY";
RoboErik4ba19df2011-09-22 11:31:21 -0700103 public static final String INTENT_KEY_HOME = "KEY_HOME";
Erik275232d2010-09-07 15:03:21 -0700104
Erik981874e2010-10-05 16:52:52 -0700105 public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3;
Isaac Katzenelsone6109c52011-10-14 17:23:11 -0700106 public static final int DECLINED_EVENT_ALPHA = 0x66;
Isaac Katzenelson4ecf0642011-10-18 12:56:18 -0700107 public static final int DECLINED_EVENT_TEXT_ALPHA = 0xC0;
Erik981874e2010-10-05 16:52:52 -0700108
Michael Chanf9411fe2012-02-10 17:05:52 -0800109 private static final float SATURATION_ADJUST = 1.3f;
110 private static final float INTENSITY_ADJUST = 0.8f;
RoboErik4acb2fd2011-07-18 15:39:49 -0700111
RoboErik092caec2011-06-23 10:26:12 -0700112 // Defines used by the DNA generation code
113 static final int DAY_IN_MINUTES = 60 * 24;
114 static final int WEEK_IN_MINUTES = DAY_IN_MINUTES * 7;
115 // The work day is being counted as 6am to 8pm
116 static int WORK_DAY_MINUTES = 14 * 60;
117 static int WORK_DAY_START_MINUTES = 6 * 60;
118 static int WORK_DAY_END_MINUTES = 20 * 60;
119 static int WORK_DAY_END_LENGTH = (24 * 60) - WORK_DAY_END_MINUTES;
120 static int CONFLICT_COLOR = 0xFF000000;
121 static boolean mMinutesLoaded = false;
122
James Kung56f42bf2013-03-29 14:59:29 -0700123 public static final int YEAR_MIN = 1970;
James Kungeb65d842013-04-08 16:05:01 -0700124 public static final int YEAR_MAX = 2036;
James Kung56f42bf2013-03-29 14:59:29 -0700125
Erika48b9d42010-09-22 15:31:03 -0700126 // The name of the shared preferences file. This name must be maintained for
127 // historical
128 // reasons, as it's what PreferenceManager assigned the first time the file
129 // was created.
Sara Ting75f53662012-04-09 15:37:10 -0700130 static final String SHARED_PREFS_NAME = "com.android.calendar_preferences";
Erik35d13622010-09-08 11:21:40 -0700131
Michael Chane98dca72012-06-16 08:22:47 -0700132 public static final String KEY_QUICK_RESPONSES = "preferences_quick_responses";
133
Michael Chan0b674be2012-11-20 07:16:14 -0800134 public static final String KEY_ALERTS_VIBRATE_WHEN = "preferences_alerts_vibrateWhen";
135
RoboErik42dabd12011-07-12 18:01:03 -0700136 public static final String APPWIDGET_DATA_TYPE = "vnd.android.data/update";
137
Sara Tingddbc0022012-04-26 17:08:46 -0700138 static final String MACHINE_GENERATED_ADDRESS = "calendar.google.com";
139
Erika48b9d42010-09-22 15:31:03 -0700140 private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME);
Michael Chanb60218a2010-12-14 16:34:39 -0800141 private static boolean mAllowWeekForDetailView = false;
Erikca478672011-01-19 20:02:47 -0800142 private static long mTardis = 0;
Sara Tingdacfb662012-08-21 10:33:22 -0700143 private static String sVersion = null;
Michael Chand6734db2010-07-22 00:48:08 -0700144
Sam Blitzstein7e19bf92012-11-13 10:02:41 -0800145 private static final Pattern mWildcardPattern = Pattern.compile("^.*$");
Sam Blitzstein29dc76a2012-11-19 10:46:54 -0800146
147 /**
148 * A coordinate must be of the following form for Google Maps to correctly use it:
149 * Latitude, Longitude
150 *
151 * This may be in decimal form:
152 * Latitude: {-90 to 90}
153 * Longitude: {-180 to 180}
154 *
155 * Or, in degrees, minutes, and seconds:
156 * Latitude: {-90 to 90}° {0 to 59}' {0 to 59}"
157 * Latitude: {-180 to 180}° {0 to 59}' {0 to 59}"
158 * + or - degrees may also be represented with N or n, S or s for latitude, and with
159 * E or e, W or w for longitude, where the direction may either precede or follow the value.
160 *
161 * Some examples of coordinates that will be accepted by the regex:
162 * 37.422081°, -122.084576°
163 * 37.422081,-122.084576
164 * +37°25'19.49", -122°5'4.47"
165 * 37°25'19.49"N, 122°5'4.47"W
166 * N 37° 25' 19.49", W 122° 5' 4.47"
167 **/
168 private static final String COORD_DEGREES_LATITUDE =
169 "([-+NnSs]" + "(\\s)*)?"
170 + "[1-9]?[0-9](\u00B0)" + "(\\s)*"
171 + "([1-5]?[0-9]\')?" + "(\\s)*"
172 + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?"
173 + "((\\s)*" + "[NnSs])?";
174 private static final String COORD_DEGREES_LONGITUDE =
175 "([-+EeWw]" + "(\\s)*)?"
176 + "(1)?[0-9]?[0-9](\u00B0)" + "(\\s)*"
177 + "([1-5]?[0-9]\')?" + "(\\s)*"
178 + "([1-5]?[0-9]" + "(\\.[0-9]+)?\")?"
179 + "((\\s)*" + "[EeWw])?";
180 private static final String COORD_DEGREES_PATTERN =
181 COORD_DEGREES_LATITUDE
182 + "(\\s)*" + "," + "(\\s)*"
183 + COORD_DEGREES_LONGITUDE;
184 private static final String COORD_DECIMAL_LATITUDE =
185 "[+-]?"
186 + "[1-9]?[0-9]" + "(\\.[0-9]+)"
187 + "(\u00B0)?";
188 private static final String COORD_DECIMAL_LONGITUDE =
189 "[+-]?"
190 + "(1)?[0-9]?[0-9]" + "(\\.[0-9]+)"
191 + "(\u00B0)?";
192 private static final String COORD_DECIMAL_PATTERN =
193 COORD_DECIMAL_LATITUDE
194 + "(\\s)*" + "," + "(\\s)*"
195 + COORD_DECIMAL_LONGITUDE;
196 private static final Pattern COORD_PATTERN =
197 Pattern.compile(COORD_DEGREES_PATTERN + "|" + COORD_DECIMAL_PATTERN);
198
Sam Blitzstein7e19bf92012-11-13 10:02:41 -0800199 private static final String NANP_ALLOWED_SYMBOLS = "()+-*#.";
200 private static final int NANP_MIN_DIGITS = 7;
201 private static final int NANP_MAX_DIGITS = 11;
202
203
Sara Tingfac2d152012-05-31 14:59:57 -0700204 /**
205 * Returns whether the SDK is the Jellybean release or later.
206 */
207 public static boolean isJellybeanOrLater() {
208 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
209 }
210
Isaac Katzenelsone8305d62013-09-03 14:56:26 -0700211 /**
212 * Returns whether the SDK is the KeyLimePie release or later.
213 */
214 public static boolean isKeyLimePieOrLater() {
Sam Blitzstein4e7b1b22013-10-09 10:19:37 -0700215 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
Isaac Katzenelsone8305d62013-09-03 14:56:26 -0700216 }
217
Michael Chand6734db2010-07-22 00:48:08 -0700218 public static int getViewTypeFromIntentAndSharedPref(Activity activity) {
Erikdd95df52010-08-27 09:31:18 -0700219 Intent intent = activity.getIntent();
220 Bundle extras = intent.getExtras();
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700221 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity);
Michael Chand6734db2010-07-22 00:48:08 -0700222
Erik7b92da22010-09-23 14:55:22 -0700223 if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) {
Erikdd95df52010-08-27 09:31:18 -0700224 return ViewType.EDIT;
225 }
Michael Chand6734db2010-07-22 00:48:08 -0700226 if (extras != null) {
227 if (extras.getBoolean(INTENT_KEY_DETAIL_VIEW, false)) {
228 // This is the "detail" view which is either agenda or day view
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700229 return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW,
230 GeneralPreferences.DEFAULT_DETAILED_VIEW);
Michael Chand6734db2010-07-22 00:48:08 -0700231 } else if (INTENT_VALUE_VIEW_TYPE_DAY.equals(extras.getString(INTENT_KEY_VIEW_TYPE))) {
232 // Not sure who uses this. This logic came from LaunchActivity
233 return ViewType.DAY;
234 }
235 }
236
237 // Default to the last view
Erik7b92da22010-09-23 14:55:22 -0700238 return prefs.getInt(
239 GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW);
Michael Chand6734db2010-07-22 00:48:08 -0700240 }
Michael Chanff6be832010-03-11 17:52:48 -0800241
Erik235d59c2010-09-02 16:29:59 -0700242 /**
RoboErik064beb92011-06-16 17:25:14 -0700243 * Gets the intent action for telling the widget to update.
244 */
245 public static String getWidgetUpdateAction(Context context) {
246 return context.getPackageName() + ".APPWIDGET_UPDATE";
247 }
248
249 /**
250 * Gets the intent action for telling the widget to update.
251 */
252 public static String getWidgetScheduledUpdateAction(Context context) {
253 return context.getPackageName() + ".APPWIDGET_SCHEDULED_UPDATE";
254 }
255
256 /**
257 * Gets the intent action for telling the widget to update.
258 */
259 public static String getSearchAuthority(Context context) {
260 return context.getPackageName() + ".CalendarRecentSuggestionsProvider";
261 }
262
263 /**
Erik7b92da22010-09-23 14:55:22 -0700264 * Writes a new home time zone to the db. Updates the home time zone in the
265 * db asynchronously and updates the local cache. Sending a time zone of
266 * **tbd** will cause it to be set to the device's time zone. null or empty
267 * tz will be ignored.
Erik3dc5e902010-09-07 11:33:07 -0700268 *
269 * @param context The calling activity
270 * @param timeZone The time zone to set Calendar to, or **tbd**
271 */
272 public static void setTimeZone(Context context, String timeZone) {
Erika48b9d42010-09-22 15:31:03 -0700273 mTZUtils.setTimeZone(context, timeZone);
Erik3dc5e902010-09-07 11:33:07 -0700274 }
275
276 /**
Erik7b92da22010-09-23 14:55:22 -0700277 * Gets the time zone that Calendar should be displayed in This is a helper
278 * method to get the appropriate time zone for Calendar. If this is the
279 * first time this method has been called it will initiate an asynchronous
280 * query to verify that the data in preferences is correct. The callback
281 * supplied will only be called if this query returns a value other than
282 * what is stored in preferences and should cause the calling activity to
283 * refresh anything that depends on calling this method.
Erik235d59c2010-09-02 16:29:59 -0700284 *
285 * @param context The calling activity
Erik7b92da22010-09-23 14:55:22 -0700286 * @param callback The runnable that should execute if a query returns new
287 * values
288 * @return The string value representing the time zone Calendar should
289 * display
Erik235d59c2010-09-02 16:29:59 -0700290 */
291 public static String getTimeZone(Context context, Runnable callback) {
Erika48b9d42010-09-22 15:31:03 -0700292 return mTZUtils.getTimeZone(context, callback);
Michael Chan45efa092010-02-03 17:44:37 -0800293 }
294
Erik14276572010-09-03 15:05:28 -0700295 /**
296 * Formats a date or a time range according to the local conventions.
297 *
298 * @param context the context is required only if the time is shown
299 * @param startMillis the start time in UTC milliseconds
300 * @param endMillis the end time in UTC milliseconds
Daisuke Miyakawa29190972010-10-27 13:14:38 -0700301 * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter,
302 * long, long, int, String) formatDateRange}
Erik14276572010-09-03 15:05:28 -0700303 * @return a string containing the formatted date/time range.
304 */
Erik7b92da22010-09-23 14:55:22 -0700305 public static String formatDateRange(
306 Context context, long startMillis, long endMillis, int flags) {
Erika48b9d42010-09-22 15:31:03 -0700307 return mTZUtils.formatDateRange(context, startMillis, endMillis, flags);
308 }
309
Michael Chan0b674be2012-11-20 07:16:14 -0800310 public static boolean getDefaultVibrate(Context context, SharedPreferences prefs) {
311 boolean vibrate;
312 if (prefs.contains(KEY_ALERTS_VIBRATE_WHEN)) {
313 // Migrate setting to new 4.2 behavior
314 //
315 // silent and never -> off
316 // always -> on
317 String vibrateWhen = prefs.getString(KEY_ALERTS_VIBRATE_WHEN, null);
318 vibrate = vibrateWhen != null && vibrateWhen.equals(context
319 .getString(R.string.prefDefault_alerts_vibrate_true));
320 prefs.edit().remove(KEY_ALERTS_VIBRATE_WHEN).commit();
321 Log.d(TAG, "Migrating KEY_ALERTS_VIBRATE_WHEN(" + vibrateWhen
322 + ") to KEY_ALERTS_VIBRATE = " + vibrate);
323 } else {
324 vibrate = prefs.getBoolean(GeneralPreferences.KEY_ALERTS_VIBRATE,
325 false);
326 }
327 return vibrate;
328 }
329
Michael Chane98dca72012-06-16 08:22:47 -0700330 public static String[] getSharedPreference(Context context, String key, String[] defaultValue) {
331 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
332 Set<String> ss = prefs.getStringSet(key, null);
333 if (ss != null) {
334 String strings[] = new String[ss.size()];
335 return ss.toArray(strings);
336 }
337 return defaultValue;
338 }
339
Erika48b9d42010-09-22 15:31:03 -0700340 public static String getSharedPreference(Context context, String key, String defaultValue) {
341 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
342 return prefs.getString(key, defaultValue);
Erik14276572010-09-03 15:05:28 -0700343 }
344
Michael Chand6734db2010-07-22 00:48:08 -0700345 public static int getSharedPreference(Context context, String key, int defaultValue) {
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700346 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
Michael Chand6734db2010-07-22 00:48:08 -0700347 return prefs.getInt(key, defaultValue);
348 }
349
Erikca478672011-01-19 20:02:47 -0800350 public static boolean getSharedPreference(Context context, String key, boolean defaultValue) {
351 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
352 return prefs.getBoolean(key, defaultValue);
353 }
354
Mason Tangf4ad4752010-08-23 17:54:08 -0700355 /**
356 * Asynchronously sets the preference with the given key to the given value
357 *
358 * @param context the context to use to get preferences from
359 * @param key the key of the preference to set
360 * @param value the value to set
361 */
Erikfbce65e2010-08-16 12:43:13 -0700362 public static void setSharedPreference(Context context, String key, String value) {
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700363 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
Brad Fitzpatrick24fac462010-08-30 19:07:20 -0700364 prefs.edit().putString(key, value).apply();
Michael Chan45efa092010-02-03 17:44:37 -0800365 }
366
Michael Chane98dca72012-06-16 08:22:47 -0700367 public static void setSharedPreference(Context context, String key, String[] values) {
368 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
369 LinkedHashSet<String> set = new LinkedHashSet<String>();
Michael Chan64270902012-09-26 15:08:57 -0700370 for (String value : values) {
371 set.add(value);
Michael Chane98dca72012-06-16 08:22:47 -0700372 }
373 prefs.edit().putStringSet(key, set).apply();
374 }
375
Erikca478672011-01-19 20:02:47 -0800376 protected static void tardis() {
377 mTardis = System.currentTimeMillis();
378 }
379
380 protected static long getTardis() {
381 return mTardis;
382 }
383
Sara Ting3a07a682012-10-31 13:19:38 -0700384 public static void setSharedPreference(Context context, String key, boolean value) {
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700385 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
Erik3dc5e902010-09-07 11:33:07 -0700386 SharedPreferences.Editor editor = prefs.edit();
387 editor.putBoolean(key, value);
Erik275232d2010-09-07 15:03:21 -0700388 editor.apply();
Erik3dc5e902010-09-07 11:33:07 -0700389 }
390
Michael Chand885c1a2010-08-26 00:06:25 -0700391 static void setSharedPreference(Context context, String key, int value) {
392 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
393 SharedPreferences.Editor editor = prefs.edit();
394 editor.putInt(key, value);
395 editor.apply();
396 }
397
Michael Chan1851ecb2013-04-18 18:36:24 -0700398 public static void removeSharedPreference(Context context, String key) {
399 SharedPreferences prefs = context.getSharedPreferences(
400 GeneralPreferences.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
401 prefs.edit().remove(key).apply();
402 }
403
404 // The backed up ring tone preference should not used because it is a device
405 // specific Uri. The preference now lives in a separate non-backed-up
406 // shared_pref file (SHARED_PREFS_NAME_NO_BACKUP). The preference in the old
407 // backed-up shared_pref file (SHARED_PREFS_NAME) is used only to control the
408 // default value when the ringtone dialog opens up.
409 //
410 // At backup manager "restore" time (which should happen before launcher
411 // comes up for the first time), the value will be set/reset to default
412 // ringtone.
413 public static String getRingTonePreference(Context context) {
414 SharedPreferences prefs = context.getSharedPreferences(
415 GeneralPreferences.SHARED_PREFS_NAME_NO_BACKUP, Context.MODE_PRIVATE);
416 String ringtone = prefs.getString(GeneralPreferences.KEY_ALERTS_RINGTONE, null);
417
418 // If it hasn't been populated yet, that means new code is running for
419 // the first time and restore hasn't happened. Migrate value from
420 // backed-up shared_pref to non-shared_pref.
421 if (ringtone == null) {
422 // Read from the old place with a default of DEFAULT_RINGTONE
423 ringtone = getSharedPreference(context, GeneralPreferences.KEY_ALERTS_RINGTONE,
424 GeneralPreferences.DEFAULT_RINGTONE);
425
426 // Write it to the new place
427 setRingTonePreference(context, ringtone);
428 }
429
430 return ringtone;
431 }
432
433 public static void setRingTonePreference(Context context, String value) {
434 SharedPreferences prefs = context.getSharedPreferences(
435 GeneralPreferences.SHARED_PREFS_NAME_NO_BACKUP, Context.MODE_PRIVATE);
436 prefs.edit().putString(GeneralPreferences.KEY_ALERTS_RINGTONE, value).apply();
437 }
438
Michael Chand6734db2010-07-22 00:48:08 -0700439 /**
440 * Save default agenda/day/week/month view for next time
441 *
442 * @param context
443 * @param viewId {@link CalendarController.ViewType}
444 */
Michael Chane8aa59d2009-09-15 14:44:43 -0700445 static void setDefaultView(Context context, int viewId) {
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700446 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
Michael Chane8aa59d2009-09-15 14:44:43 -0700447 SharedPreferences.Editor editor = prefs.edit();
Mason Tangf4ad4752010-08-23 17:54:08 -0700448
Michael Chanb60218a2010-12-14 16:34:39 -0800449 boolean validDetailView = false;
450 if (mAllowWeekForDetailView && viewId == CalendarController.ViewType.WEEK) {
451 validDetailView = true;
452 } else {
453 validDetailView = viewId == CalendarController.ViewType.AGENDA
454 || viewId == CalendarController.ViewType.DAY;
455 }
456
457 if (validDetailView) {
458 // Record the detail start view
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700459 editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId);
Michael Chane8aa59d2009-09-15 14:44:43 -0700460 }
461
462 // Record the (new) start view
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700463 editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId);
Brad Fitzpatrick24fac462010-08-30 19:07:20 -0700464 editor.apply();
Michael Chane8aa59d2009-09-15 14:44:43 -0700465 }
466
Erika144f862010-03-29 18:20:32 -0700467 public static MatrixCursor matrixCursorFromCursor(Cursor cursor) {
Michael Chan64270902012-09-26 15:08:57 -0700468 if (cursor == null) {
469 return null;
470 }
471
Sara Ting85e3cef2012-04-29 22:41:56 -0700472 String[] columnNames = cursor.getColumnNames();
473 if (columnNames == null) {
474 columnNames = new String[] {};
475 }
476 MatrixCursor newCursor = new MatrixCursor(columnNames);
Erika144f862010-03-29 18:20:32 -0700477 int numColumns = cursor.getColumnCount();
478 String data[] = new String[numColumns];
479 cursor.moveToPosition(-1);
480 while (cursor.moveToNext()) {
481 for (int i = 0; i < numColumns; i++) {
482 data[i] = cursor.getString(i);
483 }
484 newCursor.addRow(data);
485 }
486 return newCursor;
487 }
488
489 /**
490 * Compares two cursors to see if they contain the same data.
491 *
Erik7b92da22010-09-23 14:55:22 -0700492 * @return Returns true of the cursors contain the same data and are not
493 * null, false otherwise
Erika144f862010-03-29 18:20:32 -0700494 */
495 public static boolean compareCursors(Cursor c1, Cursor c2) {
Erik7b92da22010-09-23 14:55:22 -0700496 if (c1 == null || c2 == null) {
Erika144f862010-03-29 18:20:32 -0700497 return false;
498 }
499
500 int numColumns = c1.getColumnCount();
501 if (numColumns != c2.getColumnCount()) {
502 return false;
503 }
504
505 if (c1.getCount() != c2.getCount()) {
506 return false;
507 }
508
509 c1.moveToPosition(-1);
510 c2.moveToPosition(-1);
Erik7b92da22010-09-23 14:55:22 -0700511 while (c1.moveToNext() && c2.moveToNext()) {
512 for (int i = 0; i < numColumns; i++) {
513 if (!TextUtils.equals(c1.getString(i), c2.getString(i))) {
Erika144f862010-03-29 18:20:32 -0700514 return false;
515 }
516 }
517 }
518
519 return true;
520 }
521
The Android Open Source Project146de362009-03-03 19:32:18 -0800522 /**
523 * If the given intent specifies a time (in milliseconds since the epoch),
524 * then that time is returned. Otherwise, the current time is returned.
525 */
526 public static final long timeFromIntentInMillis(Intent intent) {
Erik7b92da22010-09-23 14:55:22 -0700527 // If the time was specified, then use that. Otherwise, use the current
528 // time.
Erik1ef7f3a2010-02-24 14:46:03 -0800529 Uri data = intent.getData();
RoboErika27a8862011-06-23 15:26:23 -0700530 long millis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1);
Erik1ef7f3a2010-02-24 14:46:03 -0800531 if (millis == -1 && data != null && data.isHierarchical()) {
532 List<String> path = data.getPathSegments();
Erik7b92da22010-09-23 14:55:22 -0700533 if (path.size() == 2 && path.get(0).equals("time")) {
Erik1ef7f3a2010-02-24 14:46:03 -0800534 try {
535 millis = Long.valueOf(data.getLastPathSegment());
536 } catch (NumberFormatException e) {
Erik7b92da22010-09-23 14:55:22 -0700537 Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time "
538 + "found. Using current time.");
Erik1ef7f3a2010-02-24 14:46:03 -0800539 }
540 }
541 }
Erik76727b72010-02-26 15:29:37 -0800542 if (millis <= 0) {
The Android Open Source Project146de362009-03-03 19:32:18 -0800543 millis = System.currentTimeMillis();
544 }
545 return millis;
546 }
547
The Android Open Source Project146de362009-03-03 19:32:18 -0800548 /**
Erik7b92da22010-09-23 14:55:22 -0700549 * Formats the given Time object so that it gives the month and year (for
550 * example, "September 2007").
The Android Open Source Project146de362009-03-03 19:32:18 -0800551 *
552 * @param time the time to format
553 * @return the string containing the weekday and the date
554 */
Michael Chanad36a3c2010-01-27 18:03:25 -0800555 public static String formatMonthYear(Context context, Time time) {
RoboErikcfa204b2011-02-22 15:42:16 -0800556 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
557 | DateUtils.FORMAT_SHOW_YEAR;
558 long millis = time.toMillis(true);
559 return formatDateRange(context, millis, millis, flags);
The Android Open Source Project146de362009-03-03 19:32:18 -0800560 }
561
The Android Open Source Project146de362009-03-03 19:32:18 -0800562 /**
Mason Tang4c8871b2010-08-10 10:17:43 -0700563 * Returns a list joined together by the provided delimiter, for example,
564 * ["a", "b", "c"] could be joined into "a,b,c"
565 *
566 * @param things the things to join together
567 * @param delim the delimiter to use
568 * @return a string contained the things joined together
569 */
570 public static String join(List<?> things, String delim) {
571 StringBuilder builder = new StringBuilder();
572 boolean first = true;
573 for (Object thing : things) {
574 if (first) {
575 first = false;
576 } else {
577 builder.append(delim);
578 }
579 builder.append(thing.toString());
580 }
581 return builder.toString();
582 }
583
584 /**
Erik981874e2010-10-05 16:52:52 -0700585 * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970)
586 * adjusted for first day of week.
587 *
588 * This takes a julian day and the week start day and calculates which
589 * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting
590 * at 0. *Do not* use this to compute the ISO week number for the year.
591 *
592 * @param julianDay The julian day to calculate the week number for
593 * @param firstDayOfWeek Which week day is the first day of the week,
594 * see {@link Time#SUNDAY}
595 * @return Weeks since the epoch
596 */
597 public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
598 int diff = Time.THURSDAY - firstDayOfWeek;
599 if (diff < 0) {
600 diff += 7;
601 }
602 int refDay = Time.EPOCH_JULIAN_DAY - diff;
603 return (julianDay - refDay) / 7;
604 }
605
606 /**
607 * Takes a number of weeks since the epoch and calculates the Julian day of
608 * the Monday for that week.
609 *
610 * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY}
611 * is considered week 0. It returns the Julian day for the Monday
612 * {@code week} weeks after the Monday of the week containing the epoch.
613 *
614 * @param week Number of weeks since the epoch
615 * @return The julian day for the Monday of the given week since the epoch
616 */
617 public static int getJulianMondayFromWeeksSinceEpoch(int week) {
618 return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
619 }
620
621 /**
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900622 * Get first day of week as android.text.format.Time constant.
Erik7b92da22010-09-23 14:55:22 -0700623 *
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900624 * @return the first day of week in android.text.format.Time
625 */
Mason Tang8e3d4302010-07-12 17:39:30 -0700626 public static int getFirstDayOfWeek(Context context) {
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700627 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
Erik7b92da22010-09-23 14:55:22 -0700628 String pref = prefs.getString(
629 GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT);
Mason Tang8e3d4302010-07-12 17:39:30 -0700630
631 int startDay;
Daisuke Miyakawa4b441bd2010-09-16 14:55:36 -0700632 if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) {
Mason Tang8e3d4302010-07-12 17:39:30 -0700633 startDay = Calendar.getInstance().getFirstDayOfWeek();
634 } else {
635 startDay = Integer.parseInt(pref);
636 }
637
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900638 if (startDay == Calendar.SATURDAY) {
639 return Time.SATURDAY;
640 } else if (startDay == Calendar.MONDAY) {
641 return Time.MONDAY;
642 } else {
643 return Time.SUNDAY;
644 }
645 }
646
647 /**
James Kung56f42bf2013-03-29 14:59:29 -0700648 * Get first day of week as java.util.Calendar constant.
649 *
650 * @return the first day of week as a java.util.Calendar constant
651 */
652 public static int getFirstDayOfWeekAsCalendar(Context context) {
653 return convertDayOfWeekFromTimeToCalendar(getFirstDayOfWeek(context));
654 }
655
656 /**
657 * Converts the day of the week from android.text.format.Time to java.util.Calendar
658 */
659 public static int convertDayOfWeekFromTimeToCalendar(int timeDayOfWeek) {
660 switch (timeDayOfWeek) {
661 case Time.MONDAY:
662 return Calendar.MONDAY;
663 case Time.TUESDAY:
664 return Calendar.TUESDAY;
665 case Time.WEDNESDAY:
666 return Calendar.WEDNESDAY;
667 case Time.THURSDAY:
668 return Calendar.THURSDAY;
669 case Time.FRIDAY:
670 return Calendar.FRIDAY;
671 case Time.SATURDAY:
672 return Calendar.SATURDAY;
673 case Time.SUNDAY:
674 return Calendar.SUNDAY;
675 default:
676 throw new IllegalArgumentException("Argument must be between Time.SUNDAY and " +
677 "Time.SATURDAY");
678 }
679 }
680
681 /**
Daisuke Miyakawad644b0d2010-10-21 15:45:12 -0700682 * @return true when week number should be shown.
Erik981874e2010-10-05 16:52:52 -0700683 */
684 public static boolean getShowWeekNumber(Context context) {
Daisuke Miyakawad644b0d2010-10-21 15:45:12 -0700685 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
Erik981874e2010-10-05 16:52:52 -0700686 return prefs.getBoolean(
687 GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM);
688 }
689
690 /**
Erik40bcd102010-11-16 15:46:40 -0800691 * @return true when declined events should be hidden.
692 */
693 public static boolean getHideDeclinedEvents(Context context) {
694 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
695 return prefs.getBoolean(GeneralPreferences.KEY_HIDE_DECLINED, false);
696 }
697
Erik91b01ed2010-11-22 17:46:15 -0800698 public static int getDaysPerWeek(Context context) {
699 final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context);
700 return prefs.getInt(GeneralPreferences.KEY_DAYS_PER_WEEK, 7);
701 }
702
Erik40bcd102010-11-16 15:46:40 -0800703 /**
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900704 * Determine whether the column position is Saturday or not.
Erik7b92da22010-09-23 14:55:22 -0700705 *
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900706 * @param column the column position
707 * @param firstDayOfWeek the first day of week in android.text.format.Time
708 * @return true if the column is Saturday position
709 */
710 public static boolean isSaturday(int column, int firstDayOfWeek) {
711 return (firstDayOfWeek == Time.SUNDAY && column == 6)
Erik7b92da22010-09-23 14:55:22 -0700712 || (firstDayOfWeek == Time.MONDAY && column == 5)
713 || (firstDayOfWeek == Time.SATURDAY && column == 0);
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900714 }
715
716 /**
717 * Determine whether the column position is Sunday or not.
Erik7b92da22010-09-23 14:55:22 -0700718 *
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900719 * @param column the column position
720 * @param firstDayOfWeek the first day of week in android.text.format.Time
721 * @return true if the column is Sunday position
722 */
723 public static boolean isSunday(int column, int firstDayOfWeek) {
724 return (firstDayOfWeek == Time.SUNDAY && column == 0)
Erik7b92da22010-09-23 14:55:22 -0700725 || (firstDayOfWeek == Time.MONDAY && column == 6)
726 || (firstDayOfWeek == Time.SATURDAY && column == 1);
Takaoka G. Tadashi56adc7b2010-01-22 19:16:43 +0900727 }
Michael Chanff6be832010-03-11 17:52:48 -0800728
729 /**
RoboErik9da910f2011-03-15 11:26:15 -0700730 * Convert given UTC time into current local time. This assumes it is for an
731 * allday event and will adjust the time to be on a midnight boundary.
Mason Tang3ea333d2010-08-24 15:57:00 -0700732 *
733 * @param recycle Time object to recycle, otherwise null.
734 * @param utcTime Time to convert, in UTC.
RoboErik9da910f2011-03-15 11:26:15 -0700735 * @param tz The time zone to convert this time to.
Mason Tang3ea333d2010-08-24 15:57:00 -0700736 */
RoboErik9da910f2011-03-15 11:26:15 -0700737 public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) {
Mason Tang3ea333d2010-08-24 15:57:00 -0700738 if (recycle == null) {
739 recycle = new Time();
740 }
741 recycle.timezone = Time.TIMEZONE_UTC;
742 recycle.set(utcTime);
RoboErik9da910f2011-03-15 11:26:15 -0700743 recycle.timezone = tz;
744 return recycle.normalize(true);
745 }
746
747 public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) {
748 if (recycle == null) {
749 recycle = new Time();
750 }
751 recycle.timezone = tz;
752 recycle.set(localTime);
753 recycle.timezone = Time.TIMEZONE_UTC;
Mason Tang3ea333d2010-08-24 15:57:00 -0700754 return recycle.normalize(true);
755 }
756
757 /**
Isaac Katzenelsonc1fae4d2011-11-07 11:19:32 -0800758 * Finds and returns the next midnight after "theTime" in milliseconds UTC
759 *
760 * @param recycle - Time object to recycle, otherwise null.
761 * @param theTime - Time used for calculations (in UTC)
762 * @param tz The time zone to convert this time to.
763 */
764 public static long getNextMidnight(Time recycle, long theTime, String tz) {
765 if (recycle == null) {
766 recycle = new Time();
767 }
768 recycle.timezone = tz;
769 recycle.set(theTime);
770 recycle.monthDay ++;
771 recycle.hour = 0;
772 recycle.minute = 0;
773 recycle.second = 0;
774 return recycle.normalize(true);
775 }
776
777 /**
Michael Chanff6be832010-03-11 17:52:48 -0800778 * Scan through a cursor of calendars and check if names are duplicated.
Erik7b92da22010-09-23 14:55:22 -0700779 * This travels a cursor containing calendar display names and fills in the
780 * provided map with whether or not each name is repeated.
Michael Chanff6be832010-03-11 17:52:48 -0800781 *
Michael Chanff6be832010-03-11 17:52:48 -0800782 * @param isDuplicateName The map to put the duplicate check results in.
783 * @param cursor The query of calendars to check
784 * @param nameIndex The column of the query that contains the display name
785 */
Erik7b92da22010-09-23 14:55:22 -0700786 public static void checkForDuplicateNames(
787 Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) {
Michael Chanff6be832010-03-11 17:52:48 -0800788 isDuplicateName.clear();
789 cursor.moveToPosition(-1);
790 while (cursor.moveToNext()) {
791 String displayName = cursor.getString(nameIndex);
792 // Set it to true if we've seen this name before, false otherwise
793 if (displayName != null) {
794 isDuplicateName.put(displayName, isDuplicateName.containsKey(displayName));
795 }
796 }
797 }
Mason Tang9138ce82010-06-28 11:08:46 -0700798
799 /**
800 * Null-safe object comparison
Erik7b92da22010-09-23 14:55:22 -0700801 *
Mason Tang9138ce82010-06-28 11:08:46 -0700802 * @param s1
803 * @param s2
804 * @return
805 */
806 public static boolean equals(Object o1, Object o2) {
807 return o1 == null ? o2 == null : o1.equals(o2);
808 }
Michael Chanb60218a2010-12-14 16:34:39 -0800809
Erik63cd0532011-01-26 14:16:03 -0800810 public static void setAllowWeekForDetailView(boolean allowWeekView) {
Michael Chanb60218a2010-12-14 16:34:39 -0800811 mAllowWeekForDetailView = allowWeekView;
812 }
Erik63cd0532011-01-26 14:16:03 -0800813
814 public static boolean getAllowWeekForDetailView() {
815 return mAllowWeekForDetailView;
816 }
Isaac Katzenelson0b1bd102011-04-07 14:26:29 -0700817
Isaac Katzenelsonff5c4342011-04-14 16:53:24 -0700818 public static boolean getConfigBool(Context c, int key) {
819 return c.getResources().getBoolean(key);
820 }
Isaac Katzenelson82400dd2011-04-15 11:13:49 -0700821
James Kungede0fb12013-02-19 15:52:40 -0800822 /**
823 * For devices with Jellybean or later, darkens the given color to ensure that white text is
824 * clearly visible on top of it. For devices prior to Jellybean, does nothing, as the
825 * sync adapter handles the color change.
826 *
827 * @param color
828 */
RoboErik4acb2fd2011-07-18 15:39:49 -0700829 public static int getDisplayColorFromColor(int color) {
Michael Chan537f43d2012-08-03 10:51:18 -0700830 if (!isJellybeanOrLater()) {
831 return color;
832 }
Michael Chanf9411fe2012-02-10 17:05:52 -0800833
RoboErik4acb2fd2011-07-18 15:39:49 -0700834 float[] hsv = new float[3];
835 Color.colorToHSV(color, hsv);
Michael Chanf9411fe2012-02-10 17:05:52 -0800836 hsv[1] = Math.min(hsv[1] * SATURATION_ADJUST, 1.0f);
837 hsv[2] = hsv[2] * INTENSITY_ADJUST;
RoboErik4acb2fd2011-07-18 15:39:49 -0700838 return Color.HSVToColor(hsv);
839 }
840
RoboErik3c40e072011-09-14 11:25:20 -0700841 // This takes a color and computes what it would look like blended with
842 // white. The result is the color that should be used for declined events.
843 public static int getDeclinedColorFromColor(int color) {
844 int bg = 0xffffffff;
Isaac Katzenelsone6109c52011-10-14 17:23:11 -0700845 int a = DECLINED_EVENT_ALPHA;
RoboErik3c40e072011-09-14 11:25:20 -0700846 int r = (((color & 0x00ff0000) * a) + ((bg & 0x00ff0000) * (0xff - a))) & 0xff000000;
847 int g = (((color & 0x0000ff00) * a) + ((bg & 0x0000ff00) * (0xff - a))) & 0x00ff0000;
848 int b = (((color & 0x000000ff) * a) + ((bg & 0x000000ff) * (0xff - a))) & 0x0000ff00;
849 return (0xff000000) | ((r | g | b) >> 8);
850 }
851
Sam Blitzsteinf52c6412013-12-04 12:59:36 -0800852 public static void trySyncAndDisableUpgradeReceiver(Context context) {
853 final PackageManager pm = context.getPackageManager();
854 ComponentName upgradeComponent = new ComponentName(context, UpgradeReceiver.class);
855 if (pm.getComponentEnabledSetting(upgradeComponent) ==
856 PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
857 // The upgrade receiver has been disabled, which means this code has been run before,
858 // so no need to sync.
859 return;
860 }
861
862 Bundle extras = new Bundle();
863 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
864 ContentResolver.requestSync(
865 null /* no account */,
866 Calendars.CONTENT_URI.getAuthority(),
867 extras);
868
869 // Now unregister the receiver so that we won't continue to sync every time.
870 pm.setComponentEnabledSetting(upgradeComponent,
871 PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
872 }
873
RoboErik092caec2011-06-23 10:26:12 -0700874 // A single strand represents one color of events. Events are divided up by
875 // color to make them convenient to draw. The black strand is special in
876 // that it holds conflicting events as well as color settings for allday on
877 // each day.
878 public static class DNAStrand {
879 public float[] points;
880 public int[] allDays; // color for the allday, 0 means no event
881 int position;
882 public int color;
883 int count;
884 }
Isaac Katzenelson82400dd2011-04-15 11:13:49 -0700885
RoboErik092caec2011-06-23 10:26:12 -0700886 // A segment is a single continuous length of time occupied by a single
887 // color. Segments should never span multiple days.
888 private static class DNASegment {
889 int startMinute; // in minutes since the start of the week
890 int endMinute;
891 int color; // Calendar color or black for conflicts
892 int day; // quick reference to the day this segment is on
Isaac Katzenelson72a94592011-05-05 12:37:57 -0700893 }
894
895 /**
RoboErik092caec2011-06-23 10:26:12 -0700896 * Converts a list of events to a list of segments to draw. Assumes list is
897 * ordered by start time of the events. The function processes events for a
898 * range of days from firstJulianDay to firstJulianDay + dayXs.length - 1.
899 * The algorithm goes over all the events and creates a set of segments
900 * ordered by start time. This list of segments is then converted into a
901 * HashMap of strands which contain the draw points and are organized by
902 * color. The strands can then be drawn by setting the paint color to each
903 * strand's color and calling drawLines on its set of points. The points are
904 * set up using the following parameters.
905 * <ul>
906 * <li>Events between midnight and WORK_DAY_START_MINUTES are compressed
907 * into the first 1/8th of the space between top and bottom.</li>
908 * <li>Events between WORK_DAY_END_MINUTES and the following midnight are
909 * compressed into the last 1/8th of the space between top and bottom</li>
910 * <li>Events between WORK_DAY_START_MINUTES and WORK_DAY_END_MINUTES use
911 * the remaining 3/4ths of the space</li>
912 * <li>All segments drawn will maintain at least minPixels height, except
913 * for conflicts in the first or last 1/8th, which may be smaller</li>
914 * </ul>
Isaac Katzenelson72a94592011-05-05 12:37:57 -0700915 *
RoboErik092caec2011-06-23 10:26:12 -0700916 * @param firstJulianDay The julian day of the first day of events
917 * @param events A list of events sorted by start time
918 * @param top The lowest y value the dna should be drawn at
919 * @param bottom The highest y value the dna should be drawn at
920 * @param dayXs An array of x values to draw the dna at, one for each day
921 * @param conflictColor the color to use for conflicts
922 * @return
Isaac Katzenelson72a94592011-05-05 12:37:57 -0700923 */
RoboErik092caec2011-06-23 10:26:12 -0700924 public static HashMap<Integer, DNAStrand> createDNAStrands(int firstJulianDay,
925 ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs,
926 Context context) {
Isaac Katzenelson72a94592011-05-05 12:37:57 -0700927
RoboErik092caec2011-06-23 10:26:12 -0700928 if (!mMinutesLoaded) {
929 if (context == null) {
930 Log.wtf(TAG, "No context and haven't loaded parameters yet! Can't create DNA.");
931 }
932 Resources res = context.getResources();
933 CONFLICT_COLOR = res.getColor(R.color.month_dna_conflict_time_color);
934 WORK_DAY_START_MINUTES = res.getInteger(R.integer.work_start_minutes);
935 WORK_DAY_END_MINUTES = res.getInteger(R.integer.work_end_minutes);
936 WORK_DAY_END_LENGTH = DAY_IN_MINUTES - WORK_DAY_END_MINUTES;
937 WORK_DAY_MINUTES = WORK_DAY_END_MINUTES - WORK_DAY_START_MINUTES;
938 mMinutesLoaded = true;
Isaac Katzenelson72a94592011-05-05 12:37:57 -0700939 }
940
RoboErik092caec2011-06-23 10:26:12 -0700941 if (events == null || events.isEmpty() || dayXs == null || dayXs.length < 1
942 || bottom - top < 8 || minPixels < 0) {
943 Log.e(TAG,
944 "Bad values for createDNAStrands! events:" + events + " dayXs:"
945 + Arrays.toString(dayXs) + " bot-top:" + (bottom - top) + " minPixels:"
946 + minPixels);
947 return null;
Isaac Katzenelson72a94592011-05-05 12:37:57 -0700948 }
Isaac Katzenelson71b9ce32011-05-05 17:41:54 -0700949
RoboErik092caec2011-06-23 10:26:12 -0700950 LinkedList<DNASegment> segments = new LinkedList<DNASegment>();
951 HashMap<Integer, DNAStrand> strands = new HashMap<Integer, DNAStrand>();
952 // add a black strand by default, other colors will get added in
953 // the loop
954 DNAStrand blackStrand = new DNAStrand();
955 blackStrand.color = CONFLICT_COLOR;
956 strands.put(CONFLICT_COLOR, blackStrand);
957 // the min length is the number of minutes that will occupy
958 // MIN_SEGMENT_PIXELS in the 'work day' time slot. This computes the
959 // minutes/pixel * minpx where the number of pixels are 3/4 the total
960 // dna height: 4*(mins/(px * 3/4))
961 int minMinutes = minPixels * 4 * WORK_DAY_MINUTES / (3 * (bottom - top));
Isaac Katzenelson71b9ce32011-05-05 17:41:54 -0700962
RoboErik092caec2011-06-23 10:26:12 -0700963 // There are slightly fewer than half as many pixels in 1/6 the space,
964 // so round to 2.5x for the min minutes in the non-work area
965 int minOtherMinutes = minMinutes * 5 / 2;
966 int lastJulianDay = firstJulianDay + dayXs.length - 1;
Isaac Katzenelson71b9ce32011-05-05 17:41:54 -0700967
RoboErik092caec2011-06-23 10:26:12 -0700968 Event event = new Event();
969 // Go through all the events for the week
970 for (Event currEvent : events) {
971 // if this event is outside the weeks range skip it
972 if (currEvent.endDay < firstJulianDay || currEvent.startDay > lastJulianDay) {
973 continue;
974 }
975 if (currEvent.drawAsAllday()) {
976 addAllDayToStrands(currEvent, strands, firstJulianDay, dayXs.length);
977 continue;
978 }
979 // Copy the event over so we can clip its start and end to our range
980 currEvent.copyTo(event);
981 if (event.startDay < firstJulianDay) {
982 event.startDay = firstJulianDay;
983 event.startTime = 0;
984 }
985 // If it starts after the work day make sure the start is at least
986 // minPixels from midnight
987 if (event.startTime > DAY_IN_MINUTES - minOtherMinutes) {
988 event.startTime = DAY_IN_MINUTES - minOtherMinutes;
989 }
990 if (event.endDay > lastJulianDay) {
991 event.endDay = lastJulianDay;
992 event.endTime = DAY_IN_MINUTES - 1;
993 }
994 // If the end time is before the work day make sure it ends at least
995 // minPixels after midnight
996 if (event.endTime < minOtherMinutes) {
997 event.endTime = minOtherMinutes;
998 }
999 // If the start and end are on the same day make sure they are at
1000 // least minPixels apart. This only needs to be done for times
1001 // outside the work day as the min distance for within the work day
1002 // is enforced in the segment code.
1003 if (event.startDay == event.endDay &&
1004 event.endTime - event.startTime < minOtherMinutes) {
1005 // If it's less than minPixels in an area before the work
1006 // day
1007 if (event.startTime < WORK_DAY_START_MINUTES) {
1008 // extend the end to the first easy guarantee that it's
1009 // minPixels
1010 event.endTime = Math.min(event.startTime + minOtherMinutes,
1011 WORK_DAY_START_MINUTES + minMinutes);
1012 // if it's in the area after the work day
1013 } else if (event.endTime > WORK_DAY_END_MINUTES) {
1014 // First try shifting the end but not past midnight
1015 event.endTime = Math.min(event.endTime + minOtherMinutes, DAY_IN_MINUTES - 1);
1016 // if it's still too small move the start back
1017 if (event.endTime - event.startTime < minOtherMinutes) {
1018 event.startTime = event.endTime - minOtherMinutes;
1019 }
1020 }
Isaac Katzenelson71b9ce32011-05-05 17:41:54 -07001021 }
1022
RoboErik092caec2011-06-23 10:26:12 -07001023 // This handles adding the first segment
1024 if (segments.size() == 0) {
1025 addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes);
1026 continue;
Isaac Katzenelson71b9ce32011-05-05 17:41:54 -07001027 }
RoboErik092caec2011-06-23 10:26:12 -07001028 // Now compare our current start time to the end time of the last
1029 // segment in the list
1030 DNASegment lastSegment = segments.getLast();
1031 int startMinute = (event.startDay - firstJulianDay) * DAY_IN_MINUTES + event.startTime;
1032 int endMinute = Math.max((event.endDay - firstJulianDay) * DAY_IN_MINUTES
1033 + event.endTime, startMinute + minMinutes);
1034
1035 if (startMinute < 0) {
1036 startMinute = 0;
1037 }
1038 if (endMinute >= WEEK_IN_MINUTES) {
1039 endMinute = WEEK_IN_MINUTES - 1;
1040 }
1041 // If we start before the last segment in the list ends we need to
1042 // start going through the list as this may conflict with other
1043 // events
1044 if (startMinute < lastSegment.endMinute) {
1045 int i = segments.size();
1046 // find the last segment this event intersects with
1047 while (--i >= 0 && endMinute < segments.get(i).startMinute);
1048
1049 DNASegment currSegment;
1050 // for each segment this event intersects with
1051 for (; i >= 0 && startMinute <= (currSegment = segments.get(i)).endMinute; i--) {
1052 // if the segment is already a conflict ignore it
1053 if (currSegment.color == CONFLICT_COLOR) {
1054 continue;
1055 }
1056 // if the event ends before the segment and wouldn't create
1057 // a segment that is too small split off the right side
1058 if (endMinute < currSegment.endMinute - minMinutes) {
1059 DNASegment rhs = new DNASegment();
1060 rhs.endMinute = currSegment.endMinute;
1061 rhs.color = currSegment.color;
1062 rhs.startMinute = endMinute + 1;
1063 rhs.day = currSegment.day;
1064 currSegment.endMinute = endMinute;
1065 segments.add(i + 1, rhs);
1066 strands.get(rhs.color).count++;
1067 if (DEBUG) {
1068 Log.d(TAG, "Added rhs, curr:" + currSegment.toString() + " i:"
1069 + segments.get(i).toString());
1070 }
1071 }
1072 // if the event starts after the segment and wouldn't create
1073 // a segment that is too small split off the left side
1074 if (startMinute > currSegment.startMinute + minMinutes) {
1075 DNASegment lhs = new DNASegment();
1076 lhs.startMinute = currSegment.startMinute;
1077 lhs.color = currSegment.color;
1078 lhs.endMinute = startMinute - 1;
1079 lhs.day = currSegment.day;
1080 currSegment.startMinute = startMinute;
1081 // increment i so that we are at the right position when
1082 // referencing the segments to the right and left of the
1083 // current segment.
1084 segments.add(i++, lhs);
1085 strands.get(lhs.color).count++;
1086 if (DEBUG) {
1087 Log.d(TAG, "Added lhs, curr:" + currSegment.toString() + " i:"
1088 + segments.get(i).toString());
1089 }
1090 }
1091 // if the right side is black merge this with the segment to
1092 // the right if they're on the same day and overlap
1093 if (i + 1 < segments.size()) {
1094 DNASegment rhs = segments.get(i + 1);
1095 if (rhs.color == CONFLICT_COLOR && currSegment.day == rhs.day
1096 && rhs.startMinute <= currSegment.endMinute + 1) {
1097 rhs.startMinute = Math.min(currSegment.startMinute, rhs.startMinute);
1098 segments.remove(currSegment);
1099 strands.get(currSegment.color).count--;
1100 // point at the new current segment
1101 currSegment = rhs;
1102 }
1103 }
1104 // if the left side is black merge this with the segment to
1105 // the left if they're on the same day and overlap
1106 if (i - 1 >= 0) {
1107 DNASegment lhs = segments.get(i - 1);
1108 if (lhs.color == CONFLICT_COLOR && currSegment.day == lhs.day
1109 && lhs.endMinute >= currSegment.startMinute - 1) {
1110 lhs.endMinute = Math.max(currSegment.endMinute, lhs.endMinute);
1111 segments.remove(currSegment);
1112 strands.get(currSegment.color).count--;
1113 // point at the new current segment
1114 currSegment = lhs;
1115 // point i at the new current segment in case new
1116 // code is added
1117 i--;
1118 }
1119 }
1120 // if we're still not black, decrement the count for the
1121 // color being removed, change this to black, and increment
1122 // the black count
1123 if (currSegment.color != CONFLICT_COLOR) {
1124 strands.get(currSegment.color).count--;
1125 currSegment.color = CONFLICT_COLOR;
1126 strands.get(CONFLICT_COLOR).count++;
1127 }
1128 }
1129
1130 }
1131 // If this event extends beyond the last segment add a new segment
1132 if (endMinute > lastSegment.endMinute) {
1133 addNewSegment(segments, event, strands, firstJulianDay, lastSegment.endMinute,
1134 minMinutes);
1135 }
Isaac Katzenelson71b9ce32011-05-05 17:41:54 -07001136 }
RoboErik092caec2011-06-23 10:26:12 -07001137 weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs);
1138 return strands;
Isaac Katzenelson72a94592011-05-05 12:37:57 -07001139 }
1140
RoboErik092caec2011-06-23 10:26:12 -07001141 // This figures out allDay colors as allDay events are found
1142 private static void addAllDayToStrands(Event event, HashMap<Integer, DNAStrand> strands,
1143 int firstJulianDay, int numDays) {
1144 DNAStrand strand = getOrCreateStrand(strands, CONFLICT_COLOR);
1145 // if we haven't initialized the allDay portion create it now
1146 if (strand.allDays == null) {
1147 strand.allDays = new int[numDays];
Isaac Katzenelsonc18dd7a2011-04-19 10:33:19 -07001148 }
1149
RoboErik092caec2011-06-23 10:26:12 -07001150 // For each day this event is on update the color
1151 int end = Math.min(event.endDay - firstJulianDay, numDays - 1);
1152 for (int i = Math.max(event.startDay - firstJulianDay, 0); i <= end; i++) {
1153 if (strand.allDays[i] != 0) {
1154 // if this day already had a color, it is now a conflict
1155 strand.allDays[i] = CONFLICT_COLOR;
Isaac Katzenelson72a94592011-05-05 12:37:57 -07001156 } else {
RoboErik092caec2011-06-23 10:26:12 -07001157 // else it's just the color of the event
1158 strand.allDays[i] = event.color;
Isaac Katzenelson72a94592011-05-05 12:37:57 -07001159 }
Isaac Katzenelson82400dd2011-04-15 11:13:49 -07001160 }
Isaac Katzenelson82400dd2011-04-15 11:13:49 -07001161 }
RoboErik092caec2011-06-23 10:26:12 -07001162
1163 // This processes all the segments, sorts them by color, and generates a
1164 // list of points to draw
1165 private static void weaveDNAStrands(LinkedList<DNASegment> segments, int firstJulianDay,
1166 HashMap<Integer, DNAStrand> strands, int top, int bottom, int[] dayXs) {
1167 // First, get rid of any colors that ended up with no segments
1168 Iterator<DNAStrand> strandIterator = strands.values().iterator();
1169 while (strandIterator.hasNext()) {
1170 DNAStrand strand = strandIterator.next();
1171 if (strand.count < 1 && strand.allDays == null) {
1172 strandIterator.remove();
1173 continue;
1174 }
1175 strand.points = new float[strand.count * 4];
1176 strand.position = 0;
1177 }
1178 // Go through each segment and compute its points
1179 for (DNASegment segment : segments) {
1180 // Add the points to the strand of that color
1181 DNAStrand strand = strands.get(segment.color);
1182 int dayIndex = segment.day - firstJulianDay;
1183 int dayStartMinute = segment.startMinute % DAY_IN_MINUTES;
1184 int dayEndMinute = segment.endMinute % DAY_IN_MINUTES;
1185 int height = bottom - top;
1186 int workDayHeight = height * 3 / 4;
1187 int remainderHeight = (height - workDayHeight) / 2;
1188
1189 int x = dayXs[dayIndex];
1190 int y0 = 0;
1191 int y1 = 0;
1192
1193 y0 = top + getPixelOffsetFromMinutes(dayStartMinute, workDayHeight, remainderHeight);
1194 y1 = top + getPixelOffsetFromMinutes(dayEndMinute, workDayHeight, remainderHeight);
1195 if (DEBUG) {
1196 Log.d(TAG, "Adding " + Integer.toHexString(segment.color) + " at x,y0,y1: " + x
1197 + " " + y0 + " " + y1 + " for " + dayStartMinute + " " + dayEndMinute);
1198 }
1199 strand.points[strand.position++] = x;
1200 strand.points[strand.position++] = y0;
1201 strand.points[strand.position++] = x;
1202 strand.points[strand.position++] = y1;
1203 }
1204 }
1205
1206 /**
1207 * Compute a pixel offset from the top for a given minute from the work day
1208 * height and the height of the top area.
1209 */
1210 private static int getPixelOffsetFromMinutes(int minute, int workDayHeight,
1211 int remainderHeight) {
1212 int y;
1213 if (minute < WORK_DAY_START_MINUTES) {
1214 y = minute * remainderHeight / WORK_DAY_START_MINUTES;
1215 } else if (minute < WORK_DAY_END_MINUTES) {
1216 y = remainderHeight + (minute - WORK_DAY_START_MINUTES) * workDayHeight
1217 / WORK_DAY_MINUTES;
1218 } else {
1219 y = remainderHeight + workDayHeight + (minute - WORK_DAY_END_MINUTES) * remainderHeight
1220 / WORK_DAY_END_LENGTH;
1221 }
1222 return y;
1223 }
1224
1225 /**
1226 * Add a new segment based on the event provided. This will handle splitting
1227 * segments across day boundaries and ensures a minimum size for segments.
1228 */
1229 private static void addNewSegment(LinkedList<DNASegment> segments, Event event,
1230 HashMap<Integer, DNAStrand> strands, int firstJulianDay, int minStart, int minMinutes) {
1231 if (event.startDay > event.endDay) {
1232 Log.wtf(TAG, "Event starts after it ends: " + event.toString());
1233 }
1234 // If this is a multiday event split it up by day
1235 if (event.startDay != event.endDay) {
1236 Event lhs = new Event();
1237 lhs.color = event.color;
1238 lhs.startDay = event.startDay;
1239 // the first day we want the start time to be the actual start time
1240 lhs.startTime = event.startTime;
1241 lhs.endDay = lhs.startDay;
1242 lhs.endTime = DAY_IN_MINUTES - 1;
1243 // Nearly recursive iteration!
1244 while (lhs.startDay != event.endDay) {
1245 addNewSegment(segments, lhs, strands, firstJulianDay, minStart, minMinutes);
1246 // The days in between are all day, even though that shouldn't
1247 // actually happen due to the allday filtering
1248 lhs.startDay++;
1249 lhs.endDay = lhs.startDay;
1250 lhs.startTime = 0;
1251 minStart = 0;
1252 }
1253 // The last day we want the end time to be the actual end time
1254 lhs.endTime = event.endTime;
1255 event = lhs;
1256 }
1257 // Create the new segment and compute its fields
1258 DNASegment segment = new DNASegment();
1259 int dayOffset = (event.startDay - firstJulianDay) * DAY_IN_MINUTES;
1260 int endOfDay = dayOffset + DAY_IN_MINUTES - 1;
1261 // clip the start if needed
1262 segment.startMinute = Math.max(dayOffset + event.startTime, minStart);
1263 // and extend the end if it's too small, but not beyond the end of the
1264 // day
1265 int minEnd = Math.min(segment.startMinute + minMinutes, endOfDay);
1266 segment.endMinute = Math.max(dayOffset + event.endTime, minEnd);
1267 if (segment.endMinute > endOfDay) {
1268 segment.endMinute = endOfDay;
1269 }
1270
1271 segment.color = event.color;
1272 segment.day = event.startDay;
1273 segments.add(segment);
1274 // increment the count for the correct color or add a new strand if we
1275 // don't have that color yet
1276 DNAStrand strand = getOrCreateStrand(strands, segment.color);
1277 strand.count++;
1278 }
1279
1280 /**
1281 * Try to get a strand of the given color. Create it if it doesn't exist.
1282 */
1283 private static DNAStrand getOrCreateStrand(HashMap<Integer, DNAStrand> strands, int color) {
1284 DNAStrand strand = strands.get(color);
1285 if (strand == null) {
1286 strand = new DNAStrand();
1287 strand.color = color;
1288 strand.count = 0;
1289 strands.put(strand.color, strand);
1290 }
1291 return strand;
1292 }
1293
RoboErikc0f6efe2011-07-11 15:46:34 -07001294 /**
1295 * Sends an intent to launch the top level Calendar view.
1296 *
1297 * @param context
1298 */
1299 public static void returnToCalendarHome(Context context) {
RoboErik3864be02011-07-25 15:56:50 -07001300 Intent launchIntent = new Intent(context, AllInOneActivity.class);
RoboErik4ba19df2011-09-22 11:31:21 -07001301 launchIntent.setAction(Intent.ACTION_DEFAULT);
RoboErik3864be02011-07-25 15:56:50 -07001302 launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
RoboErik4ba19df2011-09-22 11:31:21 -07001303 launchIntent.putExtra(INTENT_KEY_HOME, true);
RoboErikc0f6efe2011-07-11 15:46:34 -07001304 context.startActivity(launchIntent);
1305 }
RoboErik14e82b42011-07-19 09:46:39 -07001306
1307 /**
RoboErik50f10942011-07-26 14:30:25 -07001308 * This sets up a search view to use Calendar's search suggestions provider
1309 * and to allow refining the search.
Michael Chan5d894062011-08-12 17:07:49 -07001310 *
RoboErik50f10942011-07-26 14:30:25 -07001311 * @param view The {@link SearchView} to set up
1312 * @param act The activity using the view
1313 */
1314 public static void setUpSearchView(SearchView view, Activity act) {
1315 SearchManager searchManager = (SearchManager) act.getSystemService(Context.SEARCH_SERVICE);
1316 view.setSearchableInfo(searchManager.getSearchableInfo(act.getComponentName()));
1317 view.setQueryRefinementEnabled(true);
1318 }
1319
1320 /**
RoboErik14e82b42011-07-19 09:46:39 -07001321 * Given a context and a time in millis since unix epoch figures out the
1322 * correct week of the year for that time.
1323 *
1324 * @param millisSinceEpoch
1325 * @return
1326 */
1327 public static int getWeekNumberFromTime(long millisSinceEpoch, Context context) {
1328 Time weekTime = new Time(getTimeZone(context, null));
1329 weekTime.set(millisSinceEpoch);
1330 weekTime.normalize(true);
1331 int firstDayOfWeek = getFirstDayOfWeek(context);
1332 // if the date is on Saturday or Sunday and the start of the week
1333 // isn't Monday we may need to shift the date to be in the correct
1334 // week
1335 if (weekTime.weekDay == Time.SUNDAY
1336 && (firstDayOfWeek == Time.SUNDAY || firstDayOfWeek == Time.SATURDAY)) {
1337 weekTime.monthDay++;
1338 weekTime.normalize(true);
1339 } else if (weekTime.weekDay == Time.SATURDAY && firstDayOfWeek == Time.SATURDAY) {
1340 weekTime.monthDay += 2;
1341 weekTime.normalize(true);
1342 }
1343 return weekTime.getWeekNumber();
1344 }
RoboErik4eb34322011-08-19 15:47:51 -07001345
1346 /**
1347 * Formats a day of the week string. This is either just the name of the day
1348 * or a combination of yesterday/today/tomorrow and the day of the week.
1349 *
1350 * @param julianDay The julian day to get the string for
1351 * @param todayJulianDay The julian day for today's date
1352 * @param millis A utc millis since epoch time that falls on julian day
1353 * @param context The calling context, used to get the timezone and do the
1354 * formatting
1355 * @return
1356 */
1357 public static String getDayOfWeekString(int julianDay, int todayJulianDay, long millis,
1358 Context context) {
Michael Chan99704a22011-11-11 10:24:37 -08001359 getTimeZone(context, null);
RoboErik4eb34322011-08-19 15:47:51 -07001360 int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
1361 String dayViewText;
1362 if (julianDay == todayJulianDay) {
1363 dayViewText = context.getString(R.string.agenda_today,
1364 mTZUtils.formatDateRange(context, millis, millis, flags).toString());
1365 } else if (julianDay == todayJulianDay - 1) {
1366 dayViewText = context.getString(R.string.agenda_yesterday,
1367 mTZUtils.formatDateRange(context, millis, millis, flags).toString());
1368 } else if (julianDay == todayJulianDay + 1) {
1369 dayViewText = context.getString(R.string.agenda_tomorrow,
1370 mTZUtils.formatDateRange(context, millis, millis, flags).toString());
1371 } else {
1372 dayViewText = mTZUtils.formatDateRange(context, millis, millis, flags).toString();
1373 }
1374 dayViewText = dayViewText.toUpperCase();
1375 return dayViewText;
1376 }
Isaac Katzenelson4bd4a5c2012-03-20 11:02:03 -07001377
1378 // Calculate the time until midnight + 1 second and set the handler to
1379 // do run the runnable
1380 public static void setMidnightUpdater(Handler h, Runnable r, String timezone) {
1381 if (h == null || r == null || timezone == null) {
1382 return;
1383 }
1384 long now = System.currentTimeMillis();
1385 Time time = new Time(timezone);
1386 time.set(now);
1387 long runInMillis = (24 * 3600 - time.hour * 3600 - time.minute * 60 -
1388 time.second + 1) * 1000;
1389 h.removeCallbacks(r);
1390 h.postDelayed(r, runInMillis);
1391 }
1392
1393 // Stop the midnight update thread
1394 public static void resetMidnightUpdater(Handler h, Runnable r) {
1395 if (h == null || r == null) {
1396 return;
1397 }
1398 h.removeCallbacks(r);
1399 }
Sara Ting75f53662012-04-09 15:37:10 -07001400
1401 /**
1402 * Returns a string description of the specified time interval.
1403 */
1404 public static String getDisplayedDatetime(long startMillis, long endMillis, long currentMillis,
Sara Ting23acd262012-04-20 13:27:39 -07001405 String localTimezone, boolean allDay, Context context) {
Sara Ting75f53662012-04-09 15:37:10 -07001406 // Configure date/time formatting.
1407 int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY;
1408 int flagsTime = DateUtils.FORMAT_SHOW_TIME;
1409 if (DateFormat.is24HourFormat(context)) {
1410 flagsTime |= DateUtils.FORMAT_24HOUR;
1411 }
1412
1413 Time currentTime = new Time(localTimezone);
1414 currentTime.set(currentMillis);
Sara Ting4e926272012-04-19 10:41:56 -07001415 Resources resources = context.getResources();
Sara Ting75f53662012-04-09 15:37:10 -07001416 String datetimeString = null;
1417 if (allDay) {
1418 // All day events require special timezone adjustment.
1419 long localStartMillis = convertAlldayUtcToLocal(null, startMillis, localTimezone);
1420 long localEndMillis = convertAlldayUtcToLocal(null, endMillis, localTimezone);
1421 if (singleDayEvent(localStartMillis, localEndMillis, currentTime.gmtoff)) {
1422 // If possible, use "Today" or "Tomorrow" instead of a full date string.
Sara Ting4e926272012-04-19 10:41:56 -07001423 int todayOrTomorrow = isTodayOrTomorrow(context.getResources(),
Sara Ting75f53662012-04-09 15:37:10 -07001424 localStartMillis, currentMillis, currentTime.gmtoff);
Sara Ting4e926272012-04-19 10:41:56 -07001425 if (TODAY == todayOrTomorrow) {
1426 datetimeString = resources.getString(R.string.today);
1427 } else if (TOMORROW == todayOrTomorrow) {
1428 datetimeString = resources.getString(R.string.tomorrow);
Sara Ting4e926272012-04-19 10:41:56 -07001429 }
Sara Ting75f53662012-04-09 15:37:10 -07001430 }
Sara Ting059117d2012-04-25 00:12:44 -07001431 if (datetimeString == null) {
1432 // For multi-day allday events or single-day all-day events that are not
1433 // today or tomorrow, use framework formatter.
1434 Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault());
1435 datetimeString = DateUtils.formatDateRange(context, f, startMillis,
1436 endMillis, flagsDate, Time.TIMEZONE_UTC).toString();
1437 }
Sara Ting75f53662012-04-09 15:37:10 -07001438 } else {
1439 if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) {
Sara Ting4e926272012-04-19 10:41:56 -07001440 // Format the time.
Sara Ting75f53662012-04-09 15:37:10 -07001441 String timeString = Utils.formatDateRange(context, startMillis, endMillis,
1442 flagsTime);
Sara Ting4e926272012-04-19 10:41:56 -07001443
1444 // If possible, use "Today" or "Tomorrow" instead of a full date string.
1445 int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), startMillis,
1446 currentMillis, currentTime.gmtoff);
1447 if (TODAY == todayOrTomorrow) {
1448 // Example: "Today at 1:00pm - 2:00 pm"
1449 datetimeString = resources.getString(R.string.today_at_time_fmt,
1450 timeString);
1451 } else if (TOMORROW == todayOrTomorrow) {
1452 // Example: "Tomorrow at 1:00pm - 2:00 pm"
1453 datetimeString = resources.getString(R.string.tomorrow_at_time_fmt,
1454 timeString);
1455 } else {
1456 // Format the full date. Example: "Thursday, April 12, 1:00pm - 2:00pm"
1457 String dateString = Utils.formatDateRange(context, startMillis, endMillis,
1458 flagsDate);
1459 datetimeString = resources.getString(R.string.date_time_fmt, dateString,
1460 timeString);
1461 }
Sara Ting75f53662012-04-09 15:37:10 -07001462 } else {
1463 // For multiday events, shorten day/month names.
1464 // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm"
1465 int flagsDatetime = flagsDate | flagsTime | DateUtils.FORMAT_ABBREV_MONTH |
1466 DateUtils.FORMAT_ABBREV_WEEKDAY;
1467 datetimeString = Utils.formatDateRange(context, startMillis, endMillis,
1468 flagsDatetime);
1469 }
Sara Ting75f53662012-04-09 15:37:10 -07001470 }
1471 return datetimeString;
1472 }
1473
1474 /**
Sara Ting23acd262012-04-20 13:27:39 -07001475 * Returns the timezone to display in the event info, if the local timezone is different
1476 * from the event timezone. Otherwise returns null.
1477 */
1478 public static String getDisplayedTimezone(long startMillis, String localTimezone,
1479 String eventTimezone) {
1480 String tzDisplay = null;
1481 if (!TextUtils.equals(localTimezone, eventTimezone)) {
1482 // Figure out if this is in DST
1483 TimeZone tz = TimeZone.getTimeZone(localTimezone);
1484 if (tz == null || tz.getID().equals("GMT")) {
1485 tzDisplay = localTimezone;
1486 } else {
1487 Time startTime = new Time(localTimezone);
1488 startTime.set(startMillis);
1489 tzDisplay = tz.getDisplayName(startTime.isDst != 0, TimeZone.SHORT);
1490 }
1491 }
1492 return tzDisplay;
1493 }
1494
1495 /**
Sara Ting75f53662012-04-09 15:37:10 -07001496 * Returns whether the specified time interval is in a single day.
1497 */
1498 private static boolean singleDayEvent(long startMillis, long endMillis, long localGmtOffset) {
1499 if (startMillis == endMillis) {
1500 return true;
1501 }
1502
1503 // An event ending at midnight should still be a single-day event, so check
1504 // time end-1.
1505 int startDay = Time.getJulianDay(startMillis, localGmtOffset);
1506 int endDay = Time.getJulianDay(endMillis - 1, localGmtOffset);
1507 return startDay == endDay;
1508 }
1509
Sara Ting4e926272012-04-19 10:41:56 -07001510 // Using int constants as a return value instead of an enum to minimize resources.
1511 private static final int TODAY = 1;
1512 private static final int TOMORROW = 2;
1513 private static final int NONE = 0;
1514
Sara Ting75f53662012-04-09 15:37:10 -07001515 /**
Sara Ting4e926272012-04-19 10:41:56 -07001516 * Returns TODAY or TOMORROW if applicable. Otherwise returns NONE.
Sara Ting75f53662012-04-09 15:37:10 -07001517 */
Sara Ting4e926272012-04-19 10:41:56 -07001518 private static int isTodayOrTomorrow(Resources r, long dayMillis,
Sara Ting75f53662012-04-09 15:37:10 -07001519 long currentMillis, long localGmtOffset) {
1520 int startDay = Time.getJulianDay(dayMillis, localGmtOffset);
1521 int currentDay = Time.getJulianDay(currentMillis, localGmtOffset);
1522
1523 int days = startDay - currentDay;
1524 if (days == 1) {
Sara Ting4e926272012-04-19 10:41:56 -07001525 return TOMORROW;
Sara Ting75f53662012-04-09 15:37:10 -07001526 } else if (days == 0) {
Sara Ting4e926272012-04-19 10:41:56 -07001527 return TODAY;
Sara Ting75f53662012-04-09 15:37:10 -07001528 } else {
Sara Ting4e926272012-04-19 10:41:56 -07001529 return NONE;
Sara Ting75f53662012-04-09 15:37:10 -07001530 }
1531 }
Sara Tingd9d123d2012-04-23 15:50:46 -07001532
1533 /**
1534 * Create an intent for emailing attendees of an event.
1535 *
1536 * @param resources The resources for translating strings.
1537 * @param eventTitle The title of the event to use as the email subject.
Michael Chane98dca72012-06-16 08:22:47 -07001538 * @param body The default text for the email body.
Sara Tingd9d123d2012-04-23 15:50:46 -07001539 * @param toEmails The list of emails for the 'to' line.
1540 * @param ccEmails The list of emails for the 'cc' line.
1541 * @param ownerAccount The owner account to use as the email sender.
1542 */
1543 public static Intent createEmailAttendeesIntent(Resources resources, String eventTitle,
Michael Chane98dca72012-06-16 08:22:47 -07001544 String body, List<String> toEmails, List<String> ccEmails, String ownerAccount) {
Sara Tingd9d123d2012-04-23 15:50:46 -07001545 List<String> toList = toEmails;
1546 List<String> ccList = ccEmails;
1547 if (toEmails.size() <= 0) {
1548 if (ccEmails.size() <= 0) {
1549 // TODO: Return a SEND intent if no one to email to, to at least populate
1550 // a draft email with the subject (and no recipients).
Michael Chane98dca72012-06-16 08:22:47 -07001551 throw new IllegalArgumentException("Both toEmails and ccEmails are empty.");
Sara Tingd9d123d2012-04-23 15:50:46 -07001552 }
1553
1554 // Email app does not work with no "to" recipient. Move all 'cc' to 'to'
1555 // in this case.
1556 toList = ccEmails;
1557 ccList = null;
1558 }
1559
1560 // Use the event title as the email subject (prepended with 'Re: ').
1561 String subject = null;
1562 if (eventTitle != null) {
1563 subject = resources.getString(R.string.email_subject_prefix) + eventTitle;
1564 }
1565
1566 // Use the SENDTO intent with a 'mailto' URI, because using SEND will cause
1567 // the picker to show apps like text messaging, which does not make sense
1568 // for email addresses. We put all data in the URI instead of using the extra
1569 // Intent fields (ie. EXTRA_CC, etc) because some email apps might not handle
1570 // those (though gmail does).
1571 Uri.Builder uriBuilder = new Uri.Builder();
1572 uriBuilder.scheme("mailto");
1573
1574 // We will append the first email to the 'mailto' field later (because the
1575 // current state of the Email app requires it). Add the remaining 'to' values
1576 // here. When the email codebase is updated, we can simplify this.
1577 if (toList.size() > 1) {
1578 for (int i = 1; i < toList.size(); i++) {
1579 // The Email app requires repeated parameter settings instead of
1580 // a single comma-separated list.
1581 uriBuilder.appendQueryParameter("to", toList.get(i));
1582 }
1583 }
1584
1585 // Add the subject parameter.
1586 if (subject != null) {
1587 uriBuilder.appendQueryParameter("subject", subject);
1588 }
1589
Michael Chane98dca72012-06-16 08:22:47 -07001590 // Add the subject parameter.
1591 if (body != null) {
1592 uriBuilder.appendQueryParameter("body", body);
1593 }
1594
Sara Tingd9d123d2012-04-23 15:50:46 -07001595 // Add the cc parameters.
1596 if (ccList != null && ccList.size() > 0) {
1597 for (String email : ccList) {
1598 uriBuilder.appendQueryParameter("cc", email);
1599 }
1600 }
1601
1602 // Insert the first email after 'mailto:' in the URI manually since Uri.Builder
1603 // doesn't seem to have a way to do this.
1604 String uri = uriBuilder.toString();
1605 if (uri.startsWith("mailto:")) {
1606 StringBuilder builder = new StringBuilder(uri);
1607 builder.insert(7, Uri.encode(toList.get(0)));
1608 uri = builder.toString();
1609 }
1610
1611 // Start the email intent. Email from the account of the calendar owner in case there
1612 // are multiple email accounts.
1613 Intent emailIntent = new Intent(android.content.Intent.ACTION_SENDTO, Uri.parse(uri));
1614 emailIntent.putExtra("fromAccountString", ownerAccount);
Sara Tinge6baa6a2012-10-05 13:24:42 -07001615
1616 // Workaround a Email bug that overwrites the body with this intent extra. If not
1617 // set, it clears the body.
1618 if (body != null) {
1619 emailIntent.putExtra(Intent.EXTRA_TEXT, body);
1620 }
1621
Sara Tingd9d123d2012-04-23 15:50:46 -07001622 return Intent.createChooser(emailIntent, resources.getString(R.string.email_picker_label));
1623 }
1624
1625 /**
Sara Tingddbc0022012-04-26 17:08:46 -07001626 * Example fake email addresses used as attendee emails are resources like conference rooms,
1627 * or another calendar, etc. These all end in "calendar.google.com".
Sara Tingd9d123d2012-04-23 15:50:46 -07001628 */
Sara Tingddbc0022012-04-26 17:08:46 -07001629 public static boolean isValidEmail(String email) {
1630 return email != null && !email.endsWith(MACHINE_GENERATED_ADDRESS);
Sara Tingd9d123d2012-04-23 15:50:46 -07001631 }
Isaac Katzenelsonc9993162012-05-08 19:15:12 -07001632
1633 /**
Sara Ting247a2f12012-05-14 01:07:39 -07001634 * Returns true if:
1635 * (1) the email is not a resource like a conference room or another calendar.
1636 * Catch most of these by filtering out suffix calendar.google.com.
1637 * (2) the email is not equal to the sync account to prevent mailing himself.
1638 */
1639 public static boolean isEmailableFrom(String email, String syncAccountName) {
1640 return Utils.isValidEmail(email) && !email.equals(syncAccountName);
1641 }
1642
1643 /**
Isaac Katzenelsonc9993162012-05-08 19:15:12 -07001644 * Inserts a drawable with today's day into the today's icon in the option menu
1645 * @param icon - today's icon from the options menu
1646 */
1647 public static void setTodayIcon(LayerDrawable icon, Context c, String timezone) {
1648 DayOfMonthDrawable today;
1649
1650 // Reuse current drawable if possible
1651 Drawable currentDrawable = icon.findDrawableByLayerId(R.id.today_icon_day);
1652 if (currentDrawable != null && currentDrawable instanceof DayOfMonthDrawable) {
1653 today = (DayOfMonthDrawable)currentDrawable;
1654 } else {
1655 today = new DayOfMonthDrawable(c);
1656 }
1657 // Set the day and update the icon
1658 Time now = new Time(timezone);
1659 now.setToNow();
1660 now.normalize(false);
1661 today.setDayOfMonth(now.monthDay);
1662 icon.mutate();
1663 icon.setDrawableByLayerId(R.id.today_icon_day, today);
1664 }
1665
1666 private static class CalendarBroadcastReceiver extends BroadcastReceiver {
1667
1668 Runnable mCallBack;
1669
1670 public CalendarBroadcastReceiver(Runnable callback) {
1671 super();
1672 mCallBack = callback;
1673 }
1674 @Override
1675 public void onReceive(Context context, Intent intent) {
1676 if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED) ||
1677 intent.getAction().equals(Intent.ACTION_TIME_CHANGED) ||
1678 intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED) ||
1679 intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
1680 if (mCallBack != null) {
1681 mCallBack.run();
1682 }
1683 }
1684 }
1685 }
1686
1687 public static BroadcastReceiver setTimeChangesReceiver(Context c, Runnable callback) {
1688 IntentFilter filter = new IntentFilter();
1689 filter.addAction(Intent.ACTION_TIME_CHANGED);
1690 filter.addAction(Intent.ACTION_DATE_CHANGED);
1691 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
1692 filter.addAction(Intent.ACTION_LOCALE_CHANGED);
1693
1694 CalendarBroadcastReceiver r = new CalendarBroadcastReceiver(callback);
1695 c.registerReceiver(r, filter);
1696 return r;
1697 }
1698
1699 public static void clearTimeChangesReceiver(Context c, BroadcastReceiver r) {
1700 c.unregisterReceiver(r);
1701 }
Michael Chane98dca72012-06-16 08:22:47 -07001702
1703 /**
1704 * Get a list of quick responses used for emailing guests from the
1705 * SharedPreferences. If not are found, get the hard coded ones that shipped
1706 * with the app
1707 *
1708 * @param context
1709 * @return a list of quick responses.
1710 */
1711 public static String[] getQuickResponses(Context context) {
1712 String[] s = Utils.getSharedPreference(context, KEY_QUICK_RESPONSES, (String[]) null);
1713
1714 if (s == null) {
1715 s = context.getResources().getStringArray(R.array.quick_response_defaults);
1716 }
1717
1718 return s;
1719 }
Sara Tingdacfb662012-08-21 10:33:22 -07001720
1721 /**
1722 * Return the app version code.
1723 */
1724 public static String getVersionCode(Context context) {
1725 if (sVersion == null) {
1726 try {
1727 sVersion = context.getPackageManager().getPackageInfo(
1728 context.getPackageName(), 0).versionName;
1729 } catch (PackageManager.NameNotFoundException e) {
1730 // Can't find version; just leave it blank.
1731 Log.e(TAG, "Error finding package " + context.getApplicationInfo().packageName);
1732 }
1733 }
1734 return sVersion;
1735 }
Sam Blitzsteinceae8db2012-11-01 17:29:35 -07001736
1737 /**
1738 * Checks the server for an updated list of Calendars (in the background).
1739 *
1740 * If a Calendar is added on the web (and it is selected and not
1741 * hidden) then it will be added to the list of calendars on the phone
1742 * (when this finishes). When a new calendar from the
1743 * web is added to the phone, then the events for that calendar are also
1744 * downloaded from the web.
1745 *
1746 * This sync is done automatically in the background when the
1747 * SelectCalendars activity and fragment are started.
1748 *
1749 * @param account - The account to sync. May be null to sync all accounts.
1750 */
1751 public static void startCalendarMetafeedSync(Account account) {
1752 Bundle extras = new Bundle();
1753 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
1754 extras.putBoolean("metafeedonly", true);
1755 ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
1756 }
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001757
1758 /**
1759 * Replaces stretches of text that look like addresses and phone numbers with clickable
1760 * links. If lastDitchGeo is true, then if no links are found in the textview, the entire
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001761 * string will be converted to a single geo link. Any spans that may have previously been
1762 * in the text will be cleared out.
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001763 * <p>
1764 * This is really just an enhanced version of Linkify.addLinks().
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001765 *
1766 * @param text - The string to search for links.
1767 * @param lastDitchGeo - If no links are found, turn the entire string into one geo link.
1768 * @return Spannable object containing the list of URL spans found.
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001769 */
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001770 public static Spannable extendedLinkify(String text, boolean lastDitchGeo) {
1771 // We use a copy of the string argument so it's available for later if necessary.
1772 Spannable spanText = SpannableString.valueOf(text);
1773
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001774 /*
1775 * If the text includes a street address like "1600 Amphitheater Parkway, 94043",
1776 * the current Linkify code will identify "94043" as a phone number and invite
1777 * you to dial it (and not provide a map link for the address). For outside US,
1778 * use Linkify result iff it spans the entire text. Otherwise send the user to maps.
1779 */
1780 String defaultPhoneRegion = System.getProperty("user.region", "US");
1781 if (!defaultPhoneRegion.equals("US")) {
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001782 Linkify.addLinks(spanText, Linkify.ALL);
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001783
1784 // If Linkify links the entire text, use that result.
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001785 URLSpan[] spans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1786 if (spans.length == 1) {
1787 int linkStart = spanText.getSpanStart(spans[0]);
1788 int linkEnd = spanText.getSpanEnd(spans[0]);
1789 if (linkStart <= indexFirstNonWhitespaceChar(spanText) &&
1790 linkEnd >= indexLastNonWhitespaceChar(spanText) + 1) {
1791 return spanText;
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001792 }
1793 }
1794
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001795 // Otherwise, to be cautious and to try to prevent false positives, reset the spannable.
1796 spanText = SpannableString.valueOf(text);
1797 // If lastDitchGeo is true, default the entire string to geo.
1798 if (lastDitchGeo && !text.isEmpty()) {
1799 Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q=");
Sam Blitzsteina92e7602012-11-19 11:36:47 -08001800 }
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001801 return spanText;
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001802 }
1803
1804 /*
1805 * For within US, we want to have better recognition of phone numbers without losing
1806 * any of the existing annotations. Ideally this would be addressed by improving Linkify.
1807 * For now we manage it as a second pass over the text.
1808 *
1809 * URIs and e-mail addresses are pretty easy to pick out of text. Phone numbers
1810 * are a bit tricky because they have radically different formats in different
1811 * countries, in terms of both the digits and the way in which they are commonly
1812 * written or presented (e.g. the punctuation and spaces in "(650) 555-1212").
1813 * The expected format of a street address is defined in WebView.findAddress(). It's
1814 * pretty narrowly defined, so it won't often match.
1815 *
1816 * The RFC 3966 specification defines the format of a "tel:" URI.
1817 *
1818 * Start by letting Linkify find anything that isn't a phone number. We have to let it
1819 * run first because every invocation removes all previous URLSpan annotations.
1820 *
1821 * Ideally we'd use the external/libphonenumber routines, but those aren't available
1822 * to unbundled applications.
1823 */
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001824 boolean linkifyFoundLinks = Linkify.addLinks(spanText,
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001825 Linkify.ALL & ~(Linkify.PHONE_NUMBERS));
1826
1827 /*
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001828 * Get a list of any spans created by Linkify, for the coordinate overlapping span check.
1829 */
1830 URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1831
1832 /*
1833 * Check for coordinates.
1834 * This must be done before phone numbers because longitude may look like a phone number.
1835 */
1836 Matcher coordMatcher = COORD_PATTERN.matcher(spanText);
1837 int coordCount = 0;
1838 while (coordMatcher.find()) {
1839 int start = coordMatcher.start();
1840 int end = coordMatcher.end();
1841 if (spanWillOverlap(spanText, existingSpans, start, end)) {
1842 continue;
1843 }
1844
1845 URLSpan span = new URLSpan("geo:0,0?q=" + coordMatcher.group());
1846 spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1847 coordCount++;
1848 }
1849
1850 /*
1851 * Update the list of existing spans, for the phone number overlapping span check.
1852 */
1853 existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class);
1854
1855 /*
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001856 * Search for phone numbers.
1857 *
1858 * Some URIs contain strings of digits that look like phone numbers. If both the URI
1859 * scanner and the phone number scanner find them, we want the URI link to win. Since
1860 * the URI scanner runs first, we just need to avoid creating overlapping spans.
1861 */
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001862 int[] phoneSequences = findNanpPhoneNumbers(text);
1863
1864 /*
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001865 * Insert spans for the numbers we found. We generate "tel:" URIs.
1866 */
1867 int phoneCount = 0;
1868 for (int match = 0; match < phoneSequences.length / 2; match++) {
1869 int start = phoneSequences[match*2];
1870 int end = phoneSequences[match*2 + 1];
1871
1872 if (spanWillOverlap(spanText, existingSpans, start, end)) {
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001873 continue;
1874 }
1875
1876 /*
1877 * The Linkify code takes the matching span and strips out everything that isn't a
1878 * digit or '+' sign. We do the same here. Extension numbers will get appended
1879 * without a separator, but the dialer wasn't doing anything useful with ";ext="
1880 * anyway.
1881 */
1882
1883 //String dialStr = phoneUtil.format(match.number(),
1884 // PhoneNumberUtil.PhoneNumberFormat.RFC3966);
1885 StringBuilder dialBuilder = new StringBuilder();
1886 for (int i = start; i < end; i++) {
1887 char ch = spanText.charAt(i);
1888 if (ch == '+' || Character.isDigit(ch)) {
1889 dialBuilder.append(ch);
1890 }
1891 }
1892 URLSpan span = new URLSpan("tel:" + dialBuilder.toString());
1893
1894 spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1895 phoneCount++;
1896 }
1897
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001898 /*
1899 * If lastDitchGeo, and no other links have been found, set the entire string as a geo link.
1900 */
1901 if (lastDitchGeo && !text.isEmpty() &&
1902 !linkifyFoundLinks && phoneCount == 0 && coordCount == 0) {
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001903 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1904 Log.v(TAG, "No linkification matches, using geo default");
1905 }
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001906 Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q=");
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001907 }
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08001908
1909 return spanText;
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08001910 }
1911
1912 private static int indexFirstNonWhitespaceChar(CharSequence str) {
1913 for (int i = 0; i < str.length(); i++) {
1914 if (!Character.isWhitespace(str.charAt(i))) {
1915 return i;
1916 }
1917 }
1918 return -1;
1919 }
1920
1921 private static int indexLastNonWhitespaceChar(CharSequence str) {
1922 for (int i = str.length() - 1; i >= 0; i--) {
1923 if (!Character.isWhitespace(str.charAt(i))) {
1924 return i;
1925 }
1926 }
1927 return -1;
1928 }
1929
1930 /**
1931 * Finds North American Numbering Plan (NANP) phone numbers in the input text.
1932 *
1933 * @param text The text to scan.
1934 * @return A list of [start, end) pairs indicating the positions of phone numbers in the input.
1935 */
1936 // @VisibleForTesting
1937 static int[] findNanpPhoneNumbers(CharSequence text) {
1938 ArrayList<Integer> list = new ArrayList<Integer>();
1939
1940 int startPos = 0;
1941 int endPos = text.length() - NANP_MIN_DIGITS + 1;
1942 if (endPos < 0) {
1943 return new int[] {};
1944 }
1945
1946 /*
1947 * We can't just strip the whitespace out and crunch it down, because the whitespace
1948 * is significant. March through, trying to figure out where numbers start and end.
1949 */
1950 while (startPos < endPos) {
1951 // skip whitespace
1952 while (Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
1953 startPos++;
1954 }
1955 if (startPos == endPos) {
1956 break;
1957 }
1958
1959 // check for a match at this position
1960 int matchEnd = findNanpMatchEnd(text, startPos);
1961 if (matchEnd > startPos) {
1962 list.add(startPos);
1963 list.add(matchEnd);
1964 startPos = matchEnd; // skip past match
1965 } else {
1966 // skip to next whitespace char
1967 while (!Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) {
1968 startPos++;
1969 }
1970 }
1971 }
1972
1973 int[] result = new int[list.size()];
1974 for (int i = list.size() - 1; i >= 0; i--) {
1975 result[i] = list.get(i);
1976 }
1977 return result;
1978 }
1979
1980 /**
1981 * Checks to see if there is a valid phone number in the input, starting at the specified
1982 * offset. If so, the index of the last character + 1 is returned. The input is assumed
1983 * to begin with a non-whitespace character.
1984 *
1985 * @return Exclusive end position, or -1 if not a match.
1986 */
1987 private static int findNanpMatchEnd(CharSequence text, int startPos) {
1988 /*
1989 * A few interesting cases:
1990 * 94043 # too short, ignore
1991 * 123456789012 # too long, ignore
1992 * +1 (650) 555-1212 # 11 digits, spaces
1993 * (650) 555 5555 # Second space, only when first is present.
1994 * (650) 555-1212, (650) 555-1213 # two numbers, return first
1995 * 1-650-555-1212 # 11 digits with leading '1'
1996 * *#650.555.1212#*! # 10 digits, include #*, ignore trailing '!'
1997 * 555.1212 # 7 digits
1998 *
1999 * For the most part we want to break on whitespace, but it's common to leave a space
2000 * between the initial '1' and/or after the area code.
2001 */
2002
2003 // Check for "tel:" URI prefix.
2004 if (text.length() > startPos+4
2005 && text.subSequence(startPos, startPos+4).toString().equalsIgnoreCase("tel:")) {
2006 startPos += 4;
2007 }
2008
2009 int endPos = text.length();
2010 int curPos = startPos;
2011 int foundDigits = 0;
2012 char firstDigit = 'x';
2013 boolean foundWhiteSpaceAfterAreaCode = false;
2014
2015 while (curPos <= endPos) {
2016 char ch;
2017 if (curPos < endPos) {
2018 ch = text.charAt(curPos);
2019 } else {
2020 ch = 27; // fake invalid symbol at end to trigger loop break
2021 }
2022
2023 if (Character.isDigit(ch)) {
2024 if (foundDigits == 0) {
2025 firstDigit = ch;
2026 }
2027 foundDigits++;
2028 if (foundDigits > NANP_MAX_DIGITS) {
2029 // too many digits, stop early
2030 return -1;
2031 }
2032 } else if (Character.isWhitespace(ch)) {
2033 if ( (firstDigit == '1' && foundDigits == 4) ||
2034 (foundDigits == 3)) {
2035 foundWhiteSpaceAfterAreaCode = true;
2036 } else if (firstDigit == '1' && foundDigits == 1) {
2037 } else if (foundWhiteSpaceAfterAreaCode
2038 && ( (firstDigit == '1' && (foundDigits == 7)) || (foundDigits == 6))) {
2039 } else {
2040 break;
2041 }
2042 } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) {
2043 break;
2044 }
2045 // else it's an allowed symbol
2046
2047 curPos++;
2048 }
2049
2050 if ((firstDigit != '1' && (foundDigits == 7 || foundDigits == 10)) ||
2051 (firstDigit == '1' && foundDigits == 11)) {
2052 // match
2053 return curPos;
2054 }
2055
2056 return -1;
2057 }
2058
2059 /**
2060 * Determines whether a new span at [start,end) will overlap with any existing span.
2061 */
2062 private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start,
2063 int end) {
2064 if (start == end) {
2065 // empty span, ignore
2066 return false;
2067 }
2068 for (URLSpan span : spanList) {
2069 int existingStart = spanText.getSpanStart(span);
2070 int existingEnd = spanText.getSpanEnd(span);
2071 if ((start >= existingStart && start < existingEnd) ||
2072 end > existingStart && end <= existingEnd) {
Sam Blitzstein29dc76a2012-11-19 10:46:54 -08002073 if (Log.isLoggable(TAG, Log.VERBOSE)) {
2074 CharSequence seq = spanText.subSequence(start, end);
2075 Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap");
2076 }
Sam Blitzstein7e19bf92012-11-13 10:02:41 -08002077 return true;
2078 }
2079 }
2080
2081 return false;
2082 }
2083
Sam Blitzstein94a1f1a2013-02-12 15:16:35 -08002084 /**
2085 * @param bundle The incoming bundle that contains the reminder info.
2086 * @return ArrayList<ReminderEntry> of the reminder minutes and methods.
2087 */
2088 public static ArrayList<ReminderEntry> readRemindersFromBundle(Bundle bundle) {
2089 ArrayList<ReminderEntry> reminders = null;
2090
2091 ArrayList<Integer> reminderMinutes = bundle.getIntegerArrayList(
2092 EventInfoFragment.BUNDLE_KEY_REMINDER_MINUTES);
2093 ArrayList<Integer> reminderMethods = bundle.getIntegerArrayList(
2094 EventInfoFragment.BUNDLE_KEY_REMINDER_METHODS);
2095 if (reminderMinutes == null || reminderMethods == null) {
2096 if (reminderMinutes != null || reminderMethods != null) {
2097 String nullList = (reminderMinutes == null?
2098 "reminderMinutes" : "reminderMethods");
2099 Log.d(TAG, String.format("Error resolving reminders: %s was null",
2100 nullList));
2101 }
2102 return null;
2103 }
2104
2105 int numReminders = reminderMinutes.size();
2106 if (numReminders == reminderMethods.size()) {
2107 // Only if the size of the reminder minutes we've read in is
2108 // the same as the size of the reminder methods. Otherwise,
2109 // something went wrong with bundling them.
2110 reminders = new ArrayList<ReminderEntry>(numReminders);
2111 for (int reminder_i = 0; reminder_i < numReminders;
2112 reminder_i++) {
2113 int minutes = reminderMinutes.get(reminder_i);
2114 int method = reminderMethods.get(reminder_i);
2115 reminders.add(ReminderEntry.valueOf(minutes, method));
2116 }
2117 } else {
2118 Log.d(TAG, String.format("Error resolving reminders." +
2119 " Found %d reminderMinutes, but %d reminderMethods.",
2120 numReminders, reminderMethods.size()));
2121 }
2122
2123 return reminders;
2124 }
2125
The Android Open Source Project146de362009-03-03 19:32:18 -08002126}