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