公司内部分享—《EaseIM项目环信封装讲解》

提示:本文是方便以后自己查阅理解,不是知识讲解,涉及到公司关键项目,不方便贴出全部代码,看客可关闭该文章了,谢谢。

一、环信的理解

1.平台架构图理解

下图是从环信官方文档上弄出来的平台架构图片。
在这里插入图片描述
我个人对这张平台架构图的理解:客户端通过sdk调用环信的IM服务器,这里使用的是TCP socket通信方式,也就是IP+端口号进行连接的,我们的app后台与我们客户端也可以通过调用接口的形式(http通信)去与他们的REST服务器进行通信。

目前我们医生端app仅使用了sdk的方式与环信进行通信,剩下的就是通过接口的方式去请求我们自己的后端,间接地通过后台与环信进行通信。


下图是从环信官网上找到的登录流程图
在这里插入图片描述
环信登录有几种方式:
1、通过 用户 ID + 密码 登录
2、通过 用户 ID + token 登录
3、通过 用户 ID + agoraToken 登录(使用声网时用)
目前我们我们登录使用的是第一种方式,也就是用户 ID + 密码 登录,这是传统的登录方式。用户名和密码都是终端用户自行决定的。
登录时也是先去后台获取token与环信id,但不知道该token是否是环信给得token,我们登录时并未用到该token,仅在请求其他接口时传给后台校验是否登录而已。


2.环信接入理解

在这里插入图片描述
从环信的Demo中可以看到,它是集成了3个sdk,而我们只需要集成基础的那个sdk就可以使用即时通讯功能了。

implementation 'io.hyphenate:hyphenate-chat:3.8.3'

简单介绍一下EaseIMKit EaseCallKit
EaseIMKit 是 基于环信 IM SDK 的一款 UI 组件库,它提供了一些通用的 UI 组件,例如‘会话列表’、‘聊天界面’和‘联系人列表’等,开发者可根据实际业务需求通过该组件库快速地搭建自定义 IM 应用。
引入方式:

implementation 'io.hyphenate:ease-im-kit:xxx版本'

EaseCallKit 是一套基于环信 IM 和声网音视频结合开发的音视频 UI 库,实现了一对一语音和视频通话以及多人音视频通话的功能。基于 EaseCallKit 可以快速实现通用音视频功能。
引入方式:

implementation 'io.hyphenate:ease-call-kit:xxx版本'

3.数据库理解

在这里插入图片描述

从实际作战经验与环信api文档上可以得知,环信的消息存储是有两个数据库管理的,第一个数据库是本地数据库,第二个数据库是环信后台的数据库。

获取环信后台数据库里的消息,在环信那边称为消息漫游(这个是要花钱的),拉取后默认 SDK 会自动将消息更新到本地数据库中。

这里有两个坑:
第一:在第二次获取漫游时,要注意第一次已经把拉取过的消息存储在本地数据库中了,不能重复拉取,否则会重复存储在本地数据中,也就是说,环信本地数据库没有去重。
第二:在获取会话未读数,环信是通过本地数据库新增数量算的。所以漫游拉取的所有消息都是未读。如果重复拉取,那么会话未读数会持续增加。

目前医生端app的历史消息获取方式,是通过调用接口直接获取我们后台数据库中的消息。因为只拉取环信本地消息的话,在两个设备切换登录时会出现消息丢失的情况。如果只拉取环信漫游数据的话,就会出现本地数据库存在大量重复消息的情况,因为漫游数据会自动存到本地数据库中,这就会对本地数据库中的消息管理起来带来巨大的麻烦。
环信人员给的建议是,先获取本地数据库中的消息,如果没有了,再拉取漫游,但是这也存在丢消息的情况。而且漫游消息是有时间存储限制的。旗舰版90 天
所以综合考虑,直接通过调用接口获取我们后台数据库中的消息


提示:以下是环信封装代码层面上的讲解,仅贴出部分代码,非公司人员可绕行了。

二、EaseIM项目的架构理解

在这里插入图片描述
从上图可以看到,最主要的关键类就几个,会话实体类、消息实体类、初始化类、3个监听。其余的都是协助我实现封装的各种工具类,如:LiveDataBus工具类、环信sdk帮助类xxxHelper、震动响铃工具类、然后就是各种监听回调类了。
接下来会主要讲解几个关键类和目前使用到的环信sdk帮助类(IMMessageHelperIMObservableHelper)。

三、关键类讲解

1.HHEMClient

在这里插入图片描述

这个类是程序的入口,初始化操作和各个xxxHelper类的入口调用都在里面

目前外层调用:

 private static void initHX(Application application, String curProcessName) {
        if (TextUtils.equals(curProcessName, application.getPackageName())) {
            //环信appkey根据不同的环境切换
            EnvironmentConfig environmentConfig  = PropertyUtils.getDefault().getEnvironmentConfig();
            if (environmentConfig == null) {
                environmentConfig = EnvironmentConfig.RELEASE;
            }
            //设置环信推送
            EMPushConfig.Builder builder = new EMPushConfig.Builder(application);
            builder.enableMiPush("2882303761518122374", "5701812213374")
                    .enableVivoPush()
                    .enableOppoPush("b2f1fe8fb78a4774a3c9d21962426d30", "54835110d1d843228f25b6663a11d70b")
                    .enableHWPush(); 
            HHEMClient.getInstance().getOptions().setPushConfig(builder.build());
            //初始化
            HHEMClient.getInstance()
                    .addConnectionListener(new IMConnectStateListener())
                    .addMessageListener(new IMMessageListener())
                    .init(application, environmentConfig);
        }
    }

接下来看下HHEMClient类的初始化方法init()

//init方法是两个,一个直接传入appkey,一个使用EnvironmentConfig 类
 public boolean init(Context context, String appkey)
 //项目中使用的是下面这个方法,重点讲解一下
 public boolean init(Context context, EnvironmentConfig environmentConfig) {
        //判断是否是debug状态
        DebugHelper.syncIsDebug(context);
        // sp存储初始化,方便存储一些状态,比如登录状态
        IMSpUtil.init(context);
        //根据EnvironmentConfig 对象值获取不同的appkey
        options.setAppKey(DebugHelper.getAppKey(environmentConfig));
        //公共的初始化方法
        initCommen(context);
        return isSDKInit();
   }
   
   //公共的初始化方法
   private void initCommen(Context context){
        //初始化环信的sdk,传入了自定义参数options,所以在调用init之前需要把自定义参数设置在init方法前面
        //options参数可以设置很多东西,比如是否开启自动登录、设置推送信息等
        EMClient.getInstance().init(context, options);
        isSDKInit = true;
        //环信要求登录后需要调用下面两个方法,由于环信第二次是自动登录,所以需要在init方法中进行调用。
        //这个环信是建议写在程序的开屏页。目前医生端开屏页,登录后回调,初始化都会有这两个方法
        if (IMSpUtil.getInstance().isLogin()){
            EMClient.getInstance().chatManager().loadAllConversations();
            EMClient.getInstance().groupManager().loadAllGroups();
        }
        //初始化监听,如果外层有传自己的监听类,则使用自己的监听类,没有则使用封装里的类
        if (mConnectStateListener == null)
            mConnectStateListener = new IMMyConnectStateListener();
        if (mContactListener == null)
            mContactListener = new IMMyContactListener();
        if (msgListener == null)
            msgListener = new IMMyMessageListener();
        if (mGroupChangeListener == null)
            mGroupChangeListener = new IMMyGroupChangeListener();
        IMListenerHelper.build()
                .addConnectionListener(mConnectStateListener)//添加连接监听
                .addContactListener(mContactListener)//添加联系人监听
                .addMessageListener(msgListener)//添加公共消息监听
                .addIMGroupListener(mGroupChangeListener);//添加群监听
        //开启debug模式,方便线上问题出现,环信开启debug模式更方便查看消息情况。
        EMClient.getInstance().setDebugMode(true);
    } 

在这个类里面,还有几个方法,是获取帮助类的对象,也是调用帮助类各个方法的入口

   /**
     * 聊天帮助类对象
     * 调用: HHEMClient.getInstance().chatManager().xxxx()
     */
    public IMMessageHelper chatManager(){
        return IMMessageHelper.build();
    }

    /**
     * 离线消息帮助类对象
     * 调用: HHEMClient.getInstance().pushManager().xxxx()
     */
    public IMPushHelper pushManager(){
        return IMPushHelper.build();
    }

2.三个监听

在这里插入图片描述

1、使用

外层使用

//IMMessageListener 是自定义的类,直接继承 HHGlobalEMMessageListener 即可。
public class IMMessageListener extends HHGlobalEMMessageListener

医生端目前仅使用了HHGlobalEMMessageListener类,全局监听消息接收,用于处理响铃与透传消息的逻辑处理。
目前医生端也使用了HHGlobalEMConnectionListener类,仅使用了其互踢功能。

2、HHGlobalEMConnectionListener

这个类是监听环信服务器的连接状态。外层调用直接继承HHGlobalEMConnectionListener类即可

//它直接继承的环信连接接听
public abstract class HHGlobalEMConnectionListener implements EMConnectionListener {

    /**
     * 成功连接到Hyphenate IM服务器时触发
     */
    @Override
    public void onConnected() {
     //子类会复写该方法
        onConnect();
    }


    /**
     * 用来监听账号异常
     */
    @Override
    public void onDisconnected(int errorCode) {
        //用来监听登录异常退出的,
        String event = null;
        if (errorCode == EMError.USER_REMOVED) {
        //账户被删除
            event = IMConstant.ACCOUNT_REMOVED;
        } else if (errorCode == EMError.USER_LOGIN_ANOTHER_DEVICE) {
        //账户在另外一台设备登录
            event = IMConstant.ACCOUNT_CONFLICT;
        } else if (errorCode == EMError.SERVER_SERVICE_RESTRICTED) {
        //IM功能限制
            event = IMConstant.ACCOUNT_FORBIDDEN;
        } else if (errorCode == EMError.USER_KICKED_BY_CHANGE_PASSWORD) {
        //用户修改密码
            event = IMConstant.ACCOUNT_KICKED_BY_CHANGE_PASSWORD;
        } else if (errorCode == EMError.USER_KICKED_BY_OTHER_DEVICE) {
        //用户被其他设备踢掉
            event = IMConstant.ACCOUNT_KICKED_BY_OTHER_DEVICE;
        }
        //只要event有值,则发送LiveDataBus消息,IMObservableHelper类中会进行接收该消息
        //外层只需要调用IMObservableHelper类中的回调方法去自定义相关操作即可
        if(!TextUtils.isEmpty(event)) {
            LiveDataBus.get().with(IMConstant.ACCOUNT_CHANGE).postValue(IMEaseEvent.create(event, IMEaseEvent.TYPE.ACCOUNT));
        }
        //子类会复写该方法
        onDisconnect(errorCode);
    }

    /**
     * 成功连接到Hyphenate IM服务器时触发
     */
    public abstract void onConnect();

    /**
     * 用来处理连接异常
     */
    public abstract void onDisconnect(int errorCode);
}

HHGlobalEMConnectionListener 类中使用了LiveDataBus发送消息,当账号异常登录时会发出账号异常的消息,app接收到该消息后做相关操作。目前医生端会弹出下面的提示:
在这里插入图片描述
医生端app中目前的使用:

        //这是写在DBActivity中的,统一的互踢功能
        //IMObservableHelper是专门来接收LiveDataBus发送的消息的,后面会专门介绍
        IMObservableHelper.build().addAccountObservable(this, event -> {
            GeneralReqExceptionProcess.checkCode(this,
                    "1", "用户信息验证失败,请重新登录");
        });

3、HHGlobalEMMessageListener

环信的消息监听,是不区分某个群或某一个人的,无论是否是该群的消息,只要有消息过来,都会回调环信的EMMessageListener消息监听,但在封装中我进行了区分,拆成了不区分群的HHGlobalEMMessageListener类(用于响铃与传递公共消息)与区分群的HHLocalEMMessageListener类(用于单个聊天页面里的消息接收)

这个类是用于监听全局消息的,不区分某个会话,外层调用直接继承HHGlobalEMMessageListener类即可

public abstract class HHGlobalEMMessageListener implements EMMessageListener {

    @Override
    public void onMessageReceived(List<EMMessage> messages) {
        //收到消息
        //环信的回调都是在线程中,所以回调给外层时,我进行了线程转换,转换到了主线程,在app中回调后的操作都是在主线程进行的
        IMThreadManager.getInstance().runOnMainThread(() -> {
        //这里我是进行了消息类型转换,把环信EMMessage转换为封装的HHEMMessage类型
            List<HHEMMessage> imMessages = new ArrayList<>();
            for (EMMessage message : messages) {
                imMessages.add(HHEMMessage.createMessage(message));
            }
            //这里发送了一个LiveDataBus,主要是为了在首页有消息过来时,刷新首页消息列表而做,IMObservableHelper类中会进行接收该消息
            //外层只需要调用IMObservableHelper类中的回调方法去自定义相关操作即可
            LiveDataBus.get().with(IMConstant.MESSAGE_CHANGE).postValue(
                    IMEaseEvent.create(IMConstant.MESSAGE_CHANGE, IMEaseEvent.TYPE.MESSAGE,imMessages,true));
            //子类会复写该方法,传递到外层是已经封装好的HHEMMessage类型
            messageReceived(imMessages);
        });

    }

    @Override
    public void onCmdMessageReceived(List<EMMessage> messages) {
        //收到透传消息
        IMThreadManager.getInstance().runOnMainThread(() -> {
         //这里我是进行了消息类型转换,把环信EMMessage转换为封装的HHEMMessage类型
            List<HHEMMessage> imMessages = new ArrayList<>();
            for (EMMessage message : messages) {
                imMessages.add(HHEMMessage.createMessage(message));
            }
            //子类会复写该方法,传递到外层是已经封装好的HHEMMessage类型
            cmdMessageReceived(imMessages);
        });

    }

    @Override
    public void onMessageRead(List<EMMessage> messages) {...}

    @Override
    public void onMessageDelivered(List<EMMessage> messages) {...}

    @Override
    public void onMessageRecalled(List<EMMessage> messages) {
        //消息被撤回
        IMThreadManager.getInstance().runOnMainThread(() -> {
        //这里我是进行了消息类型转换,把环信EMMessage转换为封装的HHEMMessage类型
            List<HHEMMessage> imMessages = new ArrayList<>();
            for (EMMessage message : messages) {
                imMessages.add(HHEMMessage.createMessage(message));
            }
            //这里发送了一个LiveDataBus,主要是为了在首页有消息撤回时做相关操作,IMObservableHelper类中会进行接收该消息
            //外层只需要调用IMObservableHelper类中的回调方法去自定义相关操作即可
            LiveDataBus.get().with(IMConstant.MESSAGE_RECALL).postValue(
                    IMEaseEvent.create(IMConstant.MESSAGE_RECALL, IMEaseEvent.TYPE.MESSAGE_RECALL, imMessages,true));
           //子类会复写该方法,传递到外层是已经封装好的HHEMMessage类型
            messageRecalled(imMessages);
        });

    }

    @Override
    public void onMessageChanged(EMMessage message, Object change) {...}

    public abstract void messageReceived(List<HHEMMessage> messages);
    public abstract void cmdMessageReceived(List<HHEMMessage> messages);
    public abstract void messageRead(List<HHEMMessage> messages);
    public abstract void messageDelivered(List<HHEMMessage> messages);
    public abstract void messageRecalled(List<HHEMMessage> messages);
    public abstract void messageChanged(HHEMMessage message, Object change);

}

4、HHLocalEMMessageListener

这个类是用于监听某一个会话的消息的,与HHGlobalEMMessageListener区别是它会区分groupId,非该groupId的不会传递消息。外层调用直接new该类即可,该类与其他监听不太一样,这个不是抽象类而是实体类,直接new出来一个对象即可,需要传入会话id与回调对象。

public class HHLocalEMMessageListener implements EMMessageListener {

    /**
     * 会话id,可能是对方环信id,也可能是群id或者聊天室id
     */
    private String conversationId = "";
    //方法回调
    private OnListener mListener;

    public HHLocalEMMessageListener(String conversationId, OnListener onListener) {
        this.conversationId = conversationId;
        mListener = onListener;
    }

    @Override
    public void onMessageReceived(List<EMMessage> messages) {
        //收到消息
        //把所有当前conversationId 中的所有消息汇集到imMessages中
        List<HHEMMessage> imMessages = isCurrentMessage(messages);
        if (imMessages != null && imMessages.size() != 0){
            IMThreadManager.getInstance().runOnMainThread(() -> {
            //子类会复写该方法,传递到外层是已经封装好的HHEMMessage类型
                mListener.messageReceived(imMessages);
            });
        }
    }

    @Override
    public void onCmdMessageReceived(List<EMMessage> messages) {...}

    @Override
    public void onMessageRead(List<EMMessage> messages) {...}

    @Override
    public void onMessageDelivered(List<EMMessage> messages) {...}

    @Override
    public void onMessageRecalled(List<EMMessage> messages) {...}

    @Override
    public void onMessageChanged(EMMessage message, Object change) {...}

    //用于判断是否是当前消息
    private List<HHEMMessage> isCurrentMessage(List<EMMessage> messages){
        List<HHEMMessage> imMessages = new ArrayList<>();
        for (EMMessage message : messages) {
            //会话id
            String username = null;
            if (message.getChatType() == EMMessage.ChatType.GroupChat || message.getChatType() == EMMessage.ChatType.ChatRoom) {
                //如果消息类型是群聊或者聊天室,会话id则是消息的接收者id
                username = message.getTo();
            } else {
                //如果是单聊,会话id则是消息的发送者id
                username = message.getFrom();
            }
            //如果消息是该会话的消息,则存入imMessages数组中
            if (username.equals(conversationId) || message.getTo().equals(conversationId) || message.conversationId().equals(conversationId)) {
                imMessages.add(HHEMMessage.createMessage(message));
            }
        }
        //返回目标数组
        return imMessages;
    }

    public interface OnListener{
        public abstract void messageReceived(List<HHEMMessage> messages);
        public abstract void cmdMessageReceived(List<HHEMMessage> messages);
        public abstract void messageRead(List<HHEMMessage> messages);
        public abstract void messageDelivered(List<HHEMMessage> messages);
        public abstract void messageRecalled(List<HHEMMessage> messages);
        public abstract void messageChanged(HHEMMessage message, Object change);
    }

}

3.xxxHelper帮助类的介绍

在这里插入图片描述

这里将重点介绍IMObservableHelper类与IMMessageHelper类。目前医生端使用最多也是最关键的两个类。
其余的帮助类和IMMessageHelper类是一样的编写逻辑,仅介绍IMMessageHelper类即可。

1、IMObservableHelper

这个类是专门接收LiveDataBus事件的。外层只需要调用IMObservableHelper类中的各个方法去自定义相关操作即可

public class IMObservableHelper {

    private IMObservableHelper() {
    }

    private static class SingleObservable {
        private static final IMObservableHelper OBSERVABLE_HELPER = new IMObservableHelper();
    }

    public static IMObservableHelper build() {
        return IMObservableHelper.SingleObservable.OBSERVABLE_HELPER;
    }
    
    //全局账号异常退出监听(目前使用在医生端DBActivity中)
    public void addAccountObservable(LifecycleOwner owner, IMAccountExitInterface exitInterface) {
        LiveDataBus.get().with(IMConstant.ACCOUNT_CHANGE, IMEaseEvent.class)
                .observe(owner, event -> {
                    if (event == null) {
                        return;
                    }
                    if (!event.isAccountChange()) {
                        return;
                    }
                    //编写异常退出逻辑
                    IMThreadManager.getInstance().runOnMainThread(() -> {
                        if (exitInterface != null)
                        //回调
                        exitInterface.AccountExitFormError(event);
                    });
                });
    }

    //接收到消息后处理刷新操作(用于具体页面接收消息后处理相关业务逻辑)
    //您也可以直接继承HHGlobalEMMessageListener类实现全局监听消息然后自己分发到各个页面
    //我只是帮助您做了分发操作而已
    public void addConversationObservable(LifecycleOwner owner ,  IMConversationInterface imConversationInterface){
        LiveDataBus.get().with(IMConstant.MESSAGE_CHANGE,  IMEaseEvent.class)
                .observe(owner, event ->{
                    if (event == null) {
                        return;
                    }
                    if (!event.isMessageChange()) {
                        return;
                    }
                    IMThreadManager.getInstance().runOnMainThread(() -> {
                        if (imConversationInterface != null)
                            imConversationInterface.onRefreshList(event);
                    });
                });
    }

    //接收到消息撤回后的处理刷新操作(目前使用在医生端HomeFragment中,用于实现首页显示撤回一条消息的相关操作)
    public void messageRecallObservable(LifecycleOwner owner ,  IMConversationInterface imConversationInterface){
        LiveDataBus.get().with(IMConstant.MESSAGE_RECALL,  IMEaseEvent.class)
                .observe(owner, event ->{
                    if (event == null) {
                        return;
                    }
                    if (!event.isMessageRecall()) {
                        return;
                    }
                    IMThreadManager.getInstance().runOnMainThread(() -> {
                        if (imConversationInterface != null)
                            imConversationInterface.onRefreshList(event);
                    });
                });
    }
}

外层调用:

 //账号异常退出回调
 IMObservableHelper.build().addAccountObservable(this, event -> { ...});
 //接收消息后回调
 IMObservableHelper.build().addConversationObservable(this, imEaseEvent -> { ...});
 //消息撤回后回调
 IMObservableHelper.build().messageRecallObservable(this, event -> { ...});

2、IMMessageHelper

这个类是发送消息、获取历史消息、撤回某个消息等相关消息功能,除此之外,该类也包含获取某个会话的未读数、获取会话列表、删除某个会话等相关会话功能。
对于会话和消息的所有相关操作,都在这里类里。
该类的编写逻辑,仅是嵌套一层环信的方法而已,每个方法内的实现都是环信api的具体实现代码,但做了消息实体类和会话实体类的转换。

public class IMMessageHelper {
    private IMMessageHelper() { }
    private static class Single {
        private static final IMMessageHelper MESSAGE_HELPER = new IMMessageHelper();
    }
    public static IMMessageHelper build() {
        return IMMessageHelper.Single.MESSAGE_HELPER;
    }
   /**
     * 发送文本消息
     * @param content 内容
     * @param groupId 群id
     * @param messageListener 发送成功与否回调
     */
    public HHEMMessage sendMessage(String content , String groupId , IMSendMessageListener messageListener){
         //创建一个HHEMMessage消息实体类对象
         HHEMMessage message = HHEMMessage.createMessage(content, groupId);
         //发送状态回调
         message.getMessage().setMessageStatusCallback(new EMCallBack() {
            @Override
            public void onSuccess() {
                IMThreadManager.getInstance().runOnMainThread(() -> {
                    messageListener.onSuccess(message);
                });
            }

            @Override
            public void onError(int i, String s) {
                IMThreadManager.getInstance().runOnMainThread(() -> {
                    messageListener.onFile(message,i, s);
                });
            }

            @Override
            public void onProgress(int i, String s) {}
        });
        //发送消息
        EMClient.getInstance().chatManager().sendMessage(message.getMessage());
        //返回的也是自己封装的实体类HHEMMessage
        return message;
    }
    
    //这个方法的区别,仅是没有回调而已,目前医生端并未使用这种方法
    public static HHEMMessage sendMessage(String content , String groupId){...}

    ...
    //其他发送消息的方法与文本消息一样的,就不过多阐述了。
    //每个类型的消息都有两种方式,一个有回调,一个没回调,目前医生端都是使用有回调的发送方法
    ...
    
    //获取所有消息的未读数量
    //这里可以看到,书写方式就是我自己的方法嵌套了环信api。该类几乎所有方法都是类似这种,没有做其他相关操作
    public int getUnRedNum(){
        return EMClient.getInstance().chatManager().getUnreadMessageCount();
    }
    ...

    //获取所有会话列表,在这个方法里与直接嵌套api有点不一样,不过核心想法还是一致的,只是做了简单的数据转换和处理而已
    public List<HHEMConversation> getAllConversation(){
       //这里是调用环信api获得的map对象
        Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
        //对map对象进行了循环,把实体类EMConversation变成了自己封装的HHEMConversation
        List<HHEMConversation> bean = new ArrayList<>();
        for (EMConversation value : conversations.values()) {
            bean.add(HHEMConversation.createConversation(value));
        }
        //直接返回一个封装好的HHEMConversation的list数据
        return bean;
    }
}

IMMessageHelper类是比较有代表性的,在封装项目中,有很多xxxHelper类,而他们的编写想法与IMMessageHelper类是一致的——就是我自己的方法嵌套了一层环信api。

如果后期要更换第三方,可以把每个方法里的实现改为目标第三方的实现api即可。外层调用的方法名不变。


4.实体类

在这里插入图片描述
目前医生端仅用到HHEMConversation实体类与HHEMMessage类。

1、HHEMMessage

这个是封装的消息实体类,外层调用到的消息实体类就是HHEMMessage类,环信的消息实体类EMMessage,外层无法获得EMMessage

public class HHEMMessage {
    //环信的消息体
    protected EMMessage message;

    protected String content = "";//文案

    protected Map<String,String> param = new HashMap<>();//自定义消息中的自定义参数

    protected String photoHttpUrl = "";//网络图片
    protected String photoLocalUri = "";//本地图片

    protected String voiceLocalUri = "";//语音本地资源
    protected String voiceRemoteUrl = "";//语音远程资源
    protected long voiceDuration = 0;//语音时间
    protected boolean isVoiceListened = false;//语音是否已听

    protected String videoRemoteUrl = "";//视频远程资源
    protected String videoLocalUri = "";//视频本地资源

    protected String groupId = "";//群id
    protected String userId = "";//发送者环信id
    protected long messageTime = 0;//消息发送时间
    protected int messageType = HHMessageType.UNKNOWN;//消息的type 默认是未知消息
    protected String messageId = "";//消息id
    //---------------------以下是环信不知道的-------------------------/
    protected String userHeadUrl = "";//发送者头像 默认没有
    protected String userName = "";//发送者名字 默认没有
    protected String userOldId = "";//以前的老id
    protected String sendStatus = "0";//发送状态 1成功 -1失败  0发送中
    protected String messageStatus = "0";//是否撤回 0否 -1是
    protected long messageRecallTime = 0;//消息撤回的时间
    
    //获取远程的视频链接(想法:优先获取环信消息体中的远程链接,环信消息体为null,获取不到则根据set值)
    public String getVideoRemoteUrl() {
        if (message != null && message.getType() == VIDEO){
            EMVideoMessageBody videoBody = (EMVideoMessageBody) message.getBody();
            //获取视频文件在服务器的路径
            if (!TextUtils.isEmpty(videoBody.getRemoteUrl()))
            //与小程序那边调试发现,小程序上传的视频链接偶尔是http的,在我们医生端会加载失败
                if (videoBody.getRemoteUrl().startsWith("http:")){
                    videoRemoteUrl = videoBody.getRemoteUrl().replace("http","https");
                }else {
                    videoRemoteUrl = videoBody.getRemoteUrl();
                }
        }
        return videoRemoteUrl;
    }

    //获取图片的远程链接(想法:优先获取环信消息体中的远程链接,环信消息体为null,获取不到则根据set值)
    public String getPhotoHttpUri() {
        if (message != null && message.getType() == IMAGE){
            EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();
            if (!TextUtils.isEmpty(imgBody.getRemoteUrl()))
                photoHttpUrl = imgBody.getRemoteUrl();
        }
        return photoHttpUrl;
    }

   //获取语音的远程链接(想法同上)
    public String getVoiceRemoteUrl() {
        if (message != null && message.getType() == VOICE){
            EMVoiceMessageBody imgBody = (EMVoiceMessageBody) message.getBody();
            if (!TextUtils.isEmpty(imgBody.getRemoteUrl()))
                voiceRemoteUrl  = imgBody.getRemoteUrl();
        }
        return voiceRemoteUrl;
    }

    //获取语音的语音时长(想法同上)
    public long getVoiceDuration() {
        if (message != null && message.getType() == VOICE){
            EMVoiceMessageBody imgBody = (EMVoiceMessageBody) message.getBody();
            if (imgBody.getLength() != 0)
                voiceDuration  = imgBody.getLength();
        }
        return voiceDuration;
    }
 
    //获取语音是否已听(想法同上)
    public boolean isVoiceListened(){
        if (message != null && message.getType() == VOICE){
            isVoiceListened = message.isListened();
        }
        return isVoiceListened;
    }

    ....
    语音、视频、图片的本地链接,就是正常是get与set方法,环信消息体内也会存本地链接的,但是环信消息体内的本地链接
    是发送者的本地链接。我这里的本地链接是医生端当事人的本地,所以按set的值是多少,get的值就是多少,默认空字符串。
    ....

   //获取环信消息体,这里是protected修饰,外层无法调用,仅有相同包路径下可调用
    protected EMMessage getMessage() {
        return message;
    }

    //判断环信消息体是否为null  true为null  false不为null
    public boolean isMessageNull(){
        return message == null;
    }

    //设置环信消息体。这在获取历史语音消息时有用到,因为历史语音消息是从服务端获取的,获取到历史语音消息后,
    //会根据消息id去本地数据库中查找环信语音消息体原型,然后赋值过来。目的是获取语音的已读与未读功能,还有消息撤回时也有用到。
    public void setMessage(HHEMMessage message) {
        if (message != null){
            this.message = message.getMessage();
        }else {
            this.message = null;
        }
    }
   
    //本类的构造方法
    private HHEMMessage(EMMessage message) {
        this.message = message;
    }
    private HHEMMessage() {}

   //获取发送者的环信id(想法:优先获取环信消息体中的发送者id,无法获得则根据set什么值就什么值)
    public String getUserId() {
        if (message != null){
            userId = message.getFrom();
        }
        return userId;
    }
    
    //获取接收者的环信id,我们都是群聊,接收者都是该群,所以也是群id(想法同上)
    public String getGroupId() {
        if (message != null){
            groupId = message.getTo();
        }
        return groupId;
    }

    //获取消息时间(想法同上)
    public long getTime() {
        if (message != null){
            messageTime = message.getMsgTime();
        }
        return messageTime;
    }

    //给环信设置消息时间,这在第一次进入app时,初始化原始数据时需要使用到
    public void setMsgTime(long msgTime) {
        if (message != null){
            message.setMsgTime(msgTime);
        }
        messageTime = msgTime;
    }
    //设置消息时间,和上面的区别是不会设置环信的消息时间
    public void setMessageTime(long messageTime) {
        this.messageTime = messageTime;
    }

   //获取消息类型(想法:优先从环信获取消息类型,获取不到则根据set值)
   //未知消息在外层会显示让用户去升级app
    public int getType(){
        if (message != null){
            if (message.getType() == TXT) {
                return HHMessageType.TEXT;
            }else if (message.getType() == IMAGE){
                return HHMessageType.PHOTO;
            }else if (message.getType() == VOICE){
                return HHMessageType.VOICE;
            }else if (message.getType() == VIDEO){
                return HHMessageType.MOVIE;
            }else if (message.getType() == CUSTOM){
                EMCustomMessageBody customMessageBody = (EMCustomMessageBody) message.getBody();
                if (!TextUtils.isEmpty(customMessageBody.event()) && !customMessageBody.event().contains("null")){
                    BigDecimal decimal = new BigDecimal(customMessageBody.event());
                    return decimal.intValue();
                }else {
                    //获取不到自定义消息的event字段,则为未知消息
                    return HHMessageType.UNKNOWN;
                }
            }else if (message.getType() == CMD){
                EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody) message.getBody();
                if (!TextUtils.isEmpty(cmdMessageBody.action()) && !cmdMessageBody.action().contains("null")){
                    BigDecimal decimal = new BigDecimal(cmdMessageBody.action());
                    return decimal.intValue();
                }else {
                    //获取不到透传消息的action字段,则为未知消息
                    return HHMessageType.UNKNOWN;
                }
            }else {
                //除了文本、语音、图片、视频、自定义、透传消息外,其余的其他消息为未知消息
                return HHMessageType.UNKNOWN;
            }
        }else {
            //环信消息体为null,则根据set值多少就多少
            return messageType;
        }
    }
 
    //这个是基础消息的文本内容(不包含自定义消息与透传消息),一般用在首页会话列表的显示
    public String getContent() {
        if (message != null){
            if (message.getType() == TXT) {
                EMTextMessageBody txtBody = (EMTextMessageBody) message.getBody();
                content = txtBody.getMessage();
            }else if (message.getType() == IMAGE){
                content = "[图片]";
            }else if (message.getType() == VOICE){
                content = "[语音]";
            }else if (message.getType() == VIDEO){
                content = "[小视频]";
            }
        }
        return content;
    }

    //这个是自定义字段,获取透传消息与自定义消息的自定义字段。(这个想法与其他有所区别,这个是优先获取set的值,否则再获取环信中的值,
    //这是因为获取历史消息是从服务端获取的,需要通过解析接口中的自定义字段赋值进来,所以以set值为主)
    public Map<String, String> getParam() {
        if (param.size() == 0 && message != null ){
            if (message.getType() == CUSTOM){
                EMCustomMessageBody customMessageBody = (EMCustomMessageBody) message.getBody();
                param = customMessageBody.getParams();
            }else if (message.getType() == CMD){
                EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody) message.getBody();
                param = cmdMessageBody.getParams();
            }
        }
        return param;
    }

    public String getId(){
        if (message != null){
            messageId = message.getMsgId();
        }
        return messageId;
    }

    //设置语音已听(由于获取是否已听是优先根据环信消息体中是否已听来的,无法获取再根据set值来。所以设置已听优先设置环信中的消息已听字段,
    //无法设置时再直接改变isVoiceListened 等于true即可)
    public void setVoiceMessageListened(){
        if (message != null){
            EMMessage.ChatType chatType = message.getChatType();
            if (!message.isAcked() && chatType == EMMessage.ChatType.Chat) {
                try {
                    EMClient.getInstance().chatManager().ackMessageRead(message.getFrom(), message.getMsgId());
                } catch (HyphenateException e) {
                    e.printStackTrace();
                }
            }
            if (!message.isListened()) {
                EMClient.getInstance().chatManager().setVoiceMessageListened(message);
            }
        }else {
            isVoiceListened = true;
        }
    }

   //创建消息,直接传入EMMessage
    public static HHEMMessage createMessage(EMMessage message) {
        setExt(message);
        HHEMMessage imMessage = new HHEMMessage(message);
        return imMessage;
    }

 //创建一条文本消息,content为消息文字内容,groupId为对方用户或者群聊的id,后文皆是如此
    public static HHEMMessage createMessage(String content , String groupId){
        //环信创建一个文本发送消息
        EMMessage message = EMMessage.createTxtSendMessage(content, groupId);
        //消息的聊天类型
        message.setChatType(EMMessage.ChatType.GroupChat);
        //设置消息的额外字段(用于桌面角标)
        setExt(message);
        //新建本类对象
        HHEMMessage imMessage = new HHEMMessage(message);
        //返回本类对象
        return imMessage;
    }

   ....
   创建语音、图片消息与文本消息一样,则不再阐述
   ....

   //创建一条自定义消息
    public static HHEMMessage createMessage(int type, Map<String, String> params, String groupId) {
        //创建一条自定义发送消息
        EMMessage message = EMMessage.createSendMessage(CUSTOM);
        //event为需要传递的自定义消息事件,比如礼物消息,可以设置event = "gift"
        EMCustomMessageBody customBody = new EMCustomMessageBody(type + "");
        //自定义参数params
        customBody.setParams(params);
        message.addBody(customBody);
        //to指另一方环信id(或者群组id,聊天室id)
        message.setTo(groupId);
        // 如果是群聊,设置chattype,默认是单聊
        message.setChatType(EMMessage.ChatType.GroupChat);
        //设置消息的额外字段(用于桌面角标)
        setExt(message);
         //新建本类对象
        HHEMMessage imMessage = new HHEMMessage(message);
         //返回本类对象
        return imMessage;
    }

    //设置消息的额外字段(用于桌面角标)
    private static void setExt(EMMessage message){
        // 设置自定义推送提示
        JSONObject extObject = new JSONObject();
        try {
            extObject.put("em_huawei_push_badge_class", "com.naiterui.ehp.activity.LoadActivity");
            extObject.put("em_push_mutable_content", true);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        // 将推送扩展设置到消息中
        message.setAttribute("em_apns_ext", extObject);
    }

    //创建一条消息,该消息的EMMessage为null,目前用于从服务器获取历史聊天数据后根据数据生成一条消息
    //历史聊天记录里的消息,不能使用createReceiveMessage与createSendMessage生成,因为那两个方法都是生成新消息
    //历史聊天记录里的消息都已经是本地内存中的消息了,所以正确的方式应该是寻找目标环信消息。
    public static HHEMMessage createHistoryMessage(String userId, String groupId , int type , long time , String messageId){
        HHEMMessage imMessage = new HHEMMessage();
        imMessage.setUserId(userId);
        imMessage.setGroupId(groupId);
        imMessage.setMessageType(type);
        imMessage.setMessageTime(time);
        imMessage.setMessageId(messageId);
        return imMessage;
    }

    //创建一条接收到的消息,EMMessage的id会自动生成,目前用于首次进入app需要恢复老数据的时候
    public static HHEMMessage createReceiveMessage(String content ,String groupId){
        //与createSendMessage的区别在于,createSendMessage消息用于发送,createReceiveMessage消息是已经接收到的
        //这种消息需要手动插入环信本地数据库中,插入后不会产生未读数
        //createSendMessage是用于发送的消息,需要调用环信的发送api,发送后接收方会有一条未读。
        //两个的共同点是:都会生成新的消息
        EMMessage emMessage = EMMessage.createReceiveMessage(EMMessage.Type.TXT);
        emMessage.setFrom(groupId);
        emMessage.setStatus(EMMessage.Status.SUCCESS);
        emMessage.addBody(new EMTextMessageBody(content));
        HHEMMessage imMessage = new HHEMMessage(emMessage);
        return imMessage;
    }

    /**
     * 撤回消息功能
     * 消息撤回时限默认2分钟,可根据开发者需求以AppKey为单位进行单独设置,如需修改请联系环信商务。
     * 返回成功true或者失败false
     * 由于想写成: message.recallMessage(),所以该方法是放在了HHEMMessage中
     * 但是根据最初原则以环信的书写方式封装,所以在IMMessageHelper中也写了recallMessage()方法,调用方式:HHEMClient.getInstance().chatManager().recallMessage(message)
     */
    public Boolean recallMessage(){
        try {
            EMClient.getInstance().chatManager().recallMessage(message);
            return true;
        } catch (HyphenateException e) {
            e.printStackTrace();
            Log.e("HHEMMessage_mxt", e.getErrorCode() + ":" + e.getDescription());
            return false;
        }
    }

}

2、HHEMConversation

这个是封装的会话实体类,外层调用到的会话实体类就是HHEMConversation类,环信的会话实体类EMConversation,外层无法获得EMConversation

public class HHEMConversation {

    //环信的会话对象
    private EMConversation mConversation;
    
    //得到环信的会话对象,protected修饰,外层无法调用
    protected EMConversation getConversation() {
        return mConversation;
    }

    /**
     * 判断是否为null
     * @return true为null  false不为null
     */
    public boolean isConversationNull(){
        return mConversation == null;
    }
  
    //带参数的构造方法
    private HHEMConversation(EMConversation conversation) {
        mConversation = conversation;
    }

   //根据groupId去环信本地数据库中寻找该会话,如果找不到,则为null
    public static HHEMConversation createConversation(String groupId){
        EMConversation mConversation = EMClient.getInstance().chatManager().getConversation(groupId);
        HHEMConversation hhemConversation = new HHEMConversation(mConversation);
        return hhemConversation;
    }

   //根据groupId去环信本地数据库中寻找该会话,如果找不到,则新创建一个会话。
   //目前仅用于首次进入app需要恢复首页会话列表的时候,因为创建会话会在首页会话列表多出来一条会话记录。
    public static HHEMConversation createConversation(String groupId ,  boolean createIfNotExists){
        EMConversation mConversation = EMClient.getInstance().chatManager().getConversation(groupId, EMConversation.EMConversationType.GroupChat,createIfNotExists);
        HHEMConversation hhemConversation = new HHEMConversation(mConversation);
        return hhemConversation;
    }

    public static HHEMConversation createConversation(EMConversation conversation){
        HHEMConversation hhemConversation = new HHEMConversation(conversation);
        return hhemConversation;
    }

    /**
     * 指定会话消息未读数清零
     */
    public void markAllMessagesAsRead(){
        if (mConversation != null){
            mConversation.markAllMessagesAsRead();
        }
    }

    /**
     * 获取指定会话未读消息数量
     */
    public int getUnreadMsgCount(){
        int num = 0;
        if (mConversation != null){
            num = mConversation.getUnreadMsgCount();
        }
        return num;
    }

    /**
     * 获取会话里的最后一条消息
     * @return
     */
    public HHEMMessage getLastMessage(){
        if (mConversation != null && mConversation.getLastMessage() != null){
            return HHEMMessage.createMessage(mConversation.getLastMessage());
        }else {
            return null;
        }
    }

    /**
     * 会话id
     * @return
     */
    public String getId(){
        if (mConversation != null){
            return mConversation.conversationId();
        }
        return "";
    }

    /**
     * 把一条消息置为已读
     */
    public void markMessageAsRead(String messageId){
        //把一条消息置为已读
        if (mConversation != null)
            mConversation.markMessageAsRead(messageId);
    }

    /**
     * 根据msgid获取消息
     * @param messageId 需要获取的消息id
     * @param markAsRead 是否获取消息的同时标记消息为已读
     * @return 获取到的message实例
     */
    public HHEMMessage getMessage(String messageId, boolean markAsRead){
        if (mConversation != null && mConversation.getMessage(messageId, markAsRead) != null){
            return HHEMMessage.createMessage(mConversation.getMessage(messageId, markAsRead));
        }else {
            return null;
        }
    }

    /**
     * 插入一条消息,消息的conversationId应该和会话的conversationId一致,消息会被插入DB,并且更新会话的latestMessage等属性
     * @param msg
     * @return
     */
    public boolean insertMessage(HHEMMessage msg){
        if (mConversation != null){
            return mConversation.insertMessage(msg.getMessage());
        }
        return false;
    }

    /**
     * 获取用户可以自行定义会话扩展字段
     * 该字段只保存在本地,不进行网络同步
     * @return  会话对应扩展字段的内容
     */
    public String getExtField(){
        if (mConversation != null){
            return mConversation.getExtField();
        }
        return "";
    }

    /**
     * 用户可以自行定义会话扩展字段,该字段只保存在本地,不进行网络同步
     * @param ext 会话对应扩展字段的内容
     */
     //目前用于首页会话列表那里辅助实现空消息的筛查以及快速显示本地数据的逻辑
    public void setExtField(String ext){
        if (mConversation != null)
            mConversation.setExtField(ext);
    }

}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值