环信 android

环信 android

7
回复

收集基于环信SDK开发的开源项目 开源项目

回眸淡然笑 回复了问题 • 9 人关注 • 4025 次浏览 • 2017-12-03 00:50 • 来自相关话题

11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9077 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 8038 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
3
回复

调用 EaseUI.getInstance().init(this, options); 初始化报错 删掉就好了。 环信 android

baoshu 回复了问题 • 4 人关注 • 394 次浏览 • 2017-12-12 10:30 • 来自相关话题

0
回复

背景:Android 设置自动同意好友申请,添加好友 !登陆失败! 环信 android 添加好友 登录失败

回复

Mister_Li 发起了问题 • 0 人关注 • 94 次浏览 • 2017-11-16 15:35 • 来自相关话题

1
回复

环信创建EMConversation 环信 android

geri_yang 回复了问题 • 2 人关注 • 282 次浏览 • 2017-09-04 14:56 • 来自相关话题

1
回复

环信android demo,更改为自己的appKey之后连接聊天服务器失败 demo 环信 android

geri_yang 回复了问题 • 2 人关注 • 286 次浏览 • 2017-08-15 12:33 • 来自相关话题

1
回复
2
回复

android中环信支持 视频群聊吗?我们可以通过环信查看用户视频的视频记录或者语音记录并播放吗? 环信 android

geri_yang 回复了问题 • 2 人关注 • 294 次浏览 • 2017-08-10 15:33 • 来自相关话题

1
回复

Android 群聊显示为单聊 环信 android

pakrs12341 回复了问题 • 2 人关注 • 455 次浏览 • 2017-08-08 17:42 • 来自相关话题

1
回复

android中环信支持 视频群聊吗?我们可以通过环信查看用户视频的视频记录或者语音记录并播放吗? 环信 android

geri_yang 回复了问题 • 2 人关注 • 270 次浏览 • 2017-07-26 19:26 • 来自相关话题

1
回复

安卓环信的会话列表怎么实时监听消息,实时更新 环信 android

lzan13 回复了问题 • 2 人关注 • 536 次浏览 • 2017-06-07 15:52 • 来自相关话题

0
回复

您好 按照你的视频步骤集成EaseUi 时 compileDebugJavaWithJavac 环信 android

回复

Lee_Mofeel。 发起了问题 • 1 人关注 • 412 次浏览 • 2017-06-05 15:16 • 来自相关话题

3
回复

环信集成第三方推送唤醒apk 华信 环信小米 环信华为 环信 android APP自启

baoshu 回复了问题 • 2 人关注 • 346 次浏览 • 2017-05-26 10:23 • 来自相关话题

1
最佳

环信初始化报错:Could not find class 'com.hyphenate.chat.EMCallManager' 环信 android

lzan13 回复了问题 • 2 人关注 • 481 次浏览 • 2017-05-22 18:46 • 来自相关话题

1
回复

求大神帮帮忙 环信 android

geri_yang 回复了问题 • 2 人关注 • 299 次浏览 • 2017-05-08 19:41 • 来自相关话题

0
评论

环信Android/ios V3.3.1 SDK 已发布,支持token登录,红包集成更快捷! 环信 ios 环信 android 产品更新

产品更新 发表了文章 • 537 次浏览 • 2017-04-12 17:41 • 来自相关话题

Android ​V3.3.1 2017-04-07
 
新功能:
新增:使用token登录接口新增:群组群成员进出群组回调

优化:
Demo中红包集成方式更改为aar,默认支持支付宝渠道支付

修复
之前EMChatManager.getMessage对应的消息会保存在缓存中,修改后不缓存getMessage产生的消息。之前的代码会导致loadMoreMessage部分消息不显示。3.3.0版本Demo中群组@键,弹出列表没有包含群组管理员3.3.0版本EMGroup.getMuteList会崩溃3.3.0版本EMChatRoom hash code错误修复音视频被叫时多个应用都会收到通知的错误
 
iOS V3.3.1 2017-04-07新功能:
新增:使用token登录新增:群组群成员进出群组回调

优化:
红包改用cocoapods方式集成,支持支付宝和京东支付

修复:
insertMessage小概率下会崩溃[EMMessage setTo:]赋值错误聊天室获取详情接口[IEMChatroomManager fetchChatroomInfo:includeMembersList:error:]第2个参数传入YES时不能获取成员2.x和3.x互通情况下,群组和聊天室的memberlist中出现admin和owner发送消息成功后,对应的EMConversation没有更新最后一条消息
 
版本历史:AndroidSDK 更新日志  ios SDK更新日志
下载地址:SDK下载 查看全部
Android ​V3.3.1 2017-04-07
 
新功能:
  1. 新增:使用token登录接口
  2. 新增:群组群成员进出群组回调


优化:
  1. Demo中红包集成方式更改为aar,默认支持支付宝渠道支付


修复
  1. 之前EMChatManager.getMessage对应的消息会保存在缓存中,修改后不缓存getMessage产生的消息。之前的代码会导致loadMoreMessage部分消息不显示。
  2. 3.3.0版本Demo中群组@键,弹出列表没有包含群组管理员
  3. 3.3.0版本EMGroup.getMuteList会崩溃
  4. 3.3.0版本EMChatRoom hash code错误
  5. 修复音视频被叫时多个应用都会收到通知的错误

 
iOS V3.3.1 2017-04-07新功能:
  1. 新增:使用token登录
  2. 新增:群组群成员进出群组回调


优化:
  1. 红包改用cocoapods方式集成,支持支付宝和京东支付


修复:
  1. insertMessage小概率下会崩溃
  2. [EMMessage setTo:]赋值错误
  3. 聊天室获取详情接口[IEMChatroomManager fetchChatroomInfo:includeMembersList:error:]第2个参数传入YES时不能获取成员
  4. 2.x和3.x互通情况下,群组和聊天室的memberlist中出现admin和owner
  5. 发送消息成功后,对应的EMConversation没有更新最后一条消息

 
版本历史:AndroidSDK 更新日志  ios SDK更新日志
下载地址:SDK下载
2
回复

集成环信语音通话对方,拨打出去之后对方总是弹出exception callstate:ringing android 环信 android

baoshu 回复了问题 • 2 人关注 • 367 次浏览 • 2017-02-28 16:45 • 来自相关话题

1
回复

集成环信的音视频通话功能,一方接受不到是什么导致的,广播我是在Application里面注册的 环信 android

baoshu 回复了问题 • 2 人关注 • 354 次浏览 • 2017-02-28 11:45 • 来自相关话题

1
回复

环信报错求解 环信 android

Wxin 回复了问题 • 2 人关注 • 352 次浏览 • 2017-02-23 09:27 • 来自相关话题

2
评论

环信之Android修改圆形头像 环信 android

ColoThor 发表了文章 • 1428 次浏览 • 2017-02-21 16:25 • 来自相关话题

直接进入正题吧,在demo的EaseUi里面utils包下面有个EaseUserUtils类里面有如下代码:





然后只要在setUserAvatar这个方法里面稍作修改,可以看出用的是glide加载图片,于是我们可以写一个把图片转为圆形的类GlideCircleTransform, 代码如下:
public class GlideCircleTransform extends BitmapTransformation {

public GlideCircleTransform(Context context) {
super(context);
}

protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return circleCrop(pool, toTransform);
}

private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
if (source == null) return null;
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
// TODO this could be acquired from the pool too
Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
return result;
}

@Override
public String getId() {
return getClass().getName();
}
}然后稍加修改setUserAvatar方法,代码如下:
/**
* set user avatar
* @param username
*/
public static void setUserAvatar(Context context, String username, ImageView imageView){
EaseUser user = getUserInfo(username);
if(user != null && user.getAvatar() != null){
try {
int avatarResId = Integer.parseInt(user.getAvatar());
// Glide.with(context).load(avatarResId).into(imageView);
Glide.with(context).load(avatarResId).transform(new GlideCircleTransform(context)).into(imageView);
} catch (Exception e) {
//use default avatar
// Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL).placeholder(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL). placeholder(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}else{
// Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}其中default_user_head_img是我项目中的默认头像,大家改为自己的即可。
到现在EaseConversationListFragment中的头像就变成圆形的了,但是EaseChatFragment还要修改easeui布局文件, 文件列表如下:





我们在这些资源文件中可以用于显示头像的ImageView





把android:src="@drawable/ease_default_avatar"这行 删掉即可。
跑起来看看,是不是都是圆形头像了。
好了,教程结束。 查看全部
直接进入正题吧,在demo的EaseUi里面utils包下面有个EaseUserUtils类里面有如下代码:

1.PNG

然后只要在setUserAvatar这个方法里面稍作修改,可以看出用的是glide加载图片,于是我们可以写一个把图片转为圆形的类GlideCircleTransform, 代码如下:
public class GlideCircleTransform extends BitmapTransformation {

public GlideCircleTransform(Context context) {
super(context);
}

protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return circleCrop(pool, toTransform);
}

private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
if (source == null) return null;
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
// TODO this could be acquired from the pool too
Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
return result;
}

@Override
public String getId() {
return getClass().getName();
}
}
然后稍加修改setUserAvatar方法,代码如下:
/**
* set user avatar
* @param username
*/
public static void setUserAvatar(Context context, String username, ImageView imageView){
EaseUser user = getUserInfo(username);
if(user != null && user.getAvatar() != null){
try {
int avatarResId = Integer.parseInt(user.getAvatar());
// Glide.with(context).load(avatarResId).into(imageView);
Glide.with(context).load(avatarResId).transform(new GlideCircleTransform(context)).into(imageView);
} catch (Exception e) {
//use default avatar
// Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL).placeholder(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL). placeholder(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}else{
// Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}
其中default_user_head_img是我项目中的默认头像,大家改为自己的即可。
到现在EaseConversationListFragment中的头像就变成圆形的了,但是EaseChatFragment还要修改easeui布局文件, 文件列表如下:

2.PNG

我们在这些资源文件中可以用于显示头像的ImageView

3.PNG

把android:src="@drawable/ease_default_avatar"这行 删掉即可。
跑起来看看,是不是都是圆形头像了。
好了,教程结束。
1
回复

哪里有环信android集成音视频的demo或者视频? android集成音视频 环信 android

回复

编号9527 回复了问题 • 1 人关注 • 367 次浏览 • 2017-02-21 13:01 • 来自相关话题

3
回复

环信登录错误,unknow server error 有专职工程师值守 环信 android

baoshu 回复了问题 • 3 人关注 • 372 次浏览 • 2017-02-20 08:11 • 来自相关话题

1
回复

添加依赖compile 'com.hyphenate:hyphenate-sdk:3.2.3'后运行异常 环信 android

jiangym 回复了问题 • 2 人关注 • 830 次浏览 • 2017-02-18 13:40 • 来自相关话题

1
回复

Android环信集成时出现问题 环信 android

jiangym 回复了问题 • 2 人关注 • 321 次浏览 • 2017-02-18 13:39 • 来自相关话题

1
回复

Android环信集成时出现问题 环信 android

jiangym 回复了问题 • 2 人关注 • 752 次浏览 • 2017-02-18 13:39 • 来自相关话题

1
最佳

Android 收到消息怎么区分类型 环信 android

baoshu 回复了问题 • 2 人关注 • 346 次浏览 • 2017-02-10 11:48 • 来自相关话题

2
回复

使用demo中的appkey测试自己的app 环信 android 环信_Android appkey

chieei 回复了问题 • 3 人关注 • 969 次浏览 • 2017-02-09 16:13 • 来自相关话题

条新动态, 点击查看
发送的图片会否超过了10M,还有没有其他的错误信息
发送的图片会否超过了10M,还有没有其他的错误信息
黄宝

黄宝 回答了问题 • 2016-08-30 12:36 • 1 个回复 不感兴趣

怎么判断两个人是否是好友关系?

赞同来自:

 已找到怎么解决,获取好友列表保存本地去判断
 已找到怎么解决,获取好友列表保存本地去判断
删除之后调用会话列表的刷新试试
//会话列表刷新
private void refreshUIWithMessage() {
runOnUiThread(new Runnable() {
public void run() {
// re... 显示全部 »
删除之后调用会话列表的刷新试试
//会话列表刷新
private void refreshUIWithMessage() {
runOnUiThread(new Runnable() {
public void run() {
// refresh unread count
updateUnreadLabel();
if (currentTabIndex == 0) {
// refresh conversation list
if (conversationListFragment != null) {
conversationListFragment.refresh();
}
}
}
});
}  
 
没有调用会话列表的刷新 //会话列表刷新
private void refreshUIWithMessage() {
runOnUiThread(new Runnable() {
public void run() {
// refre... 显示全部 »
没有调用会话列表的刷新 //会话列表刷新
private void refreshUIWithMessage() {
runOnUiThread(new Runnable() {
public void run() {
// refresh unread count
updateUnreadLabel();
if (currentTabIndex == 0) {
// refresh conversation list
if (conversationListFragment != null) {
conversationListFragment.refresh();
}
}
}
});
}
baoshu

baoshu 回答了问题 • 2017-02-10 11:48 • 1 个回复 不感兴趣

Android 收到消息怎么区分类型

赞同来自:

接收消息的时候做判断区分,详细的参考easeui的EaseMessageAdapter类
/**
* get type of item
*/
public int getItemViewType(int position) {
EMMessa... 显示全部 »
接收消息的时候做判断区分,详细的参考easeui的EaseMessageAdapter类
/**
* get type of item
*/
public int getItemViewType(int position) {
EMMessage message = getItem(position);
if (message == null) {
return -1;
}

if(customRowProvider != null && customRowProvider.getCustomChatRowType(message) > 0){
return customRowProvider.getCustomChatRowType(message) + 13;
}

if (message.getType() == EMMessage.Type.TXT) {
if(message.getBooleanAttribute(EaseConstant.MESSAGE_ATTR_IS_BIG_EXPRESSION, false)){
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_EXPRESSION : MESSAGE_TYPE_SENT_EXPRESSION;
}
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_TXT : MESSAGE_TYPE_SENT_TXT;
}
if (message.getType() == EMMessage.Type.IMAGE) {
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_IMAGE : MESSAGE_TYPE_SENT_IMAGE;

}
if (message.getType() == EMMessage.Type.LOCATION) {
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_LOCATION : MESSAGE_TYPE_SENT_LOCATION;
}
if (message.getType() == EMMessage.Type.VOICE) {
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VOICE : MESSAGE_TYPE_SENT_VOICE;
}
if (message.getType() == EMMessage.Type.VIDEO) {
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VIDEO : MESSAGE_TYPE_SENT_VIDEO;
}
if (message.getType() == EMMessage.Type.FILE) {
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_FILE : MESSAGE_TYPE_SENT_FILE;
}

return -1;// invalid
}
首先确认下你的 sdk 版本是多少?
检查下你的 sdk 是否用了包含音视频的,然后又删除了音视频相关的 so,如果不使用音视频的功能,可以用 libs.without.下的 sdk
首先确认下你的 sdk 版本是多少?
检查下你的 sdk 是否用了包含音视频的,然后又删除了音视频相关的 so,如果不使用音视频的功能,可以用 libs.without.下的 sdk
11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9077 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 8038 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
7
回复

收集基于环信SDK开发的开源项目 开源项目

回眸淡然笑 回复了问题 • 9 人关注 • 4025 次浏览 • 2017-12-03 00:50 • 来自相关话题

7
回复

收集基于环信SDK开发的开源项目 开源项目

回复

回眸淡然笑 回复了问题 • 9 人关注 • 4025 次浏览 • 2017-12-03 00:50 • 来自相关话题

11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9077 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 8038 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
3
回复

调用 EaseUI.getInstance().init(this, options); 初始化报错 删掉就好了。 环信 android

回复

baoshu 回复了问题 • 4 人关注 • 394 次浏览 • 2017-12-12 10:30 • 来自相关话题

0
回复

背景:Android 设置自动同意好友申请,添加好友 !登陆失败! 环信 android 添加好友 登录失败

回复

Mister_Li 发起了问题 • 0 人关注 • 94 次浏览 • 2017-11-16 15:35 • 来自相关话题

1
回复

环信创建EMConversation 环信 android

回复

geri_yang 回复了问题 • 2 人关注 • 282 次浏览 • 2017-09-04 14:56 • 来自相关话题

1
回复

环信android demo,更改为自己的appKey之后连接聊天服务器失败 demo 环信 android

回复

geri_yang 回复了问题 • 2 人关注 • 286 次浏览 • 2017-08-15 12:33 • 来自相关话题

2
回复
1
回复

Android 群聊显示为单聊 环信 android

回复

pakrs12341 回复了问题 • 2 人关注 • 455 次浏览 • 2017-08-08 17:42 • 来自相关话题

1
回复
1
回复

安卓环信的会话列表怎么实时监听消息,实时更新 环信 android

回复

lzan13 回复了问题 • 2 人关注 • 536 次浏览 • 2017-06-07 15:52 • 来自相关话题

0
回复

您好 按照你的视频步骤集成EaseUi 时 compileDebugJavaWithJavac 环信 android

回复

Lee_Mofeel。 发起了问题 • 1 人关注 • 412 次浏览 • 2017-06-05 15:16 • 来自相关话题

3
回复

环信集成第三方推送唤醒apk 华信 环信小米 环信华为 环信 android APP自启

回复

baoshu 回复了问题 • 2 人关注 • 346 次浏览 • 2017-05-26 10:23 • 来自相关话题

1
最佳

环信初始化报错:Could not find class 'com.hyphenate.chat.EMCallManager' 环信 android

回复

lzan13 回复了问题 • 2 人关注 • 481 次浏览 • 2017-05-22 18:46 • 来自相关话题

1
回复

求大神帮帮忙 环信 android

回复

geri_yang 回复了问题 • 2 人关注 • 299 次浏览 • 2017-05-08 19:41 • 来自相关话题

2
回复

集成环信语音通话对方,拨打出去之后对方总是弹出exception callstate:ringing android 环信 android

回复

baoshu 回复了问题 • 2 人关注 • 367 次浏览 • 2017-02-28 16:45 • 来自相关话题

1
回复

集成环信的音视频通话功能,一方接受不到是什么导致的,广播我是在Application里面注册的 环信 android

回复

baoshu 回复了问题 • 2 人关注 • 354 次浏览 • 2017-02-28 11:45 • 来自相关话题

1
回复

环信报错求解 环信 android

回复

Wxin 回复了问题 • 2 人关注 • 352 次浏览 • 2017-02-23 09:27 • 来自相关话题

1
回复

哪里有环信android集成音视频的demo或者视频? android集成音视频 环信 android

回复

编号9527 回复了问题 • 1 人关注 • 367 次浏览 • 2017-02-21 13:01 • 来自相关话题

3
回复

环信登录错误,unknow server error 有专职工程师值守 环信 android

回复

baoshu 回复了问题 • 3 人关注 • 372 次浏览 • 2017-02-20 08:11 • 来自相关话题

1
回复

添加依赖compile 'com.hyphenate:hyphenate-sdk:3.2.3'后运行异常 环信 android

回复

jiangym 回复了问题 • 2 人关注 • 830 次浏览 • 2017-02-18 13:40 • 来自相关话题

1
回复

Android环信集成时出现问题 环信 android

回复

jiangym 回复了问题 • 2 人关注 • 321 次浏览 • 2017-02-18 13:39 • 来自相关话题

1
回复

Android环信集成时出现问题 环信 android

回复

jiangym 回复了问题 • 2 人关注 • 752 次浏览 • 2017-02-18 13:39 • 来自相关话题

1
最佳

Android 收到消息怎么区分类型 环信 android

回复

baoshu 回复了问题 • 2 人关注 • 346 次浏览 • 2017-02-10 11:48 • 来自相关话题

2
回复

使用demo中的appkey测试自己的app 环信 android 环信_Android appkey

回复

chieei 回复了问题 • 3 人关注 • 969 次浏览 • 2017-02-09 16:13 • 来自相关话题

1
回复

服务器端添加好友,安卓端登陆闪退 闪退 环信 android

回复

Wxin 回复了问题 • 2 人关注 • 490 次浏览 • 2016-12-20 17:36 • 来自相关话题

1
回复

环信如何实现在A手机接收好友邀请后不做任何处理在B手机还能接收到好友邀请。 环信 android

回复

zhangyb 回复了问题 • 2 人关注 • 455 次浏览 • 2016-12-14 18:46 • 来自相关话题

7
回复

收集基于环信SDK开发的开源项目 开源项目

回复

回眸淡然笑 回复了问题 • 9 人关注 • 4025 次浏览 • 2017-12-03 00:50 • 来自相关话题

11
评论

【开源OA项目】基于环信IM开发完整的企业通讯解决方案-Dolores Dolores OA 开源项目

KevinGong 发表了文章 • 9077 次浏览 • 2017-06-26 10:53 • 来自相关话题

  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?
   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。




关于Dolores?
Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。
Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
企业通讯录的管理:部门/员工的增删改查通讯录全量更新:全量/增量更新 企业通讯录权限管理:基于RBAC权限管理模型企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
支持管理人员(例如HR)对部门和员工进行增删改查支持部门和员工自定义排序,自定义元信息存储权限管理员工通讯录视图 (员工根据自己的权限生成通讯录)通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)集成 IM 用户系统

在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限

在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。|ˉˉˉ| |ˉˉ ˉ| |ˉˉˉˉ ˉˉ|
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|
    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}
客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
会话列表优秀的聊天界面,历史记录组织机构全量/增量更新员工个人资料展示

客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。员工隶属于部门,同一员工可以存在于多个部门。员工角色用title来表示。

用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正THE CALM BEFORE THE STORM.
暴风雨前的宁静
ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。 查看全部
  

  前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。
   对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的重要性,安全与隐私等问题急需未雨绸缪,“可定制”、“可私有化部署”的OA办公系统成为了更多企业的首选。
公司想自己开发一套IM系统应该从哪里开始呢? 企业通讯录怎么保持同步呢? 企业通讯录的权限管理应该怎么做?

   三个关于OA办公系统的究极问题,从开源的OA办公项目-Dolores(朵拉)诞生迎刃而解了。Dolores项目遵循Apache Licence 2.0 开源协议,可以直接拿来用,也可以修改代码来满足需要并作为开源或商业产品发布/销售。
OA广告图.jpg

关于Dolores?

Dolores是一套完整的企业通信解决方案,一个完整的企业沟通工具(以下简称企业IM),支持以下几个功能:IM消息服务、组织架构管理、工作流集成。


Dolores项目源码地址:https://github.com/DoloresTeam​ 
技术讨论群:641256202(QQ群)

整个解决方案都包括了什么?
  • 企业通讯录的管理:部门/员工的增删改查
  • 通讯录全量更新:全量/增量更新 
  • 企业通讯录权限管理:基于RBAC权限管理模型
  • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信

 
 
组织架构

企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。
 
组织架构-Server

服务端主要包括以下功能:
  1. 支持管理人员(例如HR)对部门和员工进行增删改查
  2. 支持部门和员工自定义排序,自定义元信息存储
  3. 权限管理
  4. 员工通讯录视图 (员工根据自己的权限生成通讯录)
  5. 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  6. 集成 IM 用户系统


在这里我们主要讨论以下两个问题:
 
权限管理

  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。

基于Role-based access control(RBAC)的权限管理模型

为了介绍此权限管理模型,我们先解释一下基本概念
  • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
  • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
  • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
  • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限


在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
|ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  
| User |---URA---> | Role |<---RPA---| Permission |
|______| |_______| |_____________|

    以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。

一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。

   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。

增量更新

   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。

基于版本号与变更日志的增量更新模型

   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。

版本号

   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version

变更日志

   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。

   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
{
"content" : [
{
"cn" : "Lucy.Liu",
"id" : "b4vlfg91scgi1dcju8v0",
"title" : "市场运营负责人",
"email" : [
"lucy.liu@dolores.store"
],
"priority" : "101111",
"name" : "刘小飞",
"telephoneNumber" : "18888888888"
}
],
"createTimestamp" : "20170614063303Z",
"category" : "member",
"action" : "add"
}

客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。

组织架构-Client

   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。

客户端App主要包括以下功能:
  1. 会话列表
  2. 优秀的聊天界面,历史记录
  3. 组织机构全量/增量更新
  4. 员工个人资料展示


客户端数据库设计

IM数据库设计
 
当前版本使用环信SDK
 
组织架构数据库设计

表设计

客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
  • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
  • 员工隶属于部门,同一员工可以存在于多个部门。
  • 员工角色用title来表示。


用户在登录客户端成功后,会根据该用户信息创建用户对应的数据库文件,用户表(User)保存用户相关信息,关联该用户staff信息。

客户端组织架构同服务端逻辑。

工作流集成

(TODO)
 
如何使用Dolores

本项目现在已经完成了第一个测试版本,本小节将指导您如何安装使用。

后端数据库

鉴于通讯录对数据库操作的特点多度少写,以及部门之间的树状关系,我们选择LDAP协议来存取数据。

我们有独立的repo来帮助您完成数据库的安装与初始化。请移步这里

组织架构管理

Dolores 初始版本使用Golang实现,大家既可以下载各个平台的可执行包,也可以安装Go语言的开发环境自己编译。

我们有独立的repo来帮助您,运行后端服务。请移步这里

客户端

我们现在有提供一个iOS版的Demo。请移步这里

Done

如果您顺利的完成以上三步,访问 http://localhost:3280 (端口号根据自己的配置,可能会有差异),使用 username: admin, password: dolores 登陆后端管理页面,添加权限规则,添加角色,添加员工、部门,然后使用iOS客户端登陆,就可以愉快的开始聊天啦~
 
负载均衡

(TODO)

多机容灾

(TODO)

LICENSE
 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

更多信息请前往github项目主页

 
这里我对每个repo做一个简单的介绍


Dolores: 项目简介, 整个项目的架构, 数据库设计等等 你想了解的一切都可以在这里看到
dolores-ios: iOS版demo,可以聊天查看组织架构
dolores-android: 哈哈 还没有,当然我们欢迎各路安卓大牛贡献安卓版demo
organization: 组织架构的创建管理、更新、审计等等核心的东西都在这里啦
dolores-server: 为客户端提供restfull api 与环信服务器集成
dolores-admin: 后台管理网站,用于管理部门员工。一个基于React的webapp还很基础,欢迎各位大牛pr.
dolores-ldap-init: 后台数据库的初始化工具,详情可以查看readme
easemob-resty:对环信rest api的封装,让调用环信api更简单
dolores-avatar:生成类似钉钉那样的默认头像


最后再说一点整个服务端是用go来写的,作者也是golang的初学者,如果代码哪里写的有问题或者架构有问题欢迎大家指正
THE CALM BEFORE THE STORM.
暴风雨前的宁静

ONE MORE THING 最后附上Dolores项目LOGO
当时作者正在二刷 《西部世界》这部剧,所以选择了女主的名字dolores作为整个项目的名字,而这个logo则寓意剧中的host。
687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png
8
评论

【新手快速入门】集成环信常见问题+解决方案汇总 常见问题

dujiepeng 发表了文章 • 8038 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
0
评论

环信Android/ios V3.3.1 SDK 已发布,支持token登录,红包集成更快捷! 环信 ios 环信 android 产品更新

产品更新 发表了文章 • 537 次浏览 • 2017-04-12 17:41 • 来自相关话题

Android ​V3.3.1 2017-04-07
 
新功能:
新增:使用token登录接口新增:群组群成员进出群组回调

优化:
Demo中红包集成方式更改为aar,默认支持支付宝渠道支付

修复
之前EMChatManager.getMessage对应的消息会保存在缓存中,修改后不缓存getMessage产生的消息。之前的代码会导致loadMoreMessage部分消息不显示。3.3.0版本Demo中群组@键,弹出列表没有包含群组管理员3.3.0版本EMGroup.getMuteList会崩溃3.3.0版本EMChatRoom hash code错误修复音视频被叫时多个应用都会收到通知的错误
 
iOS V3.3.1 2017-04-07新功能:
新增:使用token登录新增:群组群成员进出群组回调

优化:
红包改用cocoapods方式集成,支持支付宝和京东支付

修复:
insertMessage小概率下会崩溃[EMMessage setTo:]赋值错误聊天室获取详情接口[IEMChatroomManager fetchChatroomInfo:includeMembersList:error:]第2个参数传入YES时不能获取成员2.x和3.x互通情况下,群组和聊天室的memberlist中出现admin和owner发送消息成功后,对应的EMConversation没有更新最后一条消息
 
版本历史:AndroidSDK 更新日志  ios SDK更新日志
下载地址:SDK下载 查看全部
Android ​V3.3.1 2017-04-07
 
新功能:
  1. 新增:使用token登录接口
  2. 新增:群组群成员进出群组回调


优化:
  1. Demo中红包集成方式更改为aar,默认支持支付宝渠道支付


修复
  1. 之前EMChatManager.getMessage对应的消息会保存在缓存中,修改后不缓存getMessage产生的消息。之前的代码会导致loadMoreMessage部分消息不显示。
  2. 3.3.0版本Demo中群组@键,弹出列表没有包含群组管理员
  3. 3.3.0版本EMGroup.getMuteList会崩溃
  4. 3.3.0版本EMChatRoom hash code错误
  5. 修复音视频被叫时多个应用都会收到通知的错误

 
iOS V3.3.1 2017-04-07新功能:
  1. 新增:使用token登录
  2. 新增:群组群成员进出群组回调


优化:
  1. 红包改用cocoapods方式集成,支持支付宝和京东支付


修复:
  1. insertMessage小概率下会崩溃
  2. [EMMessage setTo:]赋值错误
  3. 聊天室获取详情接口[IEMChatroomManager fetchChatroomInfo:includeMembersList:error:]第2个参数传入YES时不能获取成员
  4. 2.x和3.x互通情况下,群组和聊天室的memberlist中出现admin和owner
  5. 发送消息成功后,对应的EMConversation没有更新最后一条消息

 
版本历史:AndroidSDK 更新日志  ios SDK更新日志
下载地址:SDK下载
2
评论

环信之Android修改圆形头像 环信 android

ColoThor 发表了文章 • 1428 次浏览 • 2017-02-21 16:25 • 来自相关话题

直接进入正题吧,在demo的EaseUi里面utils包下面有个EaseUserUtils类里面有如下代码:





然后只要在setUserAvatar这个方法里面稍作修改,可以看出用的是glide加载图片,于是我们可以写一个把图片转为圆形的类GlideCircleTransform, 代码如下:
public class GlideCircleTransform extends BitmapTransformation {

public GlideCircleTransform(Context context) {
super(context);
}

protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return circleCrop(pool, toTransform);
}

private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
if (source == null) return null;
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
// TODO this could be acquired from the pool too
Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
return result;
}

@Override
public String getId() {
return getClass().getName();
}
}然后稍加修改setUserAvatar方法,代码如下:
/**
* set user avatar
* @param username
*/
public static void setUserAvatar(Context context, String username, ImageView imageView){
EaseUser user = getUserInfo(username);
if(user != null && user.getAvatar() != null){
try {
int avatarResId = Integer.parseInt(user.getAvatar());
// Glide.with(context).load(avatarResId).into(imageView);
Glide.with(context).load(avatarResId).transform(new GlideCircleTransform(context)).into(imageView);
} catch (Exception e) {
//use default avatar
// Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL).placeholder(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL). placeholder(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}else{
// Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}其中default_user_head_img是我项目中的默认头像,大家改为自己的即可。
到现在EaseConversationListFragment中的头像就变成圆形的了,但是EaseChatFragment还要修改easeui布局文件, 文件列表如下:





我们在这些资源文件中可以用于显示头像的ImageView





把android:src="@drawable/ease_default_avatar"这行 删掉即可。
跑起来看看,是不是都是圆形头像了。
好了,教程结束。 查看全部
直接进入正题吧,在demo的EaseUi里面utils包下面有个EaseUserUtils类里面有如下代码:

1.PNG

然后只要在setUserAvatar这个方法里面稍作修改,可以看出用的是glide加载图片,于是我们可以写一个把图片转为圆形的类GlideCircleTransform, 代码如下:
public class GlideCircleTransform extends BitmapTransformation {

public GlideCircleTransform(Context context) {
super(context);
}

protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
return circleCrop(pool, toTransform);
}

private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
if (source == null) return null;
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
// TODO this could be acquired from the pool too
Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
if (result == null) {
result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
return result;
}

@Override
public String getId() {
return getClass().getName();
}
}
然后稍加修改setUserAvatar方法,代码如下:
/**
* set user avatar
* @param username
*/
public static void setUserAvatar(Context context, String username, ImageView imageView){
EaseUser user = getUserInfo(username);
if(user != null && user.getAvatar() != null){
try {
int avatarResId = Integer.parseInt(user.getAvatar());
// Glide.with(context).load(avatarResId).into(imageView);
Glide.with(context).load(avatarResId).transform(new GlideCircleTransform(context)).into(imageView);
} catch (Exception e) {
//use default avatar
// Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL).placeholder(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL). placeholder(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}else{
// Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(R.drawable.default_user_head_img).transform(new GlideCircleTransform(context)).into(imageView);
}
}
其中default_user_head_img是我项目中的默认头像,大家改为自己的即可。
到现在EaseConversationListFragment中的头像就变成圆形的了,但是EaseChatFragment还要修改easeui布局文件, 文件列表如下:

2.PNG

我们在这些资源文件中可以用于显示头像的ImageView

3.PNG

把android:src="@drawable/ease_default_avatar"这行 删掉即可。
跑起来看看,是不是都是圆形头像了。
好了,教程结束。
1
评论

基于环信的仿QQ即时通讯的简单实现 QQ 环信 android Android

beyond 发表了文章 • 2004 次浏览 • 2017-01-24 14:52 • 来自相关话题

   之前一直想实现聊天的功能,但是感觉有点困难,今天看了环信的API,就利用下午的时间动手试了试,然后做了一个小Demo。
   因为没有刻意去做聊天软件,花的时间也不多,然后界面就很简单,都是一些基本知识,如果觉得功能简单,可以自行添加,我这就不多介绍了。
 
照例先来一波动态演示:




功能很简单,注册用户 --> 用户登录 --> 选择聊天对象 --> 开始聊天

使用到的知识点:RecyclerView
CardView
环信的API的简单使用依赖的库
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:cardview-v7:24.1.1'
compile 'com.android.support:recyclerview-v7:24.0.0'1、聊天页面

首先是看了郭神的《第二行代码》做了聊天界面,用的是RecyclerView

a. 消息类的封装public class MSG {
public static final int TYPE_RECEIVED = 0;//消息的类型:接收
public static final int TYPE_SEND = 1; //消息的类型:发送

private String content;//消息的内容
private int type; //消息的类型

public MSG(String content, int type) {
this.content = content;
this.type = type;
}

public String getContent() {
return content;
}

public int getType() {
return type;
}
}b. RecyclerView子项的布局<LinearLayout
android:id="@+id/ll_msg_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!-- 设置点击效果为水波纹(5.0以上) -->
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:padding="2dp">

<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="20dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true">

<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="centerCrop"
android:src="@mipmap/man" />
</android.support.v7.widget.CardView>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/message_left"
android:orientation="horizontal">

<TextView
android:id="@+id/tv_msg_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff" />
</LinearLayout>

</LinearLayout>这是左边的部分,至于右边应该也就简单了。我用CardView把ImageView包裹起来,这样比较好看。效果如下:




c. RecyclerView适配器 public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.MyViewHolder> {

private List<MSG> mMsgList;

public MsgAdapter(List<MSG> mMsgList) {
this.mMsgList = mMsgList;
}

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = View.inflate(parent.getContext(), R.layout.item_msg, null);
MyViewHolder holder = new MyViewHolder(view);
return holder;
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
MSG msg = mMsgList.get(position);
if (msg.getType() == MSG.TYPE_RECEIVED){
//如果是收到的消息,显示左边布局,隐藏右边布局
holder.llLeft.setVisibility(View.VISIBLE);
holder.llRight.setVisibility(View.GONE);
holder.tv_Left.setText(msg.getContent());
} else if (msg.getType() == MSG.TYPE_SEND){
//如果是发送的消息,显示右边布局,隐藏左边布局
holder.llLeft.setVisibility(View.GONE);
holder.llRight.setVisibility(View.VISIBLE);
holder.tv_Right.setText(msg.getContent());
}
}

@Override
public int getItemCount() {
return mMsgList.size();
}

static class MyViewHolder extends RecyclerView.ViewHolder{

LinearLayout llLeft;
LinearLayout llRight;

TextView tv_Left;
TextView tv_Right;

public MyViewHolder(View itemView) {
super(itemView);

llLeft = (LinearLayout) itemView.findViewById(R.id.ll_msg_left);
llRight = (LinearLayout) itemView.findViewById(R.id.ll_msg_right);

tv_Left = (TextView) itemView.findViewById(R.id.tv_msg_left);
tv_Right = (TextView) itemView.findViewById(R.id.tv_msg_right);

}
}
}这部分应该也没什么问题,就是适配器的创建,我之前的文章也讲过 传送门:简单粗暴----RecyclerViewd. 
 
RecyclerView初始化

就是一些基本的初始化,我就不赘述了,讲一下添加数据的细节处理 btSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = etInput.getText().toString().trim();
if (!TextUtils.isEmpty(content)){

...//环信部分的发送消息

MSG msg = new MSG(content, MSG.TYPE_SEND);
mList.add(msg);
//当有新消息时,刷新RecyclerView中的显示
mAdapter.notifyItemInserted(mList.size() - 1);
//将RecyclerView定位到最后一行
mRecyclerView.scrollToPosition(mList.size() - 1);
etInput.setText("");
}
}
});至此界面已经结束了,接下来就是数据的读取
 
2. 环信API的简单应用

官网有详细的API介绍 环信即时通讯云V3.0,我这里就简单介绍如何简单集成

a. 环信开发账号的注册
 
环信官网




b. SDK导入

你可以直接下载然后拷贝工程的libs目录下

Android Studio可以直接添加依赖
 
将以下代码放到项目根目录的build.gradle文件里repositories {
maven { url "https://raw.githubusercontent. ... ot%3B }
}在你的module的build.gradle里加入以下代码android {
//use legacy for android 6.0
useLibrary 'org.apache.http.legacy'
}
dependencies {
compile 'com.android.support:appcompat-v7:23.4.0'
//Optional compile for GCM (Google Cloud Messaging).
compile 'com.google.android.gms:play-services-gcm:9.4.0'
compile 'com.hyphenate:hyphenate-sdk:3.2.3'
}如果想使用不包含音视频通话的sdk,用compile 'com.hyphenate:hyphenate-sdk-lite:3.2.3'c. 清单文件配置<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="Your Package"
android:versionCode="100"
android:versionName="1.0.0">

<!-- Required -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:name="Your Application">

<!-- 设置环信应用的AppKey -->
<meta-data android:name="EASEMOB_APPKEY" android:value="Your AppKey" />
<!-- 声明SDK所需的service SDK核心功能-->
<service android:name="com.hyphenate.chat.EMChatService" android:exported="true"/>
<service android:name="com.hyphenate.chat.EMJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"
/>
<!-- 声明SDK所需的receiver -->
<receiver android:name="com.hyphenate.chat.EMMonitorReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REMOVED"/>
<data android:scheme="package"/>
</intent-filter>
<!-- 可选filter -->
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
</application>
</manifest>APP打包混淆-keep class com.hyphenate.** {*;}
-dontwarn com.hyphenate.**d. 初始化SDK​
在自定义Application的onCreate中初始化public class MyApplication extends Application {

private Context appContext;

@Override
public void onCreate() {
super.onCreate();
EMOptions options = new EMOptions();
options.setAcceptInvitationAlways(false);

appContext = this;
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果APP启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的APP会在以包名为默认的process name下运行,如果查到的process name不是APP的process name就立即返回

if (processAppName == null || !processAppName.equalsIgnoreCase(appContext.getPackageName())) {
Log.e("--->", "enter the service process!");

// 则此application::onCreate 是被service 调用的,直接返回
return;
}

//初始化
EMClient.getInstance().init(getApplicationContext(), options);
//在做打包混淆时,关闭debug模式,避免消耗不必要的资源
EMClient.getInstance().setDebugMode(true);
}

private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
try {
if (info.pid == pID) {
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}
}e. 注册和登陆
 
注册要在子线程中执行//注册失败会抛出HyphenateException
EMClient.getInstance().createAccount(username, pwd);//同步方法

EMClient.getInstance().login(userName,password,new EMCallBack() {//回调
@Override
public void onSuccess() {
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();
Log.d("main", "登录聊天服务器成功!");
}

@Override
public void onProgress(int progress, String status) {

}

@Override
public void onError(int code, String message) {
Log.d("main", "登录聊天服务器失败!");
}
});f. 发送消息//创建一条文本消息,content为消息文字内容,toChatUsername为对方用户或者群聊的id,后文皆是如此
EMMessage message = EMMessage.createTxtSendMessage(content, toChatUsername);
//发送消息
EMClient.getInstance().chatManager().sendMessage(message);g. 接收消息msgListener = new EMMessageListener() {

@Override
public void onMessageReceived(List<EMMessage> messages) {
//收到消息
String result = messages.get(0).getBody().toString();
String msgReceived = result.substring(5, result.length() - 1);

Log.i(TAG, "onMessageReceived: " + msgReceived);
final MSG msg = new MSG(msgReceived, MSG.TYPE_RECEIVED);
runOnUiThread(new Runnable() {
@Override
public void run() {
mList.add(msg);
mAdapter.notifyDataSetChanged();
mRecyclerView.scrollToPosition(mList.size() - 1);
}
});
}

@Override
public void onCmdMessageReceived(List<EMMessage> messages) {
//收到透传消息
}

@Override
public void onMessageRead(List<EMMessage> list) {

}

@Override
public void onMessageDelivered(List<EMMessage> list) {

}

@Override
public void onMessageChanged(EMMessage message, Object change) {
//消息状态变动
}
};接收消息的监听器分别需要在OnResume()和OnDestory()方法中注册和取消注册EMClient.getInstance().chatManager().addMessageListener(msgListener);//注册

EMClient.getInstance().chatManager().removeMessageListener(msgListener);//取消注册需要注意的是,当接收到消息,需要在主线程中更新适配器,否则会不能及时刷新出来
 
到此,一个简单的即时聊天Demo已经完成,功能很简单,如果需要添加额外功能的话,可以自行参考官网,官网给出的教程还是很不错的!

最后希望大家能多多支持我,需要你们的支持喜欢!!
 
作者:环信开发者下位子 查看全部
   之前一直想实现聊天的功能,但是感觉有点困难,今天看了环信的API,就利用下午的时间动手试了试,然后做了一个小Demo。
   因为没有刻意去做聊天软件,花的时间也不多,然后界面就很简单,都是一些基本知识,如果觉得功能简单,可以自行添加,我这就不多介绍了。
 
照例先来一波动态演示:
4043475-d16a88926805236a.gif

功能很简单,注册用户 --> 用户登录 --> 选择聊天对象 --> 开始聊天

使用到的知识点:
RecyclerView
CardView
环信的API的简单使用
依赖的库
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:cardview-v7:24.1.1'
compile 'com.android.support:recyclerview-v7:24.0.0'
1、聊天页面

首先是看了郭神的《第二行代码》做了聊天界面,用的是RecyclerView

a. 消息类的封装
public class MSG {
public static final int TYPE_RECEIVED = 0;//消息的类型:接收
public static final int TYPE_SEND = 1; //消息的类型:发送

private String content;//消息的内容
private int type; //消息的类型

public MSG(String content, int type) {
this.content = content;
this.type = type;
}

public String getContent() {
return content;
}

public int getType() {
return type;
}
}
b. RecyclerView子项的布局
<LinearLayout
android:id="@+id/ll_msg_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!-- 设置点击效果为水波纹(5.0以上) -->
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:padding="2dp">

<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="20dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true">

<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="centerCrop"
android:src="@mipmap/man" />
</android.support.v7.widget.CardView>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/message_left"
android:orientation="horizontal">

<TextView
android:id="@+id/tv_msg_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff" />
</LinearLayout>

</LinearLayout>
这是左边的部分,至于右边应该也就简单了。我用CardView把ImageView包裹起来,这样比较好看。效果如下:
4043475-76ea5370b4d09d89.png

c. RecyclerView适配器
 public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.MyViewHolder> {

private List<MSG> mMsgList;

public MsgAdapter(List<MSG> mMsgList) {
this.mMsgList = mMsgList;
}

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = View.inflate(parent.getContext(), R.layout.item_msg, null);
MyViewHolder holder = new MyViewHolder(view);
return holder;
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
MSG msg = mMsgList.get(position);
if (msg.getType() == MSG.TYPE_RECEIVED){
//如果是收到的消息,显示左边布局,隐藏右边布局
holder.llLeft.setVisibility(View.VISIBLE);
holder.llRight.setVisibility(View.GONE);
holder.tv_Left.setText(msg.getContent());
} else if (msg.getType() == MSG.TYPE_SEND){
//如果是发送的消息,显示右边布局,隐藏左边布局
holder.llLeft.setVisibility(View.GONE);
holder.llRight.setVisibility(View.VISIBLE);
holder.tv_Right.setText(msg.getContent());
}
}

@Override
public int getItemCount() {
return mMsgList.size();
}

static class MyViewHolder extends RecyclerView.ViewHolder{

LinearLayout llLeft;
LinearLayout llRight;

TextView tv_Left;
TextView tv_Right;

public MyViewHolder(View itemView) {
super(itemView);

llLeft = (LinearLayout) itemView.findViewById(R.id.ll_msg_left);
llRight = (LinearLayout) itemView.findViewById(R.id.ll_msg_right);

tv_Left = (TextView) itemView.findViewById(R.id.tv_msg_left);
tv_Right = (TextView) itemView.findViewById(R.id.tv_msg_right);

}
}
}
这部分应该也没什么问题,就是适配器的创建,我之前的文章也讲过 传送门:简单粗暴----RecyclerViewd. 
 
RecyclerView初始化

就是一些基本的初始化,我就不赘述了,讲一下添加数据的细节处理
 btSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = etInput.getText().toString().trim();
if (!TextUtils.isEmpty(content)){

...//环信部分的发送消息

MSG msg = new MSG(content, MSG.TYPE_SEND);
mList.add(msg);
//当有新消息时,刷新RecyclerView中的显示
mAdapter.notifyItemInserted(mList.size() - 1);
//将RecyclerView定位到最后一行
mRecyclerView.scrollToPosition(mList.size() - 1);
etInput.setText("");
}
}
});
至此界面已经结束了,接下来就是数据的读取
 
2. 环信API的简单应用

官网有详细的API介绍 环信即时通讯云V3.0,我这里就简单介绍如何简单集成

a. 环信开发账号的注册
 
环信官网

b. SDK导入

你可以直接下载然后拷贝工程的libs目录下

Android Studio可以直接添加依赖
 
将以下代码放到项目根目录的build.gradle文件里
repositories {
maven { url "https://raw.githubusercontent. ... ot%3B }
}
在你的module的build.gradle里加入以下代码
android {
//use legacy for android 6.0
useLibrary 'org.apache.http.legacy'
}
dependencies {
compile 'com.android.support:appcompat-v7:23.4.0'
//Optional compile for GCM (Google Cloud Messaging).
compile 'com.google.android.gms:play-services-gcm:9.4.0'
compile 'com.hyphenate:hyphenate-sdk:3.2.3'
}
如果想使用不包含音视频通话的sdk,用
compile 'com.hyphenate:hyphenate-sdk-lite:3.2.3'
c. 清单文件配置
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="Your Package"
android:versionCode="100"
android:versionName="1.0.0">

<!-- Required -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:name="Your Application">

<!-- 设置环信应用的AppKey -->
<meta-data android:name="EASEMOB_APPKEY" android:value="Your AppKey" />
<!-- 声明SDK所需的service SDK核心功能-->
<service android:name="com.hyphenate.chat.EMChatService" android:exported="true"/>
<service android:name="com.hyphenate.chat.EMJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"
/>
<!-- 声明SDK所需的receiver -->
<receiver android:name="com.hyphenate.chat.EMMonitorReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REMOVED"/>
<data android:scheme="package"/>
</intent-filter>
<!-- 可选filter -->
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
</application>
</manifest>
APP打包混淆
-keep class com.hyphenate.** {*;}
-dontwarn com.hyphenate.**
d. 初始化SDK​
在自定义Application的onCreate中初始化
public class MyApplication extends Application {

private Context appContext;

@Override
public void onCreate() {
super.onCreate();
EMOptions options = new EMOptions();
options.setAcceptInvitationAlways(false);

appContext = this;
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果APP启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的APP会在以包名为默认的process name下运行,如果查到的process name不是APP的process name就立即返回

if (processAppName == null || !processAppName.equalsIgnoreCase(appContext.getPackageName())) {
Log.e("--->", "enter the service process!");

// 则此application::onCreate 是被service 调用的,直接返回
return;
}

//初始化
EMClient.getInstance().init(getApplicationContext(), options);
//在做打包混淆时,关闭debug模式,避免消耗不必要的资源
EMClient.getInstance().setDebugMode(true);
}

private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
try {
if (info.pid == pID) {
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}
}
e. 注册和登陆
 
注册要在子线程中执行
//注册失败会抛出HyphenateException
EMClient.getInstance().createAccount(username, pwd);//同步方法

EMClient.getInstance().login(userName,password,new EMCallBack() {//回调
@Override
public void onSuccess() {
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();
Log.d("main", "登录聊天服务器成功!");
}

@Override
public void onProgress(int progress, String status) {

}

@Override
public void onError(int code, String message) {
Log.d("main", "登录聊天服务器失败!");
}
});
f. 发送消息
//创建一条文本消息,content为消息文字内容,toChatUsername为对方用户或者群聊的id,后文皆是如此
EMMessage message = EMMessage.createTxtSendMessage(content, toChatUsername);
//发送消息
EMClient.getInstance().chatManager().sendMessage(message);
g. 接收消息
msgListener = new EMMessageListener() {

@Override
public void onMessageReceived(List<EMMessage> messages) {
//收到消息
String result = messages.get(0).getBody().toString();
String msgReceived = result.substring(5, result.length() - 1);

Log.i(TAG, "onMessageReceived: " + msgReceived);
final MSG msg = new MSG(msgReceived, MSG.TYPE_RECEIVED);
runOnUiThread(new Runnable() {
@Override
public void run() {
mList.add(msg);
mAdapter.notifyDataSetChanged();
mRecyclerView.scrollToPosition(mList.size() - 1);
}
});
}

@Override
public void onCmdMessageReceived(List<EMMessage> messages) {
//收到透传消息
}

@Override
public void onMessageRead(List<EMMessage> list) {

}

@Override
public void onMessageDelivered(List<EMMessage> list) {

}

@Override
public void onMessageChanged(EMMessage message, Object change) {
//消息状态变动
}
};
接收消息的监听器分别需要在OnResume()和OnDestory()方法中注册和取消注册
EMClient.getInstance().chatManager().addMessageListener(msgListener);//注册

EMClient.getInstance().chatManager().removeMessageListener(msgListener);//取消注册
需要注意的是,当接收到消息,需要在主线程中更新适配器,否则会不能及时刷新出来
 
到此,一个简单的即时聊天Demo已经完成,功能很简单,如果需要添加额外功能的话,可以自行参考官网,官网给出的教程还是很不错的!

最后希望大家能多多支持我,需要你们的支持喜欢!!
 
作者:环信开发者下位子
2
评论

先定一个小目标,比如写一个QQ QQ 环信 android

沉默的范大叔 发表了文章 • 4452 次浏览 • 2016-11-11 21:33 • 来自相关话题

 项目简介 

   本项目是即时通讯的示例项目,使用了MVP模式,集成了环信SDK和Bmob后端云,展示了即时通讯基本功能的实现,包括注册登录,退出登录,联系人列表,添加好友,删除好友,收发消息,消息提醒等功能。
 
 使用的开源项目 
BottomBarEventBusgreenDAObutterknife
 
 
学习目标 
 环信SDK的集成与使用 MVP模式的运用 ORM数据库的集成与使用 模块化思想的运用

即时通讯 IM(Instant Messaging)允许两人或多人使用网络即时的传递文字讯息、档案、语音与视频交流。
相关产品
 鼻祖 ICQ 国内主流 QQ 微信 陌陌 YY等 国外主流 Facebook Messenger WhatsApp Skype Instagram Line

第三方服务平台 
环信

环信 
官网即时通信云3.x文档





环信集成
注册并创建应用下载SDKSDK的导入SDK初始化
 
so文件夹

1. 放在jniLibs
2. 也可以放在libs目录下,不过需要在模块下的配置文件中配置
   android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
注意事项

运行出错:Didn't find class "com.hyphenate.chat.adapter.EMACallSession",原因是hyphenatechat_3.2.0.jar包内没有该类。

解决办法:导入Demo源码中EaseUI库里面的hyphenatechat_3.2.0.jar替换。

软件架构

MVC

MVC应用于Ruby on Rails, Spring Framework, iOS开发和 ASP.NET等。
 Model: 获取数据的业务逻辑,网络操作,数据库操作 View: UI Controller: 操作Model层获取数据传递给UI






服务器端的MVC





Android中MVC

Android中并没有清晰的MVC框架,如果把Activity当做Controller,根据我们实际开发经验,里面会有大量的UI操作,所以V和C就傻傻分不清了。
 Model:Java Bean, NetworkManager, DataBaseHelper View: xml res Controller: Activity Fragment ArrayList-ListView-Adapter(MVC)

MVP
MVP主要应用于ASP.NET等。MVP与MVC主要区别是View和Model不再耦合。
 Model: 获取数据的业务逻辑,网络操作,数据库操作 View: UI Presenter: 操作Model层获取数据传递给UI
 





MVVM

MVVM主要应用于WPF, Silverlight, Caliburn, nRoute等。
 Model: 获取数据的业务逻辑,网络操作,数据库操作 View: UI ViewModel: 将View和Model绑定






Android中MVVM
Data Binding Library中文翻译
 
软件架构的核心思想分层分模块



 
参考
MVC,MVP和MVVM模式如何选择教你认清MVC,MVP和MVVMandroid architectureUnderstanding MVC, MVP and MVVM Design PatternsAndroid Data BindingClean Architecture
 
准备好了么? 开车啦!!!
 
包的创建
 adapter 存放适配器 app 存放常量类,Application类以及一些app层级的全局类 database 数据库相关类 event EventBus使用的事件类 factory 工厂类 model 数据模型 presenter MVP模型中的Presenter类 ui 存放activity和fragment utils 工具类 view MVP模型中的View类 widget 自定义控件

基类的创建
 BaseActivity BaseFragment

Splash界面


功能需求
如果没有登录,延时2s, 跳转到登录界面如果已经登录,则跳转到主界面

MVP实现
 SplashView SplashPresenter

判断是否登录环信@Override
public void checkLoginStatus() {
if (EMClient.getInstance().isLoggedInBefore() && EMClient.getInstance().isConnected()) {
mSplashView.onLoggedIn();
} else {
mSplashView.onNotLogin();
}
}
 登录界面

功能需求
点击登录按钮或者点击虚拟键盘上的Action键都能发起登录操作点击新用户,跳转到注册界面。

MVP实现
 LoginView LoginPresenter

IME Options

注意配置EditText的imeOptions属性时,需要配合inputType才能起作用。android:imeOptions="actionNext"//下一个
android:imeOptions="actionGo"//启动
android:imeOptions="actionDone"//完成
android:imeOptions="actionPrevious"//上一个
android:imeOptions="actionSearch"//搜索
android:imeOptions="actionSend"//发送监听软键盘Action事件,发起登录 private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {

@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
startLogin();
return true;
}
return false;
}
};EMCallBack的适配器 
 
   EMCallBack是环信的一个请求回调接口,包括请求成功的回调onSuccess,请求失败的回调onError和请求进度回调onProgress,
但在实际使用过程中,通常只使用到请求成功和失败的回调,请求进度回调通常留在那里成了一个空方法,对于一个有
代码洁癖的搬砖师来说,这是很难受的。所以我们可以创建一个适配器类实现这个接口,使用时用适配器类来替换EMCallBack
接口,这样只需要覆写我们想覆写的方法就可以了。//EMCallBack接口的适配器
public class EMCallBackAdapter implements EMCallBack{

@Override
public void onSuccess() {

}

@Override
public void onError(int i, String s) {

}

@Override
public void onProgress(int i, String s) {

}
}

//EMCallBack适配器的使用
private EMCallBackAdapter mEMCallBack = new EMCallBackAdapter() {

@Override
public void onSuccess() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginSuccess();
}
});
}

@Override
public void onError(int i, String s) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginFailed();
}
});
}
};Android6.0动态权限管理







Andrioid6.0对权限进行了分组,涉及到用户敏感信息的权限只能动态的去获取。当应用的targetSdkVersion小于23时,
会默认采用以前的权限管理机制,当targetSdkVersion大于等于23时并且运行在Andorid6.0系统上,它才会采用这套新的权限管理机制。

参考 
适配Android6.0动态权限管理Android6.0权限管理的解析与实战

动态获取写磁盘权限

环信SDK内部维护了一个数据库来存储聊天记录等数据,需要读写磁盘的权限,所以我们在用户登录之前要申请该权限。
类似的,很多同学在集成百度地图或者高德地图时常常不能正常定位,往往是忽略了在Android6.0上需要获取位置的权限。/**
* 是否有写磁盘权限
*/
private boolean hasWriteExternalStoragePermission() {
int result = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
return result == PermissionChecker.PERMISSION_GRANTED;
}

/**
* 申请权限
*/
private void applyPermission() {
String permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
ActivityCompat.requestPermissions(this, permissions, REQUEST_WRITE_EXTERNAL_STORAGE);
}

/**
* 申请权限回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions, @NonNull int grantResults) {
switch (requestCode) {
case REQUEST_WRITE_EXTERNAL_STORAGE:
if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) {
login();
} else {
toast(getString(R.string.not_get_permission));
}
break;
}
}
注册界面











功能需求
用户名的长度必须是3-20位,首字母必须为英文字符,其他字符则除了英文外还可以是数字或者下划线。密码必须是3-20位的数字。密码和确认密码一致

MVP实现
RegisterViewRegisterPresenter

正则表达式
正则表达式-元字符
 private static final String USER_NAME_REGEX = "^[a-zA-Z]\\w{2,19}$";//用户名的正则表达式
private static final String PASSWORD_REGEX = "^[0-9]{3,20}$";//密码的正则表达式
 ^ 匹配输入字符串的开始位置[a-zA-Z]     字符范围。匹配指定范围内的任意字符。\w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。 $ 匹配输入字符串的结束位置
 注册流程
 实际项目中,注册会将用户名和密码注册到APP的服务器,然后APP的服务器再通过REST API方式注册到环信服务器。 由于本项目没有APP服务器,会将用户数据注册到第三方云数据库Bmob,注册成功后,再在客户端发送请求注册到环信服务器。






云数据库
LeanCloud
BmobParse(2017年1月28日关闭)
 
Bmob集成
Bmob开发文档
 
 注册创建应用 下载SDK 导入SDK 初始化SDk

隐藏软键盘
在点击注册按钮发起注册流程后,我们需要隐藏掉软键盘并且弹出进度条。protected void hideSoftKeyboard() {
if (mInputMethodManager == null) {
mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
}
mInputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
注册用户到Bmob
Bmob用户管理private void registerBmob(final String userName, final String pwd) {
User user = new User(userName, pwd);
user.signUp(new SaveListener<User>() {
@Override
public void done(User user, BmobException e) {
if (e == null) {
registerEaseMob(userName, pwd);
} else {
notifyRegisterFailed(e);
}
}
});
}注册到环信private void registerEaseMob(final String userName, final String pwd) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
try {
EMClient.getInstance().createAccount(userName, pwd);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mRegisterView.onRegisterSuccess();
}
});
} catch (HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mRegisterView.onRegisterError();
}
});
}
}
});
}
用户名已注册的处理
当我们注册一个用户名到Bmob云数据库后,使用相同的用户名再次注册,Bmob会返回错误码202,信息为username '%s' already taken。
所以我们应该处理该错误,通知用户换一个用户名注册。
Bmob错误码
  private void notifyRegisterFailed(BmobException e) {
if (e.getErrorCode() == Constant.ErrorCode.USER_ALREADY_EXIST) {
mRegisterView.onResisterUserExist();
} else {
mRegisterView.onRegisterError();
}
}主界面




底部导航条 
主界面底部是一个Tab导航条,实现的方法可以有很多种,我们可以使用RadioGroup或者FragmentTabHost, 或者我们可以自定义一个控件,
为了不重复造轮子,本Demo使用了第三方导航条BottomBar。BottomBar的使用请参考其Github地址。

第三方导航条
BottomBar
AHBottomNavigationBottomNavigation
Fragment的切换
    private OnTabSelectListener mOnTabSelectListener = new OnTabSelectListener() {
@Override
public void onTabSelected(@IdRes int tabId) {
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, FragmentFactory.getInstance().getFragment(tabId)).commit();
}
};动态界面






MVP实现
 DynamicView DynamicPresenter

动态界面目前只实现了退出登录功能。@Override
public void logout() {
mDynamicView.onStartLogout();
EMClient.getInstance().logout(true, mEMCallBackAdapter);
}
联系人界面











MVP实现
 ContactView ContactPresenter

使用RecyclerView实现联系人列表
RecyclerView类似ListView, 但比ListView更高级更灵活。使用RecyclerView必须指定一个适配器和一个布局管理器,
经常有同学忘记设置布局管理器,结果导致RecyclerView什么也没有显示。适配器通常继承自RecyclerView.Adapter。

RecyclerView提供这些内置管理器:
 LinearLayoutManager 线性布局管理器 (类似ListView效果) GridLayoutManager 网格布局管理器 (类似GridView效果) StaggeredGridLayoutManager 分散网格布局管理器(瀑布流效果)

如果要自定义一个布局管理器,请继承自RecyclerView.LayoutManager。另外,RecyclerView在默认情况下启用增添与删除item的动画,
如果要定义这些动画,请继承RecyclerView.ItemAnimator并使用RecyclerView.setItemAnimator()。

 初始化 
   private void initRecyclerView() {
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));//设置布局管理器
mRecyclerView.setHasFixedSize(true);//设置true如果列表内容不会影响RecyclerView的大小
mContactListAdapter = new ContactListAdapter(getContext(), mContactPresenter.getContactList());
mContactListAdapter.setOnItemClickListener(mOnItemClickListener);
mRecyclerView.setAdapter(mContactListAdapter);
}
ContactListAdapter的实现 

ContactListItemView






运用模块化的思想,将联系人的列表项抽取成一个独立的自定义组合式控件ContactListItemView。ContactListItemView只需传入一个
与之对应的数据模型ContactListItem即可完成渲染。

联系人是否在同一个组






当多个联系人的首字符相同时,只有第一个ContactListItemView显示首字符,后续首字符相同的ContactListItemView均不显示首字符。
在ContactListItem中声明一个布尔型变量showFirstLetter来标记是否显示首字符。该变量在创建ContactListItem时赋值。/**
* 获取联系人列表数据
* @throws HyphenateException
*/
private void startGetContactList() throws HyphenateException {
List<String> contacts = EMClient.getInstance().contactManager().getAllContactsFromServer();
DatabaseManager.getInstance().deleteAllContacts();
if (!contacts.isEmpty()) {
for (int i = 0; i < contacts.size(); i++) {
ContactListItem item = new ContactListItem();
item.userName = contacts.get(i);
if (itemInSameGroup(i, item)) {
item.showFirstLetter = false;
}
mContactListItems.add(item);
saveContactToDatabase(item.userName);
}
}
}

/**
* 当前联系人跟上个联系人比较,如果首字符相同则返回true
* @param i 当前联系人下标
* @param item 当前联系人数据模型
* @return true 表示当前联系人和上一联系人在同一组
*/
private boolean itemInSameGroup(int i, ContactListItem item) {
return i > 0 && (item.getFirstLetter() == mContactListItems.get(i - 1).getFirstLetter());
}CardView的使用






CardView继承自FrameLayout, 是Material Design里面的卡片设计,带有圆角和阴影效果。CardView效果非常好看,但也不能滥用,
比如:





更多的使用规范,请参考Material Design的设计规范Material Design Cards。

这里我们给ContactListItemView的布局里面包了一层CardView,实际上是不可取的做法,这里只是为了展示CardView的使用姿势,实际项目中请大家遵循Material Design的设计规范。

参考
Creating Lists and CardsAndroid RecyclerView 使用完全解析 体验艺术般的控件

SwipeRefreshLayout的使用  //设置SwipeRefreshLayout的颜色
mSwipeRefreshLayout.setColorSchemeResources(R.color.qq_blue, R.color.qq_red);
//设置SwipeRefreshLayout的监听器
mSwipeRefreshLayout.setOnRefreshListener(mOnRefreshListener);自定义控件SlideBar
分类字符数组 rivate static final String SECTIONS = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"
, "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}; 绘制居中文本
FontMetrics




@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mTextSize = h * 1.0f / SECTIONS.length;//计算分配给字符的高度
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float mTextHeight = fontMetrics.descent - fontMetrics.ascent;//获取绘制字符的实际高度
mTextBaseline = mTextSize / 2 + mTextHeight/2 - fontMetrics.descent;//计算字符居中时的baseline
}

@Override
protected void onDraw(Canvas canvas) {
float x = getWidth() * 1.0f / 2;
float baseline = mTextBaseline;
for(int i = 0; i < SECTIONS.length; i++) {
canvas.drawText(SECTIONS[i], x, baseline, mPaint);
baseline += mTextSize;
}
}[/i]在ContactFragment里面监听SlideBar的事件 

当用户在SlideBar上ACTION_DOWN和ACTION_MOVE时,如果用户触摸到的首字符发生变化,则会回调监听器onSectionChange,
当ACTION_UP时回调onSlidingFinish()。private SlideBar.OnSlideBarChangeListener mOnSlideBarChangeListener = new SlideBar.OnSlideBarChangeListener() {
@Override
public void onSectionChange(int index, String section) {
mSection.setVisibility(View.VISIBLE);//显示中间绿色背景的首字符
mSection.setText(section);
scrollToSection(section);//滚动RecyclerView到用户滑到的首字符
}

@Override
public void onSlidingFinish() {
mSection.setVisibility(View.GONE);//隐藏中间绿色背景的首字符
}
};
联系人点击事件

当单击联系人时跳转到聊天界面,当长按联系人时弹出Dialog询问用户是否删除好友。

  private ContactListAdapter.OnItemClickListener mOnItemClickListener = new ContactListAdapter.OnItemClickListener() {

/**
* 单击跳转到聊天界面
* @param name 点击item的联系人名字
*/
@Override
public void onItemClick(String name) {
startActivity(ChatActivity.class, Constant.Extra.USER_NAME, name);
}

/**
* 长按删除好友
* @param name 点击item的联系人名字
*/
@Override
public void onItemLongClick(final String name) {
showDeleteFriendDialog(name);
}
};
添加好友界面 






 MVP实现 
 
AddFriendView[list][*]AddFriendPresenter
[/*]
[/list]


搜索用户
Bmob查询数据
 @Override
public void searchFriend(final String keyword) {
mAddFriendView.onStartSearch();
//注:模糊查询只对付费用户开放,付费后可直接使用。
BmobQuery<User> query = new BmobQuery<User>();
query.addWhereContains("username", keyword).addWhereNotEqualTo("username", EMClient.getInstance().getCurrentUser());
query.findObjects(new FindListener<User>() {
@Override
public void done(List<User> list, BmobException e) {
processResult(list, e);
}
});
} greenDAO
greenDAO是Android SQLite数据库ORM框架的一种。ORM即对象关系映射, object/relational mapping, 将Java对象映射成数据库的表。
我们需要存储联系人数据到数据库,在添加好友界面,需要判断搜索出来的用户是否已经是联系人,如果已经是好友,则应显示“已添加”。

其他ORM框架
 
DBFlow[list][*]Ormlite[list][*]Sugar[list][*]ActiveAndroid[list][*]Sprinkles[list][*]Ollie
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]

参考 
Github[list][*]greeDao官网[list][*]greeDao使用文档[list][*]greeDao中文使用文档[list][*]AppBrain
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]


创建实体类 
   @Entity
public class Contact {

@Id
public Long id;

public String userName;
} 初始化 
   public void init(Context context) {
DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(context, Constant.Database.DATABASE_NAME, null);
SQLiteDatabase writableDatabase = devOpenHelper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(writableDatabase);
mDaoSession = daoMaster.newSession();
}保存联系人public void saveContact(String userName) {
Contact contact = new Contact();
contact.setUsername(userName);
mDaoSession.getContactDao().save(contact);
}
查询联系人 
   public List<String> queryAllContacts() {
List<Contact> list = mDaoSession.getContactDao().queryBuilder().list();
ArrayList<String> contacts = new ArrayList<String>();
for (int i = 0; i < list.size(); i++) {
String contact = list.get(i).getUsername();
contacts.add(contact);
}
return contacts;
}
 删除所有联系人public void deleteAllContacts() {
ContactDao contactDao = mDaoSession.getContactDao();
contactDao.deleteAll();
}
发送好友请求 
AddFriendItemView里面处理添加好友的点击事件 
   @OnClick(R.id.add)
public void onClick() {
String friendName = mUserName.getText().toString().trim();
String addFriendReason = getContext().getString(R.string.add_friend_reason);
AddFriendEvent event = new AddFriendEvent(friendName, addFriendReason);//通过EventBus发布添加好友事件
EventBus.getDefault().post(event);
}AddFriendPresenterImpl实现发送好友请求 
  @Subscribe(threadMode = ThreadMode.BACKGROUND)
public void addFriend(AddFriendEvent event) {
try {
EMClient.getInstance().contactManager().addContact(event.getFriendName(), event.getReason());
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onAddFriendSuccess();
}
});
} catch (HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onAddFriendFailed();
}
});
}在联系人列表中监听联系人变化
当新增好友或者被好友删除时刷新联系人列表。

 private EMContactListenerAdapter mEMContactListener = new EMContactListenerAdapter() {

@Override
public void onContactAdded(String s) {
mContactPresenter.refreshContactList();
}

@Override
public void onContactDeleted(String s) {
mContactPresenter.refreshContactList();
}
};
聊天界面 





MVP实现 
 ChatView[list][*] ChatPresenter
[/*]
[/list]

UI

监听发送按钮的状态变化 
    mEdit.addTextChangedListener(mTextWatcher);
private TextWatcherAdapter mTextWatcher = new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
mSend.setEnabled(s.length() != 0);
}
};动画文件 
 
 anim文件夹:存放补间动画[list][*] animator文件夹:存放属性动画[list][*] drawable文件夹:存放帧动画
[/*]
[/list]
[/*]
[/list]


发送消息的进度动画 
   <?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk ... gt%3B
<item android:drawable="@mipmap/loading1" android:duration="100"/>
<item android:drawable="@mipmap/loading2" android:duration="100"/>
<item android:drawable="@mipmap/loading3" android:duration="100"/>
<item android:drawable="@mipmap/loading4" android:duration="100"/>
<item android:drawable="@mipmap/loading5" android:duration="100"/>
<item android:drawable="@mipmap/loading6" android:duration="100"/>
<item android:drawable="@mipmap/loading7" android:duration="100"/>
<item android:drawable="@mipmap/loading8" android:duration="100"/>
</animation-list>9文件制作
官方说明











发送一条消息 

返回消息的类型 
根据是发送的消息还是接受的消息,来创建不同的item。@Override
public int getItemViewType(int position) {
EMMessage message = mMessages.get(position);
return message.direct() == EMMessage.Direct.SEND ? ITEM_TYPE_SEND_MESSAGE : ITEM_TYPE_RECEIVE_MESSAGE;
}是否显示时间戳的判断 /**
* 如果两个消息之间的时间太近,就不显示时间戳
*/
private boolean shouldShowTimeStamp(int position) {
long currentItemTimestamp = mMessages.get(position).getMsgTime();
long preItemTimestamp = mMessages.get(position - 1).getMsgTime();
boolean closeEnough = DateUtils.isCloseEnough(currentItemTimestamp, preItemTimestamp);
return !closeEnough;
}更新消息的状态
处理三种消息状态,正在发送中INPROGRESS,发送成功SUCCESS,发送失败FAIL。

  private void updateSendingStatus(EMMessage emMessage) {
switch (emMessage.status()) {
case INPROGRESS:
mSendMessageProgress.setVisibility(VISIBLE);
mSendMessageProgress.setImageResource(R.drawable.send_message_progress);
AnimationDrawable drawable = (AnimationDrawable) mSendMessageProgress.getDrawable();
drawable.start();
break;
case SUCCESS:
mSendMessageProgress.setVisibility(GONE);
break;
case FAIL:
mSendMessageProgress.setImageResource(R.mipmap.msg_error);
break;
}
} 接收一条消息 
监听消息的接收,当收到一条消息后,通知MessageListAdapter进行刷新,并且滚动RecyclerView到底部。private EMMessageListenerAdapter mEMMessageListener = new EMMessageListenerAdapter() {
@Override
public void onMessageReceived(final List<EMMessage> list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
final EMMessage emMessage = list.get(0);
mChatPresenter.makeMessageRead(mUserName);
mMessageListAdapter.addNewMessage(emMessage);
smoothScrollToBottom();
}
});
}
};初始化聊天记录 
 
环信官方文档
 
通信过程及聊天记录保存


 @Override
public void loadMessages(final String userName) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);
if (conversation != null) {
//获取此会话的所有消息
List<EMMessage> messages = conversation.getAllMessages();
mEMMessageList.addAll(messages);
//指定会话消息未读数清零
conversation.markAllMessagesAsRead();
}
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onMessagesLoaded();
}
});
}
});
}加载更多聊天记录 
  @Override
public void loadMoreMessages(final String userName) {
if (hasMoreData) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);
EMMessage firstMessage = mEMMessageList.get(0);
//SDK初始化加载的聊天记录为20条,到顶时需要去DB里获取更多
//获取startMsgId之前的pagesize条消息,此方法获取的messages SDK会自动存入到此会话中,APP中无需再次把获取到的messages添加到会话中
final List<EMMessage> messages = conversation.loadMoreMsgFromDB(firstMessage.getMsgId(), DEFAULT_PAGE_SIZE);
hasMoreData = (messages.size() == DEFAULT_PAGE_SIZE);
mEMMessageList.addAll(0, messages);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onMoreMessagesLoaded(messages.size());
}
});
}
});
} else {
mChatView.onNoMoreData();
}
}会话界面





 MVP实现 
 
 ConversationView[list][*] ConversationPresenter
[/*]
[/list]


加载所有会话@Override
public void loadAllConversations() {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
mEMConversations.addAll(conversations.values());
//根据最后一条消息的时间进行排序
Collections.sort(mEMConversations, new Comparator<EMConversation>() {
@Override
public int compare(EMConversation o1, EMConversation o2) {
return (int) (o2.getLastMessage().getMsgTime() - o1.getLastMessage().getMsgTime());
}
});
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mConversationView.onAllConversationsLoaded();
}
});
}
});
}
未读消息计数更新 
环信官方文档
会话列表中未读消息的更新 
  private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

@Override
public void onMessageReceived(List<EMMessage> list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
toast(getString(R.string.receive_new_message));
//当收到一个新消息时,重新加载会话列表。
mConversationPresenter.loadAllConversations();
}
});
}
};
BottomBar badge的更新 
  private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

//该回调在子线程中调用
@Override
public void onMessageReceived(List<EMMessage> list) {
updateUnreadCount();
}
};

private void updateUnreadCount() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
BottomBarTab bottomBar = mBottomBar.getTabWithId(R.id.conversations);
int count = EMClient.getInstance().chatManager().getUnreadMsgsCount();
bottomBar.setBadgeCount(count);
}
});
}
标记消息已读 
加载聊天数据时标记已读 //指定会话消息未读数清零
conversation.markAllMessagesAsRead();
在聊天界面收到新消息时将消息标记已读
   private EMMessageListenerAdapter mEMMessageListener = new EMMessageListenerAdapter() {
@Override
public void onMessageReceived(final List<EMMessage> list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
final EMMessage emMessage = list.get(0);
if (emMessage.getUserName().equals(mUserName)) {
//标记消息已读
mChatPresenter.makeMessageRead(mUserName);
mMessageListAdapter.addNewMessage(emMessage);
smoothScrollToBottom();
}
}
});
}
}; 聊天界面返回时更新未读badge 
    @Override
protected void onResume() {
super.onResume();
updateUnreadCount();
}
消息通知
通知 
判断app是否在前台 
 public boolean isForeground() {
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
if (runningAppProcesses == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info :runningAppProcesses) {
if (info.processName.equals(getPackageName()) && info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
return false;
}
 如果app在后台则弹出Notification private void showNotification(EMMessage emMessage) {
String contentText = "";
if (emMessage.getBody() instanceof EMTextMessageBody) {
contentText = ((EMTextMessageBody) emMessage.getBody()).getMessage();
}

Intent chat = new Intent(this, ChatActivity.class);
chat.putExtra(Constant.Extra.USER_NAME, emMessage.getUserName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, chat, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new Notification.Builder(this)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.avatar1))
.setSmallIcon(R.mipmap.ic_contact_selected_2)
.setContentTitle(getString(R.string.receive_new_message))
.setContentText(contentText)
.setPriority(Notification.PRIORITY_MAX)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
} 声音  

 初始化SoundPoolprivate void initSoundPool() {
mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
mDuanSound = mSoundPool.load(this, R.raw.duan, 1);
mYuluSound = mSoundPool.load(this, R.raw.yulu, 1);
}
 播放音效 
  private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

@Override
public void onMessageReceived(List<EMMessage> list) {
if (isForeground()) {
mSoundPool.play(mDuanSound, 1, 1, 0, 0, 1);
} else {
mSoundPool.play(mYuluSound, 1, 1, 0, 0, 1);
showNotification(list.get(0));
}
}
}; 多设备登录 
环信官方文档private EMConnectionListener mEMConnectionListener = new EMConnectionListener() {
@Override
public void onConnected() {

}

@Override
public void onDisconnected(int i) {
if (i == EMError.USER_LOGIN_ANOTHER_DEVICE) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
startActivity(LoginActivity.class);
toast(getString(R.string.user_login_another_device));
}
});
}
}
};
github地址:https://github.com/uncleleonfan/QQDemo 查看全部
 项目简介 

   本项目是即时通讯的示例项目,使用了MVP模式,集成了环信SDK和Bmob后端云,展示了即时通讯基本功能的实现,包括注册登录,退出登录,联系人列表,添加好友,删除好友,收发消息,消息提醒等功能。
 
 使用的开源项目 

 
 
学习目标 
  •  环信SDK的集成与使用
  •  MVP模式的运用
  •  ORM数据库的集成与使用
  •  模块化思想的运用


即时通讯 IM(Instant Messaging)
允许两人或多人使用网络即时的传递文字讯息、档案、语音与视频交流。

相关产品
  •  鼻祖 ICQ
  •  国内主流 QQ 微信 陌陌 YY等
  •  国外主流 Facebook Messenger WhatsApp Skype Instagram Line


第三方服务平台 


环信 

001.png


环信集成
  1. 注册并创建应用
  2. 下载SDK
  3. SDK的导入
  4. SDK初始化

 
so文件夹

1. 放在jniLibs
2. 也可以放在libs目录下,不过需要在模块下的配置文件中配置
  
 android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}

注意事项

运行出错:Didn't find class "com.hyphenate.chat.adapter.EMACallSession",原因是hyphenatechat_3.2.0.jar包内没有该类。

解决办法:导入Demo源码中EaseUI库里面的hyphenatechat_3.2.0.jar替换。

软件架构

MVC

MVC应用于Ruby on Rails, Spring Framework, iOS开发和 ASP.NET等。
  •  Model: 获取数据的业务逻辑,网络操作,数据库操作
  •  View: UI
  •  Controller: 操作Model层获取数据传递给UI


002.png


服务器端的MVC

003.png

Android中MVC

Android中并没有清晰的MVC框架,如果把Activity当做Controller,根据我们实际开发经验,里面会有大量的UI操作,所以V和C就傻傻分不清了。
  •  Model:Java Bean, NetworkManager, DataBaseHelper
  •  View: xml res
  •  Controller: Activity Fragment
  •  ArrayList-ListView-Adapter(MVC)


MVP
MVP主要应用于ASP.NET等。MVP与MVC主要区别是View和Model不再耦合。
  •  Model: 获取数据的业务逻辑,网络操作,数据库操作
  •  View: UI
  •  Presenter: 操作Model层获取数据传递给UI

 
004.png


MVVM

MVVM主要应用于WPF, Silverlight, Caliburn, nRoute等。
  1.  Model: 获取数据的业务逻辑,网络操作,数据库操作
  2.  View: UI
  3.  ViewModel: 将View和Model绑定


005.png


Android中MVVM

 
软件架构的核心思想
分层分模块
006.png

 
参考

 
准备好了么? 开车啦!!!
 
包的创建
  •  adapter 存放适配器
  •  app 存放常量类,Application类以及一些app层级的全局类
  •  database 数据库相关类
  •  event EventBus使用的事件类
  •  factory 工厂类
  •  model 数据模型
  •  presenter MVP模型中的Presenter类
  •  ui 存放activity和fragment
  •  utils 工具类
  •  view MVP模型中的View类
  •  widget 自定义控件


基类的创建
  •  BaseActivity
  •  BaseFragment


Splash界面


功能需求
  1. 如果没有登录,延时2s, 跳转到登录界面
  2. 如果已经登录,则跳转到主界面


MVP实现
  •  SplashView
  •  SplashPresenter


判断是否登录环信
@Override
public void checkLoginStatus() {
if (EMClient.getInstance().isLoggedInBefore() && EMClient.getInstance().isConnected()) {
mSplashView.onLoggedIn();
} else {
mSplashView.onNotLogin();
}
}

 登录界面

功能需求
  1. 点击登录按钮或者点击虚拟键盘上的Action键都能发起登录操作
  2. 点击新用户,跳转到注册界面。


MVP实现
  •  LoginView
  •  LoginPresenter


IME Options

注意配置EditText的imeOptions属性时,需要配合inputType才能起作用。
android:imeOptions="actionNext"//下一个
android:imeOptions="actionGo"//启动
android:imeOptions="actionDone"//完成
android:imeOptions="actionPrevious"//上一个
android:imeOptions="actionSearch"//搜索
android:imeOptions="actionSend"//发送
监听软键盘Action事件,发起登录 
private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {

@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
startLogin();
return true;
}
return false;
}
};
EMCallBack的适配器 
 
   EMCallBack是环信的一个请求回调接口,包括请求成功的回调onSuccess,请求失败的回调onError和请求进度回调onProgress,
但在实际使用过程中,通常只使用到请求成功和失败的回调,请求进度回调通常留在那里成了一个空方法,对于一个有
代码洁癖的搬砖师来说,这是很难受的。所以我们可以创建一个适配器类实现这个接口,使用时用适配器类来替换EMCallBack
接口,这样只需要覆写我们想覆写的方法就可以了。
//EMCallBack接口的适配器
public class EMCallBackAdapter implements EMCallBack{

@Override
public void onSuccess() {

}

@Override
public void onError(int i, String s) {

}

@Override
public void onProgress(int i, String s) {

}
}

//EMCallBack适配器的使用
private EMCallBackAdapter mEMCallBack = new EMCallBackAdapter() {

@Override
public void onSuccess() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginSuccess();
}
});
}

@Override
public void onError(int i, String s) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginFailed();
}
});
}
};
Android6.0动态权限管理

009.png



Andrioid6.0对权限进行了分组,涉及到用户敏感信息的权限只能动态的去获取。当应用的targetSdkVersion小于23时,
会默认采用以前的权限管理机制,当targetSdkVersion大于等于23时并且运行在Andorid6.0系统上,它才会采用这套新的权限管理机制。

参考 


动态获取写磁盘权限

环信SDK内部维护了一个数据库来存储聊天记录等数据,需要读写磁盘的权限,所以我们在用户登录之前要申请该权限。
类似的,很多同学在集成百度地图或者高德地图时常常不能正常定位,往往是忽略了在Android6.0上需要获取位置的权限。
/**
* 是否有写磁盘权限
*/
private boolean hasWriteExternalStoragePermission() {
int result = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
return result == PermissionChecker.PERMISSION_GRANTED;
}

/**
* 申请权限
*/
private void applyPermission() {
String permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
ActivityCompat.requestPermissions(this, permissions, REQUEST_WRITE_EXTERNAL_STORAGE);
}

/**
* 申请权限回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions, @NonNull int grantResults) {
switch (requestCode) {
case REQUEST_WRITE_EXTERNAL_STORAGE:
if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) {
login();
} else {
toast(getString(R.string.not_get_permission));
}
break;
}
}

注册界面

010.png


011.png


功能需求
  1. 用户名的长度必须是3-20位,首字母必须为英文字符,其他字符则除了英文外还可以是数字或者下划线。
  2. 密码必须是3-20位的数字。
  3. 密码和确认密码一致


MVP实现
  • RegisterView
  • RegisterPresenter


正则表达式

 
private static final String USER_NAME_REGEX = "^[a-zA-Z]\\w{2,19}$";//用户名的正则表达式
private static final String PASSWORD_REGEX = "^[0-9]{3,20}$";//密码的正则表达式

  •  ^ 匹配输入字符串的开始位置
  • [a-zA-Z]     字符范围。匹配指定范围内的任意字符。
  • \w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
  •  $ 匹配输入字符串的结束位置

 注册流程
  1.  实际项目中,注册会将用户名和密码注册到APP的服务器,然后APP的服务器再通过REST API方式注册到环信服务器。
  2.  由于本项目没有APP服务器,会将用户数据注册到第三方云数据库Bmob,注册成功后,再在客户端发送请求注册到环信服务器。


012.png


云数据库


 
Bmob集成
Bmob开发文档
 
  •  注册创建应用
  •  下载SDK
  •  导入SDK
  •  初始化SDk


隐藏软键盘
在点击注册按钮发起注册流程后,我们需要隐藏掉软键盘并且弹出进度条。
protected void hideSoftKeyboard() {
if (mInputMethodManager == null) {
mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
}
mInputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}

注册用户到Bmob
Bmob用户管理
private void registerBmob(final String userName, final String pwd) {
User user = new User(userName, pwd);
user.signUp(new SaveListener<User>() {
@Override
public void done(User user, BmobException e) {
if (e == null) {
registerEaseMob(userName, pwd);
} else {
notifyRegisterFailed(e);
}
}
});
}
注册到环信
private void registerEaseMob(final String userName, final String pwd) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
try {
EMClient.getInstance().createAccount(userName, pwd);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mRegisterView.onRegisterSuccess();
}
});
} catch (HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mRegisterView.onRegisterError();
}
});
}
}
});
}

用户名已注册的处理
当我们注册一个用户名到Bmob云数据库后,使用相同的用户名再次注册,Bmob会返回错误码202,信息为username '%s' already taken。
所以我们应该处理该错误,通知用户换一个用户名注册。
Bmob错误码
  
private void notifyRegisterFailed(BmobException e) {
if (e.getErrorCode() == Constant.ErrorCode.USER_ALREADY_EXIST) {
mRegisterView.onResisterUserExist();
} else {
mRegisterView.onRegisterError();
}
}
主界面
013.png

底部导航条 
主界面底部是一个Tab导航条,实现的方法可以有很多种,我们可以使用RadioGroup或者FragmentTabHost, 或者我们可以自定义一个控件,
为了不重复造轮子,本Demo使用了第三方导航条BottomBar。BottomBar的使用请参考其Github地址。

第三方导航条


Fragment的切换
    
private OnTabSelectListener mOnTabSelectListener = new OnTabSelectListener() {
@Override
public void onTabSelected(@IdRes int tabId) {
FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, FragmentFactory.getInstance().getFragment(tabId)).commit();
}
};
动态界面

014.png


MVP实现
  •  DynamicView
  •  DynamicPresenter


动态界面目前只实现了退出登录功能。
@Override
public void logout() {
mDynamicView.onStartLogout();
EMClient.getInstance().logout(true, mEMCallBackAdapter);
}

联系人界面

015.png


016.jpg


MVP实现
  •  ContactView
  •  ContactPresenter


使用RecyclerView实现联系人列表
RecyclerView类似ListView, 但比ListView更高级更灵活。使用RecyclerView必须指定一个适配器和一个布局管理器,
经常有同学忘记设置布局管理器,结果导致RecyclerView什么也没有显示。适配器通常继承自RecyclerView.Adapter。

RecyclerView提供这些内置管理器:
  •  LinearLayoutManager 线性布局管理器 (类似ListView效果)
  •  GridLayoutManager 网格布局管理器 (类似GridView效果)
  •  StaggeredGridLayoutManager 分散网格布局管理器(瀑布流效果)


如果要自定义一个布局管理器,请继承自RecyclerView.LayoutManager。另外,RecyclerView在默认情况下启用增添与删除item的动画,
如果要定义这些动画,请继承RecyclerView.ItemAnimator并使用RecyclerView.setItemAnimator()。

 初始化 
   
private void initRecyclerView() {
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));//设置布局管理器
mRecyclerView.setHasFixedSize(true);//设置true如果列表内容不会影响RecyclerView的大小
mContactListAdapter = new ContactListAdapter(getContext(), mContactPresenter.getContactList());
mContactListAdapter.setOnItemClickListener(mOnItemClickListener);
mRecyclerView.setAdapter(mContactListAdapter);
}

ContactListAdapter的实现 

ContactListItemView

017.png


运用模块化的思想,将联系人的列表项抽取成一个独立的自定义组合式控件ContactListItemView。ContactListItemView只需传入一个
与之对应的数据模型ContactListItem即可完成渲染。

联系人是否在同一个组

018.png


当多个联系人的首字符相同时,只有第一个ContactListItemView显示首字符,后续首字符相同的ContactListItemView均不显示首字符。
在ContactListItem中声明一个布尔型变量showFirstLetter来标记是否显示首字符。该变量在创建ContactListItem时赋值。
/**
* 获取联系人列表数据
* @throws HyphenateException
*/
private void startGetContactList() throws HyphenateException {
List<String> contacts = EMClient.getInstance().contactManager().getAllContactsFromServer();
DatabaseManager.getInstance().deleteAllContacts();
if (!contacts.isEmpty()) {
for (int i = 0; i < contacts.size(); i++) {
ContactListItem item = new ContactListItem();
item.userName = contacts.get(i);
if (itemInSameGroup(i, item)) {
item.showFirstLetter = false;
}
mContactListItems.add(item);
saveContactToDatabase(item.userName);
}
}
}

/**
* 当前联系人跟上个联系人比较,如果首字符相同则返回true
* @param i 当前联系人下标
* @param item 当前联系人数据模型
* @return true 表示当前联系人和上一联系人在同一组
*/
private boolean itemInSameGroup(int i, ContactListItem item) {
return i > 0 && (item.getFirstLetter() == mContactListItems.get(i - 1).getFirstLetter());
}
CardView的使用

019.png


CardView继承自FrameLayout, 是Material Design里面的卡片设计,带有圆角和阴影效果。CardView效果非常好看,但也不能滥用,
比如:
020.png


更多的使用规范,请参考Material Design的设计规范Material Design Cards

这里我们给ContactListItemView的布局里面包了一层CardView,实际上是不可取的做法,这里只是为了展示CardView的使用姿势,实际项目中请大家遵循Material Design的设计规范。

参考


SwipeRefreshLayout的使用  
//设置SwipeRefreshLayout的颜色
mSwipeRefreshLayout.setColorSchemeResources(R.color.qq_blue, R.color.qq_red);
//设置SwipeRefreshLayout的监听器
mSwipeRefreshLayout.setOnRefreshListener(mOnRefreshListener);
自定义控件SlideBar
分类字符数组 
rivate static final String SECTIONS = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"
, "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
 绘制居中文本
FontMetrics

021.png
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mTextSize = h * 1.0f / SECTIONS.length;//计算分配给字符的高度
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float mTextHeight = fontMetrics.descent - fontMetrics.ascent;//获取绘制字符的实际高度
mTextBaseline = mTextSize / 2 + mTextHeight/2 - fontMetrics.descent;//计算字符居中时的baseline
}

@Override
protected void onDraw(Canvas canvas) {
float x = getWidth() * 1.0f / 2;
float baseline = mTextBaseline;
for(int i = 0; i < SECTIONS.length; i++) {
canvas.drawText(SECTIONS[i], x, baseline, mPaint);
baseline += mTextSize;
}
}[/i]
在ContactFragment里面监听SlideBar的事件 

当用户在SlideBar上ACTION_DOWN和ACTION_MOVE时,如果用户触摸到的首字符发生变化,则会回调监听器onSectionChange,
当ACTION_UP时回调onSlidingFinish()。
private SlideBar.OnSlideBarChangeListener mOnSlideBarChangeListener = new SlideBar.OnSlideBarChangeListener() {
@Override
public void onSectionChange(int index, String section) {
mSection.setVisibility(View.VISIBLE);//显示中间绿色背景的首字符
mSection.setText(section);
scrollToSection(section);//滚动RecyclerView到用户滑到的首字符
}

@Override
public void onSlidingFinish() {
mSection.setVisibility(View.GONE);//隐藏中间绿色背景的首字符
}
};

联系人点击事件

当单击联系人时跳转到聊天界面,当长按联系人时弹出Dialog询问用户是否删除好友。

  
private ContactListAdapter.OnItemClickListener mOnItemClickListener = new ContactListAdapter.OnItemClickListener() {

/**
* 单击跳转到聊天界面
* @param name 点击item的联系人名字
*/
@Override
public void onItemClick(String name) {
startActivity(ChatActivity.class, Constant.Extra.USER_NAME, name);
}

/**
* 长按删除好友
* @param name 点击item的联系人名字
*/
@Override
public void onItemLongClick(final String name) {
showDeleteFriendDialog(name);
}
};

添加好友界面 

022.png


 MVP实现 
 
  • AddFriendView[list][*]AddFriendPresenter

[/*]
[/list]


搜索用户

 
@Override
public void searchFriend(final String keyword) {
mAddFriendView.onStartSearch();
//注:模糊查询只对付费用户开放,付费后可直接使用。
BmobQuery<User> query = new BmobQuery<User>();
query.addWhereContains("username", keyword).addWhereNotEqualTo("username", EMClient.getInstance().getCurrentUser());
query.findObjects(new FindListener<User>() {
@Override
public void done(List<User> list, BmobException e) {
processResult(list, e);
}
});
}
 greenDAO
greenDAO是Android SQLite数据库ORM框架的一种。ORM即对象关系映射, object/relational mapping, 将Java对象映射成数据库的表。
我们需要存储联系人数据到数据库,在添加好友界面,需要判断搜索出来的用户是否已经是联系人,如果已经是好友,则应显示“已添加”。

其他ORM框架
 

[/*]
[/list]
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]

参考 

[/*]
[/list]
[/*]
[/list]
[/*]
[/list]
[/*]
[/list]


创建实体类 
   
@Entity
public class Contact {

@Id
public Long id;

public String userName;
}
 初始化 
   
public void init(Context context) {
DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(context, Constant.Database.DATABASE_NAME, null);
SQLiteDatabase writableDatabase = devOpenHelper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(writableDatabase);
mDaoSession = daoMaster.newSession();
}
保存联系人
public void saveContact(String userName) {
Contact contact = new Contact();
contact.setUsername(userName);
mDaoSession.getContactDao().save(contact);
}

查询联系人 
   
public List<String> queryAllContacts() {
List<Contact> list = mDaoSession.getContactDao().queryBuilder().list();
ArrayList<String> contacts = new ArrayList<String>();
for (int i = 0; i < list.size(); i++) {
String contact = list.get(i).getUsername();
contacts.add(contact);
}
return contacts;
}

 删除所有联系人
public void deleteAllContacts() {
ContactDao contactDao = mDaoSession.getContactDao();
contactDao.deleteAll();
}

发送好友请求 
AddFriendItemView里面处理添加好友的点击事件 

   
@OnClick(R.id.add)
public void onClick() {
String friendName = mUserName.getText().toString().trim();
String addFriendReason = getContext().getString(R.string.add_friend_reason);
AddFriendEvent event = new AddFriendEvent(friendName, addFriendReason);//通过EventBus发布添加好友事件
EventBus.getDefault().post(event);
}
AddFriendPresenterImpl实现发送好友请求 
  
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void addFriend(AddFriendEvent event) {
try {
EMClient.getInstance().contactManager().addContact(event.getFriendName(), event.getReason());
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onAddFriendSuccess();
}
});
} catch (HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onAddFriendFailed();
}
});
}
在联系人列表中监听联系人变化
当新增好友或者被好友删除时刷新联系人列表。

 
private EMContactListenerAdapter mEMContactListener = new EMContactListenerAdapter() {

@Override
public void onContactAdded(String s) {
mContactPresenter.refreshContactList();
}

@Override
public void onContactDeleted(String s) {
mContactPresenter.refreshContactList();
}
};

聊天界面 
023.png


MVP实现 
  •  ChatView[list][*] ChatPresenter

[/*]
[/list]

UI

监听发送按钮的状态变化 
    
mEdit.addTextChangedListener(mTextWatcher);
private TextWatcherAdapter mTextWatcher = new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
mSend.setEnabled(s.length() != 0);
}
};
动画文件 
 
  •  anim文件夹:存放补间动画[list][*] animator文件夹:存放属性动画[list][*] drawable文件夹:存放帧动画

[/*]
[/list]
[/*]
[/list]


发送消息的进度动画 
   
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk ... gt%3B
<item android:drawable="@mipmap/loading1" android:duration="100"/>
<item android:drawable="@mipmap/loading2" android:duration="100"/>
<item android:drawable="@mipmap/loading3" android:duration="100"/>
<item android:drawable="@mipmap/loading4" android:duration="100"/>
<item android:drawable="@mipmap/loading5" android:duration="100"/>
<item android:drawable="@mipmap/loading6" android:duration="100"/>
<item android:drawable="@mipmap/loading7" android:duration="100"/>
<item android:drawable="@mipmap/loading8" android:duration="100"/>
</animation-list>
9文件制作
官方说明

24.png


25.png


发送一条消息 

返回消息的类型 
根据是发送的消息还是接受的消息,来创建不同的item。
@Override
public int getItemViewType(int position) {
EMMessage message = mMessages.get(position);
return message.direct() == EMMessage.Direct.SEND ? ITEM_TYPE_SEND_MESSAGE : ITEM_TYPE_RECEIVE_MESSAGE;
}
是否显示时间戳的判断 
/**
* 如果两个消息之间的时间太近,就不显示时间戳
*/
private boolean shouldShowTimeStamp(int position) {
long currentItemTimestamp = mMessages.get(position).getMsgTime();
long preItemTimestamp = mMessages.get(position - 1).getMsgTime();
boolean closeEnough = DateUtils.isCloseEnough(currentItemTimestamp, preItemTimestamp);
return !closeEnough;
}
更新消息的状态
处理三种消息状态,正在发送中INPROGRESS,发送成功SUCCESS,发送失败FAIL。

  
private void updateSendingStatus(EMMessage emMessage) {
switch (emMessage.status()) {
case INPROGRESS:
mSendMessageProgress.setVisibility(VISIBLE);
mSendMessageProgress.setImageResource(R.drawable.send_message_progress);
AnimationDrawable drawable = (AnimationDrawable) mSendMessageProgress.getDrawable();
drawable.start();
break;
case SUCCESS:
mSendMessageProgress.setVisibility(GONE);
break;
case FAIL:
mSendMessageProgress.setImageResource(R.mipmap.msg_error);
break;
}
}
 接收一条消息 
监听消息的接收,当收到一条消息后,通知MessageListAdapter进行刷新,并且滚动RecyclerView到底部。
private EMMessageListenerAdapter mEMMessageListener = new EMMessageListenerAdapter() {
@Override
public void onMessageReceived(final List<EMMessage> list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
final EMMessage emMessage = list.get(0);
mChatPresenter.makeMessageRead(mUserName);
mMessageListAdapter.addNewMessage(emMessage);
smoothScrollToBottom();
}
});
}
};
初始化聊天记录 
 

 



 
@Override
public void loadMessages(final String userName) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);
if (conversation != null) {
//获取此会话的所有消息
List<EMMessage> messages = conversation.getAllMessages();
mEMMessageList.addAll(messages);
//指定会话消息未读数清零
conversation.markAllMessagesAsRead();
}
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onMessagesLoaded();
}
});
}
});
}
加载更多聊天记录 
  
@Override
public void loadMoreMessages(final String userName) {
if (hasMoreData) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);
EMMessage firstMessage = mEMMessageList.get(0);
//SDK初始化加载的聊天记录为20条,到顶时需要去DB里获取更多
//获取startMsgId之前的pagesize条消息,此方法获取的messages SDK会自动存入到此会话中,APP中无需再次把获取到的messages添加到会话中
final List<EMMessage> messages = conversation.loadMoreMsgFromDB(firstMessage.getMsgId(), DEFAULT_PAGE_SIZE);
hasMoreData = (messages.size() == DEFAULT_PAGE_SIZE);
mEMMessageList.addAll(0, messages);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onMoreMessagesLoaded(messages.size());
}
});
}
});
} else {
mChatView.onNoMoreData();
}
}
会话界面

26.png

 MVP实现 
 
  •  ConversationView[list][*] ConversationPresenter

[/*]
[/list]


加载所有会话
@Override
public void loadAllConversations() {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
mEMConversations.addAll(conversations.values());
//根据最后一条消息的时间进行排序
Collections.sort(mEMConversations, new Comparator<EMConversation>() {
@Override
public int compare(EMConversation o1, EMConversation o2) {
return (int) (o2.getLastMessage().getMsgTime() - o1.getLastMessage().getMsgTime());
}
});
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mConversationView.onAllConversationsLoaded();
}
});
}
});
}

未读消息计数更新 
环信官方文档
会话列表中未读消息的更新 
  
private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

@Override
public void onMessageReceived(List<EMMessage> list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
toast(getString(R.string.receive_new_message));
//当收到一个新消息时,重新加载会话列表。
mConversationPresenter.loadAllConversations();
}
});
}
};

BottomBar badge的更新 
  
private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

//该回调在子线程中调用
@Override
public void onMessageReceived(List<EMMessage> list) {
updateUnreadCount();
}
};

private void updateUnreadCount() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
BottomBarTab bottomBar = mBottomBar.getTabWithId(R.id.conversations);
int count = EMClient.getInstance().chatManager().getUnreadMsgsCount();
bottomBar.setBadgeCount(count);
}
});
}

标记消息已读 
加载聊天数据时标记已读 
//指定会话消息未读数清零
conversation.markAllMessagesAsRead();

在聊天界面收到新消息时将消息标记已读
   
private EMMessageListenerAdapter mEMMessageListener = new EMMessageListenerAdapter() {
@Override
public void onMessageReceived(final List<EMMessage> list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
final EMMessage emMessage = list.get(0);
if (emMessage.getUserName().equals(mUserName)) {
//标记消息已读
mChatPresenter.makeMessageRead(mUserName);
mMessageListAdapter.addNewMessage(emMessage);
smoothScrollToBottom();
}
}
});
}
};
 聊天界面返回时更新未读badge 
    
@Override
protected void onResume() {
super.onResume();
updateUnreadCount();
}

消息通知
通知 
判断app是否在前台 

 
public boolean isForeground() {
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
if (runningAppProcesses == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info :runningAppProcesses) {
if (info.processName.equals(getPackageName()) && info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
return false;
}

 如果app在后台则弹出Notification 
private void showNotification(EMMessage emMessage) {
String contentText = "";
if (emMessage.getBody() instanceof EMTextMessageBody) {
contentText = ((EMTextMessageBody) emMessage.getBody()).getMessage();
}

Intent chat = new Intent(this, ChatActivity.class);
chat.putExtra(Constant.Extra.USER_NAME, emMessage.getUserName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, chat, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new Notification.Builder(this)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.avatar1))
.setSmallIcon(R.mipmap.ic_contact_selected_2)
.setContentTitle(getString(R.string.receive_new_message))
.setContentText(contentText)
.setPriority(Notification.PRIORITY_MAX)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
 声音  

 初始化SoundPool
private void initSoundPool() {
mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
mDuanSound = mSoundPool.load(this, R.raw.duan, 1);
mYuluSound = mSoundPool.load(this, R.raw.yulu, 1);
}

 播放音效 
  
private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

@Override
public void onMessageReceived(List<EMMessage> list) {
if (isForeground()) {
mSoundPool.play(mDuanSound, 1, 1, 0, 0, 1);
} else {
mSoundPool.play(mYuluSound, 1, 1, 0, 0, 1);
showNotification(list.get(0));
}
}
};
 多设备登录 
环信官方文档
private EMConnectionListener mEMConnectionListener = new EMConnectionListener() {
@Override
public void onConnected() {

}

@Override
public void onDisconnected(int i) {
if (i == EMError.USER_LOGIN_ANOTHER_DEVICE) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
startActivity(LoginActivity.class);
toast(getString(R.string.user_login_another_device));
}
});
}
}
};

github地址https://github.com/uncleleonfan/QQDemo