Android

Android

7
回复

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

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

11
评论

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

KevinGong 发表了文章 • 9039 次浏览 • 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 发表了文章 • 7985 次浏览 • 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...小伙伴们还有什么想知道欢迎跟帖提出。
 
2
最佳

关于 java.lang.UnsatisfiedLinkError错误 Android UnsatisfiedLinkError错误

baoshu 回复了问题 • 2 人关注 • 119 次浏览 • 2017-11-30 16:46 • 来自相关话题

1
回复

请问下环信的视频聊天离线推送问题 离线推送 视频 Android 环信_Android

回复

金鱼先森 回复了问题 • 1 人关注 • 115 次浏览 • 2017-11-20 16:14 • 来自相关话题

1
回复

android环信2.0默认添加好友 环信2.0 Android

geri_yang 回复了问题 • 2 人关注 • 128 次浏览 • 2017-11-15 19:04 • 来自相关话题

0
回复

环信2.0默认添加好友成功后,对方好友列表有我,而我没有他,是什么情况 Android 环信2.0

回复

yuan 发起了问题 • 1 人关注 • 111 次浏览 • 2017-11-05 23:35 • 来自相关话题

0
评论

环信即时通讯云V3.3.5 SDK已发布,安全传输升级,支持FCM推送 iOS Android 产品更新

产品更新 发表了文章 • 101 次浏览 • 2017-10-23 19:00 • 来自相关话题

10月23日,环信即时通讯云发布了新版本SDK,此次更新的V3.3.5版本增加了传输安全性,AndroidSDK支持FCM推送,FCM是谷歌推出的最新的Android系统级别的消息推送服务(用来替换GCM)。
iOS SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:
增加了传输安全性增加广告插件,可以收集用户信息
优化:
私有部署设置dns的接口优化私有部署重连逻辑限制用户名长度为255需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505)添加i386库解决模拟器profile时的编译问题
功能修复:
修复4G与wifi切换时偶然出现发送消息失败的bug
 
Android SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:
增加了传输安全性;支持FCM推送;

优化:
私有部署设置dns的接口;优化私有部署重连逻辑;限制用户名长度为255;需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505);

修复:
修复4G与wifi切换时偶然出现发送消息失败的bug;修复VIVO手机JobService crash问题;
 
版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载​ 查看全部
10月23日,环信即时通讯云发布了新版本SDK,此次更新的V3.3.5版本增加了传输安全性,AndroidSDK支持FCM推送,FCM是谷歌推出的最新的Android系统级别的消息推送服务(用来替换GCM)。

iOS SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:

  • 增加了传输安全性
  • 增加广告插件,可以收集用户信息

优化:
  • 私有部署设置dns的接口
  • 优化私有部署重连逻辑
  • 限制用户名长度为255
  • 需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505)
  • 添加i386库解决模拟器profile时的编译问题

功能修复:
  • 修复4G与wifi切换时偶然出现发送消息失败的bug

 
Android SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:

  • 增加了传输安全性;
  • 支持FCM推送;


优化:
  • 私有部署设置dns的接口;
  • 优化私有部署重连逻辑;
  • 限制用户名长度为255;
  • 需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505);


修复:
  • 修复4G与wifi切换时偶然出现发送消息失败的bug;
  • 修复VIVO手机JobService crash问题;

 
版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载​
1
回复

android板在连接着USB摄像头时,第一次视频通话正常,再打就是正在连接中直到挂断 Android 视频通话 视频聊天 视频

lzan13 回复了问题 • 2 人关注 • 344 次浏览 • 2017-10-12 19:28 • 来自相关话题

1
回复

群组创建时是公开群,任何人都可加入。创建后有方法可以改变isMembersOnly状态吗 Android 群组 群组添加成员

qzusers 回复了问题 • 2 人关注 • 246 次浏览 • 2017-09-30 17:54 • 来自相关话题

1
回复

大佬们,Android 环信easeui已发送消息就崩,空指针是什么原因? Android 环信_Android

baoshu 回复了问题 • 2 人关注 • 345 次浏览 • 2017-09-15 10:08 • 来自相关话题

0
评论

android studio 2.3.3 最新 中文 汉化包 韩梦飞沙 安卓工作室 美化包 Android AndroidStudio

韩梦飞沙_韩亚飞 发表了文章 • 376 次浏览 • 2017-09-05 11:59 • 来自相关话题

韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha

汉化包 百度云盘 下载地址:https://pan.baidu.com/s/1pLjwyeB 
最新最详细全面最牛逼的汉化!稳定无BUG!设置界面可以打开!不会报错!中英对照!界面酷炫!
使用汉化美化包,让你的开发工具IDE 不再单调普通,彰显个性,与众不同!
中文的 菜单工具设置信息,让你一眼看懂,让你用好这款开发软件,最大发挥它的作用!
  查看全部
韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha

汉化包 百度云盘 下载地址:https://pan.baidu.com/s/1pLjwyeB 
最新最详细全面最牛逼的汉化!稳定无BUG!设置界面可以打开!不会报错!中英对照!界面酷炫!
使用汉化美化包,让你的开发工具IDE 不再单调普通,彰显个性,与众不同!
中文的 菜单工具设置信息,让你一眼看懂,让你用好这款开发软件,最大发挥它的作用!
 
3
回复

华为p10 无法接收到离线消息 环信_Android 有专职工程师值守 Android

geri_yang 回复了问题 • 3 人关注 • 620 次浏览 • 2017-09-04 14:57 • 来自相关话题

1
回复

android离线推送怎样设置带铃声的推送? 有专职工程师值守 Android

geri_yang 回复了问题 • 2 人关注 • 396 次浏览 • 2017-08-01 20:11 • 来自相关话题

1
回复

关于回话列表 怎么弄啊 环信 Android

geri_yang 回复了问题 • 2 人关注 • 334 次浏览 • 2017-07-20 18:46 • 来自相关话题

0
评论

【环信征文】Android程序员北漂记-从逃离北上广到逃回北上广 Android

东风玖哥 发表了文章 • 2091 次浏览 • 2017-07-04 13:55 • 来自相关话题

司马雼是我的学长,先后在国内两家top 10大厂担任资深Android工程师,对Android技术有如痴如醉的热情,并且乐于帮助同行,最难得的是他还有一个漂亮的女朋友,不愧是Android程序员中的人生赢家。我将他的经历稍作加工后用明清小说的笔法写出来,希望每个读到这篇文章的Android工程师都能走他的路。

第一回:小庙无地容巨擘 大厂有礼迎硕士诗曰:

老板抠门巧计乖,却将忠义苦挤排。

基础扎实绩点高,苍天岂能误人才。

司马雼2008年考入合肥工业大学,2012年保研本校,2014年进入合肥某小公司实习。

司马雼一开始选择的是JavaEE方向,他在实习期间就以专家的标准严格要求自己,不但让服务端的内核稳定度提升了好几个档次,还让内存消耗下降了好多个数量级。完成本职工作后还帮助运维那边修复了几次硬件的故障。不料老板嫉贤妒能成性,只爱奴才,不要人才。这个老板不但不给司马雼升职加薪,还侮辱司马雼:““雼”是“宕”的繁体字,你越俎代庖搞运维不怕把服务器搞宕机?”信而见疑、怀才不遇的司马雼愤而辞职。司马雼辞职刚好赶上某大厂(国内top 10)招Android实习生,他突击学习了几天Android就去面试了。

一面的时候主要考察Android和Java的基础知识,比如Java的数据类型、运算符优先级和Android的布局以及生命周期,基础扎实的司马雼都能准确无误地回答。

二面的时候面试官问了许多数据结构和算法、设计模式、架构的方面的问题,司马雼不但能画出好几种设计模式的UML图,还对MVC、MVP、MVVM的区别和优缺点发表了自己的看法。

最后HR面的时候收了司马雼的成绩单复印件,对他4.0的GPA和专业top 5%感到很满意。

司马雼通过了面试,顺利成为该厂的Android实习生。不久,司马雼以专业top 5%的优异成绩硕士毕业,转正成为Android工程师。而那家小公司呢,不知道什么时候就因为bug太多导致用户严重流失而倒闭了。

异史氏曰:Java玩得6的学生选择Android作为发展方向是一个明智的抉择,这个世界正处于、并将长期处于移动互联网时代。校招面试除了考察Android和Java基础知识,还考察你学习的基础理论课程的知识,最后通常还要收成绩单的复印件。大厂的校招面试一般有固定的问题和模式,比如阿里校招笔试题来自《技术之瞳》,微软校招笔试题来自《编程之美》等,尽管临阵磨枪也有一定的成功几率,但别忘了玩3年LOL的枪不可能比刷6年LeetCode的枪更快。(时间没错,沉迷LOL的大学生都考不上硕士)

第二回:荣升高级愁田舍 游子低头思莼鲈诗曰:

当年许汜初炒房,羞无才气见刘郎。

如今司马有远志,却愁无房迎新娘。

炒房团自古有之,祖师爷是东汉末年的许汜。当年许汜空有国士之名,却全无救世之意,整天就知道求田问舍,因此被胸襟海阔,志向山高,忧国忘家的刘备鄙视。此事有辛弃疾《水龙吟》为证:“求田问舍,怕应羞见,刘郎才气。”

司马雼和许汜的志向有天壤之别,他的梦想就是把有限的生命投入无限的Android技术中,他不甘心每天只做UI的微调,他发现App存在很大的改进空间,并付诸行动。

司马雼对App的代码进行了大规模重构,在MVP与MVVM中选择了MVP架构,并自主研发了一套的网络请求框架(结合了OKHttp和Gson,可以理解为国产的Retrofit)(该框架不开源,类似框架的源码:https://github.com/qiujuer/OkHttpPacker)代替OKHttp。这样一来不但减少了大量冗余代码,层次结构也变得更加清晰。


订单状态这种需要和服务器实时同步的数据,以前一直用每秒一次的轮询,司马雼发现这是App又费电又费流量的祸根,采取了用推送代替轮询的解决方案。


APK瘦身也是Android性能优化的重要组成部分,司马雼去掉了很多不必要依赖和重复的工具类,让APK打包后的体量轻了一半。

两年后,这个App完成了从3.3到6.3共13个版本的迭代,App的启动速度提升了120%,Crash 率也由8‰降低到1‰。立下汗马功劳的司马雼被任命为项目组长、技术指导、高级工程师。

尽管司马雼工作兢兢业业,也为我国的开源事业添砖加瓦,还写技术博客帮助了很多人。北京高昂的房价却让他有点羡慕那个被他鄙视了多年的许汜。

异史氏曰:Android工程师(在没有“高级”等前缀时)每天最多的工作都是UI的改来改去,想在平凡的工作中取得不平庸的业绩就要付出努力,提升App的性能是有效途径之一。贡献开源代码和分享技术文章的时间也是可以挤出来的。如果在工作中有出色的成绩,升职加薪只是时间问题。

第三回:桑梓惜别因缘浅 楷模入职即资深诗曰:

北京买房要筑台,还有堵车与雾霾。

回到省会想定居,一问工资又回来。

北京的房价、堵车、雾霾逼得司马雼决定裸辞,逃离北上广,他的目的地是上了七年学的合肥。他刚辞职,老东家的最大竞争对手(全国top 20)和另一家全国top 10的大厂都邀请他去面试。他两个月来参加了这两家大厂和合肥当地两家大厂的面试。

面试官首先询问的总是Java和Android的高级特性,Java的高级特性主要有JVM模型、类加载机制和GC原理等,Android的高级特性主要有几大FLAG和LauchMode的区别和使用场景、Binder的引用和实体以及权限系统的交互等。司马雼对技术钻研很有深度,总是对答如流。


面试的第二阶段就是让司马雼自己去讲他做过的项目,然后面试官会冷不丁的让他去解释其中某一部分,有时候让他解释当时为什么要这么做,有时候问他现在觉得有没有更好的办法。司马雼处理问题的思路和解决问题的能力给面试官留下了深刻印象。


面试官有时候会问一些该企业所在行业需要关注的Android技术,比如研究输入法的公司会询问他Android手势和多点触摸,研究物联网的公司会问他Bluetooth相关知识等。因为司马雼广泛涉猎Android知识,他的技术广度也让面试官啧啧赞叹。

每次面试官问司马雼:“你还有什么想和我说的吗?”的时候,司马雼就把他的技术博客和开源项目一股脑砸向面试官,面试官是司马雼的粉丝或者面试官正在用司马雼的开源项目的情况发生了好几次。

司马雼在合肥拿到了两个资深Android工程师的offer,尽管合肥房价不到北京的1/3,可是最多30W的年薪让司马雼在合肥买房遥遥无期。这时司马雼接到了一个电话,逃离北京前面试的全国top 10的大厂给了他60W年薪和项目经理、技术经理、资深工程师的title。他决定逃回北上广,逃离北上广的计划刚开始就结束了。

异史氏曰:有多年工作经验的求职者几乎不需要在求职网站上投简历,想让你做他的同时的同行朋友会内推你。社招面试和校招面试是不同的,社招面试没有固定的问题和模式,临时抱佛脚是行不通的。社招面试基本都会考察这几个问题:第一个阶段是Android和Java的高级特性,考察技术深度;第二个阶段是讲述自己的项目,并在中间穿插着问题,考察解决问题的经验;第三个阶段(未必有)是问该公司所在行业需要掌握的Android知识,考察技术广度和快速上手情况。技术博客和开源项目是很重要的加分项,如果平时不积累、不分享,求职者会失去很多机会。二三线城市的房价更亲民,但工资非常不人性,逃离北上广需谨慎。

第四回:专家立功施小计 淑女出闺成大礼诗曰:

经验丰富技术精,消除隐患立大功。

全球大会登讲台,抱得淑女入后宫。

话说司马雼所在的团队负责公司的Android客户端的安全工作,工作内容包含保活、防拦截、防篡改和防反编译等工作。他发现自家App存在不少可能被恶意利用的隐患:

首先,坏人可以通过NotificationListenerService拦截自家App的推送,给用户造成自家App没有推送的假象。


其次,在上一条基础上。坏人可以在虚拟机里运行一个窃取推送App,收到自家App推送后,用AccessibilityService打开Notification对应的Activity,找到里面的WebView,然后取得链接及网页中的内容,稍加修改(换logo、改名字)推送给坏人自己的App。


此外公司的微信公众号打开自己App需要用浏览器打开链接,这也给坏人以可乘之机。


更有甚者,坏人最丧心病狂的手段就是卸载了自家App后,在肉鸡中安装一个与自家App的packgaeName一致,但Signature不一样而且没有launcher的假App,这样肉鸡的用户永远也安装不了自家App,而且还不能用长按拖拽桌面icon进垃圾桶的方式删除假App。

司马雼花了两年时间为公司消除了以上隐患,荣升架构师、技术专家(时间没错,这是小说,出现未来时间很正常)。各大IT论坛、IT活动的聘书和邀请函也如雪片般飞来,不是邀请他做特约作者,就是邀请他当讲座嘉宾。

某IT大会在北京举行,司马雼作为特约嘉宾在台上妙语连珠、口若悬河。大会期间,司马雼结识了某IT社区的技术编辑椎名,这位淑女负责大会的后勤和报道工作。椎名不但知书达理、兰心蕙质,相貌也倾国倾城,有诗为证:乐天浔阳歌《长恨》,陈王洛水赋《感甄》。

摩诘青溪《西施咏》,潇湘红楼《明妃吟》。

没错,这诗集合了白居易《长恨歌》、曹植《感甄赋》、王维《西施咏》、林黛玉《明妃吟》,椎名的美貌用古往今来的美女诗都搁一块也写不完。淑女椎名倾慕司马雼的才华,从此司马雼正式脱单。

不久,年薪100W的司马雼在北京买房定居。又过了几个月,这对为我国IT事业做出了巨大贡献的情侣在北京举办了盛大的婚礼。

异史氏曰:公司大到一定程度,别有用心的竞争对手就会搞小动作,因此想成为大厂的技术专家,懂点安全方面的知识是很有帮助的。程序员是经常被嘲笑为“注定孤独一生”的群体,但出人头地的程序员抱得美人归的可能性非常大,毕竟女人的颜值通常和她的男朋友的收入成正比。
 
本文首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201707/543909.htm 查看全部
司马雼是我的学长,先后在国内两家top 10大厂担任资深Android工程师,对Android技术有如痴如醉的热情,并且乐于帮助同行,最难得的是他还有一个漂亮的女朋友,不愧是Android程序员中的人生赢家。我将他的经历稍作加工后用明清小说的笔法写出来,希望每个读到这篇文章的Android工程师都能走他的路。

第一回:小庙无地容巨擘 大厂有礼迎硕士
诗曰:

老板抠门巧计乖,却将忠义苦挤排。

基础扎实绩点高,苍天岂能误人才。


司马雼2008年考入合肥工业大学,2012年保研本校,2014年进入合肥某小公司实习。

司马雼一开始选择的是JavaEE方向,他在实习期间就以专家的标准严格要求自己,不但让服务端的内核稳定度提升了好几个档次,还让内存消耗下降了好多个数量级。完成本职工作后还帮助运维那边修复了几次硬件的故障。不料老板嫉贤妒能成性,只爱奴才,不要人才。这个老板不但不给司马雼升职加薪,还侮辱司马雼:““雼”是“宕”的繁体字,你越俎代庖搞运维不怕把服务器搞宕机?”信而见疑、怀才不遇的司马雼愤而辞职。司马雼辞职刚好赶上某大厂(国内top 10)招Android实习生,他突击学习了几天Android就去面试了。

一面的时候主要考察Android和Java的基础知识,比如Java的数据类型、运算符优先级和Android的布局以及生命周期,基础扎实的司马雼都能准确无误地回答。

二面的时候面试官问了许多数据结构和算法、设计模式、架构的方面的问题,司马雼不但能画出好几种设计模式的UML图,还对MVC、MVP、MVVM的区别和优缺点发表了自己的看法。

最后HR面的时候收了司马雼的成绩单复印件,对他4.0的GPA和专业top 5%感到很满意。

司马雼通过了面试,顺利成为该厂的Android实习生。不久,司马雼以专业top 5%的优异成绩硕士毕业,转正成为Android工程师。而那家小公司呢,不知道什么时候就因为bug太多导致用户严重流失而倒闭了。

异史氏曰:Java玩得6的学生选择Android作为发展方向是一个明智的抉择,这个世界正处于、并将长期处于移动互联网时代。校招面试除了考察Android和Java基础知识,还考察你学习的基础理论课程的知识,最后通常还要收成绩单的复印件。大厂的校招面试一般有固定的问题和模式,比如阿里校招笔试题来自《技术之瞳》,微软校招笔试题来自《编程之美》等,尽管临阵磨枪也有一定的成功几率,但别忘了玩3年LOL的枪不可能比刷6年LeetCode的枪更快。(时间没错,沉迷LOL的大学生都考不上硕士)

第二回:荣升高级愁田舍 游子低头思莼鲈
诗曰:

当年许汜初炒房,羞无才气见刘郎。

如今司马有远志,却愁无房迎新娘。


炒房团自古有之,祖师爷是东汉末年的许汜。当年许汜空有国士之名,却全无救世之意,整天就知道求田问舍,因此被胸襟海阔,志向山高,忧国忘家的刘备鄙视。此事有辛弃疾《水龙吟》为证:“求田问舍,怕应羞见,刘郎才气。”

司马雼和许汜的志向有天壤之别,他的梦想就是把有限的生命投入无限的Android技术中,他不甘心每天只做UI的微调,他发现App存在很大的改进空间,并付诸行动。

司马雼对App的代码进行了大规模重构,在MVP与MVVM中选择了MVP架构,并自主研发了一套的网络请求框架(结合了OKHttp和Gson,可以理解为国产的Retrofit)(该框架不开源,类似框架的源码:https://github.com/qiujuer/OkHttpPacker)代替OKHttp。这样一来不但减少了大量冗余代码,层次结构也变得更加清晰。


订单状态这种需要和服务器实时同步的数据,以前一直用每秒一次的轮询,司马雼发现这是App又费电又费流量的祸根,采取了用推送代替轮询的解决方案。


APK瘦身也是Android性能优化的重要组成部分,司马雼去掉了很多不必要依赖和重复的工具类,让APK打包后的体量轻了一半。

两年后,这个App完成了从3.3到6.3共13个版本的迭代,App的启动速度提升了120%,Crash 率也由8‰降低到1‰。立下汗马功劳的司马雼被任命为项目组长、技术指导、高级工程师。

尽管司马雼工作兢兢业业,也为我国的开源事业添砖加瓦,还写技术博客帮助了很多人。北京高昂的房价却让他有点羡慕那个被他鄙视了多年的许汜。

异史氏曰:Android工程师(在没有“高级”等前缀时)每天最多的工作都是UI的改来改去,想在平凡的工作中取得不平庸的业绩就要付出努力,提升App的性能是有效途径之一。贡献开源代码和分享技术文章的时间也是可以挤出来的。如果在工作中有出色的成绩,升职加薪只是时间问题。

第三回:桑梓惜别因缘浅 楷模入职即资深
诗曰:

北京买房要筑台,还有堵车与雾霾。

回到省会想定居,一问工资又回来。


北京的房价、堵车、雾霾逼得司马雼决定裸辞,逃离北上广,他的目的地是上了七年学的合肥。他刚辞职,老东家的最大竞争对手(全国top 20)和另一家全国top 10的大厂都邀请他去面试。他两个月来参加了这两家大厂和合肥当地两家大厂的面试。

面试官首先询问的总是Java和Android的高级特性,Java的高级特性主要有JVM模型、类加载机制和GC原理等,Android的高级特性主要有几大FLAG和LauchMode的区别和使用场景、Binder的引用和实体以及权限系统的交互等。司马雼对技术钻研很有深度,总是对答如流。


面试的第二阶段就是让司马雼自己去讲他做过的项目,然后面试官会冷不丁的让他去解释其中某一部分,有时候让他解释当时为什么要这么做,有时候问他现在觉得有没有更好的办法。司马雼处理问题的思路和解决问题的能力给面试官留下了深刻印象。


面试官有时候会问一些该企业所在行业需要关注的Android技术,比如研究输入法的公司会询问他Android手势和多点触摸,研究物联网的公司会问他Bluetooth相关知识等。因为司马雼广泛涉猎Android知识,他的技术广度也让面试官啧啧赞叹。

每次面试官问司马雼:“你还有什么想和我说的吗?”的时候,司马雼就把他的技术博客和开源项目一股脑砸向面试官,面试官是司马雼的粉丝或者面试官正在用司马雼的开源项目的情况发生了好几次。

司马雼在合肥拿到了两个资深Android工程师的offer,尽管合肥房价不到北京的1/3,可是最多30W的年薪让司马雼在合肥买房遥遥无期。这时司马雼接到了一个电话,逃离北京前面试的全国top 10的大厂给了他60W年薪和项目经理、技术经理、资深工程师的title。他决定逃回北上广,逃离北上广的计划刚开始就结束了。

异史氏曰:有多年工作经验的求职者几乎不需要在求职网站上投简历,想让你做他的同时的同行朋友会内推你。社招面试和校招面试是不同的,社招面试没有固定的问题和模式,临时抱佛脚是行不通的。社招面试基本都会考察这几个问题:第一个阶段是Android和Java的高级特性,考察技术深度;第二个阶段是讲述自己的项目,并在中间穿插着问题,考察解决问题的经验;第三个阶段(未必有)是问该公司所在行业需要掌握的Android知识,考察技术广度和快速上手情况。技术博客和开源项目是很重要的加分项,如果平时不积累、不分享,求职者会失去很多机会。二三线城市的房价更亲民,但工资非常不人性,逃离北上广需谨慎。

第四回:专家立功施小计 淑女出闺成大礼
诗曰:

经验丰富技术精,消除隐患立大功。

全球大会登讲台,抱得淑女入后宫。


话说司马雼所在的团队负责公司的Android客户端的安全工作,工作内容包含保活、防拦截、防篡改和防反编译等工作。他发现自家App存在不少可能被恶意利用的隐患:

首先,坏人可以通过NotificationListenerService拦截自家App的推送,给用户造成自家App没有推送的假象。


其次,在上一条基础上。坏人可以在虚拟机里运行一个窃取推送App,收到自家App推送后,用AccessibilityService打开Notification对应的Activity,找到里面的WebView,然后取得链接及网页中的内容,稍加修改(换logo、改名字)推送给坏人自己的App。


此外公司的微信公众号打开自己App需要用浏览器打开链接,这也给坏人以可乘之机。


更有甚者,坏人最丧心病狂的手段就是卸载了自家App后,在肉鸡中安装一个与自家App的packgaeName一致,但Signature不一样而且没有launcher的假App,这样肉鸡的用户永远也安装不了自家App,而且还不能用长按拖拽桌面icon进垃圾桶的方式删除假App。

司马雼花了两年时间为公司消除了以上隐患,荣升架构师、技术专家(时间没错,这是小说,出现未来时间很正常)。各大IT论坛、IT活动的聘书和邀请函也如雪片般飞来,不是邀请他做特约作者,就是邀请他当讲座嘉宾。

某IT大会在北京举行,司马雼作为特约嘉宾在台上妙语连珠、口若悬河。大会期间,司马雼结识了某IT社区的技术编辑椎名,这位淑女负责大会的后勤和报道工作。椎名不但知书达理、兰心蕙质,相貌也倾国倾城,有诗为证:
乐天浔阳歌《长恨》,陈王洛水赋《感甄》。

摩诘青溪《西施咏》,潇湘红楼《明妃吟》。


没错,这诗集合了白居易《长恨歌》、曹植《感甄赋》、王维《西施咏》、林黛玉《明妃吟》,椎名的美貌用古往今来的美女诗都搁一块也写不完。淑女椎名倾慕司马雼的才华,从此司马雼正式脱单。

不久,年薪100W的司马雼在北京买房定居。又过了几个月,这对为我国IT事业做出了巨大贡献的情侣在北京举办了盛大的婚礼。

异史氏曰:公司大到一定程度,别有用心的竞争对手就会搞小动作,因此想成为大厂的技术专家,懂点安全方面的知识是很有帮助的。程序员是经常被嘲笑为“注定孤独一生”的群体,但出人头地的程序员抱得美人归的可能性非常大,毕竟女人的颜值通常和她的男朋友的收入成正比。
 
本文首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201707/543909.htm
2
回复

导入AndroidPdfView的时候,点击运行,会报错 环信_Android Android 有专职工程师值守

回复

justsyhxxxx 回复了问题 • 1 人关注 • 337 次浏览 • 2017-06-27 10:56 • 来自相关话题

3
回复

easeui 里的 simpledemo 登录 时 提示 用 uidemo中的账户登录即可, 在哪里啊 Android

geri_yang 回复了问题 • 6 人关注 • 1167 次浏览 • 2017-06-22 16:13 • 来自相关话题

1
回复

Android 会话列表只展示群组 Android 环信

geri_yang 回复了问题 • 2 人关注 • 370 次浏览 • 2017-06-21 12:49 • 来自相关话题

0
回复

android 3.0以后怎么获取自定义消息中Ext消息 环信 ext 接收消息 3.0 Android

回复

╰☆╮末↘ 发起了问题 • 1 人关注 • 340 次浏览 • 2017-06-16 10:30 • 来自相关话题

0
评论

Android 守护进程的实现方式 推送 Android 进程

beyond 发表了文章 • 761 次浏览 • 2017-06-15 14:38 • 来自相关话题

 
   “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。
 
为什么微信可以一直在手机后台跑着能收到消息?

    国内手机厂商对 android rom 进行了定制,对后台服务以及运行在后台的程序进行了严格的限制,微信等这些大厂商的 app 都已经通过和设备厂商合作在安装时都已经加入了系统的白名单,因此设备并不会限制对方 app 在后台运行;

我自己的APP该如何实现进程保活?
 
引导用户把当前 app 加入到设备的白名单中,解除设备对 app 的限制;小米和华为设备可以集成对应的推送实现在app 被干掉后依然收推送通知;可以自己在 app 端实现守护进程的方式,让 app 在系统级别自动回收的情况下减少被杀死的概率,这种方式对用户主动回收无效。

第一条和第二条就不多说了,环信imgeek社区里已经有了相应的文章(http://www.imgeek.org/article/825308754 )接下来介绍守护进程的实现。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在Android里永生不死的,请忽略本文!请忽略!
 什么是守护进程?

    守护进程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的。
 
守护进程的实现,本文两个核心观点:
 
提高进程优先级,降低被回收或杀死概率在进程被干掉后,进行拉起
 要实现实现上边所说,通过下边几点来实现,首先我们需要了解下进程的优先级划分:

Process Importance记录在ActivityManager.java类中:**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* 此进程正在运行前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情
*/
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
* 这个过程不是用户的直接意识到,但在某种程度上是他们可以察觉的。
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* 此进程正在运行前台UI,但设备处于睡眠状态,因此用户不可见,意思是用户意识不到的进程,因为他们看不到或与它交互,
* 但它是相当重要,因为用户解锁设备时期望的返回到这个进程
*/
public static final int IMPORTANCE_TOP_SLEEPING = 150;

/**
* 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* 此进程正在运行某些对用户主动可见的内容,但不是直接显示在UI,
* 这可能运行在当前前台之后的窗口(因此暂停并且其状态被保存,不与用户交互,但在某种程度上对他们可见);
* 也可能在系统的控制下运行其他服务,
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* 服务进程,此进程包含在后台保持运行的服务,这些后台服务用户察觉不到,是无感知的,所以它们可以由系统相对自由地杀死
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* 后台进程
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* 空进程,此进程没有任何正在运行的代码
*/
public static final int IMPORTANCE_EMPTY = 500;

// 此过程不存在。
public static final int IMPORTANCE_GONE = 1000;进程回收机制

了解进程优先级之后,我们还需要知道一个进程回收机制的东西;这里参考AngelDevil在博客园上的一篇文章:
 
详情参考:【Android Low Memory Killer】
 
Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:alloc_pages -> out_of_memory() -> select_bad_process() -> badness()在Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
// 进程不存在。
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考/**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
*/
public static int procStateToImportance(int procState) {
if (procState == PROCESS_STATE_NONEXISTENT) {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_BACKGROUND;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
} else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
} else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
return IMPORTANCE_FOREGROUND;
}
}一般情况下,设备端进程被干掉有一下几种情况




由以上分析,我们可以可以总结出,如果想提高我们应用后台运行时间,就需要提高当前应用进程优先级,来减少被杀死的概率

守护进程的实现

分析了那么多,现在对Android自身后台进程管理,以及进程的回收也有了一个大致的了解,后边我们要做的就是想尽一切办法去提高应用进程优先级,降低进程被杀的概率;或者是在被杀死后能够重新启动后台守护进程

1.模拟前台进程

第一种方式就是利用系统漏洞,使用startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高(这里所说的最高是服务所能达到的最高,即1);

这种方式在7.x之前都是很好用的,QQ、微信、IReader、Keep 等好多应用都是用的这种方式实现;因为在7.x 以后的设备上,这种伪装前台进程的方式也会显示出来通知栏提醒,这个是取消不掉的,虽然Google现在还没有对这种方式加以限制,不过这个已经能够被用户感知到了,这种方式估计也用不了多久了

下边看下实现方式,这边这个VMDaemonService就是一个守护进程服务,其中在服务的onStartCommand()方法中调用startForeground()将服务进程设置为前台进程,当运行在 API18 以下的设备是可以直接设置,API18 以上需要实现一个内部的Service,这个内部类实现和外部类同样的操作,然后结束自己;当这个服务启动后就会创建一个定时器去发送广播,当我们的核心服务被干掉后,就由另外的广播接收器去接收我们守护进程发出的广播,然后唤醒我们的核心服务;/**
* 以实现内部 Service 类的方式实现守护进程,这里是利用 android 漏洞提高当前进程优先级
*
* Created by lzan13 on 2017/3/7.
*/
public class VMDaemonService extends Service {

private final static String TAG = VMDaemonService.class.getSimpleName();

// 定时唤醒的时间间隔,这里为了自己测试方边设置了一分钟
private final static int ALARM_INTERVAL = 1 * 60 * 1000;
// 发送唤醒广播请求码
private final static int WAKE_REQUEST_CODE = 5121;
// 守护进程 Service ID
private final static int DAEMON_SERVICE_ID = -5121;

@Override public void onCreate() {
Log.i(TAG, "VMDaemonService->onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
// 利用 Android 漏洞提高进程优先级,
startForeground(DAEMON_SERVICE_ID, new Notification());
// 当 SDk 版本大于18时,需要通过内部 Service 类启动同样 id 的 Service
if (Build.VERSION.SDK_INT >= 18) {
Intent innerIntent = new Intent(this, DaemonInnerService.class);
startService(innerIntent);
}

// 发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(VMWakeReceiver.DAEMON_WAKE_ACTION);

PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
ALARM_INTERVAL, operation);

/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
*/
return START_STICKY;
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "VMDaemonService->onDestroy");
super.onDestroy();
}

/**
* 实现一个内部的 Service,实现让后台服务的优先级提高到前台服务,这里利用了 android 系统的漏洞,
* 不保证所有系统可用,测试在7.1.1 之前大部分系统都是可以的,不排除个别厂商优化限制
*/
public static class DaemonInnerService extends Service {

@Override public void onCreate() {
Log.i(TAG, "DaemonInnerService -> onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "DaemonInnerService -> onStartCommand");
startForeground(DAEMON_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "DaemonInnerService -> onDestroy");
super.onDestroy();
}
}
}当我们启动这个守护进程的时候,就可以使用以下adb命令查看当前程序的进程情况(需要adb shell进去设备),
为了等下区分进程优先级,我启动了一个普通的后台进程,两外两个一个是我们启动的守护进程,一个是当前程序的核心进程,可以看到除了后台进程外,另外两个进程都带有isForeground=true的属性:# 这个命令的 services 可以换成 service,这样会只显示当前,进程,不显示详细内容
# dumpsys activity services <Your Package Name>
root@vbox86p:/ # dumpsys activity services com.vmloft.develop.daemon
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{170fe1dd u0 com.vmloft.develop.daemon/.services.VMDaemonService}
intent={cmp=com.vmloft.develop.daemon/.services.VMDaemonService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{173fe77f 2370:com.vmloft.develop.daemon:daemon/u0a68}
isForeground=true foregroundId=-5121 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-6s196ms startingBgTimeout=--
lastActivity=-6s157ms restartTime=-6s157ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2fee4f84 u0 com.vmloft.develop.daemon/.services.VMCoreService}
intent={cmp=com.vmloft.develop.daemon/.services.VMCoreService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{18c6a1b4 2343:com.vmloft.develop.daemon/u0a68}
isForeground=true foregroundId=-5120 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-28s136ms startingBgTimeout=--
lastActivity=-28s136ms restartTime=-28s136ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2ef6909e u0 com.vmloft.develop.daemon/.services.VMBackgroundService}
intent={cmp=com.vmloft.develop.daemon/.services.VMBackgroundService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:background
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{29f8734c 2388:com.vmloft.develop.daemon:background/u0a68}
createTime=-3s279ms startingBgTimeout=--
lastActivity=-3s262ms restartTime=-3s262ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1然后我们可以用下边的命令查看ProcessID# 这个命令可以查看当前DProcessID(数据结果第二列),我们可以看到当前程序有两个进程
# ps | grep com.vmloft.develop.daemon
root@vbox86p:/ # ps | grep com.vmloft.develop.daemon
u0_a68 2343 274 1012408 42188 ffffffff f74f1b45 S com.vmloft.develop.daemon
u0_a68 2370 274 997012 26152 ffffffff f74f1b45 S com.vmloft.develop.daemon:daemon
u0_a68 2388 274 997012 25668 ffffffff f74f1b45 S com.vmloft.develop.daemon:background有了ProcessID之后,我们可以根据这个ProcessID获取到当前进程的优先级状态Process State,对应Linux层的oom_adj
可以看到当前核心进程的级别为0,因为这个表示当前程序运行在前台 UI 界面,守护进程级别为1,因为我们利用漏洞设置成了前台进程,虽然不可见,但是他的级别也是比较高的,仅次于前台 UI 进程,然后普通后台进程级别为4;当我们退到后台时,可以看到核心进程的级别变为1了,这就是因为我们利用startForeground()将进程设置成前台进程的原因,这样就降低了进程被系统回收的概率了;# 这个命令就是通过 ProcessID 输出其对应 oom_adj
# cat /proc/ProcessID/oom_adj
# 程序在前台时,查询进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
0
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
# 当程序退到后台时,再次查看进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
1
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4可以看到这种方式确实能够提高进程优先级,但是在一些国产的设备上还是会被杀死的,比我我测试的时候小米点击清空最近运行的应用进程就别干掉了;当把应用加入到设备白名单里就不会被杀死了,微信就是这样,人家直接装上之后就已经在白名单里了,我们要做的就是在用户使用中引导他们将我们的程序设置进白名单,将守护进程和白名单结合起来,这样才能保证我们的应用持续或者

2.JobScheduler机制唤醒

Android系统在5.x以上版本提供了一个JobSchedule接口,系统会根据自己实现定时去调用改接口传递的进程去实现一些操作,而且这个接口在被强制停止后依然能够正常的启动;不过在一些国产设备上可能无效,比如小米;
下边是 JobServcie 的实现:/**
* 5.x 以上使用 JobService 实现守护进程,这个守护进程要做的工作很简单,就是启动应用的核心进程
* Created by lzan13 on 2017/3/8.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VMDaemonJobService extends JobService {

private final static String TAG = VMDaemonJobService.class.getSimpleName();

@Override public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob");
// 这里为了掩饰直接启动核心进程,没有做其他判断操作
startService(new Intent(getApplicationContext(), VMCoreService.class));
return false;
}

@Override public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob");
return false;
}
}我们要做的就是在需要的时候调用JobSchedule的schedule来启动任务;剩下的就不需要关心了,JobSchedule会帮我们做好,下边就是我这边实现的启动任务的方法:/**
* 5.x以上系统启用 JobScheduler API 进行实现守护进程的唤醒操作
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startJobScheduler() {
int jobId = 1;
JobInfo.Builder jobInfo = new JobInfo.Builder(jobId, new ComponentName(this, VMDaemonJobService.class));
jobInfo.setPeriodic(10000);
jobInfo.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo.build());
}3.系统 Service START_STICKY 机制重启

在实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在Service挂掉后自动拉活;不过这种方式只适合比较原生一些的系统,像小米,华为等这些定制化比较高的第三方厂商,他们都已经把这些给限制掉了;@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
* 3.一些定制化比较高的第三方系统也不适用
*/
return START_STICKY;
}这种方式在以下两种情况无效:
Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,这个服务就不能再次重启了;进程被取得Root权限的管理工具或系统工具通过fores-top方式停止掉,无法重启;一些定制化比较高的第三方系统也不适用

4.其他保活方式
利用 Native 本地进程,这个主要使用到 jni 调用底层实现,而且在 Android 5.x 以后对这个限制也比较高,不适用了,暂时不研究集成第三方SDK互相唤醒,这个只要正常集成了第三方的SDK,并使用了他们对应的服务,当一个设备安装的多个应用都集成了某一个第三方SDK时,启动任意一个 app 都会唤醒其他的 app,不过这个在一些新版的国内厂商系统也是做了限制,这种方式并没有什么效果一像素的 Activity 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;
结语

事事没有绝对,万物总有一些漏洞,就算上边的那些方式不可用了,后边肯定还会出现其他的方式;我们不能保证我们的应用不死,但我们可以提高存活率;

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是 查看全部
 
   “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。
 
为什么微信可以一直在手机后台跑着能收到消息?

    国内手机厂商对 android rom 进行了定制,对后台服务以及运行在后台的程序进行了严格的限制,微信等这些大厂商的 app 都已经通过和设备厂商合作在安装时都已经加入了系统的白名单,因此设备并不会限制对方 app 在后台运行;

我自己的APP该如何实现进程保活?
 
  1. 引导用户把当前 app 加入到设备的白名单中,解除设备对 app 的限制;
  2. 小米和华为设备可以集成对应的推送实现在app 被干掉后依然收推送通知;
  3. 可以自己在 app 端实现守护进程的方式,让 app 在系统级别自动回收的情况下减少被杀死的概率,这种方式对用户主动回收无效。


第一条和第二条就不多说了,环信imgeek社区里已经有了相应的文章(http://www.imgeek.org/article/825308754 )接下来介绍守护进程的实现。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在Android里永生不死的,请忽略本文!请忽略!
 什么是守护进程?

    守护进程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的。
 
守护进程的实现,本文两个核心观点:
 
  1. 提高进程优先级,降低被回收或杀死概率
  2. 在进程被干掉后,进行拉起

 要实现实现上边所说,通过下边几点来实现,首先我们需要了解下进程的优先级划分:

Process Importance记录在ActivityManager.java类中:
**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* 此进程正在运行前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情
*/
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
* 这个过程不是用户的直接意识到,但在某种程度上是他们可以察觉的。
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* 此进程正在运行前台UI,但设备处于睡眠状态,因此用户不可见,意思是用户意识不到的进程,因为他们看不到或与它交互,
* 但它是相当重要,因为用户解锁设备时期望的返回到这个进程
*/
public static final int IMPORTANCE_TOP_SLEEPING = 150;

/**
* 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* 此进程正在运行某些对用户主动可见的内容,但不是直接显示在UI,
* 这可能运行在当前前台之后的窗口(因此暂停并且其状态被保存,不与用户交互,但在某种程度上对他们可见);
* 也可能在系统的控制下运行其他服务,
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* 服务进程,此进程包含在后台保持运行的服务,这些后台服务用户察觉不到,是无感知的,所以它们可以由系统相对自由地杀死
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* 后台进程
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* 空进程,此进程没有任何正在运行的代码
*/
public static final int IMPORTANCE_EMPTY = 500;

// 此过程不存在。
public static final int IMPORTANCE_GONE = 1000;
进程回收机制

了解进程优先级之后,我们还需要知道一个进程回收机制的东西;这里参考AngelDevil在博客园上的一篇文章:
 
详情参考:【Android Low Memory Killer】
 
Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:
alloc_pages -> out_of_memory() -> select_bad_process() -> badness()
在Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态
// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java 
// 进程不存在。
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;
Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考
/** 
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
*/
public static int procStateToImportance(int procState) {
if (procState == PROCESS_STATE_NONEXISTENT) {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_BACKGROUND;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
} else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
} else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
return IMPORTANCE_FOREGROUND;
}
}
一般情况下,设备端进程被干掉有一下几种情况
QQ截图20170615143439.jpg

由以上分析,我们可以可以总结出,如果想提高我们应用后台运行时间,就需要提高当前应用进程优先级,来减少被杀死的概率

守护进程的实现

分析了那么多,现在对Android自身后台进程管理,以及进程的回收也有了一个大致的了解,后边我们要做的就是想尽一切办法去提高应用进程优先级,降低进程被杀的概率;或者是在被杀死后能够重新启动后台守护进程

1.模拟前台进程

第一种方式就是利用系统漏洞,使用startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高(这里所说的最高是服务所能达到的最高,即1);

这种方式在7.x之前都是很好用的,QQ、微信、IReader、Keep 等好多应用都是用的这种方式实现;因为在7.x 以后的设备上,这种伪装前台进程的方式也会显示出来通知栏提醒,这个是取消不掉的,虽然Google现在还没有对这种方式加以限制,不过这个已经能够被用户感知到了,这种方式估计也用不了多久了

下边看下实现方式,这边这个VMDaemonService就是一个守护进程服务,其中在服务的onStartCommand()方法中调用startForeground()将服务进程设置为前台进程,当运行在 API18 以下的设备是可以直接设置,API18 以上需要实现一个内部的Service,这个内部类实现和外部类同样的操作,然后结束自己;当这个服务启动后就会创建一个定时器去发送广播,当我们的核心服务被干掉后,就由另外的广播接收器去接收我们守护进程发出的广播,然后唤醒我们的核心服务;
/**
* 以实现内部 Service 类的方式实现守护进程,这里是利用 android 漏洞提高当前进程优先级
*
* Created by lzan13 on 2017/3/7.
*/
public class VMDaemonService extends Service {

private final static String TAG = VMDaemonService.class.getSimpleName();

// 定时唤醒的时间间隔,这里为了自己测试方边设置了一分钟
private final static int ALARM_INTERVAL = 1 * 60 * 1000;
// 发送唤醒广播请求码
private final static int WAKE_REQUEST_CODE = 5121;
// 守护进程 Service ID
private final static int DAEMON_SERVICE_ID = -5121;

@Override public void onCreate() {
Log.i(TAG, "VMDaemonService->onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
// 利用 Android 漏洞提高进程优先级,
startForeground(DAEMON_SERVICE_ID, new Notification());
// 当 SDk 版本大于18时,需要通过内部 Service 类启动同样 id 的 Service
if (Build.VERSION.SDK_INT >= 18) {
Intent innerIntent = new Intent(this, DaemonInnerService.class);
startService(innerIntent);
}

// 发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(VMWakeReceiver.DAEMON_WAKE_ACTION);

PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
ALARM_INTERVAL, operation);

/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
*/
return START_STICKY;
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "VMDaemonService->onDestroy");
super.onDestroy();
}

/**
* 实现一个内部的 Service,实现让后台服务的优先级提高到前台服务,这里利用了 android 系统的漏洞,
* 不保证所有系统可用,测试在7.1.1 之前大部分系统都是可以的,不排除个别厂商优化限制
*/
public static class DaemonInnerService extends Service {

@Override public void onCreate() {
Log.i(TAG, "DaemonInnerService -> onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "DaemonInnerService -> onStartCommand");
startForeground(DAEMON_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "DaemonInnerService -> onDestroy");
super.onDestroy();
}
}
}
当我们启动这个守护进程的时候,就可以使用以下adb命令查看当前程序的进程情况(需要adb shell进去设备),
为了等下区分进程优先级,我启动了一个普通的后台进程,两外两个一个是我们启动的守护进程,一个是当前程序的核心进程,可以看到除了后台进程外,另外两个进程都带有isForeground=true的属性:
# 这个命令的 services 可以换成 service,这样会只显示当前,进程,不显示详细内容
# dumpsys activity services <Your Package Name>
root@vbox86p:/ # dumpsys activity services com.vmloft.develop.daemon
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{170fe1dd u0 com.vmloft.develop.daemon/.services.VMDaemonService}
intent={cmp=com.vmloft.develop.daemon/.services.VMDaemonService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{173fe77f 2370:com.vmloft.develop.daemon:daemon/u0a68}
isForeground=true foregroundId=-5121 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-6s196ms startingBgTimeout=--
lastActivity=-6s157ms restartTime=-6s157ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2fee4f84 u0 com.vmloft.develop.daemon/.services.VMCoreService}
intent={cmp=com.vmloft.develop.daemon/.services.VMCoreService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{18c6a1b4 2343:com.vmloft.develop.daemon/u0a68}
isForeground=true foregroundId=-5120 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-28s136ms startingBgTimeout=--
lastActivity=-28s136ms restartTime=-28s136ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2ef6909e u0 com.vmloft.develop.daemon/.services.VMBackgroundService}
intent={cmp=com.vmloft.develop.daemon/.services.VMBackgroundService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:background
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{29f8734c 2388:com.vmloft.develop.daemon:background/u0a68}
createTime=-3s279ms startingBgTimeout=--
lastActivity=-3s262ms restartTime=-3s262ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1
然后我们可以用下边的命令查看ProcessID
# 这个命令可以查看当前DProcessID(数据结果第二列),我们可以看到当前程序有两个进程
# ps | grep com.vmloft.develop.daemon
root@vbox86p:/ # ps | grep com.vmloft.develop.daemon
u0_a68 2343 274 1012408 42188 ffffffff f74f1b45 S com.vmloft.develop.daemon
u0_a68 2370 274 997012 26152 ffffffff f74f1b45 S com.vmloft.develop.daemon:daemon
u0_a68 2388 274 997012 25668 ffffffff f74f1b45 S com.vmloft.develop.daemon:background
有了ProcessID之后,我们可以根据这个ProcessID获取到当前进程的优先级状态Process State,对应Linux层的oom_adj
可以看到当前核心进程的级别为0,因为这个表示当前程序运行在前台 UI 界面,守护进程级别为1,因为我们利用漏洞设置成了前台进程,虽然不可见,但是他的级别也是比较高的,仅次于前台 UI 进程,然后普通后台进程级别为4;当我们退到后台时,可以看到核心进程的级别变为1了,这就是因为我们利用startForeground()将进程设置成前台进程的原因,这样就降低了进程被系统回收的概率了;
# 这个命令就是通过 ProcessID 输出其对应 oom_adj
# cat /proc/ProcessID/oom_adj
# 程序在前台时,查询进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
0
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
# 当程序退到后台时,再次查看进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
1
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
可以看到这种方式确实能够提高进程优先级,但是在一些国产的设备上还是会被杀死的,比我我测试的时候小米点击清空最近运行的应用进程就别干掉了;当把应用加入到设备白名单里就不会被杀死了,微信就是这样,人家直接装上之后就已经在白名单里了,我们要做的就是在用户使用中引导他们将我们的程序设置进白名单,将守护进程和白名单结合起来,这样才能保证我们的应用持续或者

2.JobScheduler机制唤醒

Android系统在5.x以上版本提供了一个JobSchedule接口,系统会根据自己实现定时去调用改接口传递的进程去实现一些操作,而且这个接口在被强制停止后依然能够正常的启动;不过在一些国产设备上可能无效,比如小米;
下边是 JobServcie 的实现:
/**
* 5.x 以上使用 JobService 实现守护进程,这个守护进程要做的工作很简单,就是启动应用的核心进程
* Created by lzan13 on 2017/3/8.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VMDaemonJobService extends JobService {

private final static String TAG = VMDaemonJobService.class.getSimpleName();

@Override public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob");
// 这里为了掩饰直接启动核心进程,没有做其他判断操作
startService(new Intent(getApplicationContext(), VMCoreService.class));
return false;
}

@Override public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob");
return false;
}
}
我们要做的就是在需要的时候调用JobSchedule的schedule来启动任务;剩下的就不需要关心了,JobSchedule会帮我们做好,下边就是我这边实现的启动任务的方法:
/**
* 5.x以上系统启用 JobScheduler API 进行实现守护进程的唤醒操作
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startJobScheduler() {
int jobId = 1;
JobInfo.Builder jobInfo = new JobInfo.Builder(jobId, new ComponentName(this, VMDaemonJobService.class));
jobInfo.setPeriodic(10000);
jobInfo.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo.build());
}
3.系统 Service START_STICKY 机制重启

在实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在Service挂掉后自动拉活;不过这种方式只适合比较原生一些的系统,像小米,华为等这些定制化比较高的第三方厂商,他们都已经把这些给限制掉了;
@Override 
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
* 3.一些定制化比较高的第三方系统也不适用
*/
return START_STICKY;
}
这种方式在以下两种情况无效:
  • Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,这个服务就不能再次重启了;
  • 进程被取得Root权限的管理工具或系统工具通过fores-top方式停止掉,无法重启;
  • 一些定制化比较高的第三方系统也不适用


4.其他保活方式

  • 利用 Native 本地进程,这个主要使用到 jni 调用底层实现,而且在 Android 5.x 以后对这个限制也比较高,不适用了,暂时不研究
  • 集成第三方SDK互相唤醒,这个只要正常集成了第三方的SDK,并使用了他们对应的服务,当一个设备安装的多个应用都集成了某一个第三方SDK时,启动任意一个 app 都会唤醒其他的 app,不过这个在一些新版的国内厂商系统也是做了限制,这种方式并没有什么效果
  • 一像素的 Activity 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;


结语

事事没有绝对,万物总有一些漏洞,就算上边的那些方式不可用了,后边肯定还会出现其他的方式;我们不能保证我们的应用不死,但我们可以提高存活率;

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是
0
回复

android 端 app 后台被杀死收不到消息的解决方案 Android 推送 守护进程

回复

lzan13 发起了问题 • 1 人关注 • 1154 次浏览 • 2017-06-15 14:34 • 来自相关话题

3
评论

IM-SDK和客服SDK并存开发指南—Android篇 环信移动客服 Android

beyond 发表了文章 • 2404 次浏览 • 2017-06-06 10:37 • 来自相关话题

  
   环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴在环信开发过程中,同时使用了环信即时通讯云和环信移动客服服务,就会有这样的疑问,这个时候应该使用哪个SDK 呢?这两个有什么区别?于是让开发者惊呼“还有这种操作”的《IM-SDK和客服-SDK并存开发指南》应运而生,希望能帮助小伙伴们更快速、高效集成环信。嗯,就是有这种操作,不服就来尬舞啊!!!
 一、SDK介绍
访客端客服SDK基于IM-SDK 3.x开发,包含了IM-SDK所有的API和功能,当同时使用IM和客服时,只需要在初始化、登录、登出操作时使用访客端客服 SDK 提供的相应API即可。UI部分集成需要分别导入Kefu-easeui和EaseUI(IM的EaseUI),也可以自己写UI部分。

二、注意事项
开发过程中 初始化、登录、登出,务必使用客服SDK的API。IM-SDK和客服SDK都包含了armeabi、armeabi-v7、arm64-v8a、x86的CPU架构,在发版的时候,可以选择部分CPU,例如剔除模拟器用的x86架构,在build.gradle中配置即可。
 
三、资源准备
到环信官网下载访客端客服SDK+Demo源码,下载链接:http://www.easemob.com/download/cs选择“Android客服访客端”下载(如下图)。 


到环信官网下载IM的SDK+Demo源码,下载链接:http://www.easemob.com/download/im 选择Android SDK(如下图)。  




四、资源简介
解压后的SDK压缩包中,含有基础版和实时音视频版,根据需求的不同使用不同的SDK。默认EaseUI使用的是libs文件夹内的SDK,此SDK含有实时音视频功能,因此比较大,如果不需要使用实时音视频功能的,可以使用libs.without.audio文件夹下的SDK。从官网下载的客服访客端SDK包括以下目录: 





其中:kefu-android-demo为包含实时音视频的商城demo,可以直接运行。
 libs 为客服访客端SDK,包含实时音视频功能。
libs.without.audio 为不包含实时音视频功能的客服访客端SDK。
 
五、集成步骤
参考客服访客端文档或demo源码集成客服的访客端SDK,文档地址:http://docs.easemob.com/cs/300 ... idsdk 。可以把Kefu-Easeui作为一个module,libs放入客服的SDK(kefu-sdk_*.jar和相关so),import IM的easeUI到项目中,去掉里面的hyphenate_*.jar以及相关so。注释掉EaseUI.java中的IMSDK的初始化方法,最终结果为:app项目依赖IM-easeui module,IM-easeui 依赖kefu-easeui module。在自己项目中的Application中的onCreate方法中,先调用客服SDK的初始化方法,再调动IM-easeui和Kefu-easeui的初始化方法(如果不用IM或客服的EaseUI则不需要对EaseUI做初始化)。在app项目中,调用登录、登出方法需要调动客服的API,其他的API为各自的API。在调用EaseUI相关的Activity时,如为IM-easeui的Activity需要在AndroidManifest.xml中注册,kefu-easeui则不需要再注册(因为在kefu-easeui这个module中的AndroidManifest.xml中已注册)。Demo和EaseUI的源码是开源的,也可查看下。
 
六、注意事项

   APP的通知栏提醒,客服和IM的easeui中均有自己的通知栏,代码均开源,可按照自己的方式去修改,具体应用可看下载的Demo源码中的演示实例。
 
提供的兼容Demo介绍:
 Demo是在客服的商城Demo上修改,在左上角添加了一个聊天室的按钮,点击按钮会根据appkey随机创建一个账号并登录,登录成功后会进入聊天室列表界面,点击某个聊天室可以在聊天室中聊天。 Demo中客服部分功能还是和原商城Demo功能一致。 Demo中为了演示因此采用随机注册账号的方式,对于用户场景中,可以先注册好这些账号和自己的账号绑定,这样每次咨询客服就都是同一个人了,也可以显示这个访客曾经的聊天记录。
 
Demo源码地址:http://kefu-prod-apk.oss-cn-ha ... o.zip 查看全部
  
   环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴在环信开发过程中,同时使用了环信即时通讯云和环信移动客服服务,就会有这样的疑问,这个时候应该使用哪个SDK 呢?这两个有什么区别?于是让开发者惊呼“还有这种操作”的《IM-SDK和客服-SDK并存开发指南》应运而生,希望能帮助小伙伴们更快速、高效集成环信。嗯,就是有这种操作,不服就来尬舞啊!!!
 一、SDK介绍
  1. 访客端客服SDK基于IM-SDK 3.x开发,包含了IM-SDK所有的API和功能,当同时使用IM和客服时,只需要在初始化、登录、登出操作时使用访客端客服 SDK 提供的相应API即可。
  2. UI部分集成需要分别导入Kefu-easeui和EaseUI(IM的EaseUI),也可以自己写UI部分。


二、注意事项
  1. 开发过程中 初始化、登录、登出,务必使用客服SDK的API。
  2. IM-SDK和客服SDK都包含了armeabi、armeabi-v7、arm64-v8a、x86的CPU架构,在发版的时候,可以选择部分CPU,例如剔除模拟器用的x86架构,在build.gradle中配置即可。

 
三、资源准备
  1. 到环信官网下载访客端客服SDK+Demo源码,下载链接:http://www.easemob.com/download/cs选择“Android客服访客端”下载(如下图)。 
    001.jpg
  2. 到环信官网下载IM的SDK+Demo源码,下载链接:http://www.easemob.com/download/im 选择Android SDK(如下图)。  
    002.jpg


四、资源简介
  1. 解压后的SDK压缩包中,含有基础版和实时音视频版,根据需求的不同使用不同的SDK。默认EaseUI使用的是libs文件夹内的SDK,此SDK含有实时音视频功能,因此比较大,如果不需要使用实时音视频功能的,可以使用libs.without.audio文件夹下的SDK。
  2. 从官网下载的客服访客端SDK包括以下目录: 


003.png

其中:kefu-android-demo为包含实时音视频的商城demo,可以直接运行。
 libs 为客服访客端SDK,包含实时音视频功能。
libs.without.audio 为不包含实时音视频功能的客服访客端SDK。
 
五、集成步骤
  1. 参考客服访客端文档或demo源码集成客服的访客端SDK,文档地址:http://docs.easemob.com/cs/300 ... idsdk
  2. 可以把Kefu-Easeui作为一个module,libs放入客服的SDK(kefu-sdk_*.jar和相关so),import IM的easeUI到项目中,去掉里面的hyphenate_*.jar以及相关so。注释掉EaseUI.java中的IMSDK的初始化方法,最终结果为:app项目依赖IM-easeui module,IM-easeui 依赖kefu-easeui module。
  3. 在自己项目中的Application中的onCreate方法中,先调用客服SDK的初始化方法,再调动IM-easeui和Kefu-easeui的初始化方法(如果不用IM或客服的EaseUI则不需要对EaseUI做初始化)。
  4. 在app项目中,调用登录、登出方法需要调动客服的API,其他的API为各自的API。在调用EaseUI相关的Activity时,如为IM-easeui的Activity需要在AndroidManifest.xml中注册,kefu-easeui则不需要再注册(因为在kefu-easeui这个module中的AndroidManifest.xml中已注册)。Demo和EaseUI的源码是开源的,也可查看下。

 
六、注意事项

   APP的通知栏提醒,客服和IM的easeui中均有自己的通知栏,代码均开源,可按照自己的方式去修改,具体应用可看下载的Demo源码中的演示实例。
 
提供的兼容Demo介绍:
  1.  Demo是在客服的商城Demo上修改,在左上角添加了一个聊天室的按钮,点击按钮会根据appkey随机创建一个账号并登录,登录成功后会进入聊天室列表界面,点击某个聊天室可以在聊天室中聊天。
  2.  Demo中客服部分功能还是和原商城Demo功能一致。
  3.  Demo中为了演示因此采用随机注册账号的方式,对于用户场景中,可以先注册好这些账号和自己的账号绑定,这样每次咨询客服就都是同一个人了,也可以显示这个访客曾经的聊天记录。

 
Demo源码地址:http://kefu-prod-apk.oss-cn-ha ... o.zip
1
回复

环信conversation.isGroup()这个一直为false Android

zspxsx 回复了问题 • 2 人关注 • 255 次浏览 • 2017-06-02 17:25 • 来自相关话题

0
回复

环信即时通讯关于三天不登陆还能不能收到未读消息的??? Android

回复

研别松荇 发起了问题 • 1 人关注 • 280 次浏览 • 2017-05-30 14:34 • 来自相关话题

0
评论

Error:warning: Ignoring InnerClasses attribute for an anonymous inner class 解决方案 Android

Style℃ 发表了文章 • 820 次浏览 • 2017-05-27 17:06 • 来自相关话题

首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持:

defaultConfig {
        multiDexEnabled true
   }

dependencies { compile 'com.Android.support:multidex:1.0.1' } 

然后让应用支持多DEX文件。在MultiDexApplication JavaDoc中描述了三种可选方法:

1、在AndroidManifest.xml的application中声明android.support.multidex.MultiDexApplication;
2、如果你已经有自己的Application类,让其继承MultiDexApplication;
3、如果你的Application类已经继承自其它类,你不想修改它,那么可以重写attachBaseContext()方法:
 
@Override   
protected void attachBaseContext(Context base) {  
    super.attachBaseContext(base); MultiDex.install(this);  
}  
 
运行就可以了,也可能打包了,这个问题貌似是工程中的方法数量超过安卓规定65536个方法数了,,,
  查看全部

20160701171527638.png

首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持:

defaultConfig {
        multiDexEnabled true
   }

dependencies { compile 'com.Android.support:multidex:1.0.1' } 

然后让应用支持多DEX文件。在MultiDexApplication JavaDoc中描述了三种可选方法:

1、在AndroidManifest.xml的application中声明android.support.multidex.MultiDexApplication;
2、如果你已经有自己的Application类,让其继承MultiDexApplication;
3、如果你的Application类已经继承自其它类,你不想修改它,那么可以重写attachBaseContext()方法:
 
@Override   
protected void attachBaseContext(Context base) {  
    super.attachBaseContext(base); MultiDex.install(this);  
}  
 
运行就可以了,也可能打包了,这个问题貌似是工程中的方法数量超过安卓规定65536个方法数了,,,
 
2
回复

按照文档方式初始化之后,获取到的EMClient实例内部全为null 初始化 Android

baoshu 回复了问题 • 2 人关注 • 344 次浏览 • 2017-05-24 10:40 • 来自相关话题

1
回复

Android studio集成环信compile之后还需要受到引入so文件么? Android

baoshu 回复了问题 • 2 人关注 • 308 次浏览 • 2017-05-24 10:22 • 来自相关话题

条新动态, 点击查看
zhangnan

zhangnan 回答了问题 • 2015-12-04 10:41 • 1 个回复 不感兴趣

EMVideoCallHelper callHelper.setRenderFlag(true);

赞同来自:

// 显示对方图像的surfaceview
        oppositeSurface = (SurfaceView) findViewById(R.id.opposite_surface);
        oppositeSurfaceHolder =... 显示全部 »
// 显示对方图像的surfaceview
        oppositeSurface = (SurfaceView) findViewById(R.id.opposite_surface);
        oppositeSurfaceHolder = oppositeSurface.getHolder();
        // 设置显示对方图像的surfaceview
        callHelper.setSurfaceView(oppositeSurface);
你调用这个方法在服务器获取好友列表
List<String> usernames = EMContactManager.getInstance().getContactUserNames();//需异步执行
 
你调用这个方法在服务器获取好友列表
List<String> usernames = EMContactManager.getInstance().getContactUserNames();//需异步执行
 
收到对方被同意后,需要执行onContactAdded保存好友信息
收到对方被同意后,需要执行onContactAdded保存好友信息
两次登录账号之间最好加上sdk中的logout方法,清除内存中的数据信息。
两次登录账号之间最好加上sdk中的logout方法,清除内存中的数据信息。
你是有这个创建消息体,不加map,这样创建试一试看下
EMCmdMessageBody cmdBody = new EMCmdMessageBody(action); 
 
你是有这个创建消息体,不加map,这样创建试一试看下
EMCmdMessageBody cmdBody = new EMCmdMessageBody(action); 
 
jiangym

jiangym 回答了问题 • 2016-03-17 22:12 • 1 个回复 不感兴趣

error":"reach_limit"

赞同来自:

接口限流说明: 同一个IP每秒最多可调用30次, 超过的部分会返回503错误, 所以在调用程序中, 如果碰到了这样的错误, 需要稍微暂停一下并且重试。如果该限流控制不满足需求,请联系商务经理开放更高的权限。
接口限流说明: 同一个IP每秒最多可调用30次, 超过的部分会返回503错误, 所以在调用程序中, 如果碰到了这样的错误, 需要稍微暂停一下并且重试。如果该限流控制不满足需求,请联系商务经理开放更高的权限。
注册是在子线程中执行的吗?
注册是在子线程中执行的吗?
com.easemob.easeui.ui 下面
EaseChatFragment
重新定义这个两个数组
itemStrings,
itemdrawables

 
com.easemob.easeui.ui 下面
EaseChatFragment
重新定义这个两个数组
itemStrings,
itemdrawables

 
获取所有联系人的username,然后再依次取
获取所有联系人的username,然后再依次取
ChrisWu

ChrisWu 回答了问题 • 2016-12-06 17:06 • 1 个回复 不感兴趣

集成easeui时一直报错

赞同来自:

把你的v4包的版本改高一点,它默认的是19+
 
把你的v4包的版本改高一点,它默认的是19+
 
环信沈冲

环信沈冲 回答了问题 • 2016-12-26 14:32 • 2 个回复 不感兴趣

环信服务端集成,如何集成?

赞同来自:

客户端直接集成SDK即可,服务端可以根据自己需求调用相应的rest接口集成
客户端直接集成SDK即可,服务端可以根据自己需求调用相应的rest接口集成
baoshu

baoshu 回答了问题 • 2017-04-21 18:14 • 4 个回复 不感兴趣

导入EaseUI 出现J爆红

赞同来自:

好的,感谢您的意见,之后这个提个需求
好的,感谢您的意见,之后这个提个需求
baoshu

baoshu 回答了问题 • 2017-04-24 12:28 • 2 个回复 不感兴趣

java.lang.StackOverflowError

赞同来自:

getUserInfo是需要自己实现的方法
getUserInfo是需要自己实现的方法
Wxin

Wxin 回答了问题 • 2017-04-24 16:20 • 1 个回复 不感兴趣

为什么EaseChatFragment监听不到后台发送的消息?

赞同来自:

在application里注册消息监听,可以在后台监听新消息
在application里注册消息监听,可以在后台监听新消息
Wxin

Wxin 回答了问题 • 2017-04-25 18:57 • 1 个回复 不感兴趣

无法实时获取对方发送过来的消息

赞同来自:

其他地方的消息监听去注销了看看
其他地方的消息监听去注销了看看
gaomode

gaomode 回答了问题 • 2017-04-26 18:24 • 1 个回复 不感兴趣

进入群聊页面闪退

赞同来自:

问题已经解决了,是自己传值为null导致的
问题已经解决了,是自己传值为null导致的
Wxin

Wxin 回答了问题 • 2017-04-28 14:29 • 1 个回复 不感兴趣

EaseConversationFragment界面如何显示群的头像

赞同来自:

需要你自己去把群组头像传到EaseConversationAdapter里去设置
需要你自己去把群组头像传到EaseConversationAdapter里去设置
gaomode

gaomode 回答了问题 • 2017-04-29 12:46 • 3 个回复 不感兴趣

服务器发送通知乱码

赞同来自:

过滤conversationid即可,然后在适配器中做处理。
过滤conversationid即可,然后在适配器中做处理。
怎样调用?
 在需要通知栏通知的地方调用这个:
 
EaseUI.getInstance().getNotifier().onNewMsg(msg);
onNewMsg(EMMessage message) 和 onNewMesg(List<EMMe... 显示全部 »
怎样调用?
 在需要通知栏通知的地方调用这个:
 
EaseUI.getInstance().getNotifier().onNewMsg(msg);
onNewMsg(EMMessage message) 和 onNewMesg(List<EMMessage> messages) 就是上面我让你改的两个方法.根据需要调用其中一个方法就可以了.
 
 
在哪里调用?
 
聊天消息监听器中,群组消息监听器中,,,,,以及任何一个你需要通知栏通知的地方.
比如:在EMMessageListener的onMessageReceived方法中,调用onNewMesg,就可以实现收到聊天消息就进行通知栏通知了.

 
没明白具体的问题,是说登录第二个账号之后保留的还是上一个用户的信息吗?这个问题导致的原因就是账号没有退出成功,在登录之前要做判断是否已登录,在登录之前要保证已退出
没明白具体的问题,是说登录第二个账号之后保留的还是上一个用户的信息吗?这个问题导致的原因就是账号没有退出成功,在登录之前要做判断是否已登录,在登录之前要保证已退出
baoshu

baoshu 回答了问题 • 2017-05-09 11:27 • 2 个回复 不感兴趣

杀死app进程后,android无法接收到信息

赞同来自:

杀死进程之后是收不到消息的,可以集成离线推送,目前支持的离线推送是华为小米
杀死进程之后是收不到消息的,可以集成离线推送,目前支持的离线推送是华为小米
baoshu

baoshu 回答了问题 • 2017-11-14 11:35 • 2 个回复 不感兴趣

关于 java.lang.UnsatisfiedLinkError错误

赞同来自:

http://www.jianshu.com/p/b9a524f24b7e
http://www.jianshu.com/p/b9a524f24b7e
11
评论

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

KevinGong 发表了文章 • 9039 次浏览 • 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 发表了文章 • 7985 次浏览 • 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 人关注 • 3978 次浏览 • 2017-12-03 00:50 • 来自相关话题

7
回复

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

回复

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

11
评论

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

KevinGong 发表了文章 • 9039 次浏览 • 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 发表了文章 • 7985 次浏览 • 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...小伙伴们还有什么想知道欢迎跟帖提出。
 
2
最佳

关于 java.lang.UnsatisfiedLinkError错误 Android UnsatisfiedLinkError错误

回复

baoshu 回复了问题 • 2 人关注 • 119 次浏览 • 2017-11-30 16:46 • 来自相关话题

1
回复

请问下环信的视频聊天离线推送问题 离线推送 视频 Android 环信_Android

回复

金鱼先森 回复了问题 • 1 人关注 • 115 次浏览 • 2017-11-20 16:14 • 来自相关话题

1
回复

android环信2.0默认添加好友 环信2.0 Android

回复

geri_yang 回复了问题 • 2 人关注 • 128 次浏览 • 2017-11-15 19:04 • 来自相关话题

0
回复

环信2.0默认添加好友成功后,对方好友列表有我,而我没有他,是什么情况 Android 环信2.0

回复

yuan 发起了问题 • 1 人关注 • 111 次浏览 • 2017-11-05 23:35 • 来自相关话题

1
回复
1
回复

群组创建时是公开群,任何人都可加入。创建后有方法可以改变isMembersOnly状态吗 Android 群组 群组添加成员

回复

qzusers 回复了问题 • 2 人关注 • 246 次浏览 • 2017-09-30 17:54 • 来自相关话题

1
回复

大佬们,Android 环信easeui已发送消息就崩,空指针是什么原因? Android 环信_Android

回复

baoshu 回复了问题 • 2 人关注 • 345 次浏览 • 2017-09-15 10:08 • 来自相关话题

3
回复

华为p10 无法接收到离线消息 环信_Android 有专职工程师值守 Android

回复

geri_yang 回复了问题 • 3 人关注 • 620 次浏览 • 2017-09-04 14:57 • 来自相关话题

1
回复

android离线推送怎样设置带铃声的推送? 有专职工程师值守 Android

回复

geri_yang 回复了问题 • 2 人关注 • 396 次浏览 • 2017-08-01 20:11 • 来自相关话题

1
回复

关于回话列表 怎么弄啊 环信 Android

回复

geri_yang 回复了问题 • 2 人关注 • 334 次浏览 • 2017-07-20 18:46 • 来自相关话题

2
回复

导入AndroidPdfView的时候,点击运行,会报错 环信_Android Android 有专职工程师值守

回复

justsyhxxxx 回复了问题 • 1 人关注 • 337 次浏览 • 2017-06-27 10:56 • 来自相关话题

3
回复

easeui 里的 simpledemo 登录 时 提示 用 uidemo中的账户登录即可, 在哪里啊 Android

回复

geri_yang 回复了问题 • 6 人关注 • 1167 次浏览 • 2017-06-22 16:13 • 来自相关话题

1
回复

Android 会话列表只展示群组 Android 环信

回复

geri_yang 回复了问题 • 2 人关注 • 370 次浏览 • 2017-06-21 12:49 • 来自相关话题

0
回复

android 3.0以后怎么获取自定义消息中Ext消息 环信 ext 接收消息 3.0 Android

回复

╰☆╮末↘ 发起了问题 • 1 人关注 • 340 次浏览 • 2017-06-16 10:30 • 来自相关话题

0
回复

android 端 app 后台被杀死收不到消息的解决方案 Android 推送 守护进程

回复

lzan13 发起了问题 • 1 人关注 • 1154 次浏览 • 2017-06-15 14:34 • 来自相关话题

1
回复

环信conversation.isGroup()这个一直为false Android

回复

zspxsx 回复了问题 • 2 人关注 • 255 次浏览 • 2017-06-02 17:25 • 来自相关话题

0
回复

环信即时通讯关于三天不登陆还能不能收到未读消息的??? Android

回复

研别松荇 发起了问题 • 1 人关注 • 280 次浏览 • 2017-05-30 14:34 • 来自相关话题

2
回复

按照文档方式初始化之后,获取到的EMClient实例内部全为null 初始化 Android

回复

baoshu 回复了问题 • 2 人关注 • 344 次浏览 • 2017-05-24 10:40 • 来自相关话题

1
回复

Android studio集成环信compile之后还需要受到引入so文件么? Android

回复

baoshu 回复了问题 • 2 人关注 • 308 次浏览 • 2017-05-24 10:22 • 来自相关话题

2
回复

点击了广播,却无法跳转 有专职工程师值守 Android

回复

gaomode 回复了问题 • 2 人关注 • 338 次浏览 • 2017-05-10 13:39 • 来自相关话题

2
最佳

杀死app进程后,android无法接收到信息 有专职工程师值守 Android 环信_Android

回复

baoshu 回复了问题 • 2 人关注 • 391 次浏览 • 2017-05-09 11:57 • 来自相关话题

1
最佳
3
最佳

服务器发送通知乱码 环信_Android Android 有专职工程师值守

回复

baoshu 回复了问题 • 3 人关注 • 627 次浏览 • 2017-04-29 17:25 • 来自相关话题

1
最佳

EaseConversationFragment界面如何显示群的头像 有专职工程师值守 Android 环信_Android

回复

Wxin 回复了问题 • 2 人关注 • 331 次浏览 • 2017-04-28 14:29 • 来自相关话题

7
回复

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

回复

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

11
评论

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

KevinGong 发表了文章 • 9039 次浏览 • 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 发表了文章 • 7985 次浏览 • 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
评论

环信即时通讯云V3.3.5 SDK已发布,安全传输升级,支持FCM推送 iOS Android 产品更新

产品更新 发表了文章 • 101 次浏览 • 2017-10-23 19:00 • 来自相关话题

10月23日,环信即时通讯云发布了新版本SDK,此次更新的V3.3.5版本增加了传输安全性,AndroidSDK支持FCM推送,FCM是谷歌推出的最新的Android系统级别的消息推送服务(用来替换GCM)。
iOS SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:
增加了传输安全性增加广告插件,可以收集用户信息
优化:
私有部署设置dns的接口优化私有部署重连逻辑限制用户名长度为255需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505)添加i386库解决模拟器profile时的编译问题
功能修复:
修复4G与wifi切换时偶然出现发送消息失败的bug
 
Android SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:
增加了传输安全性;支持FCM推送;

优化:
私有部署设置dns的接口;优化私有部署重连逻辑;限制用户名长度为255;需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505);

修复:
修复4G与wifi切换时偶然出现发送消息失败的bug;修复VIVO手机JobService crash问题;
 
版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载​ 查看全部
10月23日,环信即时通讯云发布了新版本SDK,此次更新的V3.3.5版本增加了传输安全性,AndroidSDK支持FCM推送,FCM是谷歌推出的最新的Android系统级别的消息推送服务(用来替换GCM)。

iOS SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:

  • 增加了传输安全性
  • 增加广告插件,可以收集用户信息

优化:
  • 私有部署设置dns的接口
  • 优化私有部署重连逻辑
  • 限制用户名长度为255
  • 需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505)
  • 添加i386库解决模拟器profile时的编译问题

功能修复:
  • 修复4G与wifi切换时偶然出现发送消息失败的bug

 
Android SDK 更新日志

版本 V3.3.5 2017-10-23

新功能:

  • 增加了传输安全性;
  • 支持FCM推送;


优化:
  • 私有部署设置dns的接口;
  • 优化私有部署重连逻辑;
  • 限制用户名长度为255;
  • 需要服务器开通的功能接口返回SERVICE_NOT_ENABLED(505);


修复:
  • 修复4G与wifi切换时偶然出现发送消息失败的bug;
  • 修复VIVO手机JobService crash问题;

 
版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载​
0
评论

android studio 2.3.3 最新 中文 汉化包 韩梦飞沙 安卓工作室 美化包 Android AndroidStudio

韩梦飞沙_韩亚飞 发表了文章 • 376 次浏览 • 2017-09-05 11:59 • 来自相关话题

韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha

汉化包 百度云盘 下载地址:https://pan.baidu.com/s/1pLjwyeB 
最新最详细全面最牛逼的汉化!稳定无BUG!设置界面可以打开!不会报错!中英对照!界面酷炫!
使用汉化美化包,让你的开发工具IDE 不再单调普通,彰显个性,与众不同!
中文的 菜单工具设置信息,让你一眼看懂,让你用好这款开发软件,最大发挥它的作用!
  查看全部
韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha

汉化包 百度云盘 下载地址:https://pan.baidu.com/s/1pLjwyeB 
最新最详细全面最牛逼的汉化!稳定无BUG!设置界面可以打开!不会报错!中英对照!界面酷炫!
使用汉化美化包,让你的开发工具IDE 不再单调普通,彰显个性,与众不同!
中文的 菜单工具设置信息,让你一眼看懂,让你用好这款开发软件,最大发挥它的作用!
 
0
评论

【环信征文】Android程序员北漂记-从逃离北上广到逃回北上广 Android

东风玖哥 发表了文章 • 2091 次浏览 • 2017-07-04 13:55 • 来自相关话题

司马雼是我的学长,先后在国内两家top 10大厂担任资深Android工程师,对Android技术有如痴如醉的热情,并且乐于帮助同行,最难得的是他还有一个漂亮的女朋友,不愧是Android程序员中的人生赢家。我将他的经历稍作加工后用明清小说的笔法写出来,希望每个读到这篇文章的Android工程师都能走他的路。

第一回:小庙无地容巨擘 大厂有礼迎硕士诗曰:

老板抠门巧计乖,却将忠义苦挤排。

基础扎实绩点高,苍天岂能误人才。

司马雼2008年考入合肥工业大学,2012年保研本校,2014年进入合肥某小公司实习。

司马雼一开始选择的是JavaEE方向,他在实习期间就以专家的标准严格要求自己,不但让服务端的内核稳定度提升了好几个档次,还让内存消耗下降了好多个数量级。完成本职工作后还帮助运维那边修复了几次硬件的故障。不料老板嫉贤妒能成性,只爱奴才,不要人才。这个老板不但不给司马雼升职加薪,还侮辱司马雼:““雼”是“宕”的繁体字,你越俎代庖搞运维不怕把服务器搞宕机?”信而见疑、怀才不遇的司马雼愤而辞职。司马雼辞职刚好赶上某大厂(国内top 10)招Android实习生,他突击学习了几天Android就去面试了。

一面的时候主要考察Android和Java的基础知识,比如Java的数据类型、运算符优先级和Android的布局以及生命周期,基础扎实的司马雼都能准确无误地回答。

二面的时候面试官问了许多数据结构和算法、设计模式、架构的方面的问题,司马雼不但能画出好几种设计模式的UML图,还对MVC、MVP、MVVM的区别和优缺点发表了自己的看法。

最后HR面的时候收了司马雼的成绩单复印件,对他4.0的GPA和专业top 5%感到很满意。

司马雼通过了面试,顺利成为该厂的Android实习生。不久,司马雼以专业top 5%的优异成绩硕士毕业,转正成为Android工程师。而那家小公司呢,不知道什么时候就因为bug太多导致用户严重流失而倒闭了。

异史氏曰:Java玩得6的学生选择Android作为发展方向是一个明智的抉择,这个世界正处于、并将长期处于移动互联网时代。校招面试除了考察Android和Java基础知识,还考察你学习的基础理论课程的知识,最后通常还要收成绩单的复印件。大厂的校招面试一般有固定的问题和模式,比如阿里校招笔试题来自《技术之瞳》,微软校招笔试题来自《编程之美》等,尽管临阵磨枪也有一定的成功几率,但别忘了玩3年LOL的枪不可能比刷6年LeetCode的枪更快。(时间没错,沉迷LOL的大学生都考不上硕士)

第二回:荣升高级愁田舍 游子低头思莼鲈诗曰:

当年许汜初炒房,羞无才气见刘郎。

如今司马有远志,却愁无房迎新娘。

炒房团自古有之,祖师爷是东汉末年的许汜。当年许汜空有国士之名,却全无救世之意,整天就知道求田问舍,因此被胸襟海阔,志向山高,忧国忘家的刘备鄙视。此事有辛弃疾《水龙吟》为证:“求田问舍,怕应羞见,刘郎才气。”

司马雼和许汜的志向有天壤之别,他的梦想就是把有限的生命投入无限的Android技术中,他不甘心每天只做UI的微调,他发现App存在很大的改进空间,并付诸行动。

司马雼对App的代码进行了大规模重构,在MVP与MVVM中选择了MVP架构,并自主研发了一套的网络请求框架(结合了OKHttp和Gson,可以理解为国产的Retrofit)(该框架不开源,类似框架的源码:https://github.com/qiujuer/OkHttpPacker)代替OKHttp。这样一来不但减少了大量冗余代码,层次结构也变得更加清晰。


订单状态这种需要和服务器实时同步的数据,以前一直用每秒一次的轮询,司马雼发现这是App又费电又费流量的祸根,采取了用推送代替轮询的解决方案。


APK瘦身也是Android性能优化的重要组成部分,司马雼去掉了很多不必要依赖和重复的工具类,让APK打包后的体量轻了一半。

两年后,这个App完成了从3.3到6.3共13个版本的迭代,App的启动速度提升了120%,Crash 率也由8‰降低到1‰。立下汗马功劳的司马雼被任命为项目组长、技术指导、高级工程师。

尽管司马雼工作兢兢业业,也为我国的开源事业添砖加瓦,还写技术博客帮助了很多人。北京高昂的房价却让他有点羡慕那个被他鄙视了多年的许汜。

异史氏曰:Android工程师(在没有“高级”等前缀时)每天最多的工作都是UI的改来改去,想在平凡的工作中取得不平庸的业绩就要付出努力,提升App的性能是有效途径之一。贡献开源代码和分享技术文章的时间也是可以挤出来的。如果在工作中有出色的成绩,升职加薪只是时间问题。

第三回:桑梓惜别因缘浅 楷模入职即资深诗曰:

北京买房要筑台,还有堵车与雾霾。

回到省会想定居,一问工资又回来。

北京的房价、堵车、雾霾逼得司马雼决定裸辞,逃离北上广,他的目的地是上了七年学的合肥。他刚辞职,老东家的最大竞争对手(全国top 20)和另一家全国top 10的大厂都邀请他去面试。他两个月来参加了这两家大厂和合肥当地两家大厂的面试。

面试官首先询问的总是Java和Android的高级特性,Java的高级特性主要有JVM模型、类加载机制和GC原理等,Android的高级特性主要有几大FLAG和LauchMode的区别和使用场景、Binder的引用和实体以及权限系统的交互等。司马雼对技术钻研很有深度,总是对答如流。


面试的第二阶段就是让司马雼自己去讲他做过的项目,然后面试官会冷不丁的让他去解释其中某一部分,有时候让他解释当时为什么要这么做,有时候问他现在觉得有没有更好的办法。司马雼处理问题的思路和解决问题的能力给面试官留下了深刻印象。


面试官有时候会问一些该企业所在行业需要关注的Android技术,比如研究输入法的公司会询问他Android手势和多点触摸,研究物联网的公司会问他Bluetooth相关知识等。因为司马雼广泛涉猎Android知识,他的技术广度也让面试官啧啧赞叹。

每次面试官问司马雼:“你还有什么想和我说的吗?”的时候,司马雼就把他的技术博客和开源项目一股脑砸向面试官,面试官是司马雼的粉丝或者面试官正在用司马雼的开源项目的情况发生了好几次。

司马雼在合肥拿到了两个资深Android工程师的offer,尽管合肥房价不到北京的1/3,可是最多30W的年薪让司马雼在合肥买房遥遥无期。这时司马雼接到了一个电话,逃离北京前面试的全国top 10的大厂给了他60W年薪和项目经理、技术经理、资深工程师的title。他决定逃回北上广,逃离北上广的计划刚开始就结束了。

异史氏曰:有多年工作经验的求职者几乎不需要在求职网站上投简历,想让你做他的同时的同行朋友会内推你。社招面试和校招面试是不同的,社招面试没有固定的问题和模式,临时抱佛脚是行不通的。社招面试基本都会考察这几个问题:第一个阶段是Android和Java的高级特性,考察技术深度;第二个阶段是讲述自己的项目,并在中间穿插着问题,考察解决问题的经验;第三个阶段(未必有)是问该公司所在行业需要掌握的Android知识,考察技术广度和快速上手情况。技术博客和开源项目是很重要的加分项,如果平时不积累、不分享,求职者会失去很多机会。二三线城市的房价更亲民,但工资非常不人性,逃离北上广需谨慎。

第四回:专家立功施小计 淑女出闺成大礼诗曰:

经验丰富技术精,消除隐患立大功。

全球大会登讲台,抱得淑女入后宫。

话说司马雼所在的团队负责公司的Android客户端的安全工作,工作内容包含保活、防拦截、防篡改和防反编译等工作。他发现自家App存在不少可能被恶意利用的隐患:

首先,坏人可以通过NotificationListenerService拦截自家App的推送,给用户造成自家App没有推送的假象。


其次,在上一条基础上。坏人可以在虚拟机里运行一个窃取推送App,收到自家App推送后,用AccessibilityService打开Notification对应的Activity,找到里面的WebView,然后取得链接及网页中的内容,稍加修改(换logo、改名字)推送给坏人自己的App。


此外公司的微信公众号打开自己App需要用浏览器打开链接,这也给坏人以可乘之机。


更有甚者,坏人最丧心病狂的手段就是卸载了自家App后,在肉鸡中安装一个与自家App的packgaeName一致,但Signature不一样而且没有launcher的假App,这样肉鸡的用户永远也安装不了自家App,而且还不能用长按拖拽桌面icon进垃圾桶的方式删除假App。

司马雼花了两年时间为公司消除了以上隐患,荣升架构师、技术专家(时间没错,这是小说,出现未来时间很正常)。各大IT论坛、IT活动的聘书和邀请函也如雪片般飞来,不是邀请他做特约作者,就是邀请他当讲座嘉宾。

某IT大会在北京举行,司马雼作为特约嘉宾在台上妙语连珠、口若悬河。大会期间,司马雼结识了某IT社区的技术编辑椎名,这位淑女负责大会的后勤和报道工作。椎名不但知书达理、兰心蕙质,相貌也倾国倾城,有诗为证:乐天浔阳歌《长恨》,陈王洛水赋《感甄》。

摩诘青溪《西施咏》,潇湘红楼《明妃吟》。

没错,这诗集合了白居易《长恨歌》、曹植《感甄赋》、王维《西施咏》、林黛玉《明妃吟》,椎名的美貌用古往今来的美女诗都搁一块也写不完。淑女椎名倾慕司马雼的才华,从此司马雼正式脱单。

不久,年薪100W的司马雼在北京买房定居。又过了几个月,这对为我国IT事业做出了巨大贡献的情侣在北京举办了盛大的婚礼。

异史氏曰:公司大到一定程度,别有用心的竞争对手就会搞小动作,因此想成为大厂的技术专家,懂点安全方面的知识是很有帮助的。程序员是经常被嘲笑为“注定孤独一生”的群体,但出人头地的程序员抱得美人归的可能性非常大,毕竟女人的颜值通常和她的男朋友的收入成正比。
 
本文首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201707/543909.htm 查看全部
司马雼是我的学长,先后在国内两家top 10大厂担任资深Android工程师,对Android技术有如痴如醉的热情,并且乐于帮助同行,最难得的是他还有一个漂亮的女朋友,不愧是Android程序员中的人生赢家。我将他的经历稍作加工后用明清小说的笔法写出来,希望每个读到这篇文章的Android工程师都能走他的路。

第一回:小庙无地容巨擘 大厂有礼迎硕士
诗曰:

老板抠门巧计乖,却将忠义苦挤排。

基础扎实绩点高,苍天岂能误人才。


司马雼2008年考入合肥工业大学,2012年保研本校,2014年进入合肥某小公司实习。

司马雼一开始选择的是JavaEE方向,他在实习期间就以专家的标准严格要求自己,不但让服务端的内核稳定度提升了好几个档次,还让内存消耗下降了好多个数量级。完成本职工作后还帮助运维那边修复了几次硬件的故障。不料老板嫉贤妒能成性,只爱奴才,不要人才。这个老板不但不给司马雼升职加薪,还侮辱司马雼:““雼”是“宕”的繁体字,你越俎代庖搞运维不怕把服务器搞宕机?”信而见疑、怀才不遇的司马雼愤而辞职。司马雼辞职刚好赶上某大厂(国内top 10)招Android实习生,他突击学习了几天Android就去面试了。

一面的时候主要考察Android和Java的基础知识,比如Java的数据类型、运算符优先级和Android的布局以及生命周期,基础扎实的司马雼都能准确无误地回答。

二面的时候面试官问了许多数据结构和算法、设计模式、架构的方面的问题,司马雼不但能画出好几种设计模式的UML图,还对MVC、MVP、MVVM的区别和优缺点发表了自己的看法。

最后HR面的时候收了司马雼的成绩单复印件,对他4.0的GPA和专业top 5%感到很满意。

司马雼通过了面试,顺利成为该厂的Android实习生。不久,司马雼以专业top 5%的优异成绩硕士毕业,转正成为Android工程师。而那家小公司呢,不知道什么时候就因为bug太多导致用户严重流失而倒闭了。

异史氏曰:Java玩得6的学生选择Android作为发展方向是一个明智的抉择,这个世界正处于、并将长期处于移动互联网时代。校招面试除了考察Android和Java基础知识,还考察你学习的基础理论课程的知识,最后通常还要收成绩单的复印件。大厂的校招面试一般有固定的问题和模式,比如阿里校招笔试题来自《技术之瞳》,微软校招笔试题来自《编程之美》等,尽管临阵磨枪也有一定的成功几率,但别忘了玩3年LOL的枪不可能比刷6年LeetCode的枪更快。(时间没错,沉迷LOL的大学生都考不上硕士)

第二回:荣升高级愁田舍 游子低头思莼鲈
诗曰:

当年许汜初炒房,羞无才气见刘郎。

如今司马有远志,却愁无房迎新娘。


炒房团自古有之,祖师爷是东汉末年的许汜。当年许汜空有国士之名,却全无救世之意,整天就知道求田问舍,因此被胸襟海阔,志向山高,忧国忘家的刘备鄙视。此事有辛弃疾《水龙吟》为证:“求田问舍,怕应羞见,刘郎才气。”

司马雼和许汜的志向有天壤之别,他的梦想就是把有限的生命投入无限的Android技术中,他不甘心每天只做UI的微调,他发现App存在很大的改进空间,并付诸行动。

司马雼对App的代码进行了大规模重构,在MVP与MVVM中选择了MVP架构,并自主研发了一套的网络请求框架(结合了OKHttp和Gson,可以理解为国产的Retrofit)(该框架不开源,类似框架的源码:https://github.com/qiujuer/OkHttpPacker)代替OKHttp。这样一来不但减少了大量冗余代码,层次结构也变得更加清晰。


订单状态这种需要和服务器实时同步的数据,以前一直用每秒一次的轮询,司马雼发现这是App又费电又费流量的祸根,采取了用推送代替轮询的解决方案。


APK瘦身也是Android性能优化的重要组成部分,司马雼去掉了很多不必要依赖和重复的工具类,让APK打包后的体量轻了一半。

两年后,这个App完成了从3.3到6.3共13个版本的迭代,App的启动速度提升了120%,Crash 率也由8‰降低到1‰。立下汗马功劳的司马雼被任命为项目组长、技术指导、高级工程师。

尽管司马雼工作兢兢业业,也为我国的开源事业添砖加瓦,还写技术博客帮助了很多人。北京高昂的房价却让他有点羡慕那个被他鄙视了多年的许汜。

异史氏曰:Android工程师(在没有“高级”等前缀时)每天最多的工作都是UI的改来改去,想在平凡的工作中取得不平庸的业绩就要付出努力,提升App的性能是有效途径之一。贡献开源代码和分享技术文章的时间也是可以挤出来的。如果在工作中有出色的成绩,升职加薪只是时间问题。

第三回:桑梓惜别因缘浅 楷模入职即资深
诗曰:

北京买房要筑台,还有堵车与雾霾。

回到省会想定居,一问工资又回来。


北京的房价、堵车、雾霾逼得司马雼决定裸辞,逃离北上广,他的目的地是上了七年学的合肥。他刚辞职,老东家的最大竞争对手(全国top 20)和另一家全国top 10的大厂都邀请他去面试。他两个月来参加了这两家大厂和合肥当地两家大厂的面试。

面试官首先询问的总是Java和Android的高级特性,Java的高级特性主要有JVM模型、类加载机制和GC原理等,Android的高级特性主要有几大FLAG和LauchMode的区别和使用场景、Binder的引用和实体以及权限系统的交互等。司马雼对技术钻研很有深度,总是对答如流。


面试的第二阶段就是让司马雼自己去讲他做过的项目,然后面试官会冷不丁的让他去解释其中某一部分,有时候让他解释当时为什么要这么做,有时候问他现在觉得有没有更好的办法。司马雼处理问题的思路和解决问题的能力给面试官留下了深刻印象。


面试官有时候会问一些该企业所在行业需要关注的Android技术,比如研究输入法的公司会询问他Android手势和多点触摸,研究物联网的公司会问他Bluetooth相关知识等。因为司马雼广泛涉猎Android知识,他的技术广度也让面试官啧啧赞叹。

每次面试官问司马雼:“你还有什么想和我说的吗?”的时候,司马雼就把他的技术博客和开源项目一股脑砸向面试官,面试官是司马雼的粉丝或者面试官正在用司马雼的开源项目的情况发生了好几次。

司马雼在合肥拿到了两个资深Android工程师的offer,尽管合肥房价不到北京的1/3,可是最多30W的年薪让司马雼在合肥买房遥遥无期。这时司马雼接到了一个电话,逃离北京前面试的全国top 10的大厂给了他60W年薪和项目经理、技术经理、资深工程师的title。他决定逃回北上广,逃离北上广的计划刚开始就结束了。

异史氏曰:有多年工作经验的求职者几乎不需要在求职网站上投简历,想让你做他的同时的同行朋友会内推你。社招面试和校招面试是不同的,社招面试没有固定的问题和模式,临时抱佛脚是行不通的。社招面试基本都会考察这几个问题:第一个阶段是Android和Java的高级特性,考察技术深度;第二个阶段是讲述自己的项目,并在中间穿插着问题,考察解决问题的经验;第三个阶段(未必有)是问该公司所在行业需要掌握的Android知识,考察技术广度和快速上手情况。技术博客和开源项目是很重要的加分项,如果平时不积累、不分享,求职者会失去很多机会。二三线城市的房价更亲民,但工资非常不人性,逃离北上广需谨慎。

第四回:专家立功施小计 淑女出闺成大礼
诗曰:

经验丰富技术精,消除隐患立大功。

全球大会登讲台,抱得淑女入后宫。


话说司马雼所在的团队负责公司的Android客户端的安全工作,工作内容包含保活、防拦截、防篡改和防反编译等工作。他发现自家App存在不少可能被恶意利用的隐患:

首先,坏人可以通过NotificationListenerService拦截自家App的推送,给用户造成自家App没有推送的假象。


其次,在上一条基础上。坏人可以在虚拟机里运行一个窃取推送App,收到自家App推送后,用AccessibilityService打开Notification对应的Activity,找到里面的WebView,然后取得链接及网页中的内容,稍加修改(换logo、改名字)推送给坏人自己的App。


此外公司的微信公众号打开自己App需要用浏览器打开链接,这也给坏人以可乘之机。


更有甚者,坏人最丧心病狂的手段就是卸载了自家App后,在肉鸡中安装一个与自家App的packgaeName一致,但Signature不一样而且没有launcher的假App,这样肉鸡的用户永远也安装不了自家App,而且还不能用长按拖拽桌面icon进垃圾桶的方式删除假App。

司马雼花了两年时间为公司消除了以上隐患,荣升架构师、技术专家(时间没错,这是小说,出现未来时间很正常)。各大IT论坛、IT活动的聘书和邀请函也如雪片般飞来,不是邀请他做特约作者,就是邀请他当讲座嘉宾。

某IT大会在北京举行,司马雼作为特约嘉宾在台上妙语连珠、口若悬河。大会期间,司马雼结识了某IT社区的技术编辑椎名,这位淑女负责大会的后勤和报道工作。椎名不但知书达理、兰心蕙质,相貌也倾国倾城,有诗为证:
乐天浔阳歌《长恨》,陈王洛水赋《感甄》。

摩诘青溪《西施咏》,潇湘红楼《明妃吟》。


没错,这诗集合了白居易《长恨歌》、曹植《感甄赋》、王维《西施咏》、林黛玉《明妃吟》,椎名的美貌用古往今来的美女诗都搁一块也写不完。淑女椎名倾慕司马雼的才华,从此司马雼正式脱单。

不久,年薪100W的司马雼在北京买房定居。又过了几个月,这对为我国IT事业做出了巨大贡献的情侣在北京举办了盛大的婚礼。

异史氏曰:公司大到一定程度,别有用心的竞争对手就会搞小动作,因此想成为大厂的技术专家,懂点安全方面的知识是很有帮助的。程序员是经常被嘲笑为“注定孤独一生”的群体,但出人头地的程序员抱得美人归的可能性非常大,毕竟女人的颜值通常和她的男朋友的收入成正比。
 
本文首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201707/543909.htm
0
评论

Android 守护进程的实现方式 推送 Android 进程

beyond 发表了文章 • 761 次浏览 • 2017-06-15 14:38 • 来自相关话题

 
   “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。
 
为什么微信可以一直在手机后台跑着能收到消息?

    国内手机厂商对 android rom 进行了定制,对后台服务以及运行在后台的程序进行了严格的限制,微信等这些大厂商的 app 都已经通过和设备厂商合作在安装时都已经加入了系统的白名单,因此设备并不会限制对方 app 在后台运行;

我自己的APP该如何实现进程保活?
 
引导用户把当前 app 加入到设备的白名单中,解除设备对 app 的限制;小米和华为设备可以集成对应的推送实现在app 被干掉后依然收推送通知;可以自己在 app 端实现守护进程的方式,让 app 在系统级别自动回收的情况下减少被杀死的概率,这种方式对用户主动回收无效。

第一条和第二条就不多说了,环信imgeek社区里已经有了相应的文章(http://www.imgeek.org/article/825308754 )接下来介绍守护进程的实现。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在Android里永生不死的,请忽略本文!请忽略!
 什么是守护进程?

    守护进程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的。
 
守护进程的实现,本文两个核心观点:
 
提高进程优先级,降低被回收或杀死概率在进程被干掉后,进行拉起
 要实现实现上边所说,通过下边几点来实现,首先我们需要了解下进程的优先级划分:

Process Importance记录在ActivityManager.java类中:**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* 此进程正在运行前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情
*/
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
* 这个过程不是用户的直接意识到,但在某种程度上是他们可以察觉的。
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* 此进程正在运行前台UI,但设备处于睡眠状态,因此用户不可见,意思是用户意识不到的进程,因为他们看不到或与它交互,
* 但它是相当重要,因为用户解锁设备时期望的返回到这个进程
*/
public static final int IMPORTANCE_TOP_SLEEPING = 150;

/**
* 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* 此进程正在运行某些对用户主动可见的内容,但不是直接显示在UI,
* 这可能运行在当前前台之后的窗口(因此暂停并且其状态被保存,不与用户交互,但在某种程度上对他们可见);
* 也可能在系统的控制下运行其他服务,
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* 服务进程,此进程包含在后台保持运行的服务,这些后台服务用户察觉不到,是无感知的,所以它们可以由系统相对自由地杀死
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* 后台进程
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* 空进程,此进程没有任何正在运行的代码
*/
public static final int IMPORTANCE_EMPTY = 500;

// 此过程不存在。
public static final int IMPORTANCE_GONE = 1000;进程回收机制

了解进程优先级之后,我们还需要知道一个进程回收机制的东西;这里参考AngelDevil在博客园上的一篇文章:
 
详情参考:【Android Low Memory Killer】
 
Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:alloc_pages -> out_of_memory() -> select_bad_process() -> badness()在Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
// 进程不存在。
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考/**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
*/
public static int procStateToImportance(int procState) {
if (procState == PROCESS_STATE_NONEXISTENT) {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_BACKGROUND;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
} else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
} else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
return IMPORTANCE_FOREGROUND;
}
}一般情况下,设备端进程被干掉有一下几种情况




由以上分析,我们可以可以总结出,如果想提高我们应用后台运行时间,就需要提高当前应用进程优先级,来减少被杀死的概率

守护进程的实现

分析了那么多,现在对Android自身后台进程管理,以及进程的回收也有了一个大致的了解,后边我们要做的就是想尽一切办法去提高应用进程优先级,降低进程被杀的概率;或者是在被杀死后能够重新启动后台守护进程

1.模拟前台进程

第一种方式就是利用系统漏洞,使用startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高(这里所说的最高是服务所能达到的最高,即1);

这种方式在7.x之前都是很好用的,QQ、微信、IReader、Keep 等好多应用都是用的这种方式实现;因为在7.x 以后的设备上,这种伪装前台进程的方式也会显示出来通知栏提醒,这个是取消不掉的,虽然Google现在还没有对这种方式加以限制,不过这个已经能够被用户感知到了,这种方式估计也用不了多久了

下边看下实现方式,这边这个VMDaemonService就是一个守护进程服务,其中在服务的onStartCommand()方法中调用startForeground()将服务进程设置为前台进程,当运行在 API18 以下的设备是可以直接设置,API18 以上需要实现一个内部的Service,这个内部类实现和外部类同样的操作,然后结束自己;当这个服务启动后就会创建一个定时器去发送广播,当我们的核心服务被干掉后,就由另外的广播接收器去接收我们守护进程发出的广播,然后唤醒我们的核心服务;/**
* 以实现内部 Service 类的方式实现守护进程,这里是利用 android 漏洞提高当前进程优先级
*
* Created by lzan13 on 2017/3/7.
*/
public class VMDaemonService extends Service {

private final static String TAG = VMDaemonService.class.getSimpleName();

// 定时唤醒的时间间隔,这里为了自己测试方边设置了一分钟
private final static int ALARM_INTERVAL = 1 * 60 * 1000;
// 发送唤醒广播请求码
private final static int WAKE_REQUEST_CODE = 5121;
// 守护进程 Service ID
private final static int DAEMON_SERVICE_ID = -5121;

@Override public void onCreate() {
Log.i(TAG, "VMDaemonService->onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
// 利用 Android 漏洞提高进程优先级,
startForeground(DAEMON_SERVICE_ID, new Notification());
// 当 SDk 版本大于18时,需要通过内部 Service 类启动同样 id 的 Service
if (Build.VERSION.SDK_INT >= 18) {
Intent innerIntent = new Intent(this, DaemonInnerService.class);
startService(innerIntent);
}

// 发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(VMWakeReceiver.DAEMON_WAKE_ACTION);

PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
ALARM_INTERVAL, operation);

/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
*/
return START_STICKY;
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "VMDaemonService->onDestroy");
super.onDestroy();
}

/**
* 实现一个内部的 Service,实现让后台服务的优先级提高到前台服务,这里利用了 android 系统的漏洞,
* 不保证所有系统可用,测试在7.1.1 之前大部分系统都是可以的,不排除个别厂商优化限制
*/
public static class DaemonInnerService extends Service {

@Override public void onCreate() {
Log.i(TAG, "DaemonInnerService -> onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "DaemonInnerService -> onStartCommand");
startForeground(DAEMON_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "DaemonInnerService -> onDestroy");
super.onDestroy();
}
}
}当我们启动这个守护进程的时候,就可以使用以下adb命令查看当前程序的进程情况(需要adb shell进去设备),
为了等下区分进程优先级,我启动了一个普通的后台进程,两外两个一个是我们启动的守护进程,一个是当前程序的核心进程,可以看到除了后台进程外,另外两个进程都带有isForeground=true的属性:# 这个命令的 services 可以换成 service,这样会只显示当前,进程,不显示详细内容
# dumpsys activity services <Your Package Name>
root@vbox86p:/ # dumpsys activity services com.vmloft.develop.daemon
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{170fe1dd u0 com.vmloft.develop.daemon/.services.VMDaemonService}
intent={cmp=com.vmloft.develop.daemon/.services.VMDaemonService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{173fe77f 2370:com.vmloft.develop.daemon:daemon/u0a68}
isForeground=true foregroundId=-5121 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-6s196ms startingBgTimeout=--
lastActivity=-6s157ms restartTime=-6s157ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2fee4f84 u0 com.vmloft.develop.daemon/.services.VMCoreService}
intent={cmp=com.vmloft.develop.daemon/.services.VMCoreService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{18c6a1b4 2343:com.vmloft.develop.daemon/u0a68}
isForeground=true foregroundId=-5120 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-28s136ms startingBgTimeout=--
lastActivity=-28s136ms restartTime=-28s136ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2ef6909e u0 com.vmloft.develop.daemon/.services.VMBackgroundService}
intent={cmp=com.vmloft.develop.daemon/.services.VMBackgroundService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:background
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{29f8734c 2388:com.vmloft.develop.daemon:background/u0a68}
createTime=-3s279ms startingBgTimeout=--
lastActivity=-3s262ms restartTime=-3s262ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1然后我们可以用下边的命令查看ProcessID# 这个命令可以查看当前DProcessID(数据结果第二列),我们可以看到当前程序有两个进程
# ps | grep com.vmloft.develop.daemon
root@vbox86p:/ # ps | grep com.vmloft.develop.daemon
u0_a68 2343 274 1012408 42188 ffffffff f74f1b45 S com.vmloft.develop.daemon
u0_a68 2370 274 997012 26152 ffffffff f74f1b45 S com.vmloft.develop.daemon:daemon
u0_a68 2388 274 997012 25668 ffffffff f74f1b45 S com.vmloft.develop.daemon:background有了ProcessID之后,我们可以根据这个ProcessID获取到当前进程的优先级状态Process State,对应Linux层的oom_adj
可以看到当前核心进程的级别为0,因为这个表示当前程序运行在前台 UI 界面,守护进程级别为1,因为我们利用漏洞设置成了前台进程,虽然不可见,但是他的级别也是比较高的,仅次于前台 UI 进程,然后普通后台进程级别为4;当我们退到后台时,可以看到核心进程的级别变为1了,这就是因为我们利用startForeground()将进程设置成前台进程的原因,这样就降低了进程被系统回收的概率了;# 这个命令就是通过 ProcessID 输出其对应 oom_adj
# cat /proc/ProcessID/oom_adj
# 程序在前台时,查询进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
0
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
# 当程序退到后台时,再次查看进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
1
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4可以看到这种方式确实能够提高进程优先级,但是在一些国产的设备上还是会被杀死的,比我我测试的时候小米点击清空最近运行的应用进程就别干掉了;当把应用加入到设备白名单里就不会被杀死了,微信就是这样,人家直接装上之后就已经在白名单里了,我们要做的就是在用户使用中引导他们将我们的程序设置进白名单,将守护进程和白名单结合起来,这样才能保证我们的应用持续或者

2.JobScheduler机制唤醒

Android系统在5.x以上版本提供了一个JobSchedule接口,系统会根据自己实现定时去调用改接口传递的进程去实现一些操作,而且这个接口在被强制停止后依然能够正常的启动;不过在一些国产设备上可能无效,比如小米;
下边是 JobServcie 的实现:/**
* 5.x 以上使用 JobService 实现守护进程,这个守护进程要做的工作很简单,就是启动应用的核心进程
* Created by lzan13 on 2017/3/8.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VMDaemonJobService extends JobService {

private final static String TAG = VMDaemonJobService.class.getSimpleName();

@Override public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob");
// 这里为了掩饰直接启动核心进程,没有做其他判断操作
startService(new Intent(getApplicationContext(), VMCoreService.class));
return false;
}

@Override public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob");
return false;
}
}我们要做的就是在需要的时候调用JobSchedule的schedule来启动任务;剩下的就不需要关心了,JobSchedule会帮我们做好,下边就是我这边实现的启动任务的方法:/**
* 5.x以上系统启用 JobScheduler API 进行实现守护进程的唤醒操作
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startJobScheduler() {
int jobId = 1;
JobInfo.Builder jobInfo = new JobInfo.Builder(jobId, new ComponentName(this, VMDaemonJobService.class));
jobInfo.setPeriodic(10000);
jobInfo.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo.build());
}3.系统 Service START_STICKY 机制重启

在实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在Service挂掉后自动拉活;不过这种方式只适合比较原生一些的系统,像小米,华为等这些定制化比较高的第三方厂商,他们都已经把这些给限制掉了;@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
* 3.一些定制化比较高的第三方系统也不适用
*/
return START_STICKY;
}这种方式在以下两种情况无效:
Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,这个服务就不能再次重启了;进程被取得Root权限的管理工具或系统工具通过fores-top方式停止掉,无法重启;一些定制化比较高的第三方系统也不适用

4.其他保活方式
利用 Native 本地进程,这个主要使用到 jni 调用底层实现,而且在 Android 5.x 以后对这个限制也比较高,不适用了,暂时不研究集成第三方SDK互相唤醒,这个只要正常集成了第三方的SDK,并使用了他们对应的服务,当一个设备安装的多个应用都集成了某一个第三方SDK时,启动任意一个 app 都会唤醒其他的 app,不过这个在一些新版的国内厂商系统也是做了限制,这种方式并没有什么效果一像素的 Activity 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;
结语

事事没有绝对,万物总有一些漏洞,就算上边的那些方式不可用了,后边肯定还会出现其他的方式;我们不能保证我们的应用不死,但我们可以提高存活率;

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是 查看全部
 
   “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。
 
为什么微信可以一直在手机后台跑着能收到消息?

    国内手机厂商对 android rom 进行了定制,对后台服务以及运行在后台的程序进行了严格的限制,微信等这些大厂商的 app 都已经通过和设备厂商合作在安装时都已经加入了系统的白名单,因此设备并不会限制对方 app 在后台运行;

我自己的APP该如何实现进程保活?
 
  1. 引导用户把当前 app 加入到设备的白名单中,解除设备对 app 的限制;
  2. 小米和华为设备可以集成对应的推送实现在app 被干掉后依然收推送通知;
  3. 可以自己在 app 端实现守护进程的方式,让 app 在系统级别自动回收的情况下减少被杀死的概率,这种方式对用户主动回收无效。


第一条和第二条就不多说了,环信imgeek社区里已经有了相应的文章(http://www.imgeek.org/article/825308754 )接下来介绍守护进程的实现。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在Android里永生不死的,请忽略本文!请忽略!
 什么是守护进程?

    守护进程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的。
 
守护进程的实现,本文两个核心观点:
 
  1. 提高进程优先级,降低被回收或杀死概率
  2. 在进程被干掉后,进行拉起

 要实现实现上边所说,通过下边几点来实现,首先我们需要了解下进程的优先级划分:

Process Importance记录在ActivityManager.java类中:
**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* 此进程正在运行前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情
*/
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
* 这个过程不是用户的直接意识到,但在某种程度上是他们可以察觉的。
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* 此进程正在运行前台UI,但设备处于睡眠状态,因此用户不可见,意思是用户意识不到的进程,因为他们看不到或与它交互,
* 但它是相当重要,因为用户解锁设备时期望的返回到这个进程
*/
public static final int IMPORTANCE_TOP_SLEEPING = 150;

/**
* 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* 此进程正在运行某些对用户主动可见的内容,但不是直接显示在UI,
* 这可能运行在当前前台之后的窗口(因此暂停并且其状态被保存,不与用户交互,但在某种程度上对他们可见);
* 也可能在系统的控制下运行其他服务,
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* 服务进程,此进程包含在后台保持运行的服务,这些后台服务用户察觉不到,是无感知的,所以它们可以由系统相对自由地杀死
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* 后台进程
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* 空进程,此进程没有任何正在运行的代码
*/
public static final int IMPORTANCE_EMPTY = 500;

// 此过程不存在。
public static final int IMPORTANCE_GONE = 1000;
进程回收机制

了解进程优先级之后,我们还需要知道一个进程回收机制的东西;这里参考AngelDevil在博客园上的一篇文章:
 
详情参考:【Android Low Memory Killer】
 
Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:
alloc_pages -> out_of_memory() -> select_bad_process() -> badness()
在Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态
// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java 
// 进程不存在。
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;
Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考
/** 
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
*/
public static int procStateToImportance(int procState) {
if (procState == PROCESS_STATE_NONEXISTENT) {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_BACKGROUND;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
} else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
} else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
return IMPORTANCE_FOREGROUND;
}
}
一般情况下,设备端进程被干掉有一下几种情况
QQ截图20170615143439.jpg

由以上分析,我们可以可以总结出,如果想提高我们应用后台运行时间,就需要提高当前应用进程优先级,来减少被杀死的概率

守护进程的实现

分析了那么多,现在对Android自身后台进程管理,以及进程的回收也有了一个大致的了解,后边我们要做的就是想尽一切办法去提高应用进程优先级,降低进程被杀的概率;或者是在被杀死后能够重新启动后台守护进程

1.模拟前台进程

第一种方式就是利用系统漏洞,使用startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高(这里所说的最高是服务所能达到的最高,即1);

这种方式在7.x之前都是很好用的,QQ、微信、IReader、Keep 等好多应用都是用的这种方式实现;因为在7.x 以后的设备上,这种伪装前台进程的方式也会显示出来通知栏提醒,这个是取消不掉的,虽然Google现在还没有对这种方式加以限制,不过这个已经能够被用户感知到了,这种方式估计也用不了多久了

下边看下实现方式,这边这个VMDaemonService就是一个守护进程服务,其中在服务的onStartCommand()方法中调用startForeground()将服务进程设置为前台进程,当运行在 API18 以下的设备是可以直接设置,API18 以上需要实现一个内部的Service,这个内部类实现和外部类同样的操作,然后结束自己;当这个服务启动后就会创建一个定时器去发送广播,当我们的核心服务被干掉后,就由另外的广播接收器去接收我们守护进程发出的广播,然后唤醒我们的核心服务;
/**
* 以实现内部 Service 类的方式实现守护进程,这里是利用 android 漏洞提高当前进程优先级
*
* Created by lzan13 on 2017/3/7.
*/
public class VMDaemonService extends Service {

private final static String TAG = VMDaemonService.class.getSimpleName();

// 定时唤醒的时间间隔,这里为了自己测试方边设置了一分钟
private final static int ALARM_INTERVAL = 1 * 60 * 1000;
// 发送唤醒广播请求码
private final static int WAKE_REQUEST_CODE = 5121;
// 守护进程 Service ID
private final static int DAEMON_SERVICE_ID = -5121;

@Override public void onCreate() {
Log.i(TAG, "VMDaemonService->onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
// 利用 Android 漏洞提高进程优先级,
startForeground(DAEMON_SERVICE_ID, new Notification());
// 当 SDk 版本大于18时,需要通过内部 Service 类启动同样 id 的 Service
if (Build.VERSION.SDK_INT >= 18) {
Intent innerIntent = new Intent(this, DaemonInnerService.class);
startService(innerIntent);
}

// 发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(VMWakeReceiver.DAEMON_WAKE_ACTION);

PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
ALARM_INTERVAL, operation);

/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
*/
return START_STICKY;
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "VMDaemonService->onDestroy");
super.onDestroy();
}

/**
* 实现一个内部的 Service,实现让后台服务的优先级提高到前台服务,这里利用了 android 系统的漏洞,
* 不保证所有系统可用,测试在7.1.1 之前大部分系统都是可以的,不排除个别厂商优化限制
*/
public static class DaemonInnerService extends Service {

@Override public void onCreate() {
Log.i(TAG, "DaemonInnerService -> onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "DaemonInnerService -> onStartCommand");
startForeground(DAEMON_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "DaemonInnerService -> onDestroy");
super.onDestroy();
}
}
}
当我们启动这个守护进程的时候,就可以使用以下adb命令查看当前程序的进程情况(需要adb shell进去设备),
为了等下区分进程优先级,我启动了一个普通的后台进程,两外两个一个是我们启动的守护进程,一个是当前程序的核心进程,可以看到除了后台进程外,另外两个进程都带有isForeground=true的属性:
# 这个命令的 services 可以换成 service,这样会只显示当前,进程,不显示详细内容
# dumpsys activity services <Your Package Name>
root@vbox86p:/ # dumpsys activity services com.vmloft.develop.daemon
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{170fe1dd u0 com.vmloft.develop.daemon/.services.VMDaemonService}
intent={cmp=com.vmloft.develop.daemon/.services.VMDaemonService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{173fe77f 2370:com.vmloft.develop.daemon:daemon/u0a68}
isForeground=true foregroundId=-5121 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-6s196ms startingBgTimeout=--
lastActivity=-6s157ms restartTime=-6s157ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2fee4f84 u0 com.vmloft.develop.daemon/.services.VMCoreService}
intent={cmp=com.vmloft.develop.daemon/.services.VMCoreService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{18c6a1b4 2343:com.vmloft.develop.daemon/u0a68}
isForeground=true foregroundId=-5120 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-28s136ms startingBgTimeout=--
lastActivity=-28s136ms restartTime=-28s136ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2ef6909e u0 com.vmloft.develop.daemon/.services.VMBackgroundService}
intent={cmp=com.vmloft.develop.daemon/.services.VMBackgroundService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:background
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{29f8734c 2388:com.vmloft.develop.daemon:background/u0a68}
createTime=-3s279ms startingBgTimeout=--
lastActivity=-3s262ms restartTime=-3s262ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1
然后我们可以用下边的命令查看ProcessID
# 这个命令可以查看当前DProcessID(数据结果第二列),我们可以看到当前程序有两个进程
# ps | grep com.vmloft.develop.daemon
root@vbox86p:/ # ps | grep com.vmloft.develop.daemon
u0_a68 2343 274 1012408 42188 ffffffff f74f1b45 S com.vmloft.develop.daemon
u0_a68 2370 274 997012 26152 ffffffff f74f1b45 S com.vmloft.develop.daemon:daemon
u0_a68 2388 274 997012 25668 ffffffff f74f1b45 S com.vmloft.develop.daemon:background
有了ProcessID之后,我们可以根据这个ProcessID获取到当前进程的优先级状态Process State,对应Linux层的oom_adj
可以看到当前核心进程的级别为0,因为这个表示当前程序运行在前台 UI 界面,守护进程级别为1,因为我们利用漏洞设置成了前台进程,虽然不可见,但是他的级别也是比较高的,仅次于前台 UI 进程,然后普通后台进程级别为4;当我们退到后台时,可以看到核心进程的级别变为1了,这就是因为我们利用startForeground()将进程设置成前台进程的原因,这样就降低了进程被系统回收的概率了;
# 这个命令就是通过 ProcessID 输出其对应 oom_adj
# cat /proc/ProcessID/oom_adj
# 程序在前台时,查询进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
0
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
# 当程序退到后台时,再次查看进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
1
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
可以看到这种方式确实能够提高进程优先级,但是在一些国产的设备上还是会被杀死的,比我我测试的时候小米点击清空最近运行的应用进程就别干掉了;当把应用加入到设备白名单里就不会被杀死了,微信就是这样,人家直接装上之后就已经在白名单里了,我们要做的就是在用户使用中引导他们将我们的程序设置进白名单,将守护进程和白名单结合起来,这样才能保证我们的应用持续或者

2.JobScheduler机制唤醒

Android系统在5.x以上版本提供了一个JobSchedule接口,系统会根据自己实现定时去调用改接口传递的进程去实现一些操作,而且这个接口在被强制停止后依然能够正常的启动;不过在一些国产设备上可能无效,比如小米;
下边是 JobServcie 的实现:
/**
* 5.x 以上使用 JobService 实现守护进程,这个守护进程要做的工作很简单,就是启动应用的核心进程
* Created by lzan13 on 2017/3/8.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VMDaemonJobService extends JobService {

private final static String TAG = VMDaemonJobService.class.getSimpleName();

@Override public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob");
// 这里为了掩饰直接启动核心进程,没有做其他判断操作
startService(new Intent(getApplicationContext(), VMCoreService.class));
return false;
}

@Override public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob");
return false;
}
}
我们要做的就是在需要的时候调用JobSchedule的schedule来启动任务;剩下的就不需要关心了,JobSchedule会帮我们做好,下边就是我这边实现的启动任务的方法:
/**
* 5.x以上系统启用 JobScheduler API 进行实现守护进程的唤醒操作
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startJobScheduler() {
int jobId = 1;
JobInfo.Builder jobInfo = new JobInfo.Builder(jobId, new ComponentName(this, VMDaemonJobService.class));
jobInfo.setPeriodic(10000);
jobInfo.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo.build());
}
3.系统 Service START_STICKY 机制重启

在实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在Service挂掉后自动拉活;不过这种方式只适合比较原生一些的系统,像小米,华为等这些定制化比较高的第三方厂商,他们都已经把这些给限制掉了;
@Override 
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
* 3.一些定制化比较高的第三方系统也不适用
*/
return START_STICKY;
}
这种方式在以下两种情况无效:
  • Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,这个服务就不能再次重启了;
  • 进程被取得Root权限的管理工具或系统工具通过fores-top方式停止掉,无法重启;
  • 一些定制化比较高的第三方系统也不适用


4.其他保活方式

  • 利用 Native 本地进程,这个主要使用到 jni 调用底层实现,而且在 Android 5.x 以后对这个限制也比较高,不适用了,暂时不研究
  • 集成第三方SDK互相唤醒,这个只要正常集成了第三方的SDK,并使用了他们对应的服务,当一个设备安装的多个应用都集成了某一个第三方SDK时,启动任意一个 app 都会唤醒其他的 app,不过这个在一些新版的国内厂商系统也是做了限制,这种方式并没有什么效果
  • 一像素的 Activity 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;


结语

事事没有绝对,万物总有一些漏洞,就算上边的那些方式不可用了,后边肯定还会出现其他的方式;我们不能保证我们的应用不死,但我们可以提高存活率;

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是
3
评论

IM-SDK和客服SDK并存开发指南—Android篇 环信移动客服 Android

beyond 发表了文章 • 2404 次浏览 • 2017-06-06 10:37 • 来自相关话题

  
   环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴在环信开发过程中,同时使用了环信即时通讯云和环信移动客服服务,就会有这样的疑问,这个时候应该使用哪个SDK 呢?这两个有什么区别?于是让开发者惊呼“还有这种操作”的《IM-SDK和客服-SDK并存开发指南》应运而生,希望能帮助小伙伴们更快速、高效集成环信。嗯,就是有这种操作,不服就来尬舞啊!!!
 一、SDK介绍
访客端客服SDK基于IM-SDK 3.x开发,包含了IM-SDK所有的API和功能,当同时使用IM和客服时,只需要在初始化、登录、登出操作时使用访客端客服 SDK 提供的相应API即可。UI部分集成需要分别导入Kefu-easeui和EaseUI(IM的EaseUI),也可以自己写UI部分。

二、注意事项
开发过程中 初始化、登录、登出,务必使用客服SDK的API。IM-SDK和客服SDK都包含了armeabi、armeabi-v7、arm64-v8a、x86的CPU架构,在发版的时候,可以选择部分CPU,例如剔除模拟器用的x86架构,在build.gradle中配置即可。
 
三、资源准备
到环信官网下载访客端客服SDK+Demo源码,下载链接:http://www.easemob.com/download/cs选择“Android客服访客端”下载(如下图)。 


到环信官网下载IM的SDK+Demo源码,下载链接:http://www.easemob.com/download/im 选择Android SDK(如下图)。  




四、资源简介
解压后的SDK压缩包中,含有基础版和实时音视频版,根据需求的不同使用不同的SDK。默认EaseUI使用的是libs文件夹内的SDK,此SDK含有实时音视频功能,因此比较大,如果不需要使用实时音视频功能的,可以使用libs.without.audio文件夹下的SDK。从官网下载的客服访客端SDK包括以下目录: 





其中:kefu-android-demo为包含实时音视频的商城demo,可以直接运行。
 libs 为客服访客端SDK,包含实时音视频功能。
libs.without.audio 为不包含实时音视频功能的客服访客端SDK。
 
五、集成步骤
参考客服访客端文档或demo源码集成客服的访客端SDK,文档地址:http://docs.easemob.com/cs/300 ... idsdk 。可以把Kefu-Easeui作为一个module,libs放入客服的SDK(kefu-sdk_*.jar和相关so),import IM的easeUI到项目中,去掉里面的hyphenate_*.jar以及相关so。注释掉EaseUI.java中的IMSDK的初始化方法,最终结果为:app项目依赖IM-easeui module,IM-easeui 依赖kefu-easeui module。在自己项目中的Application中的onCreate方法中,先调用客服SDK的初始化方法,再调动IM-easeui和Kefu-easeui的初始化方法(如果不用IM或客服的EaseUI则不需要对EaseUI做初始化)。在app项目中,调用登录、登出方法需要调动客服的API,其他的API为各自的API。在调用EaseUI相关的Activity时,如为IM-easeui的Activity需要在AndroidManifest.xml中注册,kefu-easeui则不需要再注册(因为在kefu-easeui这个module中的AndroidManifest.xml中已注册)。Demo和EaseUI的源码是开源的,也可查看下。
 
六、注意事项

   APP的通知栏提醒,客服和IM的easeui中均有自己的通知栏,代码均开源,可按照自己的方式去修改,具体应用可看下载的Demo源码中的演示实例。
 
提供的兼容Demo介绍:
 Demo是在客服的商城Demo上修改,在左上角添加了一个聊天室的按钮,点击按钮会根据appkey随机创建一个账号并登录,登录成功后会进入聊天室列表界面,点击某个聊天室可以在聊天室中聊天。 Demo中客服部分功能还是和原商城Demo功能一致。 Demo中为了演示因此采用随机注册账号的方式,对于用户场景中,可以先注册好这些账号和自己的账号绑定,这样每次咨询客服就都是同一个人了,也可以显示这个访客曾经的聊天记录。
 
Demo源码地址:http://kefu-prod-apk.oss-cn-ha ... o.zip 查看全部
  
   环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴在环信开发过程中,同时使用了环信即时通讯云和环信移动客服服务,就会有这样的疑问,这个时候应该使用哪个SDK 呢?这两个有什么区别?于是让开发者惊呼“还有这种操作”的《IM-SDK和客服-SDK并存开发指南》应运而生,希望能帮助小伙伴们更快速、高效集成环信。嗯,就是有这种操作,不服就来尬舞啊!!!
 一、SDK介绍
  1. 访客端客服SDK基于IM-SDK 3.x开发,包含了IM-SDK所有的API和功能,当同时使用IM和客服时,只需要在初始化、登录、登出操作时使用访客端客服 SDK 提供的相应API即可。
  2. UI部分集成需要分别导入Kefu-easeui和EaseUI(IM的EaseUI),也可以自己写UI部分。


二、注意事项
  1. 开发过程中 初始化、登录、登出,务必使用客服SDK的API。
  2. IM-SDK和客服SDK都包含了armeabi、armeabi-v7、arm64-v8a、x86的CPU架构,在发版的时候,可以选择部分CPU,例如剔除模拟器用的x86架构,在build.gradle中配置即可。

 
三、资源准备
  1. 到环信官网下载访客端客服SDK+Demo源码,下载链接:http://www.easemob.com/download/cs选择“Android客服访客端”下载(如下图)。 
    001.jpg
  2. 到环信官网下载IM的SDK+Demo源码,下载链接:http://www.easemob.com/download/im 选择Android SDK(如下图)。  
    002.jpg


四、资源简介
  1. 解压后的SDK压缩包中,含有基础版和实时音视频版,根据需求的不同使用不同的SDK。默认EaseUI使用的是libs文件夹内的SDK,此SDK含有实时音视频功能,因此比较大,如果不需要使用实时音视频功能的,可以使用libs.without.audio文件夹下的SDK。
  2. 从官网下载的客服访客端SDK包括以下目录: 


003.png

其中:kefu-android-demo为包含实时音视频的商城demo,可以直接运行。
 libs 为客服访客端SDK,包含实时音视频功能。
libs.without.audio 为不包含实时音视频功能的客服访客端SDK。
 
五、集成步骤
  1. 参考客服访客端文档或demo源码集成客服的访客端SDK,文档地址:http://docs.easemob.com/cs/300 ... idsdk
  2. 可以把Kefu-Easeui作为一个module,libs放入客服的SDK(kefu-sdk_*.jar和相关so),import IM的easeUI到项目中,去掉里面的hyphenate_*.jar以及相关so。注释掉EaseUI.java中的IMSDK的初始化方法,最终结果为:app项目依赖IM-easeui module,IM-easeui 依赖kefu-easeui module。
  3. 在自己项目中的Application中的onCreate方法中,先调用客服SDK的初始化方法,再调动IM-easeui和Kefu-easeui的初始化方法(如果不用IM或客服的EaseUI则不需要对EaseUI做初始化)。
  4. 在app项目中,调用登录、登出方法需要调动客服的API,其他的API为各自的API。在调用EaseUI相关的Activity时,如为IM-easeui的Activity需要在AndroidManifest.xml中注册,kefu-easeui则不需要再注册(因为在kefu-easeui这个module中的AndroidManifest.xml中已注册)。Demo和EaseUI的源码是开源的,也可查看下。

 
六、注意事项

   APP的通知栏提醒,客服和IM的easeui中均有自己的通知栏,代码均开源,可按照自己的方式去修改,具体应用可看下载的Demo源码中的演示实例。
 
提供的兼容Demo介绍:
  1.  Demo是在客服的商城Demo上修改,在左上角添加了一个聊天室的按钮,点击按钮会根据appkey随机创建一个账号并登录,登录成功后会进入聊天室列表界面,点击某个聊天室可以在聊天室中聊天。
  2.  Demo中客服部分功能还是和原商城Demo功能一致。
  3.  Demo中为了演示因此采用随机注册账号的方式,对于用户场景中,可以先注册好这些账号和自己的账号绑定,这样每次咨询客服就都是同一个人了,也可以显示这个访客曾经的聊天记录。

 
Demo源码地址:http://kefu-prod-apk.oss-cn-ha ... o.zip
0
评论

Error:warning: Ignoring InnerClasses attribute for an anonymous inner class 解决方案 Android

Style℃ 发表了文章 • 820 次浏览 • 2017-05-27 17:06 • 来自相关话题

首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持:

defaultConfig {
        multiDexEnabled true
   }

dependencies { compile 'com.Android.support:multidex:1.0.1' } 

然后让应用支持多DEX文件。在MultiDexApplication JavaDoc中描述了三种可选方法:

1、在AndroidManifest.xml的application中声明android.support.multidex.MultiDexApplication;
2、如果你已经有自己的Application类,让其继承MultiDexApplication;
3、如果你的Application类已经继承自其它类,你不想修改它,那么可以重写attachBaseContext()方法:
 
@Override   
protected void attachBaseContext(Context base) {  
    super.attachBaseContext(base); MultiDex.install(this);  
}  
 
运行就可以了,也可能打包了,这个问题貌似是工程中的方法数量超过安卓规定65536个方法数了,,,
  查看全部

20160701171527638.png

首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持:

defaultConfig {
        multiDexEnabled true
   }

dependencies { compile 'com.Android.support:multidex:1.0.1' } 

然后让应用支持多DEX文件。在MultiDexApplication JavaDoc中描述了三种可选方法:

1、在AndroidManifest.xml的application中声明android.support.multidex.MultiDexApplication;
2、如果你已经有自己的Application类,让其继承MultiDexApplication;
3、如果你的Application类已经继承自其它类,你不想修改它,那么可以重写attachBaseContext()方法:
 
@Override   
protected void attachBaseContext(Context base) {  
    super.attachBaseContext(base); MultiDex.install(this);  
}  
 
运行就可以了,也可能打包了,这个问题貌似是工程中的方法数量超过安卓规定65536个方法数了,,,
 
1
评论

拍照闪退 Android

gaomode 发表了文章 • 218 次浏览 • 2017-05-16 18:09 • 来自相关话题

在聊天页面点击拍照时闪退——>7.0及以上的系统手机处理拍照处理与原有的方法不一样了
参考:
http://blog.csdn.net/ganshenml/article/details/72315636
在聊天页面点击拍照时闪退——>7.0及以上的系统手机处理拍照处理与原有的方法不一样了
参考:
http://blog.csdn.net/ganshenml/article/details/72315636
3
评论

【环信3.0SDK集成小米推送教程】实现离线消息推送和后台视频电话通知 小米推送 Message Offline Push call Android

lzan13 发表了文章 • 3387 次浏览 • 2017-05-05 18:04 • 来自相关话题

教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall】
前言

  从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频通话请求就不能再走之前的聊天通道了,所以就要用到我们今天的主角-推送。苹果手机自带了apns,上传推送证书到环信后台就可以实现ios手机的消息推送,Android虽然也有gcm, 但是在大陆地区是不能正常使用的(海外APP不受影响),那么在国内Android的APP就需要用到第三方推送,环信调研了市场设备情况,选择集成了两家厂商推送,分别是小米推送和华为推送,最大程度保证了应用在后台被杀死的情况下也收到离线消息的通知。






废话不多说,今天就通过集成最新的小米推送来实现下消息的离线推送通知,以及被呼叫方离线时方推送提醒对方启动 app 接听通话;其实都是通过集成推送完成!

准备工作

首先你的项目需要集成环信 sdk,并且已经实现了发送消息以及音视频通话功能(这个可以直接用我上边 github 上的项目);
然后你需要有小米的开发者账户,需要创建一个应用,包名要和你自己的项目一样,然后需要用到的就是应用的appId、appKey、appSecret,这些在环信开发者后台上传小米证书,以及在项目中初始化小米推送需要用到;

开始集成

首先这边先把证书弄好了,证书的名字和秘钥以及包名一定要对应:




 
然后需要做的就是在代码中集成小米推送,需要做的有两个地方:

在初始化 sdk 的时候调用 options 设置小米的 appId 和 appKey
在 AndroidManifest配置文件配置相应的权限和广播接收器以及服务 /**
* 初始化环信sdk,并做一些注册监听的操作,这里把其他的处理都去掉了只写了小米推送
*/
private void initHyphenate() {
// 初始化sdk的一些配置
EMOptions options = new EMOptions();
// 设置小米推送 appID 和 appKey
options.setMipushConfig("2882303761517573806", "5981757315806");
// 初始化环信SDK,一定要先调用init()
EMClient.getInstance().init(context, options);
// 开启 debug 模式
EMClient.getInstance().setDebugMode(true);
}
然后就是AndroidManifest配置<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vmloft.develop.app.demo.call">

<!-- 项目权限配置 -->
<!--小米推送相关权限-->
<permission
android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"
android:protectionLevel="signature"/>
<uses-permission android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"/>
<!--小米推送权限 end-->
<!--程序入口-->
<application
android:name="com.vmloft.develop.app.demo.call.AppApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<!--小米推送相关配置-->
<service
android:name="com.xiaomi.push.service.XMJobService"
android:enabled="true"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"
android:process=":pushservice"/>

<service
android:name="com.xiaomi.push.service.XMPushService"
android:enabled="true"
android:process=":pushservice"/>

<service
android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
android:enabled="true"
android:exported="true"/>
<service
android:name="com.xiaomi.mipush.sdk.MessageHandleService"
android:enabled="true"/>

<!--推送消息广播接收器-->
<receiver
android:name=".push.MIPushReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE"/>
</intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED"/>
</intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.ERROR"/>
</intent-filter>
</receiver>
<receiver
android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>

<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<receiver
android:name="com.xiaomi.push.service.receivers.PingReceiver"
android:exported="false"
android:process=":pushservice">
<intent-filter>
<action android:name="com.xiaomi.push.PING_TIMER"/>
</intent-filter>
</receiver>
<!--小米推送配置 end-->
</application>
</manifest>










其中MIPushReceiver这个广播接收器可以不用自己实现,环信 sdk 已经集成小米广播接收器EMMipushReceiver实现,可以直接用(这里如果需要自己与自己的业务处理可以继承它去处理自己的逻辑;详细可以根据小米推送官方 sdk 文档进行了解下);

当我们做完这些之后在收到离线消息后就可以收到推送通知了,只不过这个推送通知我们不能自定义,因为这些都是服务器推什么我们接受什么,这点比较坑!

通话的离线通知

上边已经实现了消息的离线通知,我们下边就要做当呼叫对方时,对方却不在线,我们怎么通知对方打开 app 进行接听呢?
曾经集成过环信用户应该知道,在呼叫对方不在线后会马上结束通话,回调对方不在线,在新版3.2.2的 sdk 中新增设置音视频参数及呼叫时对方离线是否发推送的接口,在初始化的时候进行以下设置:// 设置通话过程中对方如果离线是否发送离线推送通知,默认 false
EMClient.getInstance().callManager().getCallOptions().setIsSendPushIfOffline(true);
// 设置了这个之后就不会在通话状态监听中回调对方不在线,需要实现另外一个回调
...
// 设置音频通话推送提供者,在 onRemoteOffline()回调中给对方发送消息就行了
EMClient.getInstance().callManager().setPushProvider(EMCallManager.EMCallPushProvider {
@Override public void onRemoteOffline(String username) {
EMMessage message = EMMessage.createTxtSendMessage("有人呼叫你,开启 APP 接听吧", username);
// 设置强制推送
message.setAttribute("em_force_notification", "true");
// 设置自定义推送提示
JSONObject extObj = new JSONObject();
try {
extObj.put("em_push_title", "有人呼叫你,开启 APP 接听吧");
extObj.put("extern", "定义推送扩展内容");
} catch (JSONException e) {
e.printStackTrace();
}
message.setAttribute("em_apns_ext", extObj);
message.setMessageStatusCallback(new EMCallBack() {
@Override public void onSuccess() {
// 在这里可以删除消息
}
@Override public void onError(int i, String s) {
// 在这里可以删除消息
}
@Override public void onProgress(int i, String s) {}
});
EMClient.getInstance().chatManager().sendMessage(message);
}
});
实现了上边的这个推送提供者之后,当对方不在线就会回调 onRemoteOffline()方法,就可以发送一条消息给对方,然后上边我们已经集成了小米推送,就可以通过离线推送的方式通知对方有新消息,对方看到后点击通知栏就可以打开 app了,这个时候我们的语音或视频呼叫还在一直呼叫,然后就可以连通了!

结语

OK 到这里基本就已经完成了,大家可以运行自己的项目,或者我上边的 demo 测试下,我这边通过小米5测试 OK;其实集成推送部分并不难,只是有几点需要注意:

环信开发者后台的推送证书设置时一定要注意应用包名和小米推送后台的应用包名以及自己项目的包名,三个地方一定要一致
初始化设置一定要通过环信的 options 去设置小米推送的 appId 和 appKey,不需要用小米的注册方法自己注册;
Androidmanifest 一定要加上环信的广播接收器,或者继承自环信封装的广播接收器

注意以上几点基本推送就没有问题了,如果不行可以先通过小米开发者后台的推送工具测试推送是否通了,然后检查以上几点;
PS:华为推送相关其实一样,不过因为华为不允许个人开发者注册账户,所以这里暂时不赘述

参考资料

【小米推送 Android SDK文档】
【环信推送相关文档】 查看全部

教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall


前言

  从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频通话请求就不能再走之前的聊天通道了,所以就要用到我们今天的主角-推送。苹果手机自带了apns,上传推送证书到环信后台就可以实现ios手机的消息推送,Android虽然也有gcm, 但是在大陆地区是不能正常使用的(海外APP不受影响),那么在国内Android的APP就需要用到第三方推送,环信调研了市场设备情况,选择集成了两家厂商推送,分别是小米推送和华为推送,最大程度保证了应用在后台被杀死的情况下也收到离线消息的通知。

banner.jpg


废话不多说,今天就通过集成最新的小米推送来实现下消息的离线推送通知,以及被呼叫方离线时方推送提醒对方启动 app 接听通话;其实都是通过集成推送完成!

准备工作

首先你的项目需要集成环信 sdk,并且已经实现了发送消息以及音视频通话功能(这个可以直接用我上边 github 上的项目);
然后你需要有小米的开发者账户,需要创建一个应用,包名要和你自己的项目一样,然后需要用到的就是应用的appId、appKey、appSecret,这些在环信开发者后台上传小米证书,以及在项目中初始化小米推送需要用到;

开始集成

首先这边先把证书弄好了,证书的名字和秘钥以及包名一定要对应:
QQ20170505-162024.png

 
然后需要做的就是在代码中集成小米推送,需要做的有两个地方:

在初始化 sdk 的时候调用 options 设置小米的 appId 和 appKey
在 AndroidManifest配置文件配置相应的权限和广播接收器以及服务
    /**
* 初始化环信sdk,并做一些注册监听的操作,这里把其他的处理都去掉了只写了小米推送
*/
private void initHyphenate() {
// 初始化sdk的一些配置
EMOptions options = new EMOptions();
// 设置小米推送 appID 和 appKey
options.setMipushConfig("2882303761517573806", "5981757315806");
// 初始化环信SDK,一定要先调用init()
EMClient.getInstance().init(context, options);
// 开启 debug 模式
EMClient.getInstance().setDebugMode(true);
}

然后就是AndroidManifest配置
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.vmloft.develop.app.demo.call">

<!-- 项目权限配置 -->
<!--小米推送相关权限-->
<permission
android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"
android:protectionLevel="signature"/>
<uses-permission android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"/>
<!--小米推送权限 end-->
<!--程序入口-->
<application
android:name="com.vmloft.develop.app.demo.call.AppApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<!--小米推送相关配置-->
<service
android:name="com.xiaomi.push.service.XMJobService"
android:enabled="true"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE"
android:process=":pushservice"/>

<service
android:name="com.xiaomi.push.service.XMPushService"
android:enabled="true"
android:process=":pushservice"/>

<service
android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
android:enabled="true"
android:exported="true"/>
<service
android:name="com.xiaomi.mipush.sdk.MessageHandleService"
android:enabled="true"/>

<!--推送消息广播接收器-->
<receiver
android:name=".push.MIPushReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE"/>
</intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED"/>
</intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.ERROR"/>
</intent-filter>
</receiver>
<receiver
android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>

<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<receiver
android:name="com.xiaomi.push.service.receivers.PingReceiver"
android:exported="false"
android:process=":pushservice">
<intent-filter>
<action android:name="com.xiaomi.push.PING_TIMER"/>
</intent-filter>
</receiver>
<!--小米推送配置 end-->
</application>
</manifest>










其中MIPushReceiver这个广播接收器可以不用自己实现,环信 sdk 已经集成小米广播接收器EMMipushReceiver实现,可以直接用(这里如果需要自己与自己的业务处理可以继承它去处理自己的逻辑;详细可以根据小米推送官方 sdk 文档进行了解下);

当我们做完这些之后在收到离线消息后就可以收到推送通知了,只不过这个推送通知我们不能自定义,因为这些都是服务器推什么我们接受什么,这点比较坑!

通话的离线通知

上边已经实现了消息的离线通知,我们下边就要做当呼叫对方时,对方却不在线,我们怎么通知对方打开 app 进行接听呢?
曾经集成过环信用户应该知道,在呼叫对方不在线后会马上结束通话,回调对方不在线,在新版3.2.2的 sdk 中新增设置音视频参数及呼叫时对方离线是否发推送的接口,在初始化的时候进行以下设置:
// 设置通话过程中对方如果离线是否发送离线推送通知,默认 false
EMClient.getInstance().callManager().getCallOptions().setIsSendPushIfOffline(true);
// 设置了这个之后就不会在通话状态监听中回调对方不在线,需要实现另外一个回调
...
// 设置音频通话推送提供者,在 onRemoteOffline()回调中给对方发送消息就行了
EMClient.getInstance().callManager().setPushProvider(EMCallManager.EMCallPushProvider {
@Override public void onRemoteOffline(String username) {
EMMessage message = EMMessage.createTxtSendMessage("有人呼叫你,开启 APP 接听吧", username);
// 设置强制推送
message.setAttribute("em_force_notification", "true");
// 设置自定义推送提示
JSONObject extObj = new JSONObject();
try {
extObj.put("em_push_title", "有人呼叫你,开启 APP 接听吧");
extObj.put("extern", "定义推送扩展内容");
} catch (JSONException e) {
e.printStackTrace();
}
message.setAttribute("em_apns_ext", extObj);
message.setMessageStatusCallback(new EMCallBack() {
@Override public void onSuccess() {
// 在这里可以删除消息
}
@Override public void onError(int i, String s) {
// 在这里可以删除消息
}
@Override public void onProgress(int i, String s) {}
});
EMClient.getInstance().chatManager().sendMessage(message);
}
});

实现了上边的这个推送提供者之后,当对方不在线就会回调 onRemoteOffline()方法,就可以发送一条消息给对方,然后上边我们已经集成了小米推送,就可以通过离线推送的方式通知对方有新消息,对方看到后点击通知栏就可以打开 app了,这个时候我们的语音或视频呼叫还在一直呼叫,然后就可以连通了!

结语

OK 到这里基本就已经完成了,大家可以运行自己的项目,或者我上边的 demo 测试下,我这边通过小米5测试 OK;其实集成推送部分并不难,只是有几点需要注意:

环信开发者后台的推送证书设置时一定要注意应用包名和小米推送后台的应用包名以及自己项目的包名,三个地方一定要一致
初始化设置一定要通过环信的 options 去设置小米推送的 appId 和 appKey,不需要用小米的注册方法自己注册;
Androidmanifest 一定要加上环信的广播接收器,或者继承自环信封装的广播接收器

注意以上几点基本推送就没有问题了,如果不行可以先通过小米开发者后台的推送工具测试推送是否通了,然后检查以上几点;

PS:华为推送相关其实一样,不过因为华为不允许个人开发者注册账户,所以这里暂时不赘述



参考资料

小米推送 Android SDK文档
环信推送相关文档
6
评论

使用环信3.xSDK 在 TV 端集成音视频通话功能 TV 音视频 Hyphenate 环信 Android

lzan13 发表了文章 • 1356 次浏览 • 2017-04-07 17:00 • 来自相关话题

使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上
 
项目git源码https://github.com/lzan13/VMChatDemoCall
 
VMTVCall

使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上,让爸妈在家和自己进行高清通话

使用版本
AndrodiStudio 2.3.0Gradle 3.3SDK Build Tools 25.0.2SDK Compile 25SDK mini 19Leanback 25.3.0CardView 25.3.0ButterKnife 8.5.1EventBus 3.0.0环信 SDK 3.3.0自己封装的工具类库,暂时只能下载源码引用

需要注意的是,这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下

实现功能
项目首次启动自动注册登录拨号盘实现历史通话记录 TODO视频通话功能(因为电视不需要语音通话以及最小化)视频通话的录制通话截图

其他相关项目

这也实现了一个移动端的音视频小项目,使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能

移动端项目【移动端实现音视频通话项目】

项目截图

首界面 




通话界面 




  查看全部
使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上
 
项目git源码https://github.com/lzan13/VMChatDemoCall
 
VMTVCall

使用环信 SDK 开发一款在 TV 上视频通话应用,可以安装在自己的电视上,让爸妈在家和自己进行高清通话

使用版本


需要注意的是,这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下

实现功能
  • 项目首次启动自动注册登录
  • 拨号盘实现
  • 历史通话记录 TODO
  • 视频通话功能(因为电视不需要语音通话以及最小化)
  • 视频通话的录制
  • 通话截图


其他相关项目

这也实现了一个移动端的音视频小项目,使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能

移动端项目【移动端实现音视频通话项目

项目截图

首界面 
001.jpg

通话界面 
002.jpg

 
0
评论

使用环信3.xSDK 集成音视频通话功能 音视频 Hyphenate 环信 Android

lzan13 发表了文章 • 2406 次浏览 • 2017-04-07 17:00 • 来自相关话题

    使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能
 
项目源码git地址https://github.com/lzan13/VMLibraryManager
 
使用版本
AndrodiStudio 2.3.0Gradle 3.3SDK Build Tools 25.0.2SDK Compile 25SDK mini 19Design 25.3.0ButterKnife 8.5.1EventBus 3.0.0环信 SDK 3.3.0自己封装的工具类库,暂时只能下载源码引用

PS:这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下 PS:必须使用环信SDK3.3.0以后的版本

实现功能
通话界面最小化及恢复通话悬浮窗的实现,可拖动视频通话界面切换视频通话的录制视频通话的截图横竖屏的自动切换

已知问题
未接通时切换到悬浮窗,当接通时无法显示画面主叫方接通时无法显示远程图像

项目截图























关联项目

实现有一个 TV 端的应用,可以实现和移动端进行实时通话,给大家在 TV 端使用环信 SDK 进行集成音视频通话加以参考
【TV 端视频通话项目】
 
  查看全部
    使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能
 
项目源码git地址https://github.com/lzan13/VMLibraryManager
 
使用版本


PS:这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下 PS:必须使用环信SDK3.3.0以后的版本

实现功能
  • 通话界面最小化及恢复
  • 通话悬浮窗的实现,可拖动
  • 视频通话界面切换
  • 视频通话的录制
  • 视频通话的截图
  • 横竖屏的自动切换


已知问题
  • 未接通时切换到悬浮窗,当接通时无法显示画面
  • 主叫方接通时无法显示远程图像


项目截图
001.png

002.png


003.png


004.png


005.png

关联项目

实现有一个 TV 端的应用,可以实现和移动端进行实时通话,给大家在 TV 端使用环信 SDK 进行集成音视频通话加以参考
TV 端视频通话项目
 
 
3
评论

Android ios V3.3.0 SDK 已发布,增加群组、聊天室管理员权限 iOS Android 产品更新

产品更新 发表了文章 • 611 次浏览 • 2017-03-08 16:37 • 来自相关话题

 Android​ V3.3.0 2017-03-07
 
新功能:
群组和聊天室改造:增加管理员权限,新增禁言,增减管理员的功能,支持使用分批的方式获取成员,禁言,管理员列表,支持完善的聊天室功能。新增加API请查看链接3.3.0 api修改优化dns劫持时的处理增加EMConversation.latestMessageFromOthers,表示收到对方的最后一条消息增加EMClient.compressLogs,压缩log,Demo中增加通过邮件发送log的示例libs.without.audio继续支持armeabi,解决armeabi-v5te的支持问题

bug 修订:
修复2.x升级3.x消息未读数为0的bugDemo在视频通话时,主叫方铃声没有播放的问题Demo在视频通话时,主叫方在建立连接成功后,文字提示不正确Demo在聊天窗口界面,清空消息后,收到新的消息,返回会话列表,未读消息数显示不正确修复在Oppo和Vivo手机上出现的JobService报错。EMGroupManager.createGroup成员列表数超过512产生的overflow错误修复部分手机在网络切换时发消息慢的bug
 
ios V3.3.0 2017-03-07
 
新功能:
新增:群组改造,增加一系列新接口,具体查看iOS iOS 3.3.0 api修改新增:获取SDK日志路径接口,将日志文件压缩成.gz文件,返回gz文件路径,[EMClient getLogFilesPath:]更新:使用视频通话录制功能时,必须在开始通话之前调用[EMVideoRecorderPlugin initGlobalConfig]

优化:
优化DNS劫持时的处理切换网络时,减小消息重发的等待时间

修复:
音视频通话丢包率(以前返回的是丢包数)IOS动态库用H264编码在iPhone6s上崩溃实时音视频新旧版互通崩溃
 
版本历史:Android SDK更新日志  ios SDK更新日志
下载地址:SDK下载 查看全部

7658.jpg_wh860_.jpg

 Android​ V3.3.0 2017-03-07
 
新功能:
  1. 群组和聊天室改造:增加管理员权限,新增禁言,增减管理员的功能,支持使用分批的方式获取成员,禁言,管理员列表,支持完善的聊天室功能。新增加API请查看链接3.3.0 api修改
  2. 优化dns劫持时的处理
  3. 增加EMConversation.latestMessageFromOthers,表示收到对方的最后一条消息
  4. 增加EMClient.compressLogs,压缩log,Demo中增加通过邮件发送log的示例
  5. libs.without.audio继续支持armeabi,解决armeabi-v5te的支持问题


bug 修订:
  1. 修复2.x升级3.x消息未读数为0的bug
  2. Demo在视频通话时,主叫方铃声没有播放的问题
  3. Demo在视频通话时,主叫方在建立连接成功后,文字提示不正确
  4. Demo在聊天窗口界面,清空消息后,收到新的消息,返回会话列表,未读消息数显示不正确
  5. 修复在Oppo和Vivo手机上出现的JobService报错。
  6. EMGroupManager.createGroup成员列表数超过512产生的overflow错误
  7. 修复部分手机在网络切换时发消息慢的bug

 
ios V3.3.0 2017-03-07
 
新功能:
  1. 新增:群组改造,增加一系列新接口,具体查看iOS iOS 3.3.0 api修改
  2. 新增:获取SDK日志路径接口,将日志文件压缩成.gz文件,返回gz文件路径,[EMClient getLogFilesPath:]
  3. 更新:使用视频通话录制功能时,必须在开始通话之前调用[EMVideoRecorderPlugin initGlobalConfig]


优化:
  1. 优化DNS劫持时的处理
  2. 切换网络时,减小消息重发的等待时间


修复:
  1. 音视频通话丢包率(以前返回的是丢包数)
  2. IOS动态库用H264编码在iPhone6s上崩溃
  3. 实时音视频新旧版互通崩溃

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

环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现 Android Demo源码分析 集成笔记

随缘 发表了文章 • 1734 次浏览 • 2017-02-21 16:55 • 来自相关话题

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面

环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 前言

   手头工作上,正好需要在已有的两个App上集成IM功能。且迭代流程中是有开发详案这一项的。就分享给大家,边写开发详案边写代码。好吧,废话不多说,我们一起来学习如何集成和改造这款简单易用而又非常强大的环信SDK。
 具体步骤
迭代点

需要做的功能点及工作

1.集成环信

2.围绕UE和UI进行编码
房源详情增加咨询按钮,点击进入咨询对话框,并且将房源信息带入对话框。消息中心
主界面TABBAR点击消息进入该界面包含系统消息入库和咨询用户列表从TABBAR点击“消息”图标进入本页面后,可以在本页面进入”系统消息“,并且将咨询过的用户会话显示在本页,长按任意一条会话,提示删除当前会话确定。列表排序:“系统通知“仍然在最上面的位置,不受排序影响咨询排序:按最后聊天时间倒序排列,咨询列表默认显示20条,多了拖动加载分页数据。无咨询用户时,只显示”系统通知“入口即可。咨询列表:长按可删除当前聊天对象,需要有确认对话框(只删除会话,不删除聊天记录)
 
根据UE和UI改造聊天窗口(EaseUI库)
注意以下几点
从房源详情页进入时,就返回房源详情页,从消息中心进入时,就返回消息中心。显示当前咨询人的经纪人姓名,并显示当前咨询的对象的在线状态(在线/离线)标题头中的电话按钮可以直接拨打电话对于从房源详情进入时带入的房源详情类型的聊天条目,经纪人可以点击查看该房源在STM中的详情。显示经纪人照片上传的照片,如果经纪人没有上传照片,就显示一个经纪人的占位图(要区别于用户的占位图)当前用户头像默认显示当前用户的头像,如果没有头像,就显示一个默认的占位图聊天内容上长按可复制发送的是手机号码时可以直接打电话。
思路
先做加法,再做减法

我们来按照原有代码改造和设计环信SDK部分相关代码改造,两个部分来做工作。将具体的功能点拆分并给出实现。

我们在Demo上修改,修改完成后剔除无关代码抽取成独立的我们需要的相关代码。整个工作也就结束了。

通过之前的代码阅读,我们知道整个Demo是一个相对完整的App,而我们实际工作中集成个im基本出不了这个范围。

就好比这次迭代也是。

因为实际整个涉及的只有会话列表和聊天界面,我们主要关注ConversationListFragment与ChatActivity就行了。
实现
SeeHouse相关改造
原有代码改造
房源详情增加咨询按钮,点击进入咨询对话框,并且将房源信息带入对话框。
主界面TABBAR点击消息进入该界面
涉及环信SDK部分相关代码改造
包含系统消息入库和咨询用户列表

同列表,不同type类型区分,并置顶系统消息
 从TABBAR点击“消息”图标进入本页面后,可以在本页面进入”系统消息“,并且将咨询过的用户会话显示在本页,长按任意一条会话,提示删除当前会话确定。
直接贴过去,Demo已经实现。
 
列表排序:“系统通知“仍然在最上面的位置,不受排序影响根据Type来判断类型,并排序置顶。
 
咨询排序:按最后聊天时间倒序排列,咨询列表默认显示20条,多了拖动加载分页数据。sort算法改一下,看下本身是否带分页。
 
无咨询用户时,只显示”系统通知“入口即可。
无需实现。
 
咨询列表:长按可删除当前聊天对象,需要有确认对话框(只删除会话,不删除聊天记录)




环信的哥哥们已经帮我们实现了。但是根据要求呢,我没只需要删除会话,所以我们把第二项注释掉。




我们把对应处的判断代码和对应的menu文件em_delete_message中的标签给注释掉。看效果。




从房源详情页进入时,就返回房源详情页,从消息中心进入时,就返回消息中心。​
直接finish();
显示当前咨询人的经纪人姓名,并显示当前咨询的对象的在线状态(在线/离线)官方的EaseUi是这么说的




我们来找下EaseTitleBar




我们来看下他的布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="@dimen/height_top_bar"
android:background="@color/top_bar_normal_bg"
android:gravity="center_vertical" >

<RelativeLayout
android:id="@+id/left_layout"
android:layout_width="50dip"
android:layout_height="match_parent"
android:background="@drawable/ease_common_tab_bg"
android:clickable="true" >

<ImageView
android:id="@+id/left_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:scaleType="centerInside" />
</RelativeLayout>

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#ffffff"
android:textSize="20sp" />

<RelativeLayout
android:id="@+id/right_layout"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:background="@drawable/ease_common_tab_bg" >

<ImageView
android:id="@+id/right_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:scaleType="centerInside" />
</RelativeLayout>

</RelativeLayout>其实有title和rightview的。




我们来对title加入一个是否在线的状态
1.获取tokenMacBook:~ mli$ curl -X POST "https://a1.easemob.com/1177170 ... ot%3B -d '{"grant_type":"client_credentials","client_id":"YXA6vcNInEeatzGVyK0tA","client_secret":"YXA6YACo7qumFfgYdWher3D3Cs"}'{"access_token":"YWMtOT73nvcIEeaPCCuTQsCAAAVuOB_MQchxsIsxVJFXsW6lZ8f2l__xn8","expires_in":5168429,"application":"bd09c370-d227-11e6-adcc-65700322b4b4"}2.拿token获取用户状态MacBook:~ mli$ curl -X GET -i -H "Authorization: Bearer YWMtOT73nvcIEeaPCCuTQsC6kwAAAVuOB_MQchxsIsxybVJFXsW6lZ8f2l__xn8" "https://a1.easemob.com/1177170 ... ot%3B

HTTP/1.1 200 OK

Server: Tengine/2.0.3

Date: Mon, 20 Feb 2017 05:24:00 GMT

Content-Type: application/json;charset=UTF-8

Transfer-Encoding: chunked

Connection: keep-alive

Access-Control-Allow-Origin: *

Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sun, 19-Feb-2017 05:24:00 GMT

{

"action" : "get",

"uri" : "http://a1.easemob.com/11771701 ... ot%3B,

"entities" : [ ],

"data" : {

"2" : "offline"

},

"timestamp" : 1487568240699,

"duration" : 25,

"count" : 0

}MacBook:~ mli$ curl -X GET -i -H "Authorization: Bearer YWMtOT73nvcIEeaPCCuCkwAAAVuOB_MQchxsIJFXsW6lZ8f2l__xn8" "https://a1.easemob.com/1177170 ... ot%3B

HTTP/1.1 200 OK

Server: Tengine/2.0.3

Date: Mon, 20 Feb 2017 05:24:08 GMT

Content-Type: application/json;charset=UTF-8

Transfer-Encoding: chunked

Connection: keep-alive

Access-Control-Allow-Origin: *

Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sun, 19-Feb-2017 05:24:08 GMT

{

"action" : "get",

"uri" : "http://a1.easemob.com/11771701 ... ot%3B,

"entities" : [ ],

"data" : {

"1" : "online"

},

"timestamp" : 1487568248135,

"duration" : 14,

"count" : 0

MacBook:~ mli$ 我们可以看到2是离线,1是在线的。

注意一点





所以昵称是在咱自己的体系的。可以从现有的App里提取,如果有的话。

我们知道从列表ConversationListFragment->ChatActivity->ChatFragment

那么如何接受和发送自己与他人的头像和昵称呢?

我们来玩这个ChatFragment




在OnSetMessageAttributes中,设置我们要发送时的消息扩展属性。

那么接收怎么办呢,我们来看下DemoHelper中的getUserInfo()方法。




无聊的用鄙人蹩脚的英文写了一把注释。英文若是写的不对就不对吧。
标题头中的电话按钮可以直接拨打电话
修改删除按钮为打电话,并改动相关代码
显示经纪人照片上传的照片,如果经纪人没有上传照片,就显示一个经纪人的占位图(要区别于用户的占位图)
修改原demo
当前用户头像默认显示当前用户的头像,如果没有头像,就显示一个默认的占位图
修改原demo。
聊天内容上长按可复制




自带了,后面我们可能需要去掉转发。

发送的是手机号码时可以直接打电话。

我们再长按后判断其是否为电话号码,如果是添加一项拨打电话。

引用关系是这样的

ChatFragment->ContextMenuActivity->em_context_menu_for_location.xml

最后调回ChatFragment的onActivityResult

我们来改em_context_menu_for_location.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:gravity="center_horizontal"
android:orientation="vertical" >

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="copy"
android:padding="10dp"
android:text="@string/copy_message"
android:textColor="@android:color/black"
android:textSize="20sp" />

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="delete"
android:padding="10dp"
android:text="@string/delete_message"
android:textColor="@android:color/black"
android:textSize="20sp" />
<!-- <View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray" />

<TextView
android:id="@+id/forward"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="forward"
android:padding="10dp"
android:text="@string/forward"
android:textColor="@android:color/black"
android:textSize="20sp" />-->
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray" />
<TextView
android:id="@+id/call_phone"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="call"
android:padding="10dp"
android:text="@string/call_phone"
android:textColor="@android:color/black"
android:textSize="20sp" />
</LinearLayout>再来改ContextMenuActivity/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.R;

public class ContextMenuActivity extends BaseActivity {
public static final int RESULT_CODE_COPY = 1;
public static final int RESULT_CODE_DELETE = 2;
public static final int RESULT_CODE_FORWARD = 3;
public static final int RESUTL_CALL_PHONE = 4;
String phoneNumber;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EMMessage message = getIntent().getParcelableExtra("message");
boolean isChatroom = getIntent().getBooleanExtra("ischatroom", false);
phoneNumber = getIntent().getStringExtra("phone_number");

int type = message.getType().ordinal();
if (type == EMMessage.Type.TXT.ordinal()) {
if(message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)
|| message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false)
//red packet code : 屏蔽红包消息、转账消息的转发功能
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)){
//end of red packet code
setContentView(R.layout.em_context_menu_for_location);
}else if(message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_BIG_EXPRESSION, false)){
setContentView(R.layout.em_context_menu_for_image);
}else{
//for text content
setContentView(R.layout.em_context_menu_for_text);
//for call phone number
TextView callPhone = (TextView) findViewById(R.id.call_phone);
if(!TextUtils.isEmpty(phoneNumber)){
callPhone.setVisibility(View.VISIBLE);
callPhone.setText("拨打电话:" + phoneNumber);
}else{
callPhone.setVisibility(View.GONE);
}
}
} else if (type == EMMessage.Type.LOCATION.ordinal()) {
setContentView(R.layout.em_context_menu_for_location);
} else if (type == EMMessage.Type.IMAGE.ordinal()) {
setContentView(R.layout.em_context_menu_for_image);
} else if (type == EMMessage.Type.VOICE.ordinal()) {
setContentView(R.layout.em_context_menu_for_voice);
} else if (type == EMMessage.Type.VIDEO.ordinal()) {
setContentView(R.layout.em_context_menu_for_video);
} else if (type == EMMessage.Type.FILE.ordinal()) {
setContentView(R.layout.em_context_menu_for_location);
}
if (isChatroom
//red packet code : 屏蔽红包消息、转账消息的撤回功能
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
//end of red packet code
View v = (View) findViewById(R.id.forward);
if (v != null) {
v.setVisibility(View.GONE);
}
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
finish();
return true;
}

public void copy(View view){
setResult(RESULT_CODE_COPY);
finish();
}
public void delete(View view){
setResult(RESULT_CODE_DELETE);
finish();
}
public void forward(View view){
setResult(RESULT_CODE_FORWARD);
finish();
}

public void call(View view) {
Intent it = new Intent();
it.putExtra("phone_number",phoneNumber);
setResult(RESUTL_CALL_PHONE,it);
finish();
}
}再来判断内容是否为电话号码 String phoneNumber="";
if(isPhoneNumber(content)){
phoneNumber = content;
}
// no message forward when in chat room
startActivityForResult((new Intent(getActivity(), ContextMenuActivity.class)).putExtra("message",message)
//if message's context is a phone number ,make it can be call it.
.putExtra("ischatroom", chatType == EaseConstant.CHATTYPE_CHATROOM).putExtra("phone_number",phoneNumber),
REQUEST_CODE_CONTEXT_MENU);onActivityResult部分 public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CONTEXT_MENU) {
//for Context MenuActivity Result
switch (resultCode) {
case ContextMenuActivity.RESULT_CODE_COPY: // copy
clipboard.setPrimaryClip(ClipData.newPlainText(null,
((EMTextMessageBody) contextMenuMessage.getBody()).getMessage()));
break;
case ContextMenuActivity.RESULT_CODE_DELETE: // delete
conversation.removeMessage(contextMenuMessage.getMsgId());
messageList.refresh();
break;


// case ContextMenuActivity.RESULT_CODE_FORWARD: // forward
// Intent intent = new Intent(getActivity(), ForwardMessageActivity.class);
// intent.putExtra("forward_msg_id", contextMenuMessage.getMsgId());
// startActivity(intent);
//
// break;

case ContextMenuActivity.RESUTL_CALL_PHONE:
Intent intent = new Intent(Intent.ACTION_DIAL);
Uri callData = Uri.parse("tel:" +data.getStringExtra("phone_number"));
intent.setData(callData);
startActivity(intent);
break;

default:
break;
}
}记住先提取字符串中的数字,再去匹配正则。









STM集成

在本质上是相同的。不同的是一个是用户端,一个是经纪人端

标注下需要注意的几个地方
头像和昵称的扩展互通,是SeeHouse和STM两边都需要做的。因为有一条对于从房源详情进入时带入的房源详情类型的聊天条目,经纪人可以点击查看该房源在STM中的详情。是在STM中单独实现的。SeeHouse负责带入,STM负责点击跳转。
对于从房源详情进入时带入的房源详情类型的聊天条目,经纪人可以点击查看该房源在STM中的详情。

创建图文chatrow并设置对应点击事件代码。

集成至目标App
不需要的代码,我们只做注释,不删除,防止后面增加了,需要了。避免一系列麻烦。
​剔除红包库​
在ChatUIDemo3.0的build.gradle中注释编译红包依赖库。

各种编译,遇到报错就删除相关代码
剔除不需要的代码

注意EaseUI下有个SimpleDemo




目标App集成与调试

因为是公司的商业项目,这里就不贴出来了。接着完成需调试才能完成的功能点

总结
好了,至此,我们开发详案写完了,代码也写完了。因为本文写的时候UI还未出,所以后面就是根据UI改改的调整调整界面的小事情了。

有任何问题或者其他事宜请联系我: 5108168@qq.com,欢迎指正和勘误。 查看全部
环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面

环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 前言

   手头工作上,正好需要在已有的两个App上集成IM功能。且迭代流程中是有开发详案这一项的。就分享给大家,边写开发详案边写代码。好吧,废话不多说,我们一起来学习如何集成和改造这款简单易用而又非常强大的环信SDK。
 具体步骤
迭代点

需要做的功能点及工作

1.集成环信

2.围绕UE和UI进行编码
  • 房源详情增加咨询按钮,点击进入咨询对话框,并且将房源信息带入对话框。
  • 消息中心

  1. 主界面TABBAR点击消息进入该界面
  2. 包含系统消息入库和咨询用户列表
  3. 从TABBAR点击“消息”图标进入本页面后,可以在本页面进入”系统消息“,并且将咨询过的用户会话显示在本页,长按任意一条会话,提示删除当前会话确定。
  4. 列表排序:“系统通知“仍然在最上面的位置,不受排序影响
  5. 咨询排序:按最后聊天时间倒序排列,咨询列表默认显示20条,多了拖动加载分页数据。
  6. 无咨询用户时,只显示”系统通知“入口即可。
  7. 咨询列表:长按可删除当前聊天对象,需要有确认对话框(只删除会话,不删除聊天记录)

 
  • 根据UE和UI改造聊天窗口(EaseUI库)

注意以下几点
  1. 从房源详情页进入时,就返回房源详情页,从消息中心进入时,就返回消息中心。
  2. 显示当前咨询人的经纪人姓名,并显示当前咨询的对象的在线状态(在线/离线)
  3. 标题头中的电话按钮可以直接拨打电话
  4. 对于从房源详情进入时带入的房源详情类型的聊天条目,经纪人可以点击查看该房源在STM中的详情。
  5. 显示经纪人照片上传的照片,如果经纪人没有上传照片,就显示一个经纪人的占位图(要区别于用户的占位图)
  6. 当前用户头像默认显示当前用户的头像,如果没有头像,就显示一个默认的占位图
  7. 聊天内容上长按可复制
  8. 发送的是手机号码时可以直接打电话。

思路
先做加法,再做减法

我们来按照原有代码改造和设计环信SDK部分相关代码改造,两个部分来做工作。将具体的功能点拆分并给出实现。

我们在Demo上修改,修改完成后剔除无关代码抽取成独立的我们需要的相关代码。整个工作也就结束了。

通过之前的代码阅读,我们知道整个Demo是一个相对完整的App,而我们实际工作中集成个im基本出不了这个范围。

就好比这次迭代也是。

因为实际整个涉及的只有会话列表和聊天界面,我们主要关注ConversationListFragmentChatActivity就行了。
实现
SeeHouse相关改造

原有代码改造
房源详情增加咨询按钮,点击进入咨询对话框,并且将房源信息带入对话框。
主界面TABBAR点击消息进入该界面

涉及环信SDK部分相关代码改造
包含系统消息入库和咨询用户列表


同列表,不同type类型区分,并置顶系统消息
 从TABBAR点击“消息”图标进入本页面后,可以在本页面进入”系统消息“,并且将咨询过的用户会话显示在本页,长按任意一条会话,提示删除当前会话确定。
直接贴过去,Demo已经实现。
 
列表排序:“系统通知“仍然在最上面的位置,不受排序影响根据Type来判断类型,并排序置顶。
 
咨询排序:按最后聊天时间倒序排列,咨询列表默认显示20条,多了拖动加载分页数据。sort算法改一下,看下本身是否带分页。
 
无咨询用户时,只显示”系统通知“入口即可。
无需实现。
 
咨询列表:长按可删除当前聊天对象,需要有确认对话框(只删除会话,不删除聊天记录)
001.jpg

环信的哥哥们已经帮我们实现了。但是根据要求呢,我没只需要删除会话,所以我们把第二项注释掉。
002.jpg

我们把对应处的判断代码和对应的menu文件em_delete_message中的标签给注释掉。看效果。
003.jpg

从房源详情页进入时,就返回房源详情页,从消息中心进入时,就返回消息中心。​
直接finish();
显示当前咨询人的经纪人姓名,并显示当前咨询的对象的在线状态(在线/离线)官方的EaseUi是这么说的
004.png

我们来找下EaseTitleBar
004.jpg

我们来看下他的布局
 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="@dimen/height_top_bar"
android:background="@color/top_bar_normal_bg"
android:gravity="center_vertical" >

<RelativeLayout
android:id="@+id/left_layout"
android:layout_width="50dip"
android:layout_height="match_parent"
android:background="@drawable/ease_common_tab_bg"
android:clickable="true" >

<ImageView
android:id="@+id/left_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:scaleType="centerInside" />
</RelativeLayout>

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#ffffff"
android:textSize="20sp" />

<RelativeLayout
android:id="@+id/right_layout"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:background="@drawable/ease_common_tab_bg" >

<ImageView
android:id="@+id/right_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:scaleType="centerInside" />
</RelativeLayout>

</RelativeLayout>
其实有title和rightview的。
005.jpg

我们来对title加入一个是否在线的状态
1.获取token
MacBook:~ mli$ curl -X POST "https://a1.easemob.com/1177170 ... ot%3B -d '{"grant_type":"client_credentials","client_id":"YXA6vcNInEeatzGVyK0tA","client_secret":"YXA6YACo7qumFfgYdWher3D3Cs"}'
{"access_token":"YWMtOT73nvcIEeaPCCuTQsCAAAVuOB_MQchxsIsxVJFXsW6lZ8f2l__xn8","expires_in":5168429,"application":"bd09c370-d227-11e6-adcc-65700322b4b4"}
2.拿token获取用户状态
MacBook:~ mli$ curl -X GET -i -H "Authorization: Bearer YWMtOT73nvcIEeaPCCuTQsC6kwAAAVuOB_MQchxsIsxybVJFXsW6lZ8f2l__xn8" "https://a1.easemob.com/1177170 ... ot%3B

HTTP/1.1 200 OK

Server: Tengine/2.0.3

Date: Mon, 20 Feb 2017 05:24:00 GMT

Content-Type: application/json;charset=UTF-8

Transfer-Encoding: chunked

Connection: keep-alive

Access-Control-Allow-Origin: *

Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sun, 19-Feb-2017 05:24:00 GMT

{

"action" : "get",

"uri" : "http://a1.easemob.com/11771701 ... ot%3B,

"entities" : [ ],

"data" : {

"2" : "offline"

},

"timestamp" : 1487568240699,

"duration" : 25,

"count" : 0

}MacBook:~ mli$ curl -X GET -i -H "Authorization: Bearer YWMtOT73nvcIEeaPCCuCkwAAAVuOB_MQchxsIJFXsW6lZ8f2l__xn8" "https://a1.easemob.com/1177170 ... ot%3B

HTTP/1.1 200 OK

Server: Tengine/2.0.3

Date: Mon, 20 Feb 2017 05:24:08 GMT

Content-Type: application/json;charset=UTF-8

Transfer-Encoding: chunked

Connection: keep-alive

Access-Control-Allow-Origin: *

Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sun, 19-Feb-2017 05:24:08 GMT

{

"action" : "get",

"uri" : "http://a1.easemob.com/11771701 ... ot%3B,

"entities" : [ ],

"data" : {

"1" : "online"

},

"timestamp" : 1487568248135,

"duration" : 14,

"count" : 0

MacBook:~ mli$
我们可以看到2是离线,1是在线的。

注意一点

006.jpg

所以昵称是在咱自己的体系的。可以从现有的App里提取,如果有的话。

我们知道从列表ConversationListFragment->ChatActivity->ChatFragment

那么如何接受和发送自己与他人的头像和昵称呢?

我们来玩这个ChatFragment
007.jpg

在OnSetMessageAttributes中,设置我们要发送时的消息扩展属性。

那么接收怎么办呢,我们来看下DemoHelper中的getUserInfo()方法。
008.jpg

无聊的用鄙人蹩脚的英文写了一把注释。英文若是写的不对就不对吧。
标题头中的电话按钮可以直接拨打电话
修改删除按钮为打电话,并改动相关代码
显示经纪人照片上传的照片,如果经纪人没有上传照片,就显示一个经纪人的占位图(要区别于用户的占位图)
修改原demo
当前用户头像默认显示当前用户的头像,如果没有头像,就显示一个默认的占位图
修改原demo。
聊天内容上长按可复制
009.jpg

自带了,后面我们可能需要去掉转发。

发送的是手机号码时可以直接打电话。

我们再长按后判断其是否为电话号码,如果是添加一项拨打电话。

引用关系是这样的

ChatFragment->ContextMenuActivity->em_context_menu_for_location.xml

最后调回ChatFragment的onActivityResult

我们来改em_context_menu_for_location.xml
 
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:gravity="center_horizontal"
android:orientation="vertical" >

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="copy"
android:padding="10dp"
android:text="@string/copy_message"
android:textColor="@android:color/black"
android:textSize="20sp" />

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="delete"
android:padding="10dp"
android:text="@string/delete_message"
android:textColor="@android:color/black"
android:textSize="20sp" />
<!-- <View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray" />

<TextView
android:id="@+id/forward"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="forward"
android:padding="10dp"
android:text="@string/forward"
android:textColor="@android:color/black"
android:textSize="20sp" />-->
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@android:color/darker_gray" />
<TextView
android:id="@+id/call_phone"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/em_context_menu_item_bg"
android:clickable="true"
android:gravity="center_vertical"
android:onClick="call"
android:padding="10dp"
android:text="@string/call_phone"
android:textColor="@android:color/black"
android:textSize="20sp" />
</LinearLayout>
再来改ContextMenuActivity
/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.R;

public class ContextMenuActivity extends BaseActivity {
public static final int RESULT_CODE_COPY = 1;
public static final int RESULT_CODE_DELETE = 2;
public static final int RESULT_CODE_FORWARD = 3;
public static final int RESUTL_CALL_PHONE = 4;
String phoneNumber;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EMMessage message = getIntent().getParcelableExtra("message");
boolean isChatroom = getIntent().getBooleanExtra("ischatroom", false);
phoneNumber = getIntent().getStringExtra("phone_number");

int type = message.getType().ordinal();
if (type == EMMessage.Type.TXT.ordinal()) {
if(message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)
|| message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false)
//red packet code : 屏蔽红包消息、转账消息的转发功能
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)){
//end of red packet code
setContentView(R.layout.em_context_menu_for_location);
}else if(message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_BIG_EXPRESSION, false)){
setContentView(R.layout.em_context_menu_for_image);
}else{
//for text content
setContentView(R.layout.em_context_menu_for_text);
//for call phone number
TextView callPhone = (TextView) findViewById(R.id.call_phone);
if(!TextUtils.isEmpty(phoneNumber)){
callPhone.setVisibility(View.VISIBLE);
callPhone.setText("拨打电话:" + phoneNumber);
}else{
callPhone.setVisibility(View.GONE);
}
}
} else if (type == EMMessage.Type.LOCATION.ordinal()) {
setContentView(R.layout.em_context_menu_for_location);
} else if (type == EMMessage.Type.IMAGE.ordinal()) {
setContentView(R.layout.em_context_menu_for_image);
} else if (type == EMMessage.Type.VOICE.ordinal()) {
setContentView(R.layout.em_context_menu_for_voice);
} else if (type == EMMessage.Type.VIDEO.ordinal()) {
setContentView(R.layout.em_context_menu_for_video);
} else if (type == EMMessage.Type.FILE.ordinal()) {
setContentView(R.layout.em_context_menu_for_location);
}
if (isChatroom
//red packet code : 屏蔽红包消息、转账消息的撤回功能
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)
|| message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
//end of red packet code
View v = (View) findViewById(R.id.forward);
if (v != null) {
v.setVisibility(View.GONE);
}
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
finish();
return true;
}

public void copy(View view){
setResult(RESULT_CODE_COPY);
finish();
}
public void delete(View view){
setResult(RESULT_CODE_DELETE);
finish();
}
public void forward(View view){
setResult(RESULT_CODE_FORWARD);
finish();
}

public void call(View view) {
Intent it = new Intent();
it.putExtra("phone_number",phoneNumber);
setResult(RESUTL_CALL_PHONE,it);
finish();
}
}
再来判断内容是否为电话号码
  String phoneNumber="";
if(isPhoneNumber(content)){
phoneNumber = content;
}
// no message forward when in chat room
startActivityForResult((new Intent(getActivity(), ContextMenuActivity.class)).putExtra("message",message)
//if message's context is a phone number ,make it can be call it.
.putExtra("ischatroom", chatType == EaseConstant.CHATTYPE_CHATROOM).putExtra("phone_number",phoneNumber),
REQUEST_CODE_CONTEXT_MENU);
onActivityResult部分
 public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CONTEXT_MENU) {
//for Context MenuActivity Result
switch (resultCode) {
case ContextMenuActivity.RESULT_CODE_COPY: // copy
clipboard.setPrimaryClip(ClipData.newPlainText(null,
((EMTextMessageBody) contextMenuMessage.getBody()).getMessage()));
break;
case ContextMenuActivity.RESULT_CODE_DELETE: // delete
conversation.removeMessage(contextMenuMessage.getMsgId());
messageList.refresh();
break;


// case ContextMenuActivity.RESULT_CODE_FORWARD: // forward
// Intent intent = new Intent(getActivity(), ForwardMessageActivity.class);
// intent.putExtra("forward_msg_id", contextMenuMessage.getMsgId());
// startActivity(intent);
//
// break;

case ContextMenuActivity.RESUTL_CALL_PHONE:
Intent intent = new Intent(Intent.ACTION_DIAL);
Uri callData = Uri.parse("tel:" +data.getStringExtra("phone_number"));
intent.setData(callData);
startActivity(intent);
break;

default:
break;
}
}
记住先提取字符串中的数字,再去匹配正则。
010.jpg


011.jpg

STM集成

在本质上是相同的。不同的是一个是用户端,一个是经纪人端

标注下需要注意的几个地方
  • 头像和昵称的扩展互通,是SeeHouse和STM两边都需要做的。
  • 因为有一条对于从房源详情进入时带入的房源详情类型的聊天条目,经纪人可以点击查看该房源在STM中的详情。是在STM中单独实现的。SeeHouse负责带入,STM负责点击跳转。

对于从房源详情进入时带入的房源详情类型的聊天条目,经纪人可以点击查看该房源在STM中的详情。

创建图文chatrow并设置对应点击事件代码。

集成至目标App
不需要的代码,我们只做注释,不删除,防止后面增加了,需要了。避免一系列麻烦。
​剔除红包库​
在ChatUIDemo3.0的build.gradle中注释编译红包依赖库。

各种编译,遇到报错就删除相关代码
剔除不需要的代码

注意EaseUI下有个SimpleDemo
012.jpg

目标App集成与调试

因为是公司的商业项目,这里就不贴出来了。接着完成需调试才能完成的功能点

总结
好了,至此,我们开发详案写完了,代码也写完了。因为本文写的时候UI还未出,所以后面就是根据UI改改的调整调整界面的小事情了。

有任何问题或者其他事宜请联系我: 5108168@qq.com,欢迎指正和勘误。
0
评论

环信官方Demo源码分析及SDK简单应用-EaseUI Android Demo源码分析 集成笔记

随缘 发表了文章 • 1077 次浏览 • 2017-02-21 16:19 • 来自相关话题

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现

EaseUI

实际工作过程中,我们是用不了太多东西的,如果只是集成个im最多用到的就是聊天列表和聊天页面。

我们来看重头戏EaseUI这个库。

官方文档

其实官方的WiKi已经介绍的特别详细了。官方EaseUI文档
我们来看Demo
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);ChatActivity

我们来看看ChatActivitypackage com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.runtimepermissions.PermissionsManager;
import com.hyphenate.easeui.ui.EaseChatFragment;
import com.hyphenate.util.EasyUtils;

/**
* chat activity,EaseChatFragment was used {@link #EaseChatFragment}
*
*/
public class ChatActivity extends BaseActivity{
public static ChatActivity activityInstance;
private EaseChatFragment chatFragment;
String toChatUsername;

@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
setContentView(R.layout.em_activity_chat);
activityInstance = this;
//get user id or group id
toChatUsername = getIntent().getExtras().getString("userId");
//use EaseChatFratFragment
chatFragment = new ChatFragment();
//pass parameters to chat fragment
chatFragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().add(R.id.container, chatFragment).commit();

}

@Override
protected void onDestroy() {
super.onDestroy();
activityInstance = null;
}

@Override
protected void onNewIntent(Intent intent) {
// make sure only one chat activity is opened
String username = intent.getStringExtra("userId");
if (toChatUsername.equals(username))
super.onNewIntent(intent);
else {
finish();
startActivity(intent);
}

}

@Override
public void onBackPressed() {
chatFragment.onBackPressed();
if (EasyUtils.isSingleActivity(this)) {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
}

public String getToChatUsername(){
return toChatUsername;
}

@Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions,
@NonNull int grantResults) {
PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults);
}
}官方文档是这么说的

封装EaseChatFragment的ChatFragment

那么Demo中是做了一层封装的。package com.hyphenate.chatuidemo.ui;

import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Toast;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.easemob.redpacketui.utils.RPRedPacketUtil;
import com.easemob.redpacketui.utils.RedPacketUtil;
import com.easemob.redpacketui.widget.ChatRowRandomPacket;
import com.easemob.redpacketui.widget.ChatRowRedPacket;
import com.easemob.redpacketui.widget.ChatRowRedPacketAck;
import com.easemob.redpacketui.widget.ChatRowTransfer;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMCmdMessageBody;
import com.hyphenate.chat.EMGroup;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chat.EMTextMessageBody;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.DemoHelper;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.domain.EmojiconExampleGroupData;
import com.hyphenate.chatuidemo.domain.RobotUser;
import com.hyphenate.chatuidemo.widget.ChatRowVoiceCall;
import com.hyphenate.easeui.EaseConstant;
import com.hyphenate.easeui.ui.EaseChatFragment;
import com.hyphenate.easeui.ui.EaseChatFragment.EaseChatFragmentHelper;
import com.hyphenate.easeui.widget.chatrow.EaseChatRow;
import com.hyphenate.easeui.widget.chatrow.EaseCustomChatRowProvider;
import com.hyphenate.easeui.widget.emojicon.EaseEmojiconMenu;
import com.hyphenate.util.EasyUtils;
import com.hyphenate.util.PathUtil;

import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;

public class ChatFragment extends EaseChatFragment implements EaseChatFragmentHelper{

// constant start from 11 to avoid conflict with constant in base class
private static final int ITEM_VIDEO = 11;
private static final int ITEM_FILE = 12;
private static final int ITEM_VOICE_CALL = 13;
private static final int ITEM_VIDEO_CALL = 14;

private static final int REQUEST_CODE_SELECT_VIDEO = 11;
private static final int REQUEST_CODE_SELECT_FILE = 12;
private static final int REQUEST_CODE_GROUP_DETAIL = 13;
private static final int REQUEST_CODE_CONTEXT_MENU = 14;
private static final int REQUEST_CODE_SELECT_AT_USER = 15;


private static final int MESSAGE_TYPE_SENT_VOICE_CALL = 1;
private static final int MESSAGE_TYPE_RECV_VOICE_CALL = 2;
private static final int MESSAGE_TYPE_SENT_VIDEO_CALL = 3;
private static final int MESSAGE_TYPE_RECV_VIDEO_CALL = 4;

//red packet code : 红包功能使用的常量
private static final int MESSAGE_TYPE_RECV_RED_PACKET = 5;
private static final int MESSAGE_TYPE_SEND_RED_PACKET = 6;
private static final int MESSAGE_TYPE_SEND_RED_PACKET_ACK = 7;
private static final int MESSAGE_TYPE_RECV_RED_PACKET_ACK = 8;
private static final int MESSAGE_TYPE_RECV_TRANSFER_PACKET = 9;
private static final int MESSAGE_TYPE_SEND_TRANSFER_PACKET = 10;
private static final int MESSAGE_TYPE_RECV_RANDOM = 11;
private static final int MESSAGE_TYPE_SEND_RANDOM = 12;
private static final int REQUEST_CODE_SEND_RED_PACKET = 16;
private static final int ITEM_RED_PACKET = 16;
private static final int REQUEST_CODE_SEND_TRANSFER_PACKET = 17;
private static final int ITEM_TRANSFER_PACKET = 17;
//end of red packet code

/**
* if it is chatBot
*/
private boolean isRobot;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}

@Override
protected void setUpView() {
setChatFragmentListener(this);
if (chatType == Constant.CHATTYPE_SINGLE) {
Map<String,RobotUser> robotMap = DemoHelper.getInstance().getRobotList();
if(robotMap!=null && robotMap.containsKey(toChatUsername)){
isRobot = true;
}
}
super.setUpView();
// set click listener
titleBar.setLeftLayoutClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
if (EasyUtils.isSingleActivity(getActivity())) {
Intent intent = new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}
onBackPressed();
}
});
((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
if(chatType == EaseConstant.CHATTYPE_GROUP){
inputMenu.getPrimaryMenu().getEditText().addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if(count == 1 && "@".equals(String.valueOf(s.charAt(start)))){
startActivityForResult(new Intent(getActivity(), PickAtUserActivity.class).
putExtra("groupId", toChatUsername), REQUEST_CODE_SELECT_AT_USER);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}
@Override
public void afterTextChanged(Editable s) {

}
});
}
}

@Override
protected void registerExtendMenuItem() {
//use the menu in base class
super.registerExtendMenuItem();
//extend menu items
inputMenu.registerExtendMenuItem(R.string.attach_video, R.drawable.em_chat_video_selector, ITEM_VIDEO, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_file, R.drawable.em_chat_file_selector, ITEM_FILE, extendMenuItemClickListener);
if(chatType == Constant.CHATTYPE_SINGLE){
inputMenu.registerExtendMenuItem(R.string.attach_voice_call, R.drawable.em_chat_voice_call_selector, ITEM_VOICE_CALL, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_video_call, R.drawable.em_chat_video_call_selector, ITEM_VIDEO_CALL, extendMenuItemClickListener);
}
//聊天室暂时不支持红包功能
//red packet code : 注册红包菜单选项
if (chatType != Constant.CHATTYPE_CHATROOM) {
inputMenu.registerExtendMenuItem(R.string.attach_red_packet, R.drawable.em_chat_red_packet_selector, ITEM_RED_PACKET, extendMenuItemClickListener);
}
//red packet code : 注册转账菜单选项
if (chatType == Constant.CHATTYPE_SINGLE) {
inputMenu.registerExtendMenuItem(R.string.attach_transfer_money, R.drawable.em_chat_transfer_selector, ITEM_TRANSFER_PACKET, extendMenuItemClickListener);
}
//end of red packet code
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CONTEXT_MENU) {
switch (resultCode) {
case ContextMenuActivity.RESULT_CODE_COPY: // copy
clipboard.setPrimaryClip(ClipData.newPlainText(null,
((EMTextMessageBody) contextMenuMessage.getBody()).getMessage()));
break;
case ContextMenuActivity.RESULT_CODE_DELETE: // delete
conversation.removeMessage(contextMenuMessage.getMsgId());
messageList.refresh();
break;

case ContextMenuActivity.RESULT_CODE_FORWARD: // forward
Intent intent = new Intent(getActivity(), ForwardMessageActivity.class);
intent.putExtra("forward_msg_id", contextMenuMessage.getMsgId());
startActivity(intent);

break;

default:
break;
}
}
if(resultCode == Activity.RESULT_OK){
switch (requestCode) {
case REQUEST_CODE_SELECT_VIDEO: //send the video
if (data != null) {
int duration = data.getIntExtra("dur", 0);
String videoPath = data.getStringExtra("path");
File file = new File(PathUtil.getInstance().getImagePath(), "thvideo" + System.currentTimeMillis());
try {
FileOutputStream fos = new FileOutputStream(file);
Bitmap ThumbBitmap = ThumbnailUtils.createVideoThumbnail(videoPath, 3);
ThumbBitmap.compress(CompressFormat.JPEG, 100, fos);
fos.close();
sendVideoMessage(videoPath, file.getAbsolutePath(), duration);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
case REQUEST_CODE_SELECT_FILE: //send the file
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
sendFileByUri(uri);
}
}
break;
case REQUEST_CODE_SELECT_AT_USER:
if(data != null){
String username = data.getStringExtra("username");
inputAtUsername(username, false);
}
break;
//red packet code : 发送红包消息到聊天界面
case REQUEST_CODE_SEND_RED_PACKET:
if (data != null){
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}
break;
case REQUEST_CODE_SEND_TRANSFER_PACKET://发送转账消息
if (data != null) {
sendMessage(RedPacketUtil.createTRMessage(getActivity(), data, toChatUsername));
}
break;
//end of red packet code
default:
break;
}
}

}

@Override
public void onSetMessageAttributes(EMMessage message) {
if(isRobot){
//set message extension
message.setAttribute("em_robot_message", isRobot);
}
}

@Override
public EaseCustomChatRowProvider onSetCustomChatRowProvider() {
return new CustomChatRowProvider();
}


@Override
public void onEnterToChatDetails() {
if (chatType == Constant.CHATTYPE_GROUP) {
EMGroup group = EMClient.getInstance().groupManager().getGroup(toChatUsername);
if (group == null) {
Toast.makeText(getActivity(), R.string.gorup_not_found, Toast.LENGTH_SHORT).show();
return;
}
startActivityForResult(
(new Intent(getActivity(), GroupDetailsActivity.class).putExtra("groupId", toChatUsername)),
REQUEST_CODE_GROUP_DETAIL);
}else if(chatType == Constant.CHATTYPE_CHATROOM){
startActivityForResult(new Intent(getActivity(), ChatRoomDetailsActivity.class).putExtra("roomId", toChatUsername), REQUEST_CODE_GROUP_DETAIL);
}
}

@Override
public void onAvatarClick(String username) {
//handling when user click avatar
Intent intent = new Intent(getActivity(), UserProfileActivity.class);
intent.putExtra("username", username);
startActivity(intent);
}

@Override
public void onAvatarLongClick(String username) {
inputAtUsername(username);
}


@Override
public boolean onMessageBubbleClick(EMMessage message) {
//消息框点击事件,demo这里不做覆盖,如需覆盖,return true
//red packet code : 拆红包页面
if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)){
if (RedPacketUtil.isRandomRedPacket(message)){
RedPacketUtil.openRandomPacket(getActivity(),message);
} else {
RedPacketUtil.openRedPacket(getActivity(), chatType, message, toChatUsername, messageList);
}
return true;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
RedPacketUtil.openTransferPacket(getActivity(), message);
return true;
}
//end of red packet code
return false;
}
@Override
public void onCmdMessageReceived(List<EMMessage> messages) {
//red packet code : 处理红包回执透传消息
for (EMMessage message : messages) {
EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
String action = cmdMsgBody.action();//获取自定义action
if (action.equals(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION)){
RedPacketUtil.receiveRedPacketAckMessage(message);
messageList.refresh();
}
}
//end of red packet code
super.onCmdMessageReceived(messages);
}

@Override
public void onMessageBubbleLongClick(EMMessage message) {
// no message forward when in chat room
startActivityForResult((new Intent(getActivity(), ContextMenuActivity.class)).putExtra("message",message)
.putExtra("ischatroom", chatType == EaseConstant.CHATTYPE_CHATROOM),
REQUEST_CODE_CONTEXT_MENU);
}

@Override
public boolean onExtendMenuItemClick(int itemId, View view) {
switch (itemId) {
case ITEM_VIDEO:
Intent intent = new Intent(getActivity(), ImageGridActivity.class);
startActivityForResult(intent, REQUEST_CODE_SELECT_VIDEO);
break;
case ITEM_FILE: //file
selectFileFromLocal();
break;
case ITEM_VOICE_CALL:
startVoiceCall();
break;
case ITEM_VIDEO_CALL:
startVideoCall();
break;
//red packet code : 进入发红包页面
case ITEM_RED_PACKET:
if (chatType == Constant.CHATTYPE_SINGLE) {
//单聊红包修改进入红包的方法,可以在小额随机红包和普通单聊红包之间切换
RedPacketUtil.startRandomPacket(new RPRedPacketUtil.RPRandomCallback() {
@Override
public void onSendPacketSuccess(Intent data) {
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}

@Override
public void switchToNormalPacket() {
RedPacketUtil.startRedPacketActivityForResult(ChatFragment.this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
},getActivity(),toChatUsername);
} else {
RedPacketUtil.startRedPacketActivityForResult(this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
break;
case ITEM_TRANSFER_PACKET://进入转账页面
RedPacketUtil.startTransferActivityForResult(this, toChatUsername, REQUEST_CODE_SEND_TRANSFER_PACKET);
break;
//end of red packet code
default:
break;
}
//keep exist extend menu
return false;
}

/**
* select file
*/
protected void selectFileFromLocal() {
Intent intent = null;
if (Build.VERSION.SDK_INT < 19) { //api 19 and later, we can't use this way, demo just select from images
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);

} else {
intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
startActivityForResult(intent, REQUEST_CODE_SELECT_FILE);
}

/**
* make a voice call
*/
protected void startVoiceCall() {
if (!EMClient.getInstance().isConnected()) {
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
} else {
startActivity(new Intent(getActivity(), VoiceCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// voiceCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* make a video call
*/
protected void startVideoCall() {
if (!EMClient.getInstance().isConnected())
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
else {
startActivity(new Intent(getActivity(), VideoCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// videoCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* chat row provider
*
*/
private final class CustomChatRowProvider implements EaseCustomChatRowProvider {
@Override
public int getCustomChatRowTypeCount() {
//here the number is the message type in EMMessage::Type
//which is used to count the number of different chat row
return 12;
}

@Override
public int getCustomChatRowType(EMMessage message) {
if(message.getType() == EMMessage.Type.TXT){
//voice call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false)){
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VOICE_CALL : MESSAGE_TYPE_SENT_VOICE_CALL;
}else if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
//video call
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VIDEO_CALL : MESSAGE_TYPE_SENT_VIDEO_CALL;
}
//red packet code : 红包消息、红包回执消息以及转账消息的chatrow type
else if (RedPacketUtil.isRandomRedPacket(message)) {
//小额随机红包
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RANDOM : MESSAGE_TYPE_SEND_RANDOM;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {
//发送红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET : MESSAGE_TYPE_SEND_RED_PACKET;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
//领取红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET_ACK : MESSAGE_TYPE_SEND_RED_PACKET_ACK;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
//转账消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_TRANSFER_PACKET : MESSAGE_TYPE_SEND_TRANSFER_PACKET;
}
//end of red packet code
}
return 0;
}

@Override
public EaseChatRow getCustomChatRow(EMMessage message, int position, BaseAdapter adapter) {
if(message.getType() == EMMessage.Type.TXT){
// voice call or video call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false) ||
message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
return new ChatRowVoiceCall(getActivity(), message, position, adapter);
}
//red packet code : 红包消息、红包回执消息以及转账消息的chat row
else if (RedPacketUtil.isRandomRedPacket(message)) {//小额随机红包
return new ChatRowRandomPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {//红包消息
return new ChatRowRedPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {//红包回执消息
return new ChatRowRedPacketAck(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {//转账消息
return new ChatRowTransfer(getActivity(), message, position, adapter);
}
//end of red packet code
}
return null;
}

}

}判断是不是机器人及添加监听
setChatFragmentListener(this);
if (chatType == Constant.CHATTYPE_SINGLE) {
Map<String,RobotUser> robotMap = DemoHelper.getInstance().getRobotList();
if(robotMap!=null && robotMap.containsKey(toChatUsername)){
isRobot = true;
}
}点击标题返回及群聊@别人的功能​// set click listener
titleBar.setLeftLayoutClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
if (EasyUtils.isSingleActivity(getActivity())) {
Intent intent = new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}
onBackPressed();
}
});
((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
if(chatType == EaseConstant.CHATTYPE_GROUP){
inputMenu.getPrimaryMenu().getEditText().addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if(count == 1 && "@".equals(String.valueOf(s.charAt(start)))){
startActivityForResult(new Intent(getActivity(), PickAtUserActivity.class).
putExtra("groupId", toChatUsername), REQUEST_CODE_SELECT_AT_USER);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}
@Override
public void afterTextChanged(Editable s) {

}
});
}菜单的操作​
super.registerExtendMenuItem();
//extend menu items
inputMenu.registerExtendMenuItem(R.string.attach_video, R.drawable.em_chat_video_selector, ITEM_VIDEO, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_file, R.drawable.em_chat_file_selector, ITEM_FILE, extendMenuItemClickListener);
if(chatType == Constant.CHATTYPE_SINGLE){
inputMenu.registerExtendMenuItem(R.string.attach_voice_call, R.drawable.em_chat_voice_call_selector, ITEM_VOICE_CALL, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_video_call, R.drawable.em_chat_video_call_selector, ITEM_VIDEO_CALL, extendMenuItemClickListener);
}
//聊天室暂时不支持红包功能
//red packet code : 注册红包菜单选项
if (chatType != Constant.CHATTYPE_CHATROOM) {
inputMenu.registerExtendMenuItem(R.string.attach_red_packet, R.drawable.em_chat_red_packet_selector, ITEM_RED_PACKET, extendMenuItemClickListener);
}
//red packet code : 注册转账菜单选项
if (chatType == Constant.CHATTYPE_SINGLE) {
inputMenu.registerExtendMenuItem(R.string.attach_transfer_money, R.drawable.em_chat_transfer_selector, ITEM_TRANSFER_PACKET, extendMenuItemClickListener);
}
//end of red packet code一些功能操作​if (requestCode == REQUEST_CODE_CONTEXT_MENU) {
switch (resultCode) {
case ContextMenuActivity.RESULT_CODE_COPY: // copy
clipboard.setPrimaryClip(ClipData.newPlainText(null,
((EMTextMessageBody) contextMenuMessage.getBody()).getMessage()));
break;
case ContextMenuActivity.RESULT_CODE_DELETE: // delete
conversation.removeMessage(contextMenuMessage.getMsgId());
messageList.refresh();
break;

case ContextMenuActivity.RESULT_CODE_FORWARD: // forward
Intent intent = new Intent(getActivity(), ForwardMessageActivity.class);
intent.putExtra("forward_msg_id", contextMenuMessage.getMsgId());
startActivity(intent);

break;

default:
break;
}
}
if(resultCode == Activity.RESULT_OK){
switch (requestCode) {
case REQUEST_CODE_SELECT_VIDEO: //send the video
if (data != null) {
int duration = data.getIntExtra("dur", 0);
String videoPath = data.getStringExtra("path");
File file = new File(PathUtil.getInstance().getImagePath(), "thvideo" + System.currentTimeMillis());
try {
FileOutputStream fos = new FileOutputStream(file);
Bitmap ThumbBitmap = ThumbnailUtils.createVideoThumbnail(videoPath, 3);
ThumbBitmap.compress(CompressFormat.JPEG, 100, fos);
fos.close();
sendVideoMessage(videoPath, file.getAbsolutePath(), duration);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
case REQUEST_CODE_SELECT_FILE: //send the file
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
sendFileByUri(uri);
}
}
break;
case REQUEST_CODE_SELECT_AT_USER:
if(data != null){
String username = data.getStringExtra("username");
inputAtUsername(username, false);
}
break;
//red packet code : 发送红包消息到聊天界面
case REQUEST_CODE_SEND_RED_PACKET:
if (data != null){
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}
break;
case REQUEST_CODE_SEND_TRANSFER_PACKET://发送转账消息
if (data != null) {
sendMessage(RedPacketUtil.createTRMessage(getActivity(), data, toChatUsername));
}
break;
//end of red packet code
default:
break;
}
}进入聊天详情​
@Override
public void onEnterToChatDetails() {
if (chatType == Constant.CHATTYPE_GROUP) {
EMGroup group = EMClient.getInstance().groupManager().getGroup(toChatUsername);
if (group == null) {
Toast.makeText(getActivity(), R.string.gorup_not_found, Toast.LENGTH_SHORT).show();
return;
}
startActivityForResult(
(new Intent(getActivity(), GroupDetailsActivity.class).putExtra("groupId", toChatUsername)),
REQUEST_CODE_GROUP_DETAIL);
}else if(chatType == Constant.CHATTYPE_CHATROOM){
startActivityForResult(new Intent(getActivity(), ChatRoomDetailsActivity.class).putExtra("roomId", toChatUsername), REQUEST_CODE_GROUP_DETAIL);
}
}点击头像​@Override
public void onAvatarClick(String username) {
//handling when user click avatar
Intent intent = new Intent(getActivity(), UserProfileActivity.class);
intent.putExtra("username", username);
startActivity(intent);
}消息框点击事件、拆红包@Override
public boolean onMessageBubbleClick(EMMessage message) {
//消息框点击事件,demo这里不做覆盖,如需覆盖,return true
//red packet code : 拆红包页面
if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)){
if (RedPacketUtil.isRandomRedPacket(message)){
RedPacketUtil.openRandomPacket(getActivity(),message);
} else {
RedPacketUtil.openRedPacket(getActivity(), chatType, message, toChatUsername, messageList);
}
return true;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
RedPacketUtil.openTransferPacket(getActivity(), message);
return true;
}
//end of red packet code
return false;
}红包回执及消息框长按​@Override
public void onCmdMessageReceived(List<EMMessage> messages) {
//red packet code : 处理红包回执透传消息
for (EMMessage message : messages) {
EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
String action = cmdMsgBody.action();//获取自定义action
if (action.equals(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION)){
RedPacketUtil.receiveRedPacketAckMessage(message);
messageList.refresh();
}
}
//end of red packet code
super.onCmdMessageReceived(messages);
}

@Override
public void onMessageBubbleLongClick(EMMessage message) {
// no message forward when in chat room
startActivityForResult((new Intent(getActivity(), ContextMenuActivity.class)).putExtra("message",message)
.putExtra("ischatroom", chatType == EaseConstant.CHATTYPE_CHATROOM),
REQUEST_CODE_CONTEXT_MENU);
}扩展按钮​@Override
public boolean onExtendMenuItemClick(int itemId, View view) {
switch (itemId) {
case ITEM_VIDEO:
Intent intent = new Intent(getActivity(), ImageGridActivity.class);
startActivityForResult(intent, REQUEST_CODE_SELECT_VIDEO);
break;
case ITEM_FILE: //file
selectFileFromLocal();
break;
case ITEM_VOICE_CALL:
startVoiceCall();
break;
case ITEM_VIDEO_CALL:
startVideoCall();
break;
//red packet code : 进入发红包页面
case ITEM_RED_PACKET:
if (chatType == Constant.CHATTYPE_SINGLE) {
//单聊红包修改进入红包的方法,可以在小额随机红包和普通单聊红包之间切换
RedPacketUtil.startRandomPacket(new RPRedPacketUtil.RPRandomCallback() {
@Override
public void onSendPacketSuccess(Intent data) {
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}

@Override
public void switchToNormalPacket() {
RedPacketUtil.startRedPacketActivityForResult(ChatFragment.this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
},getActivity(),toChatUsername);
} else {
RedPacketUtil.startRedPacketActivityForResult(this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
break;
case ITEM_TRANSFER_PACKET://进入转账页面
RedPacketUtil.startTransferActivityForResult(this, toChatUsername, REQUEST_CODE_SEND_TRANSFER_PACKET);
break;
//end of red packet code
default:
break;
}
//keep exist extend menu
return false;
}本地文件选择、语音通话、视频通话、及自定义chatrow类型​
/**
* select file
*/
protected void selectFileFromLocal() {
Intent intent = null;
if (Build.VERSION.SDK_INT < 19) { //api 19 and later, we can't use this way, demo just select from images
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);

} else {
intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
startActivityForResult(intent, REQUEST_CODE_SELECT_FILE);
}

/**
* make a voice call
*/
protected void startVoiceCall() {
if (!EMClient.getInstance().isConnected()) {
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
} else {
startActivity(new Intent(getActivity(), VoiceCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// voiceCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* make a video call
*/
protected void startVideoCall() {
if (!EMClient.getInstance().isConnected())
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
else {
startActivity(new Intent(getActivity(), VideoCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// videoCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* chat row provider
*
*/
private final class CustomChatRowProvider implements EaseCustomChatRowProvider {
@Override
public int getCustomChatRowTypeCount() {
//here the number is the message type in EMMessage::Type
//which is used to count the number of different chat row
return 12;
}

@Override
public int getCustomChatRowType(EMMessage message) {
if(message.getType() == EMMessage.Type.TXT){
//voice call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false)){
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VOICE_CALL : MESSAGE_TYPE_SENT_VOICE_CALL;
}else if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
//video call
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VIDEO_CALL : MESSAGE_TYPE_SENT_VIDEO_CALL;
}
//red packet code : 红包消息、红包回执消息以及转账消息的chatrow type
else if (RedPacketUtil.isRandomRedPacket(message)) {
//小额随机红包
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RANDOM : MESSAGE_TYPE_SEND_RANDOM;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {
//发送红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET : MESSAGE_TYPE_SEND_RED_PACKET;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
//领取红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET_ACK : MESSAGE_TYPE_SEND_RED_PACKET_ACK;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
//转账消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_TRANSFER_PACKET : MESSAGE_TYPE_SEND_TRANSFER_PACKET;
}
//end of red packet code
}
return 0;
}

@Override
public EaseChatRow getCustomChatRow(EMMessage message, int position, BaseAdapter adapter) {
if(message.getType() == EMMessage.Type.TXT){
// voice call or video call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false) ||
message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
return new ChatRowVoiceCall(getActivity(), message, position, adapter);
}
//red packet code : 红包消息、红包回执消息以及转账消息的chat row
else if (RedPacketUtil.isRandomRedPacket(message)) {//小额随机红包
return new ChatRowRandomPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {//红包消息
return new ChatRowRedPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {//红包回执消息
return new ChatRowRedPacketAck(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {//转账消息
return new ChatRowTransfer(getActivity(), message, position, adapter);
}
//end of red packet code
}
return null;
}

}Redpacketlibrary

由于业务未涉及,暂不作分析。
 
总结及其他

其实正常集成,按照于海同学所说也就半天时间,这是因为的确环信的SDK使用起来比较方便。

通过大致的阅读代码,环信的Demo代码写的还是很不错的,功能齐全,注释完整。值得学习和研究。

写在最后

多学习,多积累,多输出。!
 
附:最近两天实际工作采用环信SDK的开发详案

环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现 查看全部
环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现

EaseUI

实际工作过程中,我们是用不了太多东西的,如果只是集成个im最多用到的就是聊天列表和聊天页面。

我们来看重头戏EaseUI这个库。

官方文档

其实官方的WiKi已经介绍的特别详细了。官方EaseUI文档
我们来看Demo
 
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
ChatActivity

我们来看看ChatActivity
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.runtimepermissions.PermissionsManager;
import com.hyphenate.easeui.ui.EaseChatFragment;
import com.hyphenate.util.EasyUtils;

/**
* chat activity,EaseChatFragment was used {@link #EaseChatFragment}
*
*/
public class ChatActivity extends BaseActivity{
public static ChatActivity activityInstance;
private EaseChatFragment chatFragment;
String toChatUsername;

@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
setContentView(R.layout.em_activity_chat);
activityInstance = this;
//get user id or group id
toChatUsername = getIntent().getExtras().getString("userId");
//use EaseChatFratFragment
chatFragment = new ChatFragment();
//pass parameters to chat fragment
chatFragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().add(R.id.container, chatFragment).commit();

}

@Override
protected void onDestroy() {
super.onDestroy();
activityInstance = null;
}

@Override
protected void onNewIntent(Intent intent) {
// make sure only one chat activity is opened
String username = intent.getStringExtra("userId");
if (toChatUsername.equals(username))
super.onNewIntent(intent);
else {
finish();
startActivity(intent);
}

}

@Override
public void onBackPressed() {
chatFragment.onBackPressed();
if (EasyUtils.isSingleActivity(this)) {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
}

public String getToChatUsername(){
return toChatUsername;
}

@Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions,
@NonNull int grantResults) {
PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults);
}
}
官方文档是这么说的

封装EaseChatFragment的ChatFragment

那么Demo中是做了一层封装的。
package com.hyphenate.chatuidemo.ui;

import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Toast;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.easemob.redpacketui.utils.RPRedPacketUtil;
import com.easemob.redpacketui.utils.RedPacketUtil;
import com.easemob.redpacketui.widget.ChatRowRandomPacket;
import com.easemob.redpacketui.widget.ChatRowRedPacket;
import com.easemob.redpacketui.widget.ChatRowRedPacketAck;
import com.easemob.redpacketui.widget.ChatRowTransfer;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMCmdMessageBody;
import com.hyphenate.chat.EMGroup;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chat.EMTextMessageBody;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.DemoHelper;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.domain.EmojiconExampleGroupData;
import com.hyphenate.chatuidemo.domain.RobotUser;
import com.hyphenate.chatuidemo.widget.ChatRowVoiceCall;
import com.hyphenate.easeui.EaseConstant;
import com.hyphenate.easeui.ui.EaseChatFragment;
import com.hyphenate.easeui.ui.EaseChatFragment.EaseChatFragmentHelper;
import com.hyphenate.easeui.widget.chatrow.EaseChatRow;
import com.hyphenate.easeui.widget.chatrow.EaseCustomChatRowProvider;
import com.hyphenate.easeui.widget.emojicon.EaseEmojiconMenu;
import com.hyphenate.util.EasyUtils;
import com.hyphenate.util.PathUtil;

import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;

public class ChatFragment extends EaseChatFragment implements EaseChatFragmentHelper{

// constant start from 11 to avoid conflict with constant in base class
private static final int ITEM_VIDEO = 11;
private static final int ITEM_FILE = 12;
private static final int ITEM_VOICE_CALL = 13;
private static final int ITEM_VIDEO_CALL = 14;

private static final int REQUEST_CODE_SELECT_VIDEO = 11;
private static final int REQUEST_CODE_SELECT_FILE = 12;
private static final int REQUEST_CODE_GROUP_DETAIL = 13;
private static final int REQUEST_CODE_CONTEXT_MENU = 14;
private static final int REQUEST_CODE_SELECT_AT_USER = 15;


private static final int MESSAGE_TYPE_SENT_VOICE_CALL = 1;
private static final int MESSAGE_TYPE_RECV_VOICE_CALL = 2;
private static final int MESSAGE_TYPE_SENT_VIDEO_CALL = 3;
private static final int MESSAGE_TYPE_RECV_VIDEO_CALL = 4;

//red packet code : 红包功能使用的常量
private static final int MESSAGE_TYPE_RECV_RED_PACKET = 5;
private static final int MESSAGE_TYPE_SEND_RED_PACKET = 6;
private static final int MESSAGE_TYPE_SEND_RED_PACKET_ACK = 7;
private static final int MESSAGE_TYPE_RECV_RED_PACKET_ACK = 8;
private static final int MESSAGE_TYPE_RECV_TRANSFER_PACKET = 9;
private static final int MESSAGE_TYPE_SEND_TRANSFER_PACKET = 10;
private static final int MESSAGE_TYPE_RECV_RANDOM = 11;
private static final int MESSAGE_TYPE_SEND_RANDOM = 12;
private static final int REQUEST_CODE_SEND_RED_PACKET = 16;
private static final int ITEM_RED_PACKET = 16;
private static final int REQUEST_CODE_SEND_TRANSFER_PACKET = 17;
private static final int ITEM_TRANSFER_PACKET = 17;
//end of red packet code

/**
* if it is chatBot
*/
private boolean isRobot;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}

@Override
protected void setUpView() {
setChatFragmentListener(this);
if (chatType == Constant.CHATTYPE_SINGLE) {
Map<String,RobotUser> robotMap = DemoHelper.getInstance().getRobotList();
if(robotMap!=null && robotMap.containsKey(toChatUsername)){
isRobot = true;
}
}
super.setUpView();
// set click listener
titleBar.setLeftLayoutClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
if (EasyUtils.isSingleActivity(getActivity())) {
Intent intent = new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}
onBackPressed();
}
});
((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
if(chatType == EaseConstant.CHATTYPE_GROUP){
inputMenu.getPrimaryMenu().getEditText().addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if(count == 1 && "@".equals(String.valueOf(s.charAt(start)))){
startActivityForResult(new Intent(getActivity(), PickAtUserActivity.class).
putExtra("groupId", toChatUsername), REQUEST_CODE_SELECT_AT_USER);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}
@Override
public void afterTextChanged(Editable s) {

}
});
}
}

@Override
protected void registerExtendMenuItem() {
//use the menu in base class
super.registerExtendMenuItem();
//extend menu items
inputMenu.registerExtendMenuItem(R.string.attach_video, R.drawable.em_chat_video_selector, ITEM_VIDEO, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_file, R.drawable.em_chat_file_selector, ITEM_FILE, extendMenuItemClickListener);
if(chatType == Constant.CHATTYPE_SINGLE){
inputMenu.registerExtendMenuItem(R.string.attach_voice_call, R.drawable.em_chat_voice_call_selector, ITEM_VOICE_CALL, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_video_call, R.drawable.em_chat_video_call_selector, ITEM_VIDEO_CALL, extendMenuItemClickListener);
}
//聊天室暂时不支持红包功能
//red packet code : 注册红包菜单选项
if (chatType != Constant.CHATTYPE_CHATROOM) {
inputMenu.registerExtendMenuItem(R.string.attach_red_packet, R.drawable.em_chat_red_packet_selector, ITEM_RED_PACKET, extendMenuItemClickListener);
}
//red packet code : 注册转账菜单选项
if (chatType == Constant.CHATTYPE_SINGLE) {
inputMenu.registerExtendMenuItem(R.string.attach_transfer_money, R.drawable.em_chat_transfer_selector, ITEM_TRANSFER_PACKET, extendMenuItemClickListener);
}
//end of red packet code
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CONTEXT_MENU) {
switch (resultCode) {
case ContextMenuActivity.RESULT_CODE_COPY: // copy
clipboard.setPrimaryClip(ClipData.newPlainText(null,
((EMTextMessageBody) contextMenuMessage.getBody()).getMessage()));
break;
case ContextMenuActivity.RESULT_CODE_DELETE: // delete
conversation.removeMessage(contextMenuMessage.getMsgId());
messageList.refresh();
break;

case ContextMenuActivity.RESULT_CODE_FORWARD: // forward
Intent intent = new Intent(getActivity(), ForwardMessageActivity.class);
intent.putExtra("forward_msg_id", contextMenuMessage.getMsgId());
startActivity(intent);

break;

default:
break;
}
}
if(resultCode == Activity.RESULT_OK){
switch (requestCode) {
case REQUEST_CODE_SELECT_VIDEO: //send the video
if (data != null) {
int duration = data.getIntExtra("dur", 0);
String videoPath = data.getStringExtra("path");
File file = new File(PathUtil.getInstance().getImagePath(), "thvideo" + System.currentTimeMillis());
try {
FileOutputStream fos = new FileOutputStream(file);
Bitmap ThumbBitmap = ThumbnailUtils.createVideoThumbnail(videoPath, 3);
ThumbBitmap.compress(CompressFormat.JPEG, 100, fos);
fos.close();
sendVideoMessage(videoPath, file.getAbsolutePath(), duration);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
case REQUEST_CODE_SELECT_FILE: //send the file
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
sendFileByUri(uri);
}
}
break;
case REQUEST_CODE_SELECT_AT_USER:
if(data != null){
String username = data.getStringExtra("username");
inputAtUsername(username, false);
}
break;
//red packet code : 发送红包消息到聊天界面
case REQUEST_CODE_SEND_RED_PACKET:
if (data != null){
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}
break;
case REQUEST_CODE_SEND_TRANSFER_PACKET://发送转账消息
if (data != null) {
sendMessage(RedPacketUtil.createTRMessage(getActivity(), data, toChatUsername));
}
break;
//end of red packet code
default:
break;
}
}

}

@Override
public void onSetMessageAttributes(EMMessage message) {
if(isRobot){
//set message extension
message.setAttribute("em_robot_message", isRobot);
}
}

@Override
public EaseCustomChatRowProvider onSetCustomChatRowProvider() {
return new CustomChatRowProvider();
}


@Override
public void onEnterToChatDetails() {
if (chatType == Constant.CHATTYPE_GROUP) {
EMGroup group = EMClient.getInstance().groupManager().getGroup(toChatUsername);
if (group == null) {
Toast.makeText(getActivity(), R.string.gorup_not_found, Toast.LENGTH_SHORT).show();
return;
}
startActivityForResult(
(new Intent(getActivity(), GroupDetailsActivity.class).putExtra("groupId", toChatUsername)),
REQUEST_CODE_GROUP_DETAIL);
}else if(chatType == Constant.CHATTYPE_CHATROOM){
startActivityForResult(new Intent(getActivity(), ChatRoomDetailsActivity.class).putExtra("roomId", toChatUsername), REQUEST_CODE_GROUP_DETAIL);
}
}

@Override
public void onAvatarClick(String username) {
//handling when user click avatar
Intent intent = new Intent(getActivity(), UserProfileActivity.class);
intent.putExtra("username", username);
startActivity(intent);
}

@Override
public void onAvatarLongClick(String username) {
inputAtUsername(username);
}


@Override
public boolean onMessageBubbleClick(EMMessage message) {
//消息框点击事件,demo这里不做覆盖,如需覆盖,return true
//red packet code : 拆红包页面
if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)){
if (RedPacketUtil.isRandomRedPacket(message)){
RedPacketUtil.openRandomPacket(getActivity(),message);
} else {
RedPacketUtil.openRedPacket(getActivity(), chatType, message, toChatUsername, messageList);
}
return true;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
RedPacketUtil.openTransferPacket(getActivity(), message);
return true;
}
//end of red packet code
return false;
}
@Override
public void onCmdMessageReceived(List<EMMessage> messages) {
//red packet code : 处理红包回执透传消息
for (EMMessage message : messages) {
EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
String action = cmdMsgBody.action();//获取自定义action
if (action.equals(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION)){
RedPacketUtil.receiveRedPacketAckMessage(message);
messageList.refresh();
}
}
//end of red packet code
super.onCmdMessageReceived(messages);
}

@Override
public void onMessageBubbleLongClick(EMMessage message) {
// no message forward when in chat room
startActivityForResult((new Intent(getActivity(), ContextMenuActivity.class)).putExtra("message",message)
.putExtra("ischatroom", chatType == EaseConstant.CHATTYPE_CHATROOM),
REQUEST_CODE_CONTEXT_MENU);
}

@Override
public boolean onExtendMenuItemClick(int itemId, View view) {
switch (itemId) {
case ITEM_VIDEO:
Intent intent = new Intent(getActivity(), ImageGridActivity.class);
startActivityForResult(intent, REQUEST_CODE_SELECT_VIDEO);
break;
case ITEM_FILE: //file
selectFileFromLocal();
break;
case ITEM_VOICE_CALL:
startVoiceCall();
break;
case ITEM_VIDEO_CALL:
startVideoCall();
break;
//red packet code : 进入发红包页面
case ITEM_RED_PACKET:
if (chatType == Constant.CHATTYPE_SINGLE) {
//单聊红包修改进入红包的方法,可以在小额随机红包和普通单聊红包之间切换
RedPacketUtil.startRandomPacket(new RPRedPacketUtil.RPRandomCallback() {
@Override
public void onSendPacketSuccess(Intent data) {
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}

@Override
public void switchToNormalPacket() {
RedPacketUtil.startRedPacketActivityForResult(ChatFragment.this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
},getActivity(),toChatUsername);
} else {
RedPacketUtil.startRedPacketActivityForResult(this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
break;
case ITEM_TRANSFER_PACKET://进入转账页面
RedPacketUtil.startTransferActivityForResult(this, toChatUsername, REQUEST_CODE_SEND_TRANSFER_PACKET);
break;
//end of red packet code
default:
break;
}
//keep exist extend menu
return false;
}

/**
* select file
*/
protected void selectFileFromLocal() {
Intent intent = null;
if (Build.VERSION.SDK_INT < 19) { //api 19 and later, we can't use this way, demo just select from images
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);

} else {
intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
startActivityForResult(intent, REQUEST_CODE_SELECT_FILE);
}

/**
* make a voice call
*/
protected void startVoiceCall() {
if (!EMClient.getInstance().isConnected()) {
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
} else {
startActivity(new Intent(getActivity(), VoiceCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// voiceCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* make a video call
*/
protected void startVideoCall() {
if (!EMClient.getInstance().isConnected())
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
else {
startActivity(new Intent(getActivity(), VideoCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// videoCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* chat row provider
*
*/
private final class CustomChatRowProvider implements EaseCustomChatRowProvider {
@Override
public int getCustomChatRowTypeCount() {
//here the number is the message type in EMMessage::Type
//which is used to count the number of different chat row
return 12;
}

@Override
public int getCustomChatRowType(EMMessage message) {
if(message.getType() == EMMessage.Type.TXT){
//voice call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false)){
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VOICE_CALL : MESSAGE_TYPE_SENT_VOICE_CALL;
}else if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
//video call
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VIDEO_CALL : MESSAGE_TYPE_SENT_VIDEO_CALL;
}
//red packet code : 红包消息、红包回执消息以及转账消息的chatrow type
else if (RedPacketUtil.isRandomRedPacket(message)) {
//小额随机红包
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RANDOM : MESSAGE_TYPE_SEND_RANDOM;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {
//发送红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET : MESSAGE_TYPE_SEND_RED_PACKET;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
//领取红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET_ACK : MESSAGE_TYPE_SEND_RED_PACKET_ACK;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
//转账消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_TRANSFER_PACKET : MESSAGE_TYPE_SEND_TRANSFER_PACKET;
}
//end of red packet code
}
return 0;
}

@Override
public EaseChatRow getCustomChatRow(EMMessage message, int position, BaseAdapter adapter) {
if(message.getType() == EMMessage.Type.TXT){
// voice call or video call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false) ||
message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
return new ChatRowVoiceCall(getActivity(), message, position, adapter);
}
//red packet code : 红包消息、红包回执消息以及转账消息的chat row
else if (RedPacketUtil.isRandomRedPacket(message)) {//小额随机红包
return new ChatRowRandomPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {//红包消息
return new ChatRowRedPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {//红包回执消息
return new ChatRowRedPacketAck(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {//转账消息
return new ChatRowTransfer(getActivity(), message, position, adapter);
}
//end of red packet code
}
return null;
}

}

}
判断是不是机器人及添加监听
 
setChatFragmentListener(this);
if (chatType == Constant.CHATTYPE_SINGLE) {
Map<String,RobotUser> robotMap = DemoHelper.getInstance().getRobotList();
if(robotMap!=null && robotMap.containsKey(toChatUsername)){
isRobot = true;
}
}
点击标题返回及群聊@别人的功能​
// set click listener
titleBar.setLeftLayoutClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
if (EasyUtils.isSingleActivity(getActivity())) {
Intent intent = new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}
onBackPressed();
}
});
((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
if(chatType == EaseConstant.CHATTYPE_GROUP){
inputMenu.getPrimaryMenu().getEditText().addTextChangedListener(new TextWatcher() {

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if(count == 1 && "@".equals(String.valueOf(s.charAt(start)))){
startActivityForResult(new Intent(getActivity(), PickAtUserActivity.class).
putExtra("groupId", toChatUsername), REQUEST_CODE_SELECT_AT_USER);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}
@Override
public void afterTextChanged(Editable s) {

}
});
}
菜单的操作​
 
super.registerExtendMenuItem();
//extend menu items
inputMenu.registerExtendMenuItem(R.string.attach_video, R.drawable.em_chat_video_selector, ITEM_VIDEO, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_file, R.drawable.em_chat_file_selector, ITEM_FILE, extendMenuItemClickListener);
if(chatType == Constant.CHATTYPE_SINGLE){
inputMenu.registerExtendMenuItem(R.string.attach_voice_call, R.drawable.em_chat_voice_call_selector, ITEM_VOICE_CALL, extendMenuItemClickListener);
inputMenu.registerExtendMenuItem(R.string.attach_video_call, R.drawable.em_chat_video_call_selector, ITEM_VIDEO_CALL, extendMenuItemClickListener);
}
//聊天室暂时不支持红包功能
//red packet code : 注册红包菜单选项
if (chatType != Constant.CHATTYPE_CHATROOM) {
inputMenu.registerExtendMenuItem(R.string.attach_red_packet, R.drawable.em_chat_red_packet_selector, ITEM_RED_PACKET, extendMenuItemClickListener);
}
//red packet code : 注册转账菜单选项
if (chatType == Constant.CHATTYPE_SINGLE) {
inputMenu.registerExtendMenuItem(R.string.attach_transfer_money, R.drawable.em_chat_transfer_selector, ITEM_TRANSFER_PACKET, extendMenuItemClickListener);
}
//end of red packet code
一些功能操作​
if (requestCode == REQUEST_CODE_CONTEXT_MENU) {
switch (resultCode) {
case ContextMenuActivity.RESULT_CODE_COPY: // copy
clipboard.setPrimaryClip(ClipData.newPlainText(null,
((EMTextMessageBody) contextMenuMessage.getBody()).getMessage()));
break;
case ContextMenuActivity.RESULT_CODE_DELETE: // delete
conversation.removeMessage(contextMenuMessage.getMsgId());
messageList.refresh();
break;

case ContextMenuActivity.RESULT_CODE_FORWARD: // forward
Intent intent = new Intent(getActivity(), ForwardMessageActivity.class);
intent.putExtra("forward_msg_id", contextMenuMessage.getMsgId());
startActivity(intent);

break;

default:
break;
}
}
if(resultCode == Activity.RESULT_OK){
switch (requestCode) {
case REQUEST_CODE_SELECT_VIDEO: //send the video
if (data != null) {
int duration = data.getIntExtra("dur", 0);
String videoPath = data.getStringExtra("path");
File file = new File(PathUtil.getInstance().getImagePath(), "thvideo" + System.currentTimeMillis());
try {
FileOutputStream fos = new FileOutputStream(file);
Bitmap ThumbBitmap = ThumbnailUtils.createVideoThumbnail(videoPath, 3);
ThumbBitmap.compress(CompressFormat.JPEG, 100, fos);
fos.close();
sendVideoMessage(videoPath, file.getAbsolutePath(), duration);
} catch (Exception e) {
e.printStackTrace();
}
}
break;
case REQUEST_CODE_SELECT_FILE: //send the file
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
sendFileByUri(uri);
}
}
break;
case REQUEST_CODE_SELECT_AT_USER:
if(data != null){
String username = data.getStringExtra("username");
inputAtUsername(username, false);
}
break;
//red packet code : 发送红包消息到聊天界面
case REQUEST_CODE_SEND_RED_PACKET:
if (data != null){
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}
break;
case REQUEST_CODE_SEND_TRANSFER_PACKET://发送转账消息
if (data != null) {
sendMessage(RedPacketUtil.createTRMessage(getActivity(), data, toChatUsername));
}
break;
//end of red packet code
default:
break;
}
}
进入聊天详情​
 
@Override
public void onEnterToChatDetails() {
if (chatType == Constant.CHATTYPE_GROUP) {
EMGroup group = EMClient.getInstance().groupManager().getGroup(toChatUsername);
if (group == null) {
Toast.makeText(getActivity(), R.string.gorup_not_found, Toast.LENGTH_SHORT).show();
return;
}
startActivityForResult(
(new Intent(getActivity(), GroupDetailsActivity.class).putExtra("groupId", toChatUsername)),
REQUEST_CODE_GROUP_DETAIL);
}else if(chatType == Constant.CHATTYPE_CHATROOM){
startActivityForResult(new Intent(getActivity(), ChatRoomDetailsActivity.class).putExtra("roomId", toChatUsername), REQUEST_CODE_GROUP_DETAIL);
}
}
点击头像​
@Override
public void onAvatarClick(String username) {
//handling when user click avatar
Intent intent = new Intent(getActivity(), UserProfileActivity.class);
intent.putExtra("username", username);
startActivity(intent);
}
消息框点击事件、拆红包
@Override
public boolean onMessageBubbleClick(EMMessage message) {
//消息框点击事件,demo这里不做覆盖,如需覆盖,return true
//red packet code : 拆红包页面
if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)){
if (RedPacketUtil.isRandomRedPacket(message)){
RedPacketUtil.openRandomPacket(getActivity(),message);
} else {
RedPacketUtil.openRedPacket(getActivity(), chatType, message, toChatUsername, messageList);
}
return true;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
RedPacketUtil.openTransferPacket(getActivity(), message);
return true;
}
//end of red packet code
return false;
}
红包回执及消息框长按​
@Override
public void onCmdMessageReceived(List<EMMessage> messages) {
//red packet code : 处理红包回执透传消息
for (EMMessage message : messages) {
EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
String action = cmdMsgBody.action();//获取自定义action
if (action.equals(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION)){
RedPacketUtil.receiveRedPacketAckMessage(message);
messageList.refresh();
}
}
//end of red packet code
super.onCmdMessageReceived(messages);
}

@Override
public void onMessageBubbleLongClick(EMMessage message) {
// no message forward when in chat room
startActivityForResult((new Intent(getActivity(), ContextMenuActivity.class)).putExtra("message",message)
.putExtra("ischatroom", chatType == EaseConstant.CHATTYPE_CHATROOM),
REQUEST_CODE_CONTEXT_MENU);
}
扩展按钮​
@Override
public boolean onExtendMenuItemClick(int itemId, View view) {
switch (itemId) {
case ITEM_VIDEO:
Intent intent = new Intent(getActivity(), ImageGridActivity.class);
startActivityForResult(intent, REQUEST_CODE_SELECT_VIDEO);
break;
case ITEM_FILE: //file
selectFileFromLocal();
break;
case ITEM_VOICE_CALL:
startVoiceCall();
break;
case ITEM_VIDEO_CALL:
startVideoCall();
break;
//red packet code : 进入发红包页面
case ITEM_RED_PACKET:
if (chatType == Constant.CHATTYPE_SINGLE) {
//单聊红包修改进入红包的方法,可以在小额随机红包和普通单聊红包之间切换
RedPacketUtil.startRandomPacket(new RPRedPacketUtil.RPRandomCallback() {
@Override
public void onSendPacketSuccess(Intent data) {
sendMessage(RedPacketUtil.createRPMessage(getActivity(), data, toChatUsername));
}

@Override
public void switchToNormalPacket() {
RedPacketUtil.startRedPacketActivityForResult(ChatFragment.this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
},getActivity(),toChatUsername);
} else {
RedPacketUtil.startRedPacketActivityForResult(this, chatType, toChatUsername, REQUEST_CODE_SEND_RED_PACKET);
}
break;
case ITEM_TRANSFER_PACKET://进入转账页面
RedPacketUtil.startTransferActivityForResult(this, toChatUsername, REQUEST_CODE_SEND_TRANSFER_PACKET);
break;
//end of red packet code
default:
break;
}
//keep exist extend menu
return false;
}
本地文件选择、语音通话、视频通话、及自定义chatrow类型​
 
/**
* select file
*/
protected void selectFileFromLocal() {
Intent intent = null;
if (Build.VERSION.SDK_INT < 19) { //api 19 and later, we can't use this way, demo just select from images
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);

} else {
intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
startActivityForResult(intent, REQUEST_CODE_SELECT_FILE);
}

/**
* make a voice call
*/
protected void startVoiceCall() {
if (!EMClient.getInstance().isConnected()) {
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
} else {
startActivity(new Intent(getActivity(), VoiceCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// voiceCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* make a video call
*/
protected void startVideoCall() {
if (!EMClient.getInstance().isConnected())
Toast.makeText(getActivity(), R.string.not_connect_to_server, Toast.LENGTH_SHORT).show();
else {
startActivity(new Intent(getActivity(), VideoCallActivity.class).putExtra("username", toChatUsername)
.putExtra("isComingCall", false));
// videoCallBtn.setEnabled(false);
inputMenu.hideExtendMenuContainer();
}
}

/**
* chat row provider
*
*/
private final class CustomChatRowProvider implements EaseCustomChatRowProvider {
@Override
public int getCustomChatRowTypeCount() {
//here the number is the message type in EMMessage::Type
//which is used to count the number of different chat row
return 12;
}

@Override
public int getCustomChatRowType(EMMessage message) {
if(message.getType() == EMMessage.Type.TXT){
//voice call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false)){
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VOICE_CALL : MESSAGE_TYPE_SENT_VOICE_CALL;
}else if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
//video call
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_VIDEO_CALL : MESSAGE_TYPE_SENT_VIDEO_CALL;
}
//red packet code : 红包消息、红包回执消息以及转账消息的chatrow type
else if (RedPacketUtil.isRandomRedPacket(message)) {
//小额随机红包
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RANDOM : MESSAGE_TYPE_SEND_RANDOM;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {
//发送红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET : MESSAGE_TYPE_SEND_RED_PACKET;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
//领取红包消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_RED_PACKET_ACK : MESSAGE_TYPE_SEND_RED_PACKET_ACK;
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
//转账消息
return message.direct() == EMMessage.Direct.RECEIVE ? MESSAGE_TYPE_RECV_TRANSFER_PACKET : MESSAGE_TYPE_SEND_TRANSFER_PACKET;
}
//end of red packet code
}
return 0;
}

@Override
public EaseChatRow getCustomChatRow(EMMessage message, int position, BaseAdapter adapter) {
if(message.getType() == EMMessage.Type.TXT){
// voice call or video call
if (message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VOICE_CALL, false) ||
message.getBooleanAttribute(Constant.MESSAGE_ATTR_IS_VIDEO_CALL, false)){
return new ChatRowVoiceCall(getActivity(), message, position, adapter);
}
//red packet code : 红包消息、红包回执消息以及转账消息的chat row
else if (RedPacketUtil.isRandomRedPacket(message)) {//小额随机红包
return new ChatRowRandomPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_MESSAGE, false)) {//红包消息
return new ChatRowRedPacket(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {//红包回执消息
return new ChatRowRedPacketAck(getActivity(), message, position, adapter);
} else if (message.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {//转账消息
return new ChatRowTransfer(getActivity(), message, position, adapter);
}
//end of red packet code
}
return null;
}

}
Redpacketlibrary

由于业务未涉及,暂不作分析。
 
总结及其他

其实正常集成,按照于海同学所说也就半天时间,这是因为的确环信的SDK使用起来比较方便。

通过大致的阅读代码,环信的Demo代码写的还是很不错的,功能齐全,注释完整。值得学习和研究。

写在最后

多学习,多积累,多输出。!
 
附:最近两天实际工作采用环信SDK的开发详案

环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
0
评论

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面 Android Demo源码分析 集成笔记

随缘 发表了文章 • 750 次浏览 • 2017-02-21 16:11 • 来自相关话题

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 
设置界面

我们来贴代码

跟我们平常写的什么我的界面是大同小异的。主要有这些,其大多设置与demoModel有关

零钱RedPacketUtil.startChangeActivity(getActivity());接受新消息通知settingsModel.setSettingMsgNotification(false);
PreferenceManager.getInstance().setSettingMsgNotification(paramBoolean);
valueCache.put(Key.VibrateAndPlayToneOn, paramBoolean);声音​settingsModel.setSettingMsgSound(false);震动​settingsModel.setSettingMsgVibrate(false);消息推送设置

使用扬声器播放语音settingsModel.setSettingMsgSpeaker(false);自定义AppKey​settingsModel.enableCustomAppkey(false);自定义server​settingsModel.enableCustomServer(false); settingsModel.enableCustomServer(false);个人资料​startActivity(new Intent(getActivity(), UserProfileActivity.class).putExtra("setting", true)
.putExtra("username", EMClient.getInstance().getCurrentUser()));通讯录黑名单​startActivity(new Intent(getActivity(), BlacklistActivity.class));诊断​startActivity(new Intent(getActivity(), DiagnoseActivity.class));IOS离线推送昵称​startActivity(new Intent(getActivity(), OfflinePushNickActivity.class));通话设置​startActivity(new Intent(getActivity(), CallOptionActivity.class));允许聊天室群主离开​settingsModel.allowChatroomOwnerLeave(false);
chatOptions.allowChatroomOwnerLeave(false);退出群组时删除聊天数据​settingsModel.setDeleteMessagesAsExitGroup(false);
chatOptions.setDeleteMessagesAsExitGroup(false);自动同意群组加群邀请settingsModel.setAutoAcceptGroupInvitation(false);
chatOptions.setAutoAcceptGroupInvitation(false);视频自适应编码​settingsModel.setAdaptiveVideoEncode(false);
EMClient.getInstance().callManager().getCallOptions().enableFixedVideoResolution(true);退出登录​DemoHelper.getInstance().logout(false,new EMCallBack() {

@Override
public void onSuccess() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
pd.dismiss();
// show login screen
((MainActivity) getActivity()).finish();
startActivity(new Intent(getActivity(), LoginActivity.class));

}
});
}

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

}

@Override
public void onError(int code, String message) {
getActivity().runOnUiThread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
pd.dismiss();
Toast.makeText(getActivity(), "unbind devicetokens failed", Toast.LENGTH_SHORT).show();
}
});
}
});到这里主界面的三个fragment就都讲完了,我们来看重头戏。
 
环信官方Demo源码分析及SDK简单应用-EaseUI 查看全部
环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 
设置界面

我们来贴代码

跟我们平常写的什么我的界面是大同小异的。主要有这些,其大多设置与demoModel有关

零钱
RedPacketUtil.startChangeActivity(getActivity());
接受新消息通知
settingsModel.setSettingMsgNotification(false);
 
PreferenceManager.getInstance().setSettingMsgNotification(paramBoolean);
valueCache.put(Key.VibrateAndPlayToneOn, paramBoolean);
声音​
settingsModel.setSettingMsgSound(false);
震动​
settingsModel.setSettingMsgVibrate(false);
消息推送设置

使用扬声器播放语音
settingsModel.setSettingMsgSpeaker(false);
自定义AppKey​
settingsModel.enableCustomAppkey(false);
自定义server​
settingsModel.enableCustomServer(false);	settingsModel.enableCustomServer(false);
个人资料​
startActivity(new Intent(getActivity(), UserProfileActivity.class).putExtra("setting", true)
.putExtra("username", EMClient.getInstance().getCurrentUser()));
通讯录黑名单​
startActivity(new Intent(getActivity(), BlacklistActivity.class));
诊断​
startActivity(new Intent(getActivity(), DiagnoseActivity.class));
IOS离线推送昵称​
startActivity(new Intent(getActivity(), OfflinePushNickActivity.class));
通话设置​
startActivity(new Intent(getActivity(), CallOptionActivity.class));
允许聊天室群主离开​
settingsModel.allowChatroomOwnerLeave(false);
chatOptions.allowChatroomOwnerLeave(false);
退出群组时删除聊天数据​
settingsModel.setDeleteMessagesAsExitGroup(false);
chatOptions.setDeleteMessagesAsExitGroup(false);
自动同意群组加群邀请
settingsModel.setAutoAcceptGroupInvitation(false);
chatOptions.setAutoAcceptGroupInvitation(false);
视频自适应编码​
settingsModel.setAdaptiveVideoEncode(false);
EMClient.getInstance().callManager().getCallOptions().enableFixedVideoResolution(true);
退出登录​
DemoHelper.getInstance().logout(false,new EMCallBack() {

@Override
public void onSuccess() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
pd.dismiss();
// show login screen
((MainActivity) getActivity()).finish();
startActivity(new Intent(getActivity(), LoginActivity.class));

}
});
}

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

}

@Override
public void onError(int code, String message) {
getActivity().runOnUiThread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
pd.dismiss();
Toast.makeText(getActivity(), "unbind devicetokens failed", Toast.LENGTH_SHORT).show();
}
});
}
});
到这里主界面的三个fragment就都讲完了,我们来看重头戏。
 
环信官方Demo源码分析及SDK简单应用-EaseUI
0
评论

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面 Android Demo源码分析 集成笔记

随缘 发表了文章 • 1409 次浏览 • 2017-02-21 15:37 • 来自相关话题

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 
现在来看具体的主界面的三个Fragment
主界面的三个fragment
会话界面

​ 我们来看会话界面的代码
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.chat.EMConversation.EMConversationType;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.db.InviteMessgeDao;
import com.hyphenate.easeui.model.EaseAtMessageHelper;
import com.hyphenate.easeui.ui.EaseConversationListFragment;
import com.hyphenate.easeui.widget.EaseConversationList.EaseConversationListHelper;
import com.hyphenate.util.NetUtils;

public class ConversationListFragment extends EaseConversationListFragment{

private TextView errorText;

@Override
protected void initView() {
super.initView();
View errorView = (LinearLayout) View.inflate(getActivity(),R.layout.em_chat_neterror_item, null);
errorItemContainer.addView(errorView);
errorText = (TextView) errorView.findViewById(R.id.tv_connect_errormsg);
}

@Override
protected void setUpView() {
super.setUpView();
// register context menu
registerForContextMenu(conversationListView);
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
String username = conversation.conversationId();
if (username.equals(EMClient.getInstance().getCurrentUser()))
Toast.makeText(getActivity(), R.string.Cant_chat_with_yourself, Toast.LENGTH_SHORT).show();
else {
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
}
}
});
//red packet code : 红包回执消息在会话列表最后一条消息的展示
conversationListView.setConversationListHelper(new EaseConversationListHelper() {
@Override
public String onSetItemSecondaryText(EMMessage lastMessage) {
if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
String sendNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_SENDER_NAME, "");
String receiveNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_RECEIVER_NAME, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_someone_take_red_packet), receiveNick);
} else {
if (sendNick.equals(receiveNick)) {
msg = getResources().getString(R.string.msg_take_red_packet);
} else {
msg = String.format(getResources().getString(R.string.msg_take_someone_red_packet), sendNick);
}
}
return msg;
} else if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
String transferAmount = lastMessage.getStringAttribute(RPConstant.EXTRA_TRANSFER_AMOUNT, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_transfer_to_you), transferAmount);
} else {
msg = String.format(getResources().getString(R.string.msg_transfer_from_you),transferAmount);
}
return msg;
}
return null;
}
});
super.setUpView();
//end of red packet code
}

@Override
protected void onConnectionDisconnected() {
super.onConnectionDisconnected();
if (NetUtils.hasNetwork(getActivity())){
errorText.setText(R.string.can_not_connect_chat_server_connection);
} else {
errorText.setText(R.string.the_current_network);
}
}


@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.em_delete_message, menu);
}

@Override
public boolean onContextItemSelected(MenuItem item) {
boolean deleteMessage = false;
if (item.getItemId() == R.id.delete_message) {
deleteMessage = true;
} else if (item.getItemId() == R.id.delete_conversation) {
deleteMessage = false;
}
EMConversation tobeDeleteCons = conversationListView.getItem(((AdapterContextMenuInfo) item.getMenuInfo()).position);
if (tobeDeleteCons == null) {
return true;
}
if(tobeDeleteCons.getType() == EMConversationType.GroupChat){
EaseAtMessageHelper.get().removeAtMeGroup(tobeDeleteCons.conversationId());
}
try {
// delete conversation
EMClient.getInstance().chatManager().deleteConversation(tobeDeleteCons.conversationId(), deleteMessage);
InviteMessgeDao inviteMessgeDao = new InviteMessgeDao(getActivity());
inviteMessgeDao.deleteMessage(tobeDeleteCons.conversationId());
} catch (Exception e) {
e.printStackTrace();
}
refresh();

// update unread count
((MainActivity) getActivity()).updateUnreadLabel();
return true;
}

}我们还是挨个来读代码public class ConversationListFragment extends EaseConversationListFragment来,我们还是得先去找他爹算账。public class EaseConversationListFragment extends EaseBaseFragment哎呀,我们再去找他爷爷。 public abstract class EaseBaseFragment extends Fragment爷爷终于正常点是从Android系统类继承下来的了,我们看具体的代码

EaseBaseFragmentpackage com.hyphenate.easeui.ui;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;

import com.hyphenate.easeui.R;
import com.hyphenate.easeui.widget.EaseTitleBar;

public abstract class EaseBaseFragment extends Fragment{
protected EaseTitleBar titleBar;
protected InputMethodManager inputMethodManager;

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
//noinspection ConstantConditions
titleBar = (EaseTitleBar) getView().findViewById(R.id.title_bar);

initView();
setUpView();
}

public void showTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.VISIBLE);
}
}

public void hideTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.GONE);
}
}

protected void hideSoftKeyboard() {
if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getActivity().getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}

protected abstract void initView();

protected abstract void setUpView();


}我们还是挨个来看代码,研究他的功能。@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
//noinspection ConstantConditions
titleBar = (EaseTitleBar) getView().findViewById(R.id.title_bar);

initView();
setUpView();
}隐藏输入法

看到inputmethdManager要干嘛啊,隐藏键盘。果不其然。protected void hideSoftKeyboard() {
if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getActivity().getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}然后呢?

初始化标题头​ //noinspection ConstantConditions
titleBar = (EaseTitleBar) getView().findViewById(R.id.title_bar);最后初始化标题头,并且让子孙们去实现抽象方法initView和setUpView().

隐藏和显示标题头

其中还提供了两个方法,隐藏和显示标题头public void showTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.VISIBLE);
}
}

public void hideTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.GONE);
}
}好了,爷爷的帐算完了,我们来找他儿子。

EaseConversationListFragment

我们来看代码package com.hyphenate.easeui.ui;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;

import com.hyphenate.EMConnectionListener;
import com.hyphenate.EMConversationListener;
import com.hyphenate.EMError;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.easeui.R;
import com.hyphenate.easeui.widget.EaseConversationList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
* conversation list fragment
*
*/
public class EaseConversationListFragment extends EaseBaseFragment{
private final static int MSG_REFRESH = 2;
protected EditText query;
protected ImageButton clearSearch;
protected boolean hidden;
protected List<EMConversation> conversationList = new ArrayList<EMConversation>();
protected EaseConversationList conversationListView;
protected FrameLayout errorItemContainer;

protected boolean isConflict;

protected EMConversationListener convListener = new EMConversationListener(){

@Override
public void onCoversationUpdate() {
refresh();
}

};

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.ease_fragment_conversation_list, container, false);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
if(savedInstanceState != null && savedInstanceState.getBoolean("isConflict", false))
return;
super.onActivityCreated(savedInstanceState);
}

@Override
protected void initView() {
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
conversationListView = (EaseConversationList) getView().findViewById(R.id.list);
query = (EditText) getView().findViewById(R.id.query);
// button to clear content in search bar
clearSearch = (ImageButton) getView().findViewById(R.id.search_clear);
errorItemContainer = (FrameLayout) getView().findViewById(R.id.fl_error_item);
}

@Override
protected void setUpView() {
conversationList.addAll(loadConversationList());
conversationListView.init(conversationList);

if(listItemClickListener != null){
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
listItemClickListener.onListItemClicked(conversation);
}
});
}

EMClient.getInstance().addConnectionListener(connectionListener);

query.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
conversationListView.filter(s);
if (s.length() > 0) {
clearSearch.setVisibility(View.VISIBLE);
} else {
clearSearch.setVisibility(View.INVISIBLE);
}
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void afterTextChanged(Editable s) {
}
});
clearSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
query.getText().clear();
hideSoftKeyboard();
}
});

conversationListView.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});
}


protected EMConnectionListener connectionListener = new EMConnectionListener() {

@Override
public void onDisconnected(int error) {
if (error == EMError.USER_REMOVED || error == EMError.USER_LOGIN_ANOTHER_DEVICE || error == EMError.SERVER_SERVICE_RESTRICTED) {
isConflict = true;
} else {
handler.sendEmptyMessage(0);
}
}

@Override
public void onConnected() {
handler.sendEmptyMessage(1);
}
};
private EaseConversationListItemClickListener listItemClickListener;

protected Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0:
onConnectionDisconnected();
break;
case 1:
onConnectionConnected();
break;

case MSG_REFRESH:
{
conversationList.clear();
conversationList.addAll(loadConversationList());
conversationListView.refresh();
break;
}
default:
break;
}
}
};

/**
* connected to server
*/
protected void onConnectionConnected(){
errorItemContainer.setVisibility(View.GONE);
}

/**
* disconnected with server
*/
protected void onConnectionDisconnected(){
errorItemContainer.setVisibility(View.VISIBLE);
}


/**
* refresh ui
*/
public void refresh() {
if(!handler.hasMessages(MSG_REFRESH)){
handler.sendEmptyMessage(MSG_REFRESH);
}
}

/**
* load conversation list
*
* @return
+ */
protected List<EMConversation> loadConversationList(){
// get all conversations
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
List<Pair<Long, EMConversation>> sortList = new ArrayList<Pair<Long, EMConversation>>();
/**
* lastMsgTime will change if there is new message during sorting
* so use synchronized to make sure timestamp of last message won't change.
*/
synchronized (conversations) {
for (EMConversation conversation : conversations.values()) {
if (conversation.getAllMessages().size() != 0) {
sortList.add(new Pair<Long, EMConversation>(conversation.getLastMessage().getMsgTime(), conversation));
}
}
}
try {
// Internal is TimSort algorithm, has bug
sortConversationByLastChatTime(sortList);
} catch (Exception e) {
e.printStackTrace();
}
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;
}

/**
* sort conversations according time stamp of last message
*
* @param conversationList
*/
private void sortConversationByLastChatTime(List<Pair<Long, EMConversation>> conversationList) {
Collections.sort(conversationList, new Comparator<Pair<Long, EMConversation>>() {
@Override
public int compare(final Pair<Long, EMConversation> con1, final Pair<Long, EMConversation> con2) {

if (con1.first.equals(con2.first)) {
return 0;
} else if (con2.first.longValue() > con1.first.longValue()) {
return 1;
} else {
return -1;
}
}

});
}

protected void hideSoftKeyboard() {
if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getActivity().getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}

@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
this.hidden = hidden;
if (!hidden && !isConflict) {
refresh();
}
}

@Override
public void onResume() {
super.onResume();
if (!hidden) {
refresh();
}
}

@Override
public void onDestroy() {
super.onDestroy();
EMClient.getInstance().removeConnectionListener(connectionListener);
}

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(isConflict){
outState.putBoolean("isConflict", true);
}
}

public interface EaseConversationListItemClickListener {
/**
* click event for conversation list
* @param conversation -- clicked item
*/
void onListItemClicked(EMConversation conversation);
}

/**
* set conversation list item click listener
* @param listItemClickListener
*/
public void setConversationListItemClickListener(EaseConversationListItemClickListener listItemClickListener){
this.listItemClickListener = listItemClickListener;
}

}填充布局

首先onCreateView(),正常的填充了布局 return inflater.inflate(R.layout.ease_fragment_conversation_list, container, false);继续看代码
@Override
public void onActivityCreated(Bundle savedInstanceState) {
if(savedInstanceState != null && savedInstanceState.getBoolean("isConflict", false))
return;
super.onActivityCreated(savedInstanceState);
}判断冲突标志位@Override
protected void initView() {
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
conversationListView = (EaseConversationList) getView().findViewById(R.id.list);
query = (EditText) getView().findViewById(R.id.query);
// button to clear content in search bar
clearSearch = (ImageButton) getView().findViewById(R.id.search_clear);
errorItemContainer = (FrameLayout) getView().findViewById(R.id.fl_error_item);
}initView()

覆写爷爷的家规,初始化View输入法管理器
会话列表List查找联系人的输入框清除搜索的按钮errorItemContainer 错误标签容器
继续看代码setUpView()方法

setUpView()conversationList.addAll(loadConversationList());
conversationListView.init(conversationList);

if(listItemClickListener != null){
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
listItemClickListener.onListItemClicked(conversation);
}
});
}

EMClient.getInstance().addConnectionListener(connectionListener);

query.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
conversationListView.filter(s);
if (s.length() > 0) {
clearSearch.setVisibility(View.VISIBLE);
} else {
clearSearch.setVisibility(View.INVISIBLE);
}
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void afterTextChanged(Editable s) {
}
});
clearSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
query.getText().clear();
hideSoftKeyboard();
}
});

conversationListView.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});我们一句句的看conversationList.addAll(loadConversationList());
conversationListView.init(conversationList);会话列表添加全部以及数据填充初始化。

我们来看具体的方法
/**
* load conversation list
*
* @return
*/
protected List<EMConversation> loadConversationList(){
// get all conversations
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
List<Pair<Long, EMConversation>> sortList = new ArrayList<Pair<Long, EMConversation>>();
/**
* lastMsgTime will change if there is new message during sorting
* so use synchronized to make sure timestamp of last message won't change.
*/
synchronized (conversations) {
for (EMConversation conversation : conversations.values()) {
if (conversation.getAllMessages().size() != 0) {
sortList.add(new Pair<Long, EMConversation>(conversation.getLastMessage().getMsgTime(), conversation));
}
}
}
try {
// Internal is TimSort algorithm, has bug
sortConversationByLastChatTime(sortList);
} catch (Exception e) {
e.printStackTrace();
}
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;
}loadConversationList()返回一个EMConversation对象List。
// get all conversations
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
List<Pair<Long, EMConversation>> sortList = new ArrayList<Pair<Long, EMConversation>>();通过封装的chatManager拿到所有的会话列表/**
* lastMsgTime will change if there is new message during sorting
* so use synchronized to make sure timestamp of last message won't change.
*/
synchronized (conversations) {
for (EMConversation conversation : conversations.values()) {
if (conversation.getAllMessages().size() != 0) {
sortList.add(new Pair<Long, EMConversation>(conversation.getLastMessage().getMsgTime(), conversation));
}
}
}lastMsgTime会随着新消息的到来排序发生改变,所以我们用同步方法确保最新消息的时间戳不发生改变。

英文不好,大致是这么个意思。 try {
// Internal is TimSort algorithm, has bug
sortConversationByLastChatTime(sortList);
} catch (Exception e) {
e.printStackTrace();
}
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;其中还特地注释了一把,算法有点bug。
/**
* sort conversations according time stamp of last message
*
* @param conversationList
*/
private void sortConversationByLastChatTime(List<Pair<Long, EMConversation>> conversationList) {
Collections.sort(conversationList, new Comparator<Pair<Long, EMConversation>>() {
@Override
public int compare(final Pair<Long, EMConversation> con1, final Pair<Long, EMConversation> con2) {

if (con1.first.equals(con2.first)) {
return 0;
} else if (con2.first.longValue() > con1.first.longValue()) {
return 1;
} else {
return -1;
}
}

});根据最新的会话时间戳来排序。

我们接着看
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;添加完了返回list。conversationListView.init(conversationList);接着就初始化了。
if(listItemClickListener != null){
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
listItemClickListener.onListItemClicked(conversation);
}
});
}然后便是连接接听
EMClient.getInstance().addConnectionListener(connectionListener);添加了一个连接的监听。protected EMConnectionListener connectionListener = new EMConnectionListener() {

@Override
public void onDisconnected(int error) {
if (error == EMError.USER_REMOVED || error == EMError.USER_LOGIN_ANOTHER_DEVICE || error == EMError.SERVER_SERVICE_RESTRICTED) {
isConflict = true;
} else {
handler.sendEmptyMessage(0);
}
}

@Override
public void onConnected() {
handler.sendEmptyMessage(1);
}
};在断开连接时判断用户是否移除,是否在其他设备登陆,或者服务端的服务受到限制,是的话则标记冲突。不是则发送handler空消息。protected Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0:
onConnectionDisconnected();
break;
case 1:
onConnectionConnected();
break;

case MSG_REFRESH:
{
conversationList.clear();
conversationList.addAll(loadConversationList());
conversationListView.refresh();
break;
}
default:
break;
}
}
};干嘛啊?调用 onConnectionDisconnected 即连接断开的处理方法
/**
* disconnected with server
*/
protected void onConnectionDisconnected(){
errorItemContainer.setVisibility(View.VISIBLE);
}即显示错误条。

我们再接着看代码query.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
conversationListView.filter(s);
if (s.length() > 0) {
clearSearch.setVisibility(View.VISIBLE);
} else {
clearSearch.setVisibility(View.INVISIBLE);
}
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void afterTextChanged(Editable s) {
}
});
clearSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
query.getText().clear();
hideSoftKeyboard();
}
});

conversationListView.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});干了些什么啊?查询、清除搜索、会话列表点击监听。

其他方法​
/**
* connected to server
*/
protected void onConnectionConnected(){
errorItemContainer.setVisibility(View.GONE);
}连接后将错误条隐藏case MSG_REFRESH:
{
conversationList.clear();
conversationList.addAll(loadConversationList());
conversationListView.refresh();
break;
}服务器告诉要刷新了,那么我们就去清楚列表,然后去服务器拿并排序,然后刷新listview。其中该listview为自定义的EaseConversationList。

那么儿子齐活了,我们再看孙子
ConversationListFragmentpackage com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.chat.EMConversation.EMConversationType;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.db.InviteMessgeDao;
import com.hyphenate.easeui.model.EaseAtMessageHelper;
import com.hyphenate.easeui.ui.EaseConversationListFragment;
import com.hyphenate.easeui.widget.EaseConversationList.EaseConversationListHelper;
import com.hyphenate.util.NetUtils;

public class ConversationListFragment extends EaseConversationListFragment{

private TextView errorText;

@Override
protected void initView() {
super.initView();
View errorView = (LinearLayout) View.inflate(getActivity(),R.layout.em_chat_neterror_item, null);
errorItemContainer.addView(errorView);
errorText = (TextView) errorView.findViewById(R.id.tv_connect_errormsg);
}

@Override
protected void setUpView() {
super.setUpView();
// register context menu
registerForContextMenu(conversationListView);
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
String username = conversation.conversationId();
if (username.equals(EMClient.getInstance().getCurrentUser()))
Toast.makeText(getActivity(), R.string.Cant_chat_with_yourself, Toast.LENGTH_SHORT).show();
else {
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
}
}
});
//red packet code : 红包回执消息在会话列表最后一条消息的展示
conversationListView.setConversationListHelper(new EaseConversationListHelper() {
@Override
public String onSetItemSecondaryText(EMMessage lastMessage) {
if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
String sendNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_SENDER_NAME, "");
String receiveNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_RECEIVER_NAME, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_someone_take_red_packet), receiveNick);
} else {
if (sendNick.equals(receiveNick)) {
msg = getResources().getString(R.string.msg_take_red_packet);
} else {
msg = String.format(getResources().getString(R.string.msg_take_someone_red_packet), sendNick);
}
}
return msg;
} else if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
String transferAmount = lastMessage.getStringAttribute(RPConstant.EXTRA_TRANSFER_AMOUNT, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_transfer_to_you), transferAmount);
} else {
msg = String.format(getResources().getString(R.string.msg_transfer_from_you),transferAmount);
}
return msg;
}
return null;
}
});
super.setUpView();
//end of red packet code
}

@Override
protected void onConnectionDisconnected() {
super.onConnectionDisconnected();
if (NetUtils.hasNetwork(getActivity())){
errorText.setText(R.string.can_not_connect_chat_server_connection);
} else {
errorText.setText(R.string.the_current_network);
}
}


@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.em_delete_message, menu);
}

@Override
public boolean onContextItemSelected(MenuItem item) {
boolean deleteMessage = false;
if (item.getItemId() == R.id.delete_message) {
deleteMessage = true;
} else if (item.getItemId() == R.id.delete_conversation) {
deleteMessage = false;
}
EMConversation tobeDeleteCons = conversationListView.getItem(((AdapterContextMenuInfo) item.getMenuInfo()).position);
if (tobeDeleteCons == null) {
return true;
}
if(tobeDeleteCons.getType() == EMConversationType.GroupChat){
EaseAtMessageHelper.get().removeAtMeGroup(tobeDeleteCons.conversationId());
}
try {
// delete conversation
EMClient.getInstance().chatManager().deleteConversation(tobeDeleteCons.conversationId(), deleteMessage);
InviteMessgeDao inviteMessgeDao = new InviteMessgeDao(getActivity());
inviteMessgeDao.deleteMessage(tobeDeleteCons.conversationId());
} catch (Exception e) {
e.printStackTrace();
}
refresh();

// update unread count
((MainActivity) getActivity()).updateUnreadLabel();
return true;
}

}initView()
@Override
protected void initView() {
super.initView();
View errorView = (LinearLayout) View.inflate(getActivity(),R.layout.em_chat_neterror_item, null);
errorItemContainer.addView(errorView);
errorText = (TextView) errorView.findViewById(R.id.tv_connect_errormsg);
}添加了错误的容器、初始化错误消息控件。registerForContextMenu(conversationListView);注册上下文菜单
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
String username = conversation.conversationId();
if (username.equals(EMClient.getInstance().getCurrentUser()))
Toast.makeText(getActivity(), R.string.Cant_chat_with_yourself, Toast.LENGTH_SHORT).show();
else {
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
}
}
});条目的点击监听

其中做了这么些事情:
判断用户名是否等于当前登陆用户,是则提示不能跟自己聊天如果是群聊的话,则继续判断是聊天室还是群组,并带值给ChatActivity即聊天界面最后将用户名带上,跳转ChatActivity。
//red packet code : 红包回执消息在会话列表最后一条消息的展示
conversationListView.setConversationListHelper(new EaseConversationListHelper() {
@Override
public String onSetItemSecondaryText(EMMessage lastMessage) {
if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
String sendNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_SENDER_NAME, "");
String receiveNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_RECEIVER_NAME, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_someone_take_red_packet), receiveNick);
} else {
if (sendNick.equals(receiveNick)) {
msg = getResources().getString(R.string.msg_take_red_packet);
} else {
msg = String.format(getResources().getString(R.string.msg_take_someone_red_packet), sendNick);
}
}
return msg;
} else if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
String transferAmount = lastMessage.getStringAttribute(RPConstant.EXTRA_TRANSFER_AMOUNT, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_transfer_to_you), transferAmount);
} else {
msg = String.format(getResources().getString(R.string.msg_transfer_from_you),transferAmount);
}
return msg;
}
return null;
}
});
super.setUpView();最后是红包回执信息。

我们接着看其他的方法
@Override
protected void onConnectionDisconnected() {
super.onConnectionDisconnected();
if (NetUtils.hasNetwork(getActivity())){
errorText.setText(R.string.can_not_connect_chat_server_connection);
} else {
errorText.setText(R.string.the_current_network);
}
}端口网络则提示没网标签。
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.em_delete_message, menu);
}创建上下文菜单@Override
public boolean onContextItemSelected(MenuItem item) {
boolean deleteMessage = false;
if (item.getItemId() == R.id.delete_message) {
deleteMessage = true;
} else if (item.getItemId() == R.id.delete_conversation) {
deleteMessage = false;
}
EMConversation tobeDeleteCons = conversationListView.getItem(((AdapterContextMenuInfo) item.getMenuInfo()).position);
if (tobeDeleteCons == null) {
return true;
}
if(tobeDeleteCons.getType() == EMConversationType.GroupChat){
EaseAtMessageHelper.get().removeAtMeGroup(tobeDeleteCons.conversationId());
}
try {
// delete conversation
EMClient.getInstance().chatManager().deleteConversation(tobeDeleteCons.conversationId(), deleteMessage);
InviteMessgeDao inviteMessgeDao = new InviteMessgeDao(getActivity());
inviteMessgeDao.deleteMessage(tobeDeleteCons.conversationId());
} catch (Exception e) {
e.printStackTrace();
}
refresh();

// update unread count[url=http://www.imgeek.org/article/825308690]环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面[/url]
((MainActivity) getActivity()).updateUnreadLabel();
return true;
}上下文菜单选择的处理方法

删除消息并更新未读消息。

好,至此,第一个界面,会话界面到此结束。

我们再来看通讯录界面。
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面 查看全部
环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 
现在来看具体的主界面的三个Fragment
主界面的三个fragment
会话界面


​ 我们来看会话界面的代码
 
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.chat.EMConversation.EMConversationType;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.db.InviteMessgeDao;
import com.hyphenate.easeui.model.EaseAtMessageHelper;
import com.hyphenate.easeui.ui.EaseConversationListFragment;
import com.hyphenate.easeui.widget.EaseConversationList.EaseConversationListHelper;
import com.hyphenate.util.NetUtils;

public class ConversationListFragment extends EaseConversationListFragment{

private TextView errorText;

@Override
protected void initView() {
super.initView();
View errorView = (LinearLayout) View.inflate(getActivity(),R.layout.em_chat_neterror_item, null);
errorItemContainer.addView(errorView);
errorText = (TextView) errorView.findViewById(R.id.tv_connect_errormsg);
}

@Override
protected void setUpView() {
super.setUpView();
// register context menu
registerForContextMenu(conversationListView);
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
String username = conversation.conversationId();
if (username.equals(EMClient.getInstance().getCurrentUser()))
Toast.makeText(getActivity(), R.string.Cant_chat_with_yourself, Toast.LENGTH_SHORT).show();
else {
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
}
}
});
//red packet code : 红包回执消息在会话列表最后一条消息的展示
conversationListView.setConversationListHelper(new EaseConversationListHelper() {
@Override
public String onSetItemSecondaryText(EMMessage lastMessage) {
if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
String sendNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_SENDER_NAME, "");
String receiveNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_RECEIVER_NAME, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_someone_take_red_packet), receiveNick);
} else {
if (sendNick.equals(receiveNick)) {
msg = getResources().getString(R.string.msg_take_red_packet);
} else {
msg = String.format(getResources().getString(R.string.msg_take_someone_red_packet), sendNick);
}
}
return msg;
} else if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
String transferAmount = lastMessage.getStringAttribute(RPConstant.EXTRA_TRANSFER_AMOUNT, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_transfer_to_you), transferAmount);
} else {
msg = String.format(getResources().getString(R.string.msg_transfer_from_you),transferAmount);
}
return msg;
}
return null;
}
});
super.setUpView();
//end of red packet code
}

@Override
protected void onConnectionDisconnected() {
super.onConnectionDisconnected();
if (NetUtils.hasNetwork(getActivity())){
errorText.setText(R.string.can_not_connect_chat_server_connection);
} else {
errorText.setText(R.string.the_current_network);
}
}


@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.em_delete_message, menu);
}

@Override
public boolean onContextItemSelected(MenuItem item) {
boolean deleteMessage = false;
if (item.getItemId() == R.id.delete_message) {
deleteMessage = true;
} else if (item.getItemId() == R.id.delete_conversation) {
deleteMessage = false;
}
EMConversation tobeDeleteCons = conversationListView.getItem(((AdapterContextMenuInfo) item.getMenuInfo()).position);
if (tobeDeleteCons == null) {
return true;
}
if(tobeDeleteCons.getType() == EMConversationType.GroupChat){
EaseAtMessageHelper.get().removeAtMeGroup(tobeDeleteCons.conversationId());
}
try {
// delete conversation
EMClient.getInstance().chatManager().deleteConversation(tobeDeleteCons.conversationId(), deleteMessage);
InviteMessgeDao inviteMessgeDao = new InviteMessgeDao(getActivity());
inviteMessgeDao.deleteMessage(tobeDeleteCons.conversationId());
} catch (Exception e) {
e.printStackTrace();
}
refresh();

// update unread count
((MainActivity) getActivity()).updateUnreadLabel();
return true;
}

}
我们还是挨个来读代码
public class ConversationListFragment extends EaseConversationListFragment
来,我们还是得先去找他爹算账。
public class EaseConversationListFragment extends EaseBaseFragment
哎呀,我们再去找他爷爷。
 public abstract class EaseBaseFragment extends Fragment
爷爷终于正常点是从Android系统类继承下来的了,我们看具体的代码

EaseBaseFragment
package com.hyphenate.easeui.ui;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;

import com.hyphenate.easeui.R;
import com.hyphenate.easeui.widget.EaseTitleBar;

public abstract class EaseBaseFragment extends Fragment{
protected EaseTitleBar titleBar;
protected InputMethodManager inputMethodManager;

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
//noinspection ConstantConditions
titleBar = (EaseTitleBar) getView().findViewById(R.id.title_bar);

initView();
setUpView();
}

public void showTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.VISIBLE);
}
}

public void hideTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.GONE);
}
}

protected void hideSoftKeyboard() {
if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getActivity().getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}

protected abstract void initView();

protected abstract void setUpView();


}
我们还是挨个来看代码,研究他的功能。
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
//noinspection ConstantConditions
titleBar = (EaseTitleBar) getView().findViewById(R.id.title_bar);

initView();
setUpView();
}
隐藏输入法

看到inputmethdManager要干嘛啊,隐藏键盘。果不其然。
protected void hideSoftKeyboard() {
if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getActivity().getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}
然后呢?

初始化标题头​
 //noinspection ConstantConditions
titleBar = (EaseTitleBar) getView().findViewById(R.id.title_bar);
最后初始化标题头,并且让子孙们去实现抽象方法initView和setUpView().

隐藏和显示标题头

其中还提供了两个方法,隐藏和显示标题头
public void showTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.VISIBLE);
}
}

public void hideTitleBar(){
if(titleBar != null){
titleBar.setVisibility(View.GONE);
}
}
好了,爷爷的帐算完了,我们来找他儿子。

EaseConversationListFragment

我们来看代码
package com.hyphenate.easeui.ui;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;

import com.hyphenate.EMConnectionListener;
import com.hyphenate.EMConversationListener;
import com.hyphenate.EMError;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.easeui.R;
import com.hyphenate.easeui.widget.EaseConversationList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
* conversation list fragment
*
*/
public class EaseConversationListFragment extends EaseBaseFragment{
private final static int MSG_REFRESH = 2;
protected EditText query;
protected ImageButton clearSearch;
protected boolean hidden;
protected List<EMConversation> conversationList = new ArrayList<EMConversation>();
protected EaseConversationList conversationListView;
protected FrameLayout errorItemContainer;

protected boolean isConflict;

protected EMConversationListener convListener = new EMConversationListener(){

@Override
public void onCoversationUpdate() {
refresh();
}

};

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.ease_fragment_conversation_list, container, false);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
if(savedInstanceState != null && savedInstanceState.getBoolean("isConflict", false))
return;
super.onActivityCreated(savedInstanceState);
}

@Override
protected void initView() {
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
conversationListView = (EaseConversationList) getView().findViewById(R.id.list);
query = (EditText) getView().findViewById(R.id.query);
// button to clear content in search bar
clearSearch = (ImageButton) getView().findViewById(R.id.search_clear);
errorItemContainer = (FrameLayout) getView().findViewById(R.id.fl_error_item);
}

@Override
protected void setUpView() {
conversationList.addAll(loadConversationList());
conversationListView.init(conversationList);

if(listItemClickListener != null){
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
listItemClickListener.onListItemClicked(conversation);
}
});
}

EMClient.getInstance().addConnectionListener(connectionListener);

query.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
conversationListView.filter(s);
if (s.length() > 0) {
clearSearch.setVisibility(View.VISIBLE);
} else {
clearSearch.setVisibility(View.INVISIBLE);
}
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void afterTextChanged(Editable s) {
}
});
clearSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
query.getText().clear();
hideSoftKeyboard();
}
});

conversationListView.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});
}


protected EMConnectionListener connectionListener = new EMConnectionListener() {

@Override
public void onDisconnected(int error) {
if (error == EMError.USER_REMOVED || error == EMError.USER_LOGIN_ANOTHER_DEVICE || error == EMError.SERVER_SERVICE_RESTRICTED) {
isConflict = true;
} else {
handler.sendEmptyMessage(0);
}
}

@Override
public void onConnected() {
handler.sendEmptyMessage(1);
}
};
private EaseConversationListItemClickListener listItemClickListener;

protected Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0:
onConnectionDisconnected();
break;
case 1:
onConnectionConnected();
break;

case MSG_REFRESH:
{
conversationList.clear();
conversationList.addAll(loadConversationList());
conversationListView.refresh();
break;
}
default:
break;
}
}
};

/**
* connected to server
*/
protected void onConnectionConnected(){
errorItemContainer.setVisibility(View.GONE);
}

/**
* disconnected with server
*/
protected void onConnectionDisconnected(){
errorItemContainer.setVisibility(View.VISIBLE);
}


/**
* refresh ui
*/
public void refresh() {
if(!handler.hasMessages(MSG_REFRESH)){
handler.sendEmptyMessage(MSG_REFRESH);
}
}

/**
* load conversation list
*
* @return
+ */
protected List<EMConversation> loadConversationList(){
// get all conversations
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
List<Pair<Long, EMConversation>> sortList = new ArrayList<Pair<Long, EMConversation>>();
/**
* lastMsgTime will change if there is new message during sorting
* so use synchronized to make sure timestamp of last message won't change.
*/
synchronized (conversations) {
for (EMConversation conversation : conversations.values()) {
if (conversation.getAllMessages().size() != 0) {
sortList.add(new Pair<Long, EMConversation>(conversation.getLastMessage().getMsgTime(), conversation));
}
}
}
try {
// Internal is TimSort algorithm, has bug
sortConversationByLastChatTime(sortList);
} catch (Exception e) {
e.printStackTrace();
}
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;
}

/**
* sort conversations according time stamp of last message
*
* @param conversationList
*/
private void sortConversationByLastChatTime(List<Pair<Long, EMConversation>> conversationList) {
Collections.sort(conversationList, new Comparator<Pair<Long, EMConversation>>() {
@Override
public int compare(final Pair<Long, EMConversation> con1, final Pair<Long, EMConversation> con2) {

if (con1.first.equals(con2.first)) {
return 0;
} else if (con2.first.longValue() > con1.first.longValue()) {
return 1;
} else {
return -1;
}
}

});
}

protected void hideSoftKeyboard() {
if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getActivity().getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}

@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
this.hidden = hidden;
if (!hidden && !isConflict) {
refresh();
}
}

@Override
public void onResume() {
super.onResume();
if (!hidden) {
refresh();
}
}

@Override
public void onDestroy() {
super.onDestroy();
EMClient.getInstance().removeConnectionListener(connectionListener);
}

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(isConflict){
outState.putBoolean("isConflict", true);
}
}

public interface EaseConversationListItemClickListener {
/**
* click event for conversation list
* @param conversation -- clicked item
*/
void onListItemClicked(EMConversation conversation);
}

/**
* set conversation list item click listener
* @param listItemClickListener
*/
public void setConversationListItemClickListener(EaseConversationListItemClickListener listItemClickListener){
this.listItemClickListener = listItemClickListener;
}

}
填充布局

首先onCreateView(),正常的填充了布局
  return inflater.inflate(R.layout.ease_fragment_conversation_list, container, false);
继续看代码
 
@Override
public void onActivityCreated(Bundle savedInstanceState) {
if(savedInstanceState != null && savedInstanceState.getBoolean("isConflict", false))
return;
super.onActivityCreated(savedInstanceState);
}
判断冲突标志位
@Override
protected void initView() {
inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
conversationListView = (EaseConversationList) getView().findViewById(R.id.list);
query = (EditText) getView().findViewById(R.id.query);
// button to clear content in search bar
clearSearch = (ImageButton) getView().findViewById(R.id.search_clear);
errorItemContainer = (FrameLayout) getView().findViewById(R.id.fl_error_item);
}
initView()

覆写爷爷的家规,初始化View输入法管理器
  • 会话列表List
  • 查找联系人的输入框
  • 清除搜索的按钮
  • errorItemContainer 错误标签容器

继续看代码setUpView()方法

setUpView()
conversationList.addAll(loadConversationList());
conversationListView.init(conversationList);

if(listItemClickListener != null){
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
listItemClickListener.onListItemClicked(conversation);
}
});
}

EMClient.getInstance().addConnectionListener(connectionListener);

query.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
conversationListView.filter(s);
if (s.length() > 0) {
clearSearch.setVisibility(View.VISIBLE);
} else {
clearSearch.setVisibility(View.INVISIBLE);
}
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void afterTextChanged(Editable s) {
}
});
clearSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
query.getText().clear();
hideSoftKeyboard();
}
});

conversationListView.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});
我们一句句的看
conversationList.addAll(loadConversationList());
conversationListView.init(conversationList);
会话列表添加全部以及数据填充初始化。

我们来看具体的方法
 
/**
* load conversation list
*
* @return
*/
protected List<EMConversation> loadConversationList(){
// get all conversations
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
List<Pair<Long, EMConversation>> sortList = new ArrayList<Pair<Long, EMConversation>>();
/**
* lastMsgTime will change if there is new message during sorting
* so use synchronized to make sure timestamp of last message won't change.
*/
synchronized (conversations) {
for (EMConversation conversation : conversations.values()) {
if (conversation.getAllMessages().size() != 0) {
sortList.add(new Pair<Long, EMConversation>(conversation.getLastMessage().getMsgTime(), conversation));
}
}
}
try {
// Internal is TimSort algorithm, has bug
sortConversationByLastChatTime(sortList);
} catch (Exception e) {
e.printStackTrace();
}
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;
}
loadConversationList()返回一个EMConversation对象List。
 
// get all conversations
Map<String, EMConversation> conversations = EMClient.getInstance().chatManager().getAllConversations();
List<Pair<Long, EMConversation>> sortList = new ArrayList<Pair<Long, EMConversation>>();
通过封装的chatManager拿到所有的会话列表
/**
* lastMsgTime will change if there is new message during sorting
* so use synchronized to make sure timestamp of last message won't change.
*/
synchronized (conversations) {
for (EMConversation conversation : conversations.values()) {
if (conversation.getAllMessages().size() != 0) {
sortList.add(new Pair<Long, EMConversation>(conversation.getLastMessage().getMsgTime(), conversation));
}
}
}
lastMsgTime会随着新消息的到来排序发生改变,所以我们用同步方法确保最新消息的时间戳不发生改变。

英文不好,大致是这么个意思。
 try {
// Internal is TimSort algorithm, has bug
sortConversationByLastChatTime(sortList);
} catch (Exception e) {
e.printStackTrace();
}
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;
其中还特地注释了一把,算法有点bug。
 
/**
* sort conversations according time stamp of last message
*
* @param conversationList
*/
private void sortConversationByLastChatTime(List<Pair<Long, EMConversation>> conversationList) {
Collections.sort(conversationList, new Comparator<Pair<Long, EMConversation>>() {
@Override
public int compare(final Pair<Long, EMConversation> con1, final Pair<Long, EMConversation> con2) {

if (con1.first.equals(con2.first)) {
return 0;
} else if (con2.first.longValue() > con1.first.longValue()) {
return 1;
} else {
return -1;
}
}

});
根据最新的会话时间戳来排序。

我们接着看
 
List<EMConversation> list = new ArrayList<EMConversation>();
for (Pair<Long, EMConversation> sortItem : sortList) {
list.add(sortItem.second);
}
return list;
添加完了返回list。
conversationListView.init(conversationList);
接着就初始化了。
 
if(listItemClickListener != null){
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
listItemClickListener.onListItemClicked(conversation);
}
});
}
然后便是连接接听
 
EMClient.getInstance().addConnectionListener(connectionListener);
添加了一个连接的监听。
protected EMConnectionListener connectionListener = new EMConnectionListener() {

@Override
public void onDisconnected(int error) {
if (error == EMError.USER_REMOVED || error == EMError.USER_LOGIN_ANOTHER_DEVICE || error == EMError.SERVER_SERVICE_RESTRICTED) {
isConflict = true;
} else {
handler.sendEmptyMessage(0);
}
}

@Override
public void onConnected() {
handler.sendEmptyMessage(1);
}
};
在断开连接时判断用户是否移除,是否在其他设备登陆,或者服务端的服务受到限制,是的话则标记冲突。不是则发送handler空消息。
protected Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0:
onConnectionDisconnected();
break;
case 1:
onConnectionConnected();
break;

case MSG_REFRESH:
{
conversationList.clear();
conversationList.addAll(loadConversationList());
conversationListView.refresh();
break;
}
default:
break;
}
}
};
干嘛啊?调用 onConnectionDisconnected 即连接断开的处理方法
 
/**
* disconnected with server
*/
protected void onConnectionDisconnected(){
errorItemContainer.setVisibility(View.VISIBLE);
}
即显示错误条。

我们再接着看代码
query.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
conversationListView.filter(s);
if (s.length() > 0) {
clearSearch.setVisibility(View.VISIBLE);
} else {
clearSearch.setVisibility(View.INVISIBLE);
}
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void afterTextChanged(Editable s) {
}
});
clearSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
query.getText().clear();
hideSoftKeyboard();
}
});

conversationListView.setOnTouchListener(new OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
return false;
}
});
干了些什么啊?查询、清除搜索、会话列表点击监听。

其他方法​
 
/**
* connected to server
*/
protected void onConnectionConnected(){
errorItemContainer.setVisibility(View.GONE);
}
连接后将错误条隐藏
case MSG_REFRESH:
{
conversationList.clear();
conversationList.addAll(loadConversationList());
conversationListView.refresh();
break;
}
服务器告诉要刷新了,那么我们就去清楚列表,然后去服务器拿并排序,然后刷新listview。其中该listview为自定义的EaseConversationList。

那么儿子齐活了,我们再看孙子
ConversationListFragment
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.easemob.redpacketsdk.constant.RPConstant;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMConversation;
import com.hyphenate.chat.EMConversation.EMConversationType;
import com.hyphenate.chat.EMMessage;
import com.hyphenate.chatuidemo.Constant;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.db.InviteMessgeDao;
import com.hyphenate.easeui.model.EaseAtMessageHelper;
import com.hyphenate.easeui.ui.EaseConversationListFragment;
import com.hyphenate.easeui.widget.EaseConversationList.EaseConversationListHelper;
import com.hyphenate.util.NetUtils;

public class ConversationListFragment extends EaseConversationListFragment{

private TextView errorText;

@Override
protected void initView() {
super.initView();
View errorView = (LinearLayout) View.inflate(getActivity(),R.layout.em_chat_neterror_item, null);
errorItemContainer.addView(errorView);
errorText = (TextView) errorView.findViewById(R.id.tv_connect_errormsg);
}

@Override
protected void setUpView() {
super.setUpView();
// register context menu
registerForContextMenu(conversationListView);
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
String username = conversation.conversationId();
if (username.equals(EMClient.getInstance().getCurrentUser()))
Toast.makeText(getActivity(), R.string.Cant_chat_with_yourself, Toast.LENGTH_SHORT).show();
else {
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
}
}
});
//red packet code : 红包回执消息在会话列表最后一条消息的展示
conversationListView.setConversationListHelper(new EaseConversationListHelper() {
@Override
public String onSetItemSecondaryText(EMMessage lastMessage) {
if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
String sendNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_SENDER_NAME, "");
String receiveNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_RECEIVER_NAME, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_someone_take_red_packet), receiveNick);
} else {
if (sendNick.equals(receiveNick)) {
msg = getResources().getString(R.string.msg_take_red_packet);
} else {
msg = String.format(getResources().getString(R.string.msg_take_someone_red_packet), sendNick);
}
}
return msg;
} else if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
String transferAmount = lastMessage.getStringAttribute(RPConstant.EXTRA_TRANSFER_AMOUNT, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_transfer_to_you), transferAmount);
} else {
msg = String.format(getResources().getString(R.string.msg_transfer_from_you),transferAmount);
}
return msg;
}
return null;
}
});
super.setUpView();
//end of red packet code
}

@Override
protected void onConnectionDisconnected() {
super.onConnectionDisconnected();
if (NetUtils.hasNetwork(getActivity())){
errorText.setText(R.string.can_not_connect_chat_server_connection);
} else {
errorText.setText(R.string.the_current_network);
}
}


@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.em_delete_message, menu);
}

@Override
public boolean onContextItemSelected(MenuItem item) {
boolean deleteMessage = false;
if (item.getItemId() == R.id.delete_message) {
deleteMessage = true;
} else if (item.getItemId() == R.id.delete_conversation) {
deleteMessage = false;
}
EMConversation tobeDeleteCons = conversationListView.getItem(((AdapterContextMenuInfo) item.getMenuInfo()).position);
if (tobeDeleteCons == null) {
return true;
}
if(tobeDeleteCons.getType() == EMConversationType.GroupChat){
EaseAtMessageHelper.get().removeAtMeGroup(tobeDeleteCons.conversationId());
}
try {
// delete conversation
EMClient.getInstance().chatManager().deleteConversation(tobeDeleteCons.conversationId(), deleteMessage);
InviteMessgeDao inviteMessgeDao = new InviteMessgeDao(getActivity());
inviteMessgeDao.deleteMessage(tobeDeleteCons.conversationId());
} catch (Exception e) {
e.printStackTrace();
}
refresh();

// update unread count
((MainActivity) getActivity()).updateUnreadLabel();
return true;
}

}
initView()
 
@Override
protected void initView() {
super.initView();
View errorView = (LinearLayout) View.inflate(getActivity(),R.layout.em_chat_neterror_item, null);
errorItemContainer.addView(errorView);
errorText = (TextView) errorView.findViewById(R.id.tv_connect_errormsg);
}
添加了错误的容器、初始化错误消息控件。
registerForContextMenu(conversationListView);
注册上下文菜单
 
conversationListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
EMConversation conversation = conversationListView.getItem(position);
String username = conversation.conversationId();
if (username.equals(EMClient.getInstance().getCurrentUser()))
Toast.makeText(getActivity(), R.string.Cant_chat_with_yourself, Toast.LENGTH_SHORT).show();
else {
// start chat acitivity
Intent intent = new Intent(getActivity(), ChatActivity.class);
if(conversation.isGroup()){
if(conversation.getType() == EMConversationType.ChatRoom){
// it's group chat
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_CHATROOM);
}else{
intent.putExtra(Constant.EXTRA_CHAT_TYPE, Constant.CHATTYPE_GROUP);
}

}
// it's single chat
intent.putExtra(Constant.EXTRA_USER_ID, username);
startActivity(intent);
}
}
});
条目的点击监听

其中做了这么些事情:
  • 判断用户名是否等于当前登陆用户,是则提示不能跟自己聊天
  • 如果是群聊的话,则继续判断是聊天室还是群组,并带值给ChatActivity即聊天界面
  • 最后将用户名带上,跳转ChatActivity。

//red packet code : 红包回执消息在会话列表最后一条消息的展示
conversationListView.setConversationListHelper(new EaseConversationListHelper() {
@Override
public String onSetItemSecondaryText(EMMessage lastMessage) {
if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_RED_PACKET_ACK_MESSAGE, false)) {
String sendNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_SENDER_NAME, "");
String receiveNick = lastMessage.getStringAttribute(RPConstant.EXTRA_RED_PACKET_RECEIVER_NAME, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_someone_take_red_packet), receiveNick);
} else {
if (sendNick.equals(receiveNick)) {
msg = getResources().getString(R.string.msg_take_red_packet);
} else {
msg = String.format(getResources().getString(R.string.msg_take_someone_red_packet), sendNick);
}
}
return msg;
} else if (lastMessage.getBooleanAttribute(RPConstant.MESSAGE_ATTR_IS_TRANSFER_PACKET_MESSAGE, false)) {
String transferAmount = lastMessage.getStringAttribute(RPConstant.EXTRA_TRANSFER_AMOUNT, "");
String msg;
if (lastMessage.direct() == EMMessage.Direct.RECEIVE) {
msg = String.format(getResources().getString(R.string.msg_transfer_to_you), transferAmount);
} else {
msg = String.format(getResources().getString(R.string.msg_transfer_from_you),transferAmount);
}
return msg;
}
return null;
}
});
super.setUpView();
最后是红包回执信息。

我们接着看其他的方法
 
@Override
protected void onConnectionDisconnected() {
super.onConnectionDisconnected();
if (NetUtils.hasNetwork(getActivity())){
errorText.setText(R.string.can_not_connect_chat_server_connection);
} else {
errorText.setText(R.string.the_current_network);
}
}
端口网络则提示没网标签。
 
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.em_delete_message, menu);
}
创建上下文菜单
@Override
public boolean onContextItemSelected(MenuItem item) {
boolean deleteMessage = false;
if (item.getItemId() == R.id.delete_message) {
deleteMessage = true;
} else if (item.getItemId() == R.id.delete_conversation) {
deleteMessage = false;
}
EMConversation tobeDeleteCons = conversationListView.getItem(((AdapterContextMenuInfo) item.getMenuInfo()).position);
if (tobeDeleteCons == null) {
return true;
}
if(tobeDeleteCons.getType() == EMConversationType.GroupChat){
EaseAtMessageHelper.get().removeAtMeGroup(tobeDeleteCons.conversationId());
}
try {
// delete conversation
EMClient.getInstance().chatManager().deleteConversation(tobeDeleteCons.conversationId(), deleteMessage);
InviteMessgeDao inviteMessgeDao = new InviteMessgeDao(getActivity());
inviteMessgeDao.deleteMessage(tobeDeleteCons.conversationId());
} catch (Exception e) {
e.printStackTrace();
}
refresh();

// update unread count[url=http://www.imgeek.org/article/825308690]环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面[/url]
((MainActivity) getActivity()).updateUnreadLabel();
return true;
}
上下文菜单选择的处理方法

删除消息并更新未读消息。

好,至此,第一个界面,会话界面到此结束。

我们再来看通讯录界面。
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
0
评论

环信官方Demo源码分析及SDK简单应用-LoginActivity Android Demo源码分析 集成笔记

随缘 发表了文章 • 1171 次浏览 • 2017-02-21 15:23 • 来自相关话题

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0
 
环信官方Demo源码分析及SDK简单应用-LoginActivity
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 
上文我们在已经登录的情况下来到的MainActivity,那么我们在没有登录情况下呢,当然是来登陆页面。下面我们来看登录页。
LoginActivity/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hyphenate.chatuidemo.ui;

import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import com.hyphenate.EMCallBack;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chatuidemo.DemoApplication;
import com.hyphenate.chatuidemo.DemoHelper;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.db.DemoDBManager;
import com.hyphenate.easeui.utils.EaseCommonUtils;

/**
* Login screen
*
*/
public class LoginActivity extends BaseActivity {
private static final String TAG = "LoginActivity";
public static final int REQUEST_CODE_SETNICK = 1;
private EditText usernameEditText;
private EditText passwordEditText;

private boolean progressShow;
private boolean autoLogin = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// enter the main activity if already logged in
if (DemoHelper.getInstance().isLoggedIn()) {
autoLogin = true;
startActivity(new Intent(LoginActivity.this, MainActivity.class));

return;
}
setContentView(R.layout.em_activity_login);

usernameEditText = (EditText) findViewById(R.id.username);
passwordEditText = (EditText) findViewById(R.id.password);

// if user changed, clear the password
usernameEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
passwordEditText.setText(null);
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void afterTextChanged(Editable s) {

}
});
if (DemoHelper.getInstance().getCurrentUsernName() != null) {
usernameEditText.setText(DemoHelper.getInstance().getCurrentUsernName());
}
}

/**
* login
*
* @param view
*/
public void login(View view) {
if (!EaseCommonUtils.isNetWorkConnected(this)) {
Toast.makeText(this, R.string.network_isnot_available, Toast.LENGTH_SHORT).show();
return;
}
String currentUsername = usernameEditText.getText().toString().trim();
String currentPassword = passwordEditText.getText().toString().trim();

if (TextUtils.isEmpty(currentUsername)) {
Toast.makeText(this, R.string.User_name_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(currentPassword)) {
Toast.makeText(this, R.string.Password_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}

progressShow = true;
final ProgressDialog pd = new ProgressDialog(LoginActivity.this);
pd.setCanceledOnTouchOutside(false);
pd.setOnCancelListener(new OnCancelListener() {

@Override
public void onCancel(DialogInterface dialog) {
Log.d(TAG, "EMClient.getInstance().onCancel");
progressShow = false;
}
});
pd.setMessage(getString(R.string.Is_landing));
pd.show();

// After logout,the DemoDB may still be accessed due to async callback, so the DemoDB will be re-opened again.
// close it before login to make sure DemoDB not overlap
DemoDBManager.getInstance().closeDB();

// reset current user name before login
DemoHelper.getInstance().setCurrentUserName(currentUsername);

final long start = System.currentTimeMillis();
// call login method
Log.d(TAG, "EMClient.getInstance().login");
EMClient.getInstance().login(currentUsername, currentPassword, new EMCallBack() {

@Override
public void onSuccess() {
Log.d(TAG, "login: onSuccess");


// ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();

// update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();
}

@Override
public void onProgress(int progress, String status) {
Log.d(TAG, "login: onProgress");
}

@Override
public void onError(final int code, final String message) {
Log.d(TAG, "login: onError: " + code);
if (!progressShow) {
return;
}
runOnUiThread(new Runnable() {
public void run() {
pd.dismiss();
Toast.makeText(getApplicationContext(), getString(R.string.Login_failed) + message,
Toast.LENGTH_SHORT).show();
}
});
}
});
}


/**
* register
*
* @param view
*/
public void register(View view) {
startActivityForResult(new Intent(this, RegisterActivity.class), 0);
}

@Override
protected void onResume() {
super.onResume();
if (autoLogin) {
return;
}
}
}我们挨个来阅读

自动登录if (DemoHelper.getInstance().isLoggedIn()) {
autoLogin = true;
startActivity(new Intent(LoginActivity.this, MainActivity.class));

return;
}如果已经登录那么设置自动标志位为true,跳到主界面去。

用户名文本变动监听​// if user changed, clear the password
usernameEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
passwordEditText.setText(null);
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void afterTextChanged(Editable s) {

}
});
if (DemoHelper.getInstance().getCurrentUsernName() != null) {
usernameEditText.setText(DemoHelper.getInstance().getCurrentUsernName());
}简单的文本变化监听,用户名变化了就把密码给清空一下。

下面我们来看登录逻辑

登录逻辑

首先判断当前是否有网络连接 if (!EaseCommonUtils.isNetWorkConnected(this)) {
Toast.makeText(this, R.string.network_isnot_available, Toast.LENGTH_SHORT).show();
return;
}我们来看看这个工具类是怎么写的/**
* check if network avalable
*
* @param context
* @return
*/
public static boolean isNetWorkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable() && mNetworkInfo.isConnected();
}
}

return false;
}大家常用的通用判断网络连接方法。

接着往下看String currentUsername = usernameEditText.getText().toString().trim();
String currentPassword = passwordEditText.getText().toString().trim();

if (TextUtils.isEmpty(currentUsername)) {
Toast.makeText(this, R.string.User_name_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(currentPassword)) {
Toast.makeText(this, R.string.Password_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}

progressShow = true;
final ProgressDialog pd = new ProgressDialog(LoginActivity.this);
pd.setCanceledOnTouchOutside(false);
pd.setOnCancelListener(new OnCancelListener() {

@Override
public void onCancel(DialogInterface dialog) {
Log.d(TAG, "EMClient.getInstance().onCancel");
progressShow = false;
}
});
pd.setMessage(getString(R.string.Is_landing));
pd.show();正常的取值,弹个进度框。

来看比较有意思的 // After logout,the DemoDB may still be accessed due to async callback, so the DemoDB will be re-opened again.
// close it before login to make sure DemoDB not overlap
DemoDBManager.getInstance().closeDB();

// reset current user name before login
DemoHelper.getInstance().setCurrentUserName(currentUsername);英文不好,大致的意思就是注销以后,DemoDB可能会依然在执行一些异步回调,所以DemoDB会再次重新打开,所以我们要在登陆之前确保DemoDB不会被Overlap。所以我们关闭一下数据库。

然后就是在登陆之前重新设置下当前登陆的用户名

下面就是具体的登陆实现了EMClient.getInstance().login(currentUsername, currentPassword, new EMCallBack() {

@Override
public void onSuccess() {
Log.d(TAG, "login: onSuccess");


// ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();

// update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();
}

@Override
public void onProgress(int progress, String status) {
Log.d(TAG, "login: onProgress");
}

@Override
public void onError(final int code, final String message) {
Log.d(TAG, "login: onError: " + code);
if (!progressShow) {
return;
}
runOnUiThread(new Runnable() {
public void run() {
pd.dismiss();
Toast.makeText(getApplicationContext(), getString(R.string.Login_failed) + message,
Toast.LENGTH_SHORT).show();
}
});
}
});我们看到环信封装了自己实现的登陆方法,并做了回调。

三个接口:
 onSuccess() 成功了onError() 嗝屁了onProgress 处理中

我们看onSuccess中的代码
// ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();

// update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();我们看到跳转到MainActivity之前通用做了相同的群组加载 // ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations(); // update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}更新当前的推送昵称。
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();异步的从App后台或者三方库中获取用户信息,想想我们之前看他的分包的时候,是不是见到过parse这个包。就是这玩意。

然后跳转到主界面
/**
* register
*
* @param view
*/
public void register(View view) {
startActivityForResult(new Intent(this, RegisterActivity.class), 0);
}

@Override
protected void onResume() {
super.onResume();
if (autoLogin) {
return;
}
}然后便是注册了,是直接跳到注册界面去。onResume中如果已经登录直接return掉。

那么我们看完了这些Activity了,接着看啥呢?啰嗦了这么久,我们终于可以看具体的主界面的三个Fragment了。
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面 查看全部
环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0
 
环信官方Demo源码分析及SDK简单应用-LoginActivity
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面
 
环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
 
上文我们在已经登录的情况下来到的MainActivity,那么我们在没有登录情况下呢,当然是来登陆页面。下面我们来看登录页。
LoginActivity
/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hyphenate.chatuidemo.ui;

import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import com.hyphenate.EMCallBack;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chatuidemo.DemoApplication;
import com.hyphenate.chatuidemo.DemoHelper;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.chatuidemo.db.DemoDBManager;
import com.hyphenate.easeui.utils.EaseCommonUtils;

/**
* Login screen
*
*/
public class LoginActivity extends BaseActivity {
private static final String TAG = "LoginActivity";
public static final int REQUEST_CODE_SETNICK = 1;
private EditText usernameEditText;
private EditText passwordEditText;

private boolean progressShow;
private boolean autoLogin = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// enter the main activity if already logged in
if (DemoHelper.getInstance().isLoggedIn()) {
autoLogin = true;
startActivity(new Intent(LoginActivity.this, MainActivity.class));

return;
}
setContentView(R.layout.em_activity_login);

usernameEditText = (EditText) findViewById(R.id.username);
passwordEditText = (EditText) findViewById(R.id.password);

// if user changed, clear the password
usernameEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
passwordEditText.setText(null);
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void afterTextChanged(Editable s) {

}
});
if (DemoHelper.getInstance().getCurrentUsernName() != null) {
usernameEditText.setText(DemoHelper.getInstance().getCurrentUsernName());
}
}

/**
* login
*
* @param view
*/
public void login(View view) {
if (!EaseCommonUtils.isNetWorkConnected(this)) {
Toast.makeText(this, R.string.network_isnot_available, Toast.LENGTH_SHORT).show();
return;
}
String currentUsername = usernameEditText.getText().toString().trim();
String currentPassword = passwordEditText.getText().toString().trim();

if (TextUtils.isEmpty(currentUsername)) {
Toast.makeText(this, R.string.User_name_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(currentPassword)) {
Toast.makeText(this, R.string.Password_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}

progressShow = true;
final ProgressDialog pd = new ProgressDialog(LoginActivity.this);
pd.setCanceledOnTouchOutside(false);
pd.setOnCancelListener(new OnCancelListener() {

@Override
public void onCancel(DialogInterface dialog) {
Log.d(TAG, "EMClient.getInstance().onCancel");
progressShow = false;
}
});
pd.setMessage(getString(R.string.Is_landing));
pd.show();

// After logout,the DemoDB may still be accessed due to async callback, so the DemoDB will be re-opened again.
// close it before login to make sure DemoDB not overlap
DemoDBManager.getInstance().closeDB();

// reset current user name before login
DemoHelper.getInstance().setCurrentUserName(currentUsername);

final long start = System.currentTimeMillis();
// call login method
Log.d(TAG, "EMClient.getInstance().login");
EMClient.getInstance().login(currentUsername, currentPassword, new EMCallBack() {

@Override
public void onSuccess() {
Log.d(TAG, "login: onSuccess");


// ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();

// update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();
}

@Override
public void onProgress(int progress, String status) {
Log.d(TAG, "login: onProgress");
}

@Override
public void onError(final int code, final String message) {
Log.d(TAG, "login: onError: " + code);
if (!progressShow) {
return;
}
runOnUiThread(new Runnable() {
public void run() {
pd.dismiss();
Toast.makeText(getApplicationContext(), getString(R.string.Login_failed) + message,
Toast.LENGTH_SHORT).show();
}
});
}
});
}


/**
* register
*
* @param view
*/
public void register(View view) {
startActivityForResult(new Intent(this, RegisterActivity.class), 0);
}

@Override
protected void onResume() {
super.onResume();
if (autoLogin) {
return;
}
}
}
我们挨个来阅读

自动登录
if (DemoHelper.getInstance().isLoggedIn()) {
autoLogin = true;
startActivity(new Intent(LoginActivity.this, MainActivity.class));

return;
}
如果已经登录那么设置自动标志位为true,跳到主界面去。

用户名文本变动监听​
// if user changed, clear the password
usernameEditText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
passwordEditText.setText(null);
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void afterTextChanged(Editable s) {

}
});
if (DemoHelper.getInstance().getCurrentUsernName() != null) {
usernameEditText.setText(DemoHelper.getInstance().getCurrentUsernName());
}
简单的文本变化监听,用户名变化了就把密码给清空一下。

下面我们来看登录逻辑

登录逻辑

首先判断当前是否有网络连接
	   if (!EaseCommonUtils.isNetWorkConnected(this)) {
Toast.makeText(this, R.string.network_isnot_available, Toast.LENGTH_SHORT).show();
return;
}
我们来看看这个工具类是怎么写的
/**
* check if network avalable
*
* @param context
* @return
*/
public static boolean isNetWorkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable() && mNetworkInfo.isConnected();
}
}

return false;
}
大家常用的通用判断网络连接方法。

接着往下看
String currentUsername = usernameEditText.getText().toString().trim();
String currentPassword = passwordEditText.getText().toString().trim();

if (TextUtils.isEmpty(currentUsername)) {
Toast.makeText(this, R.string.User_name_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(currentPassword)) {
Toast.makeText(this, R.string.Password_cannot_be_empty, Toast.LENGTH_SHORT).show();
return;
}

progressShow = true;
final ProgressDialog pd = new ProgressDialog(LoginActivity.this);
pd.setCanceledOnTouchOutside(false);
pd.setOnCancelListener(new OnCancelListener() {

@Override
public void onCancel(DialogInterface dialog) {
Log.d(TAG, "EMClient.getInstance().onCancel");
progressShow = false;
}
});
pd.setMessage(getString(R.string.Is_landing));
pd.show();
正常的取值,弹个进度框。

来看比较有意思的
        // After logout,the DemoDB may still be accessed due to async callback, so the DemoDB will be re-opened again.
// close it before login to make sure DemoDB not overlap
DemoDBManager.getInstance().closeDB();

// reset current user name before login
DemoHelper.getInstance().setCurrentUserName(currentUsername);
英文不好,大致的意思就是注销以后,DemoDB可能会依然在执行一些异步回调,所以DemoDB会再次重新打开,所以我们要在登陆之前确保DemoDB不会被Overlap。所以我们关闭一下数据库。

然后就是在登陆之前重新设置下当前登陆的用户名

下面就是具体的登陆实现了
EMClient.getInstance().login(currentUsername, currentPassword, new EMCallBack() {

@Override
public void onSuccess() {
Log.d(TAG, "login: onSuccess");


// ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();

// update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();
}

@Override
public void onProgress(int progress, String status) {
Log.d(TAG, "login: onProgress");
}

@Override
public void onError(final int code, final String message) {
Log.d(TAG, "login: onError: " + code);
if (!progressShow) {
return;
}
runOnUiThread(new Runnable() {
public void run() {
pd.dismiss();
Toast.makeText(getApplicationContext(), getString(R.string.Login_failed) + message,
Toast.LENGTH_SHORT).show();
}
});
}
});
我们看到环信封装了自己实现的登陆方法,并做了回调。

三个接口:
  •  
  • onSuccess() 成功了
  • onError() 嗝屁了
  • onProgress 处理中


我们看onSuccess中的代码
 
// ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();

// update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();
我们看到跳转到MainActivity之前通用做了相同的群组加载
                // ** manually load all local groups and conversation
EMClient.getInstance().groupManager().loadAllGroups();
EMClient.getInstance().chatManager().loadAllConversations();
  // update current user's display name for APNs
boolean updatenick = EMClient.getInstance().pushManager().updatePushNickname(
DemoApplication.currentUserNick.trim());
if (!updatenick) {
Log.e("LoginActivity", "update current user nick fail");
}

if (!LoginActivity.this.isFinishing() && pd.isShowing()) {
pd.dismiss();
}
更新当前的推送昵称。
 
// get user's info (this should be get from App's server or 3rd party service)
DemoHelper.getInstance().getUserProfileManager().asyncGetCurrentUserInfo();

Intent intent = new Intent(LoginActivity.this,
MainActivity.class);
startActivity(intent);

finish();
异步的从App后台或者三方库中获取用户信息,想想我们之前看他的分包的时候,是不是见到过parse这个包。就是这玩意。

然后跳转到主界面
 
/**
* register
*
* @param view
*/
public void register(View view) {
startActivityForResult(new Intent(this, RegisterActivity.class), 0);
}

@Override
protected void onResume() {
super.onResume();
if (autoLogin) {
return;
}
}
然后便是注册了,是直接跳到注册界面去。onResume中如果已经登录直接return掉。

那么我们看完了这些Activity了,接着看啥呢?啰嗦了这么久,我们终于可以看具体的主界面的三个Fragment了。
 
环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面
0
评论

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0 Android Demo源码分析 集成笔记

随缘 发表了文章 • 2676 次浏览 • 2017-02-21 11:58 • 来自相关话题

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面

环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现

ChatDemoUI3.0
代码结构及逻辑分析

既然上面提到首先分析ChatDemoUI 3.0,那么我们来看看其目录结构





mainfests 清单文件我们稍后来看具体内容

java 具体的代码部分,其包名为com.hyphenate.chatuidemo.

有如下子包:
adapter 适配器db 数据库相关domain 实体相关parse 第三方库 parse(用于存储 Demo 中用户的信息)管理包receiver 广播接收者runtimepermissions 运行时权限相关ui 界面部分utils 工具类video.util 视频录制工具包widget 自定义view

另有如下单独非子包类:
Constant 常量类DemoApplication applicationDemoHelper Demo的帮助类DemoModel 逻辑相关类

其中主要类有这么几个
DemoApplication:继承于系统的 Application 类,其 onCreate() 为整个程序的入口,相关的初始化操作都在这里面;DemoHelper: Demo 全局帮助类,主要功能为初始化 EaseUI、环信 SDK 及 Demo 相关的实例,以及封装一些全局使用的方法;MainActivity: 主页面,包含会话列表页面(ConversationListFragment)、联系人列表页(ContactListFragment)、设置页面(SettingsFragment),前两个继承自己 EaseUI 中的 fragment;ChatActivity: 会话页面,这个类代码很少,主要原因是大部分逻辑写在 ChatFragment 中。ChatFragment 继承自 EaseChatFragment,做成 fragment 的好处在于用起来更灵活,可以单独作为一个页面使用,也可以和其他 fragment 一起放到一个 Activity 中;GroupDetailsActivity: 群组详情页面

我们通过代码结构能得到什么信息?可能你会有以下的比较直观的感受。 ​
分包挺清晰抓住了DemoHelper和DemoModel也就抓住了整个的纲领其他的你就自己扯吧。

废话不多说,我们来看代码。

我们的阅读的顺序是这样的
AndroidMainfest.xmlDemoApplicationSplashActivity各流程类

AndroidMainfest.xml
实际上没什么好说的,不过我们还是细细的研究一番




解决sdk定义版本声明的问题,我们在后面如果使用到了红包的ui,出现了一些sdk的错误可以加上。




SDK常见的一大坨权限。其中Google Cloud Messaging还是别用吧,身在何处,能稳定么?

然后就是各种各样的界面声明

总共这么些个界面(Tips:由于本文是现阅读现写,所有未中文指出部分,后面代码阅读会去补上):
开屏页登陆页注册聊天添加好友群组邀请群组列表聊天室详情新建群组退出群组提示框群组选人PickAtUserActivity地图新的朋友邀请消息页面转发消息用户列表页面自定义的contextmenu显示下载大图页面下载文件黑名单公开的群聊列表PublicChatRoomsActivity语音通话视频通话群聊简单信息群组黑名单用户列表GroupBlacklistActivityGroupSearchMessageActivityPublicGroupsSeachActivityEditActivityEaseShowVideoActivityImageGridActivityRecorderVideoActivityDiagnoseActivityOfflinePushNickActivityrobots listRobotsActivityUserProfileActivitySetServersActivityOfflinePushSettingsActivityCallOptionActivity发红包红包详情红包记录WebView零钱绑定银行卡群成员列表支付宝h5支付页面转账页面转账详情页面
再往下就是相关的一些广播接收者,服务,以及杂七杂八的东西了。有如下部分:开机自启动
GCM小米推送华为推送友盟EMChat服务EMJob服务EMMonitor Receiver百度地图服务
其中比较重要的 <!-- 设置环信应用的appkey -->
<meta-data
android:name="EASEMOB_APPKEY"
android:value="你自己的环信Key" />这样,我们基本AndroidMainfest就阅读完了。因为Androidmainfest.xml指出主Activity为ui包下的SplashActivity。按理说我们应该接着来看SplashActivity。但是别忘了App启动后DemoApplication是在主界面之前的。我们将在阅读完Application后再来看SplashActivity。

DemoApplication

上代码:/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hyphenate.chatuidemo;

import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDex;

import com.easemob.redpacketsdk.RedPacket;

public class DemoApplication extends Application {

public static Context applicationContext;
private static DemoApplication instance;
// login user name
public final String PREF_USERNAME = "username";

/**
* nickname for current user, the nickname instead of ID be shown when user receive notification from APNs
*/
public static String currentUserNick = "";

@Override
public void onCreate() {
MultiDex.install(this);
super.onCreate();
applicationContext = this;
instance = this;

//init demo helper
DemoHelper.getInstance().init(applicationContext);
//red packet code : 初始化红包上下文,开启日志输出开关
RedPacket.getInstance().initContext(applicationContext);
RedPacket.getInstance().setDebugMode(true);
//end of red packet code
}

public static DemoApplication getInstance() {
return instance;
}

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}第一句是分包,我们知道分包有以下两种方式:
项目中的Application类继承MultiDexApplication。在自己的Application类的attachBaseContext方法中调用MultiDex.install(this);。
然后又做了几件事
初始化DemoHelper初始化红包并开启日志输出
Application就这样没了,我们继续看SplashActivity。
SplashActivity
我们来看代码:
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.os.Bundle;
import android.view.animation.AlphaAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.hyphenate.chat.EMClient;
import com.hyphenate.chatuidemo.DemoHelper;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.util.EasyUtils;

/**
* 开屏页
*
*/
public class SplashActivity extends BaseActivity {

private static final int sleepTime = 2000;

@Override
protected void onCreate(Bundle arg0) {
setContentView(R.layout.em_activity_splash);
super.onCreate(arg0);

RelativeLayout rootLayout = (RelativeLayout) findViewById(R.id.splash_root);
TextView versionText = (TextView) findViewById(R.id.tv_version);

versionText.setText(getVersion());
AlphaAnimation animation = new AlphaAnimation(0.3f, 1.0f);
animation.setDuration(1500);
rootLayout.startAnimation(animation);
}

@Override
protected void onStart() {
super.onStart();

new Thread(new Runnable() {
public void run() {
if (DemoHelper.getInstance().isLoggedIn()) {
// auto login mode, make sure all group and conversation is loaed before enter the main screen
long start = System.currentTimeMillis();
EMClient.getInstance().chatManager().loadAllConversations();
EMClient.getInstance().groupManager().loadAllGroups();
long costTime = System.currentTimeMillis() - start;
//wait
if (sleepTime - costTime > 0) {
try {
Thread.sleep(sleepTime - costTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String topActivityName = EasyUtils.getTopActivityName(EMClient.getInstance().getContext());
if (topActivityName != null && (topActivityName.equals(VideoCallActivity.class.getName()) || topActivityName.equals(VoiceCallActivity.class.getName()))) {
// nop
// avoid main screen overlap Calling Activity
} else {
//enter main screen
startActivity(new Intent(SplashActivity.this, MainActivity.class));
}
finish();
}else {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
startActivity(new Intent(SplashActivity.this, LoginActivity.class));
finish();
}
}
}).start();

}

/**
* get sdk version
*/
private String getVersion() {
return EMClient.getInstance().VERSION;
}
}UI部分我们不关心,我们来看下代码逻辑部分
new Thread(new Runnable() {
public void run() {
if (DemoHelper.getInstance().isLoggedIn()) {
// auto login mode, make sure all group and conversation is loaed before enter the main screen
long start = System.currentTimeMillis();
EMClient.getInstance().chatManager().loadAllConversations();
EMClient.getInstance().groupManager().loadAllGroups();
long costTime = System.currentTimeMillis() - start;
//wait
if (sleepTime - costTime > 0) {
try {
Thread.sleep(sleepTime - costTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String topActivityName = EasyUtils.getTopActivityName(EMClient.getInstance().getContext());
if (topActivityName != null && (topActivityName.equals(VideoCallActivity.class.getName()) || topActivityName.equals(VoiceCallActivity.class.getName()))) {
// nop
// avoid main screen overlap Calling Activity
} else {
//enter main screen
startActivity(new Intent(SplashActivity.this, MainActivity.class));
}
finish();
}else {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
startActivity(new Intent(SplashActivity.this, LoginActivity.class));
finish();
}
}
}).start();在这里,我们看到了这个DemoHelper帮助类,起了个线程,判断是否已经登录。我们来看看他是如何判断的。





我们来看官方文档中关于此isLoggedInBefore()的解释。





我们再回头来看刚才的代码,代码中有句注释,是这么写到。// auto login mode, make sure all group and conversation is loaed before enter the main screen自动登录模式,请确保进入主页面后本地回话和群组都load完毕。

那么代码中有两句话就是干这个事情的EMClient.getInstance().chatManager().loadAllConversations();
EMClient.getInstance().groupManager().loadAllGroups();这里部分代码最好是放在SplashActivity因为如果登录过,APP 长期在后台再进的时候也可能会导致加载到内存的群组和会话为空。






这里做了等待和判断

如果栈顶的ActivityName不为空而且顶栈的名字为语音通话的Activity或者栈顶的名字等于语音通话的Activity。毛线都不做。这个地方猜测应该是指语音通话挂起,重新调起界面的过程。

否则,跳到主界面。

那么我们接着看主界面。

MainActivity

那么这个时候,我们应该怎样去看主界面的代码呢?

首先看Demo的界面,然后看代码的方法,再一一对应。

来,我们来看界面,界面是这个样子的。






三个界面

会话、通讯录、设置

有了直观的认识以后,我们再来看代码。
 
我们来一段一段看代码

BaseActivity

MainActivity继承自BaseActivity。/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hyphenate.chatuidemo.ui;

import android.annotation.SuppressLint;
import android.os.Bundle;
import com.hyphenate.easeui.ui.EaseBaseActivity;
import com.umeng.analytics.MobclickAgent;

@SuppressLint("Registered")
public class BaseActivity extends EaseBaseActivity {

@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
}

@Override
protected void onResume() {
super.onResume();
// umeng
MobclickAgent.onResume(this);
}

@Override
protected void onStart() {
super.onStart();
// umeng
MobclickAgent.onPause(this);
}

}只有友盟的一些数据埋点,我们继续往上挖看他爹。

EaseBaseActivity/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hyphenate.easeui.ui;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;

import com.hyphenate.easeui.controller.EaseUI;

@SuppressLint({"NewApi", "Registered"})
public class EaseBaseActivity extends FragmentActivity {

protected InputMethodManager inputMethodManager;

@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
//http://stackoverflow.com/quest ... ffer/
// should be in launcher activity, but all app use this can avoid the problem
if(!isTaskRoot()){
Intent intent = getIntent();
String action = intent.getAction();
if(intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action.equals(Intent.ACTION_MAIN)){
finish();
return;
}
}

inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
}


@Override
protected void onResume() {
super.onResume();
// cancel the notification
EaseUI.getInstance().getNotifier().reset();
}

protected void hideSoftKeyboard() {
if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}

/**
* back
*
* @param view
*/
public void back(View view) {
finish();
}
}





这段代码其实是用来解决重复实例化Launch Activity的问题。喜欢打破砂锅问到底的,可以自己去google。

至于hideSoftKeyboard则是常见的隐藏软键盘

其中有一句 EaseUI.getInstance().getNotifier().reset();其中Notifier()为新消息提醒类,reset()方法调用了resetNotificationCount()和cancelNotificaton()。重置消息提醒数和取消提醒。 public void reset(){
resetNotificationCount();
cancelNotificaton();
}

void resetNotificationCount() {
notificationNum = 0;
fromUsers.clear();
}

void cancelNotificaton() {
if (notificationManager != null)
notificationManager.cancel(notifyID);
}耗电优化

首先判断系统版本是否大于6.0,如果是,则判断是否忽略电池耗电的优化。






说实话自己英文水平不是太好,没搞懂为毛国人写的代码要用英文注释,难道是外国人开发的?

注释本身不就是让人简单易懂代码逻辑的。可能跟这个公司大了,这个心理上有些关系吧。





确保当你在其他设备登陆或者登出的时候,界面不在后台。大概我只能翻译成这样了。

但是看代码的意思应该是当你再其他设备登陆的时候啊,你的app又在后台,那么这个时候呢,咱啊就你在当前

设备点击进来的时候,我就判断你这个saveInstanceState是不是为空。如果不为空而且得到账号已经

remove 标识位为true的话,咱就把你当前的界面结束掉。跳到登陆页面去。

否则的话,如果savedInstanceState不为空,而且得到isConflict标识位为true的话,也退出去跳到登陆页面。

权限请求

我们继续看下面的,封装了请求权限的代码。











继续,之后就是常规的界面初始化及其他设置了。






初始化界面方法

initView()

友盟的更新

没用过友盟的东西MobclickAgent.updateOnlineConfig(this);

UmengUpdateAgent.setUpdateOnlyWifi(false);

UmengUpdateAgent.update(this);看字面意思第一句应该是点击数据埋起点,后面应该是设置仅wifi更新为false以及设置更新。

异常提示

从Intent中获取的异常标志位进行一个弹窗提示






从字面上意思来看来应该是当账号冲突,移除,禁止的时候去显示异常。其中用到了showExceptionDialog()方法来显示

我们来看看一下代码






当用户遇到一些异常的时候显示对话框,例如在其他设备登陆,账号被移除或者禁止。

数据库相关操作inviteMessgeDao = new InviteMessgeDao(this);
UserDao userDao = new UserDao(this);初始化Fragment​conversationListFragment = new ConversationListFragment();
contactListFragment = new ContactListFragment();
SettingsFragment settingFragment = new SettingsFragment();
fragments = new Fragment { conversationListFragment, contactListFragment, settingFragment};

getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, conversationListFragment)
.add(R.id.fragment_container, contactListFragment).hide(contactListFragment).show(conversationListFragment)
.commit();注册广播接收者​//register broadcast receiver to receive the change of group from DemoHelper
registerBroadcastReceiver();从英文注释来看,字面意思来看是用DemoHelper来注册广播接收者来接受群变化通知。我们来看具体的代码private void registerBroadcastReceiver() {
broadcastManager = LocalBroadcastManager.getInstance(this);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Constant.ACTION_CONTACT_CHANAGED);
intentFilter.addAction(Constant.ACTION_GROUP_CHANAGED);
intentFilter.addAction(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION);
broadcastReceiver = new BroadcastReceiver() {

@Override
public void onReceive(Context context, Intent intent) {
updateUnreadLabel();
updateUnreadAddressLable();
if (currentTabIndex == 0) {
// refresh conversation list
if (conversationListFragment != null) {
conversationListFragment.refresh();
}
} else if (currentTabIndex == 1) {
if(contactListFragment != null) {
contactListFragment.refresh();
}
}
String action = intent.getAction();
if(action.equals(Constant.ACTION_GROUP_CHANAGED)){
if (EaseCommonUtils.getTopActivity(MainActivity.this).equals(GroupsActivity.class.getName())) {
GroupsActivity.instance.onResume();
}
}
//red packet code : 处理红包回执透传消息
if (action.equals(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION)){
if (conversationListFragment != null){
conversationListFragment.refresh();
}
}
//end of red packet code
}
};
broadcastManager.registerReceiver(broadcastReceiver, intentFilter);
} LocalBroadcastManager是Android Support包提供了一个工具,是用来在同一个应用内的不同组件间发送Broadcast的。

使用LocalBroadcastManager有如下好处:

发送的广播只会在自己App内传播,不会泄露给其他App,确保隐私数据不会泄露 其他App也无法向你的App发送该广播,不用担心其他App会来搞破坏 比系统全局广播更加高效
 
拦截了这么几种广播,按字面意思,应该是这么几类
Constant.ACTION_CONTACT_CHANAGED 联系人变化广播Constant.ACTION_GROUP_CHANAGED 群组变化广播RPConstant.REFRESH_GROUP_RED_PACKET_ACTION 刷新群红包广播

接受了消息了以后调用了updateUnreadLabel();和updateUnreadAddressLable();方法

未读消息数更新
/**
* update unread message count
*/
public void updateUnreadLabel() {
int count = getUnreadMsgCountTotal();
if (count > 0) {
unreadLabel.setText(String.valueOf(count));
unreadLabel.setVisibility(View.VISIBLE);
} else {
unreadLabel.setVisibility(View.INVISIBLE);
}
}更新总计未读数量 /**
* update the total unread count
*/
public void updateUnreadAddressLable() {
runOnUiThread(new Runnable() {
public void run() {
int count = getUnreadAddressCountTotal();
if (count > 0) {
unreadAddressLable.setVisibility(View.VISIBLE);
} else {
unreadAddressLable.setVisibility(View.INVISIBLE);
}
}
});

}然后判断广播类型,如果当前的栈顶为主界面,则调用GroupsActivity的onResume方法。

如果为群红包更新意图则调用的converstationListFragment的refersh()方法




添加联系人监听EMClient.getInstance().contactManager().setContactListener(new MyContactListener());我们来看下这个MyContactListener()监听方法。




我们发现是MyContactListener是继承自EMContactListener的,我们再来看看EMContactListener和其官方文档的解释。




我们发现其定义了5个接口,这5个接口根据文档释义分别是如下含义:void onContactAdded (String username)//增加联系人时回调此方法

void onContactDeleted (String username)//被删除时回调此方法

void onContactInvited (String username, String reason)/**收到好友邀请 参数 username 发起加为好友用户的名称 reason 对方发起好友邀请时发出的文字性描述*/

void onFriendRequestAccepted (String username)//对方同意好友请求

void onFriendRequestDeclined (String username)//对方拒绝好友请求从而我们得知,我们demo中的自定义监听接口在被删除回调时,做了如下操作:




如果你正在和这个删除你的人聊天就提示你这个人已把你从他好友列表里移除并且结束掉聊天界面。

测试用广播监听​//debug purpose only
registerInternalDebugReceiver();/**
* debug purpose only, you can ignore this
*/
private void registerInternalDebugReceiver() {
internalDebugReceiver = new BroadcastReceiver() {

@Override
public void onReceive(Context context, Intent intent) {
DemoHelper.getInstance().logout(false,new EMCallBack() {

@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
finish();
startActivity(new Intent(MainActivity.this, LoginActivity.class));
}
});
}

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

@Override
public void onError(int code, String message) {}
});
}
};
IntentFilter filter = new IntentFilter(getPackageName() + ".em_internal_debug");
registerReceiver(internalDebugReceiver, filter);
}至此MainActivity的OnCreate方法中所有涉及到的代码我们均已看完。
其他方法

接下来我们来捡漏,看看还有剩余哪些方法没有去看。




判断当前账号是否移除/**
* check if current user account was remove
*/
public boolean getCurrentAccountRemoved() {
return isCurrentAccountRemoved;
}oncreate()

requestPermission()

initView()

界面切换方法/**
* on tab clicked
*
* @param view
*/
public void onTabClicked(View view) {
switch (view.getId()) {
case R.id.btn_conversation:
index = 0;
break;
case R.id.btn_address_list:
index = 1;
break;
case R.id.btn_setting:
index = 2;
break;
}
if (currentTabIndex != index) {
FragmentTransaction trx = getSupportFragmentManager().beginTransaction();
trx.hide(fragments[currentTabIndex]);
if (!fragments[index].isAdded()) {
trx.add(R.id.fragment_container, fragments[index]);
}
trx.show(fragments[index]).commit();
}
mTabs[currentTabIndex].setSelected(false);
// set current tab selected
mTabs[index].setSelected(true);
currentTabIndex = index;
}消息刷新private void refreshUIWithMessage() {
runOnUiThread(new Runnable() {
public void run() {
// refresh unread count
updateUnreadLabel();
if (currentTabIndex == 0) {
// refresh conversation list
if (conversationListFragment != null) {
conversationListFragment.refresh();
}
}
}
});
}registerBroadcastReceiver()

unregisterBroadcastReceiver();反注册广播接收者。
private void unregisterBroadcastReceiver(){
broadcastManager.unregisterReceiver(broadcastReceiver);
}onDestory()@Override
protected void onDestroy() {
super.onDestroy();

if (exceptionBuilder != null) {
exceptionBuilder.create().dismiss();
exceptionBuilder = null;
isExceptionDialogShow = false;
}
unregisterBroadcastReceiver();

try {
unregisterReceiver(internalDebugReceiver);
} catch (Exception e) {
}

}异常的弹窗disimiss及置空,反注册广播接收者。

updateUnreadAddressLable()

getUnreadAddressCountTotal()

getUnreadMsgCountTotal()

getExceptionMessageId() 判断异常的种类
private int getExceptionMessageId(String exceptionType) {
if(exceptionType.equals(Constant.ACCOUNT_CONFLICT)) {
return R.string.connect_conflict;
} else if (exceptionType.equals(Constant.ACCOUNT_REMOVED)) {
return R.string.em_user_remove;
} else if (exceptionType.equals(Constant.ACCOUNT_FORBIDDEN)) {
return R.string.user_forbidden;
}
return R.string.Network_error;
}showExceptionDialog()

getUnreadAddressCountTotal()

getUnreadMsgCountTotal()

onResume() 中做了一些例如更新未读应用事件消息,并且push当前Activity到easui的ActivityList中
@Override
protected void onResume() {
super.onResume();

if (!isConflict && !isCurrentAccountRemoved) {
updateUnreadLabel();
updateUnreadAddressLable();
}

// unregister this event listener when this activity enters the
// background
DemoHelper sdkHelper = DemoHelper.getInstance();
sdkHelper.pushActivity(this);

EMClient.getInstance().chatManager().addMessageListener(messageListener);
}onStop();@Override
protected void onStop() {
EMClient.getInstance().chatManager().removeMessageListener(messageListener);
DemoHelper sdkHelper = DemoHelper.getInstance();
sdkHelper.popActivity(this);

super.onStop();
}做了一些销毁的活。

onSaveInstanceState@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putBoolean("isConflict", isConflict);
outState.putBoolean(Constant.ACCOUNT_REMOVED, isCurrentAccountRemoved);
super.onSaveInstanceState(outState);
}存一下冲突和账户移除的标志位

onKeyDown();判断按了回退的时候。 moveTaskToBack(false);仅当前Activity为task根时,将activity退到后台而非finish();@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
moveTaskToBack(false);
return true;
}
return super.onKeyDown(keyCode, event);
}getExceptionMessageId()

showExceptionDialog()

showExceptionDialogFromIntent()

onNewIntent() Activity在singleTask模式下重用该实例,onNewIntent()->onResart()->onStart()->onResume()这么个顺序原地复活。

至此,我们的MainActivity就全部阅读完毕了。

我们是在已经登录的情况下来到的MainActivity,那么我们在没有登录情况下呢,当然是来登陆页面。下面我们来看登录页。
 
环信官方Demo源码分析及SDK简单应用-LoginActivity 查看全部
环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面

环信官方Demo源码分析及SDK简单应用-EaseUI
 
环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现

ChatDemoUI3.0
代码结构及逻辑分析

既然上面提到首先分析ChatDemoUI 3.0,那么我们来看看其目录结构
001.jpg


mainfests 清单文件我们稍后来看具体内容

java 具体的代码部分,其包名为com.hyphenate.chatuidemo.

有如下子包:
  • adapter 适配器
  • db 数据库相关
  • domain 实体相关
  • parse 第三方库 parse(用于存储 Demo 中用户的信息)管理包
  • receiver 广播接收者
  • runtimepermissions 运行时权限相关
  • ui 界面部分
  • utils 工具类
  • video.util 视频录制工具包
  • widget 自定义view


另有如下单独非子包类:
  • Constant 常量类
  • DemoApplication application
  • DemoHelper Demo的帮助类
  • DemoModel 逻辑相关类


其中主要类有这么几个
  • DemoApplication:继承于系统的 Application 类,其 onCreate() 为整个程序的入口,相关的初始化操作都在这里面;
  • DemoHelper: Demo 全局帮助类,主要功能为初始化 EaseUI、环信 SDK 及 Demo 相关的实例,以及封装一些全局使用的方法;
  • MainActivity: 主页面,包含会话列表页面(ConversationListFragment)、联系人列表页(ContactListFragment)、设置页面(SettingsFragment),前两个继承自己 EaseUI 中的 fragment;
  • ChatActivity: 会话页面,这个类代码很少,主要原因是大部分逻辑写在 ChatFragment 中。ChatFragment 继承自 EaseChatFragment,做成 fragment 的好处在于用起来更灵活,可以单独作为一个页面使用,也可以和其他 fragment 一起放到一个 Activity 中;
  • GroupDetailsActivity: 群组详情页面


我们通过代码结构能得到什么信息?可能你会有以下的比较直观的感受。 ​
  • 分包挺清晰
  • 抓住了DemoHelper和DemoModel也就抓住了整个的纲领
  • 其他的你就自己扯吧。


废话不多说,我们来看代码。

我们的阅读的顺序是这样的
  • AndroidMainfest.xml
  • DemoApplication
  • SplashActivity
  • 各流程类


AndroidMainfest.xml
实际上没什么好说的,不过我们还是细细的研究一番
002.jpg

解决sdk定义版本声明的问题,我们在后面如果使用到了红包的ui,出现了一些sdk的错误可以加上。
003.jpg

SDK常见的一大坨权限。其中Google Cloud Messaging还是别用吧,身在何处,能稳定么?

然后就是各种各样的界面声明

总共这么些个界面(Tips:由于本文是现阅读现写,所有未中文指出部分,后面代码阅读会去补上):
  1. 开屏页
  2. 登陆页
  3. 注册
  4. 聊天
  5. 添加好友
  6. 群组邀请
  7. 群组列表
  8. 聊天室详情
  9. 新建群组
  10. 退出群组提示框
  11. 群组选人
  12. PickAtUserActivity
  13. 地图
  14. 新的朋友邀请消息页面
  15. 转发消息用户列表页面
  16. 自定义的contextmenu
  17. 显示下载大图页面
  18. 下载文件
  19. 黑名单
  20. 公开的群聊列表
  21. PublicChatRoomsActivity
  22. 语音通话
  23. 视频通话
  24. 群聊简单信息
  25. 群组黑名单用户列表
  26. GroupBlacklistActivity
  27. GroupSearchMessageActivity
  28. PublicGroupsSeachActivity
  29. EditActivity
  30. EaseShowVideoActivity
  31. ImageGridActivity
  32. RecorderVideoActivity
  33. DiagnoseActivity
  34. OfflinePushNickActivity
  35. robots list
  36. RobotsActivity
  37. UserProfileActivity
  38. SetServersActivity
  39. OfflinePushSettingsActivity
  40. CallOptionActivity
  41. 发红包
  42. 红包详情
  43. 红包记录
  44. WebView
  45. 零钱
  46. 绑定银行卡
  47. 群成员列表
  48. 支付宝h5支付页面
  49. 转账页面
  50. 转账详情页面

再往下就是相关的一些广播接收者,服务,以及杂七杂八的东西了。有如下部分:开机自启动
  1. GCM
  2. 小米推送
  3. 华为推送
  4. 友盟
  5. EMChat服务
  6. EMJob服务
  7. EMMonitor Receiver
  8. 百度地图服务

其中比较重要的
 <!-- 设置环信应用的appkey -->
<meta-data
android:name="EASEMOB_APPKEY"
android:value="你自己的环信Key" />
这样,我们基本AndroidMainfest就阅读完了。因为Androidmainfest.xml指出主Activity为ui包下的SplashActivity。按理说我们应该接着来看SplashActivity。但是别忘了App启动后DemoApplication是在主界面之前的。我们将在阅读完Application后再来看SplashActivity。

DemoApplication

上代码:
/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hyphenate.chatuidemo;

import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDex;

import com.easemob.redpacketsdk.RedPacket;

public class DemoApplication extends Application {

public static Context applicationContext;
private static DemoApplication instance;
// login user name
public final String PREF_USERNAME = "username";

/**
* nickname for current user, the nickname instead of ID be shown when user receive notification from APNs
*/
public static String currentUserNick = "";

@Override
public void onCreate() {
MultiDex.install(this);
super.onCreate();
applicationContext = this;
instance = this;

//init demo helper
DemoHelper.getInstance().init(applicationContext);
//red packet code : 初始化红包上下文,开启日志输出开关
RedPacket.getInstance().initContext(applicationContext);
RedPacket.getInstance().setDebugMode(true);
//end of red packet code
}

public static DemoApplication getInstance() {
return instance;
}

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
第一句是分包,我们知道分包有以下两种方式:
  1. 项目中的Application类继承MultiDexApplication。
  2. 在自己的Application类的attachBaseContext方法中调用MultiDex.install(this);。

然后又做了几件事
  1. 初始化DemoHelper
  2. 初始化红包并开启日志输出

Application就这样没了,我们继续看SplashActivity。
SplashActivity
我们来看代码:
 
package com.hyphenate.chatuidemo.ui;

import android.content.Intent;
import android.os.Bundle;
import android.view.animation.AlphaAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.hyphenate.chat.EMClient;
import com.hyphenate.chatuidemo.DemoHelper;
import com.hyphenate.chatuidemo.R;
import com.hyphenate.util.EasyUtils;

/**
* 开屏页
*
*/
public class SplashActivity extends BaseActivity {

private static final int sleepTime = 2000;

@Override
protected void onCreate(Bundle arg0) {
setContentView(R.layout.em_activity_splash);
super.onCreate(arg0);

RelativeLayout rootLayout = (RelativeLayout) findViewById(R.id.splash_root);
TextView versionText = (TextView) findViewById(R.id.tv_version);

versionText.setText(getVersion());
AlphaAnimation animation = new AlphaAnimation(0.3f, 1.0f);
animation.setDuration(1500);
rootLayout.startAnimation(animation);
}

@Override
protected void onStart() {
super.onStart();

new Thread(new Runnable() {
public void run() {
if (DemoHelper.getInstance().isLoggedIn()) {
// auto login mode, make sure all group and conversation is loaed before enter the main screen
long start = System.currentTimeMillis();
EMClient.getInstance().chatManager().loadAllConversations();
EMClient.getInstance().groupManager().loadAllGroups();
long costTime = System.currentTimeMillis() - start;
//wait
if (sleepTime - costTime > 0) {
try {
Thread.sleep(sleepTime - costTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String topActivityName = EasyUtils.getTopActivityName(EMClient.getInstance().getContext());
if (topActivityName != null && (topActivityName.equals(VideoCallActivity.class.getName()) || topActivityName.equals(VoiceCallActivity.class.getName()))) {
// nop
// avoid main screen overlap Calling Activity
} else {
//enter main screen
startActivity(new Intent(SplashActivity.this, MainActivity.class));
}
finish();
}else {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
startActivity(new Intent(SplashActivity.this, LoginActivity.class));
finish();
}
}
}).start();

}

/**
* get sdk version
*/
private String getVersion() {
return EMClient.getInstance().VERSION;
}
}
UI部分我们不关心,我们来看下代码逻辑部分
 
new Thread(new Runnable() {
public void run() {
if (DemoHelper.getInstance().isLoggedIn()) {
// auto login mode, make sure all group and conversation is loaed before enter the main screen
long start = System.currentTimeMillis();
EMClient.getInstance().chatManager().loadAllConversations();
EMClient.getInstance().groupManager().loadAllGroups();
long costTime = System.currentTimeMillis() - start;
//wait
if (sleepTime - costTime > 0) {
try {
Thread.sleep(sleepTime - costTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String topActivityName = EasyUtils.getTopActivityName(EMClient.getInstance().getContext());
if (topActivityName != null && (topActivityName.equals(VideoCallActivity.class.getName()) || topActivityName.equals(VoiceCallActivity.class.getName()))) {
// nop
// avoid main screen overlap Calling Activity
} else {
//enter main screen
startActivity(new Intent(SplashActivity.this, MainActivity.class));
}
finish();
}else {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
startActivity(new Intent(SplashActivity.this, LoginActivity.class));
finish();
}
}
}).start();
在这里,我们看到了这个DemoHelper帮助类,起了个线程,判断是否已经登录。我们来看看他是如何判断的。

005.jpg

我们来看官方文档中关于此isLoggedInBefore()的解释。
006.jpg


我们再回头来看刚才的代码,代码中有句注释,是这么写到。
// auto login mode, make sure all group and conversation is loaed before enter the main screen
自动登录模式,请确保进入主页面后本地回话和群组都load完毕。

那么代码中有两句话就是干这个事情的
EMClient.getInstance().chatManager().loadAllConversations();
EMClient.getInstance().groupManager().loadAllGroups();
这里部分代码最好是放在SplashActivity因为如果登录过,APP 长期在后台再进的时候也可能会导致加载到内存的群组和会话为空。

007.jpg


这里做了等待和判断

如果栈顶的ActivityName不为空而且顶栈的名字为语音通话的Activity或者栈顶的名字等于语音通话的Activity。毛线都不做。这个地方猜测应该是指语音通话挂起,重新调起界面的过程。

否则,跳到主界面。

那么我们接着看主界面。

MainActivity

那么这个时候,我们应该怎样去看主界面的代码呢?

首先看Demo的界面,然后看代码的方法,再一一对应。

来,我们来看界面,界面是这个样子的。

008.jpg


三个界面

会话、通讯录、设置

有了直观的认识以后,我们再来看代码。
 
我们来一段一段看代码

BaseActivity

MainActivity继承自BaseActivity。
/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hyphenate.chatuidemo.ui;

import android.annotation.SuppressLint;
import android.os.Bundle;
import com.hyphenate.easeui.ui.EaseBaseActivity;
import com.umeng.analytics.MobclickAgent;

@SuppressLint("Registered")
public class BaseActivity extends EaseBaseActivity {

@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
}

@Override
protected void onResume() {
super.onResume();
// umeng
MobclickAgent.onResume(this);
}

@Override
protected void onStart() {
super.onStart();
// umeng
MobclickAgent.onPause(this);
}

}
只有友盟的一些数据埋点,我们继续往上挖看他爹。

EaseBaseActivity
/**
* Copyright (C) 2016 Hyphenate Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hyphenate.easeui.ui;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;

import com.hyphenate.easeui.controller.EaseUI;

@SuppressLint({"NewApi", "Registered"})
public class EaseBaseActivity extends FragmentActivity {

protected InputMethodManager inputMethodManager;

@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
//http://stackoverflow.com/quest ... ffer/
// should be in launcher activity, but all app use this can avoid the problem
if(!isTaskRoot()){
Intent intent = getIntent();
String action = intent.getAction();
if(intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action.equals(Intent.ACTION_MAIN)){
finish();
return;
}
}

inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
}


@Override
protected void onResume() {
super.onResume();
// cancel the notification
EaseUI.getInstance().getNotifier().reset();
}

protected void hideSoftKeyboard() {
if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getCurrentFocus() != null)
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),
InputMethodManager.HIDE_NOT_ALWAYS);
}
}

/**
* back
*
* @param view
*/
public void back(View view) {
finish();
}
}

009.jpg


这段代码其实是用来解决重复实例化Launch Activity的问题。喜欢打破砂锅问到底的,可以自己去google。

至于hideSoftKeyboard则是常见的隐藏软键盘

其中有一句
 EaseUI.getInstance().getNotifier().reset();
其中Notifier()为新消息提醒类,reset()方法调用了resetNotificationCount()和cancelNotificaton()。重置消息提醒数和取消提醒。
 public void reset(){
resetNotificationCount();
cancelNotificaton();
}

void resetNotificationCount() {
notificationNum = 0;
fromUsers.clear();
}

void cancelNotificaton() {
if (notificationManager != null)
notificationManager.cancel(notifyID);
}
耗电优化

首先判断系统版本是否大于6.0,如果是,则判断是否忽略电池耗电的优化。

010.jpg


说实话自己英文水平不是太好,没搞懂为毛国人写的代码要用英文注释,难道是外国人开发的?

注释本身不就是让人简单易懂代码逻辑的。可能跟这个公司大了,这个心理上有些关系吧。

011.jpg

确保当你在其他设备登陆或者登出的时候,界面不在后台。大概我只能翻译成这样了。

但是看代码的意思应该是当你再其他设备登陆的时候啊,你的app又在后台,那么这个时候呢,咱啊就你在当前

设备点击进来的时候,我就判断你这个saveInstanceState是不是为空。如果不为空而且得到账号已经

remove 标识位为true的话,咱就把你当前的界面结束掉。跳到登陆页面去。

否则的话,如果savedInstanceState不为空,而且得到isConflict标识位为true的话,也退出去跳到登陆页面。

权限请求

我们继续看下面的,封装了请求权限的代码。

012.jpg


013.jpg


继续,之后就是常规的界面初始化及其他设置了。

014.jpg


初始化界面方法

initView()

友盟的更新


没用过友盟的东西
MobclickAgent.updateOnlineConfig(this);

UmengUpdateAgent.setUpdateOnlyWifi(false);

UmengUpdateAgent.update(this);
看字面意思第一句应该是点击数据埋起点,后面应该是设置仅wifi更新为false以及设置更新。

异常提示

从Intent中获取的异常标志位进行一个弹窗提示

015.jpg


从字面上意思来看来应该是当账号冲突,移除,禁止的时候去显示异常。其中用到了showExceptionDialog()方法来显示

我们来看看一下代码

016.jpg


当用户遇到一些异常的时候显示对话框,例如在其他设备登陆,账号被移除或者禁止。

数据库相关操作
inviteMessgeDao = new InviteMessgeDao(this);
UserDao userDao = new UserDao(this);
初始化Fragment​
conversationListFragment = new ConversationListFragment();
contactListFragment = new ContactListFragment();
SettingsFragment settingFragment = new SettingsFragment();
fragments = new Fragment { conversationListFragment, contactListFragment, settingFragment};

getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, conversationListFragment)
.add(R.id.fragment_container, contactListFragment).hide(contactListFragment).show(conversationListFragment)
.commit();
注册广播接收者​
//register broadcast receiver to receive the change of group from DemoHelper
registerBroadcastReceiver();
从英文注释来看,字面意思来看是用DemoHelper来注册广播接收者来接受群变化通知。我们来看具体的代码
private void registerBroadcastReceiver() {
broadcastManager = LocalBroadcastManager.getInstance(this);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Constant.ACTION_CONTACT_CHANAGED);
intentFilter.addAction(Constant.ACTION_GROUP_CHANAGED);
intentFilter.addAction(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION);
broadcastReceiver = new BroadcastReceiver() {

@Override
public void onReceive(Context context, Intent intent) {
updateUnreadLabel();
updateUnreadAddressLable();
if (currentTabIndex == 0) {
// refresh conversation list
if (conversationListFragment != null) {
conversationListFragment.refresh();
}
} else if (currentTabIndex == 1) {
if(contactListFragment != null) {
contactListFragment.refresh();
}
}
String action = intent.getAction();
if(action.equals(Constant.ACTION_GROUP_CHANAGED)){
if (EaseCommonUtils.getTopActivity(MainActivity.this).equals(GroupsActivity.class.getName())) {
GroupsActivity.instance.onResume();
}
}
//red packet code : 处理红包回执透传消息
if (action.equals(RPConstant.REFRESH_GROUP_RED_PACKET_ACTION)){
if (conversationListFragment != null){
conversationListFragment.refresh();
}
}
//end of red packet code
}
};
broadcastManager.registerReceiver(broadcastReceiver, intentFilter);
}
LocalBroadcastManager是Android Support包提供了一个工具,是用来在同一个应用内的不同组件间发送Broadcast的。

使用LocalBroadcastManager有如下好处:

发送的广播只会在自己App内传播,不会泄露给其他App,确保隐私数据不会泄露 其他App也无法向你的App发送该广播,不用担心其他App会来搞破坏 比系统全局广播更加高效
 
拦截了这么几种广播,按字面意思,应该是这么几类
  • Constant.ACTION_CONTACT_CHANAGED 联系人变化广播
  • Constant.ACTION_GROUP_CHANAGED 群组变化广播
  • RPConstant.REFRESH_GROUP_RED_PACKET_ACTION 刷新群红包广播


接受了消息了以后调用了updateUnreadLabel();和updateUnreadAddressLable();方法

未读消息数更新
 
/**
* update unread message count
*/
public void updateUnreadLabel() {
int count = getUnreadMsgCountTotal();
if (count > 0) {
unreadLabel.setText(String.valueOf(count));
unreadLabel.setVisibility(View.VISIBLE);
} else {
unreadLabel.setVisibility(View.INVISIBLE);
}
}
更新总计未读数量
 /**
* update the total unread count
*/
public void updateUnreadAddressLable() {
runOnUiThread(new Runnable() {
public void run() {
int count = getUnreadAddressCountTotal();
if (count > 0) {
unreadAddressLable.setVisibility(View.VISIBLE);
} else {
unreadAddressLable.setVisibility(View.INVISIBLE);
}
}
});

}
然后判断广播类型,如果当前的栈顶为主界面,则调用GroupsActivity的onResume方法。

如果为群红包更新意图则调用的converstationListFragment的refersh()方法
017.jpg

添加联系人监听
EMClient.getInstance().contactManager().setContactListener(new MyContactListener());
我们来看下这个MyContactListener()监听方法。
018.jpg

我们发现是MyContactListener是继承自EMContactListener的,我们再来看看EMContactListener和其官方文档的解释。
019.jpg

我们发现其定义了5个接口,这5个接口根据文档释义分别是如下含义:
void    onContactAdded (String username)//增加联系人时回调此方法

void onContactDeleted (String username)//被删除时回调此方法

void onContactInvited (String username, String reason)/**收到好友邀请 参数 username 发起加为好友用户的名称 reason 对方发起好友邀请时发出的文字性描述*/

void onFriendRequestAccepted (String username)//对方同意好友请求

void onFriendRequestDeclined (String username)//对方拒绝好友请求
从而我们得知,我们demo中的自定义监听接口在被删除回调时,做了如下操作:
020.jpg

如果你正在和这个删除你的人聊天就提示你这个人已把你从他好友列表里移除并且结束掉聊天界面。

测试用广播监听​
//debug purpose only
registerInternalDebugReceiver();
/**
* debug purpose only, you can ignore this
*/
private void registerInternalDebugReceiver() {
internalDebugReceiver = new BroadcastReceiver() {

@Override
public void onReceive(Context context, Intent intent) {
DemoHelper.getInstance().logout(false,new EMCallBack() {

@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
finish();
startActivity(new Intent(MainActivity.this, LoginActivity.class));
}
});
}

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

@Override
public void onError(int code, String message) {}
});
}
};
IntentFilter filter = new IntentFilter(getPackageName() + ".em_internal_debug");
registerReceiver(internalDebugReceiver, filter);
}
至此MainActivity的OnCreate方法中所有涉及到的代码我们均已看完。
其他方法

接下来我们来捡漏,看看还有剩余哪些方法没有去看。
021.jpg

判断当前账号是否移除
/**
* check if current user account was remove
*/
public boolean getCurrentAccountRemoved() {
return isCurrentAccountRemoved;
}
oncreate()

requestPermission()

initView()

界面切换方法
/**
* on tab clicked
*
* @param view
*/
public void onTabClicked(View view) {
switch (view.getId()) {
case R.id.btn_conversation:
index = 0;
break;
case R.id.btn_address_list:
index = 1;
break;
case R.id.btn_setting:
index = 2;
break;
}
if (currentTabIndex != index) {
FragmentTransaction trx = getSupportFragmentManager().beginTransaction();
trx.hide(fragments[currentTabIndex]);
if (!fragments[index].isAdded()) {
trx.add(R.id.fragment_container, fragments[index]);
}
trx.show(fragments[index]).commit();
}
mTabs[currentTabIndex].setSelected(false);
// set current tab selected
mTabs[index].setSelected(true);
currentTabIndex = index;
}
消息刷新
private void refreshUIWithMessage() {
runOnUiThread(new Runnable() {
public void run() {
// refresh unread count
updateUnreadLabel();
if (currentTabIndex == 0) {
// refresh conversation list
if (conversationListFragment != null) {
conversationListFragment.refresh();
}
}
}
});
}
registerBroadcastReceiver()

unregisterBroadcastReceiver();反注册广播接收者。
 
private void unregisterBroadcastReceiver(){
broadcastManager.unregisterReceiver(broadcastReceiver);
}
onDestory()
@Override
protected void onDestroy() {
super.onDestroy();

if (exceptionBuilder != null) {
exceptionBuilder.create().dismiss();
exceptionBuilder = null;
isExceptionDialogShow = false;
}
unregisterBroadcastReceiver();

try {
unregisterReceiver(internalDebugReceiver);
} catch (Exception e) {
}

}
异常的弹窗disimiss及置空,反注册广播接收者。

updateUnreadAddressLable()

getUnreadAddressCountTotal()

getUnreadMsgCountTotal()

getExceptionMessageId() 判断异常的种类
 
private int getExceptionMessageId(String exceptionType) {
if(exceptionType.equals(Constant.ACCOUNT_CONFLICT)) {
return R.string.connect_conflict;
} else if (exceptionType.equals(Constant.ACCOUNT_REMOVED)) {
return R.string.em_user_remove;
} else if (exceptionType.equals(Constant.ACCOUNT_FORBIDDEN)) {
return R.string.user_forbidden;
}
return R.string.Network_error;
}
showExceptionDialog()

getUnreadAddressCountTotal()

getUnreadMsgCountTotal()

onResume() 中做了一些例如更新未读应用事件消息,并且push当前Activity到easui的ActivityList中
 
@Override
protected void onResume() {
super.onResume();

if (!isConflict && !isCurrentAccountRemoved) {
updateUnreadLabel();
updateUnreadAddressLable();
}

// unregister this event listener when this activity enters the
// background
DemoHelper sdkHelper = DemoHelper.getInstance();
sdkHelper.pushActivity(this);

EMClient.getInstance().chatManager().addMessageListener(messageListener);
}
onStop();
@Override
protected void onStop() {
EMClient.getInstance().chatManager().removeMessageListener(messageListener);
DemoHelper sdkHelper = DemoHelper.getInstance();
sdkHelper.popActivity(this);

super.onStop();
}
做了一些销毁的活。

onSaveInstanceState
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putBoolean("isConflict", isConflict);
outState.putBoolean(Constant.ACCOUNT_REMOVED, isCurrentAccountRemoved);
super.onSaveInstanceState(outState);
}
存一下冲突和账户移除的标志位

onKeyDown();判断按了回退的时候。 moveTaskToBack(false);仅当前Activity为task根时,将activity退到后台而非finish();
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
moveTaskToBack(false);
return true;
}
return super.onKeyDown(keyCode, event);
}
getExceptionMessageId()

showExceptionDialog()

showExceptionDialogFromIntent()

onNewIntent() Activity在singleTask模式下重用该实例,onNewIntent()->onResart()->onStart()->onResume()这么个顺序原地复活。

至此,我们的MainActivity就全部阅读完毕了。

我们是在已经登录的情况下来到的MainActivity,那么我们在没有登录情况下呢,当然是来登陆页面。下面我们来看登录页。
 
环信官方Demo源码分析及SDK简单应用-LoginActivity
4
评论

Android 依赖EaseUI联系人列表显示昵称 修改之前的发起的那篇文章 Android EaseUI 昵称 昵称头像

LoneWolf 发表了文章 • 518 次浏览 • 2017-02-17 00:54 • 来自相关话题

在设置时要说明 好友数据由app的服务器提供的  所以服务端也要集成
注意 我的页面以及类都是从Demo中复制过来的
我们必须要知道好友数据是在什么位置进行数据适配的
在UserDao中有一个方法是saveContactList这个就是进行好友数据保存的操作了

之前我自己创建了一个数据库  进行操作发现出现很多问题  修改的地方也比较多 走了很多弯路
这次经过观察  Demo已经为我们创建了数据库和表 我们只需要在正确的位置把我们获取的数据保存起来就可以了
那么我们的任务就是定位这个方法是在哪调用的,经过代码的跟踪,最终定位到这个位置在
DemoHelper中asyncFetchContactsFromServer()方法
这个方法在没有修改的情况下是从环信服务器获取的好友数据
为了方便我把代码贴出来public void asyncFetchContactsFromServer(final EMValueCallBack<List<String>> callback) {
if (isSyncingContactsWithServer) {
return;
}
isSyncingContactsWithServer = true;
new Thread() {
@Override public void run() {
List<String> usernames = null;
try {
usernames = EMClient.getInstance().contactManager().getAllContactsFromServer(); // in case that logout already before server returns, we should return immediately
if (!isLoggedIn()) {
isContactsSyncedWithServer = false;
isSyncingContactsWithServer = false;
notifyContactsSyncListener(false); return;
}

//这里就是开始从自己app的服务器获取好友数据了Map<String, EaseUser> userlist = new HashMap<String, EaseUser>();
String url = AppConfig.BASE_URL+AppConfig.GETFRIENDS;
HashMap<String, String> map = new HashMap<>(); map.put("userName",PreforenceUtils.getStringData("userInfo","hxid"));
Log.e(TAG,url);
MyHttpUtils myHttpUtils = new MyHttpUtils();
String s = myHttpUtils.httpPost(url, "", "&user", map.toString());
Log.e(TAG,s);
JSONArray jarr = new JSONArray(s);
if(jarr.length()!=0||jarr != null){
for (int i = 0; i < jarr.length(); i++) {
JSONObject jobj = (JSONObject) jarr.get(i);
EaseUser easeUser = new EaseUser(jobj.getString("FRIENDID")); easeUser.setNick(jobj.getString("FRIENDNICKNAME"));
easeUser.setAvatar("");
Log.e(TAG,easeUser.toString());
EaseCommonUtils.setUserInitialLetter(easeUser);
//这是关键的地方userlist.put(jobj.getString("FRIENDID"), easeUser);
}

//这是就是将数据转换成Easeuser对象 的原有方式 已经注释掉了 其他代码没有做修改
/*for (String username : usernames) {
EaseUser user = new EaseUser(username);
EaseCommonUtils.setUserInitialLetter(user);
userlist.put(username, user); }*/


// save the contact list to cache getContactList().clear(); getContactList().putAll(userlist); // save the contact list to database
UserDao dao = new UserDao(appContext);
List<EaseUser> users = new ArrayList<EaseUser>(userlist.values());
Log.e(TAG,"获取联系人");
//报讯联系人的数据就是在这了dao.saveContactList(users);
demoModel.setContactSynced(true);
EMLog.d(TAG, "set contact syn status to true");
isContactsSyncedWithServer = true; isSyncingContactsWithServer = false;
//notify sync success notifyContactsSyncListener(true); getUserProfileManager().asyncFetchContactInfosFromServer(usernames, new EMValueCallBack<List<EaseUser>>() {
@Override public void onSuccess(List<EaseUser> uList) {
updateContactList(uList);
getUserProfileManager().notifyContactInfosSyncListener(true);
}
@Override public void onError(int error, String errorMsg) { } });
if (callback != null) { callback.onSuccess(usernames); } } }
catch (HyphenateException e) { d
emoModel.setContactSynced(false);
isContactsSyncedWithServer = false;
isSyncingContactsWithServer = false;
notifyContactsSyncListener(false);
e.printStackTrace();
if (callback != null) {
callback.onError(e.getErrorCode(), e.toString()); } }
catch (JSONException e) { e.printStackTrace(); } } }.start(); }
以上就是我的代码了   希望有用  我已经解决昵称的问题了 至于头像也是一样的道理了
之前的文章有很多问题 这里给小伙们说声对不起了 查看全部
在设置时要说明 好友数据由app的服务器提供的  所以服务端也要集成
注意 我的页面以及类都是从Demo中复制过来的
我们必须要知道好友数据是在什么位置进行数据适配的
在UserDao中有一个方法是saveContactList这个就是进行好友数据保存的操作了

之前我自己创建了一个数据库  进行操作发现出现很多问题  修改的地方也比较多 走了很多弯路
这次经过观察  Demo已经为我们创建了数据库和表 我们只需要在正确的位置把我们获取的数据保存起来就可以了
那么我们的任务就是定位这个方法是在哪调用的,经过代码的跟踪,最终定位到这个位置在
DemoHelper中asyncFetchContactsFromServer()方法
这个方法在没有修改的情况下是从环信服务器获取的好友数据
为了方便我把代码贴出来
public void asyncFetchContactsFromServer(final EMValueCallBack<List<String>> callback) {
if (isSyncingContactsWithServer) {
return;
}
isSyncingContactsWithServer = true;
new Thread() {
@Override public void run() {
List<String> usernames = null;
try {
usernames = EMClient.getInstance().contactManager().getAllContactsFromServer(); // in case that logout already before server returns, we should return immediately
if (!isLoggedIn()) {
isContactsSyncedWithServer = false;
isSyncingContactsWithServer = false;
notifyContactsSyncListener(false); return;
}

//这里就是开始从自己app的服务器获取好友数据了
Map<String, EaseUser> userlist = new HashMap<String, EaseUser>();
String url = AppConfig.BASE_URL+AppConfig.GETFRIENDS;
HashMap<String, String> map = new HashMap<>(); map.put("userName",PreforenceUtils.getStringData("userInfo","hxid"));
Log.e(TAG,url);
MyHttpUtils myHttpUtils = new MyHttpUtils();
String s = myHttpUtils.httpPost(url, "", "&user", map.toString());
Log.e(TAG,s);
JSONArray jarr = new JSONArray(s);
if(jarr.length()!=0||jarr != null){
for (int i = 0; i < jarr.length(); i++) {
JSONObject jobj = (JSONObject) jarr.get(i);
EaseUser easeUser = new EaseUser(jobj.getString("FRIENDID")); easeUser.setNick(jobj.getString("FRIENDNICKNAME"));
easeUser.setAvatar("");
Log.e(TAG,easeUser.toString());
EaseCommonUtils.setUserInitialLetter(easeUser);

//这是关键的地方
userlist.put(jobj.getString("FRIENDID"), easeUser);
}

//这是就是将数据转换成Easeuser对象 的原有方式 已经注释掉了 其他代码没有做修改
/*for (String username : usernames) {
EaseUser user = new EaseUser(username);
EaseCommonUtils.setUserInitialLetter(user);
userlist.put(username, user); }*/


// save the contact list to cache getContactList().clear(); getContactList().putAll(userlist); // save the contact list to database
UserDao dao = new UserDao(appContext);
List<EaseUser> users = new ArrayList<EaseUser>(userlist.values());
Log.e(TAG,"获取联系人");

//报讯联系人的数据就是在这了
dao.saveContactList(users);
demoModel.setContactSynced(true);
EMLog.d(TAG, "set contact syn status to true");
isContactsSyncedWithServer = true; isSyncingContactsWithServer = false;
//notify sync success notifyContactsSyncListener(true); getUserProfileManager().asyncFetchContactInfosFromServer(usernames, new EMValueCallBack<List<EaseUser>>() {
@Override public void onSuccess(List<EaseUser> uList) {
updateContactList(uList);
getUserProfileManager().notifyContactInfosSyncListener(true);
}
@Override public void onError(int error, String errorMsg) { } });
if (callback != null) { callback.onSuccess(usernames); } } }
catch (HyphenateException e) { d
emoModel.setContactSynced(false);
isContactsSyncedWithServer = false;
isSyncingContactsWithServer = false;
notifyContactsSyncListener(false);
e.printStackTrace();
if (callback != null) {
callback.onError(e.getErrorCode(), e.toString()); } }
catch (JSONException e) { e.printStackTrace(); } } }.start(); }

以上就是我的代码了   希望有用  我已经解决昵称的问题了 至于头像也是一样的道理了
之前的文章有很多问题 这里给小伙们说声对不起了
3
评论

环信官方Demo源码分析及SDK简单应用 环信 Android 集成指南 源码

随缘 发表了文章 • 2257 次浏览 • 2017-02-13 16:15 • 来自相关话题

前言
环信官方Android版本的Demo,还算是功能齐全的.日常工作中我们如果只是为App加个im模块基本的界面和逻辑也出不了Demo多少。

所以如果你的公司有这方面的需求,为了能顺利拿到银子,少些波澜,我们还是一起来研究下其官方Demo吧。

感谢有环信这样强力的三方IM解决方案,并提供了简单易用而又强大的SDK,方便了我们广大中小开发者集成IM相关功能。

有缘的话,我们后面再来分析IOS版本的环信官方Demo源代码。

由于时间仓促,错误及不足之处,欢迎指正。
准备工作
 
我们说拿到一份代码,要想分析下内容。先看目录再看AndroidMainfest,抽丝剥茧一步步的去理解和分析。

当然这只是个人的习惯,其他有更好的方法或建议,可以留言一起讨论。

废话不多说,我们来看目录。




有三个Moudle
ChatDemoUI3.0 //主Demo模块EaseUI //UI库redpacketlibrary //红包库

那么我们首先分析哪个库呢?自然是主Demo库,单单的去分析EaseUI库,或者红包库并没有任何意义和连贯性。下面就来进入我们的环信官方Demo源码分析,在文章的最后会教大家一些SDK的简单应用,同时分享一个我做的基于环信开发项目。

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面

环信官方Demo源码分析及SDK简单应用-EaseUI

环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现 查看全部
前言
环信官方Android版本的Demo,还算是功能齐全的.日常工作中我们如果只是为App加个im模块基本的界面和逻辑也出不了Demo多少。

所以如果你的公司有这方面的需求,为了能顺利拿到银子,少些波澜,我们还是一起来研究下其官方Demo吧。

感谢有环信这样强力的三方IM解决方案,并提供了简单易用而又强大的SDK,方便了我们广大中小开发者集成IM相关功能。

有缘的话,我们后面再来分析IOS版本的环信官方Demo源代码。

由于时间仓促,错误及不足之处,欢迎指正。
准备工作
 
我们说拿到一份代码,要想分析下内容。先看目录再看AndroidMainfest,抽丝剥茧一步步的去理解和分析。

当然这只是个人的习惯,其他有更好的方法或建议,可以留言一起讨论。

废话不多说,我们来看目录。
001.jpg

有三个Moudle
  • ChatDemoUI3.0 //主Demo模块
  • EaseUI //UI库
  • redpacketlibrary //红包库


那么我们首先分析哪个库呢?自然是主Demo库,单单的去分析EaseUI库,或者红包库并没有任何意义和连贯性。下面就来进入我们的环信官方Demo源码分析,在文章的最后会教大家一些SDK的简单应用,同时分享一个我做的基于环信开发项目。

环信官方Demo源码分析及SDK简单应用

环信官方Demo源码分析及SDK简单应用-ChatDemoUI3.0

环信官方Demo源码分析及SDK简单应用-LoginActivity

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-会话界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-通讯录界面

环信官方Demo源码分析及SDK简单应用-主界面的三个fragment-设置界面

环信官方Demo源码分析及SDK简单应用-EaseUI

环信官方Demo源码分析及SDK简单应用-IM集成开发详案及具体代码实现
1
评论

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

beyond 发表了文章 • 1998 次浏览 • 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已经完成,功能很简单,如果需要添加额外功能的话,可以自行参考官网,官网给出的教程还是很不错的!

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

Android V2.3.4 已发布,客户端支持修改群描述 Android 产品快递

产品更新 发表了文章 • 576 次浏览 • 2017-01-17 16:21 • 来自相关话题

Android​ 版本:V2.3.4 2017-1-12

新功能/改进:
增加修改群描述方法EMGroupManager::changeGroupDescription()EMChat::setServerAddress()方法支持设置https地址EMContactManager增加addContactListener(EMContactListener contactListener)方法,方便app在不同类里监听好友变动

Bug Fix:
修复REST短时间内发多条相同内容的消息,客户端只显示一条的bug修复搜索有时候返回结果不对的bug修复上个版本出现的个别情况下堆栈溢出的问题
 版本历史:Android 2.X更新日志 
下载地址:SDK下载 查看全部
5815.jpg_wh860_-_副本_.jpg

Android​ 版本:V2.3.4 2017-1-12

新功能/改进:
  • 增加修改群描述方法EMGroupManager::changeGroupDescription()
  • EMChat::setServerAddress()方法支持设置https地址
  • EMContactManager增加addContactListener(EMContactListener contactListener)方法,方便app在不同类里监听好友变动


Bug Fix:
  • 修复REST短时间内发多条相同内容的消息,客户端只显示一条的bug
  • 修复搜索有时候返回结果不对的bug
  • 修复上个版本出现的个别情况下堆栈溢出的问题

 版本历史:Android 2.X更新日志 
下载地址:SDK下载
0
评论

美团热更方案ASM实践 Android 环信 热更新

beyond 发表了文章 • 1190 次浏览 • 2017-01-06 16:57 • 来自相关话题

    美团热更新的文章已经讲了,他们用的是Instant Run的方案。
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。美团方案实现的大致结构最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。
 方案选择:

  我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

tinker方案如图:




女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

女娲方案如图:




   女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

  作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

美团&&Instant Run方案

   美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

插桩

   插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
   插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

   asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

使用asm.jar把java class反编译为字节码

反编译为字节码对应的命令是java -classpath "asm-all.jar" org.jetbrains.org.objectweb.asm.util.ASMifier State.class    这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
 
插入前代码:class State {
long getIndex(int val) {
return 100;
}
}ASMifier反编译后字节码如下mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();插桩后代码:long getIndex(int a) {
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}ASMifier反编译后代码如下:mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();对于插桩程序来说,需要做的就是把差异部分插桩到代码