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