1 package com.android.exchange.service; 2 3 import android.content.ContentUris; 4 import android.content.ContentValues; 5 import android.content.Context; 6 import android.content.Entity; 7 import android.provider.CalendarContract.Attendees; 8 import android.provider.CalendarContract.Events; 9 10 import com.android.emailcommon.mail.Address; 11 import com.android.emailcommon.mail.MeetingInfo; 12 import com.android.emailcommon.mail.PackedString; 13 import com.android.emailcommon.provider.Account; 14 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 15 import com.android.emailcommon.provider.EmailContent.Message; 16 import com.android.emailcommon.provider.Mailbox; 17 import com.android.emailcommon.service.EmailServiceConstants; 18 import com.android.emailcommon.utility.Utility; 19 import com.android.exchange.Eas; 20 import com.android.exchange.EasResponse; 21 import com.android.exchange.adapter.MeetingResponseParser; 22 import com.android.exchange.adapter.Serializer; 23 import com.android.exchange.adapter.Tags; 24 import com.android.exchange.utility.CalendarUtilities; 25 import com.android.mail.providers.UIProvider; 26 import com.android.mail.utils.LogUtils; 27 28 import org.apache.http.HttpStatus; 29 30 import java.io.IOException; 31 import java.security.cert.CertificateException; 32 33 /** 34 * Responds to a meeting request, both notifying the EAS server and sending email. 35 */ 36 public class EasMeetingResponder extends EasServerConnection { 37 38 private static final String TAG = Eas.LOG_TAG; 39 40 /** Projection for getting the server id for a mailbox. */ 41 private static final String[] MAILBOX_SERVER_ID_PROJECTION = { MailboxColumns.SERVER_ID }; 42 private static final int MAILBOX_SERVER_ID_COLUMN = 0; 43 44 /** EAS protocol values for UserResponse. */ 45 private static final int EAS_RESPOND_ACCEPT = 1; 46 private static final int EAS_RESPOND_TENTATIVE = 2; 47 private static final int EAS_RESPOND_DECLINE = 3; 48 49 /** Value to use if we get a UI response value that we can't handle. */ 50 private static final int EAS_RESPOND_UNKNOWN = -1; 51 52 private EasMeetingResponder(final Context context, final Account account) { 53 super(context, account); 54 } 55 56 /** 57 * Translate from {@link UIProvider.MessageOperations} constants to EAS values. 58 * They're currently identical but this is for future-proofing. 59 * @param messageOperationResponse The response value that came from the UI. 60 * @return The EAS protocol value to use. 61 */ 62 private static int messageOperationResponseToUserResponse(final int messageOperationResponse) { 63 switch (messageOperationResponse) { 64 case UIProvider.MessageOperations.RESPOND_ACCEPT: 65 return EAS_RESPOND_ACCEPT; 66 case UIProvider.MessageOperations.RESPOND_TENTATIVE: 67 return EAS_RESPOND_TENTATIVE; 68 case UIProvider.MessageOperations.RESPOND_DECLINE: 69 return EAS_RESPOND_DECLINE; 70 } 71 return EAS_RESPOND_UNKNOWN; 72 } 73 74 /** 75 * Send the response to both the EAS server and as email (if appropriate). 76 * @param context Our {@link Context}. 77 * @param messageId The db id for the message containing the meeting request. 78 * @param response The UI's value for the user's response to the meeting. 79 */ 80 public static void sendMeetingResponse(final Context context, final long messageId, 81 final int response) { 82 final int easResponse = messageOperationResponseToUserResponse(response); 83 if (easResponse == EAS_RESPOND_UNKNOWN) { 84 LogUtils.e(TAG, "Bad response value: %d", response); 85 return; 86 } 87 final Message msg = Message.restoreMessageWithId(context, messageId); 88 if (msg == null) { 89 LogUtils.d(TAG, "Could not load message %d", messageId); 90 return; 91 } 92 final Account account = Account.restoreAccountWithId(context, msg.mAccountKey); 93 if (account == null) { 94 LogUtils.e(TAG, "Could not load account %d for message %d", msg.mAccountKey, msg.mId); 95 return; 96 } 97 final String mailboxServerId = Utility.getFirstRowString(context, 98 ContentUris.withAppendedId(Mailbox.CONTENT_URI, msg.mMailboxKey), 99 MAILBOX_SERVER_ID_PROJECTION, null, null, null, MAILBOX_SERVER_ID_COLUMN); 100 if (mailboxServerId == null) { 101 LogUtils.e(TAG, "Could not load mailbox %d for message %d", msg.mMailboxKey, msg.mId); 102 return; 103 } 104 105 final EasMeetingResponder responder = new EasMeetingResponder(context, account); 106 try { 107 responder.sendResponse(msg, mailboxServerId, easResponse); 108 } catch (final IOException e) { 109 LogUtils.e(TAG, "IOException: %s", e.getMessage()); 110 } catch (final CertificateException e) { 111 LogUtils.e(TAG, "CertificateException: %s", e.getMessage()); 112 } 113 } 114 115 /** 116 * Send an email response to a meeting invitation. 117 * @param meetingInfo The meeting info that was extracted from the invitation message. 118 * @param response The EAS value for the user's response to the meeting. 119 */ 120 private void sendMeetingResponseMail(final PackedString meetingInfo, final int response) { 121 // This will come as "First Last" <box (at) server.blah>, so we use Address to 122 // parse it into parts; we only need the email address part for the ics file 123 final Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL)); 124 // It shouldn't be possible, but handle it anyway 125 if (addrs.length != 1) return; 126 final String organizerEmail = addrs[0].getAddress(); 127 128 final String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP); 129 final String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART); 130 final String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND); 131 132 // What we're doing here is to create an Entity that looks like an Event as it would be 133 // stored by CalendarProvider 134 final ContentValues entityValues = new ContentValues(6); 135 final Entity entity = new Entity(entityValues); 136 137 // Fill in times, location, title, and organizer 138 entityValues.put("DTSTAMP", 139 CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp)); 140 entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart)); 141 entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd)); 142 entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION)); 143 entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE)); 144 entityValues.put(Events.ORGANIZER, organizerEmail); 145 146 // Add ourselves as an attendee, using our account email address 147 final ContentValues attendeeValues = new ContentValues(2); 148 attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, 149 Attendees.RELATIONSHIP_ATTENDEE); 150 attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress); 151 entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); 152 153 // Add the organizer 154 final ContentValues organizerValues = new ContentValues(2); 155 organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP, 156 Attendees.RELATIONSHIP_ORGANIZER); 157 organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail); 158 entity.addSubValue(Attendees.CONTENT_URI, organizerValues); 159 160 // Create a message from the Entity we've built. The message will have fields like 161 // to, subject, date, and text filled in. There will also be an "inline" attachment 162 // which is in iCalendar format 163 final int flag; 164 switch(response) { 165 case EmailServiceConstants.MEETING_REQUEST_ACCEPTED: 166 flag = Message.FLAG_OUTGOING_MEETING_ACCEPT; 167 break; 168 case EmailServiceConstants.MEETING_REQUEST_DECLINED: 169 flag = Message.FLAG_OUTGOING_MEETING_DECLINE; 170 break; 171 case EmailServiceConstants.MEETING_REQUEST_TENTATIVE: 172 default: 173 flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE; 174 break; 175 } 176 final Message outgoingMsg = 177 CalendarUtilities.createMessageForEntity(mContext, entity, flag, 178 meetingInfo.get(MeetingInfo.MEETING_UID), mAccount); 179 // Assuming we got a message back (we might not if the event has been deleted), send it 180 if (outgoingMsg != null) { 181 sendMessage(mAccount, outgoingMsg); 182 } 183 } 184 185 /** 186 * Send the response to the EAS server, and also via email if requested. 187 * @param msg The email message for the meeting invitation. 188 * @param mailboxServerId The server id for the mailbox that msg is in. 189 * @param response The EAS value for the user's response. 190 * @throws IOException 191 */ 192 private void sendResponse(final Message msg, final String mailboxServerId, final int response) 193 throws IOException, CertificateException { 194 final Serializer s = new Serializer(); 195 s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST); 196 s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(response)); 197 s.data(Tags.MREQ_COLLECTION_ID, mailboxServerId); 198 s.data(Tags.MREQ_REQ_ID, msg.mServerId); 199 s.end().end().done(); 200 final EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray()); 201 try { 202 final int status = resp.getStatus(); 203 if (status == HttpStatus.SC_OK) { 204 if (!resp.isEmpty()) { 205 // TODO: Improve the parsing to actually handle error statuses. 206 new MeetingResponseParser(resp.getInputStream()).parse(); 207 208 if (msg.mMeetingInfo != null) { 209 final PackedString meetingInfo = new PackedString(msg.mMeetingInfo); 210 final String responseRequested = 211 meetingInfo.get(MeetingInfo.MEETING_RESPONSE_REQUESTED); 212 // If there's no tag, or a non-zero tag, we send the response mail 213 if (!"0".equals(responseRequested)) { 214 sendMeetingResponseMail(meetingInfo, response); 215 } 216 } 217 } 218 } else if (resp.isAuthError()) { 219 // TODO: Handle this gracefully. 220 //throw new EasAuthenticationException(); 221 } else { 222 LogUtils.e(TAG, "Meeting response request failed, code: %d", status); 223 throw new IOException(); 224 } 225 } finally { 226 resp.close(); 227 } 228 } 229 } 230