Multi-device login
Function introduction
ZIM SDK supports the configuration of custom multi-device logins, which means that users can log in to multiple devices simultaneously with the same account, ensuring that the user's sessions, messages, groups and other data communicate with each other.
- To use this feature, please subscribe to the Professional or Ultimate package, and contact the technical support team to activate the service and configure the login policy.
- This feature only supports SDK version 2.11.0 and above.
Common login policies
Multi-device login supports configuration on 7 platforms including Android, iOS, Windows, Mac, Linux, iPad, and Web. Across Windows, macOS, Linux, Android, iOS, and iPadOS, a single user account to be simultaneously active on up to 10 devices. Multi-instance login is supported on the Web platform, with no more than 5 login instances.
The following are the three login policies supported by the ZIM SDK: single-device login, dual-device login, and multi-device login.
Single device login (by default) | Only supports users logging into the account on one device at a time. If the account is logged into another device, the account on the original device will be logged out. |
---|
Dual-device login | Allows users to log in to their accounts on any of the Windows, Mac, Linux, Android, iOS, and iPad devices while keeping their accounts online on the web device. |
Multi-device login | Allows users to simultaneously log in to their accounts on multiple devices, including Windows, Mac, Linux, Android, iOS, iPad, Web, and Mini program. The following configurations are supported:
- Multiple device and instance login on the same device at the same time.
- Configure the logic for kicking each other offline; currently only supports offline kicking between Android and iOS devices, and offline kicking between Windows and Mac devices.
|
Implementation process
Contact ZEGOCLOUD technical support team to activate the multi-terminal login service. After configuring the multi-device login, you can call login on multiple devices to log in to the ZIM SDK. The implementation process is the same as a single-terminal login. For details on how to log in, please refer to the "Logging in ZIM" section in Send and receive messages.
When the number of login devices reaches the limit set by the policy, if you log in to the account on a new device, the account that was logged in the earliest on the original device will be kicked offline. You will be notified of this through the onConnectionStateChanged callback with the event set to KickedOut
. For example, if the login specifies a maximum of 3 devices to be logged in, and the account has already been logged in on devices A, B, and C (in chronological order), logging in to the account on device D will kick the account on device A offline.
When the account is kicked offline, it is recommended to call the logout interface to log out of the ZIM service and switch the user interface to the login interface.
The impact of multi-device login on other functions
The codes for the following functions must also be changed after configuring multi-device login.
ZIM SDK provides user information synchronization function for multi-device login users.After a user updates his or her information through updateUserName, updateUserAvatarUrl, updateUserExtendedData interfaces on one device, other online clients can register the onUserInfoUpdated event to listen for user information being modified.
onUserInfoUpdated processing example
public class ZIMEventHandler {
public void onUserInfoUpdated(ZIM zim, ZIMUserFullInfo info) {
// Can be obtained in info: user ID, user name, user avatar, user additional fields
String userID = info.baseInfo.userID;
String userName = info.baseInfo.userName;
String userAvatarUrl = info.userAvatarUrl;
String extendedData = info.extendedData;
}
}
1
ZIMEventHandler.onUserInfoUpdated = (ZIM zim, ZIMUserFullInfo info){
// You can obtain the following information from info: user ID, username, user avatar, user additional fields
String userID = info.baseInfo.userID;
String userName = info.baseInfo.userName;
String userAvatarUrl = info.userAvatarUrl;
String extendedData = info.extendedData;
};
1
- (void)zim:(ZIM *)zim userInfoUpdated:(ZIMUserFullInfo *)info{
// Can be obtained in info: user ID, user name, user avatar, user additional fields
NSString *userID = info.baseInfo.userID;
NSString *userName = info.baseInfo.userName;
NSString *userAvatarUrl = info.userAvatarUrl;
NSString *userExtendedData = info.extendedData;
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onUserInfoUpdated(zim::ZIM * zim, const zim::ZIMUserFullInfo & info) override {
// Can be obtained in info: user ID, user name, user avatar, user additional fields
auto user_id = info.baseInfo.userID;
auto user_name = info.baseInfo.userName;
auto user_avatar_url = info.userAvatarUrl;
auto user_extended_data = info.extendedData;
}
...
}
1
zim.on('userInfoUpdated', (zim, { info }) => {
console.log(info.baseInfo.userName, info.userAvatarUrl, info.extendedData);
});
1
Conversation management
Delete a single server conversation
When the user deletes the server conversation on one device through the deleteConversation interface(that is, isAlsoDeleteServerConversation set to true), other online clients can register onConversationChanged event to listen for the deletion of the conversation.
onConversationChanged example
public class ZIMEventHandler{
...
public void onConversationChanged(ZIM zim, ArrayList<ZIMConversationChangeInfo> conversationChangeInfoList){
for (ZIMConversationChangeInfo convInfo : conversationChangeInfoList) {
if (convInfo.event == ZIMConversationEvent.DELETED) {
// The conversation is deleted
}
}
}
...
}
1
ZIMEventHandler.onConversationChanged = (ZIM zim, List<ZIMConversationChangeInfo> conversationChangeInfoList){
for (ZIMConversationChangeInfo convInfo : conversationChangeInfoList) {
if (convInfo.event == ZIMConversationEvent.delete) {
// The conversation has been deleted
}
}
};
1
- (void)zim:(ZIM *)zim
conversationChanged:(NSArray<ZIMConversationChangeInfo * > *)conversationChangeInfoList{
for(ZIMConversationChangeInfo *convInfo in conversationChangeInfoList){
if(convInfo.event == ZIMConversationEventDeleted){
// The conversation is deleted
}
}
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onConversationChanged(zim::ZIM *zim,
const std::vector<zim::ZIMConversationChangeInfo> &conversationChangeInfoList) override {
for (const auto &conv_info : conversationChangeInfoList) {
if (conv_info.event == zim::ZIMConversationEvent::ZIM_CONVERSATION_EVENT_DELETED) {
// The conversation is deleted
}
}
}
...
}
1
zim.on('conversationChanged', (zim, { infoList }) => {
infoList.forEach((info) => {
if (info.event == 3) {
//The conversation is deleted
}
});
});
1
Delete all server conversations
When the user deletes all server conversations on one device through the deleteAllConversations interface(that is, isAlsoDeleteServerConversation set to true), other clients can use onConversationsAllDeleted event to listen for the deletion of the.
onConversationsAllDeleted processing example
@Override
public void onConversationsAllDeleted(ZIM zim, ZIMConversationsAllDeletedInfo info) {
// The other end deleted all sessions
}
1
ZIMEventHandler.onConversationChanged = (ZIM zim, List<ZIMConversationChangeInfo> conversationChangeInfoList){
for (ZIMConversationChangeInfo convInfo : conversationChangeInfoList) {
if (convInfo.event == ZIMConversationEvent.delete) {
// The conversation has been deleted
}
}
};
1
- (void)zim:(ZIM *)zim
conversationsAllDeleted:(ZIMConversationsAllDeletedInfo *)info{
// The other end deleted all sessions
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onConversationsAllDeleted(zim::ZIM *zim,
const ZIMConversationsAllDeletedInfo &info) override {
// The other end deleted all sessions
}
...
}
1
zim.on('conversationsAllDeleted', (zim, { count }) => {
console.log('Number of deleted conversations', count);
});
1
Clear all unread conversations
When the user clears all unread conversations on one device through clearConversationTotalUnreadMessageCount. Other clients can listen for the notification of all conversations having their unread message count cleared through the onConversationTotalUnreadMessageCountUpdated event.
onConversationTotalUnreadMessageCountUpdated processing example
@Override
public void onConversationTotalUnreadMessageCountUpdated(ZIM zim, int totalUnreadMessageCount) {
//After clearing all unread messages in all conversations on other devices, the value of totalUnreadMessageCount in the current device will be 0
}
1
ZIMEventHandler.onConversationsAllDeleted = (
ZIM zim, ZIMConversationsAllDeletedInfo info){
// All conversations have been deleted by another client
}
1
- (void)zim:(ZIM *)zim
conversationTotalUnreadMessageCountUpdated:(unsigned int)totalUnreadMessageCount{
//After clearing all unread messages in all conversations on other devices, the value of totalUnreadMessageCount in the current device will be 0
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onConversationTotalUnreadMessageCountUpdated(zim::ZIM *zim,
unsigned int totalUnreadMessageCount) override {
//After clearing all unread messages in all conversations on other devices, the value of totalUnreadMessageCount in the current device will be 0
}
...
}
1
zim.on('conversationTotalUnreadMessageCountUpdated', (zim, { totalUnreadMessageCount }) => {
//After clearing all unread messages in all conversations on other devices, the value of `totalUnreadMessageCount` in the current device will be 0
});
1
Message management
Message Synchronization
When a user logs in to a new device, the SDK does not automatically synchronize the existing messages from the old device to the new device. Users need to actively call the queryHistoryMessage interface to retrieve the messages stored on the ZIM server. For more details, please refer to Get message history . As for the local messages stored on the old device, they cannot be retrieved.
Deleting server message
When a user deletes server messages in a conversation through the deleteMessages, deleteAllMessage ,deleteAllConversationMessages with (that is isAlsoDeleteServerMessage set to true), other online clients can listen for the onMessageDeleted to be notified of the deleted messages.
Example of calling the interface
public class ZIMEventHandler{
...
public void onMessageDeleted(ZIM zim, ZIMMessageDeletedInfo deletedInfo){
if (deletedInfo.messageDeleteType == ZIMMessageDeleteType.MESSAGE_LIST_DELETED)
{
// Multiple messages in a session are deleted
for (ZIMMessage message : messageList) {
// Iterate through each deleted message
}
} else if (deletedInfo.messageDeleteType ==
ZIMMessageDeleteType.CONVERSATION_ALL_MESSAGES_DELETED)
{
// all current messages for a session are deleted
} else if (deletedInfo.messageDeleteType ==
ZIMMessageDeleteType.ALL_CONVERSATION_MESSAGES_DELETED)
{
// all messages for all sessions are deleted
}
}
1
ZIMEventHandler.onMessageDeleted = (ZIM zim, ZIMMessageDeletedInfo deletedInfo){
if (deletedInfo.messageDeleteType == ZIMMessageDeleteType.messageListDeleted)
{
// Multiple messages in a conversation have been deleted
for (var message in messageList) {
// Iterate through each deleted message
}
} else if (deletedInfo.messageDeleteType ==
ZIMMessageDeleteType.conversationAllMessagesDeleted)
{
// All messages in a conversation have been deleted
} else if (deletedInfo.messageDeleteType ==
ZIMMessageDeleteType.allConversationMessagesDeleted)
{
// All messages in all conversations have been deleted
}
}
1
- (void)zim:(ZIM *)zim messageDeleted:(ZIMMessageDeletedInfo *)deletedInfo{
if (deletedInfo.messageDeleteType == ZIMMessageDeleteTypeMessageListDeleted)
{
// Multiple messages in a session are deleted
for (ZIMMessage *message in messageList) {
// Iterate through each deleted message
}
} else if (deletedInfo.messageDeleteType ==
ZIMMessageDeleteTypeConversationAllMessagesDeleted)
{
// all current messages for a session are deleted
} else if (deletedInfo.messageDeleteType ==
ZIMMessageDeleteTypeAllConversationMessagesDeleted)
{
// all messages for all sessions are deleted
}
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onMessageDeleted(zim::ZIM * /*zim*/, const zim::ZIMMessageDeletedInfo &deletedInfo) override {
if (deletedInfo.messageDeleteType == zim::ZIM_MESSAGE_DELETE_TYPE_MESSAGE_LIST_DELETED)
{
// Multiple messages in a session are deleted
for (const auto& message : deletedInfo.messageList)
{
// Iterate through each deleted message
}
} else if (deletedInfo.messageDeleteType ==
zim::ZIM_MESSAGE_DELETE_TYPE_CONVERSATION_ALL_MESSAGES_DELETED)
{
// All current messages for a session are deleted
} else if (deletedInfo.messageDeleteType ==
zim::ZIM_MESSAGE_DELETE_TYPE_ALL_CONVERSATION_MESSAGES_DELETED)
{
// all messages for all sessions are deleted
}
...
}
1
zim.on('messageDeleted', (zim, { conversationID, conversationType, isDeleteConversationAllMessage, messageList }) => {
if (messageDeleteType == 2 ) {
// All messages in the conversation are deleted
} else if (messageDeleteType == 1 ) {
// All current messages in the conversation are deleted
} else if (messageDeleteType == 0) {
// The specified message in the conversation is deleted
}
});
1
Set message receipt as read
When a user sets a message receipt as read through the sendMessageReceiptsRead, sendConversationMessageReceiptRead interface on one device, other online clients can listen for the messageReceiptChanged and onConversationMessageReceiptChanged event to be notified that the account has set the message receipt as read.
Example of calling the interface
public class ZIMEventHandler {
...
public void onMessageReceiptChanged(ZIM zim, ArrayList<ZIMMessageReceiptInfo> infos) {
for (ZIMMessageReceiptInfo info : infos) {
if (info.isSelfOperated) {
// Message receipt set as read by the user
}
}
}
public void onConversationMessageReceiptChanged(ZIM zim, ArrayList<ZIMMessageReceiptInfo> infos) {
for (ZIMMessageReceiptInfo info : infos) {
if (info.isSelfOperated) {
// Conversation receipt set as read by the user
}
}
}
...
}
1
ZIMEventHandler.onMessageReceiptChanged = (ZIM zim, List<ZIMMessageReceiptInfo> infos){
for (ZIMMessageReceiptInfo info : infos) {
if (info.isSelfOperated) {
// The user has set the message receipt as read
}
}
};
ZIMEventHandler.onConversationMessageReceiptChanged = (ZIM zim, List<ZIMMessageReceiptInfo> infos){
for (ZIMMessageReceiptInfo info : infos) {
if (info.isSelfOperated) {
// The user has set the message receipt as read
}
}
};
1
- (void)zim:(ZIM *)zim messageReceiptChanged:(NSArray<ZIMMessageReceiptInfo *> *)infos{
for(ZIMMessageReceiptInfo *info in infos){
if (info.isSelfOperated) {
// Message receipt set as read by the user
}
}
}
- (void)zim:(ZIM *)zim conversationMessageReceiptChanged:(NSArray<ZIMMessageReceiptInfo *> *)infos{
for (ZIMMessageReceiptInfo *info in infos) {
if (info.isSelfOperated) {
// Conversation receipt set as read by the user
}
}
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onMessageReceiptChanged(zim::ZIM * /*zim*/,
const std::vector<zim::ZIMMessageReceiptInfo> & infos) override {
for (const auto &info : infos) {
if (info.isSelfOperated) {
// Message receipt set as read by the user
}
}
}
virtual void onConversationMessageReceiptChanged(zim::ZIM *zim,
const std::vector<zim::ZIMMessageReceiptInfo> &infos) override {
for (const auto &info : infos) {
if (info.isSelfOperated) {
// Conversation receipt set as read by the user
}
}
}
...
}
1
zim.on('messageReceiptChanged', (zim, { infos }) => {
infos.forEach((info) => {
if (info.isSelfOperated) {
// Message receipt set as read by the user
}
});
});
zim.on('conversationMessageReceiptChanged', (zim, { infos }) => {
infos.forEach((info) => {
if (info.isSelfOperated) {
// Conversation receipt set as read by the user
}
});
});
1
Room management
The room module's related interfaces and events do not support multi-device login. When a user joins a room on device A, and then joins the same room on device B, device A will be kicked out of the room. Device A will receive the event
as KickedOutByOtherDevice
through the onRoomStateChanged.
public class ZIMEventHandler {
...
public void onRoomStateChanged(ZIM zim, ZIMRoomState state, ZIMRoomEvent event, JSONObject extendedData, String roomID) {
if (state == ZIMRoomState.DISCONNECTED &&
event == ZIMRoomEvent.KICKED_OUT) {
// Kicked out of the room due to multi-device login
}
}
...
}
1
ZIMEventHandler.onRoomStateChanged = (ZIM zim, ZIMRoomState state, ZIMRoomEvent event,
Map extendedData, String roomID){
if (state == ZIMRoomState.disconnected &&
event == ZIMRoomEvent.kickedOut) {
// Kicked out of the room due to multi-device login
}
};
1
- (void)zim:(ZIM *)zim
roomStateChanged:(ZIMRoomState)state
event:(ZIMRoomEvent)event
extendedData:(NSDictionary *)extendedData
roomID:(NSString *)roomID{
if (state == ZIMConnectionStateDisconnected &&
event == ZIMRoomEventKickedOutByOtherDevice) {
// Kicked out of the room due to multi-device login
}
}
1
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onRoomStateChanged(zim::ZIM *zim, zim::ZIMRoomState state, zim::ZIMRoomEvent event,
const std::string &extendedData,
const std::string &roomID) override {
if (state == zim::ZIMRoomState::ZIM_ROOM_STATE_DISCONNECTED &&
event == zim::ZIMRoomEvent::ZIM_ROOM_EVENT_KICKED_OUT_BY_OTHER_DEVICE) {
// Kicked out of the room due to multi-device login
}
}
...
}
1
// When a user joins a room on device A, and then joins the same room on device B, device A will be kicked out of the room
zim.on('roomStateChanged', (zim, { roomID, state, event }) => {
if (state == 0 && event == 10) {
// Kicked out of the room due to multi-device login
}
});
1
Group management
After enabling the multi-device login service, ZIM SDK will automatically synchronize group-related data between multiple devices.
Call invitation management
When the user is logged in on both device A and device B, and receives a call invitation, if the user accepts the invitation on device A(calling callAccept)or after rejecting the invitation(calling callReject) :
- Device A can know the result of the operation through the callbacks(ZIMCallAcceptanceSentResult or ZIMCallRejectionSentResult), and close the invitation popup window to perform other business operations.
- Device B should use the onCallUserStateChanged callback to know the call user state of the current user(ZIMCallUserState)as
Accepted
or Rejected
,and close the invitation popup window to perform other business operations.
Each device can receive user state change events within the call invitation, as shown in the following table:
callUserState | Device A | Device B |
---|
Inviting | ✔️ | ✔️ |
Received | ✔️ | ✔️ |
Accepted | ✔️ | ✔️ |
Rejected | ✔️ | ✔️ |
Timeout | ✖ | ✖ |
Cancelled | ✖ | ✖ |
Quit | ✔️ | ✖ |
Example of calling the interface
String selfUserID = "user_id";
String currentCallID = "call_id";
ZIMCallAcceptConfig acceptConfig;
acceptConfig.extendedData = "extra_1";
ZIMCallRejectConfig rejectConfig;
rejectConfig.extendedData = "extra_1";
// Device A accepts the invitation
zim.callAccept(currentCallID, acceptConfig, new ZIMCallAcceptanceSentCallback() {
@Override
public void onCallAcceptanceSent(String callID, ZIMError errorInfo) {
// Close the popup box for call waiting operation. Call ends.
}];
// Device A rejects the invitation
zim.callReject(currentCallID, rejectConfig, new ZIMCallRejectionSentCallback() {
@Override
public void onCallRejectionSent(String callID, ZIMError errorInfo) {
// Close the popup box for call waiting operation. Call ends.
}];
// Device B listens for callUserStateChanged
- (void)zim:(ZIM *)zim
-
public class ZIMEventHandler {
...
public void onCallUserStateChanged(ZIM zim, ZIMCallUserStateChangeInfo info, String callID) {
if (currentCallID == callID) {
// When Device A has accepted or rejected, close the popup box for call waiting operation.
}
});
}
...
}
1
String selfUserID = "user_id";
String currentCallID = "call_id";
ZIMCallAcceptConfig acceptConfig;
acceptConfig.extendedData = "extra_1";
ZIMCallRejectConfig rejectConfig;
rejectConfig.extendedData = "extra_1";
// Device A accepts the invitation
ZIM.getInstance()!.callAccept(currentCallID, acceptConfig).then((value) {
// Close the popup for call waiting operation.
}).catchError((onError){
// Handle exceptions
});
// Device A rejects the invitation
ZIM.getInstance()!.callReject(currentCallID, rejectConfig).then((value) {
// Close the popup for call waiting operation.
}).catchError((onError){
// Handle exceptions
});
// Device B listens to onCallUserStateChanged
ZIMEventHandler.onCallUserStateChanged = (ZIM zim, ZIMCallUserStateChangeInfo callUserStateChangeInfo, String callID){
if (currentCallID == callID) {
// Close the popup for call waiting operation when Device A has accepted or rejected.
}
};
1
NSString *selfUserId = @"user_id";
NSString *currentCallId = @"call_id";
ZIMCallAcceptConfig *acceptConfig = [[ZIMCallAcceptConfig alloc] init];
acceptConfig.extendedData = @"extra_1";
ZIMCallRejectConfig *rejectConfig = [[ZIMCallRejectConfig alloc] init];
rejectConfig.extendedData = @"extra_1";
// Device A accepts the invitation
[[ZIM getInstance] callAcceptWithCallID:currentCallId config:acceptConfig callback:^(NSString * _Nonnull callID, ZIMError * _Nonnull errorInfo) {
// Close the popup box for call waiting operation. Call ends.
}];
// Device A rejects the invitation
[[ZIM getInstance] callRejectWithCallID:currentCallId config:rejectConfig callback:^(NSString * _Nonnull callID, ZIMError * _Nonnull errorInfo) {
// Close the popup box for call waiting operation. Call ends.
}];
// Device B listens for callUserStateChanged
- (void)zim:(ZIM *)zim
callUserStateChanged:(ZIMCallUserStateChangeInfo *)info
callID:(NSString *)callID{
for(ZIMCallUserInfo *userInfo in info.callUserList){
if(userInfo.userID == selfUserId && (userInfo.state == ZIMCallUserStateAccepted || userInfo.state == ZIMCallUserStateRejected)){
// When Device A has accepted or rejected, close the popup box for call waiting operation.
}
}
}
}
1
const std::string self_user_id = "user_id";
const std::string current_call_id = "call_id";
zim::ZIMCallAcceptConfig accept_config;
accept_config.extendedData = "extra_1";
zim::ZIMCallRejectConfig reject_config;
reject_config.extendedData = "extra_1";
// Device A accepts the invitation
zim_->callAccept(current_call_id, accept_config, [=] (const std::string &callID, const ZIMError &errorInfo) {
// Close the popup box for call waiting operation. Call ends.
});
// Device A rejects the invitation
zim_->callReject(current_call_id, reject_config, [=] (const std::string &callID, const ZIMError &errorInfo) {
// Close the popup box for call waiting operation. Call ends.
}];
// Device B listens for callUserStateChanged
class zim_event_handler : public zim::ZIMEventHandler {
...
virtual void onCallUserStateChanged(zim::ZIM * /*zim*/,
const zim::ZIMCallUserStateChangeInfo &info,
const std::string &callID) override {
for (const auto &user_info : callUserList) {
if (user_info.userID == self_user_id &&
(user_info.state == zim::ZIMCallUserState::ZIM_CALL_USER_STATE_ACCEPTED ||
user_info.state == zim::ZIMCallUserState::ZIM_CALL_USER_STATE_REJECTED)) {
// When Device A has accepted or rejected, close the popup box for call waiting operation.
}
});
}
...
}
1
var selfUserID = '';
var curCallID = '';
// Device A accepts the invitation
zim.callAccept(curCallID, { extendedData: 'Device A accepts the invitation' }).then((res) => {
// Close the popup box for call waiting operation. Start stream publishing and playing, proceed with audio and video call.
});
// Device A rejects the invitation
zim.callReject(curCallID, { extendedData: 'Device A rejects the invitation' }).then((res) => {
// Close the popup box for call waiting operation. Call ends.
});
// Device B listens for callUserStateChanged
zim.on('callUserStateChanged', (zim, { callID, callUserList }) => {
if (curCallID == callID) {
callUserList.forEach((item) => {
if (item.userID == selfUserID && (item.state == 1 || item.state == 2)) {
// When Device A has accepted or rejected, close the popup box for call waiting operation.
}
});
}
});
1