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