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