1/* 2** 3** Copyright 2009, The Android Open Source Project 4** 5** Licensed under the Apache License, Version 2.0 (the "License"); 6** you may not use this file except in compliance with the License. 7** You may obtain a copy of the License at 8** 9** http://www.apache.org/licenses/LICENSE-2.0 10** 11** Unless required by applicable law or agreed to in writing, software 12** distributed under the License is distributed on an "AS IS" BASIS, 13** See the License for the specific language governing permissions and 14** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15** limitations under the License. 16*/ 17 18package com.android.calendar; 19 20import android.app.Activity; 21import android.content.ActivityNotFoundException; 22import android.content.AsyncQueryHandler; 23import android.content.ContentResolver; 24import android.content.ContentUris; 25import android.content.ContentValues; 26import android.content.Intent; 27import android.database.Cursor; 28import android.net.Uri; 29import android.os.Bundle; 30import android.provider.CalendarContract; 31import android.provider.CalendarContract.Attendees; 32import android.provider.CalendarContract.Calendars; 33import android.provider.CalendarContract.Events; 34import android.text.TextUtils; 35import android.util.Base64; 36import android.util.Log; 37import android.widget.Toast; 38 39import com.android.calendarcommon2.DateException; 40import com.android.calendarcommon2.Duration; 41 42public class GoogleCalendarUriIntentFilter extends Activity { 43 private static final String TAG = "GoogleCalendarUriIntentFilter"; 44 static final boolean debug = false; 45 46 private static final int EVENT_INDEX_ID = 0; 47 private static final int EVENT_INDEX_START = 1; 48 private static final int EVENT_INDEX_END = 2; 49 private static final int EVENT_INDEX_DURATION = 3; 50 51 private static final String[] EVENT_PROJECTION = new String[] { 52 Events._ID, // 0 53 Events.DTSTART, // 1 54 Events.DTEND, // 2 55 Events.DURATION, // 3 56 }; 57 58 /** 59 * Extracts the ID and calendar email from the eid parameter of a URI. 60 * 61 * The URI contains an "eid" parameter, which is comprised of an ID, followed 62 * by a space, followed by the calendar email address. The domain is sometimes 63 * shortened. See the switch statement. This is Base64-encoded before being 64 * added to the URI. 65 * 66 * @param uri incoming request 67 * @return the decoded event ID and calendar email 68 */ 69 private String[] extractEidAndEmail(Uri uri) { 70 try { 71 String eidParam = uri.getQueryParameter("eid"); 72 if (debug) Log.d(TAG, "eid=" + eidParam ); 73 if (eidParam == null) { 74 return null; 75 } 76 77 byte[] decodedBytes = Base64.decode(eidParam, Base64.DEFAULT); 78 if (debug) Log.d(TAG, "decoded eid=" + new String(decodedBytes) ); 79 80 for (int spacePosn = 0; spacePosn < decodedBytes.length; spacePosn++) { 81 if (decodedBytes[spacePosn] == ' ') { 82 int emailLen = decodedBytes.length - spacePosn - 1; 83 if (spacePosn == 0 || emailLen < 3) { 84 break; 85 } 86 87 String domain = null; 88 if (decodedBytes[decodedBytes.length - 2] == '@') { 89 // Drop the special one character domain 90 emailLen--; 91 92 switch(decodedBytes[decodedBytes.length - 1]) { 93 case 'm': 94 domain = "gmail.com"; 95 break; 96 case 'g': 97 domain = "group.calendar.google.com"; 98 break; 99 case 'h': 100 domain = "holiday.calendar.google.com"; 101 break; 102 case 'i': 103 domain = "import.calendar.google.com"; 104 break; 105 case 'v': 106 domain = "group.v.calendar.google.com"; 107 break; 108 default: 109 Log.wtf(TAG, "Unexpected one letter domain: " 110 + decodedBytes[decodedBytes.length - 1]); 111 // Add sql wild card char to handle new cases 112 // that we don't know about. 113 domain = "%"; 114 break; 115 } 116 } 117 118 String eid = new String(decodedBytes, 0, spacePosn); 119 String email = new String(decodedBytes, spacePosn + 1, emailLen); 120 if (debug) Log.d(TAG, "eid= " + eid ); 121 if (debug) Log.d(TAG, "email= " + email ); 122 if (debug) Log.d(TAG, "domain=" + domain ); 123 if (domain != null) { 124 email += domain; 125 } 126 127 return new String[] { eid, email }; 128 } 129 } 130 } catch (RuntimeException e) { 131 Log.w(TAG, "Punting malformed URI " + uri); 132 } 133 return null; 134 } 135 136 @Override 137 protected void onCreate(Bundle icicle) { 138 super.onCreate(icicle); 139 140 Intent intent = getIntent(); 141 if (intent != null) { 142 Uri uri = intent.getData(); 143 if (uri != null) { 144 String[] eidParts = extractEidAndEmail(uri); 145 if (eidParts == null) { 146 Log.i(TAG, "Could not find event for uri: " +uri); 147 } else { 148 final String syncId = eidParts[0]; 149 final String ownerAccount = eidParts[1]; 150 if (debug) Log.d(TAG, "eidParts=" + syncId + "/" + ownerAccount); 151 final String selection = Events._SYNC_ID + " LIKE \"%" + syncId + "\" AND " 152 + Calendars.OWNER_ACCOUNT + " LIKE \"" + ownerAccount + "\""; 153 154 if (debug) Log.d(TAG, "selection: " + selection); 155 Cursor eventCursor = getContentResolver().query(Events.CONTENT_URI, 156 EVENT_PROJECTION, selection, null, 157 Calendars.CALENDAR_ACCESS_LEVEL + " desc"); 158 if (debug) Log.d(TAG, "Found: " + eventCursor.getCount()); 159 160 if (eventCursor == null || eventCursor.getCount() == 0) { 161 Log.i(TAG, "NOTE: found no matches on event with id='" + syncId + "'"); 162 return; 163 } 164 Log.i(TAG, "NOTE: found " + eventCursor.getCount() 165 + " matches on event with id='" + syncId + "'"); 166 // Don't print eidPart[1] as it contains the user's PII 167 168 try { 169 // Get info from Cursor 170 while (eventCursor.moveToNext()) { 171 int eventId = eventCursor.getInt(EVENT_INDEX_ID); 172 long startMillis = eventCursor.getLong(EVENT_INDEX_START); 173 long endMillis = eventCursor.getLong(EVENT_INDEX_END); 174 if (debug) Log.d(TAG, "_id: " + eventCursor.getLong(EVENT_INDEX_ID)); 175 if (debug) Log.d(TAG, "startMillis: " + startMillis); 176 if (debug) Log.d(TAG, "endMillis: " + endMillis); 177 178 if (endMillis == 0) { 179 String duration = eventCursor.getString(EVENT_INDEX_DURATION); 180 if (debug) Log.d(TAG, "duration: " + duration); 181 if (TextUtils.isEmpty(duration)) { 182 continue; 183 } 184 185 try { 186 Duration d = new Duration(); 187 d.parse(duration); 188 endMillis = startMillis + d.getMillis(); 189 if (debug) Log.d(TAG, "startMillis! " + startMillis); 190 if (debug) Log.d(TAG, "endMillis! " + endMillis); 191 if (endMillis < startMillis) { 192 continue; 193 } 194 } catch (DateException e) { 195 if (debug) Log.d(TAG, "duration:" + e.toString()); 196 continue; 197 } 198 } 199 200 // Pick up attendee status action from uri clicked 201 int attendeeStatus = Attendees.ATTENDEE_STATUS_NONE; 202 if ("RESPOND".equals(uri.getQueryParameter("action"))) { 203 try { 204 switch (Integer.parseInt(uri.getQueryParameter("rst"))) { 205 case 1: // Yes 206 attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED; 207 break; 208 case 2: // No 209 attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED; 210 break; 211 case 3: // Maybe 212 attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE; 213 break; 214 } 215 } catch (NumberFormatException e) { 216 // ignore this error as if the response code 217 // wasn't in the uri. 218 } 219 } 220 221 final Uri calendarUri = ContentUris.withAppendedId( 222 Events.CONTENT_URI, eventId); 223 intent = new Intent(Intent.ACTION_VIEW, calendarUri); 224 intent.setClass(this, EventInfoActivity.class); 225 intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis); 226 intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis); 227 if (attendeeStatus == Attendees.ATTENDEE_STATUS_NONE) { 228 startActivity(intent); 229 } else { 230 updateSelfAttendeeStatus( 231 eventId, ownerAccount, attendeeStatus, intent); 232 } 233 finish(); 234 return; 235 } 236 } finally { 237 eventCursor.close(); 238 } 239 } 240 } 241 242 // Can't handle the intent. Pass it on to the next Activity. 243 try { 244 startNextMatchingActivity(intent); 245 } catch (ActivityNotFoundException ex) { 246 // no browser installed? Just drop it. 247 } 248 } 249 finish(); 250 } 251 252 private void updateSelfAttendeeStatus( 253 int eventId, String ownerAccount, final int status, final Intent intent) { 254 final ContentResolver cr = getContentResolver(); 255 final AsyncQueryHandler queryHandler = 256 new AsyncQueryHandler(cr) { 257 @Override 258 protected void onUpdateComplete(int token, Object cookie, int result) { 259 if (result == 0) { 260 Log.w(TAG, "No rows updated - starting event viewer"); 261 intent.putExtra(Attendees.ATTENDEE_STATUS, status); 262 startActivity(intent); 263 return; 264 } 265 final int toastId; 266 switch (status) { 267 case Attendees.ATTENDEE_STATUS_ACCEPTED: 268 toastId = R.string.rsvp_accepted; 269 break; 270 case Attendees.ATTENDEE_STATUS_DECLINED: 271 toastId = R.string.rsvp_declined; 272 break; 273 case Attendees.ATTENDEE_STATUS_TENTATIVE: 274 toastId = R.string.rsvp_tentative; 275 break; 276 default: 277 return; 278 } 279 Toast.makeText(GoogleCalendarUriIntentFilter.this, 280 toastId, Toast.LENGTH_LONG).show(); 281 } 282 }; 283 final ContentValues values = new ContentValues(); 284 values.put(Attendees.ATTENDEE_STATUS, status); 285 queryHandler.startUpdate(0, null, 286 Attendees.CONTENT_URI, 287 values, 288 Attendees.ATTENDEE_EMAIL + "=? AND " + Attendees.EVENT_ID + "=?", 289 new String[]{ ownerAccount, String.valueOf(eventId) }); 290 } 291} 292