Home | History | Annotate | Download | only in service
      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