The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2006 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.calendar; |
| 18 | |
RoboErik | a27a886 | 2011-06-23 15:26:23 -0700 | [diff] [blame] | 19 | import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; |
Michael Chan | e8aa59d | 2009-09-15 14:44:43 -0700 | [diff] [blame] | 20 | |
Sam Blitzstein | ceae8db | 2012-11-01 17:29:35 -0700 | [diff] [blame] | 21 | import android.accounts.Account; |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 22 | import android.app.Activity; |
RoboErik | 50f1094 | 2011-07-26 14:30:25 -0700 | [diff] [blame] | 23 | import android.app.SearchManager; |
Isaac Katzenelson | c999316 | 2012-05-08 19:15:12 -0700 | [diff] [blame] | 24 | import android.content.BroadcastReceiver; |
Sam Blitzstein | f52c641 | 2013-12-04 12:59:36 -0800 | [diff] [blame] | 25 | import android.content.ComponentName; |
Sam Blitzstein | ceae8db | 2012-11-01 17:29:35 -0700 | [diff] [blame] | 26 | import android.content.ContentResolver; |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 27 | import android.content.Context; |
| 28 | import android.content.Intent; |
Isaac Katzenelson | c999316 | 2012-05-08 19:15:12 -0700 | [diff] [blame] | 29 | import android.content.IntentFilter; |
Michael Chan | e8aa59d | 2009-09-15 14:44:43 -0700 | [diff] [blame] | 30 | import android.content.SharedPreferences; |
Sara Ting | dacfb66 | 2012-08-21 10:33:22 -0700 | [diff] [blame] | 31 | import android.content.pm.PackageManager; |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 32 | import android.content.res.Resources; |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 33 | import android.database.Cursor; |
Erik | a144f86 | 2010-03-29 18:20:32 -0700 | [diff] [blame] | 34 | import android.database.MatrixCursor; |
RoboErik | bbb5b55 | 2011-07-19 10:18:59 -0700 | [diff] [blame] | 35 | import android.graphics.Color; |
Isaac Katzenelson | c999316 | 2012-05-08 19:15:12 -0700 | [diff] [blame] | 36 | import android.graphics.drawable.Drawable; |
| 37 | import android.graphics.drawable.LayerDrawable; |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 38 | import android.net.Uri; |
Sara Ting | fac2d15 | 2012-05-31 14:59:57 -0700 | [diff] [blame] | 39 | import android.os.Build; |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 40 | import android.os.Bundle; |
Isaac Katzenelson | 4bd4a5c | 2012-03-20 11:02:03 -0700 | [diff] [blame] | 41 | import android.os.Handler; |
Sam Blitzstein | ceae8db | 2012-11-01 17:29:35 -0700 | [diff] [blame] | 42 | import android.provider.CalendarContract.Calendars; |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 43 | import android.text.Spannable; |
| 44 | import android.text.SpannableString; |
| 45 | import android.text.Spanned; |
Erik | eb10fa8 | 2010-04-09 16:32:17 -0700 | [diff] [blame] | 46 | import android.text.TextUtils; |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 47 | import android.text.format.DateFormat; |
Daisuke Miyakawa | 2919097 | 2010-10-27 13:14:38 -0700 | [diff] [blame] | 48 | import android.text.format.DateUtils; |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 49 | import android.text.format.Time; |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 50 | import android.text.style.URLSpan; |
| 51 | import android.text.util.Linkify; |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 52 | import android.util.Log; |
RoboErik | 50f1094 | 2011-07-26 14:30:25 -0700 | [diff] [blame] | 53 | import android.widget.SearchView; |
| 54 | |
Isaac Katzenelson | c999316 | 2012-05-08 19:15:12 -0700 | [diff] [blame] | 55 | import com.android.calendar.CalendarController.ViewType; |
Sam Blitzstein | 94a1f1a | 2013-02-12 15:16:35 -0800 | [diff] [blame] | 56 | import com.android.calendar.CalendarEventModel.ReminderEntry; |
Andy McFadden | 636269c | 2011-06-09 13:15:55 -0700 | [diff] [blame] | 57 | import com.android.calendar.CalendarUtils.TimeZoneUtils; |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 58 | |
Isaac Katzenelson | 82400dd | 2011-04-15 11:13:49 -0700 | [diff] [blame] | 59 | import java.util.ArrayList; |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 60 | import java.util.Arrays; |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 61 | import java.util.Calendar; |
Erik | 1427657 | 2010-09-03 15:05:28 -0700 | [diff] [blame] | 62 | import java.util.Formatter; |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 63 | import java.util.HashMap; |
Isaac Katzenelson | 82400dd | 2011-04-15 11:13:49 -0700 | [diff] [blame] | 64 | import java.util.Iterator; |
Michael Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 65 | import java.util.LinkedHashSet; |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 66 | import java.util.LinkedList; |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 67 | import java.util.List; |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 68 | import java.util.Locale; |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 69 | import java.util.Map; |
Michael Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 70 | import java.util.Set; |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 71 | import java.util.TimeZone; |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 72 | import java.util.regex.Matcher; |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 73 | import java.util.regex.Pattern; |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 74 | |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 75 | public class Utils { |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 76 | private static final boolean DEBUG = false; |
Erik | 3dc5e90 | 2010-09-07 11:33:07 -0700 | [diff] [blame] | 77 | private static final String TAG = "CalUtils"; |
Sara Ting | ddbc002 | 2012-04-26 17:08:46 -0700 | [diff] [blame] | 78 | |
Michael Chan | bed0275 | 2010-04-27 10:56:53 -0700 | [diff] [blame] | 79 | // Set to 0 until we have UI to perform undo |
| 80 | public static final long UNDO_DELAY = 0; |
| 81 | |
Erik | 79f2281 | 2010-06-23 16:55:38 -0700 | [diff] [blame] | 82 | // 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 | |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 88 | // 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; |
Erik | 79f2281 | 2010-06-23 16:55:38 -0700 | [diff] [blame] | 96 | |
Michael Chan | 2aeb8d9 | 2011-07-10 13:32:09 -0700 | [diff] [blame] | 97 | public static final String OPEN_EMAIL_MARKER = " <"; |
| 98 | public static final String CLOSE_EMAIL_MARKER = ">"; |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 99 | |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 100 | 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"; |
RoboErik | 4ba19df | 2011-09-22 11:31:21 -0700 | [diff] [blame] | 103 | public static final String INTENT_KEY_HOME = "KEY_HOME"; |
Erik | 275232d | 2010-09-07 15:03:21 -0700 | [diff] [blame] | 104 | |
Erik | 981874e | 2010-10-05 16:52:52 -0700 | [diff] [blame] | 105 | public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; |
Isaac Katzenelson | e6109c5 | 2011-10-14 17:23:11 -0700 | [diff] [blame] | 106 | public static final int DECLINED_EVENT_ALPHA = 0x66; |
Isaac Katzenelson | 4ecf064 | 2011-10-18 12:56:18 -0700 | [diff] [blame] | 107 | public static final int DECLINED_EVENT_TEXT_ALPHA = 0xC0; |
Erik | 981874e | 2010-10-05 16:52:52 -0700 | [diff] [blame] | 108 | |
Michael Chan | f9411fe | 2012-02-10 17:05:52 -0800 | [diff] [blame] | 109 | private static final float SATURATION_ADJUST = 1.3f; |
| 110 | private static final float INTENSITY_ADJUST = 0.8f; |
RoboErik | 4acb2fd | 2011-07-18 15:39:49 -0700 | [diff] [blame] | 111 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 112 | // 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 Kung | 56f42bf | 2013-03-29 14:59:29 -0700 | [diff] [blame] | 123 | public static final int YEAR_MIN = 1970; |
James Kung | eb65d84 | 2013-04-08 16:05:01 -0700 | [diff] [blame] | 124 | public static final int YEAR_MAX = 2036; |
James Kung | 56f42bf | 2013-03-29 14:59:29 -0700 | [diff] [blame] | 125 | |
Erik | a48b9d4 | 2010-09-22 15:31:03 -0700 | [diff] [blame] | 126 | // 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 130 | static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; |
Erik | 35d1362 | 2010-09-08 11:21:40 -0700 | [diff] [blame] | 131 | |
Michael Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 132 | public static final String KEY_QUICK_RESPONSES = "preferences_quick_responses"; |
| 133 | |
Michael Chan | 0b674be | 2012-11-20 07:16:14 -0800 | [diff] [blame] | 134 | public static final String KEY_ALERTS_VIBRATE_WHEN = "preferences_alerts_vibrateWhen"; |
| 135 | |
RoboErik | 42dabd1 | 2011-07-12 18:01:03 -0700 | [diff] [blame] | 136 | public static final String APPWIDGET_DATA_TYPE = "vnd.android.data/update"; |
| 137 | |
Sara Ting | ddbc002 | 2012-04-26 17:08:46 -0700 | [diff] [blame] | 138 | static final String MACHINE_GENERATED_ADDRESS = "calendar.google.com"; |
| 139 | |
Erik | a48b9d4 | 2010-09-22 15:31:03 -0700 | [diff] [blame] | 140 | private static final TimeZoneUtils mTZUtils = new TimeZoneUtils(SHARED_PREFS_NAME); |
Michael Chan | b60218a | 2010-12-14 16:34:39 -0800 | [diff] [blame] | 141 | private static boolean mAllowWeekForDetailView = false; |
Erik | ca47867 | 2011-01-19 20:02:47 -0800 | [diff] [blame] | 142 | private static long mTardis = 0; |
Sara Ting | dacfb66 | 2012-08-21 10:33:22 -0700 | [diff] [blame] | 143 | private static String sVersion = null; |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 144 | |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 145 | private static final Pattern mWildcardPattern = Pattern.compile("^.*$"); |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 146 | |
| 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 199 | 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 Ting | fac2d15 | 2012-05-31 14:59:57 -0700 | [diff] [blame] | 204 | /** |
| 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 Katzenelson | e8305d6 | 2013-09-03 14:56:26 -0700 | [diff] [blame] | 211 | /** |
| 212 | * Returns whether the SDK is the KeyLimePie release or later. |
| 213 | */ |
| 214 | public static boolean isKeyLimePieOrLater() { |
Sam Blitzstein | 4e7b1b2 | 2013-10-09 10:19:37 -0700 | [diff] [blame] | 215 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; |
Isaac Katzenelson | e8305d6 | 2013-09-03 14:56:26 -0700 | [diff] [blame] | 216 | } |
| 217 | |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 218 | public static int getViewTypeFromIntentAndSharedPref(Activity activity) { |
Erik | dd95df5 | 2010-08-27 09:31:18 -0700 | [diff] [blame] | 219 | Intent intent = activity.getIntent(); |
| 220 | Bundle extras = intent.getExtras(); |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 221 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(activity); |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 222 | |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 223 | if (TextUtils.equals(intent.getAction(), Intent.ACTION_EDIT)) { |
Erik | dd95df5 | 2010-08-27 09:31:18 -0700 | [diff] [blame] | 224 | return ViewType.EDIT; |
| 225 | } |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 226 | 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 Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 229 | return prefs.getInt(GeneralPreferences.KEY_DETAILED_VIEW, |
| 230 | GeneralPreferences.DEFAULT_DETAILED_VIEW); |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 231 | } 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 |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 238 | return prefs.getInt( |
| 239 | GeneralPreferences.KEY_START_VIEW, GeneralPreferences.DEFAULT_START_VIEW); |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 240 | } |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 241 | |
Erik | 235d59c | 2010-09-02 16:29:59 -0700 | [diff] [blame] | 242 | /** |
RoboErik | 064beb9 | 2011-06-16 17:25:14 -0700 | [diff] [blame] | 243 | * 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 | /** |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 264 | * 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. |
Erik | 3dc5e90 | 2010-09-07 11:33:07 -0700 | [diff] [blame] | 268 | * |
| 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) { |
Erik | a48b9d4 | 2010-09-22 15:31:03 -0700 | [diff] [blame] | 273 | mTZUtils.setTimeZone(context, timeZone); |
Erik | 3dc5e90 | 2010-09-07 11:33:07 -0700 | [diff] [blame] | 274 | } |
| 275 | |
| 276 | /** |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 277 | * 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. |
Erik | 235d59c | 2010-09-02 16:29:59 -0700 | [diff] [blame] | 284 | * |
| 285 | * @param context The calling activity |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 286 | * @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 |
Erik | 235d59c | 2010-09-02 16:29:59 -0700 | [diff] [blame] | 290 | */ |
| 291 | public static String getTimeZone(Context context, Runnable callback) { |
Erik | a48b9d4 | 2010-09-22 15:31:03 -0700 | [diff] [blame] | 292 | return mTZUtils.getTimeZone(context, callback); |
Michael Chan | 45efa09 | 2010-02-03 17:44:37 -0800 | [diff] [blame] | 293 | } |
| 294 | |
Erik | 1427657 | 2010-09-03 15:05:28 -0700 | [diff] [blame] | 295 | /** |
| 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 Miyakawa | 2919097 | 2010-10-27 13:14:38 -0700 | [diff] [blame] | 301 | * @param flags a bit mask of options See {@link DateUtils#formatDateRange(Context, Formatter, |
| 302 | * long, long, int, String) formatDateRange} |
Erik | 1427657 | 2010-09-03 15:05:28 -0700 | [diff] [blame] | 303 | * @return a string containing the formatted date/time range. |
| 304 | */ |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 305 | public static String formatDateRange( |
| 306 | Context context, long startMillis, long endMillis, int flags) { |
Erik | a48b9d4 | 2010-09-22 15:31:03 -0700 | [diff] [blame] | 307 | return mTZUtils.formatDateRange(context, startMillis, endMillis, flags); |
| 308 | } |
| 309 | |
Michael Chan | 0b674be | 2012-11-20 07:16:14 -0800 | [diff] [blame] | 310 | 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 Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 330 | 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 | |
Erik | a48b9d4 | 2010-09-22 15:31:03 -0700 | [diff] [blame] | 340 | public static String getSharedPreference(Context context, String key, String defaultValue) { |
| 341 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
| 342 | return prefs.getString(key, defaultValue); |
Erik | 1427657 | 2010-09-03 15:05:28 -0700 | [diff] [blame] | 343 | } |
| 344 | |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 345 | public static int getSharedPreference(Context context, String key, int defaultValue) { |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 346 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
Michael Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 347 | return prefs.getInt(key, defaultValue); |
| 348 | } |
| 349 | |
Erik | ca47867 | 2011-01-19 20:02:47 -0800 | [diff] [blame] | 350 | 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 Tang | f4ad475 | 2010-08-23 17:54:08 -0700 | [diff] [blame] | 355 | /** |
| 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 | */ |
Erik | fbce65e | 2010-08-16 12:43:13 -0700 | [diff] [blame] | 362 | public static void setSharedPreference(Context context, String key, String value) { |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 363 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
Brad Fitzpatrick | 24fac46 | 2010-08-30 19:07:20 -0700 | [diff] [blame] | 364 | prefs.edit().putString(key, value).apply(); |
Michael Chan | 45efa09 | 2010-02-03 17:44:37 -0800 | [diff] [blame] | 365 | } |
| 366 | |
Michael Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 367 | public static void setSharedPreference(Context context, String key, String[] values) { |
| 368 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
| 369 | LinkedHashSet<String> set = new LinkedHashSet<String>(); |
Michael Chan | 6427090 | 2012-09-26 15:08:57 -0700 | [diff] [blame] | 370 | for (String value : values) { |
| 371 | set.add(value); |
Michael Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 372 | } |
| 373 | prefs.edit().putStringSet(key, set).apply(); |
| 374 | } |
| 375 | |
Erik | ca47867 | 2011-01-19 20:02:47 -0800 | [diff] [blame] | 376 | protected static void tardis() { |
| 377 | mTardis = System.currentTimeMillis(); |
| 378 | } |
| 379 | |
| 380 | protected static long getTardis() { |
| 381 | return mTardis; |
| 382 | } |
| 383 | |
Sara Ting | 3a07a68 | 2012-10-31 13:19:38 -0700 | [diff] [blame] | 384 | public static void setSharedPreference(Context context, String key, boolean value) { |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 385 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
Erik | 3dc5e90 | 2010-09-07 11:33:07 -0700 | [diff] [blame] | 386 | SharedPreferences.Editor editor = prefs.edit(); |
| 387 | editor.putBoolean(key, value); |
Erik | 275232d | 2010-09-07 15:03:21 -0700 | [diff] [blame] | 388 | editor.apply(); |
Erik | 3dc5e90 | 2010-09-07 11:33:07 -0700 | [diff] [blame] | 389 | } |
| 390 | |
Michael Chan | d885c1a | 2010-08-26 00:06:25 -0700 | [diff] [blame] | 391 | 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 Chan | 1851ecb | 2013-04-18 18:36:24 -0700 | [diff] [blame] | 398 | 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 Chan | d6734db | 2010-07-22 00:48:08 -0700 | [diff] [blame] | 439 | /** |
| 440 | * Save default agenda/day/week/month view for next time |
| 441 | * |
| 442 | * @param context |
| 443 | * @param viewId {@link CalendarController.ViewType} |
| 444 | */ |
Michael Chan | e8aa59d | 2009-09-15 14:44:43 -0700 | [diff] [blame] | 445 | static void setDefaultView(Context context, int viewId) { |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 446 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
Michael Chan | e8aa59d | 2009-09-15 14:44:43 -0700 | [diff] [blame] | 447 | SharedPreferences.Editor editor = prefs.edit(); |
Mason Tang | f4ad475 | 2010-08-23 17:54:08 -0700 | [diff] [blame] | 448 | |
Michael Chan | b60218a | 2010-12-14 16:34:39 -0800 | [diff] [blame] | 449 | 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 Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 459 | editor.putInt(GeneralPreferences.KEY_DETAILED_VIEW, viewId); |
Michael Chan | e8aa59d | 2009-09-15 14:44:43 -0700 | [diff] [blame] | 460 | } |
| 461 | |
| 462 | // Record the (new) start view |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 463 | editor.putInt(GeneralPreferences.KEY_START_VIEW, viewId); |
Brad Fitzpatrick | 24fac46 | 2010-08-30 19:07:20 -0700 | [diff] [blame] | 464 | editor.apply(); |
Michael Chan | e8aa59d | 2009-09-15 14:44:43 -0700 | [diff] [blame] | 465 | } |
| 466 | |
Erik | a144f86 | 2010-03-29 18:20:32 -0700 | [diff] [blame] | 467 | public static MatrixCursor matrixCursorFromCursor(Cursor cursor) { |
Michael Chan | 6427090 | 2012-09-26 15:08:57 -0700 | [diff] [blame] | 468 | if (cursor == null) { |
| 469 | return null; |
| 470 | } |
| 471 | |
Sara Ting | 85e3cef | 2012-04-29 22:41:56 -0700 | [diff] [blame] | 472 | String[] columnNames = cursor.getColumnNames(); |
| 473 | if (columnNames == null) { |
| 474 | columnNames = new String[] {}; |
| 475 | } |
| 476 | MatrixCursor newCursor = new MatrixCursor(columnNames); |
Erik | a144f86 | 2010-03-29 18:20:32 -0700 | [diff] [blame] | 477 | 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 | * |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 492 | * @return Returns true of the cursors contain the same data and are not |
| 493 | * null, false otherwise |
Erik | a144f86 | 2010-03-29 18:20:32 -0700 | [diff] [blame] | 494 | */ |
| 495 | public static boolean compareCursors(Cursor c1, Cursor c2) { |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 496 | if (c1 == null || c2 == null) { |
Erik | a144f86 | 2010-03-29 18:20:32 -0700 | [diff] [blame] | 497 | 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); |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 511 | while (c1.moveToNext() && c2.moveToNext()) { |
| 512 | for (int i = 0; i < numColumns; i++) { |
| 513 | if (!TextUtils.equals(c1.getString(i), c2.getString(i))) { |
Erik | a144f86 | 2010-03-29 18:20:32 -0700 | [diff] [blame] | 514 | return false; |
| 515 | } |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | return true; |
| 520 | } |
| 521 | |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 522 | /** |
| 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) { |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 527 | // If the time was specified, then use that. Otherwise, use the current |
| 528 | // time. |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 529 | Uri data = intent.getData(); |
RoboErik | a27a886 | 2011-06-23 15:26:23 -0700 | [diff] [blame] | 530 | long millis = intent.getLongExtra(EXTRA_EVENT_BEGIN_TIME, -1); |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 531 | if (millis == -1 && data != null && data.isHierarchical()) { |
| 532 | List<String> path = data.getPathSegments(); |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 533 | if (path.size() == 2 && path.get(0).equals("time")) { |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 534 | try { |
| 535 | millis = Long.valueOf(data.getLastPathSegment()); |
| 536 | } catch (NumberFormatException e) { |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 537 | Log.i("Calendar", "timeFromIntentInMillis: Data existed but no valid time " |
| 538 | + "found. Using current time."); |
Erik | 1ef7f3a | 2010-02-24 14:46:03 -0800 | [diff] [blame] | 539 | } |
| 540 | } |
| 541 | } |
Erik | 76727b7 | 2010-02-26 15:29:37 -0800 | [diff] [blame] | 542 | if (millis <= 0) { |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 543 | millis = System.currentTimeMillis(); |
| 544 | } |
| 545 | return millis; |
| 546 | } |
| 547 | |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 548 | /** |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 549 | * Formats the given Time object so that it gives the month and year (for |
| 550 | * example, "September 2007"). |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 551 | * |
| 552 | * @param time the time to format |
| 553 | * @return the string containing the weekday and the date |
| 554 | */ |
Michael Chan | ad36a3c | 2010-01-27 18:03:25 -0800 | [diff] [blame] | 555 | public static String formatMonthYear(Context context, Time time) { |
RoboErik | cfa204b | 2011-02-22 15:42:16 -0800 | [diff] [blame] | 556 | 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 Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 560 | } |
| 561 | |
The Android Open Source Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 562 | /** |
Mason Tang | 4c8871b | 2010-08-10 10:17:43 -0700 | [diff] [blame] | 563 | * 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 | /** |
Erik | 981874e | 2010-10-05 16:52:52 -0700 | [diff] [blame] | 585 | * 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. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 622 | * Get first day of week as android.text.format.Time constant. |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 623 | * |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 624 | * @return the first day of week in android.text.format.Time |
| 625 | */ |
Mason Tang | 8e3d430 | 2010-07-12 17:39:30 -0700 | [diff] [blame] | 626 | public static int getFirstDayOfWeek(Context context) { |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 627 | SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 628 | String pref = prefs.getString( |
| 629 | GeneralPreferences.KEY_WEEK_START_DAY, GeneralPreferences.WEEK_START_DEFAULT); |
Mason Tang | 8e3d430 | 2010-07-12 17:39:30 -0700 | [diff] [blame] | 630 | |
| 631 | int startDay; |
Daisuke Miyakawa | 4b441bd | 2010-09-16 14:55:36 -0700 | [diff] [blame] | 632 | if (GeneralPreferences.WEEK_START_DEFAULT.equals(pref)) { |
Mason Tang | 8e3d430 | 2010-07-12 17:39:30 -0700 | [diff] [blame] | 633 | startDay = Calendar.getInstance().getFirstDayOfWeek(); |
| 634 | } else { |
| 635 | startDay = Integer.parseInt(pref); |
| 636 | } |
| 637 | |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 638 | 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 Kung | 56f42bf | 2013-03-29 14:59:29 -0700 | [diff] [blame] | 648 | * 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 Miyakawa | d644b0d | 2010-10-21 15:45:12 -0700 | [diff] [blame] | 682 | * @return true when week number should be shown. |
Erik | 981874e | 2010-10-05 16:52:52 -0700 | [diff] [blame] | 683 | */ |
| 684 | public static boolean getShowWeekNumber(Context context) { |
Daisuke Miyakawa | d644b0d | 2010-10-21 15:45:12 -0700 | [diff] [blame] | 685 | final SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); |
Erik | 981874e | 2010-10-05 16:52:52 -0700 | [diff] [blame] | 686 | return prefs.getBoolean( |
| 687 | GeneralPreferences.KEY_SHOW_WEEK_NUM, GeneralPreferences.DEFAULT_SHOW_WEEK_NUM); |
| 688 | } |
| 689 | |
| 690 | /** |
Erik | 40bcd10 | 2010-11-16 15:46:40 -0800 | [diff] [blame] | 691 | * @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 | |
Erik | 91b01ed | 2010-11-22 17:46:15 -0800 | [diff] [blame] | 698 | 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 | |
Erik | 40bcd10 | 2010-11-16 15:46:40 -0800 | [diff] [blame] | 703 | /** |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 704 | * Determine whether the column position is Saturday or not. |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 705 | * |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 706 | * @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) |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 712 | || (firstDayOfWeek == Time.MONDAY && column == 5) |
| 713 | || (firstDayOfWeek == Time.SATURDAY && column == 0); |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 714 | } |
| 715 | |
| 716 | /** |
| 717 | * Determine whether the column position is Sunday or not. |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 718 | * |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 719 | * @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) |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 725 | || (firstDayOfWeek == Time.MONDAY && column == 6) |
| 726 | || (firstDayOfWeek == Time.SATURDAY && column == 1); |
Takaoka G. Tadashi | 56adc7b | 2010-01-22 19:16:43 +0900 | [diff] [blame] | 727 | } |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 728 | |
| 729 | /** |
RoboErik | 9da910f | 2011-03-15 11:26:15 -0700 | [diff] [blame] | 730 | * 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 Tang | 3ea333d | 2010-08-24 15:57:00 -0700 | [diff] [blame] | 732 | * |
| 733 | * @param recycle Time object to recycle, otherwise null. |
| 734 | * @param utcTime Time to convert, in UTC. |
RoboErik | 9da910f | 2011-03-15 11:26:15 -0700 | [diff] [blame] | 735 | * @param tz The time zone to convert this time to. |
Mason Tang | 3ea333d | 2010-08-24 15:57:00 -0700 | [diff] [blame] | 736 | */ |
RoboErik | 9da910f | 2011-03-15 11:26:15 -0700 | [diff] [blame] | 737 | public static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) { |
Mason Tang | 3ea333d | 2010-08-24 15:57:00 -0700 | [diff] [blame] | 738 | if (recycle == null) { |
| 739 | recycle = new Time(); |
| 740 | } |
| 741 | recycle.timezone = Time.TIMEZONE_UTC; |
| 742 | recycle.set(utcTime); |
RoboErik | 9da910f | 2011-03-15 11:26:15 -0700 | [diff] [blame] | 743 | 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 Tang | 3ea333d | 2010-08-24 15:57:00 -0700 | [diff] [blame] | 754 | return recycle.normalize(true); |
| 755 | } |
| 756 | |
| 757 | /** |
Isaac Katzenelson | c1fae4d | 2011-11-07 11:19:32 -0800 | [diff] [blame] | 758 | * 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 Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 778 | * Scan through a cursor of calendars and check if names are duplicated. |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 779 | * This travels a cursor containing calendar display names and fills in the |
| 780 | * provided map with whether or not each name is repeated. |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 781 | * |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 782 | * @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 | */ |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 786 | public static void checkForDuplicateNames( |
| 787 | Map<String, Boolean> isDuplicateName, Cursor cursor, int nameIndex) { |
Michael Chan | ff6be83 | 2010-03-11 17:52:48 -0800 | [diff] [blame] | 788 | 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 Tang | 9138ce8 | 2010-06-28 11:08:46 -0700 | [diff] [blame] | 798 | |
| 799 | /** |
| 800 | * Null-safe object comparison |
Erik | 7b92da2 | 2010-09-23 14:55:22 -0700 | [diff] [blame] | 801 | * |
Mason Tang | 9138ce8 | 2010-06-28 11:08:46 -0700 | [diff] [blame] | 802 | * @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 Chan | b60218a | 2010-12-14 16:34:39 -0800 | [diff] [blame] | 809 | |
Erik | 63cd053 | 2011-01-26 14:16:03 -0800 | [diff] [blame] | 810 | public static void setAllowWeekForDetailView(boolean allowWeekView) { |
Michael Chan | b60218a | 2010-12-14 16:34:39 -0800 | [diff] [blame] | 811 | mAllowWeekForDetailView = allowWeekView; |
| 812 | } |
Erik | 63cd053 | 2011-01-26 14:16:03 -0800 | [diff] [blame] | 813 | |
| 814 | public static boolean getAllowWeekForDetailView() { |
| 815 | return mAllowWeekForDetailView; |
| 816 | } |
Isaac Katzenelson | 0b1bd10 | 2011-04-07 14:26:29 -0700 | [diff] [blame] | 817 | |
Isaac Katzenelson | ff5c434 | 2011-04-14 16:53:24 -0700 | [diff] [blame] | 818 | public static boolean getConfigBool(Context c, int key) { |
| 819 | return c.getResources().getBoolean(key); |
| 820 | } |
Isaac Katzenelson | 82400dd | 2011-04-15 11:13:49 -0700 | [diff] [blame] | 821 | |
James Kung | ede0fb1 | 2013-02-19 15:52:40 -0800 | [diff] [blame] | 822 | /** |
| 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 | */ |
RoboErik | 4acb2fd | 2011-07-18 15:39:49 -0700 | [diff] [blame] | 829 | public static int getDisplayColorFromColor(int color) { |
Michael Chan | 537f43d | 2012-08-03 10:51:18 -0700 | [diff] [blame] | 830 | if (!isJellybeanOrLater()) { |
| 831 | return color; |
| 832 | } |
Michael Chan | f9411fe | 2012-02-10 17:05:52 -0800 | [diff] [blame] | 833 | |
RoboErik | 4acb2fd | 2011-07-18 15:39:49 -0700 | [diff] [blame] | 834 | float[] hsv = new float[3]; |
| 835 | Color.colorToHSV(color, hsv); |
Michael Chan | f9411fe | 2012-02-10 17:05:52 -0800 | [diff] [blame] | 836 | hsv[1] = Math.min(hsv[1] * SATURATION_ADJUST, 1.0f); |
| 837 | hsv[2] = hsv[2] * INTENSITY_ADJUST; |
RoboErik | 4acb2fd | 2011-07-18 15:39:49 -0700 | [diff] [blame] | 838 | return Color.HSVToColor(hsv); |
| 839 | } |
| 840 | |
RoboErik | 3c40e07 | 2011-09-14 11:25:20 -0700 | [diff] [blame] | 841 | // 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 Katzenelson | e6109c5 | 2011-10-14 17:23:11 -0700 | [diff] [blame] | 845 | int a = DECLINED_EVENT_ALPHA; |
RoboErik | 3c40e07 | 2011-09-14 11:25:20 -0700 | [diff] [blame] | 846 | 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 Blitzstein | f52c641 | 2013-12-04 12:59:36 -0800 | [diff] [blame] | 852 | 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 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 874 | // 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 Katzenelson | 82400dd | 2011-04-15 11:13:49 -0700 | [diff] [blame] | 885 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 886 | // 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 Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 893 | } |
| 894 | |
| 895 | /** |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 896 | * 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 Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 915 | * |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 916 | * @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 Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 923 | */ |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 924 | public static HashMap<Integer, DNAStrand> createDNAStrands(int firstJulianDay, |
| 925 | ArrayList<Event> events, int top, int bottom, int minPixels, int[] dayXs, |
| 926 | Context context) { |
Isaac Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 927 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 928 | 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 Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 939 | } |
| 940 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 941 | 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 Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 948 | } |
Isaac Katzenelson | 71b9ce3 | 2011-05-05 17:41:54 -0700 | [diff] [blame] | 949 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 950 | 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 Katzenelson | 71b9ce3 | 2011-05-05 17:41:54 -0700 | [diff] [blame] | 962 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 963 | // 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 Katzenelson | 71b9ce3 | 2011-05-05 17:41:54 -0700 | [diff] [blame] | 967 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 968 | 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 Katzenelson | 71b9ce3 | 2011-05-05 17:41:54 -0700 | [diff] [blame] | 1021 | } |
| 1022 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1023 | // This handles adding the first segment |
| 1024 | if (segments.size() == 0) { |
| 1025 | addNewSegment(segments, event, strands, firstJulianDay, 0, minMinutes); |
| 1026 | continue; |
Isaac Katzenelson | 71b9ce3 | 2011-05-05 17:41:54 -0700 | [diff] [blame] | 1027 | } |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1028 | // 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 Katzenelson | 71b9ce3 | 2011-05-05 17:41:54 -0700 | [diff] [blame] | 1136 | } |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1137 | weaveDNAStrands(segments, firstJulianDay, strands, top, bottom, dayXs); |
| 1138 | return strands; |
Isaac Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 1139 | } |
| 1140 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1141 | // 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 Katzenelson | c18dd7a | 2011-04-19 10:33:19 -0700 | [diff] [blame] | 1148 | } |
| 1149 | |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1150 | // 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 Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 1156 | } else { |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1157 | // else it's just the color of the event |
| 1158 | strand.allDays[i] = event.color; |
Isaac Katzenelson | 72a9459 | 2011-05-05 12:37:57 -0700 | [diff] [blame] | 1159 | } |
Isaac Katzenelson | 82400dd | 2011-04-15 11:13:49 -0700 | [diff] [blame] | 1160 | } |
Isaac Katzenelson | 82400dd | 2011-04-15 11:13:49 -0700 | [diff] [blame] | 1161 | } |
RoboErik | 092caec | 2011-06-23 10:26:12 -0700 | [diff] [blame] | 1162 | |
| 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 | |
RoboErik | c0f6efe | 2011-07-11 15:46:34 -0700 | [diff] [blame] | 1294 | /** |
| 1295 | * Sends an intent to launch the top level Calendar view. |
| 1296 | * |
| 1297 | * @param context |
| 1298 | */ |
| 1299 | public static void returnToCalendarHome(Context context) { |
RoboErik | 3864be0 | 2011-07-25 15:56:50 -0700 | [diff] [blame] | 1300 | Intent launchIntent = new Intent(context, AllInOneActivity.class); |
RoboErik | 4ba19df | 2011-09-22 11:31:21 -0700 | [diff] [blame] | 1301 | launchIntent.setAction(Intent.ACTION_DEFAULT); |
RoboErik | 3864be0 | 2011-07-25 15:56:50 -0700 | [diff] [blame] | 1302 | launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); |
RoboErik | 4ba19df | 2011-09-22 11:31:21 -0700 | [diff] [blame] | 1303 | launchIntent.putExtra(INTENT_KEY_HOME, true); |
RoboErik | c0f6efe | 2011-07-11 15:46:34 -0700 | [diff] [blame] | 1304 | context.startActivity(launchIntent); |
| 1305 | } |
RoboErik | 14e82b4 | 2011-07-19 09:46:39 -0700 | [diff] [blame] | 1306 | |
| 1307 | /** |
RoboErik | 50f1094 | 2011-07-26 14:30:25 -0700 | [diff] [blame] | 1308 | * This sets up a search view to use Calendar's search suggestions provider |
| 1309 | * and to allow refining the search. |
Michael Chan | 5d89406 | 2011-08-12 17:07:49 -0700 | [diff] [blame] | 1310 | * |
RoboErik | 50f1094 | 2011-07-26 14:30:25 -0700 | [diff] [blame] | 1311 | * @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 | /** |
RoboErik | 14e82b4 | 2011-07-19 09:46:39 -0700 | [diff] [blame] | 1321 | * 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 | } |
RoboErik | 4eb3432 | 2011-08-19 15:47:51 -0700 | [diff] [blame] | 1345 | |
| 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 Chan | 99704a2 | 2011-11-11 10:24:37 -0800 | [diff] [blame] | 1359 | getTimeZone(context, null); |
RoboErik | 4eb3432 | 2011-08-19 15:47:51 -0700 | [diff] [blame] | 1360 | 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 Katzenelson | 4bd4a5c | 2012-03-20 11:02:03 -0700 | [diff] [blame] | 1377 | |
| 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1400 | |
| 1401 | /** |
| 1402 | * Returns a string description of the specified time interval. |
| 1403 | */ |
| 1404 | public static String getDisplayedDatetime(long startMillis, long endMillis, long currentMillis, |
Sara Ting | 23acd26 | 2012-04-20 13:27:39 -0700 | [diff] [blame] | 1405 | String localTimezone, boolean allDay, Context context) { |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1406 | // 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 Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1415 | Resources resources = context.getResources(); |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1416 | 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 Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1423 | int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1424 | localStartMillis, currentMillis, currentTime.gmtoff); |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1425 | if (TODAY == todayOrTomorrow) { |
| 1426 | datetimeString = resources.getString(R.string.today); |
| 1427 | } else if (TOMORROW == todayOrTomorrow) { |
| 1428 | datetimeString = resources.getString(R.string.tomorrow); |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1429 | } |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1430 | } |
Sara Ting | 059117d | 2012-04-25 00:12:44 -0700 | [diff] [blame] | 1431 | 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1438 | } else { |
| 1439 | if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) { |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1440 | // Format the time. |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1441 | String timeString = Utils.formatDateRange(context, startMillis, endMillis, |
| 1442 | flagsTime); |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1443 | |
| 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1462 | } 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1470 | } |
| 1471 | return datetimeString; |
| 1472 | } |
| 1473 | |
| 1474 | /** |
Sara Ting | 23acd26 | 2012-04-20 13:27:39 -0700 | [diff] [blame] | 1475 | * 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1496 | * 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 Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1510 | // 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 Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1515 | /** |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1516 | * Returns TODAY or TOMORROW if applicable. Otherwise returns NONE. |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1517 | */ |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1518 | private static int isTodayOrTomorrow(Resources r, long dayMillis, |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1519 | 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 Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1525 | return TOMORROW; |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1526 | } else if (days == 0) { |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1527 | return TODAY; |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1528 | } else { |
Sara Ting | 4e92627 | 2012-04-19 10:41:56 -0700 | [diff] [blame] | 1529 | return NONE; |
Sara Ting | 75f5366 | 2012-04-09 15:37:10 -0700 | [diff] [blame] | 1530 | } |
| 1531 | } |
Sara Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1532 | |
| 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 Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 1538 | * @param body The default text for the email body. |
Sara Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1539 | * @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 Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 1544 | String body, List<String> toEmails, List<String> ccEmails, String ownerAccount) { |
Sara Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1545 | 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 Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 1551 | throw new IllegalArgumentException("Both toEmails and ccEmails are empty."); |
Sara Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1552 | } |
| 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 Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 1590 | // Add the subject parameter. |
| 1591 | if (body != null) { |
| 1592 | uriBuilder.appendQueryParameter("body", body); |
| 1593 | } |
| 1594 | |
Sara Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1595 | // 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 Ting | e6baa6a | 2012-10-05 13:24:42 -0700 | [diff] [blame] | 1615 | |
| 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 Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1622 | return Intent.createChooser(emailIntent, resources.getString(R.string.email_picker_label)); |
| 1623 | } |
| 1624 | |
| 1625 | /** |
Sara Ting | ddbc002 | 2012-04-26 17:08:46 -0700 | [diff] [blame] | 1626 | * 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 Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1628 | */ |
Sara Ting | ddbc002 | 2012-04-26 17:08:46 -0700 | [diff] [blame] | 1629 | public static boolean isValidEmail(String email) { |
| 1630 | return email != null && !email.endsWith(MACHINE_GENERATED_ADDRESS); |
Sara Ting | d9d123d | 2012-04-23 15:50:46 -0700 | [diff] [blame] | 1631 | } |
Isaac Katzenelson | c999316 | 2012-05-08 19:15:12 -0700 | [diff] [blame] | 1632 | |
| 1633 | /** |
Sara Ting | 247a2f1 | 2012-05-14 01:07:39 -0700 | [diff] [blame] | 1634 | * 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 Katzenelson | c999316 | 2012-05-08 19:15:12 -0700 | [diff] [blame] | 1644 | * 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 Chan | e98dca7 | 2012-06-16 08:22:47 -0700 | [diff] [blame] | 1702 | |
| 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 Ting | dacfb66 | 2012-08-21 10:33:22 -0700 | [diff] [blame] | 1720 | |
| 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 Blitzstein | ceae8db | 2012-11-01 17:29:35 -0700 | [diff] [blame] | 1736 | |
| 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1757 | |
| 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 Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1761 | * 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1763 | * <p> |
| 1764 | * This is really just an enhanced version of Linkify.addLinks(). |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1765 | * |
| 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1769 | */ |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1770 | 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1774 | /* |
| 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 Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1782 | Linkify.addLinks(spanText, Linkify.ALL); |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1783 | |
| 1784 | // If Linkify links the entire text, use that result. |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1785 | 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1792 | } |
| 1793 | } |
| 1794 | |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1795 | // 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 Blitzstein | a92e760 | 2012-11-19 11:36:47 -0800 | [diff] [blame] | 1800 | } |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1801 | return spanText; |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1802 | } |
| 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 Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1824 | boolean linkifyFoundLinks = Linkify.addLinks(spanText, |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1825 | Linkify.ALL & ~(Linkify.PHONE_NUMBERS)); |
| 1826 | |
| 1827 | /* |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1828 | * 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1856 | * 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1862 | int[] phoneSequences = findNanpPhoneNumbers(text); |
| 1863 | |
| 1864 | /* |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1865 | * 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1873 | 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 Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1898 | /* |
| 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1903 | if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| 1904 | Log.v(TAG, "No linkification matches, using geo default"); |
| 1905 | } |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1906 | Linkify.addLinks(spanText, mWildcardPattern, "geo:0,0?q="); |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1907 | } |
Sam Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 1908 | |
| 1909 | return spanText; |
Sam Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 1910 | } |
| 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 Blitzstein | 29dc76a | 2012-11-19 10:46:54 -0800 | [diff] [blame] | 2073 | 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 Blitzstein | 7e19bf9 | 2012-11-13 10:02:41 -0800 | [diff] [blame] | 2077 | return true; |
| 2078 | } |
| 2079 | } |
| 2080 | |
| 2081 | return false; |
| 2082 | } |
| 2083 | |
Sam Blitzstein | 94a1f1a | 2013-02-12 15:16:35 -0800 | [diff] [blame] | 2084 | /** |
| 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 Project | 146de36 | 2009-03-03 19:32:18 -0800 | [diff] [blame] | 2126 | } |