Custom message types
In addition to the built-in message types, you can create custom message types to fit your business needs.
Choose the base class based on your requirements:
MessageContent: For standard message content (e.g., text, location).MediaMessageContent: For media content that requires file upload/download handling. ExtendsMessageContentwith media file processing logic.
For more details on the message model, see Message overview.
Custom message types must have a consistent structure across all platforms; otherwise, cross-platform messaging will fail.
Create a custom message
The SDK does not define or parse custom message content — you handle that yourself.
The @MessageTag annotation on a custom message type defines its unique identifier (objectname), storage behavior, display behavior, and unread count behavior.
Example: Standard custom message
The following is a complete example of a standard custom message:
- Kotlin
- Java
@MessageTag(value = "app:txtcontent", flag = MessageTag.ISCOUNTED)
class MyTextContent : MessageContent {
private val TAG = "appTextContent"
var content: String? = null
private constructor()
companion object {
@JvmStatic
fun obtain(content: String): MyTextContent {
val msg = MyTextContent()
msg.content = content
return msg
}
@JvmField
val CREATOR: Parcelable.Creator<MyTextContent> = object : Parcelable.Creator<MyTextContent> {
override fun createFromParcel(source: Parcel) = MyTextContent(source)
override fun newArray(size: Int) = arrayOfNulls<MyTextContent>(size)
}
}
override fun encode(): ByteArray? {
val jsonObj = super.getBaseJsonObject()
try {
jsonObj.put("content", this.content)
} catch (e: JSONException) {
Log.e(TAG, "JSONException ${e.message}")
}
return try {
jsonObj.toString().toByteArray(Charsets.UTF_8)
} catch (e: UnsupportedEncodingException) {
Log.e(TAG, "UnsupportedEncodingException", e)
null
}
}
constructor(data: ByteArray?) : super() {
if (data == null) return
val jsonStr = try {
String(data, Charsets.UTF_8)
} catch (e: UnsupportedEncodingException) {
Log.e(TAG, "UnsupportedEncodingException", e)
return
}
try {
val jsonObj = JSONObject(jsonStr)
super.parseBaseJsonObject(jsonObj)
if (jsonObj.has("content")) {
content = jsonObj.optString("content")
}
} catch (e: JSONException) {
Log.e(TAG, "JSONException ${e.message}")
}
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToBaseInfoParcel(dest)
ParcelUtils.writeToParcel(dest, content)
}
constructor(parcel: Parcel) : super() {
super.readFromBaseInfoParcel(parcel)
content = ParcelUtils.readFromParcel(parcel)
}
override fun describeContents() = 0
}
@MessageTag(value = "app:txtcontent", flag = MessageTag.ISCOUNTED)
public class MyTextContent extends MessageContent {
private static final String TAG = "appTextContent";
private String content;
private MyTextContent() {}
public static MyTextContent obtain(String content) {
MyTextContent msg = new MyTextContent();
msg.content = content;
return msg;
}
@Override
public byte[] encode() {
JSONObject jsonObj = super.getBaseJsonObject();
try {
jsonObj.put("content", this.content);
} catch (JSONException e) {
Log.e(TAG, "JSONException " + e.getMessage());
}
try {
return jsonObj.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "UnsupportedEncodingException ", e);
}
return null;
}
public MyTextContent(byte[] data) {
if (data == null) return;
String jsonStr = null;
try {
jsonStr = new String(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "UnsupportedEncodingException ", e);
}
if (jsonStr == null) return;
try {
JSONObject jsonObj = new JSONObject(jsonStr);
super.parseBaseJsonObject(jsonObj);
if (jsonObj.has("content")) {
content = jsonObj.optString("content");
}
} catch (JSONException e) {
Log.e(TAG, "JSONException " + e.getMessage());
}
}
@Override
public void writeToParcel(Parcel dest, int i) {
super.writeToBaseInfoParcel(dest);
ParcelUtils.writeToParcel(dest, content);
}
public MyTextContent(Parcel in) {
super.readFromBaseInfoParcel(in);
setContent(ParcelUtils.readFromParcel(in));
}
public static final Creator<MyTextContent> CREATOR =
new Creator<MyTextContent>() {
public MyTextContent createFromParcel(Parcel source) {
return new MyTextContent(source);
}
public MyTextContent[] newArray(int size) {
return new MyTextContent[size];
}
};
@Override
public int describeContents() { return 0; }
public void setContent(String content) { this.content = content; }
public String getContent() { return content; }
}
Code walkthrough
Here is a step-by-step explanation of the custom message example:
-
Add the
@MessageTagannotation. This is required for all custom message types.Parameter Type Description value String Required. The unique message type identifier (e.g., app:txtcontent). Keep it under 16 characters. Do not use theRC:prefix, which is reserved for built-in types.flag int Required. Controls storage and counting behavior. See the flag values table below. messageHandler Class<? extends MessageHandler>Optional. Use a custom handler if the encoded size exceeds 128 KB for media messages. Flag values:
Flag Use case MessageTag.NONENot stored locally, not counted as unread. Supports missed messages. Use for command messages that don't need to be displayed. MessageTag.ISPERSISTEDStored locally but not counted as unread. Supports missed messages and remote history. Use for notification-style messages that need to be displayed but not counted. MessageTag.ISCOUNTEDStored locally and counted as unread. Supports missed messages and remote history. Use for text, image, and other user-facing messages. MessageTag.STATUSNot stored anywhere. Not counted as unread. Only delivered to online recipients; the server discards the message if the recipient is offline. Use for transient status like typing indicators. -
Define internal variables. Add the fields your message needs. In the example,
contentstores the message text. -
Implement a factory method. Provide a convenient way to create instances.
-
Implement binary encode/decode methods. These convert between your message object and
byte[].tipThe parent class
MessageContentprovides encode/decode helpers foruser(user info),mentionedInfo(mention data), andextra(extra info). Call the parent methods if your custom message needs these fields.- Override
encode()to serialize the message object tobyte[](object → JSONObject → JSON string → byte[]). - Override the
MessageContent(byte[] data)constructor to deserialize (byte[] → JSON string → JSONObject → object).
- Override
-
Implement Parcel serialization. These methods support cross-process message transport. Use the SDK's
ParcelUtilsutility class.warningThe read and write order in Parcel serialization must match exactly.
-
Add getter and setter methods for your custom fields.
Example: Media custom message
Unless you bypass the SDK's built-in upload logic, the message must include a localPath property (type: String?).
- Kotlin
- Java
@MessageTag(value = "app:mediacontent", flag = MessageTag.ISCOUNTED)
class MyMediaMessageContent : MediaMessageContent {
constructor(localUri: Uri) {
localPath = localUri.toString()
}
companion object {
@JvmStatic
fun obtain(localUri: Uri) = MyMediaMessageContent(localUri)
@JvmField
val CREATOR: Parcelable.Creator<MyMediaMessageContent> = object : Parcelable.Creator<MyMediaMessageContent> {
override fun createFromParcel(source: Parcel) = MyMediaMessageContent(source)
override fun newArray(size: Int) = arrayOfNulls<MyMediaMessageContent>(size)
}
}
override fun encode(): ByteArray {
val jsonObj = super.getBaseJsonObject()
try {
localPath?.let { jsonObj.put("localPath", it) }
} catch (e: JSONException) {
Log.e("JSONException", e.message)
}
return jsonObj.toString().toByteArray()
}
constructor(data: ByteArray) : super() {
val jsonStr = String(data)
try {
val jsonObj = JSONObject(jsonStr)
super.parseBaseJsonObject(jsonObj)
if (jsonObj.has("localPath")) {
localPath = jsonObj.optString("localPath")
}
} catch (e: JSONException) {
Log.e("JSONException", e.message)
}
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToBaseInfoParcel(dest)
ParcelUtils.writeToParcel(dest, localPath)
}
constructor(parcel: Parcel) : super() {
super.readFromBaseInfoParcel(parcel)
localPath = ParcelUtils.readFromParcel(parcel)
}
override fun describeContents() = 0
fun getLocalUri(): Uri? = localPath?.let { Uri.parse(it) }
fun setLocalUri(localUri: Uri) { localPath = localUri.toString() }
}
@MessageTag(value = "app:mediacontent", flag = MessageTag.ISCOUNTED)
public class MyMediaMessageContent extends MediaMessageContent {
public MyMediaMessageContent(Uri localUri) {
setLocalPath(localUri.toString());
}
public static MyMediaMessageContent obtain(Uri localUri) {
return new MyMediaMessageContent(localUri);
}
@Override
public byte[] encode() {
JSONObject jsonObj = super.getBaseJsonObject();
try {
if (getLocalPath() != null) {
jsonObj.put("localPath", getLocalPath());
}
} catch (JSONException e) {
Log.e("JSONException", e.getMessage());
}
return jsonObj.toString().getBytes();
}
public MyMediaMessageContent(byte[] data) {
String jsonStr = new String(data);
try {
JSONObject jsonObj = new JSONObject(jsonStr);
super.parseBaseJsonObject(jsonObj);
if (jsonObj.has("localPath")) {
setLocalPath(jsonObj.optString("localPath"));
}
} catch (JSONException e) {
Log.e("JSONException", e.getMessage());
}
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToBaseInfoParcel(dest);
ParcelUtils.writeToParcel(dest, getLocalPath());
}
public MyMediaMessageContent(Parcel in) {
super.readFromBaseInfoParcel(in);
setLocalPath(ParcelUtils.readFromParcel(in));
}
public static final Creator<MyMediaMessageContent> CREATOR =
new Creator<MyMediaMessageContent>() {
@Override
public MyMediaMessageContent createFromParcel(Parcel source) {
return new MyMediaMessageContent(source);
}
@Override
public MyMediaMessageContent[] newArray(int size) {
return new MyMediaMessageContent[size];
}
};
@Override
public int describeContents() { return 0; }
public Uri getLocalUri() { return getLocalPath() != null ? Uri.parse(getLocalPath()) : null; }
public void setLocalUri(Uri localUri) { setLocalPath(localUri.toString()); }
}
Register custom messages
Register custom message types by calling registerCustomMessages before establishing the IM connection. Otherwise, unregistered types are handled as UnknownMessage.
Register custom messages before connecting. Call this during your application lifecycle initialization.
- Kotlin
- Java
// In Application.onCreate(), after initialize but before connect
NCEngine.registerCustomMessages(listOf(
MyTextContent::class.java,
MyMessage::class.java
))
// In Application.onCreate(), after initialize but before connect
List<Class<? extends MessageContent>> myMessages = new ArrayList<>();
myMessages.add(MyTextContent.class);
myMessages.add(MyMessage.class);
NCEngine.registerCustomMessages(myMessages);
Parameters
| Parameter | Type | Description |
|---|---|---|
| messageClasses | List<Class<? extends MessageContent>> | List of custom message classes. Each must extend MessageContent or MediaMessageContent. |
Send custom messages
Send custom message types using the same methods as built-in types:
- If the custom type extends
MessageContent, use the standard message send method. - If the custom type extends
MediaMessageContent, use the media message send method.
To enable push notifications for custom messages, provide a pushContent value when sending. Without it, offline recipients will not receive push notifications.
- Without
pushContent, the server cannot trigger push notifications for custom messages. - Transient messages (
MessageTag.STATUS) do not support push notifications.