Creating Custom Messages

Developing a Custom MessageModel and its Message View is a key step in building a Custom Messaging Experiences.

Many projects will need to implement Custom MessageModel components. Some of these MessageModel components will be simple informational messages, and others will provide rich interactive messages. MessageModel components that are interactive and allow participants in the conversation to interact using UIs provided within these Messages help form what Layer refers to as Messaging Experiences.

A Messaging Experience is:

  • a Message that is sent within a Conversation
  • is visible to participants of that Conversation
  • Enables UI interactions within that Message
  • Shares state changes from that UI across all participants

Registration with MessageModelManager

The MessageModelManager keeps a Map of MIME types to constructor references so it is required to register your custom MIME type with the MessageModelManager. For example:

messageModelManager.registerModel("application/vnd.mycompany.custom", CustomMessageModel.class);

These custom MIME types need to be compliant with RFC 2045.

Message Model Creation

When creating a MessageModel for a Message, the following lifecycle is followed:

Message Creation

To create a new Message that represents the MessageModel, a custom MessageSender should be created. In this sender, you should use the metadata object that backs your MessageModel and a Gson instance to create a Message and its necessary MessageParts via a LayerClient instance.

Message Model Methods

Here are a list of methods used to create custom MessageModel subclasses; their use is illustrated below.

Name Description
parse(MessagePart) Populate model properties/metadata from the MessagePart
getViewLayoutId() Layout resource ID to inflate for the message view
getContainerViewLayoutId() Layout resource ID to inflate for the container of the message view. (Usually is either R.layout.xdk_ui_standard_message_container, R.layout.xdk_ui_titled_message_container or R.layout.xdk_ui_empty_message_container)
shouldDownloadContentIfNotReady(MessagePart) Determine if the rich content should be downloaded at parse time or not
getHasContent() Determine if this model has content or not (usually if it is downloaded it will have content)
getPreviewText() Return text that will be shown as a preview of the message (usually used on the conversation item in the conversations list)
getTitle() Return the text to use in the title field of a MessageContainer
getDescription() Return the text to use in the subtitle or description field of a MessageContainer
getFooter() Return the text to use in the footer field of a MessageContainer

Opinion Message Example

Goals: Introduce the basics of creating a custom Message Type that uses a StandardMessageContainer.

An Opinion Message is a sample message used to allow a user to rate and comment on a previous message. Key data that this message will contain:

Name Type Description
comment String A comment that the user has about some item of content
rating Integer A number from 1-5 indicating the level of enthusiasm for their comment (1: Comment is simply brain storming throwing ideas out there; 5: This is It! This is the Idea! We are Geniuses!)
description String The Message that an opinion is being expressed about
author String The author of the message described by description
A rating of 4, and a comment of "I love this stuff" for Lamb Stew

The Opinion Message Model

Step 1: Create a metadata class to hold the JSON data

It isn’t strictly required to use a metadata POJO but for the sake of clarity we will use one here.

public class OpinionMetadata {
    public String comment;
    public Integer rating;
    public String description;
    public String author;
}
Step 2: Setup a Basic Message Model Class

This Opinion Message Model is a subclass of MessageModel. Start by creating the class and adding the parse(), shouldDownloadContentIfNotReady() and getHasContent() methods

public class OpinionMessageModel extends MessageModel {
    // Define a custom MIME type to use when creating this model and sending messages of this type
    public static final String MIME_TYPE = "application/vnd.customco.opinion+json";

    private OpinionMetadata mMetadata;

    public OpinionMessageModel(@NonNull Context context,
            @NonNull LayerClient layerClient,
            @NonNull Message message) {
        super(context, layerClient, message);
    }

    @Override
    protected void parse(@NonNull MessagePart messagePart) {
        // Populate the metadata POJO using Gson
        InputStreamReader inputStreamReader = new InputStreamReader(messagePart.getDataStream());
        JsonReader reader = new JsonReader(inputStreamReader);
        mMetadata = getGson().fromJson(reader, LinkMessageMetadata.class);
        try {
            inputStreamReader.close();
        } catch (IOException e) {
            if (Log.isLoggable(Log.ERROR)) {
                Log.e("Failed to close input stream while parsing opinion message", e);
            }
        }
    }

    @Override
    protected boolean shouldDownloadContentIfNotReady(@NonNull MessagePart messagePart) {
        // Always download this message part
        return true;
    }

    @Override
    public boolean getHasContent() {
        return mMetadata != null;
    }
}
Step 2: Working with the StandardMessageContainer

Not all Models are designed to work with the StandardMessageContainer; the Container shows metadata (title, description, footer, etc…) under the Message View. A Receipt Message would not be rendered with a title, description and footer below it. Nor would a Status Message. The Opinion Message however is going to support the StandardMessageContainer which is done by declaring the appropriate layout resource ID for the StandardMessageContainer, as well as supporting the getTitle, getDescription and getFooter methods:

public class OpinionMessageModel extends MessageModel {

    ...
    
    @Override
    public int getContainerViewLayoutId() {
        return R.layout.xdk_ui_standard_message_container;
    }

    @Nullable
    @Override
    public String getTitle() {
        // Use a resource string for localization
        return getAppContext().getString(R.string.opinion_about);
    }

    @Nullable
    @Override
    public String getDescription() {
        return mMetadata == null ? null : mMetadata.description;
    }

    @Nullable
    @Override
    public String getFooter() {
        return mMetadata == null ? null : mMetadata.author;
    }
}
Step 3: Working with the Conversation Lists

The getPreviewText() method controls how this Message is summarized, primarily for use within the Conversation List. Each Conversation in the Conversation List shows the Last Message summary; which for a Text Model/Message is usually just the text of the message. For an image though it might be “Image Received”. For this Opinion Model:

public class OpinionMessageModel extends MessageModel {

    ...
    
    @Nullable
    @Override
    public String getPreviewText() {
        return getTitle() + getDescription();
    }
}
Step 4: Create the message view

Create an Android layout that should be inflated inside the container view. Here is the view for the opinion message (res/layout/opinion_message_view.xml):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="messageModel"
            type="com.customco.message.OpinionMessageModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/rating"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/orange"
            android:padding="8dp"
            android:text="@{messageModel.ratingText}" />

        <TextView
            android:id="@+id/comment"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@color/gray"
            android:gravity="center_horizontal"
            android:padding="8dp"
            android:text="@{messageModel.comment}" />

    </LinearLayout>
</layout>

Note that this uses data binding to set the text of the rating and comment TextViews. It is required for this layout to have a variable named messageModel that has the desired type. The containers will set this variable on the message view so it can populate the necessary data. You can also have a custom view that has a setMessageModel(OpinionMessageModel model) method to gain access to the model in Java code.

Declare this layout to be used as the message view layout in the OpinionMessageModel and expose the comment and rating for data binding:

public class OpinionMessageModel extends MessageModel {

    ...
    
    @Override
    public int getViewLayoutId() {
        return R.layout.opinion_message_view;
    }

    public String getComment() {
        return mMetadata == null ? null : mMetadata.comment;
    }
    
    public String getRatingText() {
        return mMetadata == null ? null : String.valueOf(mMetadata.rating);
    }
}
Step 5: Register with MessageModelManager

Here is the full definition:

public class OpinionMessageModel extends MessageModel {
    public static final String MIME_TYPE = "application/vnd.customco.opinion+json";

    private OpinionMetadata mMetadata;

    public OpinionMessageModel(@NonNull Context context,
            @NonNull LayerClient layerClient,
            @NonNull Message message) {
        super(context, layerClient, message);
    }

    @Override
    protected void parse(@NonNull MessagePart messagePart) {
        InputStreamReader inputStreamReader = new InputStreamReader(messagePart.getDataStream());
        JsonReader reader = new JsonReader(inputStreamReader);
        mMetadata = getGson().fromJson(reader, LinkMessageMetadata.class);
        try {
            inputStreamReader.close();
        } catch (IOException e) {
            if (Log.isLoggable(Log.ERROR)) {
                Log.e("Failed to close input stream while parsing opinion message", e);
            }
        }
    }

    @Override
    public int getViewLayoutId() {
        return R.layout.opinion_message_view;
    }

    @Override
    public int getContainerViewLayoutId() {
        return R.layout.xdk_ui_standard_message_container;
    }

    @Override
    protected boolean shouldDownloadContentIfNotReady(@NonNull MessagePart messagePart) {
        return true;
    }

    @Override
    public boolean getHasContent() {
        return mMetadata != null;
    }

    @Nullable
    @Override
    public String getPreviewText() {
        return getTitle() + getDescription();
    }

    @Nullable
    @Override
    public String getTitle() {
        return getAppContext().getString(R.string.opinion_about);
    }

    @Nullable
    @Override
    public String getDescription() {
        return mMetadata == null ? null : mMetadata.description;
    }

    @Nullable
    @Override
    public String getFooter() {
        return mMetadata == null ? null : mMetadata.author;
    }
}

The above MessageModel subclass should be instantiated via a MessageModelManager. It is possible to instantiate it via the constructor but that is not recommended as you will have to ensure all dependencies are correctly supplied. You can register the OpinionMessageModel as follows:

messageModelManager.registerModel(OpinionMessageModel.MIME_TYPE, OpinionMessageModel.class);

This allows a model to be created whenever the following is called:

messageModelManager.getNewModel(OpinionMessageModel.MIME_TYPE, message);

This is typically called from either a ConversationDataSource or a MessageModelDataSource.

Sending an Opinion Message

It is recommended to create messages by creating a custom MessageSender. You can go one step further and extend an AttachmentSender to allow it to be registered on a ComposeBar.

public class OpinionSender extends MessageSender {

    private Gson mGson;

    public OpinionSender(Context context, LayerClient layerClient, Gson gson) {
        super(context, layerClient);
        mGson = gson;
    }

    public boolean requestSend(String author, String comment, String description, int rating) {
        // Get a MIME type with role = root
        String mimeType = MessagePartUtils.getAsRoleRoot(OpinionMessageModel.MIME_TYPE);

        // Create and populate the metadata object
        OpinionMetadata metadata = new OpinionMetadata();
        // This could also be always set to the current authenticated user's display name
        metadata.author = author;
        metadata.comment = comment;
        metadata.description = description;
        metadata.rating = rating;
        // Convert the metadata to a JSON string and get the byte array
        byte[] bytes = mGson.toJson(metadata).getBytes();
        // Create the message part
        MessagePart root = getLayerClient().newMessagePart(mimeType, bytes);
        // Create the message
        Message message = getLayerClient().newMessage(root);
        // Send the message
        return send(message);
    }
}
Introduction UI API Reference