Android

Android

2
评论

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

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

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题
 
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...小伙伴们还有什么想知道欢迎跟帖提出。
 
4
评论

【视频教程+源码】基于环信IM做一个仿微信APP-更新ing 郭永峰 高仿微信 仿微信 环信 XMPP

郭永峰 发表了文章 • 5573 次浏览 • 2017-05-16 15:29 • 来自相关话题

我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。
    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
4月12号成立工作室,现在18号,过了一个星期一个星期录了5天的环信教程视频,我将放在网盘免费分享环信的教程视频主要是针对有开发经验者教程主要是使用环信来模仿微信来做一个即时通讯的案例课程主要是先讲socket基础 -> 环信 ->自定义协议希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解同时能希望大家在面试时,在即时通讯这块不在陌生
  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏






支付宝打赏






谢谢大家的支持,个人微信号清扫描下面张图






 
郭永峰IT交流QQ群请加:596441895 查看全部
我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。

    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
  1. 4月12号成立工作室,现在18号,过了一个星期
  2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
  3. 环信的教程视频主要是针对有开发经验者
  4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
  5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
  6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
  7. 同时能希望大家在面试时,在即时通讯这块不在陌生

  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏

微信.png


支付宝打赏

支付宝.png


谢谢大家的支持,个人微信号清扫描下面张图

个人.png


 
郭永峰IT交流QQ群请加:596441895
2
评论

【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了 环信公开课 昵称头像 环信大表哥

beyond 发表了文章 • 1599 次浏览 • 2017-05-08 11:55 • 来自相关话题

 ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫
  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
如何利用消息扩展属性显示昵称头像?如何通过APP服务器处理昵称头像的显示?昵称头像的本地缓存策略?音视频通话如何显示昵称头像?

关于环信大表哥:
   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;

先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。





环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见”
ios简版demo地址
Android简版demo地址 查看全部
 
​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
  1. 如何利用消息扩展属性显示昵称头像?
  2. 如何通过APP服务器处理昵称头像的显示?
  3. 昵称头像的本地缓存策略?
  4. 音视频通话如何显示昵称头像?


关于环信大表哥:

   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;



先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。
QQ截图20170508115114.jpg


环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见
ios简版demo地址
Android简版demo地址
3
回复

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

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

0
评论

【环信征文】Android程序员的十大转型之路 Android

东风玖哥 发表了文章 • 146 次浏览 • 2017-06-21 16:29 • 来自相关话题

IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。
 
我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减:
首先不会转型iOS,iOS和Android工程师的工作内容都是大同小异的。
其次不会转型Windows Phone,好多Andr oid程序员就是受不了产品经理唠叨:“像QQ客户端那样做成和iOS一样”才转型的,怎么会转型比Android还难做成和iOS一样的WP?
再次不会转型Windows和MacOS等桌面软件,桌面开发周期长、难度大、升级不易,这是一个已经接近穷途末路的夕阳产业。
最后不会开JavaME或者Symbian的历史倒车,除非他有本事让每个用户都买(就一个“买”字,同时包含“想买”和“买得到”的意思)停产多年的机型。
 
我观察如今的技术形势,并亲身探索了一个Android程序员转型的几个技术方向的可行性:
 
Android病毒和恶意应用
最近肆虐全世界的WannaCry让安全成了IT圈最热的话题,开发腻了善意应用的Android工程师最便捷的转型方向就是开发Android病毒和恶意应用。在4.x时代对Android对敏感权限还不是很敏感的时候,我就研究过给肉鸡伪造短信记录和让肉鸡给通讯录里所有(或特定)联系人发送短信的病毒。去年还研究过窃取友商App推送内容、强杀友商App进程、卸载友商App甚至让友商App被卸载后就再也不能在这台肉鸡上安装的恶意应用(或应用里的恶意功能)。

 
转型建议:此外锁定肉鸡里的重要文件勒索用户(Android上的WannaCry?)和窃取肉鸡用户的支付密码的实现在技术上也像强奸8岁女童一样简单,只不过事后逍遥法外很难。这个转型方向只适合拿自己的手机当肉鸡玩玩,千万不要用这些技术赚钱。

SDK
开发SDK本质上仍然在为Android应用开发软件,只是不直接开发Android应用。
 
每个Android程序员工作几年后都积累了属于自己的或大或小的类库,比如封装好的LogUtils和ToastUtils等;也都或多或少研究过常用开源框架的底层原理,比如了解Picasso和EventBus等;还应该对不开源的第三方服务有自己简单的二次封装,比如我就封装了一键实现支付宝和微信支付的moudle(免费的Ping++?)。
 
转型建议:尽管看见自己的链接出现在无数Android应用的Gradle文件的compile后面,开发了无数软件的一部分的成就感不会比开发完整的软件差。但是几乎没有老板会为了支持你开发开源软件发你工资。
 
JavaEE
Android程序员转型Java在基础知识方面是没什么难度的,毕竟语言相通,特性相似。同时每个Android程序员在大学时J2EE课程学得都不会很差,不过有些知识是该忘掉的,比如Hibernate已经落后于时代了,SpringMVC的全面使用才是Java后台的大势所趋。
 
转型建议:建议不想每天改UI的刚入行不久的Android工程师转型,我有好几个学弟就是参加工作后从Android转型Java的,他们过得都不错。很多工作年限较长的Android工程师本来就是JavaEE转型来的,就别转回去了。
 
手游
首先考虑不放弃Java语言和Android开发习惯的情况:最合适的就是能把游戏view直接插入普通layout里的AndEngine,前几年大红大紫的Flappy Bird就是用它开发的。AndEngine的开发方式和Android别无二致,且有丰富的开源demo。不过AndEngine没有官方文档,理论学习上有一定难度。我用AndEngine开发了我的毕业设计,参加工作后也用AndEngine获得了几个奖,我珍藏着一本AndEngine的非官方文档《Android游戏开发实践指南》(全新未拆封),期待着有一天能回到2014年把它送给那个买不起它的毕业生。

 
提到了AndEngine就不得不提国产AndEngine——OGEngine,它是基于AndEngine衍生的游戏引擎,有详细的纯中文文档和说汉语的技术支持杨城(笔名:小城),极适合开发Android TV游戏。OGEngine目前已停止更新,这个国产游戏引擎的悲剧在于推出时间太早,希望Android TV普及的时候卷土重来的OGEngine能让中国在游戏引擎方面领跑全世界。
 
LibGDX是一个跨平台的游戏开发框架,同样使用Java作为开发语言,前文所说的AndEngine就是基于LiBGDX实现的。LibGDX最大的优点就是极强的兼容性,不仅兼容Android和iOS,还兼容Windows、Linux、Max OS X等桌面系统。极强的兼容性还为开发提供了便利——不必打开Android模拟器,直接用电脑debug你的应用。在LibGDX和Android之间相互转型都很容易,知名的Android专家宋志辉、吴佳俊等都是从LibGDX转型Android的。
 
如果不要Java语言,那就有Cosos2d-x可供选择。《Cocos2d-x游戏开发实战精解》的作者欧桐桐(笔名:OTT)认为Android程序员一般对面向对象的知识掌握的比较全面,上手Cosos2d-x比较容易,并且Cosos2d-x是中国人维护的,文档全、资源多、教程多。OTT在得知我是和他一样的藏书人士后还特地送我一本他的大作鼓励我。

转型建议:做好心理准备,国内手游行业比普通的移动互联网行业加班更疯狂,建议刚入行没多久的Android工程师为了加班费转型,不建议30岁以上的Android工程师转型。
 
HTML5
HTML5也是Android工程师改行的好方向,HTML5在移动互联网领域应用非常广泛,比如混合开发、手机站、小游戏、微信公众号、微信小程序等。简单的手机站和对性能要求不高小游戏直接用从懒人模板(http://www.lanrenmb.com/)上找到的资源稍微修改一下即可,这里我只说说的混合开发应用和的小游戏怎么开发。
 
最著名的HTML5移动开发框架当属Facebook发布于2015年的React Native,这是一套跨平台、动态更新的 Javascript 框架,口号是“Learn once, write anywhere”。与之类似有同属舶来的PhoneGap等。
 
国产的HTML5开发框架在国内也百家争鸣,常见的有HBuilder和AppCan,二者共同特点是都为了便于新手入门制作了专用的编译器。2016年,在Qcon大会上宣布开源的Weex也异军突起,来自阿里的它因为开发的软件与原生App别无二致受到很多人的青睐。
 
开发对性能要求比较高的HTML5游戏,靠模板是不行的。2014年2月创立于北京的Egret是一套完整的HTML5游戏开发解决方案,其核心产品白鹭引擎(Egret Engine)凭借上手简便、性能强大已占据国内超七成的手机页游引擎市场份额。
 
Egret布道师徐聪(笔名:臭臭打不死人)还送我了Egret官方教程《Egret——HTML5游戏开发指南》和Egret吉祥物。
 
转型建议:一般来说,除非手机页游或商场,大多数用HTML5开发的Android应用就是胡闹。这条路线几乎是专为电商和小游戏行业准备的,如果公司有这方面的需求,Android程序员可以凭借平时自学的这方面技术完成任务。
 
VR
2015年底游戏外设王者雷蛇推出了VR游戏头显,2016年各大游戏厂商和小工作室争先恐后开发VR游戏争夺市场,开启了“中国VR元年”。虽然目前VR主要用在娱乐领域,被很多人视为玩具,但是VR所具有的价值却远远超出“玩具”的范畴。
 
前文讨论游戏引擎的时候没说Unity-3d不是疏漏,而是要把Unity-3d放在这儿谈。Unity-3d 是Unity公司开发的一个3D游戏开发工具,近年来的新版本不断加强对VR硬件系统的支持。Android程序员转型VR不仅可以实现自己从小就想让游戏跳出四角方框的梦想,还有Unity-3d所用的C#语言本来就是嚷着“我不是Java语言”的Java语言的学习优势。
 
转型建议:VR现在正是一片蓝海,只要自学能力够强,转型VR就像2015年在合肥买房一样明智。当然前提是你能找到愿意出钱的老板或投资人。
 
大数据
移动互联网时代是一个科技发达,信息流通的时代,大数据就是这个高科技时代的产物。马云曾在演讲中提到:未来的时代将不是IT时代,而是DT的时代。DT就是Data Technology(数据科技)的缩写,大数据的合理利用与否成了很多行业成败的关键。
 
移动互联网经过这些年的发展,拿O2O和当噱头已经唬不住投资人了。Hadoop也就自然而然受到了青睐,很多每4个月“生产”一批“两年经验”的“程序员”的培训机构也问我:“Android和iOS现在不吃香了,你能帮我介绍几个Hadoop讲师吗?”
 
转型建议:与转型Java后台一样,Android程序员转型Hadoop也具备语言相通,特性相似的优势。目前各大培训机构已经如蝇逐臭争相批量生产Hadoop程序员,如果你是因为陷入了他们培训的Android程序员造成的红海才转型的话,建议你不要转型,提升自己的竞争力才是王道。
 
人工智能和深度学习
前一阵子AlphaGo战胜了人类世界的围棋世界冠军柯洁,轰动了全世界。柯洁认为AlphaGo是能够打败一切的围棋上帝,这个说法我不敢苟同,毕竟它没有和“天”对弈过,但存在能“胜天半子”的人类——祁同伟。即使AlphaGo不能打败一切,也没有人有理由认为人工智能和深度学习不能成为IT届的重要发展方向。
 
TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,具备极佳的灵活性和可延展性,在和人工智能相关的领域都有广泛的应用。TensorFlow是开源的,会大大降低深度学习在各个行业中的应用难度,有远大的发展前景。
 
转型建议:尽管我坚信将来会T(ensor)F(low)的boys受女性欢迎程度不亚于TFboys,但TensorFlow暂时很不成熟,这个“将来”距今多久还是未知数。
 
Android系统
Linux作为目前大多数服务器的操作系统,学习Linux的大多数人的目的是做一个运维。然而把脑洞再开大一点的话,Android程序员精通了Linux之后可以开发一套属于自己的Android系统。《Linux大棚命令百篇》的作者吴鹏冲(笔名:Roc,和我一样也是水浒迷)和《循序渐进Linux》的作者高俊峰都送了一本自己的作品鼓励我开发属于自己的Android ORM。

这张照片摄于2016年3月30日我拿着《循序渐进Linux(第二版)》回到母校的自习室里攻读想成为像高老师一样能定制自己的Android系统的Linux专家的路上(双关)
 
转型建议:如果Android程序员准备跳槽到生产手机等搭载Android系统的硬件的厂商的话学习Linux再合适不过了,否则就只能自己刷机玩了。
 
产品经理
每个人都可能变成自己最讨厌的人,我也不例外。我从《人人都是产品经理》中学到了产品经理的情怀,还从《从点子到产品》中学到了产品经理的技术。还有幸赶上了今年3月《从点子到产品》的作者刘飞收徒。关于我转型产品经理失败的情况是一个发生在我和刘飞之间的“挖隋炀帝坟墓的开发商名叫杨勇”的故事:
2016年初,我带新人,没有收刘飞(同名学弟)为徒
2017年初,刘飞带新人,不肯收我为徒
 
转型建议:产品经理也是技术岗位,只不过写的是给人看的需求文档。如果一个Android程序员写的代码只能让电脑看懂而不能让负责维护的程序员看懂,那么就不要转型产品经理。
 
Android程序员转型机会虽然多,但不要因为看招聘网站上某个职业平均工资高就转型,随波逐流的弄潮儿必然会在浪潮之巅摔得好惨。培训机构常说“Android不吃香了,移动互联网的寒冬来了”来吸引人报名学习速成的Hadoop和TensorFlow,其实遭遇寒冬的不是某个行业,而是某些没有打好基础的人。

本文第三人称版本首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201706/543046.htm 查看全部
IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。
 
我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减:
首先不会转型iOS,iOS和Android工程师的工作内容都是大同小异的。
其次不会转型Windows Phone,好多Andr oid程序员就是受不了产品经理唠叨:“像QQ客户端那样做成和iOS一样”才转型的,怎么会转型比Android还难做成和iOS一样的WP?
再次不会转型Windows和MacOS等桌面软件,桌面开发周期长、难度大、升级不易,这是一个已经接近穷途末路的夕阳产业。
最后不会开JavaME或者Symbian的历史倒车,除非他有本事让每个用户都买(就一个“买”字,同时包含“想买”和“买得到”的意思)停产多年的机型。
 
我观察如今的技术形势,并亲身探索了一个Android程序员转型的几个技术方向的可行性:
 
Android病毒和恶意应用
最近肆虐全世界的WannaCry让安全成了IT圈最热的话题,开发腻了善意应用的Android工程师最便捷的转型方向就是开发Android病毒和恶意应用。在4.x时代对Android对敏感权限还不是很敏感的时候,我就研究过给肉鸡伪造短信记录和让肉鸡给通讯录里所有(或特定)联系人发送短信的病毒。去年还研究过窃取友商App推送内容、强杀友商App进程、卸载友商App甚至让友商App被卸载后就再也不能在这台肉鸡上安装的恶意应用(或应用里的恶意功能)。

 
转型建议:此外锁定肉鸡里的重要文件勒索用户(Android上的WannaCry?)和窃取肉鸡用户的支付密码的实现在技术上也像强奸8岁女童一样简单,只不过事后逍遥法外很难。这个转型方向只适合拿自己的手机当肉鸡玩玩,千万不要用这些技术赚钱

SDK
开发SDK本质上仍然在为Android应用开发软件,只是不直接开发Android应用。
 
每个Android程序员工作几年后都积累了属于自己的或大或小的类库,比如封装好的LogUtils和ToastUtils等;也都或多或少研究过常用开源框架的底层原理,比如了解Picasso和EventBus等;还应该对不开源的第三方服务有自己简单的二次封装,比如我就封装了一键实现支付宝和微信支付的moudle(免费的Ping++?)。
 
转型建议:尽管看见自己的链接出现在无数Android应用的Gradle文件的compile后面,开发了无数软件的一部分的成就感不会比开发完整的软件差。但是几乎没有老板会为了支持你开发开源软件发你工资。
 
JavaEE
Android程序员转型Java在基础知识方面是没什么难度的,毕竟语言相通,特性相似。同时每个Android程序员在大学时J2EE课程学得都不会很差,不过有些知识是该忘掉的,比如Hibernate已经落后于时代了,SpringMVC的全面使用才是Java后台的大势所趋。
 
转型建议:建议不想每天改UI的刚入行不久的Android工程师转型,我有好几个学弟就是参加工作后从Android转型Java的,他们过得都不错。很多工作年限较长的Android工程师本来就是JavaEE转型来的,就别转回去了。
 
手游
首先考虑不放弃Java语言和Android开发习惯的情况:最合适的就是能把游戏view直接插入普通layout里的AndEngine,前几年大红大紫的Flappy Bird就是用它开发的。AndEngine的开发方式和Android别无二致,且有丰富的开源demo。不过AndEngine没有官方文档,理论学习上有一定难度。我用AndEngine开发了我的毕业设计,参加工作后也用AndEngine获得了几个奖,我珍藏着一本AndEngine的非官方文档《Android游戏开发实践指南》(全新未拆封),期待着有一天能回到2014年把它送给那个买不起它的毕业生。

 
提到了AndEngine就不得不提国产AndEngine——OGEngine,它是基于AndEngine衍生的游戏引擎,有详细的纯中文文档和说汉语的技术支持杨城(笔名:小城),极适合开发Android TV游戏。OGEngine目前已停止更新,这个国产游戏引擎的悲剧在于推出时间太早,希望Android TV普及的时候卷土重来的OGEngine能让中国在游戏引擎方面领跑全世界。
 
LibGDX是一个跨平台的游戏开发框架,同样使用Java作为开发语言,前文所说的AndEngine就是基于LiBGDX实现的。LibGDX最大的优点就是极强的兼容性,不仅兼容Android和iOS,还兼容Windows、Linux、Max OS X等桌面系统。极强的兼容性还为开发提供了便利——不必打开Android模拟器,直接用电脑debug你的应用。在LibGDX和Android之间相互转型都很容易,知名的Android专家宋志辉、吴佳俊等都是从LibGDX转型Android的。
 
如果不要Java语言,那就有Cosos2d-x可供选择。《Cocos2d-x游戏开发实战精解》的作者欧桐桐(笔名:OTT)认为Android程序员一般对面向对象的知识掌握的比较全面,上手Cosos2d-x比较容易,并且Cosos2d-x是中国人维护的,文档全、资源多、教程多。OTT在得知我是和他一样的藏书人士后还特地送我一本他的大作鼓励我。

转型建议:做好心理准备,国内手游行业比普通的移动互联网行业加班更疯狂,建议刚入行没多久的Android工程师为了加班费转型,不建议30岁以上的Android工程师转型。
 
HTML5
HTML5也是Android工程师改行的好方向,HTML5在移动互联网领域应用非常广泛,比如混合开发、手机站、小游戏、微信公众号、微信小程序等。简单的手机站和对性能要求不高小游戏直接用从懒人模板(http://www.lanrenmb.com/)上找到的资源稍微修改一下即可,这里我只说说的混合开发应用和的小游戏怎么开发。
 
最著名的HTML5移动开发框架当属Facebook发布于2015年的React Native,这是一套跨平台、动态更新的 Javascript 框架,口号是“Learn once, write anywhere”。与之类似有同属舶来的PhoneGap等。
 
国产的HTML5开发框架在国内也百家争鸣,常见的有HBuilder和AppCan,二者共同特点是都为了便于新手入门制作了专用的编译器。2016年,在Qcon大会上宣布开源的Weex也异军突起,来自阿里的它因为开发的软件与原生App别无二致受到很多人的青睐。
 
开发对性能要求比较高的HTML5游戏,靠模板是不行的。2014年2月创立于北京的Egret是一套完整的HTML5游戏开发解决方案,其核心产品白鹭引擎(Egret Engine)凭借上手简便、性能强大已占据国内超七成的手机页游引擎市场份额。
 
Egret布道师徐聪(笔名:臭臭打不死人)还送我了Egret官方教程《Egret——HTML5游戏开发指南》和Egret吉祥物。
 
转型建议:一般来说,除非手机页游或商场,大多数用HTML5开发的Android应用就是胡闹。这条路线几乎是专为电商和小游戏行业准备的,如果公司有这方面的需求,Android程序员可以凭借平时自学的这方面技术完成任务。
 
VR
2015年底游戏外设王者雷蛇推出了VR游戏头显,2016年各大游戏厂商和小工作室争先恐后开发VR游戏争夺市场,开启了“中国VR元年”。虽然目前VR主要用在娱乐领域,被很多人视为玩具,但是VR所具有的价值却远远超出“玩具”的范畴。
 
前文讨论游戏引擎的时候没说Unity-3d不是疏漏,而是要把Unity-3d放在这儿谈。Unity-3d 是Unity公司开发的一个3D游戏开发工具,近年来的新版本不断加强对VR硬件系统的支持。Android程序员转型VR不仅可以实现自己从小就想让游戏跳出四角方框的梦想,还有Unity-3d所用的C#语言本来就是嚷着“我不是Java语言”的Java语言的学习优势。
 
转型建议:VR现在正是一片蓝海,只要自学能力够强,转型VR就像2015年在合肥买房一样明智。当然前提是你能找到愿意出钱的老板或投资人。
 
大数据
移动互联网时代是一个科技发达,信息流通的时代,大数据就是这个高科技时代的产物。马云曾在演讲中提到:未来的时代将不是IT时代,而是DT的时代。DT就是Data Technology(数据科技)的缩写,大数据的合理利用与否成了很多行业成败的关键。
 
移动互联网经过这些年的发展,拿O2O和当噱头已经唬不住投资人了。Hadoop也就自然而然受到了青睐,很多每4个月“生产”一批“两年经验”的“程序员”的培训机构也问我:“Android和iOS现在不吃香了,你能帮我介绍几个Hadoop讲师吗?”
 
转型建议:与转型Java后台一样,Android程序员转型Hadoop也具备语言相通,特性相似的优势。目前各大培训机构已经如蝇逐臭争相批量生产Hadoop程序员,如果你是因为陷入了他们培训的Android程序员造成的红海才转型的话,建议你不要转型,提升自己的竞争力才是王道。
 
人工智能和深度学习
前一阵子AlphaGo战胜了人类世界的围棋世界冠军柯洁,轰动了全世界。柯洁认为AlphaGo是能够打败一切的围棋上帝,这个说法我不敢苟同,毕竟它没有和“天”对弈过,但存在能“胜天半子”的人类——祁同伟。即使AlphaGo不能打败一切,也没有人有理由认为人工智能和深度学习不能成为IT届的重要发展方向。
 
TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,具备极佳的灵活性和可延展性,在和人工智能相关的领域都有广泛的应用。TensorFlow是开源的,会大大降低深度学习在各个行业中的应用难度,有远大的发展前景。
 
转型建议:尽管我坚信将来会T(ensor)F(low)的boys受女性欢迎程度不亚于TFboys,但TensorFlow暂时很不成熟,这个“将来”距今多久还是未知数。
 
Android系统
Linux作为目前大多数服务器的操作系统,学习Linux的大多数人的目的是做一个运维。然而把脑洞再开大一点的话,Android程序员精通了Linux之后可以开发一套属于自己的Android系统。《Linux大棚命令百篇》的作者吴鹏冲(笔名:Roc,和我一样也是水浒迷)和《循序渐进Linux》的作者高俊峰都送了一本自己的作品鼓励我开发属于自己的Android ORM。

这张照片摄于2016年3月30日我拿着《循序渐进Linux(第二版)》回到母校的自习室里攻读想成为像高老师一样能定制自己的Android系统的Linux专家的路上(双关)
 
转型建议:如果Android程序员准备跳槽到生产手机等搭载Android系统的硬件的厂商的话学习Linux再合适不过了,否则就只能自己刷机玩了。
 
产品经理
每个人都可能变成自己最讨厌的人,我也不例外。我从《人人都是产品经理》中学到了产品经理的情怀,还从《从点子到产品》中学到了产品经理的技术。还有幸赶上了今年3月《从点子到产品》的作者刘飞收徒。关于我转型产品经理失败的情况是一个发生在我和刘飞之间的“挖隋炀帝坟墓的开发商名叫杨勇”的故事:
2016年初,我带新人,没有收刘飞(同名学弟)为徒
2017年初,刘飞带新人,不肯收我为徒
 
转型建议:产品经理也是技术岗位,只不过写的是给人看的需求文档。如果一个Android程序员写的代码只能让电脑看懂而不能让负责维护的程序员看懂,那么就不要转型产品经理。
 
Android程序员转型机会虽然多,但不要因为看招聘网站上某个职业平均工资高就转型,随波逐流的弄潮儿必然会在浪潮之巅摔得好惨。培训机构常说“Android不吃香了,移动互联网的寒冬来了”来吸引人报名学习速成的Hadoop和TensorFlow,其实遭遇寒冬的不是某个行业,而是某些没有打好基础的人。

本文第三人称版本首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201706/543046.htm
1
回复

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

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

0
回复

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

回复

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

0
评论

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

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

    Android平台的进程保活,一直是Android开发者心中一道坎,想必大家都曾为这个问题和老板、客户激励讨论过,虽然大多时候你的解释都被当成了孔乙己在鲁镇的小酒馆说的“之乎者也”,不被接受成为谈资!
Android进程保活的教程随便在网上一搜,各种神乎其乎的操作都有,但我大Android日新月异的机型还有国内各家rom大大,可以负责的讲,至今那些保活的教程没有一个靠谱的!
  同为创业公司,我很能理解各位老板们保活的需求,但同时作为Android开发者也有责任去维护Android的生态环境。现在很多Android开发工程师,主力机居然是iPhone而不是Android设备,感到相当悲哀。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在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 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;

结语

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

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是 查看全部
    Android平台的进程保活,一直是Android开发者心中一道坎,想必大家都曾为这个问题和老板、客户激励讨论过,虽然大多时候你的解释都被当成了孔乙己在鲁镇的小酒馆说的“之乎者也”,不被接受成为谈资!
Android进程保活的教程随便在网上一搜,各种神乎其乎的操作都有,但我大Android日新月异的机型还有国内各家rom大大,可以负责的讲,至今那些保活的教程没有一个靠谱的!
  同为创业公司,我很能理解各位老板们保活的需求,但同时作为Android开发者也有责任去维护Android的生态环境。现在很多Android开发工程师,主力机居然是iPhone而不是Android设备,感到相当悲哀。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在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 人关注 • 91 次浏览 • 2017-06-15 14:34 • 来自相关话题

1
评论

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

beyond 发表了文章 • 289 次浏览 • 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 人关注 • 62 次浏览 • 2017-06-02 17:25 • 来自相关话题

0
回复

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

回复

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

0
评论

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

Style℃ 发表了文章 • 97 次浏览 • 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 人关注 • 113 次浏览 • 2017-05-24 10:40 • 来自相关话题

1
回复

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

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

6
最佳

继承了EaseUI,为什么对方发送消息或者系统发送消息顶部状态栏收不到广播通知? 有专职工程师值守 Android 环信_Android

kevinhe 回复了问题 • 4 人关注 • 359 次浏览 • 2017-05-17 14:31 • 来自相关话题

1
评论

拍照闪退 Android

gaomode 发表了文章 • 88 次浏览 • 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
1
回复

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

geri_yang 回复了问题 • 2 人关注 • 127 次浏览 • 2017-05-15 13:04 • 来自相关话题

2
回复

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

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

2
最佳

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

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

1
最佳

退出登录后,再次登录另外的用户,结果聊天列表全是上一个用户的列表信息 有专职工程师值守 Android 环信_Android

baoshu 回复了问题 • 2 人关注 • 107 次浏览 • 2017-05-08 15:37 • 来自相关话题

1
评论

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

lzan13 发表了文章 • 1514 次浏览 • 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文档
环信推送相关文档
3
最佳

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

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

1
最佳

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

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

1
最佳

进入群聊页面闪退 有专职工程师值守 Android 环信_Android

回复

gaomode 回复了问题 • 1 人关注 • 172 次浏览 • 2017-04-26 18:24 • 来自相关话题

1
最佳

无法实时获取对方发送过来的消息 有专职工程师值守 Android 环信_Android

Wxin 回复了问题 • 2 人关注 • 162 次浏览 • 2017-04-25 18:57 • 来自相关话题

2
回复

请问EaseUI_CN中,聊天的内容在哪个TextView里面哪? EaseUI_CN 发送信息 Android

baoshu 回复了问题 • 2 人关注 • 156 次浏览 • 2017-04-25 17:18 • 来自相关话题

1
回复

发消息不出去 有专职工程师值守 环信_Android Android

Wxin 回复了问题 • 2 人关注 • 137 次浏览 • 2017-04-24 23:10 • 来自相关话题

条新动态, 点击查看
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无法接收到信息

赞同来自:

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

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

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

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题
 
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...小伙伴们还有什么想知道欢迎跟帖提出。
 
4
评论

【视频教程+源码】基于环信IM做一个仿微信APP-更新ing 郭永峰 高仿微信 仿微信 环信 XMPP

郭永峰 发表了文章 • 5573 次浏览 • 2017-05-16 15:29 • 来自相关话题

我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。
    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
4月12号成立工作室,现在18号,过了一个星期一个星期录了5天的环信教程视频,我将放在网盘免费分享环信的教程视频主要是针对有开发经验者教程主要是使用环信来模仿微信来做一个即时通讯的案例课程主要是先讲socket基础 -> 环信 ->自定义协议希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解同时能希望大家在面试时,在即时通讯这块不在陌生
  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏






支付宝打赏






谢谢大家的支持,个人微信号清扫描下面张图






 
郭永峰IT交流QQ群请加:596441895 查看全部
我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。

    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
  1. 4月12号成立工作室,现在18号,过了一个星期
  2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
  3. 环信的教程视频主要是针对有开发经验者
  4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
  5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
  6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
  7. 同时能希望大家在面试时,在即时通讯这块不在陌生

  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏

微信.png


支付宝打赏

支付宝.png


谢谢大家的支持,个人微信号清扫描下面张图

个人.png


 
郭永峰IT交流QQ群请加:596441895
2
评论

【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了 环信公开课 昵称头像 环信大表哥

beyond 发表了文章 • 1599 次浏览 • 2017-05-08 11:55 • 来自相关话题

 ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫
  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
如何利用消息扩展属性显示昵称头像?如何通过APP服务器处理昵称头像的显示?昵称头像的本地缓存策略?音视频通话如何显示昵称头像?

关于环信大表哥:
   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;

先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。





环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见”
ios简版demo地址
Android简版demo地址 查看全部
 
​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
  1. 如何利用消息扩展属性显示昵称头像?
  2. 如何通过APP服务器处理昵称头像的显示?
  3. 昵称头像的本地缓存策略?
  4. 音视频通话如何显示昵称头像?


关于环信大表哥:

   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;



先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。
QQ截图20170508115114.jpg


环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见
ios简版demo地址
Android简版demo地址
2
评论

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

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

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题
 
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...小伙伴们还有什么想知道欢迎跟帖提出。
 
4
评论

【视频教程+源码】基于环信IM做一个仿微信APP-更新ing 郭永峰 高仿微信 仿微信 环信 XMPP

郭永峰 发表了文章 • 5573 次浏览 • 2017-05-16 15:29 • 来自相关话题

我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。
    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
4月12号成立工作室,现在18号,过了一个星期一个星期录了5天的环信教程视频,我将放在网盘免费分享环信的教程视频主要是针对有开发经验者教程主要是使用环信来模仿微信来做一个即时通讯的案例课程主要是先讲socket基础 -> 环信 ->自定义协议希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解同时能希望大家在面试时,在即时通讯这块不在陌生
  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏






支付宝打赏






谢谢大家的支持,个人微信号清扫描下面张图






 
郭永峰IT交流QQ群请加:596441895 查看全部
我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。

    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
  1. 4月12号成立工作室,现在18号,过了一个星期
  2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
  3. 环信的教程视频主要是针对有开发经验者
  4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
  5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
  6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
  7. 同时能希望大家在面试时,在即时通讯这块不在陌生

  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏

微信.png


支付宝打赏

支付宝.png


谢谢大家的支持,个人微信号清扫描下面张图

个人.png


 
郭永峰IT交流QQ群请加:596441895
2
评论

【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了 环信公开课 昵称头像 环信大表哥

beyond 发表了文章 • 1599 次浏览 • 2017-05-08 11:55 • 来自相关话题

 ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫
  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
如何利用消息扩展属性显示昵称头像?如何通过APP服务器处理昵称头像的显示?昵称头像的本地缓存策略?音视频通话如何显示昵称头像?

关于环信大表哥:
   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;

先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。





环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见”
ios简版demo地址
Android简版demo地址 查看全部
 
​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
  1. 如何利用消息扩展属性显示昵称头像?
  2. 如何通过APP服务器处理昵称头像的显示?
  3. 昵称头像的本地缓存策略?
  4. 音视频通话如何显示昵称头像?


关于环信大表哥:

   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;



先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。
QQ截图20170508115114.jpg


环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见
ios简版demo地址
Android简版demo地址
3
回复

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

回复

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

1
回复

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

回复

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

0
回复

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

回复

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

0
回复

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

回复

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

1
回复

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

回复

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

0
回复

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

回复

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

2
回复

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

回复

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

1
回复

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

回复

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

1
回复

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

回复

geri_yang 回复了问题 • 2 人关注 • 127 次浏览 • 2017-05-15 13:04 • 来自相关话题

2
回复

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

回复

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

2
最佳

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

回复

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

1
最佳
3
最佳

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

回复

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

1
最佳

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

回复

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

1
最佳

进入群聊页面闪退 有专职工程师值守 Android 环信_Android

回复

gaomode 回复了问题 • 1 人关注 • 172 次浏览 • 2017-04-26 18:24 • 来自相关话题

1
最佳

无法实时获取对方发送过来的消息 有专职工程师值守 Android 环信_Android

回复

Wxin 回复了问题 • 2 人关注 • 162 次浏览 • 2017-04-25 18:57 • 来自相关话题

2
回复

请问EaseUI_CN中,聊天的内容在哪个TextView里面哪? EaseUI_CN 发送信息 Android

回复

baoshu 回复了问题 • 2 人关注 • 156 次浏览 • 2017-04-25 17:18 • 来自相关话题

1
回复

发消息不出去 有专职工程师值守 环信_Android Android

回复

Wxin 回复了问题 • 2 人关注 • 137 次浏览 • 2017-04-24 23:10 • 来自相关话题

1
回复

在EaseChatFragment中接收后台发送的消息中文乱码 有专职工程师值守 Android 环信_Android

回复

Wxin 回复了问题 • 2 人关注 • 159 次浏览 • 2017-04-24 16:20 • 来自相关话题

1
最佳

为什么EaseChatFragment监听不到后台发送的消息? 有专职工程师值守 Android 环信_Android

回复

Wxin 回复了问题 • 2 人关注 • 177 次浏览 • 2017-04-24 16:20 • 来自相关话题

2
最佳

java.lang.StackOverflowError 有专职工程师值守 Android

回复

baoshu 回复了问题 • 2 人关注 • 158 次浏览 • 2017-04-24 12:28 • 来自相关话题

4
最佳

导入EaseUI 出现J爆红 有专职工程师值守 Android 环信_Android

回复

baoshu 回复了问题 • 2 人关注 • 266 次浏览 • 2017-04-21 18:14 • 来自相关话题

1
回复

照视频添加的EaseUI库,但是运行不了 Android 环信

回复

zhoumin 回复了问题 • 2 人关注 • 169 次浏览 • 2017-04-20 10:26 • 来自相关话题

2
最佳

环信服务端集成,如何集成? iOS Android webim

回复

执念 回复了问题 • 3 人关注 • 1477 次浏览 • 2017-03-13 11:36 • 来自相关话题

2
评论

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

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

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题
 
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...小伙伴们还有什么想知道欢迎跟帖提出。
 
4
评论

【视频教程+源码】基于环信IM做一个仿微信APP-更新ing 郭永峰 高仿微信 仿微信 环信 XMPP

郭永峰 发表了文章 • 5573 次浏览 • 2017-05-16 15:29 • 来自相关话题

我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。
    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
4月12号成立工作室,现在18号,过了一个星期一个星期录了5天的环信教程视频,我将放在网盘免费分享环信的教程视频主要是针对有开发经验者教程主要是使用环信来模仿微信来做一个即时通讯的案例课程主要是先讲socket基础 -> 环信 ->自定义协议希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解同时能希望大家在面试时,在即时通讯这块不在陌生
  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏






支付宝打赏






谢谢大家的支持,个人微信号清扫描下面张图






 
郭永峰IT交流QQ群请加:596441895 查看全部
我只是一个普通人,做人要谦虚。
我不是大神,我也不是很厉害的。
天外有天,人外有人。
老师引入门,修行靠个人。
希望能帮助大家,谢谢。

    大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
 
本人现状况:
  
   在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

 
郭永峰IT教育工作室于2017年4月12日成立!
 
成立原因:

希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
 

进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
  1. 4月12号成立工作室,现在18号,过了一个星期
  2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
  3. 环信的教程视频主要是针对有开发经验者
  4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
  5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
  6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
  7. 同时能希望大家在面试时,在即时通讯这块不在陌生

  持续更新

第一阶段:即时通讯的了解和微信APP开发前的准备!

【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
 
【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
 
【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
 
整个项目源码,git地址https://github.com/mayaole/fWeiXin

 微信打赏

微信.png


支付宝打赏

支付宝.png


谢谢大家的支持,个人微信号清扫描下面张图

个人.png


 
郭永峰IT交流QQ群请加:596441895
2
评论

【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了 环信公开课 昵称头像 环信大表哥

beyond 发表了文章 • 1599 次浏览 • 2017-05-08 11:55 • 来自相关话题

 ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫
  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
如何利用消息扩展属性显示昵称头像?如何通过APP服务器处理昵称头像的显示?昵称头像的本地缓存策略?音视频通话如何显示昵称头像?

关于环信大表哥:
   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;

先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。





环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见”
ios简版demo地址
Android简版demo地址 查看全部
 
​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

  在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

环信公开课12期讲了什么?
  1. 如何利用消息扩展属性显示昵称头像?
  2. 如何通过APP服务器处理昵称头像的显示?
  3. 昵称头像的本地缓存策略?
  4. 音视频通话如何显示昵称头像?


关于环信大表哥:

   马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
 
自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;



先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。
QQ截图20170508115114.jpg


环信公开课视频回放:视频观看地址
  
环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见
ios简版demo地址
Android简版demo地址
0
评论

【环信征文】Android程序员的十大转型之路 Android

东风玖哥 发表了文章 • 146 次浏览 • 2017-06-21 16:29 • 来自相关话题

IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。
 
我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减:
首先不会转型iOS,iOS和Android工程师的工作内容都是大同小异的。
其次不会转型Windows Phone,好多Andr oid程序员就是受不了产品经理唠叨:“像QQ客户端那样做成和iOS一样”才转型的,怎么会转型比Android还难做成和iOS一样的WP?
再次不会转型Windows和MacOS等桌面软件,桌面开发周期长、难度大、升级不易,这是一个已经接近穷途末路的夕阳产业。
最后不会开JavaME或者Symbian的历史倒车,除非他有本事让每个用户都买(就一个“买”字,同时包含“想买”和“买得到”的意思)停产多年的机型。
 
我观察如今的技术形势,并亲身探索了一个Android程序员转型的几个技术方向的可行性:
 
Android病毒和恶意应用
最近肆虐全世界的WannaCry让安全成了IT圈最热的话题,开发腻了善意应用的Android工程师最便捷的转型方向就是开发Android病毒和恶意应用。在4.x时代对Android对敏感权限还不是很敏感的时候,我就研究过给肉鸡伪造短信记录和让肉鸡给通讯录里所有(或特定)联系人发送短信的病毒。去年还研究过窃取友商App推送内容、强杀友商App进程、卸载友商App甚至让友商App被卸载后就再也不能在这台肉鸡上安装的恶意应用(或应用里的恶意功能)。

 
转型建议:此外锁定肉鸡里的重要文件勒索用户(Android上的WannaCry?)和窃取肉鸡用户的支付密码的实现在技术上也像强奸8岁女童一样简单,只不过事后逍遥法外很难。这个转型方向只适合拿自己的手机当肉鸡玩玩,千万不要用这些技术赚钱。

SDK
开发SDK本质上仍然在为Android应用开发软件,只是不直接开发Android应用。
 
每个Android程序员工作几年后都积累了属于自己的或大或小的类库,比如封装好的LogUtils和ToastUtils等;也都或多或少研究过常用开源框架的底层原理,比如了解Picasso和EventBus等;还应该对不开源的第三方服务有自己简单的二次封装,比如我就封装了一键实现支付宝和微信支付的moudle(免费的Ping++?)。
 
转型建议:尽管看见自己的链接出现在无数Android应用的Gradle文件的compile后面,开发了无数软件的一部分的成就感不会比开发完整的软件差。但是几乎没有老板会为了支持你开发开源软件发你工资。
 
JavaEE
Android程序员转型Java在基础知识方面是没什么难度的,毕竟语言相通,特性相似。同时每个Android程序员在大学时J2EE课程学得都不会很差,不过有些知识是该忘掉的,比如Hibernate已经落后于时代了,SpringMVC的全面使用才是Java后台的大势所趋。
 
转型建议:建议不想每天改UI的刚入行不久的Android工程师转型,我有好几个学弟就是参加工作后从Android转型Java的,他们过得都不错。很多工作年限较长的Android工程师本来就是JavaEE转型来的,就别转回去了。
 
手游
首先考虑不放弃Java语言和Android开发习惯的情况:最合适的就是能把游戏view直接插入普通layout里的AndEngine,前几年大红大紫的Flappy Bird就是用它开发的。AndEngine的开发方式和Android别无二致,且有丰富的开源demo。不过AndEngine没有官方文档,理论学习上有一定难度。我用AndEngine开发了我的毕业设计,参加工作后也用AndEngine获得了几个奖,我珍藏着一本AndEngine的非官方文档《Android游戏开发实践指南》(全新未拆封),期待着有一天能回到2014年把它送给那个买不起它的毕业生。

 
提到了AndEngine就不得不提国产AndEngine——OGEngine,它是基于AndEngine衍生的游戏引擎,有详细的纯中文文档和说汉语的技术支持杨城(笔名:小城),极适合开发Android TV游戏。OGEngine目前已停止更新,这个国产游戏引擎的悲剧在于推出时间太早,希望Android TV普及的时候卷土重来的OGEngine能让中国在游戏引擎方面领跑全世界。
 
LibGDX是一个跨平台的游戏开发框架,同样使用Java作为开发语言,前文所说的AndEngine就是基于LiBGDX实现的。LibGDX最大的优点就是极强的兼容性,不仅兼容Android和iOS,还兼容Windows、Linux、Max OS X等桌面系统。极强的兼容性还为开发提供了便利——不必打开Android模拟器,直接用电脑debug你的应用。在LibGDX和Android之间相互转型都很容易,知名的Android专家宋志辉、吴佳俊等都是从LibGDX转型Android的。
 
如果不要Java语言,那就有Cosos2d-x可供选择。《Cocos2d-x游戏开发实战精解》的作者欧桐桐(笔名:OTT)认为Android程序员一般对面向对象的知识掌握的比较全面,上手Cosos2d-x比较容易,并且Cosos2d-x是中国人维护的,文档全、资源多、教程多。OTT在得知我是和他一样的藏书人士后还特地送我一本他的大作鼓励我。

转型建议:做好心理准备,国内手游行业比普通的移动互联网行业加班更疯狂,建议刚入行没多久的Android工程师为了加班费转型,不建议30岁以上的Android工程师转型。
 
HTML5
HTML5也是Android工程师改行的好方向,HTML5在移动互联网领域应用非常广泛,比如混合开发、手机站、小游戏、微信公众号、微信小程序等。简单的手机站和对性能要求不高小游戏直接用从懒人模板(http://www.lanrenmb.com/)上找到的资源稍微修改一下即可,这里我只说说的混合开发应用和的小游戏怎么开发。
 
最著名的HTML5移动开发框架当属Facebook发布于2015年的React Native,这是一套跨平台、动态更新的 Javascript 框架,口号是“Learn once, write anywhere”。与之类似有同属舶来的PhoneGap等。
 
国产的HTML5开发框架在国内也百家争鸣,常见的有HBuilder和AppCan,二者共同特点是都为了便于新手入门制作了专用的编译器。2016年,在Qcon大会上宣布开源的Weex也异军突起,来自阿里的它因为开发的软件与原生App别无二致受到很多人的青睐。
 
开发对性能要求比较高的HTML5游戏,靠模板是不行的。2014年2月创立于北京的Egret是一套完整的HTML5游戏开发解决方案,其核心产品白鹭引擎(Egret Engine)凭借上手简便、性能强大已占据国内超七成的手机页游引擎市场份额。
 
Egret布道师徐聪(笔名:臭臭打不死人)还送我了Egret官方教程《Egret——HTML5游戏开发指南》和Egret吉祥物。
 
转型建议:一般来说,除非手机页游或商场,大多数用HTML5开发的Android应用就是胡闹。这条路线几乎是专为电商和小游戏行业准备的,如果公司有这方面的需求,Android程序员可以凭借平时自学的这方面技术完成任务。
 
VR
2015年底游戏外设王者雷蛇推出了VR游戏头显,2016年各大游戏厂商和小工作室争先恐后开发VR游戏争夺市场,开启了“中国VR元年”。虽然目前VR主要用在娱乐领域,被很多人视为玩具,但是VR所具有的价值却远远超出“玩具”的范畴。
 
前文讨论游戏引擎的时候没说Unity-3d不是疏漏,而是要把Unity-3d放在这儿谈。Unity-3d 是Unity公司开发的一个3D游戏开发工具,近年来的新版本不断加强对VR硬件系统的支持。Android程序员转型VR不仅可以实现自己从小就想让游戏跳出四角方框的梦想,还有Unity-3d所用的C#语言本来就是嚷着“我不是Java语言”的Java语言的学习优势。
 
转型建议:VR现在正是一片蓝海,只要自学能力够强,转型VR就像2015年在合肥买房一样明智。当然前提是你能找到愿意出钱的老板或投资人。
 
大数据
移动互联网时代是一个科技发达,信息流通的时代,大数据就是这个高科技时代的产物。马云曾在演讲中提到:未来的时代将不是IT时代,而是DT的时代。DT就是Data Technology(数据科技)的缩写,大数据的合理利用与否成了很多行业成败的关键。
 
移动互联网经过这些年的发展,拿O2O和当噱头已经唬不住投资人了。Hadoop也就自然而然受到了青睐,很多每4个月“生产”一批“两年经验”的“程序员”的培训机构也问我:“Android和iOS现在不吃香了,你能帮我介绍几个Hadoop讲师吗?”
 
转型建议:与转型Java后台一样,Android程序员转型Hadoop也具备语言相通,特性相似的优势。目前各大培训机构已经如蝇逐臭争相批量生产Hadoop程序员,如果你是因为陷入了他们培训的Android程序员造成的红海才转型的话,建议你不要转型,提升自己的竞争力才是王道。
 
人工智能和深度学习
前一阵子AlphaGo战胜了人类世界的围棋世界冠军柯洁,轰动了全世界。柯洁认为AlphaGo是能够打败一切的围棋上帝,这个说法我不敢苟同,毕竟它没有和“天”对弈过,但存在能“胜天半子”的人类——祁同伟。即使AlphaGo不能打败一切,也没有人有理由认为人工智能和深度学习不能成为IT届的重要发展方向。
 
TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,具备极佳的灵活性和可延展性,在和人工智能相关的领域都有广泛的应用。TensorFlow是开源的,会大大降低深度学习在各个行业中的应用难度,有远大的发展前景。
 
转型建议:尽管我坚信将来会T(ensor)F(low)的boys受女性欢迎程度不亚于TFboys,但TensorFlow暂时很不成熟,这个“将来”距今多久还是未知数。
 
Android系统
Linux作为目前大多数服务器的操作系统,学习Linux的大多数人的目的是做一个运维。然而把脑洞再开大一点的话,Android程序员精通了Linux之后可以开发一套属于自己的Android系统。《Linux大棚命令百篇》的作者吴鹏冲(笔名:Roc,和我一样也是水浒迷)和《循序渐进Linux》的作者高俊峰都送了一本自己的作品鼓励我开发属于自己的Android ORM。

这张照片摄于2016年3月30日我拿着《循序渐进Linux(第二版)》回到母校的自习室里攻读想成为像高老师一样能定制自己的Android系统的Linux专家的路上(双关)
 
转型建议:如果Android程序员准备跳槽到生产手机等搭载Android系统的硬件的厂商的话学习Linux再合适不过了,否则就只能自己刷机玩了。
 
产品经理
每个人都可能变成自己最讨厌的人,我也不例外。我从《人人都是产品经理》中学到了产品经理的情怀,还从《从点子到产品》中学到了产品经理的技术。还有幸赶上了今年3月《从点子到产品》的作者刘飞收徒。关于我转型产品经理失败的情况是一个发生在我和刘飞之间的“挖隋炀帝坟墓的开发商名叫杨勇”的故事:
2016年初,我带新人,没有收刘飞(同名学弟)为徒
2017年初,刘飞带新人,不肯收我为徒
 
转型建议:产品经理也是技术岗位,只不过写的是给人看的需求文档。如果一个Android程序员写的代码只能让电脑看懂而不能让负责维护的程序员看懂,那么就不要转型产品经理。
 
Android程序员转型机会虽然多,但不要因为看招聘网站上某个职业平均工资高就转型,随波逐流的弄潮儿必然会在浪潮之巅摔得好惨。培训机构常说“Android不吃香了,移动互联网的寒冬来了”来吸引人报名学习速成的Hadoop和TensorFlow,其实遭遇寒冬的不是某个行业,而是某些没有打好基础的人。

本文第三人称版本首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201706/543046.htm 查看全部
IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。
 
我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减:
首先不会转型iOS,iOS和Android工程师的工作内容都是大同小异的。
其次不会转型Windows Phone,好多Andr oid程序员就是受不了产品经理唠叨:“像QQ客户端那样做成和iOS一样”才转型的,怎么会转型比Android还难做成和iOS一样的WP?
再次不会转型Windows和MacOS等桌面软件,桌面开发周期长、难度大、升级不易,这是一个已经接近穷途末路的夕阳产业。
最后不会开JavaME或者Symbian的历史倒车,除非他有本事让每个用户都买(就一个“买”字,同时包含“想买”和“买得到”的意思)停产多年的机型。
 
我观察如今的技术形势,并亲身探索了一个Android程序员转型的几个技术方向的可行性:
 
Android病毒和恶意应用
最近肆虐全世界的WannaCry让安全成了IT圈最热的话题,开发腻了善意应用的Android工程师最便捷的转型方向就是开发Android病毒和恶意应用。在4.x时代对Android对敏感权限还不是很敏感的时候,我就研究过给肉鸡伪造短信记录和让肉鸡给通讯录里所有(或特定)联系人发送短信的病毒。去年还研究过窃取友商App推送内容、强杀友商App进程、卸载友商App甚至让友商App被卸载后就再也不能在这台肉鸡上安装的恶意应用(或应用里的恶意功能)。

 
转型建议:此外锁定肉鸡里的重要文件勒索用户(Android上的WannaCry?)和窃取肉鸡用户的支付密码的实现在技术上也像强奸8岁女童一样简单,只不过事后逍遥法外很难。这个转型方向只适合拿自己的手机当肉鸡玩玩,千万不要用这些技术赚钱

SDK
开发SDK本质上仍然在为Android应用开发软件,只是不直接开发Android应用。
 
每个Android程序员工作几年后都积累了属于自己的或大或小的类库,比如封装好的LogUtils和ToastUtils等;也都或多或少研究过常用开源框架的底层原理,比如了解Picasso和EventBus等;还应该对不开源的第三方服务有自己简单的二次封装,比如我就封装了一键实现支付宝和微信支付的moudle(免费的Ping++?)。
 
转型建议:尽管看见自己的链接出现在无数Android应用的Gradle文件的compile后面,开发了无数软件的一部分的成就感不会比开发完整的软件差。但是几乎没有老板会为了支持你开发开源软件发你工资。
 
JavaEE
Android程序员转型Java在基础知识方面是没什么难度的,毕竟语言相通,特性相似。同时每个Android程序员在大学时J2EE课程学得都不会很差,不过有些知识是该忘掉的,比如Hibernate已经落后于时代了,SpringMVC的全面使用才是Java后台的大势所趋。
 
转型建议:建议不想每天改UI的刚入行不久的Android工程师转型,我有好几个学弟就是参加工作后从Android转型Java的,他们过得都不错。很多工作年限较长的Android工程师本来就是JavaEE转型来的,就别转回去了。
 
手游
首先考虑不放弃Java语言和Android开发习惯的情况:最合适的就是能把游戏view直接插入普通layout里的AndEngine,前几年大红大紫的Flappy Bird就是用它开发的。AndEngine的开发方式和Android别无二致,且有丰富的开源demo。不过AndEngine没有官方文档,理论学习上有一定难度。我用AndEngine开发了我的毕业设计,参加工作后也用AndEngine获得了几个奖,我珍藏着一本AndEngine的非官方文档《Android游戏开发实践指南》(全新未拆封),期待着有一天能回到2014年把它送给那个买不起它的毕业生。

 
提到了AndEngine就不得不提国产AndEngine——OGEngine,它是基于AndEngine衍生的游戏引擎,有详细的纯中文文档和说汉语的技术支持杨城(笔名:小城),极适合开发Android TV游戏。OGEngine目前已停止更新,这个国产游戏引擎的悲剧在于推出时间太早,希望Android TV普及的时候卷土重来的OGEngine能让中国在游戏引擎方面领跑全世界。
 
LibGDX是一个跨平台的游戏开发框架,同样使用Java作为开发语言,前文所说的AndEngine就是基于LiBGDX实现的。LibGDX最大的优点就是极强的兼容性,不仅兼容Android和iOS,还兼容Windows、Linux、Max OS X等桌面系统。极强的兼容性还为开发提供了便利——不必打开Android模拟器,直接用电脑debug你的应用。在LibGDX和Android之间相互转型都很容易,知名的Android专家宋志辉、吴佳俊等都是从LibGDX转型Android的。
 
如果不要Java语言,那就有Cosos2d-x可供选择。《Cocos2d-x游戏开发实战精解》的作者欧桐桐(笔名:OTT)认为Android程序员一般对面向对象的知识掌握的比较全面,上手Cosos2d-x比较容易,并且Cosos2d-x是中国人维护的,文档全、资源多、教程多。OTT在得知我是和他一样的藏书人士后还特地送我一本他的大作鼓励我。

转型建议:做好心理准备,国内手游行业比普通的移动互联网行业加班更疯狂,建议刚入行没多久的Android工程师为了加班费转型,不建议30岁以上的Android工程师转型。
 
HTML5
HTML5也是Android工程师改行的好方向,HTML5在移动互联网领域应用非常广泛,比如混合开发、手机站、小游戏、微信公众号、微信小程序等。简单的手机站和对性能要求不高小游戏直接用从懒人模板(http://www.lanrenmb.com/)上找到的资源稍微修改一下即可,这里我只说说的混合开发应用和的小游戏怎么开发。
 
最著名的HTML5移动开发框架当属Facebook发布于2015年的React Native,这是一套跨平台、动态更新的 Javascript 框架,口号是“Learn once, write anywhere”。与之类似有同属舶来的PhoneGap等。
 
国产的HTML5开发框架在国内也百家争鸣,常见的有HBuilder和AppCan,二者共同特点是都为了便于新手入门制作了专用的编译器。2016年,在Qcon大会上宣布开源的Weex也异军突起,来自阿里的它因为开发的软件与原生App别无二致受到很多人的青睐。
 
开发对性能要求比较高的HTML5游戏,靠模板是不行的。2014年2月创立于北京的Egret是一套完整的HTML5游戏开发解决方案,其核心产品白鹭引擎(Egret Engine)凭借上手简便、性能强大已占据国内超七成的手机页游引擎市场份额。
 
Egret布道师徐聪(笔名:臭臭打不死人)还送我了Egret官方教程《Egret——HTML5游戏开发指南》和Egret吉祥物。
 
转型建议:一般来说,除非手机页游或商场,大多数用HTML5开发的Android应用就是胡闹。这条路线几乎是专为电商和小游戏行业准备的,如果公司有这方面的需求,Android程序员可以凭借平时自学的这方面技术完成任务。
 
VR
2015年底游戏外设王者雷蛇推出了VR游戏头显,2016年各大游戏厂商和小工作室争先恐后开发VR游戏争夺市场,开启了“中国VR元年”。虽然目前VR主要用在娱乐领域,被很多人视为玩具,但是VR所具有的价值却远远超出“玩具”的范畴。
 
前文讨论游戏引擎的时候没说Unity-3d不是疏漏,而是要把Unity-3d放在这儿谈。Unity-3d 是Unity公司开发的一个3D游戏开发工具,近年来的新版本不断加强对VR硬件系统的支持。Android程序员转型VR不仅可以实现自己从小就想让游戏跳出四角方框的梦想,还有Unity-3d所用的C#语言本来就是嚷着“我不是Java语言”的Java语言的学习优势。
 
转型建议:VR现在正是一片蓝海,只要自学能力够强,转型VR就像2015年在合肥买房一样明智。当然前提是你能找到愿意出钱的老板或投资人。
 
大数据
移动互联网时代是一个科技发达,信息流通的时代,大数据就是这个高科技时代的产物。马云曾在演讲中提到:未来的时代将不是IT时代,而是DT的时代。DT就是Data Technology(数据科技)的缩写,大数据的合理利用与否成了很多行业成败的关键。
 
移动互联网经过这些年的发展,拿O2O和当噱头已经唬不住投资人了。Hadoop也就自然而然受到了青睐,很多每4个月“生产”一批“两年经验”的“程序员”的培训机构也问我:“Android和iOS现在不吃香了,你能帮我介绍几个Hadoop讲师吗?”
 
转型建议:与转型Java后台一样,Android程序员转型Hadoop也具备语言相通,特性相似的优势。目前各大培训机构已经如蝇逐臭争相批量生产Hadoop程序员,如果你是因为陷入了他们培训的Android程序员造成的红海才转型的话,建议你不要转型,提升自己的竞争力才是王道。
 
人工智能和深度学习
前一阵子AlphaGo战胜了人类世界的围棋世界冠军柯洁,轰动了全世界。柯洁认为AlphaGo是能够打败一切的围棋上帝,这个说法我不敢苟同,毕竟它没有和“天”对弈过,但存在能“胜天半子”的人类——祁同伟。即使AlphaGo不能打败一切,也没有人有理由认为人工智能和深度学习不能成为IT届的重要发展方向。
 
TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,具备极佳的灵活性和可延展性,在和人工智能相关的领域都有广泛的应用。TensorFlow是开源的,会大大降低深度学习在各个行业中的应用难度,有远大的发展前景。
 
转型建议:尽管我坚信将来会T(ensor)F(low)的boys受女性欢迎程度不亚于TFboys,但TensorFlow暂时很不成熟,这个“将来”距今多久还是未知数。
 
Android系统
Linux作为目前大多数服务器的操作系统,学习Linux的大多数人的目的是做一个运维。然而把脑洞再开大一点的话,Android程序员精通了Linux之后可以开发一套属于自己的Android系统。《Linux大棚命令百篇》的作者吴鹏冲(笔名:Roc,和我一样也是水浒迷)和《循序渐进Linux》的作者高俊峰都送了一本自己的作品鼓励我开发属于自己的Android ORM。

这张照片摄于2016年3月30日我拿着《循序渐进Linux(第二版)》回到母校的自习室里攻读想成为像高老师一样能定制自己的Android系统的Linux专家的路上(双关)
 
转型建议:如果Android程序员准备跳槽到生产手机等搭载Android系统的硬件的厂商的话学习Linux再合适不过了,否则就只能自己刷机玩了。
 
产品经理
每个人都可能变成自己最讨厌的人,我也不例外。我从《人人都是产品经理》中学到了产品经理的情怀,还从《从点子到产品》中学到了产品经理的技术。还有幸赶上了今年3月《从点子到产品》的作者刘飞收徒。关于我转型产品经理失败的情况是一个发生在我和刘飞之间的“挖隋炀帝坟墓的开发商名叫杨勇”的故事:
2016年初,我带新人,没有收刘飞(同名学弟)为徒
2017年初,刘飞带新人,不肯收我为徒
 
转型建议:产品经理也是技术岗位,只不过写的是给人看的需求文档。如果一个Android程序员写的代码只能让电脑看懂而不能让负责维护的程序员看懂,那么就不要转型产品经理。
 
Android程序员转型机会虽然多,但不要因为看招聘网站上某个职业平均工资高就转型,随波逐流的弄潮儿必然会在浪潮之巅摔得好惨。培训机构常说“Android不吃香了,移动互联网的寒冬来了”来吸引人报名学习速成的Hadoop和TensorFlow,其实遭遇寒冬的不是某个行业,而是某些没有打好基础的人。

本文第三人称版本首发于51CTO的IT故事汇:http://mdsa.51cto.com/art/201706/543046.htm
0
评论

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

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

    Android平台的进程保活,一直是Android开发者心中一道坎,想必大家都曾为这个问题和老板、客户激励讨论过,虽然大多时候你的解释都被当成了孔乙己在鲁镇的小酒馆说的“之乎者也”,不被接受成为谈资!
Android进程保活的教程随便在网上一搜,各种神乎其乎的操作都有,但我大Android日新月异的机型还有国内各家rom大大,可以负责的讲,至今那些保活的教程没有一个靠谱的!
  同为创业公司,我很能理解各位老板们保活的需求,但同时作为Android开发者也有责任去维护Android的生态环境。现在很多Android开发工程师,主力机居然是iPhone而不是Android设备,感到相当悲哀。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在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 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;

结语

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

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是 查看全部
    Android平台的进程保活,一直是Android开发者心中一道坎,想必大家都曾为这个问题和老板、客户激励讨论过,虽然大多时候你的解释都被当成了孔乙己在鲁镇的小酒馆说的“之乎者也”,不被接受成为谈资!
Android进程保活的教程随便在网上一搜,各种神乎其乎的操作都有,但我大Android日新月异的机型还有国内各家rom大大,可以负责的讲,至今那些保活的教程没有一个靠谱的!
  同为创业公司,我很能理解各位老板们保活的需求,但同时作为Android开发者也有责任去维护Android的生态环境。现在很多Android开发工程师,主力机居然是iPhone而不是Android设备,感到相当悲哀。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在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 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;


结语

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

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

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

beyond 发表了文章 • 289 次浏览 • 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℃ 发表了文章 • 97 次浏览 • 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 发表了文章 • 88 次浏览 • 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
1
评论

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

lzan13 发表了文章 • 1514 次浏览 • 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 发表了文章 • 885 次浏览 • 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 发表了文章 • 677 次浏览 • 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 产品更新

产品更新 发表了文章 • 477 次浏览 • 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源码分析 集成笔记

随缘 发表了文章 • 848 次浏览 • 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源码分析 集成笔记

随缘 发表了文章 • 654 次浏览 • 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源码分析 集成笔记

随缘 发表了文章 • 422 次浏览 • 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源码分析 集成笔记

随缘 发表了文章 • 756 次浏览 • 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源码分析 集成笔记

随缘 发表了文章 • 615 次浏览 • 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源码分析 集成笔记

随缘 发表了文章 • 1256 次浏览 • 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 发表了文章 • 300 次浏览 • 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 集成指南 源码

随缘 发表了文章 • 1706 次浏览 • 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 发表了文章 • 1587 次浏览 • 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 产品快递

产品更新 发表了文章 • 384 次浏览 • 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 发表了文章 • 812 次浏览 • 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();对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
 
   需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

   可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

   对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

   如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
 
   这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {   这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

   关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

   理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

分析中间部分字节码实现,com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
mv.visitIntInsn(BIPUSH, 1); # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
 
坑:

   ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

大小:

   插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

讨论

   前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

源码地址
https://github.com/easemob/empatch
 作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo 查看全部
    美团热更新的文章已经讲了,他们用的是Instant Run的方案。
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
  1. 作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。
  2. 美团方案实现的大致结构
  3. 最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。

 方案选择:

  我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

tinker方案如图:
图片1.png

女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

女娲方案如图:
图片2.png

   女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的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();
对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
 
   需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

   可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

   对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函