Java tutorial
/* Copyright (c) 2015 Magnet Systems, Inc. * * 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.magnet.mmx.client.api; import android.os.Parcel; import android.os.Parcelable; import com.google.gson.reflect.TypeToken; import com.magnet.max.android.Attachment; import com.magnet.max.android.rest.marshalling.GsonDecorator; import com.magnet.max.android.rest.marshalling.Iso8601DateConverter; import com.magnet.max.android.util.EqualityUtil; import com.magnet.max.android.util.HashCodeBuilder; import com.magnet.max.android.util.MagnetUtils; import com.magnet.max.android.util.ParcelableHelper; import com.magnet.max.android.util.StringUtil; import com.magnet.mmx.client.ext.poll.MMXPoll; import com.magnet.mmx.client.ext.poll.MMXPollOption; import com.magnet.mmx.client.internal.channel.PubSubItem; import com.magnet.mmx.util.GsonData; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.magnet.max.android.ApiCallback; import com.magnet.max.android.User; import com.magnet.mmx.client.MMXClient; import com.magnet.mmx.client.MMXTask; import com.magnet.mmx.client.common.Log; import com.magnet.mmx.client.common.MMXException; import com.magnet.mmx.client.common.MMXPayload; import com.magnet.mmx.client.common.Options; import com.magnet.mmx.protocol.MMXTopic; import com.magnet.mmx.protocol.MMXid; import com.magnet.mmx.protocol.StatusCode; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * This class holds the message payload, and operations for the message. If * the message targets to the recipients, it will be used for ad hoc messaging. * If the message targets to a channel, it will be used for group chat or forum * discussions. */ public class MMXMessage implements Parcelable { private static final String TAG = MMXMessage.class.getSimpleName(); public static final String CONTENT_ATTACHMENTS = "_attachments"; public static final String TYPED_PAYLOAD_CONTENT_TYPE = "object/"; // Register built-in types static { registerPayloadType(MMXPoll.TYPE, MMXPoll.class); registerPayloadType(MMXPollOption.TYPE, MMXPollOption.class); registerPayloadType(MMXPoll.MMXPollIdentifier.TYPE, MMXPoll.MMXPollIdentifier.class); registerPayloadType(MMXPoll.MMXPollAnswer.TYPE, MMXPoll.MMXPollAnswer.class); } /** * Failure codes for the MMXMessage class. */ public static class FailureCode extends MMX.FailureCode { public static final FailureCode INVALID_RECIPIENT = new FailureCode(404, "INVALID_RECIPIENT"); public static final FailureCode CONTENT_TOO_LARGE = new FailureCode(413, "CONTENT_TOO_LARGE"); public static final FailureCode NO_RECEIPT_ID = new FailureCode(430, "NO_RECEIPT_ID"); public static final FailureCode CONTENT_EMPTY = new FailureCode(414, "CONTENT_IS_EMPTY"); FailureCode(int value, String description) { super(value, description); } FailureCode(MMX.FailureCode code) { super(code); } static FailureCode fromMMXFailureCode(MMX.FailureCode code, Throwable throwable) { if (throwable != null) { Log.d(TAG, "fromMMXFailureCode() ex=" + throwable.getClass().getName()); } else { Log.d(TAG, "fromMMXFailureCode() ex=null"); } if (throwable instanceof MMXException) { return new FailureCode(((MMXException) throwable).getCode(), throwable.getMessage()); } else { return new FailureCode(code); } } } /** * The OnFinishedListener for MMXMessage methods. * * * * @param <T> The type of the onSuccess result */ public static abstract class OnFinishedListener<T> implements IOnFinishedListener<T, FailureCode> { /** * Called when the operation completes successfully * * @param result the result of the operation */ @Override public abstract void onSuccess(T result); /** * Called if the operation fails * * @param code the failure code * @param throwable the throwable associated with this failure (may be null) */ @Override public abstract void onFailure(FailureCode code, Throwable throwable); } /** * The builder for the MMXMessage class */ public static final class Builder { private final MMXMessage mMessage; public Builder() { mMessage = new MMXMessage(); } /** * Set the message id of the MMXMessage object. * * @param id the message id * @return this Builder instance */ MMXMessage.Builder id(String id) { mMessage.id(id); return this; } /** * Set the message type for the MMXMessage object. * * @param type the message type * @return this Builder instance */ MMXMessage.Builder type(String type) { mMessage.type(type); return this; } /** * Set timestamp for the MMXMessage (sent time). * * @param timestamp the timestamp * @return this Builder instance */ MMXMessage.Builder timestamp(Date timestamp) { mMessage.timestamp(timestamp); return this; } /** * Set the sender for the MMXMessage. * * @param sender the sender * @return this Builder instance */ MMXMessage.Builder sender(User sender) { mMessage.sender(sender); return this; } /** * Set the channel for the MMXMessage * * @param channel the channel * @return this Builder instance */ public MMXMessage.Builder channel(MMXChannel channel) { if (mMessage.getRecipients().size() > 0) { throw new IllegalArgumentException("Cannot set both the recipients and channel in a message."); } mMessage.channel(channel); return this; } /** * Set the set of recipients for the MMXMssage * * @param recipients the recipients * @return this Builder instance */ public MMXMessage.Builder recipients(Set<User> recipients) { if (mMessage.getChannel() != null) { throw new IllegalArgumentException("Cannot set both the recipients and channel in a message."); } mMessage.recipients(recipients); return this; } /** * Sets the content for the MMXMessage * NOTE: The values in the map will be flattened to their toString() representations. * * @param content the content * @return this Builder instance */ @Deprecated public MMXMessage.Builder content(Map<String, String> content) { mMessage.content(content); return this; } //public MMXMessage.Builder contentType(String contentType) { // mMessage.mContentType = contentType; // return this; //} /** * Sets the meta data for this message * * @param content the content * @return this MMXMessage instance */ public MMXMessage.Builder metaData(Map<String, String> content) { mMessage.mMeta = content; return this; } /** * Sets the meta data for this message * * @param key the key of the mata data * @param value the value of the mata data * @return this MMXMessage instance */ public MMXMessage.Builder metaData(String key, String value) { mMessage.mMeta.put(key, value); return this; } public MMXMessage.Builder payload(MMXTypedPayload payload) { mMessage.payload(payload); return this; } /** * Sets the receiptId for this MMXMessage * * @param receiptId the receiptId * @return this Builder instance */ MMXMessage.Builder receiptId(String receiptId) { mMessage.mReceiptId = receiptId; return this; } /** * Adds attachments to the message * @param attachments * @return */ public MMXMessage.Builder attachments(Attachment... attachments) { if (null != attachments && attachments.length > 0) { for (Attachment attachment : attachments) { mMessage.mAttachments.add(attachment); } } return this; } /**package*/ MMXMessage.Builder pushConfigName(String pushConfigName) { mMessage.mPushConfigName = pushConfigName; return this; } /** * Validate and builds the MMXMessage * * @return the MMXMessage * @throws IllegalArgmentException */ public MMXMessage build() { //validate message //if (mMessage.mChannel == null && mMessage.mRecipients.size() == 0) { // throw new IllegalArgumentException("No channel and no recipients are specified"); //} if (mMessage.mChannel != null && mMessage.mRecipients.size() > 0) { throw new IllegalArgumentException("Only either channel or recipients should be specified"); } if (null == mMessage.mSender) { mMessage.mSender = User.getCurrentUser(); } return mMessage; } } /** * The exception contains a list of recipient user ID's that a message * cannot be sent to. */ public static class InvalidRecipientException extends MMXException { private final String mMsgId; private final Set<String> mUserIds = new HashSet<String>(); public InvalidRecipientException(String msg, String messageId) { super(msg, StatusCode.NOT_FOUND); mMsgId = messageId; } public String getMessageId() { return mMsgId; } private void addUserId(String userId) { mUserIds.add(userId); } public Set<String> getUserIds() { return mUserIds; } @Override public String toString() { return super.toString() + ", msgId=" + mMsgId + ", uids=" + mUserIds; } } private String mId; private String mType; private Date mTimestamp; private User mSender; private MMXChannel mChannel; private Set<User> mRecipients = new HashSet<User>(); private Map<String, String> mMeta = new HashMap<String, String>(); private String mContentType; private MMXTypedPayload mPayload; private String mReceiptId; private List<Attachment> mAttachments = new ArrayList<Attachment>(); private String mPushConfigName; // Map between type name to type class private static Map<String, Class> sTypeClassMapping; private static Map<Class, String> sClassTypeMapping; /** * Default constructor */ MMXMessage() { } public static void registerPayloadType(String name, Class type) { if (null == sTypeClassMapping) { sTypeClassMapping = new HashMap<>(); } if (null == sClassTypeMapping) { sClassTypeMapping = new HashMap<>(); } sTypeClassMapping.put(name, type); sClassTypeMapping.put(type, name); } public static Class getPayloadType(String name) { return sTypeClassMapping.get(name); } static String getPayloadTypeName(Class clazz) { return sClassTypeMapping.get(clazz); } static String getPayloadTypeName(String fullName) { if (StringUtil.isNotEmpty(fullName) && fullName.startsWith(TYPED_PAYLOAD_CONTENT_TYPE)) { return fullName.substring(TYPED_PAYLOAD_CONTENT_TYPE.length()); } return null; } /** * Set the message id of this MMXMessage object. * * @param id the message id * @return this MMXMessage object */ MMXMessage id(String id) { mId = id; return this; } /** * The message id for this MMXMessage * NOTE: This is for incoming messages only. * * @return the message id */ public String getId() { return mId; } /** * Set the message type for this MMXMessage object. * * @param type the type * @return this MMXMessage object */ MMXMessage type(String type) { mType = type; return this; } /** * The message type for this MMXMessage * * @return the message type */ String getType() { return mType; } /** * Set timestamp for this MMXMessage (sent time). * * @param timestamp the timestamp * @return this MMXMessage object */ MMXMessage timestamp(Date timestamp) { mTimestamp = timestamp; return this; } /** * The timestamp for this MMXMessage (sent time) * NOTE: This is for incoming messages only. * * @return the timestamp */ public Date getTimestamp() { return mTimestamp; } /** * Set the sender for this MMXMessage. * * @param sender the sender * @return this MMXMessage object */ MMXMessage sender(User sender) { mSender = sender; return this; } /** * The sender of this MMXMessage. * NOTE: This is for incoming messages only. * * @return the sender */ public User getSender() { return mSender; } /** * Set the channel for this message * * @param channel the channel * @return this MMXMessage object */ MMXMessage channel(MMXChannel channel) { mChannel = channel; return this; } /** * The channel for this message * * @return the channel */ public MMXChannel getChannel() { return mChannel; } /** * Set the set of recipients * * @param recipients the recipients * @return this MMXMessage object */ MMXMessage recipients(Set<User> recipients) { mRecipients = recipients; return this; } /** * The recipients for this message * * @return the recipients */ public Set<User> getRecipients() { return mRecipients; } public String getContentType() { return mContentType; } //public MMXMessage contentType(String contentType) { // this.mContentType = contentType; // return this; //} /** * Sets the meta data for this message * * @param content the content * @return this MMXMessage instance */ MMXMessage metaData(Map<String, String> content) { mMeta = content; return this; } /** * Sets the meta data for this message * * @param key the key of the mata data * @param value the value of the mata data * @return this MMXMessage instance */ MMXMessage metaData(String key, String value) { mMeta.put(key, value); return this; } /** * The meta data for this message * * @return the content */ @Deprecated public Map<String, String> getMetaData() { return mMeta; } /** * Sets the meta data for this message * * @param content the content * @return this MMXMessage instance */ @Deprecated MMXMessage content(Map<String, String> content) { mMeta = content; return this; } /** * The meta data for this message * * @return the content */ @Deprecated public Map<String, String> getContent() { return mMeta; } public MMXTypedPayload getPayload() { return mPayload; } public MMXMessage payload(MMXTypedPayload payload) { if (null != payload) { String typeName = getPayloadTypeName(payload.getClass()); if (null != typeName) { mContentType = TYPED_PAYLOAD_CONTENT_TYPE + typeName; } else { throw new IllegalArgumentException("Unknown payload type " + mPayload.getClass().getName() + ", please register it by calling MMXMessage.registerPayloadType"); } } this.mPayload = payload; return this; } /** * The attachments of this message * @return attachments */ public List<Attachment> getAttachments() { return mAttachments; } public MMXMessage attachments(List<Attachment> attachments) { this.mAttachments = attachments; return this; } /** * Sets the receiptId for this MMXMessage * * @param receiptId the receiptId * @return this MMXMessage instance */ MMXMessage receiptId(String receiptId) { mReceiptId = receiptId; return this; } /** * The receiptId for this message * * @return the receipt id */ String getReceiptId() { return mReceiptId; } // Publish this message to a channel. This code should belong to MMXChannel. String publish(final MMXChannel.OnFinishedListener<String> listener) { if (MMX.getCurrentUser() == null) { //FIXME: This needs to be done in MMXClient/MMXMessageManager. Do it here for now. final Throwable exception = new IllegalStateException( "Cannot send message. " + "There is no current user. Please login() first."); if (listener == null) { Log.w(TAG, "publish() failed", exception); } else { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onFailure( MMXChannel.FailureCode.fromMMXFailureCode(MMX.FailureCode.BAD_REQUEST, exception), exception); } }); } return null; } final String generatedMessageId = MMX.getMMXClient().generateMessageId(); final MMXPayload payload = createPayload(listener); if (null == payload) { return null; } for (Map.Entry<String, String> entry : mMeta.entrySet()) { payload.setMetaData(entry.getKey(), entry.getValue()); } if (null != mPushConfigName) { payload.setMmxMetaData("pushConfigName", mPushConfigName); } MMXTask<String> task = new MMXTask<String>(MMX.getMMXClient(), MMX.getHandler()) { @Override public String doRun(MMXClient mmxClient) throws Throwable { Throwable uploadError = uploadAttachments(payload, generatedMessageId, null); if (null != uploadError) { throw new IllegalStateException( "Failed to upload attachment for message " + generatedMessageId); } String publishedId = mmxClient.getPubSubManager().publish(generatedMessageId, mChannel.getMMXTopic(), payload); if (!generatedMessageId.equals(publishedId)) { throw new RuntimeException( "SDK Error: The returned published message id does not match the generated message id."); } return publishedId; } @Override public void onException(final Throwable exception) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onFailure(MMXChannel.FailureCode .fromMMXFailureCode(MMXChannel.FailureCode.DEVICE_ERROR, exception), exception); } }); } } @Override public void onResult(final String result) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onSuccess(result); } }); } } }; id(generatedMessageId); task.execute(); return generatedMessageId; } /** * Send the current message to server. If the message is addressed to * recipients, the {@link OnFinishedListener#onSuccess(Object)} will be called * with the message id for the message to all valid recipients. If there are * any invalid recipients in the message, a partial failure code * {@link FailureCode#INVALID_RECIPIENT} in * {@link OnFinishedListener#onFailure(FailureCode, Throwable)} will be * invoked. The message ID and a set of invalid recipients can be retrieved * from {@link User#getUsersByUserNames(List, ApiCallback)}. If this message is * addressed to a channel, the listener will be called with the id of the * published message. Common failure codes are * {@link FailureCode#CONTENT_TOO_LARGE}, {@link FailureCode#BAD_REQUEST}, or * FailureCode#DEVICE_ERROR. * * @param listener the listener for this method call */ public String send(final OnFinishedListener<String> listener) { final boolean isContentEmpty = (null == mMeta || mMeta.isEmpty()) && null == mPayload; if (MMX.getCurrentUser() == null || isContentEmpty) { //FIXME: This needs to be done in MMXClient/MMXMessageManager. Do it here for now. final Throwable exception = isContentEmpty ? new IllegalArgumentException("content is empty") : new IllegalStateException( "Cannot send message. " + "There is no current user. Please login() first."); Log.w(TAG, "send() failed", exception); if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onFailure( isContentEmpty ? FailureCode.CONTENT_EMPTY : FailureCode.fromMMXFailureCode(MMX.FailureCode.BAD_REQUEST, exception), exception); } }); } return null; } final String generatedMessageId = MMX.getMMXClient().generateMessageId(); final MMXPayload payload = createPayload(listener); if (null == payload) { return null; } MMXTask<String> task; if (mChannel != null) { task = new MMXTask<String>(MMX.getMMXClient(), MMX.getHandler()) { @Override public String doRun(MMXClient mmxClient) throws Throwable { Throwable uploadError = uploadAttachments(payload, generatedMessageId, null); if (null != uploadError) { throw new IllegalStateException( "Failed to upload attachment for message " + generatedMessageId); } String publishedId = mmxClient.getPubSubManager().publish(generatedMessageId, mChannel.getMMXTopic(), payload); if (!generatedMessageId.equals(publishedId)) { throw new RuntimeException( "SDK Error: The returned published message id does not match the generated message id."); } return publishedId; } @Override public void onException(final Throwable exception) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onFailure( FailureCode.fromMMXFailureCode(FailureCode.DEVICE_ERROR, exception), exception); } }); } } @Override public void onResult(final String result) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onSuccess(result); } }); } } }; } else { if (mRecipients.size() == 0) { throw new IllegalArgumentException("Recipients is not specified"); } task = new MMXTask<String>(MMX.getMMXClient(), MMX.getHandler()) { @Override public String doRun(MMXClient mmxClient) throws Throwable { Throwable uploadError = uploadAttachments(payload, generatedMessageId, null); if (null != uploadError) { throw new IllegalStateException( "Failed to upload attachment for message " + generatedMessageId); } MMXid[] recipientsArray = new MMXid[mRecipients.size()]; int index = 0; for (User recipient : mRecipients) { recipientsArray[index++] = new MMXid(recipient.getUserIdentifier(), null, recipient.getUserName()); } if (listener != null) { synchronized (sMessageSendListeners) { sMessageSendListeners.put(generatedMessageId, new MessageListenerPair(listener, MMXMessage.this)); } } String messageId = mmxClient.getMessageManager().sendPayload(generatedMessageId, recipientsArray, payload, new Options().enableReceipt(true)); if (!generatedMessageId.equals(messageId)) { throw new RuntimeException( "SDK Error: The returned message id does not match the generated message id"); } return messageId; } @Override public void onException(final Throwable exception) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onFailure( FailureCode.fromMMXFailureCode(FailureCode.DEVICE_ERROR, exception), exception); } }); } } @Override public void onResult(String result) { // No-op. Wait until a server ack or an error message is received. } }; } id(generatedMessageId); task.execute(); return generatedMessageId; } /** * Reply to the sender of the current message with the specified content * * @param replyContent the content to include in the reply * @param listener onSuccess will return the message id of the reply message * @return the message id */ public String reply(Map<String, String> replyContent, OnFinishedListener<String> listener) { if (mTimestamp == null) { throw new IllegalStateException("Cannot reply on an outgoing message."); } MMXMessage reply = buildReply(replyContent, false); return reply.send(listener); } /** * Reply to all of the recipients with the specified content * * @param replyContent the content to include in the reply * @param listener onSuccess will returh the message id of the reply message * @return the message id */ public String replyAll(Map<String, String> replyContent, OnFinishedListener<String> listener) { if (mTimestamp == null) { throw new IllegalStateException("Cannot reply on an outgoing message."); } MMXMessage reply = buildReply(replyContent, true); return reply.send(listener); } @Override public int hashCode() { return new HashCodeBuilder().hash(mId).hashCode(); } @Override public boolean equals(Object obj) { if (!EqualityUtil.quickCheck(this, obj)) { return false; } MMXMessage theOther = (MMXMessage) obj; return StringUtil.isStringValueEqual(mId, theOther.getId()) //StringUtil.isStringValueEqual(mType, theOther.getType()) && //(null != mChannel ? mChannel.equals(theOther.getChannel()) : null == theOther.getChannel()) && //(null != mSender ? mSender.equals(theOther.getSender()) : null == theOther.getSender()) //(null != mRecipients ? mRecipients.equals(theOther.getRecipients()) : null == theOther.getRecipients()) && //null != mMeta ? mMeta.equals(theOther.getContent()) : null == theOther.getContent() ; } @Override public String toString() { return new StringBuilder().append("{").append("id = ").append(mId).append(", ").append("type = ") .append(mType).append(", ").append("sender = ").append(mSender).append(", ").append("channel = ") .append(mChannel).append(", ").append("recipients = ").append(StringUtil.toString(mRecipients)) .append(", ").append("metaData = ").append(StringUtil.toString(mMeta)).append(", ") .append("payload = ").append(mPayload).append("}").toString(); } /** * Build a reply message. * * @param isReplyAll reply to other recipients in addition to the sender * @return a new MMXMessage instance for the reply */ private MMXMessage buildReply(Map<String, String> content, boolean isReplyAll) { User me = MMX.getCurrentUser(); HashSet<User> replyRecipients = new HashSet<User>(); replyRecipients.add(mSender); if (isReplyAll) { for (User recipient : mRecipients) { if (!recipient.getUserIdentifier().equals(me.getUserIdentifier())) { //remove myself from the recipients //this applies to instances of me (including other devices) replyRecipients.add(recipient); } } } return new MMXMessage().channel(mChannel).recipients(replyRecipients).metaData(content); } /** * Acknowledge this message. This will send a delivery receipt back to the * original sender. * * @param listener the listener for this call * @see OnFinishedListener */ public final void acknowledge(final OnFinishedListener<Void> listener) { if (mReceiptId == null) { //throw new IllegalArgumentException("Cannot acknowledge() this message: " + mId); if (null != listener) { listener.onFailure(FailureCode.NO_RECEIPT_ID, new IllegalArgumentException("Cannot acknowledge() this message: " + mId)); } return; } MMXTask<Void> task = new MMXTask<Void>(MMX.getMMXClient(), MMX.getHandler()) { @Override public Void doRun(MMXClient mmxClient) throws Throwable { MMX.getMMXClient().getMessageManager().sendReceipt(mReceiptId); return null; } @Override public void onException(final Throwable exception) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onFailure(FailureCode.fromMMXFailureCode(FailureCode.DEVICE_ERROR, exception), exception); } }); } } @Override public void onResult(Void result) { if (listener != null) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listener.onSuccess(null); } }); } } }; task.execute(); } private <T extends IOnFinishedListener> MMXPayload createPayload(T listener) { MMXPayload tmpPayload = null; if (null == mPayload) { final String type = getType() != null ? getType() : null; tmpPayload = new MMXPayload(type, ""); } else { if (null != mContentType) { tmpPayload = new MMXPayload(mContentType, GsonDecorator.getInstance().toJson(mPayload)); } else { String errorMessage = "Unknown payload type " + mPayload.getClass().getName() + ", please register it by calling MMXMessage.registerPayloadType"; if (null != listener) { listener.onFailure(new MMXChannel.FailureCode(MMX.FailureCode.ILLEGAL_ARGUMENT_CODE, "unkown payload type"), new Exception(errorMessage)); } Log.e(TAG, errorMessage); return null; } } if (null != tmpPayload) { for (Map.Entry<String, String> entry : mMeta.entrySet()) { tmpPayload.setMetaData(entry.getKey(), entry.getValue()); } } return tmpPayload; } /** * Convenience method to construct this object from a lower level MMXMessage object * * @param message the lower level MMXMessage object * @return a new object of this type */ static MMXMessage fromMMXMessage(MMXTopic topic, com.magnet.mmx.client.common.MMXMessage message) { UserCache userCache = UserCache.getInstance(); HashSet<String> usersToRetrieve = new HashSet<String>(); usersToRetrieve.add(message.getFrom().getUserId()); MMXid toUserId = message.getTo(); if (toUserId != null) { usersToRetrieve.add(message.getTo().getUserId()); } //identify all the users that need to be retrieved and populate the cache MMXid[] otherRecipients = message.getReplyAll(); if (otherRecipients != null) { //this is normal message. getReplyAll() returns null for pubsub messages for (MMXid mmxId : otherRecipients) { Log.d(TAG, "------otherRecipients : " + mmxId.getUserId()); usersToRetrieve.add(mmxId.getUserId()); } } //fill the cache userCache.fillCacheByUserId(usersToRetrieve, UserCache.DEFAULT_ACCEPTED_AGE); //five minutes old is ok HashSet<User> recipients = new HashSet<User>(); //populate the values User receiver; if (null == topic && toUserId != null) { receiver = userCache.getByUserId(message.getTo().getUserId()); if (receiver == null) { Log.e(TAG, "fromMMXMessage(): FAILURE: Unable to retrieve receiver from cache: " + "receiver=" + receiver + ". Message will be dropped."); return null; } Log.d(TAG, "------receiver : " + receiver.getUserIdentifier()); recipients.add(receiver); } User sender = userCache.getByUserId(message.getFrom().getUserId()); if (sender == null) { Log.e(TAG, "fromMMXMessage(): FAILURE: Unable to retrieve sender from cache: " + "sender=" + sender + ". Message will be dropped."); return null; } if (otherRecipients != null) { for (MMXid otherRecipient : otherRecipients) { recipients.add(userCache.getByUserId(otherRecipient.getUserId())); } } //populate the message content HashMap<String, String> metaData = new HashMap<String, String>(); for (Map.Entry<String, String> entry : message.getPayload().getAllMetaData().entrySet()) { if (!CONTENT_ATTACHMENTS.equals(entry.getKey())) { metaData.put(entry.getKey(), entry.getValue()); } } MMXMessage.Builder newMessage = new MMXMessage.Builder(); // Extract attachments String attachmentsStr = MagnetUtils .trimQuotes(message.getPayload().getAllMetaData().get(CONTENT_ATTACHMENTS)); if (StringUtil.isNotEmpty(attachmentsStr)) { List<Attachment> attachments = GsonData.getGson().fromJson(attachmentsStr, new TypeToken<List<Attachment>>() { }.getType()); if (null != attachments && attachments.size() > 0) { newMessage.attachments(attachments.toArray(new Attachment[0])); } } Log.d(TAG, "-----------message conversion, topic : " + topic + ", message : " + message); if (null != topic) { Log.d(TAG, "It's a channel message"); newMessage.channel(MMXChannel.fromMMXTopic(topic)); } else if (recipients.size() > 0) { Log.d(TAG, "It's a in-app message"); newMessage.recipients(recipients); } else { throw new IllegalArgumentException("Neither recipients nor channel is set in message."); } if (null != message.getPayload() && null != message.getPayload().getType()) { parsePayload(message.getPayload().getType(), message.getPayload().getDataAsText().toString(), newMessage); } return newMessage.sender(sender).id(message.getId()).timestamp(message.getPayload().getSentTime()) .metaData(metaData).build(); } static MMXMessage fromPubSubItem(PubSubItem pubSubItem) { if (null != pubSubItem && null != pubSubItem.getContent()) { UserCache userCache = UserCache.getInstance(); HashSet<String> usersToRetrieve = new HashSet<String>(); usersToRetrieve.add(pubSubItem.getPublisher().getUserId()); //fill the cache userCache.fillCacheByUserId(usersToRetrieve, UserCache.DEFAULT_ACCEPTED_AGE); //five minutes old is ok //populate the values User sender = userCache.getByUserId(pubSubItem.getPublisher().getUserId()); if (sender == null) { Log.e(TAG, "fromMMXMessage(): FAILURE: Unable to retrieve sender from cache: " + "sender=" + sender + ". Message will be dropped."); return null; } //populate the message content HashMap<String, String> content = new HashMap<String, String>(); for (Map.Entry<String, String> entry : pubSubItem.getContent().entrySet()) { if (!CONTENT_ATTACHMENTS.equals(entry.getKey())) { content.put(entry.getKey(), entry.getValue()); } } MMXMessage.Builder newMessage = new MMXMessage.Builder(); // Extract attachments String attachmentsStr = MagnetUtils.trimQuotes(pubSubItem.getContent().get(CONTENT_ATTACHMENTS)); if (StringUtil.isNotEmpty(attachmentsStr)) { List<Attachment> attachments = GsonData.getGson().fromJson(attachmentsStr, new TypeToken<List<Attachment>>() { }.getType()); if (null != attachments && attachments.size() > 0) { newMessage.attachments(attachments.toArray(new Attachment[0])); } } if (null != pubSubItem.getMetaData()) { parsePayload(pubSubItem.getMetaData().getMtype(), pubSubItem.getMetaData().getData(), newMessage); } return newMessage.sender(sender).id(pubSubItem.getItemId()) .timestamp(Iso8601DateConverter.fromString(pubSubItem.getMetaData().getCreationDate())) .content(content).build(); } else { Log.w(TAG, "PubSubItem doesn't have content : " + pubSubItem); return null; } } private static void parsePayload(String payloadType, String data, MMXMessage.Builder newMessage) { String payloadDataType = getPayloadTypeName(payloadType); if (null != payloadDataType) { Class payloadDataTypeClass = getPayloadType(payloadDataType); if (null != payloadDataTypeClass) { if (StringUtil.isNotEmpty(data)) { newMessage.payload((MMXTypedPayload) GsonDecorator.getInstance().fromJson(data, TypeToken.get(payloadDataTypeClass))); } else { Log.e(TAG, "Payload is empty for type " + payloadDataType); } } else { Log.e(TAG, "Payload type " + payloadDataType + " is not registered"); } } } //For handling the onSuccess of send() messages when server ack is received private static class MessageListenerPair { private final OnFinishedListener<String> listener; private final MMXMessage message; private MessageListenerPair(OnFinishedListener<String> listener, MMXMessage message) { this.listener = listener; this.message = message; } } private static HashMap<String, MessageListenerPair> sMessageSendListeners = new HashMap<String, MessageListenerPair>(); static void handleMessageSubmitted(String messageId) { // synchronized (sMessageSendListeners) { // MessageListenerPair listenerPair = sMessageSendListeners.get(messageId); // if (listenerPair != null) { // listenerPair.listener.onSuccess(messageId); // } // } } static void handleMessageAccepted(List<MMXid> invalidRecipients, final String messageId) { Log.d(TAG, "handleMessageAccepted() invalid=" + invalidRecipients + " msgId=" + messageId); synchronized (sMessageSendListeners) { final MessageListenerPair listenerPair = sMessageSendListeners.remove(messageId); if (listenerPair != null) { if (invalidRecipients == null || invalidRecipients.isEmpty()) { MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listenerPair.listener.onSuccess(messageId); } }); } else { final InvalidRecipientException ex = new InvalidRecipientException("Invalid recipients", messageId); for (MMXid xid : invalidRecipients) { ex.addUserId(xid.getUserId()); } MMX.getCallbackHandler().post(new Runnable() { @Override public void run() { listenerPair.listener.onFailure(MMXMessage.FailureCode.INVALID_RECIPIENT, ex); } }); } } } } static MMXMessage getSendingMessage(String messageId) { synchronized (sMessageSendListeners) { MessageListenerPair listenerPair = sMessageSendListeners.get(messageId); if (listenerPair == null) { return null; } return listenerPair.message; } } private Throwable uploadAttachments(MMXPayload payload, final String messageId, final Attachment.UploadListener progressListener) { for (final Attachment attachment : mAttachments) { if (Attachment.Status.COMPLETE == attachment.getStatus() && StringUtil.isNotEmpty(attachment.getAttachmentId())) { Log.d(TAG, "Attahcment " + attachment.getName() + " is already uploaded"); if (null != progressListener) { progressListener.onComplete(attachment); } continue; } //Set meta data attachment.addMetaData("metadata_message_id", messageId); if (null != mChannel) { attachment.addMetaData("metadata_channel_name", mChannel.getName()); attachment.addMetaData("metadata_channel_is_public", String.valueOf(mChannel.isPublic())); } else { StringBuilder sb = new StringBuilder(); int count = 0; for (User u : mRecipients) { sb.append(u.getUserIdentifier()); if (count++ != mRecipients.size() - 1) { sb.append(","); } } attachment.addMetaData("metadata_recipients", sb.toString()); } final CountDownLatch uploadSignal = new CountDownLatch(1); final AtomicReference<Throwable> uploadError = new AtomicReference<Throwable>(); attachment.upload(new Attachment.UploadListener() { @Override public void onStart(Attachment attachment) { if (null != progressListener) { progressListener.onStart(attachment); } } @Override public void onComplete(Attachment attachment) { if (null != progressListener) { progressListener.onComplete(attachment); } uploadSignal.countDown(); } @Override public void onError(Attachment attachment, Throwable throwable) { uploadError.set(throwable); if (null != progressListener) { progressListener.onError(attachment, throwable); } uploadSignal.countDown(); } }); try { uploadSignal.await(60, TimeUnit.SECONDS); } catch (InterruptedException e) { uploadError.set(e); if (null != progressListener) { progressListener.onError(attachment, new Exception("Timeout when uploading attachment " + attachment.getName())); } } if (null != uploadError.get()) { return uploadError.get(); } if (Attachment.Status.COMPLETE == attachment.getStatus() && StringUtil.isNotEmpty(attachment.getAttachmentId())) { if (null != progressListener) { progressListener.onComplete(attachment); } Log.d(TAG, "Attachment " + attachment.getName() + " is uploaded successfully."); } else { String message = "Failed to upload attachment " + attachment.getName(); Log.d(TAG, message); if (uploadSignal.getCount() > 0) { if (null != progressListener) { progressListener.onError(attachment, new Exception(message)); } } } } payload.setMetaData(CONTENT_ATTACHMENTS, GsonData.getGson().toJson(mAttachments)); return null; } //----------------Parcelable Methods---------------- @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.mId); dest.writeString(this.mType); dest.writeLong(mTimestamp != null ? mTimestamp.getTime() : -1); dest.writeParcelable(this.mSender, flags); dest.writeParcelable(this.mChannel, 0); dest.writeParcelableArray(ParcelableHelper.setToArray(this.mRecipients), flags); dest.writeBundle(ParcelableHelper.stringMapToBundle(this.mMeta)); dest.writeString(this.mReceiptId); dest.writeList(this.mAttachments); } protected MMXMessage(Parcel in) { this.mId = in.readString(); this.mType = in.readString(); long tmpMTimestamp = in.readLong(); this.mTimestamp = tmpMTimestamp == -1 ? null : new Date(tmpMTimestamp); this.mSender = in.readParcelable(User.class.getClassLoader()); this.mChannel = in.readParcelable(MMXChannel.class.getClassLoader()); User[] tmpRecipients = (User[]) in.readParcelableArray(User.class.getClassLoader()); if (null != tmpRecipients) { this.mRecipients = new HashSet<User>(Arrays.asList(tmpRecipients)); } this.mMeta = ParcelableHelper.stringMapfromBundle(in.readBundle()); this.mReceiptId = in.readString(); this.mAttachments = new ArrayList<Attachment>(); in.readTypedList(this.mAttachments, Attachment.CREATOR); } public static final Parcelable.Creator<MMXMessage> CREATOR = new Parcelable.Creator<MMXMessage>() { @Override public MMXMessage createFromParcel(Parcel source) { return new MMXMessage(source); } @Override public MMXMessage[] newArray(int size) { return new MMXMessage[size]; } }; }