注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

环信移动客服v5.21已发布,支持客服发送语音消息、语音转文字及支付功能

客服模式 支持客服发送语音消息 Web版客服工作台支持发送语音消息。与app渠道的客户聊天时,客服可以向客户发送语音消息。 点击输入框上方的麦克风按钮,开始录音并发送。每条语音消息最多为60秒,达到60秒后自动发送。 注:发送语音消息功...
继续阅读 »
客服模式

支持客服发送语音消息

Web版客服工作台支持发送语音消息。与app渠道的客户聊天时,客服可以向客户发送语音消息。

点击输入框上方的麦克风按钮,开始录音并发送。每条语音消息最多为60秒,达到60秒后自动发送。

注:发送语音消息功能仅Chrome浏览器在https模式下支持。 

001.png


管理员模式

语音转文字beta版

Web版客服工作台支持将app、微信、微博渠道的客户的语音消息自动转为文字,并显示在语音消息下方。方便客服快速识别客户的消息内容,并作出回应。

客服模式的会话、待接入、历史会话、客户中心页面,管理员模式的搜索、历史会话、客户中心、当前会话、质量检查页面,均支持显示语音消息对应的文字。 

002.png


语音转文字功能正在beta测试阶段,如果您希望先行体验,请提供租户ID,并联系环信商务经理。

开通语音转文字功能后,需要进入“管理员模式 > 设置 > 系统开关”页面,打开“启用语音转文字服务”开关。 

003.png


在线支付

在线支付功能全新上线。支持在web版移动客服工作台购买标准版坐席、对租户进行续费、增购坐席,并查看订单信息。

进入“管理员模式 > 设置 > 企业信息”页面,点击“购买”按钮。 

004.png


选择“缴费类型”、购买期限,填写坐席数量,确定并支付订单。

目前,只提供标准版坐席的在线支付功能。缴费类型包括:

  • 新购:第一次购买坐席时,请选择“新购”;

  • 续费:需要延长租户的到期日,请选择“续费”;

  • 增购:在已有坐席的基础上,增加坐席数量,请选择“增购”。



注:增购坐席时,坐席的到期日与租户的到期日一致,我们按比例收取坐席费用。 

005.png


【优化】客服超时未回复提醒客服

原“客服超时未回复访客端提示”开关更名为“客服超时未回复提示”,并支持设置“提醒客服”和“提醒访客”。

进入“管理员模式 > 设置 >系统开关”页面,对“客服超时未回复提示”的开关进行设置。

“提醒客服”开关打开,客服超时未回复时,会话主动排在进行中会话列表的顶部并标注颜色。
“提醒访客”开关打开,客服超时未回复时,系统自动发送一条提示语给客户。


006.png


进行中会话列表提醒示例: 

007.png


【优化】工作量、工作质量支持导出客服的真实姓名

对客服的工作量、工作质量详情进行导出时,导出文件包含客服的真实姓名,便于识别对应客服,使数据更直观。
 
环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.21

环信移动客服登陆地址http://kefu.easemob.com/  收起阅读 »

【环信公开课第14期视频回放】情感计算:如何用人工智能解读人类“情感”

朱泙漫学屠龙于支离益 , 殚千金之家,三年技成而无所用其巧。 ——《庄子·列御寇》       6月25日,EmoKit创始人兼CEO魏清晨在环信公开课上以“情感计算”为主题做...
继续阅读 »
        朱泙漫学屠龙于支离益 , 殚千金之家,三年技成而无所用其巧。

——《庄子·列御寇》

      6月25日,EmoKit创始人兼CEO魏清晨在环信公开课上以“情感计算”为主题做了人工智能解读人类“情感”的分享。魏清晨称,机器人未必像人的形态一样,有胳膊、有腿、有眼睛、有嘴,但如果要让机器实现真正智能,并且跟我们产生自然而然的交互,需要具备情绪识别和表达的能力。
 
  魏清晨讲到, EmoKit主要专注于机器人情感的研发,让机器人从一个多模态的形式了解人的情感,从渠道数据来源的角度去做综合判断,可以预见的未来,曾经独属于人类的情感将“赋能”人工智能!
 
公开课课程大纲 
  • 情感计算概述与发展
[list=1]
  • 情绪是人类社会奖惩机制最底层的编码
  • 情感计算包含的模块和价值
  • 情感计算,是人工智能的下一个未来
  • 情感计算的技术实现
    • 情感信号的获取与量化
    [list=1]
  • 分析、建模与识别
  • 情感理解和反馈
  • 情感合成与表达
  • 人机交互的实现
    • AI+情感计算的应用
    [list=1]
  • 情感计算在环信IM的最佳实践
  • 环信客服通过情感计算实现智能质检
  • 更多落地场景
  • 商业变现
    • AI火爆背景下的冷思考

     
    视频回放

     






     
    已有8625名开发者加入环信公开课,妹子比例10%!还有最强王者等你开黑!赶紧加入我们一起玩耍吧!

    扫码.gif


     
      收起阅读 »

    《IT经理世界》特写:环信的Alpha刘,布局未来的商业智能!

    6月20号刊 新疆界 刘俊彦说不想做一家小老头公司——规模不大,每年挣个几千万元,日子过得滋润,但每年只有10%左右的增长。 技术男刘俊彦的思维很跳跃,2014年做即时通讯云,一年后开始做客服云,不到一年,又开辟了智能机器人业务。看起...
    继续阅读 »


    微信图片_20170627113413.gif

    6月20号刊

    新疆界


    微信图片_20170627111638.jpg


    刘俊彦说不想做一家小老头公司——规模不大,每年挣个几千万元,日子过得滋润,但每年只有10%左右的增长。

    技术男刘俊彦的思维很跳跃,2014年做即时通讯云,一年后开始做客服云,不到一年,又开辟了智能机器人业务。看起来好像在分片作战,但突然有一天他已经在一个更大的战略布局里了。

    这些年,云的概念被炒到火热,企业不惜花血本布局云计算,可一轮下来之后,不同的业务还是要各养一套人马,各养一套软件系统,说好的大数据,量是上去了,但是实际利用率却如挤牙膏,效用微乎其微。很多企业现在正处在一个升级也不是,不升级也不是的难受期。

    见到刘俊彦的时候,他刚送走一家航空公司的几位管理人员,他们现在的烦恼是,守着一大批高净值的用户,只能卖个机票,最多再卖个保险,利润已经碰到天花板,有种守着宝藏却挖不出金子的感觉。刘俊彦在他的小会议室的黑板上,画了一个完整的从用户服务到用户营销的闭环图,这张图里包含了航空公司乃至一些大企业当下的几大痛点及应对招数,如同在下一盘围棋,细节处棋势做得够厚,大局又跳出常人思维之外,细看与AlphaGo的风格颇为相似。

    这是刘俊彦第一次将自己成熟的大局想法展示给外界。
     
    高效融资与快速换挡
     
    刘俊彦毕业于英国伦敦大学国王学院计算机专业,后一直在IT外企从事技术研发工作,2013年,离职创业前已经成为红帽开源软件领域领先的专家。这一年,他已经年过40。

    最初的一年多时间,是在海淀图书城的车库咖啡里度过的,当时刘俊彦和另外三位合伙人都刚从外企里出来,各自都已经是行业里优秀的技术人才,都实现了财务自由,清楚自己要的是什么,且志趣相投,所以走到了一起。

    刚开始,大家专注于做产品,也没有急着去找投资。一年后,一个偶然的机会,投资自己找上了门。

    2014年5月,刘俊彦他们进驻到氪空间的当天下午,经纬的投资人找到了他们,大概聊了两个多小时,双方就达成了合作意向。紧接着不到半年,SIG和红杉资本相继成为投资方。几家投资方看中的是刘俊彦等人创立的环信公司在即时通讯领域的开发能力和社交大数据分析能力,以及环信自主研发的高并发可扩展架构。

    此前,刘俊彦拥有17年的开发经验,其擅长的领域在于实时消息系统和高并发消息中间件等。2013年,社交软件开始大行其道,当时很多人找他为APP开发即时通讯聊天功能,一般如果企业自己开发的话,怎么都需要好几个月时间,而使用刘俊彦的团队的产品只需一天功夫就出来了。因此,最初的产品并非他们刻意做出来的,而是需求在先,而且令他们也没想到的是,做着做着就成“风口”了。

    很快,使用了环信即时通讯功能的APP上的用户从几万飞速上升到几千万,乃至后来的几个亿,每天下发的消息达20亿条。曾经在短信高峰时代,中国移动每天的消息量是7亿,目前国内能支撑并发连接几千万的团队不超过5个,包括腾讯、阿里、新浪微博、陌陌等,另外还有一个就是环信。

    用户上去之后,新的需求又来了。开始,很多人只是把APP内的聊天功能用作连接人与人的社交管道,但很快就不断有人找上门来,希望把APP内的聊天功能做成淘宝旺旺那样连接人和商业的客服工具。

    从本质上来讲,环信早期推出的即时通讯云是基于PaaS平台的服务,而要做客服云则需建立在SaaS平台上,如此跨平台的转变,对于一家创业公司,无论是人才、资本、研发或渠道等各方面都会带来严峻的挑战。

    2015年4月,A轮的三家投资方在前几个轮次共900万美元的基础上,再次追加了共1250万美元的B轮投资,在资本方的财力和战略加持下,一个月后,环信客服产品线上市,一年后环信客服产品的客户量、销售额等硬指标均达到几十倍上百倍的增长。

    据刘俊彦介绍,早期客服云的客户几乎一半多来自即时通讯云,相当于后者是前者的流量导入渠道,后者反过来巩固了前者的价值所在。

    目前,环信客服产品的企业用户已经超过5万多家,用户也从国美在线、58到家、新东方等扩展到了十几个行业当中。

    然而,IT业一年相当于传统行业十年,这是一个变换以秒速推进的行业,刘俊彦很快又迎上了新的挑战。

    痛点与前瞻

    随着产品的积累和行业的扩展,刘俊彦开始接触到一些保险、证券、航空等行业大客户。曾经有一家保险公司的客户跟他提到亲身经历的一件事情,以往保险公司客户经理做客户维护的通常做法就是每年会定期提前一两个月给老客户打电话,提醒他们保险快到期,可以续费了。其中有一个老客户,关系维护得挺好,但是这个老客户最近新买了辆哈雷摩托车,而他的客户经理没能及时了解并提供摩托车的保险报价,结果错失了老用户。

    这种个案在保险公司普遍存在。如果有更懂用户的数据,进行再分析和精准营销,就能很快帮老用户接上新的业务,还可以把以前让利给渠道的部分,直接反馈给用户,大幅提高销售成功率。

    刘俊彦说,做即时通讯,做客服,真正的差距在于,怎么通过商业智能帮助企业去挖掘以前看不到的客户信息,提高销售转换率,发掘新销售机会,以及人工智能技术怎么代替人工,怎么帮企业营销,这都是未来技术。

    2016年年初,环信基于企业一体化智能商业的需求,开始推全媒体智能客户服务。所谓全媒体就是不管客服请求来自微信、微博或APP,还是网页或电话,都可以同时呈现在一个界面上,由专人统一处理。

    以前企业不同的渠道由不同的人马在支持,各自软件系统也不一样,信息也是各自孤立的,片段化的,环信做到了让一个支持人员同时监控所有渠道,客户身份也可以从孤立的信息中关联起来,形成一个统一身份。也就是说,同一个用户打完电话,再用微信接入,通过各种技术方法可以跨屏身份合并被识别为是同一个人。

    此外,一家企业如果每天进来10万条消息,环信的技术可以识别出来自不同渠道不同数据格式的信息,然后对其进行数据清理,再做主题分析和情感分析,从而知道用户今天都反馈了什么问题,情感是愤怒还是高兴,可能的消费趋势是什么?进一步还可以給出用户画像,画像信息详尽,一直可追溯到用户城市、职业、性别、喜好、消费能力、行为轨迹,等等。

    这些数据对于企业至关重要。不仅是保险公司,证券公司、航空公司等等行业都存在类似的强烈需求。

    环信还在自己研发客服机器人。刘俊彦认为,环信既有SaaS客服软件高市场占有率的通道优势,又有了大量的数据积累,加上对垂直行业的深度理解,在开发具有高度行业特征的客服机器人上,又领先了其他对手。

    2017年 ,环信整合旗下即时通信云、移动客服、智能客服机器人和主动营销产品线,推出环信CEC (Customer Engagement Cloud),向企业提供从客户互动渠道,到客户服务,再到精准客户营销的全流程客户互动解决方案。

    技术出身的刘俊彦,既可以不断挖掘出用户的痛点,在技术细节上下足功夫,又可以跳出来,把握住前瞻性技术,果断出击。当一个个看似不大相关的痛点技术在量的积累上突破一个爆发性临界点时,全新的需求与应用又把之前所有的技术关联在了一起。

    这一切使得刘俊彦像一个“做局”的高手。

    转变与坚持

    创业仅仅4年,刘俊彦已经看惯了互联网领域的迎来送往,2014年,他还经常在微信里看见做社交应用的CEO们发朋友圈,到2015年左右,这些人大多看不到了,换了一批做O2O的CEO,一年以后,这些人又看不见踪影了。大浪淘沙,互联网的残酷淘汰是铁律。

    当年那些做即时通讯服务的环信的老对手们,现在大多还在做老业务,日子过得还不错。刘俊彦却早已不在当初的格局里了。

    做即时通讯云有一个特点,从几十万用户到几百万用户是一个技术节点,从几百万到几千万又是一个节点,用户过亿之后则是一个更大的节点。每一个节点处,基本上架构要推倒重写一遍。这要求技术团队快速的迭代和积累,一旦有一个环节出问题,就有可能导致服务不可用。

    这期间,环信团队也经历了重大考验,也被客户骂过,好在最后都坚持过来了。现在环信即时通讯技术架构早已稳定下来,即使再扩容4~5倍也不成问题。

    然而,就在这么一个节点上,刘俊彦毅然决定再另外做一个SaaS平台,这对于整个团队是一种怎样的震撼。虽然对于技术能力强悍的环信团队来讲,技术难题最终都是可以克服的,而对市场的敏锐嗅觉以及果断决策,却非易事。事实上刘俊彦的判断是精准的。

    环信最初的客户群主要集中在互联网领域,这些客户的生命周期比较短,付费能力比较低,好处是决策周期短,商业谈判简单,能迅速达成交易,迅速验证需求,这适合早期创业。

    有了这段经历也让刘俊彦开始明白,为什么美国的SaaS软件同行们天天讲生命周期价值,讲内容营销,因为他们也跟环信一样做的是小客户,可见对于小客户这一招中外通吃。而大客户不是这么玩的。

    在美国做企业服务,底层有一批创业公司,天花板是做到几亿美元到几十亿美元市值,基本很难再往上突破了。这是因为大客户都被微软、甲骨文、Salesforce这样的公司牢牢掌握着,小企业根本没机会进入那个圈子。

    某种程度上,中国也差不多,但也有很大不同。中国大型企业还没有强大的科技公司可以很好地服务于他们,这些年主要依赖于一些大型系统集成商,系统集成商的做法跟甲骨文等这些科技巨头又不一样,他们以项目运行的方式推进,在创新和积累上相对较弱。

    以前中国的大型国企或私企也认可这种做法,但随着整体经济环境进入L型经济,增长放缓,企业追求利润的结果就是更加注重创新,尤其是服务创新,这两年刘俊彦明显地感觉到国内大型企业有一种创新急迫感,他意识到谁能服务中国500强企业,谁就会成为中国的甲骨文。

    由于求稳,当年环信的竞争对手就没赶上这波新的机会。“如果我们到现在还只是在做最初的一个业务,我们也就成为了一家小老头公司。”刘俊彦平静地说。

    2017年3月,环信完成1.03亿元的C轮融资,由经纬领投,银泰嘉禾跟投。

    当然,考验仍然存在。SaaS是一种功能密集型的技术,最考验大规模研发团队的研发效率和管理能力。2016年新组建的智能机器人团队则是资本密集型,这个团队虽然人不多,但是一年投入却上千万元。

    不同的技术、人才、资本结构,对于管理是一大考验。不过刘俊彦认为,既然创业就要全力以赴去做,“挖人要看眼光,选择很重要,路线很重要,信任也很重要,既然选择上了火箭飞船,就不要考虑是几等舱。”
     
    作者 | 刘晓芳

    微信编辑 | 李昊原

    原文发表于《IT经理世界》,转载请注明
    收起阅读 »

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

         前阵子钉钉在微信楼下刷了一波#创业很苦,坚持很酷#的广告,浓浓的“丧”文化风格文案受到了各界褒贬不一的评价,也引起了大家对OA办公系统的关注。    对企业而言,初选OA办公系统是为了满足需求,解决当下问题,由于OA办公系统的在公司运作流程中扮演的...
    继续阅读 »
      

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

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

    OA广告图.jpg


    关于Dolores?


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


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

    整个解决方案都包括了什么?
    • 企业通讯录的管理:部门/员工的增删改查
    • 通讯录全量更新:全量/增量更新 
    • 企业通讯录权限管理:基于RBAC权限管理模型
    • 企业即时通讯IM:企业通信对IM这块的可靠性要求高,选择了目前比较成熟的IM云服务厂商-环信
      组织架构企业通讯录可以说是企业沟通中最重的业务之一,能够提供员工各种服务的认证,获取员工的联系方式等。 组织架构-Server服务端主要包括以下功能:[list=1]
  • 支持管理人员(例如HR)对部门和员工进行增删改查
  • 支持部门和员工自定义排序,自定义元信息存储
  • 权限管理
  • 员工通讯录视图 (员工根据自己的权限生成通讯录)
  • 通讯录增量更新 (鉴于移动端特殊的网络环境和设备,通讯录应该支持差量更新)
  • 集成 IM 用户系统
  • 在这里我们主要讨论以下两个问题: 权限管理  随着企业逐渐的发展,团队壮大为了更有效的沟通,以及保护公司内部的一些商业信息不被泄漏,我们应该为通讯录添加权限管理。基于Role-based access control(RBAC)的权限管理模型为了介绍此权限管理模型,我们先解释一下基本概念
    • 角色:通常是指企业中某一个工作岗位,这个岗位具有特定的权利和职责。被赋予此角色的员工,将获得这种权利与职责
    • 权限:被赋予访问实体的权利。在本项目中是指访问部门和访问某一个或者某一类员工的权利
    • 用户-角色分配(User-Role Assignment URA):为某个用户指定一个或者多个角色,此员工将获得这些角色所具有权利的集合
    • 角色-权限分配(Role-Permission Assignment RPA):将权限分配给角色,一个角色可以包含多个权利。在本项目中是指多个访问部门和访问员工的权限
    在用户和权限之间引入角色中介,将用户与权限的直接关系弱化为间接关系。
    |ˉˉˉ|           |ˉˉ ˉ|          |ˉˉˉˉ ˉˉ|  | User |---URA---> |  Role |<---RPA---| Permission  ||______|           |_______|          |_____________|
        以角色为中介,首先创建访问每个部门和员工的访问权限,然后创建不同的角色,根据这些角色的职责不同分配不同的权限,建立角色-权限的关系以后,不同的角色将会有不同的权限。根据员工不同的岗位,将对应的角色分配给他们,建立用户-角色关系,这就是RBAC的主要思想。一个员工可以用户多个角色,一个角色可以用于多个访问权限。RBAC 极大的简化了员工的授权管理。   由于企业的部门和员工数量很多,在创建权限时管理员不可能去设置每一个权限可以访问的每一个部门和每一个员工。所以本项目将功能和指责类似的部门和员工看作是同一类型,在创建部门和员工的时候为每一个部门和员工分配固有属性type,管理员在设置权限规则的时候只需要指定可访问的部门类型和员工即可。增量更新   鉴于移动终端计算资源有限,如网络,存储,电量等,所以通讯录的更新技术应该保证尽量少的资源。另外由于通讯录的特殊性,通讯录的变化需要能实时通知到受影响的在线员工。基于版本号与变更日志的增量更新模型   客户端第一次登陆系统以后,我们根据当前登录角色生成对应的通讯录视图,并以当前时间戳作为版本号,返回给客户端。客户端后续通过此版本号增量更新通讯录。版本号   版本号有两种:一是客户端当前通讯录版本 c-version, 二是服务端通讯录每一次变化时的版本号s-version变更日志   在管理员修改权限规则,或者修改某个岗位的访问规则时会影响大面积员工的通讯录视图,此时如果用增量更新会导致服务器流量异常,因此在这2中情况会清空原来的变更日志并且要求客户端进行一次全量更新。   如果管理员新增了员工,服务端会根据被修改的员工或者部门type, 反推出所有受影响的员工,然后生成一条变更日志, 例如:
    {       "content" : [        {          "cn" : "Lucy.Liu",          "id" : "b4vlfg91scgi1dcju8v0",          "title" : "市场运营负责人",          "email" : [            "lucy.liu@dolores.store"          ],          "priority" : "101111",          "name" : "刘小飞",          "telephoneNumber" : "18888888888"        }      ],      "createTimestamp" : "20170614063303Z",      "category" : "member",      "action" : "add"    }
    客户端在请求增量更新的时候,通过当前登陆ID与版本号,可查找出所有与自己相关的变更日志,然后在客户端数据库中应用这些变更,即可完成同步。组织架构-Client   由于现在员工办公设备的多样性,客户端要根据自己公司的情况,覆盖的足够完整,常见的平台有 iOS Android windowsmac linux , 对于后三个平台可以用 Web APP 来覆盖,iOS&Android 用原生的app来提升用户体验。客户端App主要包括以下功能:[list=1]
  • 会话列表
  • 优秀的聊天界面,历史记录
  • 组织机构全量/增量更新
  • 员工个人资料展示
  • 客户端数据库设计IM数据库设计 当前版本使用环信SDK 组织架构数据库设计表设计客户端组织架构较服务端简单,不关联用户Role,客户端本地存储Staff(员工)和Department(部门)信息:
    • 一个部门可以包含相关子部门和部门员工。该部门员工和部门在视图上处于同级关系。
    • 员工隶属于部门,同一员工可以存在于多个部门。
    • 员工角色用title来表示。


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

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

    工作流集成

    (TODO)
     
    如何使用Dolores

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

    后端数据库

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

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

    组织架构管理

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

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

    客户端

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

    Done

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

    (TODO)

    多机容灾

    (TODO)

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

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

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


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


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

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

    687474703a2f2f6f7131696e636b76692e626b742e636c6f7564646e2e636f6d2f646f6c6f726573313032342e706e67.png

    收起阅读 »

    视频超过三十秒后再接受 无数据

    iOS 根安卓的  视频聊天 超过三十秒之后在接受就没有画面了 都是黑的 小窗口是有画面的是因为 超时了吗 ? 还是什么原因该怎么解决呢  谢谢
    iOS 根安卓的  视频聊天 超过三十秒之后在接受就没有画面了 都是黑的 小窗口是有画面的是因为 超时了吗 ? 还是什么原因该怎么解决呢  谢谢

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

    IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。   我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减: 首先不会转...
    继续阅读 »
    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,其实遭遇寒冬的不是某个行业,而是某些没有打好基础的人。
      收起阅读 »

    Android 头像和昵称的修改

    1.在EaseChatFragment中的EaseChatFragmentHelper类中的onSetMessageAttributes的方法中设置自己的头像和昵称, @Override public void onSetMessageAttri...
    继续阅读 »
    1.在EaseChatFragment中的EaseChatFragmentHelper类中的onSetMessageAttributes的方法中设置自己的头像和昵称,
        @Override
    public void onSetMessageAttributes(EMMessage message) {
    String username = xxx;//自定义名称
    String avatar = yyy;//自定义头像

    message.setAttribute("avatar", avatar);
    message.setAttribute("username", username);
    }


    2.在easeUI中的EaseUserUtils类里面有两个方法setUserNick和setUserAvatar,分别是设置昵称和头像。
    将setUserAvatar改为:
    /**
    * 显示头像
    * set user avatar
    * @param message
    */
    public static void setUserAvatar(Context context, EMMessage message, ImageView imageView){
    if (message == null){
    return;
    }

    //发送消息 显示本地头像
    if (message.direct() == EMMessage.Direct.SEND){
    Glide.with(context).load(localUrl).into(imageView);
    return;
    }else {
    EaseUser easeUser = UserProfileCache.GetSpCacheUser(context, message.getFrom());
    //本地已缓存了这个用户信息 直接绑定
    if (easeUser != null){
    String avatar = easeUser.getAvatar();
    if(avatar != null){
    try {
    Glide.with(context).load(avatar).into(imageView);
    } catch (Exception e) {
    //use default avatar
    Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView); }
    }else{
    Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
    }
    }
    //本地没有缓存了这个用户信息 保存本地
    else {
    try {
    String avatar = message.getStringAttribute("avatar");

    if(avatar != null){
    try {
    Glide.with(context).load(avatar).into(imageView);
    } catch (Exception e) {
    //use default avatar
    Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView); }
    }else{
    Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
    }
    EaseUserUtils.getUserInfo(context, message);
    } catch (HyphenateException e) {
    e.printStackTrace();
    }
    }
    }
    }

    public static EaseUser getUserInfo(Context context, EMMessage message){

    EaseUser user = null;
    try {
    String avatar = message.getStringAttribute("avatar");
    String nick = message.getStringAttribute("username");

    user = new EaseUser(message.getFrom());
    user.setAvatar(avatar);
    user.setNickname(nick);
    EaseCommonUtils.setUserInitialLetter(user);
    UserProfileCache.setSpCacheUser(context, message.getFrom(), user);
    } catch (HyphenateException e) {
    e.printStackTrace();
    }
    return user;
    }

    UserProfileCache为用户缓存类。
    public class UserProfileCache {

    private static Gson gson = new Gson();
    public static String SP_CACHE_USER = "sp_cache_user";

    private static SharedPreferences spf;

    public static void setSpCacheUser(Context context, String user_id, EaseUser cacheUser){
    if (user_id == null){
    return;
    }
    if (spf == null){
    spf = context.getSharedPreferences(SP_CACHE_USER, Context.MODE_PRIVATE);
    }
    SharedPreferences.Editor editor = spf.edit();
    editor.putString(user_id, gson.toJson(cacheUser));
    editor.commit();
    }

    public static EaseUser GetSpCacheUser(Context context, String userId){
    if (userId == null){
    return null;
    }
    if (spf == null){
    spf = context.getSharedPreferences(SP_CACHE_USER, Context.MODE_PRIVATE);
    }
    String cacheUserString = spf.getString(userId,null);
    if (cacheUserString == null){
    return null;
    }else {
    return gson.fromJson(cacheUserString,EaseUser.class);
    }
    }

    }

    注:这种方式不用再创建数据库,只是对本地消息记录的显示做了下调整。其实是可以做的更好,比如把消息记录存到自己服务器,什么问题都不是问题,但是目前这里没有过多要求,所以还是不去麻烦后台爸爸,自己在客户端改了。
      收起阅读 »

    环信进阶篇-实现名片|红包|话题聊天室等自定义cell

        伴随着即时通讯成为了越来越多APP的刚需,匿名社交、阅后即焚、红包等新玩法层出不穷,基本的聊天方式越来越难以满足变态的需求。环信提供的自定义扩展属性功能非常的强大,能够帮助我们在cell中的各种需求做定制处理。这是分享一个之前做过的方法及实现,大家可以...
    继续阅读 »
        伴随着即时通讯成为了越来越多APP的刚需,匿名社交、阅后即焚、红包等新玩法层出不穷,基本的聊天方式越来越难以满足变态的需求。环信提供的自定义扩展属性功能非常的强大,能够帮助我们在cell中的各种需求做定制处理。这是分享一个之前做过的方法及实现,大家可以借鉴处理的过程及思路,如有不妥之处,请大家及时留言告知,谢谢。 
    今天就给大家介绍下怎么对cell中的各种需求的定制处理


     类型一:在现有会话cell上修改UI效果

    类似于上面给出的截图,我们有时候需要对环信官方给出的cell进行些许的调整。例如:项目中加入了不同于普通群聊或者聊天室的功能需求
    点击话题聊天,大家加入聊天室,这里发出的各种就是不同于普通聊天,普通的聊天只需展示文字、地址、图片等等,但是这里的需求是得加上时间、私聊按钮,没砍需求之前是还有点赞和取消赞的按钮。
    我们在普通聊天的基础上新建几个cell,文字、语音、图片、地图等等,不能和原有的普通cell混合起来,因为需求有普通聊天。

    直接把普通聊天cell中的代码拷贝过来,再在此基础上进行cell的UI自定义处理,就拿文字聊天时的处理情况为例:


    1、拷贝复制原有普通聊天cell内的代码

    2、把需要的新增的UI控件初始化

    3、适配各类控件

    4、传值及赋值

    5、新增按钮点击和本身cell的点击效果处理(别和cell上的点击效果混到一起)

    6、耐心调整cell上UI效果

    以上基本就是简单的自定义cell步骤了,有基础的小伙伴看下步骤应该就有思路了


    类型二:类似于红包和名片Cell的UI效果


    通常在我们项目中,并不只有文字、图片等等这些简单的聊天内容,有时候我们需要把自己的信息作为一张名片发给对方、发个红包给好朋友、发一个项目中的一个模块介绍给对方等等功能要求。
    我们就拿雷哥的这张假名片为例:
    /*!
    @method
    @brief 新增一个新的功能按钮
    @discussion
    @param image 按钮图片
    @param highLightedImage 高亮图片
    @param title 按钮标题
    @result
    */
    - (void)insertItemWithImage:(UIImage*)image
    highlightedImage:(UIImage*)highLightedImage
    title:(NSString*)title;
    /*!
    @method
    @brief 修改功能按钮图片
    @discussion
    @param image 按钮图片
    @param highLightedImage 高亮图片
    @param title 按钮标题
    @param index 按钮索引
    @result
    */
    - (void)updateItemWithImage:(UIImage*)image
    highlightedImage:(UIImage*)highLightedImage
    title:(NSString*)title
    atIndex:(NSInteger)index;
    /*!
    @method
    @brief 根据索引删除功能按钮
    @discussion
    @param index 按钮索引
    @result
    */
    - (void)removeItematIndex:(NSInteger)index;
    *  消息体类型
    typedef enum{
    EMMessageBodyTypeText  = 1,    /*! \~chinese 文本类型 \~english Text */
    EMMessageBodyTypeImage,        /*! \~chinese 图片类型 \~english Image */
    EMMessageBodyTypeVideo,        /*! \~chinese 视频类型 \~english Video */
    EMMessageBodyTypeLocation,      /*! \~chinese 位置类型 \~english Location */
    EMMessageBodyTypeVoice,        /*! \~chinese 语音类型 \~english Voice */
    EMMessageBodyTypeFile,          /*! \~chinese 文件类型 \~english File */
    EMMessageBodyTypeCmd,          /*! \~chinese 命令类型 \~english Command */
    }EMMessageBodyType;
    如果环信把这个开放出来,或许我们就更加简单了我们只需自己修改成自己对应的类型即可。但是这个目前就想想,所以我们可以在以上类型中找一个出来,在它的基础上做些文章,变成我们想要的类型。

    红包和名片最像什么。。。。对,不就和图片差不多嘛,不过小伙伴也不要以为只能拿图片来做文章,其他的我们都可以拿来用,这里就拿文字类型来作为例子(原理都一样)。

    01.jpg


    名片类型
    这里我们只简要介绍怎么根据会话类型来显示名片,具体传值等怎么做,有基础的小伙伴应该都懂,不懂的小伙伴见文章底部。
     
    1. 我们需要在发送名片时,在拓展消息里面存一个名片的字段,这个字段可以被用来判断是名片、红包等等。
    2. 名片、红包等等中内容,同样也存在拓展属性中(这里不做过多介绍)
    3. 我们在展示自己的消息和接收到对方的消息时,在文字类型的基础上再进一步判断是什么类型,加载对应类型的视图,如果是红包就加载红包的view,如果是名片就展示名片view......



    02.jpg


    加载不同类型的cell
    好了,以上就是我们所要介绍的两种不同类型cell的处理办法。


    以下是补充自定义cell时遇到的各种情况及处理:
    1、cell上语音、图片等原始点击和新增按钮点击冲突处理:

    注释掉原有的点击方法,把原有的点击方法放到具体的控件上去,避免cell上多个控件点击的冲突

    重点:记得把气泡上的点击权限打开
    _backgroundImageView.userInteractionEnabled = YES;



    03.jpg


    解决点击冲突
    2、cell上语音气泡长度的改变,避免过段影响布局

    我们只需把原有语音上的语音长度Label距语音图片控件调大一点距离就能自动把语音类气泡拉长。(其他类型一样处理原理)

    04.jpg


    语音气泡拉长
    3、因新增控件导致在原有cell上高度的变化处理
    /*! @method @brief 根据消息的内容,获取当前cell的高度 @discussion @param model        消息对象model @result 返回cell高度 */
    + (CGFloat)cellHeightWithModel:(id)model
    在原cell高度处理的情况下,根据各种类型的判断进行cell高度的自适应。

    05.jpg


    cell高度处理
    4、文字类型气泡长度的处理

    我暂时的处理方法:判断输入的文字长度,加入文字长度小于10,我会在后面自动补全5个空格,被动撑长气泡的长度。

    假如小伙伴们有更好的建议也可以留言,谢谢! 收起阅读 »

    环信Web IM 新版本发布,提供更为丰富的群组、聊天室功能,现在更新就送小风扇!

      六月的骄阳,暑气留恋,但这风风火火却远不能掩盖季节的丰富内涵。环信发布了WEB新版本,十余项更新,带来了更加丰富的群组、聊天室功能。环信还为小伙伴们准备了一批小风扇,参与使用新版本并在文章下方跟帖使用反馈,就能获得环信usb小风扇,数量有限,先到先得! ...
    继续阅读 »
      六月的骄阳,暑气留恋,但这风风火火却远不能掩盖季节的丰富内涵。环信发布了WEB新版本,十余项更新,带来了更加丰富的群组、聊天室功能。环信还为小伙伴们准备了一批小风扇,参与使用新版本并在文章下方跟帖使用反馈,就能获得环信usb小风扇,数量有限,先到先得!



    d1fddd2e88747ea05968d89f6b696963.jpg


    环信USB小风扇


    Web IM v1.4.11 2017-06-14

     新功能:
    • [sdk] debug.js融合到sdk当中,优化日志内容输出
    • [sdk] 通过Rest屏蔽群组
    • [sdk] 通过Rest发出入群申请
    • [sdk] 通过Rest获取群组列表
    • [sdk] 通过Rest根据groupid获取群组详情
    • [sdk] 通过Rest列出某用户所加入的所有群组
    • [sdk] 通过Rest列出群组的所有成员
    • [sdk] 通过Rest禁止群用户发言
    • [sdk] 通过Rest取消对用户禁言的禁止
    • [sdk] 通过Rest获取群组下所有管理员
    • [sdk] 通过Rest获取群组下所有被禁言成员
    • [sdk] 通过Rest设置群管理员
    • [sdk] 通过Rest取消群管理员
    • [sdk] 通过Rest同意用户加入群
    • [sdk] 通过Rest拒绝用户加入群
    • [sdk] 通过Rest添加用户至群组黑名单(单个)
    • [sdk] 通过Rest添加用户至群组黑名单(批量)
    • [sdk] 通过Rest将用户从群黑名单移除(单个)
    • [sdk] 通过Rest将用户从群黑名单移除(批量)
    • [demo] 聊天窗口中记录可清空
    • [demo] 聊天窗口中发送方聊天记录显示状态(未送达、已送达、已读)
    • [demo] 查看聊天室成员
    • [demo] 通过链接直接打开与好友的对话框
    • [demo] 新增申请加入公开群面板
    • [demo] 在申请加入公开群面板可下拉分页获取公开群
    • [demo] 在申请加入公开群面板可点击群名称可查看群详情
    • [demo] 在申请加入公开群面板可搜索群查看群详情
    • [demo] 在申请加入公开群面板群详情页面可申请加入群组
    • [demo] 群主可同意、拒绝加群申请
    • [demo] 在群主的群成员列表中新增添加/移除管理员、禁言/解禁群成员按钮
    Bug修复:
    • [sdk] 添加好友会产生多余的订阅消息
    • [sdk] 频繁的发送消息会导致消息id重复的问题
    • [sdk] 适配SDK发送文件和图片的大小
    • [demo] 优化sdk/demo.html,修复某些依赖文件找不到的问题
    • [demo] 修复离线消息数量统计不准确问题

     
    webim在线体验:https://webim.easemob.com

    版本历史:更新日志
     
    SDK下载:下载地址 收起阅读 »

    Android 守护进程的实现方式

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

    QQ截图20170615143439.jpg


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

    守护进程的实现

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

    1.模拟前台进程

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    2.JobScheduler机制唤醒

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

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

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

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

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

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


    结语

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

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

    获取好友列表,报错SERVER_UNKNOWN_ERROR = 303

    List<String> usernames = EMClient.getInstance().contactManager().getAllContactsFromServer(); 获取好友列表在第一次登录成功后获取正常,但是当我在其他界面调...
    继续阅读 »
    List<String> usernames = EMClient.getInstance().contactManager().getAllContactsFromServer();

    获取好友列表在第一次登录成功后获取正常,但是当我在其他界面调用这个方法的时候去报错如下:
     
    com.hyphenate.exceptions.HyphenateException: Unknown server error
     
    解决方法:要放到子线程中。
     
    注:我想知道why,why,why!!!!!!!! 收起阅读 »

    环信移动客服v5.20已发布,支持自助开通工单功能以及客服直接处理工单

    客服模式 支持自助开通工单功能以及客服直接处理工单 新增工单页面,支持自助开通工单功能,以及客服直接处理工单,包括新建工单、回复工单、分配工单、修改帮助主题、修改工单状态,筛选工单,等等。 工单功能为增值服务,如需开通,请在工单页面提交申请,环信商务经理...
    继续阅读 »
    客服模式

    支持自助开通工单功能以及客服直接处理工单

    新增工单页面,支持自助开通工单功能,以及客服直接处理工单,包括新建工单、回复工单、分配工单、修改帮助主题、修改工单状态,筛选工单,等等。

    工单功能为增值服务,如需开通,请在工单页面提交申请,环信商务经理会主动联系您。

    申请开通工单功能

    工单功能可以帮助您实现高效的跨部门协作,只需提交申请,并绑定邮箱,即可启动您的工单功能服务。

    步骤如下:
    1. 在工单页面,点击“申请工单功能”;
    2. 填写姓名、电话、企业名称、邮箱,点击“下一步”;
    3. 填写帮助主题(帮助主题为工单的类别),点击“下一步”;
    4. 填写系统邮箱(用于接收和发送工单相关的邮件),点击“提交”。


    提交申请后,请耐心等待,环信商务经理会尽快与您联系。 


    01.png


    新建工单

    客服与客户聊天过程中,可以为客户创建工单。

    步骤如下:
    1. 在会话页面,点击输入框上方的工单按钮;
    2. 填写工单标题,选择优先级、帮助主题、分配技能组、分配坐席,填写工单内容,勾选“附带访客信息”,并保存。


    勾选“附带访客信息”时,工单包含对应的客户信息,可以在工单详情的“客户资料”页签查看。 


    02.png



    处理工单

    客服可以对工单进行回复,分配工单给技能组或坐席,修改帮助主题、优先级、工单状态,查看工单进度,查看客户资料。

    在工单页面,点击任意工单,即可查看工单详情。并执行下述工单处理操作:

    回复工单:回复工单时,如果勾选“发布为公开回复”,工单系统将回复内容通知客户;不勾选时,回复仅客服可以查看。
    • 工单处理:分配技能组、分配坐席、修改帮助主题、优先级、状态。
    • 工单进度:查看工单进度。
    • 客户资料:查看客户资料。

    03.png

    筛选工单在工单页面已为您创建一些默认工单筛选器,帮助您对工单进行分类管理。您还可以创建自定义的筛选器,以满足更具体的需求。创建自定义筛选器步骤如下:在工单页面,点击“自定义筛选”;填写筛选器名称,选择筛选条件,如工单创建时间段、工单创建人、客户名称、工单编号、工单标题、分配技能组、分配坐席、帮助主题、工单状态,并点击“确定”。创建成功后,可以根据自定义筛选器对工单进行筛选。您还可以编辑或删除自定义筛选器。 

    04.png

    工单通知当工单创建成功、分配技能组或客服、得到回复、状态变更时,创建工单的客服将会收到系统消息。 

    05.png

    会话面板聊天窗口优化会话面板聊天窗口优化,支持拖动扩展输入框,以显示更多正在输入的内容。 

    06.png

    留言支持根据更新时间进行筛选留言页面显示留言的更新时间,并支持根据更新时间进行筛选和排序。更新时间指,留言分配的客服变更或留言状态变更的时间。
    • 筛选:点击“自定义留言筛选”,选择“更新时间”范围,点击“筛选查询”,对留言进行筛选。
    • 排序:点击留言列表中“更新时间”右侧的箭头,使留言按照更新时间升序或降序排列。

    07.png

    客户中心显示客户的真实姓名客户中心新增“名字”一列,显示客户的真实姓名。 

    08.png

    管理员模式新增REST API渠道环信移动客服新增REST API渠道。开通REST API渠道并配置服务器信息后,客服向客户回复的消息,将被环信转发到服务器的回调地址中。该功能可用于环信移动客服与第三方服务器之间的消息传递。REST API渠道支持创建多个REST关联,每个REST关联均可作为环信与您的服务器之间收发消息的通道。创建REST关联:[list=1]
  • 进入“管理员模式 > 渠道管理 > REST API”页面;
  • 点击“添加REST关联”按钮,填写关联名称、回调地址,并保存。


  • 系统自动为您生成Client ID、Client Secret、POST API。Client ID和Client Secret用于向环信发送消息时的身份认证;消息API为您向环信发送消息时使用的REST API接口,方法为POST。 


    09.png




    关于REST API渠道的身份认证方式、消息格式,请参考:REST API渠道集成

    注:REST API渠道为增值服务,如需开通,请提供租户ID并联系环信商务经理。

    新增APP关联演示视频

    APP关联信息页新增APP关联的演示视频,视频说明了APP关联的Client ID和IM服务号与即时通讯云的应用之间的关系,以及如何添加APP关联。

    如果已有即时通讯云的应用,可以采取“关联IM账号”的方式创建APP关联。对应关系如下:

    Client ID: APP关联的Client ID与即时通讯云的应用的Client ID一致;
    IM服务号:APP关联的IM服务号对应即时通讯云的应用的一个IM用户。

    进入“管理员模式 > 渠道管理 > 手机APP”页面,点击APP关联信息页中Client ID和IM服务号右侧的问号,可以查看相应的演示视频。

    客户标签支持导入导出


    10.png





    客户标签支持下载模版、导入、导出,方便管理员对客户标签进行批量整理。

    在导入客户标签时,会自动过滤已存在的客户标签,只导入新增的客户标签。 
    attach]7544[/attach]
    “不活跃会话超时自动结束”开关优化

    优化“不活跃会话超时自动结束”开关,该开关不再对待接入会话生效。优化后,该开关打开时,对于客服的进行中会话,如果客户和客服在设定时间内均未回复消息,系统将自动发送提示语给客户,并结束会话。

    如果需要自动结束超时的进行中会话,进入“管理员模式 > 设置 > 系统开关”页面,打开“不活跃会话超时自动结束”开关。
    如果需要自动结束超时的待接入会话,进入“管理员模式 > 设置 > 系统开关”页面,打开“待接入超时结束会话”开关。

    客服列表优化

    优化“管理员模式 > 成员管理 > 客服”页面的客服列表,删除原导出日志按钮。所有客服的登录日志,均可以在“管理员模式 > 统计查询 > 客服时长统计”页面查看并导出。

    Android客服工作台

    当前版本:V3.0

    新功能:

    管理员模式,当前会话支持筛选、转接、关闭
    留言支持批量分配

    iOS客服工作台

    当前版本:V2.1.9

    新功能:

    新增管理员模式,包含管理员首页的数据展示
    支持查看通知详情

    移动客服Android SDK

    当前版本:V1.0.7

    新功能:

    新增发送和接收短视频功能

    移动客服iOS SDK

    当前版本:V1.1.0

    新功能:

    SDK全面升级为动态库,集成更简单,功能更全面
    离线推送支持推送详情
    优化升级HelpDeskUI

    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.20 

    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

    环信网页的onready 轨迹回调在电脑浏览器上有效 但是在手机浏览器上就不会触发了

           环信网页的onready 轨迹回调在电脑浏览器上有效 但是在手机浏览器上就不会触发了 有大神指导是怎么回事吗? 附上代码: <script>     var saleprice = document.getElementById(&...
    继续阅读 »
           环信网页的onready 轨迹回调在电脑浏览器上有效 但是在手机浏览器上就不会触发了 有大神指导是怎么回事吗?

    附上代码:
    <script>
        var saleprice = document.getElementById("spSalaPrice");
        var productName = document.getElementById("productName");
        var productPic = document.getElementById("productPic").src;
        window.easemobim = window.easemobim || {};
        easemobim.config = {
            
            //是否隐藏小的悬浮按钮
            hide: true,
            //自动连接
            autoConnect: true,
            //聊天窗口加载成功回调
            onready: function () {
                easemobim.sendExt({
                    ext: {
                        "imageName": "mallImage3.png",
                        //custom代表自定义消息,无需修改
                        "type": "custom",
                        "msgtype": {
                            "track": {
                                "title": "我正在看:",
                                "price": "$: " + saleprice.textContent,
                                "desc": productName.textContent,
                                "img_url": productPic,
                                "item_url": window.location.href
                            }
                        }
                    }
                });
            },
        };
    </script> 收起阅读 »

    云安全fun享会 | 第三期 《未知安全威胁的检测与防御》

    活动时间:2017年06月24日 13:30—16:30 活动地点:北京市朝阳区酒仙桥北路9号恒通国际创新园C8栋 MeePark    WannaCry、Struts 2等安全事件告诉我们,用规则去防御安全漏洞永远比黑客慢一步,如何在与黑客抗...
    继续阅读 »
    活动时间:2017年06月24日 13:30—16:30
    活动地点:北京市朝阳区酒仙桥北路9号恒通国际创新园C8栋 MeePark



    01.jpg



       WannaCry、Struts 2等安全事件告诉我们,用规则去防御安全漏洞永远比黑客慢一步,如何在与黑客抗争中先知先觉,占据主动地位,是未来信息安全战争的关键!

       云安全fun享会第三期《未知安全威胁的检测与防御》,邀请业界安全专家,与您分享沙盒、RASP、蜜罐、安全态势感知等对抗未知威胁的利器。

    与其惧怕0day,不如来听听我们的安全沙龙!

    02.png




    03.jpg


    椒图科技助理总经理 吴康
    《利用沙盒检测加密及未知WebShell》




    04.jpg


    云锁产品总监 田强
    《RASP技术在中国的落地与实践》


    更多议题陆续添加中,也欢迎您的议题投稿:lidong@jowto.com



    05.png


    2017年06月24日 13:30
    北京朝阳区酒仙桥北路9号恒通国际创新园C8栋 MeePark
    除了精彩的议题外,我们还准备点心和礼品,等你到来
    会务联络:sunyx@jowto.com


    06.jpg




    07.jpg



    08.jpg




    09.png



    10.jpg




    11.png


    本次fun享会活动场地由 MEE-PARK 智能活动空间提供,特此感谢。

    12.png




    活动报名:报名地址 收起阅读 »

    环信推送的一些常见问题

    原文地址 :  http://blog.csdn.net/jyt199011302/article/details/72829520 参考资料 APNS证书创建和上传到环信后台 : http://www.imgeek.org/article/82530874...
    继续阅读 »
    原文地址 :  http://blog.csdn.net/jyt199011302/article/details/72829520

    参考资料
    APNS证书创建和上传到环信后台 : http://www.imgeek.org/article/825308748 
    APNS离线推送文档 : http://docs.easemob.com/im/300iosclientintegration/75apns
    离线推送的集成代码这里就不一一介绍了, 上面文档中写的 很明白了, 接下来说下比较容易误会的几点
    一. 离线推送
    如果app集成时添加
    - (void)applicationDidEnterBackground:(UIApplication *)application {
    [[EMClient sharedClient] applicationDidEnterBackground:application];
    }
    - (void)applicationWillEnterForeground:(UIApplication *)application {
    [[EMClient sharedClient] applicationWillEnterForeground:application];
    }

    App后台静默后,能够保持长连接3分钟左右。超过3分钟,长连接会断开,当前登录的账号,在服务端被认为离线。消息会存入离线消息空间,之后接收的消息会在再次登录后,连接上服务器,然后通过长连接把消息取走,投递给此用户。如果app配置了推送证书,上传了推送证书并且集成了推送功能,服务器会给接收方发一个APNs推送,则会对离线消息进行APNs推送提示消息内容,通知接收方有一条新消息。
    如果想自定义推送的alert,可以在发消息的时候,在消息扩展中添加相应的字段。文档见:APNS内容解析

    离线推送 : 当app被杀死或者进入后台三分钟之后
    消息回调 : app在前台及app进入后台三分钟之内

    注意 : 环信支持推送消息,只是目前还不能根据标签推送给特定用户组,也暂不支持推送模板。CMD消息没有推送,好友请求也没有推送

    收不到离线推送时可以从下面几个方面找下原因

    1.测试apns推送的时候,接受消息方的app是杀掉状态吗,或者进入后台三分钟以后
    2.看看你环信后台上传的证书名称与工程中初始化SDK那里填的证书名 是不是相同的
    3.配置证书时候填的id与你工程中的bundle id 是否相同
    4.devicetoken有没有传给环信SDK。即查看管理后台中,对应 IM 账户下是否有您刚刚写的证书名。(如果没有,请检查您是否得到了 deviceToken)
    5.确认Xcode环境是否配置正确 ,Build Settings---signing,看Debug对应的是不是开发的,Release对应的是不是生产的
    6.在确认xcode运行环境是否正确 (Product-->Scheme-->Edit Scheme, 开发证书选Debug,生产证书选Release)
    7.证书制作上传过程是否有问题,配置证书的时候是否设置了密码,正确的步骤可以参考:http://www.imgeek.org/article/825308748。另外可以用推送工具进行验证。
    8.如果以上都没有问题,可以尝试重新制作上传一下推送证书。
    对照这些检查一下,基本上就是这些原因
    如果上面几点都符合的话,看下重新登录之后是否可以收到之前收不到推送消息
    可以的,话提供一下AppKey,证书名 (查下证书是否被封)以及收不到的推送消息的消息id及发送方和接收方log
    log导出请看这篇文章: http://www.imgeek.org/article/825308785 然后转成txt格式上传到工单上,同时注明上述配置都正确 , 环信这边来查下消息推送记录

    注意 :后台没有证书名 是指用户列表后面没有显示证书名。这个证书名是SDK初始化的时候传的字符串,用户登录之后会进行绑定。
    如果用户没有绑定证书名的话,肯定收不到推送的。这个证书名是用户登录之后绑定的,要确认下初始化SDK的时候有没有传。 options.apnsCertName = apnsCertName;

    ==============常见问题==============

    Q : iOS apns离线推送证书apns的离线推送可以和友盟(极光)推送共用一个证书吗?
    A : 环信的推送只要和后台上传的证书对应就可以实现,其他的不关心。
    首先苹果推送证书的生成都是统一的方式,这个不区分是极光(友盟)推送证书还是个推证书等等。使用的推送证书只要按照正确的苹果推送证书生成流程创建,都可以使用。
    环信添加推送证书可以看http://www.imgeek.org/article/825308748 不是要求必须重新生成推送证书 
    Q : 好友申请通知的离线推送?
    A : 我们的好友体系,添加好友的申请不支持离线推送。
    如果你们是使用App本身的好友体系,可以在app的添加好友业务上向被添加的好友发送文本消息,在EMMessage的ext中设置自定义字段,来区分此条文本消息是否用于好友申请提示,由此来判断处理UI的显示。

    Q : iOS的杀死进程远程推送和服务端有关么
    A : 如果客户端把远程通知给关了肯定就收不到通知,我们服务器会检测客户端是否有deviceToken,有的话才会把消息发送到deviceToken对应的设备上

    Q : 每个项目创建了一个开发的推送证书一个生产的推送证书。这俩证书什么时候要做切换?
    A : 在App上传AppStore前需要修改App内初始化SDK设置的推送证书名,EMOptions的apnsCertName。
    注意,这里的值需要和在Console管理后天上传时设置的证书名一致。

    Q : 绑定devicetoken的时候是否需要先登录到环信?
    A : 绑定是需要登录过之后才进行的,
     - (EMError *)bindDeviceToken:(NSData *)aDeviceToken; 是把deviceToken传给SDK。调用登录,SDK会进行绑定。也可以调用 - (void)registerForRemoteNotificationsWithDeviceToken:(NSData *)aDeviceToken
    completion:(void (^)(EMError *aError))aCompletionBlock;自己绑定
    需要判断是否已经登录,如果已经有登录的账号,再登录会返回 已登录的错误。

    Q : 离线推送在客户端怎么设置显示详情?
    A :
     EMPushOptions *pushOptions = [[EMClient sharedClient] pushOptions];
    pushOptions.displayStyle = EMPushDisplayStyleMessageSummary;

    可以设置离线推送消息显示具体内容还是只显示-您收到一条消息
    要设置在登录成功之后,然后要用服务器拉取一遍APNS 属性
    EMError *error = nil;
    EMPushOptions *options = [[EMClient sharedClient] getPushOptionsFromServerWithError:&error];

    然后在修改displayStyle

    Q : 两个APP通信,如果只希望其中一类APP能收到推送,而另一端的APP不希望收到推送,是不是不希望收到推送的APP不配置证书就好了?
    A : 两个App的推送证书都是在同一appkey下单独配置的,如果不希望收到推送,可以对此App不配置推送证书,同时在App代码中注释掉注册远程通知的相关代码。
    bundle id对应的证书也可以取消push的功能,针对App不使用任何远程推送服务,包括其他第三方的,如果App还需要其他第三方的推送服务,请忽略这句话。

    Q : 不配置推送证书的APP是不是只有刷新的情况下才会显示新的消息?不刷新的情况下APP是看不到新的消息?
    A : 不配置推送功能的App,只有在用户登录成功后,才能通过长连接的接收消息回掉中拿到消息体。

    Q : 在开发环境下收到了离线推送消息,但是在生成环境下没有收到?
    A : 看一下SDK初始化时,是否设置的apnsCerName与生产环境证书上传时填写的证书名一致,还有是否为adhoc打包成ipa文件安装测试的。

    Q :发送消息1,2,3,4,5 对方收到推送 2 1 5 顺序不对而且丢失, app角标也不对
    A : 1.首先,苹果不保证所有远程推送的到达率。这个可以看苹果官方文档。
    Because the delivery of remote notifications is not guaranteed, never include sensitive data or data that can be retrieved by other means in your payload.
    2.我们只保证,把离线消息执行远程推送,发给苹果服务器。苹果服务器是否能够百分之百把所有推送送达到指定移动端,这个根据苹果的策略,当 APNs 向你发送了多条推送,你的设备在 APNs 那里下线了,这时 APNs 到你的手机的链路上有多条任务堆积,APNs 的处理方式是,只保留最后一条消息推送给你,然后告知你推送数。那么其他消息会被APNs丢弃。
    3.我们保证的是离线消息,当用户重新登录时,可以都接收到。
    4. 如果需要找后台查询离线消息(前提接收方已绑定deviceToken)是否成功,需要提供离线消息的messageId,接收方环信id
    5.我们的推送角标,是接收方的在服务端的离线消息数。
    https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1
    这是官方文档

    20170605122458543-1.jpeg



    Q11 :ios是怎么判断离线了 然后发推送的啊 有时候把应用杀掉后 半天收不到推送
    A : rest可以查用户的状态,推送前提是此用户有devicetoken已经绑定成功
    如果账号所有配置都没问题,杀掉后,其他人发的消息,过几秒就能看到推送
    Q12 : Q11不管用会是什么原因呢
    A : 配置,还有账号在我们这绑的deviceToken

    Q : 多个app共用一个appkey 推送证书怎么配置呢
    A : 后台可以上传多套推送证书。

    Q :程序关闭后推送了一个消息,点击后怎样获取到环信传过来的数据
    A : 需要用户点击横幅后,重新启动App,这时从- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions中,获取字典launchOptions,UIApplicationLaunchOptionsRemoteNotificationKey这个key下的数据,就是aps的字典数据

    二 . 消息回调
    app的长连接存在的时候,环信服务器检测您为在线状态,是不会给app推送消息的。app端在线的情况下,消息会通过长连接直接收取(didreceivemessage),收到消息,SDK会通过回调通知给上层。app通过收消息的回调拿到消息对象,然后解析并展示UI。
    目前我们不支持App切后台后,可以一直执行。
    我们SDK在切后台后,实现[[EMClient sharedClient] applicationDidEnterBackground:application];,会保持,直到被系统释放iOS目前其他方式应该都无法去实现一直保持App的活跃状态了。

    ==============常见问题==============
    Q : 本地推送声音设置在哪 ?一条消息推送两声
    A : 推送声音设置的要自己实现,具体可参考demo里的ChatDemoHelper类和MainViewController类里的
    - (void)showNotificationWithMessage:(EMMessage *)message方法,该方法中有发送本地推送做的一系列操作,本地通知怎么做的,本地通知触发几次,一条消息推送几声,一下接收到多条消息响几声,都需要用户自己实现.

    Q : 视频通话,推送怎么实现?
    iOS 3.2.3之后,如果在实时通话接收方不在线时,发送提醒。
    A : 1.在发起实时音视频通话前,需要设置EMCallOptions对象属性isSendPushIfOffline为YES;
    2.遵守协议EMCallBuilderDelegate,实现其中的- (void)callRemoteOffline:(NSString *)aRemoteName 委托方法。
    3.在第2步的方法中向 aRemoteName用户发送单聊消息。
    如果被叫方已注册远程通知且绑定deviceToken,会收到对应消息的APNs推送,点击横幅来唤醒App。
    上面是接收方离线的情况。如果接收方长连接还未断开,只是App切到后台,需要在回调- (void)callDidReceive:(EMCallSession *)aSession中判断当前App是否在后台,如果是弹出本地通知。

    Q : EMCallOptions *options = [[EMClient sharedClient].callManager getCallOptions]; //当对方不在线时,是否给对方发送离线消息和推送,并等待对方回应 options.isSendPushIfOffline = YES; [[EMClient sharedClient].callManager setCallOptions:options];isSendPushIfOffline设置为YES后,A用户呼叫B用户,B用户处于离线状态,但B用户没有收到推送。
    A : 1. 先确接收方杀掉App后,文本消息是否能收到APNs推送。
    2.在1点确认App杀掉可以收到推送前提下,确认实时音视频发送方代码执行顺序如下:
    (1) EMCallOptions *callOptions = [[EMClient sharedClient].callManager getCallOptions];
    callOptions.isSendPushIfOffline = YES;
    callOptions.offlineMessageText = @"提示文本";//可选
    [[EMClient sharedClient].callManager setCallOptions:callOptions];

    (2) callManager调用
    - (void)startVideoCall:(NSString *)aUsername
    completion:(void (^)(EMCallSession *aCallSession, EMError *aError))aCompletionBlock;

    或者
    - (void)startVoiceCall:(NSString *)aUsername
    completion:(void (^)(EMCallSession *aCallSession, EMError *aError))aCompletionBlock;

    Q : 推送的提示音可不可以自定义啊。
    A: 推送的提示音目前不支持自定义,本地通知的你们以自己去设置。
     
    Q : app压后台,立刻收到聊天推送来的信息,点击通知栏信息,捕获不到唤起程序事件
    A : App切后台后,长连接为断开前,当前弹出的横幅是本地通知,那么此时唤醒时间是本地通知的回调
    - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;

    iOS10后
    - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler;

    Q : 环信在离线状态下能收消息,但是程序运行状态按home按键进入后台的时候无法接受消息,怎么处理
    A : App切入后台一段时间内,长连接还未断开,这时候接收消息都是通过SDK的接收消息回调(EMChatManagerDelegate)来收消息,不会执行APNs推送。
    如果是需要弹出横幅提醒,需要在接收消息的回调方法中,判断[[UIApplication sharedApplication] applicationState]为UIApplicationStateBackground,然后实现本地通知。可以参考demo中的处理
    (ChatDemoHelper的- (void)didReceiveMessages:(NSArray *)aMessages)
    [attach]7500[/attach][size=13]


    [/size]
    收起阅读 »

    环信CEC亮相GMIS 2017峰会,智能客服大有可为!

        近日,2017全球机器智能峰会(GMIS 2017)在北京圆满举行,47位重磅嘉宾带来的32场演讲、4个圆桌论坛、1场人机大战及5个主题Session轮番上演,使GMIS 2017成为聚焦人工智能及相关领域的顶级行业盛宴。说出来你可能不信,环信是唯一受...
    继续阅读 »
        近日,2017全球机器智能峰会(GMIS 2017)在北京圆满举行,47位重磅嘉宾带来的32场演讲、4个圆桌论坛、1场人机大战及5个主题Session轮番上演,使GMIS 2017成为聚焦人工智能及相关领域的顶级行业盛宴。说出来你可能不信,环信是唯一受邀参展GMIS 2017的智能客服公司!

    001.jpg


       LSTM 之父&Dalle Molle 人工智能研究所副主任Jürgen Schmidhube大胆预测,在未来几年人类将创造出具有灵长类动物智能的人工智能系统。而现阶段AI在行业发挥最大生产力更可能是在垂直行业,特别是客服行业,智能客服聊天机器人已经展示给世人强大的生产力。

    002.jpg


      也许你已经被环信诸如“全球最大、国际领先、国内市场占有率第一...”等狂轰滥炸的晕头转向了,我也很懵逼啊,我只是一个新媒体小编,对于市场第一描述词汇量的匮乏我也很绝望啊...

    003.jpg


       环信作为智能客服企业的先行者,基于自然语言处理和机器学习技术推出了环信智能客服机器人,辅助或代替人工客服精准回答常见或高频问题,降低企业客服人力成本。目前,环信在客服领域已经服务了58541家标杆客户,积累了人工智能在客户服务行业落地的大量最佳实践。



    004.jpg


    为什么环信在AI方面有领先优势,投资人说的对!




    005.jpg


       主会场演讲嘉宾美国通用电气GE Transportation CTO Wesly Mukai谈到机器学习目前已经应用在美国铁路运输这种非常实际的领域中,为提高效率做出了很大贡献。Wesly先生会后来到环信展台深入了解国内智能客服机器人在客服行业的应用,他认真听取了环信客服聊天机器人的实现方式(单轮会话、多轮会话、人机协作...)和应用场景以及帮助客户取得的效果和成绩,他认为中国企业在智能客服行业的探索已经走在了世界前列,同时他很看好AI在垂直领域所爆发的强大生产力。



    006.jpg


    Wesly和环信员工谈笑风生(照片由Wesly先生私人翻译帮助拍摄提供)




    007.jpg


       Citadel 首席人工智能官邓力发表了以“无监督学习的最新进展(Recent Advances in Unsupervised Learning)”为主题的演讲。他认为,聚类方法、GAN 和变分自编码器(VAE)等传统无监督学习方法关注的重点是对输入数据的结构建模。腾讯 AI Lab 副主任俞栋在大会上洞悉了语音识别领域的前沿研究,今日头条、第四范式等嘉宾们从语音交互领域、自然语言处理及人工智能平台等领域切入,详细解构了其在人工智能时代所实践的产业创新,展示出AI技术在不同领域产生的巨大价值及未来机遇。

    008jpg.jpg


       当下,科技和创新进入拐点式爆发,人工智能的浪潮席卷全球。在此背景下,GMIS 2017作为国内首次汇集起全球人工智能和机器人领域顶级专家的大会,中立、权威、系统地呈现了机器智能相关技术的前沿研究,为全球人工智能领域前沿专家学者共同探讨机器智能如何从技术转化成产品和应用提供了绝佳平台,同时深切关注人工智能未来能够解决哪些具体问题,及如何帮助人类智慧生活的体验得到提升。GMIS将对行业产生积极而深远的影响,并开启人工智能发展的新起点。

    009.jpg


       最后提前剧透:环信联合Gartner即将在国内发布客服行业首个机器人选型报告《智能客服机器人之客户服务行业最佳实践》我会到处乱说么?
     
    报告抢先看
     
       市场上关于机器人的分类很多,误区也有很多。往往人们会将客服机器人等同于聊天机器人,但客服机器人其实只是聊天机器人的一种。聊天机器人主要分为两个大类:闲聊机器人与Task Oriented 机器人。Task Oriented 机器人是以任务目的为导向的机器人,又包括个人助理机器人与客服机器人。


    010.jpg


    上表对闲聊机器人、个人助理机器人、客服机器人从解决问题领域、平台系统开放性、技术方案的角度进行了详细比较。


    011.jpg


    智能客服机器人概述与分类 收起阅读 »

    IM-SDK和客服SDK并存开发指南—Android篇

          环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴...
    继续阅读 »
      
       环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云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/300visitoraccess/androidsdk 。
    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-hangzhou.aliyuncs.com/kefu-android-demo.zip 收起阅读 »

    【环信公开课第13期视频回放】智能硬件创业供应商选型经验指南

    罗飞老师已经近一年多没有对外分享,全情投入到人工智能硬件创业上。这次分享罗飞老师带来了他这一年多的心血结晶~ 能说会道,宝宝的好伙伴-小墨​ 语音交互 | 运动控制 | 远程监控 | 视频通话 | 海量内容    随着移动互联时代向DT时代的逐渐...
    继续阅读 »
    罗飞老师已经近一年多没有对外分享,全情投入到人工智能硬件创业上。这次分享罗飞老师带来了他这一年多的心血结晶~



    微信图片_20170531170220.jpg


    能说会道,宝宝的好伙伴-小墨​
    语音交互 | 运动控制 | 远程监控 | 视频通话 | 海量内容


       随着移动互联时代向DT时代的逐渐演进,各种新模式新技术层出不穷,作为众多行业的顶层设计AI才慢慢向世人展示出了其惊人的生产力,人工智能时代也随之悄然来临!但人工智能落地并不是这么容易,它的创业门槛更高, 会涉及到包括软硬件等方方面面。

       如果您是一位想在机器人、智能家居、智能硬件等行业创业的老铁,选择对了靠谱的供应商很大程度上决定了创业成败。上周四我们邀请到智众互动CEO老司机罗飞,他和大家一起分享关于了“小墨机器人”项目创业的选型实践。
     
       罗飞丨智众互动CEO,国内流行thinkphp核心开发者之一,曾就职于新浪、创新工场。著有《内外兼修》、《swift入门实战》等书, 其中《内外兼修》是由李开复、刘东华、鸟哥等写序推荐。环信公开课第13期主题回顾



    微信图片_20170531172941.jpg


    罗飞老师在直播现场


    环信公开课第13期主题回顾


    1.智能硬件音视频通话哪家强?环信音视频通话的最佳实践!

    • 环信语义理解:是以人机交互为核心技术、语义理解为核心应用的人工智能交互服务。
    • 环信人脸识别:是一款用于提供图像和视频帧中人物分析的在线视频通话服务。
    • 环信视频处理:智能避障能在有障碍物的情况下判断并自动躲避。
    • 环信智能监控:嵌入强大的监控系统,实时监测孩子最新的动态。


    2.科大讯飞、思必驰、云知声的语音识别和语音合成各有特点,如何选择?  

    3.瑞芯微、全志,哪家芯片适合自己?  

    4.如何做大数据,通过语义分析让机器人可以像人这样回答问题?


    参加环信公开课还有礼物相送 



    QQ图片20170531172203.jpg



    罗飞老师签名书籍


     
    公开课视频回放
     







     
    公开课合作及更多信息,请添加“环信MM”


    扫码.gif


     
    小墨机器人正在众筹:小墨机器人 收起阅读 »

    环信移动客服v5.19已发布,视频客服新增截图、静音等功能

     客服模式 会话标签支持hover显示 当会话标签过长无法在列表中完整显示时,可以将鼠标放在标签上,查看完整的会话标签。  实时视频支持截图、静音功能 Web版客服工作台的实时视频支持截图、静音功能。和app、网页客户进行视频聊天时,客服可以截...
    继续阅读 »
     客服模式

    会话标签支持hover显示


    当会话标签过长无法在列表中完整显示时,可以将鼠标放在标签上,查看完整的会话标签。 

    001.png


    实时视频支持截图、静音功能

    Web版客服工作台的实时视频支持截图、静音功能。和app、网页客户进行视频聊天时,客服可以截取客户的视频截图;需要和同事讨论时,可以暂时对己方视频进行静音。

    注:实时视频功能为增值服务。仅Chrome浏览器在https模式下支持。

    管理员模式

    关键字匹配


    新增关键字匹配功能,支持为客户消息中的关键字设置正则表达式和自动回复,当关键字匹配正则表达式时,系统自动回复一条消息。使用关键字匹配功能,可以帮助客服和机器人更加灵活、快速、准确地解答客户的问题。

    关键字匹配功能为增值服务,如需开通,请提供租户ID并联系环信商务经理。

    使用示例:

    1. 进入“设置 > 关键字匹配”页面,点击“添加新的匹配组合”,设置规则名称、正则表达式、系统自动回复,并保存。
    • 正则表达式:设置正则表达式用于匹配客户消息中的关键字;
    • 系统自动回复:设置系统自动回复。在任意位置添加##,可在##位置显示匹配到的关键字。

    002.png

    2. 当客户消息中的关键字匹配上设置的规则时,系统自动回复一条消息。访客端示例: 

    003.png

    支持导出自定义报表自定义报表功能允许管理员根据不同的时间段、指标项目和指标维度自由搭配出不同的报表,能够满足多样化的报表需求。新增自定义报表导出功能,可以将数据导出为CSV格式的文件,便于在本地保存。注:自定义报表功能为增值服务,如需开通,请提供租户ID并联系环信商务经理。支持导出公共常用语公共常用语支持全部导出,方便管理员对公共常用语进行批量整理。进入“设置 > 公共常用语”页面,点击“导出常用语”按钮,导出全部常用语。在本地对公共常用语进行编辑后,可将系统中的常用语全部删除,并重新导入编辑后的常用语。 

    004.png

    自定义客户资料支持级联列表客户资料的自定义字段支持级联列表,满足多样化的客户资料需求。进入“设置 > 客户资料自定义”页面,点击“添加自定义字段”按钮,可以添加级联列表。级联列表选项最多支持10级。 

    005.png

    管理员通知支持发送全员和技能组在消息中心页面,管理员发送通知给客服团队时,可以选择全部、技能组、或客服成员。选择收件人的方法:
    • 输入@、@全部、@技能组名称、@客服昵称,在下拉列表中选择收件人。
    • 在右侧客服列表中直接选择全部、技能组、客服,或搜索成员并选择。


    注:输入@符号时,请切换至英文输入法。 

    006.png


    管理员模式顶部导航栏优化

    优化管理员模式顶部导航栏,删除体验指南页面。原体验指南提供的“商城”demo,可以前往“渠道管理 > 手机APP”页面,通过扫描app关联页面的二维码下载。 
     
    Android客服工作台

    当前版本:V2.9

    新功能/优化:

    管理员模式新增实时监控功能
    支持查看与客服同事的历史消息
    支持在进行中会话查看客户的历史消息(可跨会话)
     
    移动客服Android SDK

    当前版本:V1.0.6

    新功能/优化:

    优化部分API,使用更简单
    商城demo与EaseUI全面使用亮丽新UI
    UI全面支持国际化(根据手机设备语言切换) 
    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.19 

    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

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

    首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持: defaultConfig {         multiDexEnabled true    } dependencies { compile 'com.Andr...
    继续阅读 »


    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个方法数了,,,
      收起阅读 »

    iOS SDK 日志文件的导出

    环信SDK提供2.x和3.x两个版本。SDK会写入日志文件到本地。日志文件路径如下: 2.x 沙箱Library/EaseMobLog3.x 沙箱Documents/HyphenateSDK/easemoblog 访问沙箱目录模拟器 打印NSHomeDirec...
    继续阅读 »
    环信SDK提供2.x和3.x两个版本。SDK会写入日志文件到本地。日志文件路径如下:
    • 2.x 沙箱Library/EaseMobLog
    • 3.x 沙箱Documents/HyphenateSDK/easemoblog
     访问沙箱目录
    • 模拟器 
    • 打印NSHomeDirectory()
    • 打开Finder前往

    sandboxPath.png


    goFinder.png

    • 真机
    • 打开Xcode连接设备,前往Xcode --> Window --> Devices

      aDevice.png

        • 进入Devices界面

          deviceDownloadContainer.png

            • 选择Download Container之后会下载到本地一个.xcappdata文件。选中这个文件鼠标右键显示包内容。

              showContainer.png

                • 可以访问到沙箱目录

                  deviceSbPath.png

                  SDK日志
                  • ​2.x

                  2xLog.png

                  • 3.x

                  3xLog.png


                    收起阅读 »

                    集成环信遇到的相关问题整理

                    最近在整理这段时间被别人问到引入环信可能会出现的问题,记得的也不太多,想到一个就在这里记录一个吧,如果有遇到过本文中没有列出来的,可以问我,我会一一解答的 原文地址: http://blog.csdn.net/jyt199011302/article/deta...
                    继续阅读 »
                    最近在整理这段时间被别人问到引入环信可能会出现的问题,记得的也不太多,想到一个就在这里记录一个吧,如果有遇到过本文中没有列出来的,可以问我,我会一一解答的
                    原文地址: http://blog.csdn.net/jyt199011302/article/details/68483995

                    1. pod引入的Hyphenate里面的.h文件中和手动下载的sdk相比会缺少Hyphenate.h 。
                    A :  主要是pod 问题 本地仓库太旧了, 终端行pod repo update, 之后在pod search 'Hyphenate' 如果可以找到3.3.0版本, 就可以下载了 podfile 里面 platform 要指定8.0

                    2. iOS SDK 从低版本 升到3.3.0 后运行报错 (集成动态库版本报错)
                    dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate
                      Referenced from: /Users/white/Library/Developer/CoreSimulator/Devices/BE0DDC26-96AE-4396-A6C5-48DC6938042B/data/Containers/Bundle/Application/4F9F570A-44B5-4F81-AD19-F7AA38D26E40/SYSchoolProject.app/SYSchoolProject
                      Reason: image not found

                    20170330110241533.jpeg


                    A : 在Build setting -> General这里加上。 还有这里也加上 改不能成optional,
                    注意 : 改成optional之后会导致初始化为null

                    20170330104308569.jpeg




                    3.在AppDelegate中执行[EaseMob sharedInstance]崩溃
                    A : other link flags添加“-ObjC”选项(注意:O和C大写)


                    4. pod导入EaseUI 时报错 
                    A : 先进入Podfile文件中,添加pod 'EaseUI', :git => 'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git' ,保存退出之后执行pod update即可 ,如果还是失败,可以升级一下pod版本

                    屏幕快照_2017-05-22_上午10.27_.07_.png



                    5.‘Hyphenate/EMSDK.h’ file no found
                    A : 换下引用#import <HyphenateLite/HyphenateLite.h>
                         或者#import <Hyphenate/Hyphenate.h>
                         如果此方法不行, 可以试试选中你的项目中的Pods -> EaseUI->Build Phases->Link Binary With Libraries ,点➕->Add Other ,找到工程里面,Pods里面的Hyphenate文件夹下面的Hyphenate.framework 点击open,重新编译就好了

                    20170331200729906.jpeg




                    6. 

                    20170331110834145.jpeg


                    A :  可以参考问题2的基础上, 再看下相对路径那里


                    7.集成动态库上传AppStore出现问题, 打包上线时报错
                    ERROR ITMS-90087: "Unsupported Architectures. The executable for xiantaiApp.app/Frameworks/Hyphenate.framework contains unsupported architectures '[x86_64, i386]'."
                    A :  遇到这个问题的小伙伴一定是没有认真看咱们环信的官方文档,
                    由于 iOS 编译的特殊性,为了方便开发者使用,我们将 i386 x86_64 armv7 arm64 几个平台都合并到了一起,所以使用动态库上传appstore时需要将i386 x86_64两个平台删除后,才能正常提交审核

                    在SDK当前路径下执行以下命令删除i386 x86_64两个平台
                    实时音视频版本Hyphenate.frameworklipo Hyphenate.framework/Hyphenate -thin armv7 -output Hyphenate_armv7 lipo Hyphenate.framework/Hyphenate -thin arm64 -output Hyphenate_arm64 lipo -create Hyphenate_armv7 Hyphenate_arm64 -output Hyphenate mv Hyphenate Hyphenate.framework/
                     
                    不包含实时音视频版本HyphenateLite.frameworklipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7 lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64 lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite mv HyphenateLite HyphenateLite.framework/
                    拿实时音视频版本版本为例 : 执行完以上命令如图所示

                    20170401112052481.png


                    运行完毕后得到的Hyphenate.framework就是最后的结果,拖进工程,编译打包上架。

                    20170401112216045.png


                    注意 : 
                    1. 最后得到的包必须真机编译运行,并且工程要设置编译二进制文件General->Embedded Bunaries.
                    2. 删除i386、x86_64平台后,SDK会无法支持模拟器编译,只需要在上传AppStore时在进行删除,上传后,替换为删除前的SDK,建议先分别把i386、x86_64、arm64、armv7各平台的包拆分到本地,上传App Store时合并arm64、armv7平台,并移入Hyphenate.framework内。上传后,重新把各平台包合并移入动态库


                    打包时还有可能报这个错误
                    ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at 'Payload/xiantaiApp.app/EaseUIResource.bundle' does not contain a bundle executable. If this bundle intentionally does not contain an executable, consider removing the CFBundleExecutable key from its Info.plist and using a CFBundlePackageType of BNDL. If this bundle is part of a third-party framework, consider contacting the developer of the framework for an update to address this issue."
                    A :  ​从EaseUIResource.bundle中找到info.plist删掉CFBundleExecutable,或者整个info.plist删掉



                    8.ios apns推送是什么原因导致这个错误
                    注册deviceToken失败:application:didFailToRegisterForRemoteNotificationsWithError: Error Domain=NSCocoaErrorDomain Code=3000 "未找到应用程序的“aps-environment”的授权字符串" UserInfo={NSLocalizedDescription=未找到应用程序的“aps-environment”的授权字符串}
                    A: 工程配置没有打开推送功能。

                    9.运行demo报这个错误

                    20170519110027739.png


                    A: 没有存储空间了。
                     
                     
                    10. SDK3.3.1 以上版本手动导入EaseUI报错
                    A : 由于demo是用pod集成的,所以直接引入demo中的EaseUI会缺少相关文件,可以直接拖入附件中的EaseUI
                    如果引入之后报如下图的错误

                    10.1_.png



                    10.2_.png


                    其实碰到上面这个问题还是很好解决的,这个是因为用到了UIKit里的类,但是只导入了Foundation框架,这个错误在其他类里也会出现,我们可以手动修改Founfation为UIKit,但是我不建议这么做,第一这个做法的工程量比较大, 在其他类里面也要导入,二,不利于移植,当以后环信更新的时候我们还是需要做同样的操作,这里我的做法的创建一个pch文件,在pch文件里面导入UIKit。解决办法:建一个PCH文件在里面添加如下代码:

                    10.3_.png



                    以上应该会正常了,但是如果集成的是不包含实时音视频的SDK, 您导入的EaseUI不是Lite版的,  那么此时还会报跟第六点一样的错误 , 需要导入EaseUILite 版本,所以会找不到Hyphenate/Hyphenate.h,如果是手动集成,建议在xcode设置一下Build Settings> GCC_PREPROCESSOR_DEFINITIONS >ENABLE_LITE=1,这样easeui就去找HyphenateLite/HyphenateLite.h
                    也可以通过pod集成,文档上针对easeui集成Full版本和Lite版本sdk特殊的说明http://docs.easemob.com/im/300iosclientintegration/85easeuiguide
                     
                    或者不想导入Lite版的 , 只想引入EaseUI 
                    这时需要把 #import <Hyphenate/Hyphenate.h>注释掉,然后把报错地方的Hyphenate换成HyphenateLite就可以了
                     
                     
                    11. 

                    1.png


                    A : 可以删除或者重命名Podfile.lock文件,重新执行pod install命令 收起阅读 »

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

                       这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。   ios篇 APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录...
                    继续阅读 »
                       这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
                     
                    ios篇
                     Android篇昵称头像篇 直播篇[list=1]
                  • 一言不合你就搞个直播APP
                  •  客服集成[list=1]
                  • IM-SDK和客服SDK并存开发指南—Android篇
                  • IM-SDK和客服SDK并存开发指南—iOS篇
                  •  开源项目
                     
                    持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
                      收起阅读 »

                    环信CEC(客户互动云):“云通讯+服务云+智能营销”构建从用户服务到用户营销的完整闭环

                        近日,客户世界-洞察者2017夏季论坛在深圳顺利举行,会上发布了行业权威的《中国客户中心现状与变革报告(2017)》。基于报告的调研分析结果以及专家评委会的打分,环信移动客服荣获《客户世界》2017年度编辑推荐“全媒体客服”标杆品牌。环信CEO刘俊彦表...
                    继续阅读 »
                        近日,客户世界-洞察者2017夏季论坛在深圳顺利举行,会上发布了行业权威的《中国客户中心现状与变革报告(2017)》。基于报告的调研分析结果以及专家评委会的打分,环信移动客服荣获《客户世界》2017年度编辑推荐“全媒体客服”标杆品牌。环信CEO刘俊彦表示:“中国客户中心发展经历了从呼叫中心(Call Center)到接触中心(Contact Center)的蜕变,再到互动中心(Engagement Center)的变革,全媒体客服已经从传统客户服务形态的终点逐渐转化成了SaaS客户互动形态的起点。随着2017年环信CEC(客户互动云)的发布,人工智能驱动的互动中心(AI-driven Engagement Center)大幕即将拉开,整个SaaS客服行业将被重构和赋能。”

                    ][F6S4LQU291EE7O[}HHPKN.png


                    环信荣膺《客户世界》全媒体客服标杆品牌
                     
                       AI技术经过整整60年的发展在包含客服在内的一些特定的业务场景实现了突破。技术应用的准确率达到大规模商业应用的要求,开始广泛在一些特定的领域进入实际应用阶段。消费者连接技术、接触技术和体验技术的不断创新正深刻改变着客服行业。随着全新的人机智能时代的到来,深度学习、认知计算、服务机器人、增强现实空间又在持续改变客户交互的方式、能力与体验,以致冲击客户中心现有的运营方式。共享经济理念、区块链技术等甚至会对服务与管理范式带来更巨大的革命。
                     
                       《中国客户中心现状与变革报告(2017)》研究项目以技术为前导,关注客服场景下各种新渠道、新触点、新运营、新分析;关注新技术潮流推动下客户中心的日常管理、团队建设、组织变革、人才培养等方面的变革和趋势。面向超过200家本领域甲方企业客服高级负责人(客服总经理/呼叫中心总经理/运营总监/技术总监)发放调研问卷,搜集反馈及评价意见。环信凭借其稳定先进的产品技术能力,优秀的服务水平以及大客户优势获得了调研的一致肯定。
                     
                    《中国客户中心现状与变革报告(2017)》对环信评价

                       环信是国内最早提供全渠道客服解决方案的厂商之一,产品结构完整,其即时通信云产品、移动客服产品、智能客服机器人产品和营销云产品构成从客户互动渠道,到客户服务,到主动营销的客户互动中心完整解决方案。环信服务大客户能力强,在电商、保险、证券、汽车等主要行业的龙头企业客户中拥有成熟的案例。环信独具优势的IM长连接技术,可以提供具备更高可靠性的全渠道客户互动体验;环信一直致力于推动人工智能和大数据在客服行业的落地,其智能客服机器人产品技术先进,有众多大型企业成功实施案例,其“客户声音”产品具有行业前瞻性,值得期待!

                    )DLD4)GOJZZT{_H((IYOM.png


                       环信CEO刘俊彦给与会行业技术领袖作了关于《人工智能驱动的客户互动云(Customer Engagement Cloud)》的主题分享。他在接受年度推荐全媒体客服标杆品牌奖项时表示 :“环信一直致力于推动整个SaaS客服行业的蓬勃发展,随着全媒体客服的完善以及AI的逐渐成熟, 环信预测“云通讯+服务云+智能营销”将构成从用户服务到用户营销的完整闭环。因此,2017年 ,环信整合旗下即时通信云、移动客服、智能客服机器人和主动营销产品线,推出环信CEC (Customer Engagement Cloud),向企业提供从客户互动渠道,到客户服务,到精准客户营销 的客户互动全流程解决方案。”同时,刘俊彦认为:“全媒体客服已经从传统客户服务形态的终点转化成了SaaS客户互动形态的起点,随着2017年环信CEC(客户互动云)的发布,人工智能驱动的互动中心(AI-driven Engagement Center)即将来临,整个SaaS客服行业将被重构和赋能。”

                    QQ图片20170524105853.png


                    环信CEC:“云通讯+服务云+智能营销”构建从用户服务到用户营销的完整闭环

                    QQ图片20170524105924.png


                    环信眼中的客服行业智能变革:客户服务全面人工智能化

                    QQ图片20170524105957.png


                    环信行业标杆客户全面覆盖

                    环信CEC(客户互动云)矩阵

                    QQ图片20170524110032.png


                    客户互动云(Engagement Cloud)核心特性

                    1、全渠道客户互动:全面支持网页、微信、微博、APP/IM、工单和呼叫中心等主流客户互动渠道。其中,环信业界领先的IM长连接技术支持千万级并发,保证消息必达,助力企业打造极致的移动端客户服务体验。所有渠道支持双向互动,如主动回呼,多渠道统一推送,基于用户行为的自动营销等,真正将服务通道与营销通道融合,实现客户中心从成本中心向利润中心的升级。

                    2、视频客服:实时双向视频客服,支持Android、iOS、Pad及主流PC和手机浏览器等多平台接入,低延迟,1080P高清,支持客户端和服务器端录制,可控灵活。

                    3、全渠道客服:环信移动客服作为业内广泛使用的客户中心系统,囊括多项行业主流大奖,拥有多项国际PCT专利和国内专利,深受客户好评。环信移动客服产品成熟可靠,功能完善,全面覆盖了全渠道接入管理,客户服务与客户互动管理,运营与运维管理,工单系统,现场管理,智能报表,质检等客户中心功能。

                    4、客户声音:环信客户声音是基于人工智能和大数据挖掘的客户体验透析产品。对来自多个渠道的非结构化客服会话数据进行自然语言解析,主题聚类和情感度建模,挖掘和分析热点话题,发现服务运营问题,寻找畅销或问题产品,洞察销售机会。客户声音系统可以帮助企业识别和改善客户旅程的各个阶段。

                    5. 智能客服机器人:环信智能客服机器人不仅在常见的单轮对话能力上表现优异,预装多种行业知识库,还可以快速开发多轮对话,支持人机协作以便在复杂场景下对人工客服提供全面AI辅助支持。同时,环信智能客服机器人的自动学习能力极大的降低了机器人知识库的维护成本。

                    6、精准营销及自动化营销:大数据和AI驱动的营销功能,如自动化消息模板和自动化规则管理及A/B测试,营销计划管理,基于用户行为轨迹、用户画像和用户会话内容的自动化消息和访客CTA(Call To Action)等。 收起阅读 »

                    环信Android/ios V3.3.2 SDK 已发布,新增群组、聊天室群公告及群文件功能

                     Android V3.3.2 2017-05-18 增加群、聊天室公告相关API群组支持上传及下载共享文件群组支持设置扩展属性EMLocalSurfaceView 和 EMOppositeSurfaceView 合为同一个控件 EMCallSurfaceVi...
                    继续阅读 »

                    QQ图片20170522115322.png


                     Android V3.3.2 2017-05-18
                    1. 增加群、聊天室公告相关API
                    2. 群组支持上传及下载共享文件
                    3. 群组支持设置扩展属性
                    4. EMLocalSurfaceView 和 EMOppositeSurfaceView 合为同一个控件 EMCallSurfaceView
                    5. Demo及EaseUI改成纯Android Studio结构,不再支持Eclicpse导入
                    6. easeui没有包含SDK的jar和so, 使用需要自己拷贝libs下的库文件,或者执行copyLibs.sh完成拷贝。

                     
                     iOS V3.3.2 2017-05-18
                     
                    新功能:
                    1. 新增:修改获取群公告,上传下载删除群共享文件,修改群扩展信息接口(接口详情请查看文档群组管理
                    2. 新增:修改获取聊天室公告(接口详情请查看文档聊天室管理
                    3. 新增:批量设置群组免打扰接口


                    修复:
                    1. 修复有时调用getAllConversations时返回为空的bug
                    2. 修复获取已加入群组超时的bug

                     
                     
                    版本历史:AndroidSDK 更新日志  ios SDK更新日志
                    下载地址:SDK下载
                    收起阅读 »

                    环信Android消息回撤

                    环信现在的消息回撤开发文档没有更新, 所以得自己去写, 本人贡献点小东西.本项目用的SDK版本为3.3.1. 1. 首先在聊天消息里添加消息长按事件监听,里面添加撤回消息选项.     撤回点击之后处理为:  发送撤回消息!!!!!!cmdMsg = EMMe...
                    继续阅读 »
                    环信现在的消息回撤开发文档没有更新, 所以得自己去写, 本人贡献点小东西.本项目用的SDK版本为3.3.1.
                    1. 首先在聊天消息里添加消息长按事件监听,里面添加撤回消息选项. 
                       撤回点击之后处理为:  发送撤回消息!!!!!!
                    cmdMsg = EMMessage.createSendMessage(EMMessage.Type.CMD);
                    // 如果是群聊, 设置chatType, 默认是单聊
                    if(chatType == Constant.CHATTYPE_GROUP){
                    cmdMsg.setChatType(ChatType.GroupChat);
                    }
                    String action = "REVOKE_FLAG";
                    EMCmdMessageBody cmdBody=new EMCmdMessageBody(action);
                    // 设置消息body
                    cmdMsg.addBody(cmdBody);
                    // 设置要发给谁, 用户username 或者群聊 grouid
                    cmdMsg.setTo(toChatUsername);
                    // 通过扩展字段添加要撤回消息的iD
                    cmdMsg.setAttribute("msgId", msgid); // 长按的时候, 获取本信息的message的Id
                    // long aa = cmdMsg.getMsgTime(); // 获取这个消息的发送时间
                    // 获取当前系统的时间
                    long time = new Date().getTime();
                    long minite = (time - aa - 6000)/1000; // 1s = 1000
                    if(minite <= 120){
                    EMClient.getInstance().chatManager().sendMessage(cmdMsg);
                    cmdMsg.setMessageStatusCallback(new EMCallBack() {

                    @Override
                    public void onSuccess() {
                    conversation.removeMessage(msgid);
                    handler.sendEmptyMessage(1);
                    }

                    @Override
                    public void onProgress(int arg0, String arg1) {
                    }

                    @Override
                    public void onError(int arg0, String arg1) {
                    // TODO Auto-generated method stub
                    String a = "";
                    // conversation.removeMessage(msgid);
                    }
                    });

                    }else{
                    ToastUtils.ToastShortMessage(getActivity(), "发送时间超过2分钟的消息!不能被撤回!");
                    }
                    break;





                    此处handler.sendEmptyMessage(1);中的内容是:         
                    ToastUtils.ToastShortMessage(getActivity(), "消息已撤回!");
                    messageList.refresh();





                    2. 环信在获取CMD消息监听有三个地方: 分别为, EaseChatFragment, MainActivity, DemoHelper(此处为App后台运行时, 消息撤回的处理)
                    在EMMessageListener下的onCmdMessageReceived()中处理接受到的CMD消息, 首先贴上的为EaseChatFragment里面的: 
                    for(EMMessage emMessage : messages){
                    EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody)emMessage.getBody();
                    String action = cmdMessageBody.action();
                    if(action.equals("REVOKE_FLAG")){
                    try {
                    msgId = emMessage.getStringAttribute("msgId");
                    conversation1 = EMClient.getInstance().chatManager().getConversation(emMessage.getFrom());
                    if(emMessage.getChatType() == ChatType.GroupChat){
                    messageList.refreshSelectLast(); //刷新UI
                    }else{
                    handler.sendEmptyMessage(1);
                    }
                    } catch (HyphenateException e) {
                    e.printStackTrace();

                    }

                    }
                    }
                    此处handler.sendEmptyMessage(1)中的内容是:
                    // 删除表示撤销
                    conversation1.removeMessage(msgId);
                    messageList.refreshSelectLast();





                    3. MainActivity里面的处理方式:
                        for(EMMessage emMessage : messages){
                    EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody)emMessage.getBody();
                    String action = cmdMessageBody.action();
                    if(action.equals("REVOKE_FLAG")){
                    try {
                    msgId = emMessage.getStringAttribute("msgId");
                    conversation1 = EMClient.getInstance().chatManager().getConversation(emMessage.getFrom());
                    if(emMessage.getChatType() == ChatType.GroupChat){
                    refreshUIWithMessage(); // 刷新UI
                    }else{
                    handler.sendEmptyMessage(1);
                    }

                    } catch (HyphenateException e) {
                    e.printStackTrace();

                    }

                    }
                    }
                     
                    此时handler.sendEmperymessage(1)中: 
                    conversation1.removeMessage(msgId);
                    refreshUIWithMessage();
                    4. DemoHelper里面的处理方式:
                    for (final EMMessage message : messages) {
                    // 获取消息body
                    EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
                    final String action = cmdMsgBody.action();// 获取自定义action
                    // 发送一个透传消息
                    if(action.equals("REVOKE_FLAG")){
                    try {
                    if(message.getChatType() == ChatType.GroupChat){ // 群组处理方式
                    conversation1 = EMClient.getInstance().chatManager().getConversation(message.getTo(), EaseCommonUtils.getConversationType(2), true);
                    }else{
                    conversation1 = EMClient.getInstance().chatManager().getConversation(message.getFrom());
                    }
                    msgId = message.getStringAttribute("msgId");

                    handler.sendEmptyMessage(1);
                    } catch (HyphenateException e) {
                    e.printStackTrace();
                    }
                    }
                    }
                    此时handler.sendEmpertyMessage(1)中的方法是: 
                    conversation1.removeMessage(msgId);
                    至此,环信消息回调完成, 没有去做撤回回调处理,直接删除不好,如果想做的请自行处理.谢谢,本文纯属原创,如果有问题,可与我联系,QQ邮箱: 277667430@qq.com.本人姓氏: 侯 收起阅读 »

                    【客户世界·洞察者】智能客服机器人是下一代客服的核心驱动力(附Gartner报告全文)

                        上期我们谈完了工具层、知识层一个领先的SaaS客服厂商是如何做的,接着我们再来聊聊现在最火的AI。随着全媒体客服的普及和广泛应用导致企业和消费者多点接触,同时用户体验得到了企业的重视,导致客服咨询量暴增,企业有限的客服人力资源与日益增加的客服请求之间的...
                    继续阅读 »
                        上期我们谈完了工具层、知识层一个领先的SaaS客服厂商是如何做的,接着我们再来聊聊现在最火的AI。随着全媒体客服的普及和广泛应用导致企业和消费者多点接触,同时用户体验得到了企业的重视,导致客服咨询量暴增,企业有限的客服人力资源与日益增加的客服请求之间的矛盾日益尖锐,如何用有限的客服资源服务不断增长的海量客服请求需要一个颠覆型的技术来解决。相比人工客服,智能客服机器人将提供极大的效率优势。
                     
                       Gartner报告指出智能客服机器人(VCA-virtual customer assistance)的使用正处于临界点。大幅改进的自然语言处理技术,以聊天为中心的移动渠道与客户互动的应用,以及客户对机器人技术的接受程度,这些因素使得人们对VCA的兴趣越來越大。从被动的被人类编程出来的可以在结构化和非结构化内容库中找到问题答案的虚拟助手,到主动的有时候是机器学习得到的VCA的转变,其考察个人的特征并代表他们行动。虚拟助手正在经历从被动的被人类编程出来在结构化和非结构化内容库中找到问题答案到主动的通过机器学习能够理解用户个性化的需求并且随之采取灵活应对行为的转变。
                     
                       环信作为智能客服企业的先行者,基于自然语言处理和机器学习技术推出了环信智能客服机器人,辅助或代替人工客服精准回答常见或高频问题,降低企业客服人力成本。目前,环信在客服领域已经服务了58541家标杆客户,积累了人工智能在客户服务行业落地的大量最佳实践。
                     
                    视频观看地址:点击观看
                     
                    3.1,智能客服机器人在客服场景下的最佳实践:
                     
                    3.1.1,无需人工标记和人工维护的机器人单轮会话,极大降低客服机器人的维护成本。

                       一些问题是不依赖于对话历史,仅根据当前句子就能给出答案,难点在于机器能否理解同一语义的不同表达方法。环信智能机器人采用自然语言处理技术和深度学习技术建立对话模型,使用海量数据对模型进行训练,并借助客服系统中访客和客服的实时反馈来增强学习,精准识别用户意图,帮助人工客服回答各种问题。相比基于关键词匹配和人工定义规则大量标注数据的传统问答技术,环信智能机器人无需人工标记和人工维护相似问法,就可以在会话过程中识别同一问题的多种不同问法。

                    001.png


                    图1示例:环信机器人无需人工维护相似问法,就可以在会话过程中识别同一问题的多种不同问法。

                    3.1.2机器人多轮会话,支持更多复杂业务,进一步拓展机器人使用场景。

                       而另一些问题则由于缺少足够信息或者过于模糊,需要通过多轮对话的方式来明确用户的需求。比如用户想查物流,但是缺少订单号等信息,机器人需要引导用户提供这些数据。这和单轮的问答相比,多轮对话的技术难点更多,比如指代的理解,句子的省略,用户状态的维护等等。

                       环信智能机器人支持上下文语义和多轮会话,并预装多行业的领域知识,如电商行业的物流状态查询模型,产品保修会话模型等。这种基于行业领域和业务模型的多轮会话能力,相比单轮会话,进一步扩展了客服机器人对复杂客服业务场景的自动支撑能力。

                    002.png


                    图2示例:环信机器人通过多轮会话支持查询物流状态,并和企业业务系统做集成,真正意义上节省人工。

                    3.1.3无缝人机协作体验,复杂场景下最佳用户体验的客服模式。

                       在一些比较复杂和特殊的服务场景,比如高客单价的金融行业售前咨询,机器人客服不能完全理解客户的个性化咨询要求的时候,我们可以无缝进入人机混合模式。在人机混合模式下,环信智能客服机器人向人工客服推荐备选答案,人工客服起到了保证答案质量充当专家客服的角色,这样既保证了客服的响应速度又提高了问题的回答准确性,同时降低了人工客服的工作量。

                    3.1.4,智能质检,准确率达到替代人工质检水平。

                       环信机器人还提供自动智能质检功能,可以对全部客服会话进行实时或离线质检。 智能质检是基于环信在线客服各个领域的海量用户对话,提取出数百个客服对话特征,并用这些特征训练得到的一个通用质检模型。智能质检的准确率达到替代人工质检水平。

                    3.1.5. 支持智能自主学习,更高效的知识库

                       环信智能机器人可以快速高效搭建知识库。既支持批量导入FAQ或用户手动维护问答知识,也支持智能自主学习。智能自主学习是指客服机器人自主学习人工客服的会话,自动生成新知识规则。相比手动维护问答知识,智能自主学习能力显著降低了客服机器人的维护成本,提高了知识库的准确性和时效性。环信智能机器人可预装多个领域的行业知识库。

                    附录:Gartner研究——虚拟客户助手(智能客服机器人) 分析师:布莱恩·玛纳萨马

                    定义:虚拟客户助理(VCA),代表公司进行模拟对话以传递信息和/或代表客户采取行动并执行交易。 VCA由四部分组成:

                       ■接收请求和传递回应的用户界面■用于文本和语音的自然语言处理引擎■可以检索知识和内容数据存储库的搜索和知识引擎■用于分析意图的上下文引擎一些VCA还具有机器学习功能。

                       定位和市场接受速度:IBM Watson,Microsoft Cortana,Next-IT,Creative Virtual和其他VCA供应商的工作正在提高人们对作为实用工具的虚拟助理(VA)技术的认知。 VCA的使用正处于临界点。大幅改进的自然语言处理技术,以聊天为中心的移动渠道与客户互动的应用,以及客户对机器人技术的接受程度,这些因素使得人们对VCA的兴趣越來越大。从被动的被人类编程出来的可以在结构化和非结构化内容库中找到问题答案的虚拟助手,到主动的有时候是机器学习得到的VCA的转变,其考察个人的特征并代表他们行动。虚拟助手正在经历从被动的被人类编程出来在结构化和非结构化内容库中找到问题答案到主动的通过机器学习能够理解用户个性化的需求并且随之采取灵活应对行为的转变。VCA技术有望在两至五年内成为主流。随着移动优先的用户体验转向,许多VCA都亟待更新,以支持多渠道客户与统一知识库的互动,特别是支持客户手机。

                       用户建议:确定客户服务平台的当前状态和所需状态。今天您将使用什么样的客服方式和客服工具?您是否使用自然语言处理技术来确定客户到底咨询了什么?是将这个呼叫咨询分派给正确的客服接线员还是让客服机器人提供自动回复?VCA将成为支持多个客服渠道的起点。 VCA未来将可能改变你的日常生活;它可以是一个帮助你在移动设备上购买新健身设备的向导,也可以是一个帮助你开设银行帐户的虚拟客服人员。

                       市场正在发生一种变化,对虚拟客服助手的逐渐重视以及使用频次的减少,这个现象已不如以前那么明显。在数字渠道中提供拟人化体验的驱动力正在发生改变。随着客户逐渐适应和接受与计算机的互动,其对具有拟人化情感的3D图像的需求正在减少。公司部署虚拟客服助手时他们发现有对于提升品牌的附加价值,而非仅仅模拟一种店内体验。VCA不仅是面向客户的,而是越来越多地部署为面向员工——帮助客户服务中心减少人工坐席操作时间以及保障客户咨询回复的一致性。

                       将一组简单的串行项目与一个复杂的大型项目进行比较,以满足所有确定的需求。找到构成完整呼叫的最高频简单对话,以简单的方式实现自动化和提升客户满意度。然后,识别下一组完整的呼叫:在一段时间,技术与人工可进行合作处理这组呼叫,即当技术检测到问题(如技术储备的知识不足、客户声音难以辨别、或客户通过正确的操作明确要求由人类进行对话)时,人工操作员将接管此组呼叫。

                       业务影响:VCA是狭义的、具有特殊用途的VA,用于销售、客户服务和数字商务,且具有独特的目标。VCA的商业案例有三方面。其解决了以下需求:

                       ■满足客户对网络和移动渠道中客户支持的期望——更高的互动频率;全天候、即时的聊天可用性■将互动转向价格更低的客户自助服务渠道,更快获取解决方案;降低服务成本■提供积极的建议和参与,培养忠诚度和客户满意度。

                        VCA的有效使用便于组织衡量其——特别是联络中心的——参与数量。在数字亭或自动取款机上使用启用语音的VCA可降低对类型化干预的需求,且有助于为非传统受众提供有趣的互动。

                    好处评级:高

                    市场渗透率:目标受众的5%至20%

                    成熟度:未成熟
                     
                    点击查看Gartner报告全文 收起阅读 »

                    拍照闪退

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

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

                    我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。     大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务...
                    继续阅读 »
                    我只是一个普通人,做人要谦虚。
                    我不是大神,我也不是很厉害的。
                    天外有天,人外有人。
                    老师引入门,修行靠个人。
                    希望能帮助大家,谢谢。

                        大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的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 收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »
                    接上篇

                    【视频教程+源码】基于环信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.微信-在其它设备登录


                    收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
                    继续阅读 »

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

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

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
                    继续阅读 »

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
                    继续阅读 »
                    接上篇
                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)
                     
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
                     
                    04.环信简介(了解)
                     





                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                     
                    打赏二维码请看第一篇。

                      收起阅读 »

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

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)   我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任...
                    继续阅读 »
                    接上篇
                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
                     
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
                     
                    03.XMPP实现即时通信的准备工作(了解)​



                     
                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                     
                    打赏二维码请看第一篇、,如果你有心的话。 收起阅读 »

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

                      接上文【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。我是郭永峰,本套课程基于环信IM教大家如...
                    继续阅读 »
                     
                    接上文【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    我只是一个普通人,做人要谦虚。
                    我不是大神,我也不是很厉害的。
                    天外有天,人外有人。
                    老师引入门,修行靠个人。
                    希望能帮助大家,谢谢。
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。

                    02.XMPP简介(了解)​
                     



                     
                     
                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                     
                    打赏二维码在上一篇文章,如果你有心的话。 收起阅读 »

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

                     我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。大家好,我是郭永峰,从11年开始玩openfire服务器,再到后面的XMPP,socket,一路走来,可以说即时通入门到放弃...
                    继续阅读 »
                     
                    我只是一个普通人,做人要谦虚。
                    我不是大神,我也不是很厉害的。
                    天外有天,人外有人。
                    老师引入门,修行靠个人。
                    希望能帮助大家,谢谢。
                    大家好,我是郭永峰,从11年开始玩openfire服务器,再到后面的XMPP,socket,一路走来,可以说即时通入门到放弃,最终选择了环信这样的即时通讯提供商。
                     
                    郭永峰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. 同时能希望大家在面试时,在即时通讯这块不在陌生

                     
                    我们先来看第一个视频--01.即时通讯简介(了解) 







                     

                    微信打赏

                    QQ图片20170515172743.png


                    支付宝打赏

                    QQ图片20170515172833.png


                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信


                    QQ图片20170515172529.png


                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                      收起阅读 »

                    环信移动客服v5.18已发布,增加机器人新手引导及机器人知识库测试功能

                    客服模式 支持根据会话类型筛选历史会话 历史会话页面显示会话类型(呼入、回呼),并支持根据会话类型进行筛选。 呼入:客户主动发起的会话;回呼:客服手动回呼的会话。 管理员模式机器人新手引导新增机器人新手引导功能。引导管理员在创建机器人时,对机器人的基...
                    继续阅读 »
                    客服模式

                    支持根据会话类型筛选历史会话


                    历史会话页面显示会话类型(呼入、回呼),并支持根据会话类型进行筛选。
                    • 呼入:客户主动发起的会话;
                    • 回呼:客服手动回呼的会话。

                    001.png

                    管理员模式机器人新手引导新增机器人新手引导功能。引导管理员在创建机器人时,对机器人的基础信息、自动回复、知识规则、自定义菜单进行设置,并通过知识库测试对知识规则和自定义菜单进行优化。该新手引导用于帮助客户更好地使用机器人功能,对初次使用移动客服系统的客户和开通多机器人功能的客户均有效。 

                    002.png

                    机器人知识库测试新增机器人知识库测试功能。管理员可以随时与机器人对话,测试机器人的回复,快速优化知识规则和自定义菜单。知识库测试方法:1. 进入“管理员模式 > 智能机器人 > 机器人设置 > 知识规则”tab页签。 

                    003png.png

                    2. 点击右上角的“知识库测试”按钮,开始与机器人对话。对话窗口右侧显示机器人回复所匹配的知识规则或菜单,以及匹配率。 

                    004.png

                    3. 点击匹配到的规则,可以进入知识规则页面,对该条规则进行优化。当未匹配到知识规则时,可以快速添加知识规则。 

                    005.png

                    支持为待接入超时提示语添加留言按钮新增“为待接入超时提示语添加留言按钮”开关。当客户处于排队状态,无法及时得到接待时,允许客户发起留言(同时结束会话)。进入“管理员模式 > 设置 > 系统开关”页面,依次打开“待接入超时提醒”和“为待接入超时提示语添加留言按钮”开关,并设置超时提示语等。 

                    006.png

                    客户在网页聊天窗口和H5网页收到的超时提示语如下图所示。点击“留言”按钮后,即可进入留言页面进行留言。同时,会话自动结束。 

                    007.png

                    权限管理支持设置数据权限在权限管理页面,可以为管理员、自定义角色设置管理员模式下页面的数据权限,分为租户和技能组。
                    • 赋予角色某个页面“租户”级别的数据权限时,该角色对应的管理员可以查看并操作该页面的所有数据,包括所有技能组的数据。
                    • 赋予角色某个页面“技能组”级别的数据权限时,该角色对应的管理员只可以查看并操作该页面中自己所属技能组的数据。
                    管理员模式的以下页面支持设置数据权限:客户中心、历史会话、当前会话。 

                    008.png

                    Web插件(访客端)当前版本:v47.9在消息中显示留言按钮网页访客端支持在待接入超时提示语中显示“留言”按钮,方便等待中的客户主动发起留言(点击留言按钮后,会话自动结束)。前提:管理员在移动客服系统中“管理员模式 > 设置 > 系统开关”页面,打开“为待接入超时提示语添加留言按钮”开关。支持全屏显示图片网页访客端支持全屏显示图片。客户与客服聊天时,如果收到图片消息:
                    •  
                    • 桌面聊天窗口:点击图片,可将图片放大至浏览器全屏查看;
                    • H5网页:点击图片,可将图片放大至手机全屏查看。


                    第二通道支持图片

                    网页访客端内置第二通道功能,当IM消息通道(第一通道)出现短暂的消息发送失败的情况时,自动调用第二通道将客户消息发送至移动客服系统,确保客户的所有消息均能准时送达。

                    在之前的版本,web插件的第二通道仅支持发送文本消息;从该版本开始,第二通道支持发送文本、图片消息。

                    显示客服输入状态

                    网页访客端支持显示客服输入状态。客户通过桌面聊天窗口或H5网页与客服聊天时,可以查看客服的输入状态。

                    “显示客服输入状态”为增值服务,如需开通,请提供租户ID并联系环信商务经理。 

                    009.png


                    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.18 

                    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

                    环信聊天游客身份和正常用户身份的切换

                         最近搞环信聊天,需求是游客身份也可以进行聊天,当用户注册了我们的APP后也需要把游客身份切换过来进行聊天,首先我们的环信注册,登录全都放前段处理了,下面就按照我们的需求逻辑来如何切换游客。      1.APP用户的注册,也就注册环信,APP的登录返...
                    继续阅读 »
                     
                       最近搞环信聊天,需求是游客身份也可以进行聊天,当用户注册了我们的APP后也需要把游客身份切换过来进行聊天,首先我们的环信注册,登录全都放前段处理了,下面就按照我们的需求逻辑来如何切换游客。
                     
                       1.APP用户的注册,也就注册环信,APP的登录返回的有用户ID,这个时候并没有让他登录环信,只是保存了返回的ID,下面就是用ID来判断该用户是否注册过环信的依据
                     
                    大致说明一下,代码中用到一个类来保证uuid不会改变的状态,为防止app卸载后uuid的改变,我们把他存储到钥匙串里面来保存

                    下面用图来表示
                    我们先来看下整个身份切换实现的逻辑图

                    4861502-fa2f7d87d00c78d7.jpg



                    下面就上代码了,第一步从图中第一步来说判断userID是否存在

                    这个地方是在点击聊天按钮开始判断的
                     
                    -(void)releaseInfo:(UIButton*)sender{
                    NSString*Hxusername=[userdic objectForKey:@"useid"];//获取保存的userID
                    NSString*phonestr=  [[NSUserDefaults standardUserDefaults]objectForKey:@"phonenum"];
                    NSString*chatid=[[phonestr md5String]substringFromIndex:16];//这个是获取客服的欢信ID
                    //单例里面处理用户是否登录,以及游客随机分配uuid来注册环信IM号
                    DataManager*datamage= [DataManager shareDataManager];
                    //判断用户ID是否存在,也就证明是否注册过环信
                    if (Hxusername.length>0) {
                    if ([datamage loginKefuSDK])//判断用户是否登录
                    {//单聊
                    ChattingViewController *chatController = [[ChattingViewController alloc] initWithConversationChatter:chatid conversationType:EMConversationTypeChat];[self.navigationController pushViewController:chatController animated:YES];
                    }
                    }else{
                    //游客身份的判断
                    if ([datamage customelogin]) {
                    //单聊
                    ChattingViewController *chatController = [[ChattingViewController alloc] initWithConversationChatter:chatid conversationType:EMConversationTypeChat];[self.navigationController pushViewController:chatController animated:YES];
                    }
                    }
                    }上面这是按钮方法里面的数据下面来说,DataManager*datamage= [DataManager shareDataManager];这个单利的方法
                    DataManager.h
                    @interface DataManager : NSObject
                    -(BOOL)customelogin;//判断游客之前是否有登录
                    -(void)requestchattphone;//获取美容院客服聊天的对象电话
                    @end
                    DataManager.m


                    @implementation DataManager
                    +(instancetype)shareDataManager{
                    static DataManager *manager;
                    static dispatch_once_t onceToken;
                    dispatch_once(&onceToken, ^{
                    manager = [[DataManager alloc] init];
                    });
                    return manager;
                    }
                    //userID存在的时候 登录IM
                    - (BOOL)loginKefuSDK {
                    NSDictionary*userdic=[[NSUserDefaults standardUserDefaults]objectForKey:@"userMessage"];//接受用户是否登录
                    NSString*loguser=[NSString stringWithFormat:@"%@",[userdic objectForKey:@"useid"] ];
                    EMClient *client = [EMClient sharedClient];
                    //用户已经登录
                    if (client.isLoggedIn) {
                    if ([loguser isEqualToString:client.currentUsername])//当前登录用户的ID和即将要登录人的ID是否一样
                    {
                    return YES;
                    }else
                    {
                    EMError *error = [[EMClient sharedClient] logout:YES];
                    if (!error) {
                    NSLog(@"退出成功");
                    }
                    }
                    }//这里APP用户登录环信的密码统统是123456
                    EMError *error = [[EMClient sharedClient] loginWithUsername:loguser password:@"123456"];
                    if (!error) { //IM登录成功
                    return YES;
                    } else { //登录失败
                    NSLog(@"登录失败 error code :%d,error description:%@",error.code,error.errorDescription);
                    return NO;
                    }
                    return NO;
                    }
                    //游客身份的登录方法
                    -(BOOL)customelogin
                    {
                    EMClient *client = [EMClient sharedClient];
                    //用户已经登录
                    if (client.isLoggedIn) {
                    return YES;
                    }//该用户没有注册,来用改设备UUID来给用户注册环信,并登录环信
                    if (![self registerIMuser]) {
                    return NO;
                    }
                    EMError *error = [[EMClient sharedClient] loginWithUsername:self.Hxusername password:@"123456"];
                    if (!error) { //IM登录成功
                    return YES;
                    } else { //登录失败
                    NSLog(@"登录失败 error code :%d,error description:%@",error.code,error.errorDescription);
                    return NO;
                    }
                    return NO;
                    }
                    - (BOOL)registerIMuser { //举个栗子。注册建议在服务端创建环信id与自己app的账号一一对应,\
                    而不要放到APP中,可以在登录自己APP时从返回的结果中获取环信账号再登录环信服务器
                    EMError *error = nil;
                    NSString *newUser = [self getrandomUsername];
                    self.Hxusername = newUser;
                    error = [[EMClient sharedClient] registerWithUsername:newUser password:@"123456"];
                    if (error &&  error.code != EMErrorUserAlreadyExist) {
                    NSLog(@"注册失败;error code:%d,error description :%@",error.code,error.errorDescription);
                    return NO;
                    }return YES;
                    }
                    //创建一个随机的用户名,这里是设备UUID来代替的​
                    - (NSString *)getrandomUsername {
                    //第一种方法:
                    /*NSString *username = nil;
                    UIDevice *device = [UIDevice currentDevice];//创建设备对象
                    NSString *deviceUID = [[NSString alloc] initWithString:[[device identifierForVendor] UUIDString]];
                    if ([deviceUID length] == 0) {
                    CFUUIDRef uuid = CFUUIDCreate(NULL);
                    if (uuid)
                    {
                    deviceUID = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid);
                    CFRelease(uuid);
                    }
                    username = [deviceUID stringByReplacingOccurrencesOfString:@"-" withString:@""];
                    username = [username stringByAppendingString:[NSString stringWithFormat:@"%u",arc4random()0000]];
                    return username;*/
                    //第二种方法
                    //加上build ID是为了保证设备的唯一性,如果这里的buildID换了,设备的uuid也会变,这里的解决办法也就是放倒了钥匙串里面,不会因卸载程序,程序升级设备的标识会改变
                    NSString *SERVICE_NAME = NAVI_TEST_BUNDLE_ID;//最好用程序的bundle id
                    NSString * str =  [SFHFKeychainUtils getPasswordForUsername:@"UUID" andServiceName:SERVICE_NAME error:nil];  // 从keychain获取数据
                    if ([str length]<=0)
                    {
                    str  = [[[UIDevice currentDevice] identifierForVendor] UUIDString];  // 保存UUID作为手机唯一标识符[SFHFKeychainUtils storeUsername:@"UUID"   andPassword:str    forServiceName:SERVICE_NAME updateExisting:1  error:nil];  // 往keychain添加数据
                    }
                    str = [str stringByReplacingOccurrencesOfString:@"-" withString:@""];
                    return str;
                    }在这里用到了一个类来处理的UUID不变(APP卸载后不会改变)
                    SFHFKeychainUtils.h
                    #import@interface SFHFKeychainUtils : NSObject
                    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
                    + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
                    + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
                    @end
                    SFHFKeychainUtils.m​
                    #import "SFHFKeychainUtils.h"
                    static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
                    #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
                    @interface SFHFKeychainUtils (PrivateMethods)
                    + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
                    @end
                    #endif
                    @implementation SFHFKeychainUtils
                    #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
                    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { if (!username || !serviceName) { *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; return nil; }      SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];      if (*error || !item) { return nil; }
                    // from Advanced Mac OS X Programming, ch. 16
                    UInt32 length;
                    char *password;
                    SecKeychainAttribute attributes[8];
                    SecKeychainAttributeList list;
                    attributes[0].tag = kSecAccountItemAttr;
                    attributes[1].tag = kSecDescriptionItemAttr;
                    attributes[2].tag = kSecLabelItemAttr;
                    attributes[3].tag = kSecModDateItemAttr;
                    list.count = 4;
                    list.attr = attributes;
                    OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
                    if (status != noErr)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    return nil;
                    }
                    NSString *passwordString = nil;
                    if (password != NULL)
                    { char passwordBuffer[1024];
                    if (length > 1023) {length = 1023;
                    }
                    strncpy(passwordBuffer, password, length);
                    passwordBuffer[length] = '\0';
                    passwordString = [NSString stringWithCString:passwordBuffer];
                    }SecKeychainItemFreeContent(&list, password);CFRelease(item);return passwordString;
                    }
                    + (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
                    if (!username || !password || !serviceName) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    return;
                    }
                    OSStatus status = noErr;
                    SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
                    if (*error && [*error code] != noErr) {
                    return;
                    }
                    *error = nil;
                    if (item) {
                    status = SecKeychainItemModifyAttributesAndData(item,
                    NULL,
                    strlen([password UTF8String]),
                    [password UTF8String]);
                    CFRelease(item);
                    }
                    else {
                    status = SecKeychainAddGenericPassword(NULL,
                    strlen([serviceName UTF8String]),
                    [serviceName UTF8String],
                    strlen([username UTF8String]),
                    [username UTF8String],
                    strlen([password UTF8String]),
                    [password UTF8String],
                    NULL);
                    }
                    if (status != noErr) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    }
                    + (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
                    if (!username || !serviceName) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];
                    return;
                    }
                    *error = nil;
                    SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
                    if (*error && [*error code] != noErr) {
                    return;
                    }
                    OSStatus status;
                    if (item) {
                    status = SecKeychainItemDelete(item);
                    CFRelease(item);
                    }
                    if (status != noErr) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    }
                    + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
                    if (!username || !serviceName) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    return nil;
                    }
                    *error = nil;
                    SecKeychainItemRef item;
                    OSStatus status = SecKeychainFindGenericPassword(NULL,
                    strlen([serviceName UTF8String]),
                    [serviceName UTF8String],
                    strlen([username UTF8String]),
                    [username UTF8String],
                    NULL,
                    NULL,
                    &item);
                    if (status != noErr) {
                    if (status != errSecItemNotFound) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    return nil;
                    }
                    return item;
                    }
                    #else
                    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
                    if (!username || !serviceName) {
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    }
                    return nil;
                    }
                    if (error != nil) {
                    *error = nil;
                    }
                    // Set up a query dictionary with the base query attributes: item type (generic), username, and service
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, nil];
                    NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys];
                    // First do a query for attributes, in case we already have a Keychain item with no password data set.
                    // One likely way such an incorrect item could have come about is due to the previous (incorrect)
                    // version of this code (which set the password as a generic attribute instead of password data).
                    NSMutableDictionary *attributeQuery = [query mutableCopy];
                    [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnAttributes];
                    CFTypeRef attrResult = NULL;
                    OSStatus status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) attributeQuery, &attrResult);
                    //NSDictionary *attributeResult = (__bridge_transfer NSDictionary *)attrResult;
                    if (status != noErr) {
                    // No existing item found--simply return nil for the password
                    if (error != nil && status != errSecItemNotFound) {
                    //Only return an error if a real exception happened--not simply for "not found."
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    return nil;
                    }
                    // We have an existing item, now query for the password data associated with it.
                    NSMutableDictionary *passwordQuery = [query mutableCopy];
                    [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge_transfer id) kSecReturnData];
                    CFTypeRef resData = NULL;
                    status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) passwordQuery, (CFTypeRef *) &resData);
                    NSData *resultData = (__bridge_transfer NSData *)resData;
                    if (status != noErr) {
                    if (status == errSecItemNotFound) {
                    // We found attributes for the item previously, but no password now, so return a special error.
                    // Users of this API will probably want to detect this error and prompt the user to
                    // re-enter their credentials.  When you attempt to store the re-entered credentials
                    // using storeUsername:andPassword:forServiceName:updateExisting:error
                    // the old, incorrect entry will be deleted and a new one with a properly encrypted
                    // password will be added.
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
                    }
                    }
                    else {
                    // Something else went wrong. Simply return the normal Keychain API error code.
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    }
                    return nil;
                    }
                    NSString *password = nil;
                    if (resultData) {
                    password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
                    }
                    else {
                    // There is an existing item, but we weren't able to get password data for it for some reason,
                    // Possibly as a result of an item being incorrectly entered by the previous code.
                    // Set the -1999 error so the code above us can prompt the user again.
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
                    }
                    }
                    return password;
                    }
                    + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error
                    {
                    if (!username || !password || !serviceName)
                    {
                    if (error != nil)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    }
                    return NO;
                    }
                    // See if we already have a password entered for these credentials.
                    NSError *getError = nil;
                    NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError];
                    if ([getError code] == -1999)
                    {
                    // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
                    // Delete the existing item before moving on entering a correct one.
                    getError = nil;
                    [self deleteItemForUsername: username andServiceName: serviceName error: &getError];
                    if ([getError code] != noErr)
                    {
                    if (error != nil)
                    {
                    *error = getError;
                    }
                    return NO;
                    }
                    }
                    else if ([getError code] != noErr)
                    {
                    if (error != nil)
                    {
                    *error = getError;
                    }
                    return NO;
                    }
                    if (error != nil)
                    {
                    *error = nil;
                    }
                    OSStatus status = noErr;
                    if (existingPassword)
                    {
                    // We have an existing, properly entered item with a password.
                    // Update the existing item.
                    if (![existingPassword isEqualToString:password] && updateExisting)
                    {
                    //Only update if we're allowed to update existing.  If not, simply do nothing.
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,
                    kSecAttrService,
                    kSecAttrLabel,
                    kSecAttrAccount,
                    nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,
                    serviceName,
                    serviceName,
                    username,
                    nil];
                    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
                    status = SecItemUpdate((__bridge_retained CFDictionaryRef) query, (__bridge_retained CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge_transfer NSString *) kSecValueData]);
                    }
                    }
                    else
                    {
                    // No existing entry (or an existing, improperly entered, and therefore now
                    // deleted, entry).  Create a new entry.
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,
                    kSecAttrService,
                    kSecAttrLabel,
                    kSecAttrAccount,
                    kSecValueData,
                    nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,
                    serviceName,
                    serviceName,
                    username,
                    [password dataUsingEncoding: NSUTF8StringEncoding],
                    nil];
                    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
                    status = SecItemAdd((__bridge_retained CFDictionaryRef) query, NULL);
                    }
                    if (error != nil && status != noErr)
                    {
                    // Something went wrong with adding the new item. Return the Keychain error code.
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    return NO;
                    }
                    return YES;
                    }
                    + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error
                    {
                    if (!username || !serviceName)
                    {
                    if (error != nil)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    }
                    return NO;
                    }
                    if (error != nil)
                    {
                    *error = nil;
                    }
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil];
                    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
                    OSStatus status = SecItemDelete((__bridge_retained CFDictionaryRef) query);
                    if (error != nil && status != noErr)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    return NO;
                    }
                    return YES;
                    }
                    #endif
                    @end
                    收起阅读 »

                    【环信征文】在android中5分钟实现环信昵称头像的显示

                    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~ 一、将简版demo里的cache包(5个java文件)复制到自己项目里。 下载环信android简版Demo: 环信Android简版DEMO 昵称头像用到的工具类、mode...
                    继续阅读 »
                    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~


                    23.jpg



                    一、将简版demo里的cache包(5个java文件)复制到自己项目里。
                    下载环信android简版Demo:
                    环信Android简版DEMO

                    昵称头像用到的工具类、model都在这个cache包里。 

                    1.png


                    类介绍:

                    2.png



                    二、显示昵称头像有两种解决方案:
                    1. 从APP服务器获取;
                    2. 从消息扩展属性中获取。


                    接下来,我们就以官方demo为例,一步步教大家实现昵称头像的显示。
                    无论哪种方案,先要设置3步:
                    1.更改环信appkey;
                    在AndroidManifest.xml中更改appkey,改为【xmxx#xmxx】,如下所示。 

                    android:name="EASEMOB_APPKEY"
                    android:value=“xmxx#xmxx" />

                    2.增加第三方依赖库。根目录下的 build.gradle 下:
                    compile 'com.j256.ormlite:ormlite-android:5.0'
                    compile 'com.google.code.gson:gson:2.8.0'
                    ormlite:操作sqlite数据库
                    gson:json对象转换 

                    3.设置用户信息提供者:
                    在DemoHelper.java的getUserInfo函数里(第824行)增加如下代码: 


                    环信头像昵称显示使用的是提供者模式(EaseUserProfileProvider),只要设置了用户信息提供者(setUserProfileProvider),EaseUI界面里显示用户昵称和头像时,就会调用这个getUserInfo函数。


                    // 从本地缓存中获取用户昵称头像
                    EaseUser user = UserCacheManager.getEaseUser(username);

                    并且注释之前获取昵称头像的方法:
                    // EaseUser user = null;
                    // if(username.equals(EMClient.getInstance().getCurrentUser()))
                    // return getUserProfileManager().getCurrentUserInfo();
                    // user = getContactList().get(username);
                    // if(user == null && getRobotList() != null){
                    // user = getRobotList().get(username);
                    // }

                    然后,根据不同的方案,开发者可以选择不同步骤:

                    三、从开发者自己的APP服务器获取的步骤:
                    将UserCacheManager.java中第54行-62行代码,换成:通过okhttp(或者retrofit、volley)调用api接口,根据用户环信ID,从开发app服务器获取用户昵称头像。下面两张图是改之前和改之后的效果:
                     
                    改之前(用第三方云存储):

                    1111.png



                    改之后(用开发者自己服务器):

                    2222.png


                     

                    四、从第三方后端云存储获取的步骤:

                    1.配置后端云服务器;
                    (注意:用开发者APP服务器存储昵称头像可以忽略这一步):但是必须直接实现调用API从开发者服务器中获取昵称头像。
                    首先打开根目录下的 build.gradle 进行如下标准配置
                    buildscript {    
                    repositories {
                    jcenter() //这里是 LeanCloud 的包仓库
                    maven {
                    url "http://mvn.leancloud.cn/nexus/ ... ot%3B
                    }
                    }

                    dependencies {
                    classpath 'com.android.tools.build:gradle:1.0.0'
                    }
                    }

                    allprojects {
                    repositories {
                    jcenter() //这里是 LeanCloud 的包仓库
                    maven { url "http://mvn.leancloud.cn/nexus/ ... ot%3B }
                    }
                    }
                    继续在根目录下的build.gradle增加如下配置:
                    android {    
                    //为了解决部分第三方库重复打包了META-INF的问题
                    packagingOptions{
                    exclude 'META-INF/LICENSE.txt'
                    exclude 'META-INF/NOTICE.txt'
                    }
                    lintOptions {
                    abortOnError false
                    }
                    }
                    dependencies {
                    // LeanCloud 基础包
                    compile ('cn.leancloud.android:avoscloud-sdk:v3.+'
                    )
                    }
                    注意:以上代码,由于论坛里的编辑器会产生乱码,所以建议去【环信Android简版Demo】里复制。或者直接打开:
                    https://github.com/mengmakies/ChatDemo-UI3.00-Simple-Android/blob/master/examples/ChatDemoUI3.0/build.gradle​ 


                     3.在DemoApplication.java第49行初始化后端云。(注意:用开发者APP服务器存储昵称头像可以忽略这一步)
                    UserWebManager.config(this,"VBYeQuiVPD9CWS2aINWrwBv0-gzGzoHsz","1LItewi0x6hlkgYHxi8DERNN");
                     
                    4.在环信IM登录成功后,在后端云里新增用户。
                    参考LoginActivity.java第173行代码。(注意:用开发者APP服务器存储昵称头像可以忽略这一步) 
                    // 登录成功后,如果后端云没有缓存用户信息,则新增一个用户
                    UserWebManager.createUser(userId, nickName, avatarUrl);

                    如果需要看到完整的效果,可以用以下随机用户昵称头像的生成代码:
                    Random random=new Random();
                    String userId = EMClient.getInstance().getCurrentUser();// 用户环信ID
                    String nickName = String.format("小草%d",random.nextInt(10000));// 用户昵称
                    String avatarUrl = String.format("http://duoroux.com/chat/avatar/%d.jpg",random.nextInt(10));// 用户头像(绝对路

                    编译运行。这样就可以用方案一从APP服务端获取昵称头像了。

                    五、从消息扩展属性中获取昵称头像。
                    1.首先,要注释从APP服务器获取昵称头像的方法。
                    删除类文件:UserWebInfo.java和UserWebManager.java。
                    UserCacheManager.java中注释第57-68行,不从APP服务器中获取昵称头像: 
                    // 如果本地缓存不存在或者过期,则从存储服务器获取
                    if (notExistedOrExpired(userId)){
                    UserWebManager.getUserInfoAync(userId, new UserWebManager.UserCallback() {
                    @Override
                    public void onCompleted(UserWebInfo info) {
                    if(info == null) return;

                    // 缓存到本地
                    save(userId, info.getNickName(),info.getAvatarUrl());
                    }
                    });
                    }
                    2.登录(或注册)成功后,需要缓存当前用户的昵称头像。
                    在登录(或注册)服务端回调(不是环信IM登录回调)里,增加如下代码: 
                    // 登录成功,将用户的环信ID、昵称和头像缓存在本地
                    UserCacheManager.save(userId, nickName, avatarUrl);

                    如果需要看到完整的效果,可以用以下随机用户昵称头像的生成代码:
                    Random random=new Random();
                    String userId = EMClient.getInstance().getCurrentUser();// 用户环信ID
                    String nickName = String.format("小草%d",random.nextInt(10000));// 用户昵称
                    String avatarUrl = String.format("http://duoroux.com/chat/avatar/%d.jpg",random.nextInt(10));// 用户头像(绝对路径)
                    3.发送消息时携带昵称头像。
                    ChatFragment.java里的 onSetMessageAttributes函数。第231行增加代码:
                    // 设置消息的扩展属性,携带昵称头像
                    UserCacheManager.setMsgExt(message);

                    4.接收消息时携带昵称头像。
                    DemoHelper.java里的 onMessageReceived函数。第856行增加代码:
                    // 从消息的扩展属性里获取昵称头像
                    UserCacheManager.save(message.ext());


                    5.另外。音视频通话里,昵称头像也要进行处理。(不需要音视频通话功能的开发者可以省略后面所有步骤)
                    发送音视频通话请求时携带昵称头像。
                    CallActivity.java里第162行代码更改为: 
                    // 通过扩展属性将昵称头像传给对方
                    String ext = UserCacheManager.getMyInfoStr();
                    if (msg.what == MSG_CALL_MAKE_VIDEO) {
                    EMClient.getInstance().callManager().makeVideoCall(username,ext);
                    } else {
                    EMClient.getInstance().callManager().makeVoiceCall(username,ext);
                    }

                    6.接收音视频通话时保存昵称头像。
                    CallReceiver.java第33行增加代码: 
                    // 缓存用户昵称头像
                    String ext = EMClient.getInstance().callManager().getCurrentCallSession().getExt();
                    UserCacheManager.save(ext);

                    7.音频通话里显示昵称头像。
                    VoiceCallActivity.java第114行代码改为: 
                    // 显示昵称头像
                    UserCacheInfo user = UserCacheManager.get(username);
                    if (user != null){
                    nickTextView.setText(user.getNickName());
                    //Glide.with(VoiceCallActivity.this).load(user.getAvatarUrl()).placeholder(R.drawable.em_default_avatar).into(avatarImage);
                    }else {
                    nickTextView.setText(username);
                    }


                    8.视频通话里显示昵称头像。
                    VideoCallActivity.java第171行代码改为: 
                    // 显示昵称头像
                    UserCacheInfo user = UserCacheManager.get(username);
                    if (user != null){
                    nickTextView.setText(user.getNickName());
                    }else {
                    nickTextView.setText(username);
                    }

                    编译运行即可使用扩展消息的方式实现昵称头像显示了!
                    可以通过这个web页面发送消息测试是否设置成功:
                    http://duoroux.com/chat 

                    测试账号:
                    1.环信ID:biaoge  密码:biaoge
                    2.环信ID:biaomei  密码:biaomei

                    两种方案的完整代码,大家可以下载环信android简版Demo:
                    https://github.com/mengmakies/ChatDemo-UI3.00-Simple-Android 

                    IOS版简版demo下载地址:
                    https://github.com/mengmakies/ChatDemo-UI3.00-Simple 


                    如有任何问题,请咨询【环信IM互帮互助群】,群号:340452063 


                    注意事项


                    将此方案集成到自己项目时,要把SqliteHelper构造函数里的“DemoApplication.getInstance()”换成自己项目的应用程序类实例!




                      收起阅读 »

                    接私活发现甲方比自己还穷是什么样的体验

                    最近关于接私活的文章不少,有赞成私活的,也有反对私活的,无论怎样,接私活成了最近最火的话题。我来给大家分享一个新鲜出炉的接私活故事: 故事开始之前,我先教大家一个获取黄网的办法:和边检缉毒专查面黄肌瘦,神情萎靡,眼神游离的人一样,你观察你身边的朋友谁穷得买...
                    继续阅读 »
                    最近关于接私活的文章不少,有赞成私活的,也有反对私活的,无论怎样,接私活成了最近最火的话题。我来给大家分享一个新鲜出炉的接私活故事:


                    故事开始之前,我先教大家一个获取黄网的办法:和边检缉毒专查面黄肌瘦,神情萎靡,眼神游离的人一样,你观察你身边的朋友谁穷得买不起毒品却有和瘾君子一样的肾虚症状,那么他一定有资源。

                    昨天我从我的diao丝同事那里要来一个打擦边球的公众号,有图有文没视频那种。忽然在一篇小黄图文下面看见这么一行字:“如有能力制作开发本平台APP的个人或公司,请联系QQ:***,具体事宜详谈!”

                    我:(真没想到有机会接黄站的私活,赶紧加Q)需要Android外包?

                    他:(没错,尽管资料填“女”,但我仍能看出他是抠脚大汉)我就要简单的社区 + 相册,相册是自己的作品集

                    我:资料发我,我估个价,再找iOS同事给你做个iOS的

                    他:APP目前的功能就是自己的作品集

                    他:然后社区

                    我:(根据公众号给的链接打开了网站的手机版,然后开电脑看了电脑版,是一个基于discuzz的擦边球社区)看了网站手机版感觉做成APP难度不大

                    他:大概要多少钱?

                    我:(发了一份报价单)Android估价2000左右,最高不超过3000,iOS的价格也差不多

                    他:好贵呀

                    我:你知道一个程序员每天多少钱工资吗?

                    他:我每个月没钱吃饭啊

                    他:(过了一会)相册和怎么做成那种看12章以后要会员 才能看?或这收费

                    我:(连这个都不会弄,还能给终端提供API)我和你家技术谈谈吧

                    他:(开启连珠炮模式)米技术啊

                    他:就我和我姐妹弄的

                    他:哪有技术

                    他:全部自己学的

                    他:网站都是自己装的

                    我:我业余搞个外包容易吗?咋遇到的发包方个个比我还穷?

                    他:那肿么办?

                    他:袜子都买不起了

                    我:(他缺的不是Android和iOS上的两个软件,而是一整套技术体系)我有个免费的方案,用HBuilder打包你的手机站,这样Android和iOS的APP就都有了,零成本

                    他:作为粉丝的你不支持下我啊

                    真没想到找私活的甲方比程序员还穷,让我免费做个APP真是讹上我了,瞬间觉得高中时班主任语文老师的顺口溜“上辈子杀猪,这辈子教书;上辈子杀人,这辈子教语文”有了下联:“上辈子叛党,这辈子搞互联网;上辈子卖国,这辈子搞安卓”

                    第二天,那个公众号推送的小黄图文下面有这么一行字:“有木有洞APP开发架设的粉丝,如果有请自告奋勇联系小编,当然惊喜肯定给足” 收起阅读 »

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

                     ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫   在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙...
                    继续阅读 »
                     
                    ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

                      在刚刚过去的五四青年节,环信公开课第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.0SDK集成小米推送教程】实现离线消息推送和后台视频电话通知

                    教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall】 前言   从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频...
                    继续阅读 »


                    教程所用到的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文档
                    环信推送相关文档收起阅读 »