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 
     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