Handle Exchange meeting invitation responses

* Includes some refactoring of internal "request" code in SyncManager
* Adds Message flags to tag meeting invites and cancellations
* Adds meetingResponse method in EmailService
* Hooks into Controller and MessageView UI included

Change-Id: I4c5e10bccc4b41956b94d9dfa55925e5af030939
This commit is contained in:
Marc Blank 2010-01-25 12:38:32 -08:00
parent b313b48644
commit 5de54008e5
16 changed files with 346 additions and 107 deletions

View File

@ -551,7 +551,7 @@ public class Controller {
/**
* Delete a single attachment entry from the DB given its id.
* Does not delete any eventual associated files.
* Does not delete any eventual associated files.
*/
public void deleteAttachment(long attachmentId) {
ContentResolver resolver = mProviderContext.getContentResolver();
@ -684,6 +684,29 @@ public class Controller {
}
}
/**
* Respond to a meeting invitation.
*
* @param messageId the id of the invitation being responded to
* @param response the code representing the response to the invitation
* @callback the Controller callback by which results will be reported (currently not defined)
*/
public void sendMeetingResponse(final long messageId, final int response,
final Result callback) {
// Split here for target type (Service or MessagingController)
IEmailService service = getServiceForMessage(messageId);
if (service != null) {
// Service implementation
try {
service.sendMeetingResponse(messageId, response);
} catch (RemoteException e) {
// TODO Change exception handling to be consistent with however this method
// is implemented for other protocols
Log.e("onDownloadAttachment", "RemoteException", e);
}
}
}
/**
* Request that an attachment be loaded. It will be stored at a location controlled
* by the AttachmentProvider.

View File

@ -31,6 +31,7 @@ import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.BodyColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.exchange.EmailServiceConstants;
import org.apache.commons.io.IOUtils;
@ -64,7 +65,6 @@ import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.webkit.WebViewClient;
@ -676,6 +676,16 @@ public class MessageView extends Activity implements OnClickListener {
return null;
}
// NOTE
// This is a placeholder for code used to accept a meeting invitation, and would presumably
// be called in response to a button press or menu selection
// The appropriate EmailServiceConstant would be changed to implement "decline" and
// "tentative" responses
private void onAccept() {
mController.sendMeetingResponse(mMessageId, EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
mControllerCallback);
}
private void onDownloadAttachment(AttachmentInfo attachment) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
/*

View File

@ -542,12 +542,18 @@ public abstract class EmailContent {
public static final int FLAG_LOADED_DELETED = 3;
// Bits used in mFlags
// These three states are mutually exclusive, and indicate whether the message is an
// The following three states are mutually exclusive, and indicate whether the message is an
// original, a reply, or a forward
public static final int FLAG_TYPE_ORIGINAL = 0;
public static final int FLAG_TYPE_REPLY = 1<<0;
public static final int FLAG_TYPE_FORWARD = 1<<1;
public static final int FLAG_TYPE_MASK = FLAG_TYPE_REPLY | FLAG_TYPE_FORWARD;
// The following flags indicate messages that are determined to be meeting related
// (e.g. invites)
public static final int FLAG_MEETING_INVITE = 1<<2;
public static final int FLAG_MEETING_CANCEL_NOTICE = 1<<3;
public static final int FLAG_MEETING_MASK =
FLAG_MEETING_INVITE | FLAG_MEETING_CANCEL_NOTICE;
public Message() {
mBaseUri = CONTENT_URI;

View File

@ -287,6 +287,18 @@ public class EmailServiceProxy implements IEmailService {
});
}
public void sendMeetingResponse(final long messageId, final int response) throws RemoteException {
setTask(new Runnable () {
public void run() {
try {
if (mCallback != null) mService.setCallback(mCallback);
mService.sendMeetingResponse(messageId, response);
} catch (RemoteException e) {
}
}
});
}
public void loadMore(long messageId) throws RemoteException {
// TODO Auto-generated method stub
}

View File

@ -76,8 +76,8 @@ public abstract class AbstractSyncService implements Runnable {
protected Object mSynchronizer = new Object();
protected volatile long mRequestTime = 0;
protected ArrayList<PartRequest> mPartRequests = new ArrayList<PartRequest>();
protected PartRequest mPendingPartRequest = null;
protected ArrayList<Request> mRequests = new ArrayList<Request>();
protected PartRequest mPendingRequest = null;
/**
* Sent by SyncManager to request that the service stop itself cleanly
@ -282,54 +282,23 @@ public abstract class AbstractSyncService implements Runnable {
}
/**
* PartRequest handling (common functionality)
* Can be overridden if desired, but IMAP/EAS both use the next three methods as-is
* Request handling (common functionality)
* Can be overridden if desired
*/
public void addPartRequest(PartRequest req) {
synchronized (mPartRequests) {
mPartRequests.add(req);
public void addRequest(Request req) {
synchronized (mRequests) {
mRequests.add(req);
mRequestTime = System.currentTimeMillis();
}
}
public void removePartRequest(PartRequest req) {
synchronized (mPartRequests) {
mPartRequests.remove(req);
public void removeRequest(Request req) {
synchronized (mRequests) {
mRequests.remove(req);
}
}
public PartRequest hasPartRequest(long emailId, String part) {
synchronized (mPartRequests) {
for (PartRequest pr : mPartRequests) {
if (pr.emailId == emailId && pr.loc.equals(part))
return pr;
}
}
return null;
}
// cancelPartRequest is sent in response to user input to stop an attachment load
// that is in progress. This will almost certainly require code overriding the base
// functionality, as sockets may need to be closed, etc. and this functionality will be
// service dependent. This returns the canceled PartRequest or null
public PartRequest cancelPartRequest(long emailId, String part) {
synchronized (mPartRequests) {
PartRequest p = null;
for (PartRequest pr : mPartRequests) {
if (pr.emailId == emailId && pr.loc.equals(part)) {
p = pr;
break;
}
}
if (p != null) {
mPartRequests.remove(p);
return p;
}
}
return null;
}
/**
* Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
* The arguments are exactly the same as to contentResolver.query(). Results are returned in

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange;
import java.io.IOException;
/**
* Use this to be able to distinguish login (authentication) failures from other I/O
* exceptions during a sync, as they are handled very differently.
*/
public class EasAuthenticationException extends IOException {
private static final long serialVersionUID = 1L;
EasAuthenticationException() {
super();
}
}

View File

@ -34,6 +34,7 @@ import com.android.exchange.adapter.AccountSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
import com.android.exchange.adapter.EmailSyncAdapter;
import com.android.exchange.adapter.FolderSyncParser;
import com.android.exchange.adapter.MeetingResponseParser;
import com.android.exchange.adapter.PingParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
@ -628,7 +629,7 @@ public class EasSyncService extends AbstractSyncService {
* @throws IOException
*/
protected void getAttachment(PartRequest req) throws IOException {
Attachment att = req.att;
Attachment att = req.mAttachment;
Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
doProgressCallback(msg.mId, att.mId, 0);
@ -640,9 +641,9 @@ public class EasSyncService extends AbstractSyncService {
HttpEntity e = res.getEntity();
int len = (int)e.getContentLength();
InputStream is = res.getEntity().getContent();
File f = (req.destination != null)
? new File(req.destination)
: createUniqueFileInternal(req.destination, att.mFileName);
File f = (req.mDestination != null)
? new File(req.mDestination)
: createUniqueFileInternal(req.mDestination, att.mFileName);
if (f != null) {
// Ensure that the target directory exists
File destDir = f.getParentFile();
@ -654,7 +655,7 @@ public class EasSyncService extends AbstractSyncService {
// len < 0 means "chunked" transfer-encoding
if (len != 0) {
try {
mPendingPartRequest = req;
mPendingRequest = req;
byte[] bytes = new byte[CHUNK_SIZE];
int length = len;
// Loop terminates 1) when EOF is reached or 2) if an IOException occurs
@ -689,7 +690,7 @@ public class EasSyncService extends AbstractSyncService {
}
}
} finally {
mPendingPartRequest = null;
mPendingRequest = null;
}
}
os.flush();
@ -697,8 +698,8 @@ public class EasSyncService extends AbstractSyncService {
// EmailProvider will throw an exception if we try to update an unsaved attachment
if (att.isSaved()) {
String contentUriString = (req.contentUriString != null)
? req.contentUriString
String contentUriString = (req.mContentUriString != null)
? req.mContentUriString
: "file://" + f.getAbsolutePath();
ContentValues cv = new ContentValues();
cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
@ -711,6 +712,37 @@ public class EasSyncService extends AbstractSyncService {
}
}
/**
* Responds to a meeting request. The MeetingResponseRequest is basically our
* wrapper for the meetingResponse service call
* @param req the request (message id and response code)
* @throws IOException
*/
protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
Serializer s = new Serializer();
s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
s.data(Tags.MREQ_COLLECTION_ID, Long.toString(msg.mMailboxKey));
s.data(Tags.MREQ_REQ_ID, msg.mServerId);
s.end().end().done();
HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
int status = res.getStatusLine().getStatusCode();
if (status == HttpStatus.SC_OK) {
HttpEntity e = res.getEntity();
int len = (int)e.getContentLength();
InputStream is = res.getEntity().getContent();
if (len != 0) {
new MeetingResponseParser(is, this).parse();
}
} else if (isAuthError(status)) {
throw new EasAuthenticationException();
} else {
userLog("Meeting response request failed, code: " + status);
throw new IOException();
}
}
@SuppressWarnings("deprecation")
private String makeUriString(String cmd, String extra) throws IOException {
// Cache the authentication string and the command string
@ -1323,18 +1355,29 @@ public class EasSyncService extends AbstractSyncService {
return;
}
// Now, handle various requests
while (true) {
PartRequest req = null;
synchronized (mPartRequests) {
if (mPartRequests.isEmpty()) {
Request req = null;
synchronized (mRequests) {
if (mRequests.isEmpty()) {
break;
} else {
req = mPartRequests.get(0);
req = mRequests.get(0);
}
}
getAttachment(req);
synchronized(mPartRequests) {
mPartRequests.remove(req);
// Our two request types are PartRequest (loading attachment) and
// MeetingResponseRequest (respond to a meeting request)
if (req instanceof PartRequest) {
getAttachment((PartRequest)req);
} else if (req instanceof MeetingResponseRequest) {
sendMeetingResponse((MeetingResponseRequest)req);
}
// If there's an exception handling the request, we'll throw it
// Otherwise, we remove the request
synchronized(mRequests) {
mRequests.remove(req);
}
}
@ -1465,6 +1508,9 @@ public class EasSyncService extends AbstractSyncService {
sync(target);
} while (mRequestTime != 0);
}
} catch (EasAuthenticationException e) {
userLog("Caught authentication error");
mExitStatus = EXIT_LOGIN_FAILURE;
} catch (IOException e) {
String message = e.getMessage();
userLog("Caught IOException: ", (message == null) ? "No message" : message);

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2010 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange;
public class EmailServiceConstants {
public static final int MEETING_REQUEST_ACCEPTED = 1;
public static final int MEETING_REQUEST_TENTATIVE = 2;
public static final int MEETING_REQUEST_DECLINED = 3;
}

View File

@ -43,4 +43,6 @@ interface IEmailService {
void hostChanged(long accountId);
Bundle autoDiscover(String userName, String password);
void sendMeetingResponse(long messageId, int response);
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange;
/**
* MeetingResponseRequest is the EAS wrapper for responding to meeting requests.
*/
public class MeetingResponseRequest extends Request {
public int mResponse;
MeetingResponseRequest(long messageId, int response) {
mMessageId = messageId;
mResponse = response;
}
}

View File

@ -24,24 +24,21 @@ import com.android.email.provider.EmailContent.Attachment;
* the attachment to be loaded, it also contains the callback to be used for status/progress
* updates to the UI.
*/
public class PartRequest {
public long timeStamp;
public long emailId;
public Attachment att;
public String destination;
public String contentUriString;
public String loc;
public class PartRequest extends Request {
public Attachment mAttachment;
public String mDestination;
public String mContentUriString;
public String mLocation;
public PartRequest(Attachment _att) {
timeStamp = System.currentTimeMillis();
emailId = _att.mMessageKey;
att = _att;
loc = att.mLocation;
mMessageId = _att.mMessageKey;
mAttachment = _att;
mLocation = mAttachment.mLocation;
}
public PartRequest(Attachment _att, String _destination, String _contentUriString) {
this(_att);
destination = _destination;
contentUriString = _contentUriString;
mDestination = _destination;
mContentUriString = _contentUriString;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange;
/**
* Requests for mailbox actions are handled by subclasses of this abstract class.
* Two subclasses are now defined: PartRequest (attachment load) and MeetingResponseRequest
* (respond to a meeting invitation)
*/
public abstract class Request {
public long mTimeStamp = System.currentTimeMillis();
public long mMessageId;
}

View File

@ -312,7 +312,7 @@ public class SyncManager extends Service implements Runnable {
public void loadAttachment(long attachmentId, String destinationFile,
String contentUriString) throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
partRequest(new PartRequest(att, destinationFile, contentUriString));
sendMessageRequest(new PartRequest(att, destinationFile, contentUriString));
}
public void updateFolderList(long accountId) throws RemoteException {
@ -348,8 +348,11 @@ public class SyncManager extends Service implements Runnable {
Eas.setUserDebug(on);
}
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
sendMessageRequest(new MeetingResponseRequest(messageId, response));
}
public void loadMore(long messageId) throws RemoteException {
// TODO Auto-generated method stub
}
// The following three methods are not implemented in this version
@ -1338,7 +1341,7 @@ public class SyncManager extends Service implements Runnable {
}
}
private void startService(Mailbox m, int reason, PartRequest req) {
private void startService(Mailbox m, int reason, Request req) {
// Don't sync if there's no connectivity
if (sConnectivityHold) return;
synchronized (sSyncToken) {
@ -1351,7 +1354,7 @@ public class SyncManager extends Service implements Runnable {
if (!((EasSyncService)service).mIsValid) return;
service.mSyncReason = reason;
if (req != null) {
service.addPartRequest(req);
service.addRequest(req);
}
startService(service, m);
}
@ -1726,9 +1729,9 @@ public class SyncManager extends Service implements Runnable {
}
}
static public void partRequest(PartRequest req) {
static public void sendMessageRequest(Request req) {
if (INSTANCE == null) return;
Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
Message msg = Message.restoreMessageWithId(INSTANCE, req.mMessageId);
if (msg == null) {
return;
}
@ -1739,33 +1742,7 @@ public class SyncManager extends Service implements Runnable {
service = startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
kick("part request");
} else {
service.addPartRequest(req);
}
}
static public PartRequest hasPartRequest(long emailId, String part) {
if (INSTANCE == null) return null;
Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
if (msg == null) {
return null;
}
long mailboxId = msg.mMailboxKey;
AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
if (service != null) {
return service.hasPartRequest(emailId, part);
}
return null;
}
static public void cancelPartRequest(long emailId, String part) {
Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
if (msg == null) {
return;
}
long mailboxId = msg.mMailboxKey;
AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
if (service != null) {
service.cancelPartRequest(emailId, part);
service.addRequest(req);
}
}
@ -1793,7 +1770,7 @@ public class SyncManager extends Service implements Runnable {
return PING_STATUS_OK;
}
static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
static public AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) return null;
synchronized (sSyncToken) {
if (INSTANCE.mServiceMap.get(mailboxId) == null) {
@ -1866,7 +1843,7 @@ public class SyncManager extends Service implements Runnable {
int exitStatus = svc.mExitStatus;
switch (exitStatus) {
case AbstractSyncService.EXIT_DONE:
if (!svc.mPartRequests.isEmpty()) {
if (!svc.mRequests.isEmpty()) {
// TODO Handle this case
}
errorMap.remove(mailboxId);

View File

@ -166,6 +166,14 @@ public class EmailSyncAdapter extends AbstractSyncAdapter {
String text = getValue();
msg.mText = text;
break;
case Tags.EMAIL_MESSAGE_CLASS:
String messageClass = getValue();
if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
msg.mFlags |= Message.FLAG_MEETING_INVITE;
} else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
msg.mFlags |= Message.FLAG_MEETING_CANCEL_NOTICE;
}
break;
default:
skipTag();
}

View File

@ -0,0 +1,65 @@
/* Copyright (C) 2010 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange.adapter;
import com.android.exchange.EasSyncService;
import java.io.IOException;
import java.io.InputStream;
/**
* Parse the result of a MeetingRequest command.
*/
public class MeetingResponseParser extends Parser {
private EasSyncService mService;
public MeetingResponseParser(InputStream in, EasSyncService service) throws IOException {
super(in);
mService = service;
}
public void parseResult() throws IOException {
while (nextTag(Tags.MREQ_RESULT) != END) {
if (tag == Tags.MREQ_STATUS) {
int status = getValueInt();
if (status != 1) {
mService.userLog("Error in meeting response: " + status);
}
} else if (tag == Tags.MREQ_CAL_ID) {
mService.userLog("Meeting response calendar id: " + getValue());
} else {
skipTag();
}
}
}
@Override
public boolean parse() throws IOException {
boolean res = false;
if (nextTag(START_DOCUMENT) != Tags.MREQ_MEETING_RESPONSE) {
throw new IOException();
}
while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
if (tag == Tags.MREQ_RESULT) {
parseResult();
} else {
skipTag();
}
}
return res;
}
}

View File

@ -39,6 +39,7 @@ public class Tags {
public static final int MOVE = 0x05;
public static final int GIE = 0x06;
public static final int FOLDER = 0x07;
public static final int MREQ = 0x08;
public static final int TASK = 0x09;
public static final int CONTACTS2 = 0x0C;
public static final int PING = 0x0D;
@ -218,6 +219,17 @@ public class Tags {
public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
public static final int MREQ_PAGE = MREQ << PAGE_SHIFT;
public static final int MREQ_CAL_ID = MREQ_PAGE + 5;
public static final int MREQ_COLLECTION_ID = MREQ_PAGE + 6;
public static final int MREQ_MEETING_RESPONSE = MREQ_PAGE + 7;
public static final int MREQ_REQ_ID = MREQ_PAGE + 8;
public static final int MREQ_REQUEST = MREQ_PAGE + 9;
public static final int MREQ_RESULT = MREQ_PAGE + 0xA;
public static final int MREQ_STATUS = MREQ_PAGE + 0xB;
public static final int MREQ_USER_RESPONSE = MREQ_PAGE + 0xC;
public static final int MREQ_VERSION = MREQ_PAGE + 0xD;
public static final int EMAIL_PAGE = EMAIL << PAGE_SHIFT;
public static final int EMAIL_ATTACHMENT = EMAIL_PAGE + 5;
public static final int EMAIL_ATTACHMENTS = EMAIL_PAGE + 6;
@ -441,6 +453,8 @@ public class Tags {
},
{
// 0x08 MeetingResponse
"CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
"Result", "Status", "UserResponse", "Version"
},
{
// 0x09 Tasks